
书接上回(上篇文章:某二进制 VM 逆向分析(一)),上回已经还原了 main 函数,也知道我们关键要分析 VM 的执行流程,才能找我们想要的 Flag。
0x01:上次回顾
上回通过分析,知道了关键的代码被加密,通过 chatgpt 写的脚本完成了 smc 的解密功能。看一下 main 函数的代码,代码如下:
int main()
{
VM *pVm; // [esp+18h] [ebp-10h]
InitIO();
pVm = (VM *)VmInit();
pVm->ip = &opcode;
smc();
RunVm(pVm);
return 0;
}我们的关键是,分析 RunVm 这函数。
0x02:简单阅读 RunVm 的流程
有工具就是好,尤其是有了 F5 这个插件,很多时候不用看反汇编了,直接看 C 语言代码就可以了。
代码开头部分如下:
unsigned int __cdecl RunVm(VM *pvm)
{
_BYTE *r3; // [esp+18h] [ebp-20h]
unsigned int v3; // [esp+2Ch] [ebp-Ch]
v3 = __readgsdword(0x14u);
while ( 1 )
{
if ( *(_BYTE *)pvm->ip == 0x71 )
{
pvm->r6 -= 4;
*(_DWORD *)pvm->r6 = *(_DWORD *)(pvm->ip + 1);
pvm->ip += 5;
}
if ( *(_BYTE *)pvm->ip == 0x41 )
{
pvm->r1 += pvm->r2;
++pvm->ip;
}
if ( *(_BYTE *)pvm->ip == 0x42 )
{
pvm->r1 -= pvm->r4;
++pvm->ip;
}
if ( *(_BYTE *)pvm->ip == 0x43 )
{
pvm->r1 *= pvm->r3;
++pvm->ip;
}整个 RunVm 的代码有 208 行,里面就是一个很大的 while 循环,和一堆 if 判断。
静态分析肯定是没有办法搞了,动态调试搞清楚流程也费劲,写代码模拟执行吧。
0x03:模拟执行 C 代码框架
我们要模拟执行,并不能简单的复制它的代码,至少要加一些输出,当然了,它跑过的代码我们大部分都是得要的。
比如上面的 VmInit、RunVm 这些代码肯定都要自己写,包括结构体的定义,还要把 opcode 也复制过来。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef unsigned int _DWORD;
typedef unsigned char _BYTE;
struct VM
{
_DWORD r0;
_DWORD r1;
_DWORD r2;
_DWORD r3;
_DWORD r4;
_DWORD r5;
_DWORD r6;
_DWORD r7;
char* ip;
_DWORD r9;
_DWORD r10;
};
char opcode[] = {
0x0A1, 0x0C1, 0x0, 0x0B1, 0x77, 0x0C2, 0x4A, 0x1, 0x0, 0x0, 0x0C1,
// 为了节省篇幅,这里的代码 opcode 不全
// opcode 的长度有几百个
};
struct VM *VmInit()
{
struct VM *v1; // [esp+8h] [ebp-10h]
v1 = (struct VM *)malloc(0x3Cu);
v1->r0 = 0;
v1->r1 = 0;
v1->r2 = 0;
v1->r3 = 0;
v1->r4 = 0;
v1->r5 = 0;
v1->r9 = 0;
v1->r10 = (_DWORD)calloc(4u, 0x50u);
v1->r6 = v1->r10 + 316;
v1->r7 = v1->r10 + 316;
v1->ip = 0;
return v1;
}
unsigned int dword_804B080[] = {0x7B, 0x2F, 0x37, 0x0E8};
unsigned int dword_804B060 = 0xCF1304DC;
unsigned int dword_804B064 = 0x283B8E84;
char ReadBuf[100] = {0};
int __cdecl sub_804875F(struct VM *pvm, unsigned int a2)
{
int result; // eax
result = 0;
if ( a2 <= 2 )
return *(unsigned __int8 *)(pvm->ip + a2);
return result;
}
void __cdecl RunVm(struct VM *pvm)
{
_BYTE *r3; // [esp+18h] [ebp-20h]
unsigned int v3; // [esp+2Ch] [ebp-Ch]
// v3 = __readgsdword(0x14u);
while ( 1 )
{
printf("%3d: ", pvm->ip - opcode);
// 一堆 if 判断,就不往过抄了
}
}
int main()
{
struct VM *pvm;
pvm = VmInit();
pvm->ip = opcode;
RunVm(pvm);
return 0;
}在上面的代码中可以看出,除了我们的 main、RunVm、VmInit 等几个函数外,还定义了一些全局变量,还有一些额外的方法,这些是怎么来的呢?我把之前的代码都赋值过来,然后编译时没有定义的全局变量和额外的方法都会报错给出提示的,然后,我直接找到这些代码和数据复制过来的。
上面最关键的代码是第 72 行的代码:
printf("%3d: ", pvm->ip - opcode);我们每次输出 pvm->ip 当前所在 opcode 的偏移。这个是我们重要的信息。
还有一段代码我没有贴在上面,这里单独贴一下,代码如下:
else if ( *(_BYTE *)pvm->ip == 0xA1 )
{
// read(0, ReadBuf, 0x2Cu);
strcpy(ReadBuf, "11111111112222222222333333333344444444445555");
if ( strlen(ReadBuf) != 44 )
exit(0);
++pvm->ip;
}这段代码是整个 while 循环的第一条被执行的代码,它在 0xA1 这个分支中会要求用户进行输入,且长度要求是 44 个字节,我这里为了省事,不要输入,直接用 strcpy 拷贝了 44 个字节到数组里。
0x04:测试编译运行
编译并运行上面的代码,输出如下:
gcc vm.c -o vm.exe
.\vm.exe
0: 1: 3: 4: 5: 10: 12: 13: 14:可以看到,这就是上面我们的 printf 的输出,即下面这行代码的输出:
printf("%3d: ", pvm->ip - opcode);它输出的是 opcode 的当前偏移位置。我们的 opcode 有几百个个字节,ip 偏移输出到 14 就没有了,那说明程序退出了。
输出成这样已经达到了我们的效果,它退出是很正常的,说明里面进行了比对,我们的输入明显不对,也就是我们下面这行代码:
strcpy(ReadBuf, "11111111112222222222333333333344444444445555");拷贝的字符串是随手填的,不对是正常的,因为找到这个值就是我们的任务。
0x05:输出执行哪些分支
我们修改 while 循环的 printf 输出即可,代码如下:
printf("%3d: =>%x: ", pvm->ip - opcode, *(_BYTE *)pvm->ip);接着编译运行,输出如下:
.\vm.exe
0: =>a1: 1: =>c1: 3: =>b1: 4: =>77: 5: =>c2: 10: =>c1: 12: =>b2: 13: =>77: 14: =>c2:从上面可以看到,偏移还是到了 14 的位置处,但是我们打印出了它执行了哪些分支,比如执行了 a1、c1、b1 等。那么我们接着处理我们的代码。偏移位置处的 opcode 决定了所走的分支,而不同的分支决定了所要执行的代码。
因此,输出具体的分支对应的 opcode 就可以帮我们知道要执行哪些代码。
0x06:输出分支执行的工作
上面的代码,只是增加了输出流程分支的 opcode,然后我们需要把具体的 opcode 的值对应的代码进行输出,从上面的流程可以看出执行分支的顺序是 a1 -> c1 -> b1 ... -> c2,我们把这些流程中添加一些输出的代码。代码如下:
else if ( *(_BYTE *)pvm->ip == 0xA1 )
{
printf("strcpy(ReadBuf, \"11111111112222222222333333333344444444445555\"); if(strlen(ReadBuf) != 44 ) exit(0);\r\n");
// read(0, ReadBuf, 0x2Cu);
strcpy(ReadBuf, "11111111112222222222333333333344444444445555");
if ( strlen(ReadBuf) != 44 )
exit(0);
++pvm->ip;
}
else if ( *(_BYTE *)pvm->ip == 0xC1 )
{
printf("r1 = (unsigned __int8)ReadBuf[%d]; // (r1=%d, (unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)]=%d) \r\n",
*(unsigned __int8 *)(pvm->ip + 1),
pvm->r1,
(unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)]);
pvm->r1 = (unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)];
pvm->ip += 2;
}
else if ( *(_BYTE *)pvm->ip == 0xB1 )
{
printf("r9 = dword_804B080[0]; // (r9=%d, dword_804B080[0]=%d) \r\n",
pvm->r9, dword_804B080[0]);
pvm->r9 = dword_804B080[0];
++pvm->ip;
}后面的流程就不写了,太多了,就这样输出就可以。添加的代码并不多,每个分支里面进行一行打印,打印出当前执行了哪些代码,代码中引用的值都是多少。修改完以后编译运行,输出结果如下:
> .\vm.exe
0: =>a1: strcpy(ReadBuf, "11111111112222222222333333333344444444445555"); if(strlen(ReadBuf) != 44 ) exit(0);
1: =>c1: r1 = (unsigned __int8)ReadBuf[0]; // (r1=0, (unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)]=49)
3: =>b1: r9 = dword_804B080[0]; // (r9=0, dword_804B080[0]=123)
4: =>77: r1 ^= r9; // (r1=49, r9=123)
5: =>c2: if ((unsigned __int8)*(_DWORD *)(pvm->ip + 1) != r1) exit(0); // ((unsigned __int8)*(_DWORD *)(pvm->ip + 1)=74, r1=74)
10: =>c1: r1 = (unsigned __int8)ReadBuf[1]; // (r1=74, (unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)]=49)
12: =>b2: r9 = dword_804B080[1]; // (r9=123, dword_804B080[1]=47)
13: =>77: r1 ^= r9; // (r1=49, r9=47)
14: =>c2: if ((unsigned __int8)*(_DWORD *)(pvm->ip + 1) != r1) exit(0); // ((unsigned __int8)*(_DWORD *)(pvm->ip + 1)=25, r1=30)上面的输出可能不方便查看,这里截个图看一下:

