前言
通过ROP
绕过DEP
和ASLR
防护,我们要事先得到目标机器上的libc.so
或者具体的linux版本号
才能计算出相应的offset
。在没有目标系统libc
文件的情况下,应该如何做呢?这时候就需要通过memory leak(内存泄露)
来搜索内存找到system()
的地址。,我们可以使用pwntools
的DynELF
模块来泄漏地址信息,从而获取到shell。通过泄露内存的方式可以获取目标程序libc
中各函数的地址,这种攻击方式可以绕过地址随机化保护。下文通过一个例子讨论泄露内存的ROP
攻击。
源程序
1 |
|
编译:
1 | gcc -m32 -fno-stack-protector -o example example.c |
DynELF
DynELF
是 pwntools
中专门用来应对无libc
情况的漏洞利用模块,其基本代码框架如下:
1 | sh = process('./xxx') |
需要使用者进行的工作主要集中在leak函数的具体实现上,上面的代码只是个模板。其中,address就是leak函数要泄漏信息的所在地址,而payload
就是触发目标程序泄漏address
处信息的攻击代码。通过这个leak函数可以获取到某个地址上最少1 byte
的数据。
使用条件
不管有没有libc
文件,要想获得目标系统的system函数地址,首先都要求目标二进制程序中存在一个能够泄漏目标系统内存中libc
空间内信息的漏洞。同时,由于我们是在对方内存中不断搜索地址信息,故我们需要这样的信息泄露漏洞能够被反复调用。以下是大致归纳的主要使用条件:
1)目标程序存在可以泄露libc
空间信息的漏洞,如read@got就指向libc
地址空间内;
2)目标程序中存在的信息泄露漏洞能够反复触发,从而可以不断泄露libc
地址空间内的信息
程序分析
read 函数这里显然存在一个缓存区溢出的漏洞,buf
的长度是128
,read
函数读取了256
字节的数据,造成了缓冲区溢出。程序example
运行的时候由于通过动态链接编译,使用了libc
中的函数,我们可以通过 ldd
命令查看程序使用的共享库:
不同的操作系统的libc
版本可能不同,不同版本libc
中函数的地址也不同。比如system
函数在libc 1.9.2
中的位置和libc 2.2.3
中的位置不同。可以通过以下命令查看自己操作系统中libc
的版本
1 | $ dpkg -l | grep libc |
一般的操作系统默认开启了地址随机化的保护机制(可以通过checksec
查看),程序每次运行的时候,载入到内存中的位置是随机的。如下图,多次使用ldd
查看exmaple
使用的共享库,可以发现地址已经变化了:
但是程序运行的时候libc
已经载入到内存中了,这时libc
的地址是一个固定的值,我们可以通过泄露内存的方法dump出程序正在使用的libc
,从而找到libc
中system函数的地址。
也就是说我们需要构造一个能泄露至少一字节内存的payload:
1 | 'A' * N + p32(write_plt) + p32(ret_addr) + p32(1) + p32(address) + p32(4) |
输入N个字符后发生溢出,write_plt
的地址将会覆盖read函数的返回地址,随后程序将会跳转到write函数,我们在栈中构造了write函数的3个参数和返回地址,这段payload相当于让程序执行
write(1, address, 4)
这样就可以dump出内存中地址为address处的4字节数据。知道如何从内存中dump数据后,便可以使用pwntools
中的DynELF
模块查找system函数,并获取system的地址。首先需要确定输入多少字符时,溢出会发生 ,这里可以使用pwntools
里面的cyclic
工具生成字符串,然后用GDB
调试example
,找到溢出点,最后,再次使用pwntools
中的cyclic查找溢出点距离返回地址的偏移量【通过直接传送大量的字符覆盖返回地址使其在动态调试时报错的方法来确定偏移】:
1 | gdb调式程序,输入r运行程序,停在输入处输入cyclic字符串,查看无效地址(也即下文中的lookup_value) |
可以看到,第140字节后的4个字节会覆盖read函数的返回地址,所以泄露system地址的payload如下:
1 | 'A' * 140 + p32(write_plt) + p32(ret_addr) + p32(1) + p32(address) + p32(4) |
构造leak函数
1 | def leak(address): |
这段函数能从内存中address处dump出4字节数据,函数执行结束后会返回main函数重新执行,也就是说利用这个函数,我们可以dump出整个libc
。
使用DynELF
模块查找system函数地址:
1 | d = DynELF(leak, elf=ELF('./001')) |
获取到system地址后便可以构造system(“/bin/sh”)攻击程序。要注意的是,通过DynELF模块只能获取到system()在内存中的地址,但无法获取字符串“/bin/sh”
在内存中的地址。所以我们在payload中需要调用read()
将“/bin/sh”这字符串写入到程序的.bss段
中。.bss段是用来保存全局变量的值的,地址固定
,并且可以读可写
。通过readelf -S example
这个命令就可以获取到bss段的地址了。payload如下:
1 | 'a'*140 + p32(read_plt) + p(ppp_ret) + p32(0) + p32(bss_addr) + p32(8) + p32(system_addr) + 'a'*4 + p32(bss_addr) |
我们构造的read函数有3个参数,这3个参数和read函数的返回地址不同,返回地址在ret
指令执行时被pop出栈,但是这3个参数却还留在栈中,没有被弹出栈,这会影响我们构造的下一个函数system的执行,所以我们需要找一个连续pop三个寄存器的指令来平衡堆栈。这种指令很容易找到,如下:
1 | $ objdump -d example | grep pop -C5 |
使用字符串过滤的方法即可。
我们找的pop指令后面还需要带有一个ret
指令,这样我们平衡堆栈后可以返回到我们构造的函数,如下图所示:
我们可以选取 0x80484f9 - 0x80484fc
这四条指令:
1 | pop ebx; pop esi; pop ebp; ret |
形如这样的一串汇编指令也叫作gadgets,在ROP
攻击中利用很广泛。gadgets散落在程序汇编代码的各个角落,当程序的代码很长的时候,寻找gadgets就会变得很复杂,因此有人写过工具专门用来寻找程序中的gadgets,比如ROPgadgets
。
整个攻击过程如下:
首先通过DynELF获取到system()
的地址后,我们又通过read将“/bin/sh”
写入到.bss段上,最后再调用system(.bss)
,执行“/bin/sh”
攻击脚本
1 |
|
注:ret_addr
不能写成是 ret_addr=elf.symbols['vulnerable_function']
,我刚开始时是这样写的,但总是得不到想要的结果,后来回想起看过的一篇文章,然后改为 ret_addr=elf.symbols['main']
,结果就行了,据别人说可能是环境变量被覆盖了。