
之前写过几篇关于 VM 分析的文章,再接着写点。篇幅比较啰嗦,因为记录得比较完整。有时候看别人的文章,觉得很神奇,他们是怎么做到的,其实是把很多中间环节省略了。
我整理的时候想着是尽可能的详细吧,尽量把每一步都说清楚!
0x01:初探二进制信息
拿到二进制文件要分析,先看看它是一个怎样的文件再继续。这就是常规的操作。不能上来就开药,那是结果;起码先做个化验,看看情况。

可以看到,ELF64 文件,也是 Linux 平台下的可执行程序。没有外壳,直接分析吧。
0x02:分析源码找关键点
先定位到主函数,然后切换到翻译的 C 代码位置:
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
_QWORD v3[2]; // [rsp+10h] [rbp-10h] BYREF
v3[1] = __readfsqword(0x28u);
v3[0] = 0;
puts("Please input something:");
sub_CD1(v3);
sub_E0B(v3);
sub_F83(v3);
puts("And the flag is GWHT{true flag}");
exit(0);
}一个变量,三个函数。具体看一下每个函数的情况。
先看 sub_CD1 函数的代码,代码如下:
unsigned __int64 __fastcall sub_CD1(__int64 a1)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
*(_DWORD *)a1 = 0;
*(_DWORD *)(a1 + 4) = 18;
*(_DWORD *)(a1 + 8) = 0;
*(_DWORD *)(a1 + 12) = 0;
*(_QWORD *)(a1 + 16) = &unk_202060;
*(_BYTE *)(a1 + 24) = -15;
*(_QWORD *)(a1 + 32) = sub_B5F;
*(_BYTE *)(a1 + 40) = -14;
*(_QWORD *)(a1 + 48) = sub_A64;
*(_BYTE *)(a1 + 56) = -11;
*(_QWORD *)(a1 + 64) = sub_AC5;
*(_BYTE *)(a1 + 72) = -12;
*(_QWORD *)(a1 + 80) = sub_956;
*(_BYTE *)(a1 + 88) = -9;
*(_QWORD *)(a1 + 96) = sub_A08;
*(_BYTE *)(a1 + 104) = -8;
*(_QWORD *)(a1 + 112) = sub_8F0;
*(_BYTE *)(a1 + 120) = -10;
*(_QWORD *)(a1 + 128) = sub_99C;
qword_2022A8 = malloc(0x512u);
memset(qword_2022A8, 0, 0x512u);
return __readfsqword(0x28u) ^ v2;
}很直观,一段初始化代码,显然是个结构体,初始化也比较有规律,偏移 16 的位置处被初始化为了数据,偏移 32、48、64 等位置处初始化为函数指针;每一个函数指针赋值的上一行代码都会赋值一个数值,这个数值应该就是用来分发函数用的了。最后开辟了一块堆空间,赋值给一个全局变量。
接着看函数 sub_E0B 函数的代码,代码如下:
unsigned __int64 __fastcall sub_E0B(__int64 a1)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
*(_QWORD *)(a1 + 16) = &unk_202060;
while ( **(_BYTE **)(a1 + 16) != 0xF4 )
sub_E6E(a1);
return __readfsqword(0x28u) ^ v2;
}是个循环,循环调用了 sub_E6E 函数,那就顺便看一下这个函数吧,代码如下:
unsigned __int64 __fastcall sub_E6E(__int64 a1)
{
int i; // [rsp+14h] [rbp-Ch]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
for ( i = 0; **(_BYTE **)(a1 + 16) != *(_BYTE *)(16 * (i + 1LL) + a1 + 8); ++i )
;
(*(void (__fastcall **)(__int64))(16 * (i + 1LL) + a1 + 16))(a1);
return __readfsqword(0x28u) ^ v3;
}循环比较,找到对应的 i 值以后,通过函数指针的形式调用函数。
继续看 main 函数中的 sub_F83 函数的代码,代码如下:
unsigned __int64 sub_F83()
{
int i; // [rsp+Ch] [rbp-14h]
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
for ( i = 0; dword_2022A4 - 1 > i; ++i )
{
if ( *((_BYTE *)qword_2022A8 + i + 32) != aFzAmAmFmtSum[i] )
{
puts("WRONG!");
exit(0);
}
}
puts("Congratulation?");
puts("tips: input is the start");
return __readfsqword(0x28u) ^ v2;
}一段循环比较,不相等则输出 “WRONG!” 字符串。
那么这些函数的大体逻辑就看完了。真正要分析的是初始化函数中赋值的那些函数指针,我们需要了解每个函数的作用,然后通过 Dispatch 或者是 RunVm 就是上面的 sub_E6E 函数来分析每个函数的执行流程。大体就是这样了,后面具体看!
0x03:整理上面代码
既然大体功能了解了,那么就把它们整理一下,让自己便于阅读。
整理后的 main 函数如下:
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
VM *pVm; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
pVm = 0;
puts("Please input something:");
VmInit((VM *)&pVm);
VmRun((VM *)&pVm);
CheckFlag();
puts("And the flag is GWHT{true flag}");
exit(0);
}接着是 sub_CD1 改为了 VmInit 函数,代码如下:
unsigned __int64 __fastcall VmInit(VM *pVm)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
pVm->r0 = 0;
pVm->r1 = 18;
pVm->r2 = 0;
pVm->r3 = 0;
pVm->opcode = &opcode;
pVm->op0 = -15;
pVm->handler0 = sub_B5F;
pVm->op1 = -14;
pVm->handler1 = sub_A64;
pVm->op2 = -11;
pVm->handler2 = sub_AC5;
pVm->op3 = -12;
pVm->handler3 = sub_956;
pVm->op4 = -9;
pVm->handler4 = sub_A08;
pVm->op5 = -8;
pVm->handler5 = sub_8F0;
pVm->op6 = -10;
pVm->handler6 = sub_99C;
pMem = malloc(0x512u);
memset(pMem, 0, 0x512u);
return __readfsqword(0x28u) ^ v2;
}上面的 VM 类型是定义了一个结构体。
接着是 sub_E0B 改为了 VmRun 函数,代码如下:
unsigned __int64 __fastcall VmRun(VM *pVm)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
pVm->opcode = &opcode;
while ( *(_BYTE *)pVm->opcode != 0xF4 )
DispatchCode(pVm);
return __readfsqword(0x28u) ^ v2;
}原来的 sub_E6E 函数改为了 DispatchCode 函数,代码如下:
unsigned __int64 __fastcall DispatchCode(VM *pVm)
{
int i; // [rsp+14h] [rbp-Ch]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
for ( i = 0; *(_BYTE *)pVm->opcode != *(&pVm->op0 + 16 * i); ++i )
;
(*((void (__fastcall **)(VM *))&pVm->handler0 + 2 * i))(pVm);
return __readfsqword(0x28u) ^ v3;
}最后一个 sub_F83 函数命名为 CheckFlag,代码如下:
unsigned __int64 CheckFlag()
{
int i; // [rsp+Ch] [rbp-14h]
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
for ( i = 0; dwLength - 1 > i; ++i )
{
if ( *((_BYTE *)pMem + i + 32) != EncCodeFlag[i] )
{
puts("WRONG!");
exit(0);
}
}
puts("Congratulation?");
puts("tips: input is the start");
return __readfsqword(0x28u) ^ v2;
}从上面代码可以看出,最后的比较是从 pMem 偏移 32 的位置处开始比较。比较的内容是加密的 flag,加密的 flag 如下:
.data:0000000000202040 EncCodeFlag db 'Fz{aM{aM|}fMt~suM !!',00x04:分析初始化中的函数
初始化中的函数一共有 7 个,这些函数我们暂时还没有给它们命名,因为尚不知它们的作用。我们逐个来看一下。
unsigned __int64 __fastcall VmInit(VM *pVm)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
pVm->r0 = 0;
pVm->r1 = 18;
pVm->r2 = 0;
pVm->r3 = 0;
pVm->opcode = &opcode;
pVm->op0 = -15;
pVm->handler0 = sub_B5F;
pVm->op1 = -14;
pVm->handler1 = sub_A64;
pVm->op2 = -11;
pVm->handler2 = sub_AC5;
pVm->op3 = -12;
pVm->handler3 = sub_956;
pVm->op4 = -9;
pVm->handler4 = sub_A08;
pVm->op5 = -8;
pVm->handler5 = sub_8F0;
pVm->op6 = -10;
pVm->handler6 = sub_99C;
pMem = malloc(0x512u);
memset(pMem, 0, 0x512u);
return __readfsqword(0x28u) ^ v2;
}先来看一下 sub_B5F 函数,代码如下:
unsigned __int64 __fastcall Mov(VM *pVm)
{
int *offset; // [rsp+28h] [rbp-18h]
unsigned __int64 v3; // [rsp+38h] [rbp-8h]
v3 = __readfsqword(0x28u);
offset = (int *)(pVm->opcode + 2LL);
switch ( *(_BYTE *)(pVm->opcode + 1LL) )
{
case 0xE1:
pVm->r0 = *((char *)pMem + *offset);
break;
case 0xE2:
pVm->r1 = *((char *)pMem + *offset);
break;
case 0xE3:
pVm->r2 = *((char *)pMem + *offset);
break;
case 0xE4:
*((_BYTE *)pMem + *offset) = pVm->r0;
break;
case 0xE5:
pVm->r3 = *((char *)pMem + *offset);
break;
case 0xE7:
*((_BYTE *)pMem + *offset) = pVm->r1;
break;
default:
break;
}
pVm->opcode += 6LL;
return __readfsqword(0x28u) ^ v3;
}从代码可以看出,这段代码的功能是汇编的 mov 功能,也就是赋值,代码是我重命名整理过的代码。
再来看一下 sub_A64 函数,代码如下:
unsigned __int64 __fastcall Xor(VM *pVm)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
pVm->r0 ^= pVm->r1;
++pVm->opcode;
return __readfsqword(0x28u) ^ v2;
}上面的代码就是完成了异或的功能,因此命名为 Xor 即可。
再来看一下 sub_AC5 函数,代码如下:
unsigned __int64 __fastcall InputAndCheckLength(VM *pVm)
{
const char *buf; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
buf = (const char *)pMem;
read(0, pMem, 0x20u);
dwLength = strlen(buf);
if ( dwLength != 21 )
{
puts("WRONG!");
exit(0);
}
++pVm->opcode;
return __readfsqword(0x28u) ^ v3;
}上面的代码是输入并检查输入长度的函数,这个函数一定先被执行。
再来看一下 sub_956 函数,代码如下:
unsigned __int64 __fastcall Nop(VM *pVm)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
++pVm->opcode;
return __readfsqword(0x28u) ^ v2;
}上面的代码相当于什么都没有做,相当于就是条 Nop 指令。
再来看一下 sub_A08 函数,代码如下:
unsigned __int64 __fastcall Mul(VM *pVm)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
pVm->r0 *= pVm->r3;
++pVm->opcode;
return __readfsqword(0x28u) ^ v2;
}上面的代码相当于是一条乘法指令。
再来看一下 sub_8F0 函数,代码如下:
unsigned __int64 __fastcall Swap(VM *pVm)
{
int r0; // [rsp+14h] [rbp-Ch]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
r0 = pVm->r0;
pVm->r0 = pVm->r1;
pVm->r1 = r0;
++pVm->opcode;
return __readfsqword(0x28u) ^ v3;
}这个是一条 Swap 指令,交换寄存器 r0 和 r1 的值。
再来看一下 sub_99C 函数,代码如下:
unsigned __int64 __fastcall Arithmetic(VM *pVm)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
pVm->r0 = pVm->r2 + 2 * pVm->r1 + 3 * pVm->r0;
++pVm->opcode;
return __readfsqword(0x28u) ^ v2;
}这个相当于完成了一个算术运算吧。
完善后的初始化代码如下:
unsigned __int64 __fastcall VmInit(VM *pVm)
{
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
pVm->r0 = 0;
pVm->r1 = 18;
pVm->r2 = 0;
pVm->r3 = 0;
pVm->opcode = opcode;
pVm->op0 = 0xF1;
pVm->handler0 = Mov;
pVm->op1 = 0xF2;
pVm->handler1 = Xor;
pVm->op2 = 0xF5;
pVm->handler2 = InputAndCheckLength;
pVm->op3 = 0xF4;
pVm->handler3 = Nop;
pVm->op4 = 0xF7;
pVm->handler4 = Mul;
pVm->op5 = 0xF8;
pVm->handler5 = Swap;
pVm->op6 = 0xF6;
pVm->handler6 = Arithmetic;
pMem = malloc(0x512u);
memset(pMem, 0, 0x512u);
return __readfsqword(0x28u) ^ v2;
}好了,上面就是分析完成了初始化的代码,以及了解了整个代码的大体结构。下篇文章我们来分析整个代码的执行流程。最后看一下整个 opcode 的大小。

看到一个很大的数组,这堆字节码就是要执行的相关操作,也就是我们的 opcode,本篇文章就到这里。
未完待续!