是不是一下变得很直观了。
从上面的代码可以看出 C2 是一个比较分支,ip + 1 位置的值和 r1 的值不相等就退出程序了。
在看上面的程序,可以发现 偏移 0 ~ 偏移 5 和 偏移 10 ~ 偏移 14 的算法是一样的。就是逐个读取我们的输入,然后完成一个异或的运算。
上面的输出代码中,一共执行了两次 C2 分支。第一次走 C2 分支时,竟然没有退出,说明我们输入的第一个值竟然是对的(运气啊)!第二次走 C2 分支就退出了,说明从第二个值开始就错了。既然知道就是取值然后进行异或运算,我们继续修改代码,让它打印出我们要的值。也就是打印出正确的输入。
0x07:打印正确的值给我们
上面的流程已经知道了,逐个取出我们的输入,并与 r9 进行异或,那么,我们让 ip + 1 位置的值和 r9 异或,就可以得到正确的值。这就是异或的特点。
修改后的代码如下:
else if ( *(_BYTE *)pvm->ip == 0xC2 )
{
printf("if ((unsigned __int8)*(_DWORD *)(pvm->ip + 1) != r1) exit(0); // ((unsigned __int8)*(_DWORD *)(pvm->ip + 1)=%d, r1=%d) ==> (Value=%c) \r\n",
(unsigned __int8)*(_DWORD *)(pvm->ip + 1),
pvm->r1,
(unsigned __int8)*(_DWORD *)(pvm->ip + 1) ^ pvm->r9);
// if ( (unsigned __int8)*(_DWORD *)(pvm->ip + 1) != pvm->r1 )
// exit(0);
pvm->ip += 5;
}首先,我们输出了正确的值,其次我们不判断,也不退出了。这里只是一个打印。
编译运行一下,结果如下:
253: =>c1: r1 = (unsigned __int8)ReadBuf[28]; // (r1=4, (unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)]=51)
255: =>b2: r9 = dword_804B080[1]; // (r9=55, dword_804B080[1]=47)
256: =>77: r1 ^= r9; // (r1=51, r9=47)
257: =>c2: if ((unsigned __int8)*(_DWORD *)(pvm->ip + 1) != r1) exit(0); // ((unsigned __int8)*(_DWORD *)(pvm->ip + 1)=25, r1=28) ==> (Value=6)
262: =>c1: r1 = (unsigned __int8)ReadBuf[29]; // (r1=28, (unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)]=51)
264: =>b3: 265: =>77: r1 ^= r9; // (r1=51, r9=55)
266: =>c2: if ((unsigned __int8)*(_DWORD *)(pvm->ip + 1) != r1) exit(0); // ((unsigned __int8)*(_DWORD *)(pvm->ip + 1)=0, r1=4) ==> (Value=7)
271: =>c1: r1 = (unsigned __int8)ReadBuf[30]; // (r1=4, (unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)]=52)
273: =>b1: r9 = dword_804B080[0]; // (r9=55, dword_804B080[0]=123)
274: =>77: r1 ^= r9; // (r1=52, r9=123)
275: =>c2: if ((unsigned __int8)*(_DWORD *)(pvm->ip + 1) != r1) exit(0); // ((unsigned __int8)*(_DWORD *)(pvm->ip + 1)=75, r1=79) ==> (Value=0)
280: =>c1: r1 = (unsigned __int8)ReadBuf[31]; // (r1=79, (unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)]=52)
282: =>b2: r9 = dword_804B080[1]; // (r9=123, dword_804B080[1]=47)
283: =>77: r1 ^= r9; // (r1=52, r9=47)
284: =>c2: if ((unsigned __int8)*(_DWORD *)(pvm->ip + 1) != r1) exit(0); // ((unsigned __int8)*(_DWORD *)(pvm->ip + 1)=30, r1=27) ==> (Value=1)
289: =>c1: r1 = (unsigned __int8)ReadBuf[32]; // (r1=27, (unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)]=52)
291: =>80: 297: =>23: 298: =>10: 299: =>c1: r1 = (unsigned __int8)ReadBuf[33]; // (r1=872415232, (unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)]=52)
301: =>80: 307: =>23: 308: =>f7: 309: =>c1: r1 = (unsigned __int8)ReadBuf[34]; // (r1=3407872, (unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)]=52)
311: =>80: 317: =>23: 318: =>f7: 319: =>c1: r1 = (unsigned __int8)ReadBuf[35]; // (r1=13312, (unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)]=52)
321: =>f7: 322: =>fe: 323: =>80: 329: =>22: 330: =>77: r1 ^= r9; // (r1=27369889, r9=875836468)
331: =>10: 332: =>80: 338: =>23: 339: =>80: 345: =>31: 346: =>77: r1 ^= r9; // (r1=-2000666112, r9=898995605)
347: =>10: 348: =>80: 354: =>23: 355: =>80: 361: =>31: 362: =>77: r1 ^= r9; // (r1=352321536, r9=-1118447723)
363: =>10: 364: =>80: 370: =>22: 371: =>77: r1 ^= r9; // (r1=10773, r9=-1470769259)
372: =>a0:还是截图看吧,更直观一些:

