一.新知识学习VM题目所说的VM并非需要用虚拟机调试程序等而是出题人在题目中通过代码做出一个类似虚拟机的模拟代码。在逆向中的虚拟机保护技术可以说是一种虚拟机代码的保护技术基于x86汇编系统中的可执行代码转换为字节码指令系统的代码保护代码不被轻易逆向或修改。简单点说就是将程序代码转换为自定义的操作指令在程序执行时再通过解释这些操作码选择对应的函数执行来实现程序原有的功能。程序特征VM虚拟机保护的代码都有一套自己的循环流程while(ilen(opcode)): if(opcode[i]0x00): i1 elif(opcode[i]0x01): i2 elif(opcode[i]0x02): i3其中opcode一般情况是由操作数和操作组成的一大串数据可能会是成千上百个byte组成。逆向这类程序最核心的就在模拟执行流。原理特征解析1.vm_init初始化虚拟机规定虚拟机寄存器和操作定义寄存器和操作指令其中上面部分规定寄存器第二个a14和下面的a187之间的差值为4这就说明a14这个寄存器是存放4字节的dd类型以此类推至a112。下面的部分是操作指令的初始化我们能够发现和上面部分的略有不同变成了一个地址和一个自定义的操作照应这里有一张各个字节的对照表这里看到寄存器初始化我们就要考虑修复结构体。typedef struct { unsigned long R0; //寄存器 unsigned long R1; unsigned long R2; unsigned long R4; unsigned char *rip; //指向正在解释的opcode地址 vm_opcode op_list[OPCODE_N]; //opcode列表存放了所有的opcode及其对应的处理函数 }vm_cpu; typedef struct { unsigned long opcode; void (*handle)(void*); }vm_opcode;修复之后就是这样我们能够更清晰直观的认出各个寄存器2.vm_start:虚拟机入口负责执行环境的上下文切换。入口将真实的CPU寄存器、标志位等保存到虚拟机的模拟环境中出口则将虚拟环境的结果恢复回真实环境。void vm_start(vm_cpu *cpu) { cpu-eip (unsigned char*)opcodes; while((*cpu-eip) ! 0xf4) //如果opcode不为RET就调用vm_dispatcher来解释执行 { vm_dispatcher(*cpu-eip) } }3.vm_dispatcher调度器:核心循环负责读取当前虚拟指令指针指向的字节码。调度器解释opcode并选择对应的handle函数执行当handle执行完后会跳回这里形成一个循环。opcode :程序可执行代码转换成的操作码void vm_dispatcher(vm_cpu *cpu) { int i; for(i 0; i OPCODE_N; i) { if(*cpu-eip cpu-op_list[i].opcode) { cpu-op_list[i].handle(cpu); break; } } }常见指令特征mov--取值赋值本质上是实现vm内的数据传输__int64 __fastcall sub_1400010F0(_cpu *a1) { __int64 result; // rax unsigned __int8 v2; // [rsp0h] [rbp-18h] v2 byte_140005360[a1[1]._ecx 1]; if ( v2 ) { switch ( v2 ) { case 1u: dword_140005040[a1-_ecx] a1-_eax; //将eax的值取出来放到数据域 break; case 2u: *(a1-_eax byte_140005360[a1[1]._ecx 2]) *(a1-_eax byte_140005360[a1[1]._ecx 3]); break; case 3u: *(a1-_eax byte_140005360[a1[1]._ecx 2]) byte_140005360[a1[1]._ecx 3]; break; //将立即数赋值给指定寄存器 } } else { a1-_eax dword_140005040[a1-_ecx]; //取出内存数值给寄存器eax } result (unsigned int)(a1[1]._ecx 4); a1[1]._ecx result; return result; }case 2u: *(a1-_eax byte_140005360[a1[1]._ecx 2]) *(a1-_eax byte_140005360[a1[1]._ecx 3]); break; a1-_eax 基地址指向寄存器数组起始位置的指针 通过 a1-_eax 基址 偏移量访问不同寄存器 mov eax, ebxpush--含有典型的栈指针自增行为强制使用栈指针访问__int64 __fastcall sub_140001230(_cpu *a1) { __int64 result; // rax unsigned __int8 v2; // [rsp0h] [rbp-18h] v2 byte_140005360[a1[1]._ecx 1]; if ( v2 ) { switch ( v2 ) { case 1u: //栈指针自增连续储存 dword_140005D40[a1[1]._edx] a1-_eax; break; case 2u: dword_140005D40[a1[1]._edx] a1-_ecx; break; case 3u: dword_140005D40[a1[1]._edx] a1-_edx; break; } } else { dword_140005D40[a1[1]._edx] a1-_eax; } result (unsigned int)(a1[1]._ecx 2); a1[1]._ecx result; return result; }相比于数组和mov指令的直接索引push只能通过栈指针访问数据每次在栈顶压数据dword_140005D40[]是栈空间a1[1]._edx是栈指针(SP)pop--指针自减和push指令相似__int64 __fastcall sub_140001380(_cpu *a1) { __int64 result; // rax unsigned __int8 v2; // [rsp0h] [rbp-18h] v2 byte_140005360[a1[1]._ecx 1]; if ( v2 ) { switch ( v2 ) { case 1u: a1-_ebx dword_140005D40[a1[1]._edx--]; break; case 2u: a1-_ecx dword_140005D40[a1[1]._edx--]; break; case 3u: a1-_edx dword_140005D40[a1[1]._edx--]; break; } } else { a1-_eax dword_140005D40[a1[1]._edx--]; } result (unsigned int)(a1[1]._ecx 2); a1[1]._ecx result; return result; }加减乘除和异或--明显的运算符号__int64 __fastcall sub_1400014D0(_cpu *a1) { __int64 result; // rax switch ( byte_140005360[a1[1]._ecx 1] ) { case 0u: *(a1-_eax byte_140005360[a1[1]._ecx 2]) *(a1-_eax byte_140005360[a1[1]._ecx 3]); //加法运算 break; case 1u: *(a1-_eax byte_140005360[a1[1]._ecx 2]) - *(a1-_eax byte_140005360[a1[1]._ecx 3]); //减法运算 break; case 2u: *(a1-_eax byte_140005360[a1[1]._ecx 2]) * *(a1-_eax byte_140005360[a1[1]._ecx 3]); //乘法运算 break; case 3u: *(a1-_eax byte_140005360[a1[1]._ecx 2]) ^ *(a1-_eax byte_140005360[a1[1]._ecx 3]); //异或运算 break; case 4u: *(a1-_eax byte_140005360[a1[1]._ecx 2]) *(a1-_eax byte_140005360[a1[1]._ecx 3]); *(a1-_eax byte_140005360[a1[1]._ecx 2]) 0xFF00u; //特殊位移 break; case 5u: *(a1-_eax byte_140005360[a1[1]._ecx 2]) (unsigned int)*(a1-_eax byte_140005360[a1[1]._ecx 2]) *(a1-_eax byte_140005360[a1[1]._ecx 3]); break; default: break; } result (unsigned int)(a1[1]._ecx 4); a1[1]._ecx result; return result; }cmp--比较和zf标志寄存器有关存在明显的判断后0,1赋值__int64 __fastcall sub_1400017F0(_cpu *a1) { __int64 result; // rax if ( a1-_eax a1-_ebx ) LOBYTE(a1[2]._eax) 0;//ZF0 if ( a1-_eax ! a1-_ebx ) LOBYTE(a1[2]._eax) 1; result (unsigned int)(a1[1]._ecx 1); a1[1]._ecx result; return result; }jmp无条件跳转--无条件检查修改栈指针__int64 __fastcall sub_140001870(_cpu *a1) { __int64 result; // rax result byte_140005360[a1[1]._ecx 1]; a1[1]._ecx result;//修改栈指针 return result; }jmp条件跳转--有检查条件并修改栈指针__int64 __fastcall sub_1400018F0(_cpu *a1) { __int64 result; // rax if ( LOBYTE(a1[2]._eax) ) result (unsigned int)(a1[1]._ecx 2); else result byte_140005360[a1[1]._ecx 1]; a1[1]._ecx result; return result; }做题要点还原结构体脚本模拟执行流得到带有加密逻辑的汇编代码。再根据这个加密的指令逆向加密逻辑[HGAME 2023 week4]vm re复现首先依旧查壳养成良好做题习惯^^确定无壳我们就可以放到ida里面去了--打开之后当然直奔主函数int __fastcall main(int argc, const char **argv, const char **envp) { int i; // [rsp20h] [rbp-A8h] char v5[36]; // [rsp28h] [rbp-A0h] BYREF char v6[40]; // [rsp50h] [rbp-78h] BYREF char v7[40]; // [rsp78h] [rbp-50h] BYREF qmemcpy(v5, (const void *)sub_140001000(v6, argv, envp), sizeof(v5)); qmemcpy(v7, v5, 0x24ui64); for ( i 0; i 40; i ) dword_140005040[i] getchar(); if ( (unsigned __int8)sub_1400010B0(v7) ) sub_140001B80(std::cout, try again...); else sub_140001B80(std::cout, unk_1400032D0); return 0; }其中qmemcpy(v5, (const void *)sub_140001000(v6, argv, envp), sizeof(v5));调用函数对某个对象初始化因此我们跟进这个被调用的函数是套娃继续跟进得到对寄存器初始化操作的代码__int64 __fastcall sub_140001060(__int64 a1) { memset((void *)a1, 0, 0x18ui64); *(_DWORD *)(a1 24) 0; *(_DWORD *)(a1 28) 0; *(_BYTE *)(a1 32) 0; return a1; }那根据初始化的寄存器就可以开始着手修复结构体了这里是一个详细的修复结构体操作记录首先找到这个structures视图。进入之后如图第一步创建一个结构体。右键找到add跟随操作给它命名。这里我命名换成了_cpu创建好后对着end这个部分按住D就可以创建第一个寄存器了。如上所述两个自定义寄存器之间的差值为4则表示定义为四字节即可记得在创建寄存器的时候对后面存放的字节改为对应类型。为了方便我们查看我们还可以对已创建的寄存器改名。具体操作既对着创建好的寄存器按键盘上的N改成易于识别的名字完成之后我们就可以回到我们程序中通过修改数据类型使程序更加清晰。那接下来我们跟进另外一个函数sub_1400010B0这里在if语句中调用这个函数说明该函数存在对明文的加密或者解密等操作仍然是套娃继续跟进就好了。在这个函数中主要运用了Switch-case语句调用这些函数这就是在对操作指令定义和调用。对于修改a1的数据类型只需要对着a1前的int类型按Y修改成我们创建的结构体即可。现在就需要我们进入被调用的函数去分析表示的指令了。如上对指令特征分析部分已经详细解释过各个函数的内容和表达的指令在此不做赘述。直接对应将各个函数重命名得到完整的虚拟机执行流得到这些指令的模拟流程后我们还缺少一个opcode的数据。回到进入操作指令定义的函数这里很明显了我们的数据就存放在byte_140005360这里有一个小问题当你提取出数据时会发现有很多很多但为什么题解中只到0xff就结束了首先这里的数据储存是根据寄存器存储的寄存器类型来决定的就像在本题中寄存器都是存储四字节的所以对于操作指令的数据将被分为四个一组。至于为什么到0xff就截止了是因为上面程序中的if语句2550xff。最后需要解密的数据也同理从中获得最后附还原脚本vmcode [0x00, 0x03, 0x02, 0x00, 0x03, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x03, 0x02, 0x32, 0x03, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x03, 0x02, 0x64, 0x03, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x01, 0x00, 0x00, 0x03, 0x00, 0x08, 0x00, 0x02, 0x02, 0x01, 0x03, 0x04, 0x01, 0x00, 0x03, 0x05, 0x02, 0x00, 0x03, 0x00, 0x01, 0x02, 0x00, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0x03, 0x00, 0x01, 0x03, 0x00, 0x03, 0x00, 0x00, 0x02, 0x00, 0x03, 0x00, 0x03, 0x01, 0x28, 0x04, 0x06, 0x5F, 0x05, 0x00, 0x00, 0x03, 0x03, 0x00, 0x02, 0x01, 0x00, 0x03, 0x02, 0x96, 0x03, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x04, 0x07, 0x88, 0x00, 0x03, 0x00, 0x01, 0x03, 0x00, 0x03, 0x00, 0x00, 0x02, 0x00, 0x03, 0x00, 0x03, 0x01, 0x28, 0x04, 0x07, 0x63, 0xFF, 0xFF] i0 for j in range(1000000): if vmcode 0xFF: break match vmcode[i]: case 0x00: print((%d)%(i),end) tmp vmcode[i1] if tmp 1: print(mov flag[reg[2]] reg[0]) elif tmp 2: print(mov reg[%d] reg[%d]%(vmcode[i2],vmcode[i3])) elif tmp 3: print(mov reg[%d] reg[%d]%(vmcode[i2],vmcode[i3])) else: print(mov reg[0] flag[reg[2]]) i4 case 0x01: print((%d)%(i),end) tmp vmcode[i1] if tmp 1: print(push reg[0]) elif tmp 2: print(push reg[2]) elif tmp 3: print(push reg[3]) else: print(push reg[0]) i2 case 0x02: print((%d)%(i),end) tmp vmcode[i1] if tmp 1: print(pop reg[1]) elif tmp 2: print(pop reg[2]) elif tmp 3: print(pop reg[3]) else: print(pop reg[0]) i2 case 0x03: print((%d)%(i),end) tmp vmcode[i1] if tmp 0: print(add reg[%d] reg[%d]%(vmcode[i2],vmcode[i3])) elif tmp1: print(sub reg[%d],reg[%d]%(vmcode[i 2],vmcode[i 3])) elif tmp2: print(mul reg[%d],reg[%d]%(vmcode[i 2],vmcode[i 3])) elif tmp3: print(xor reg[%d],reg[%d]%(vmcode[i 2],vmcode[i 3])) elif tmp4: print(shl reg[%d],reg[%d]%(vmcode[i 2],vmcode[i 3])) elif tmp5: print(shr reg[%d],reg[%d]%(vmcode[i 2],vmcode[i 3])) i4 case 0x04: print((%d)%(i),end) print(cmp reg[0] reg[1]) i1 case 0x05: print((%d)%(i),end) print(jmp %d%(vmcode[i1])) i2 case 0x06: print((%d)%(i),end) print(jz %d%(vmcode[i1])) i2 case 0x07: print((%d)%(i),end) print(jnz %d%(vmcode[i1])) i2再根据这个汇编代码的指令再还原加密逻辑就好了。a [155, 168, 2, 188, 172, 156, 206, 250, 2, 185, 255, 58, 116, 72, 25, 105, 232, 3, 203, 201, 255, 252, 128, 214, 141, 215, 114, 0, 167, 29, 61, 153, 136, 153, 191, 232, 150, 46, 93, 87] b[201, 169, 189, 139, 23, 194, 110, 248, 245, 110, 99, 99, 213, 70, 93, 22, 152, 56, 48, 115, 56, 193, 94, 237, 176, 41, 90, 24, 64, 167, 253, 10, 30, 120, 139, 98, 219, 15, 143, 156,] c [18432, 61696, 16384, 8448, 13569, 25600, 30721, 63744, 6145, 20992, 9472, 23809, 18176, 64768, 26881, 23552, 44801, 45568, 60417, 20993, 20225, 6657, 20480, 34049, 52480, 8960, 63488, 3072, 52992, 15617, 17665, 33280, 53761, 10497, 54529, 1537, 41473, 56832, 42497, 51713] cc[::-1] flag[0]*40 for i in range(len(a)): flag[i]((c[i]8)0xff (c[i]8)) flag[i]^b[i] flag[i]-a[i] for i in flag: print(chr(i0xff),end)嗯目前我对于vm题型的了解就到这里可能之后再了解还会更新修改这个文章吧。今天讲完shellcode我会把shellcode搬上来的。ok拜拜加油努力为了rmb^^