从图中可以看到偏移到了 372 的字节处(但是从 291 开始输出的代码已经不多了)。在偏移 284 的位置之前,都给出了正确的值。但是它只处理到 ReadBuf[31] 的位置处。说明从 ReadBuf[31] 之后的算法变了。
但是我们至少拿到了 0 ~ 31 的正确值了。
继续修改一下代码:
// 增加了全局变量
char szResult[100] = {0};
// 一个下标索引
int i = 0;在循环中增加一个输出,这个输出增加到打印 opcode 偏移的后边即可,代码如下:
if (pvm->ip - opcode == 289)
{
printf("===> %s\r\n", szResult);
}最后,拼接我们想要的字符串,代码如下:
else if ( *(_BYTE *)pvm->ip == 0xC2 )
{
printf("if ((unsigned __int8)*(_DWORD *)(pvm->ip + 1) != r1) exit(0); // ((unsigned __int8)*(_DWORD *)(pvm->ip + 1)=%d, r1=%d) ==> (Value=%c) \r\n",
(unsigned __int8)*(_DWORD *)(pvm->ip + 1),
pvm->r1,
(unsigned __int8)*(_DWORD *)(pvm->ip + 1) ^ pvm->r9);
szResult[i++] = (unsigned __int8)*(_DWORD *)(pvm->ip + 1) ^ pvm->r9;
// if ( (unsigned __int8)*(_DWORD *)(pvm->ip + 1) != pvm->r1 )
// exit(0);
pvm->ip += 5;
}增加了这样一行代码:
szResult[i++] = (unsigned __int8)*(_DWORD *)(pvm->ip + 1) ^ pvm->r9;编译运行,输出结果如下:

可以看到,我们 0 ~ 31 位置的结果出来了。
16584abc45baff901c59dde3b1bb67010x08:修改输入并测试
继续修改我们的代码,并进行测试,代码如下:
else if ( *(_BYTE *)pvm->ip == 0xA1 )
{
printf("strcpy(ReadBuf, \"11111111112222222222333333333344444444445555\"); if(strlen(ReadBuf) != 44 ) exit(0);\r\n");
// read(0, ReadBuf, 0x2Cu);
// strcpy(ReadBuf, "11111111112222222222333333333344444444445555");
strcpy(ReadBuf, "16584abc45baff901c59dde3b1bb6701444444445555");
if ( strlen(ReadBuf) != 44 )
exit(0);
++pvm->ip;
}
else if ( *(_BYTE *)pvm->ip == 0xC2 )
{
printf("if ((unsigned __int8)*(_DWORD *)(pvm->ip + 1) != r1) exit(0); // ((unsigned __int8)*(_DWORD *)(pvm->ip + 1)=%d, r1=%d) ==> (Value=%c) \r\n",
(unsigned __int8)*(_DWORD *)(pvm->ip + 1),
pvm->r1,
(unsigned __int8)*(_DWORD *)(pvm->ip + 1) ^ pvm->r9);
szResult[i++] = (unsigned __int8)*(_DWORD *)(pvm->ip + 1) ^ pvm->r9;
if ( (unsigned __int8)*(_DWORD *)(pvm->ip + 1) != pvm->r1 )
exit(0);
pvm->ip += 5;
}上面的代码,把输入的前 32 个字符替换成了正确的字符,并把 C2 流程的判断开启了,这样我们就可以测试这 32 个字符是否真的正确了。
编译运行:

可以看到,我们让 C2 流程的判断执行了,程序也正常执行到了偏移 372 的位置了。说明我们找到 0 ~ 31 个字符是正确的。
未完待续!
涉及文章的资源:https://pan.quark.cn/s/fd8e76c9f4ab