首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >某二进制 VM 逆向分析(二)

某二进制 VM 逆向分析(二)

作者头像
码农UP2U
发布2026-03-16 17:25:10
发布2026-03-16 17:25:10
740
举报
文章被收录于专栏:码农UP2U码农UP2U

书接上回(上篇文章:某二进制 VM 逆向分析(一)),上回已经还原了 main 函数,也知道我们关键要分析 VM 的执行流程,才能找我们想要的 Flag。

0x01:上次回顾

上回通过分析,知道了关键的代码被加密,通过 chatgpt 写的脚本完成了 smc 的解密功能。看一下 main 函数的代码,代码如下:

代码语言:javascript
复制
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 语言代码就可以了。

代码开头部分如下:

代码语言:javascript
复制
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 也复制过来。

代码如下:

代码语言:javascript
复制
#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 行的代码:

代码语言:javascript
复制
printf("%3d:  ", pvm->ip - opcode);

我们每次输出 pvm->ip 当前所在 opcode 的偏移。这个是我们重要的信息。

还有一段代码我没有贴在上面,这里单独贴一下,代码如下:

代码语言:javascript
复制
    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:测试编译运行

编译并运行上面的代码,输出如下:

代码语言:javascript
复制
gcc vm.c -o vm.exe
.\vm.exe
  0:    1:    3:    4:    5:   10:   12:   13:   14:

可以看到,这就是上面我们的 printf 的输出,即下面这行代码的输出:

代码语言:javascript
复制
printf("%3d:  ", pvm->ip - opcode);

它输出的是 opcode 的当前偏移位置。我们的 opcode 有几百个个字节,ip 偏移输出到 14 就没有了,那说明程序退出了。

输出成这样已经达到了我们的效果,它退出是很正常的,说明里面进行了比对,我们的输入明显不对,也就是我们下面这行代码:

代码语言:javascript
复制
strcpy(ReadBuf, "11111111112222222222333333333344444444445555");

拷贝的字符串是随手填的,不对是正常的,因为找到这个值就是我们的任务。

0x05:输出执行哪些分支

我们修改 while 循环的 printf 输出即可,代码如下:

代码语言:javascript
复制
printf("%3d:  =>%x:  ", pvm->ip - opcode, *(_BYTE *)pvm->ip);

接着编译运行,输出如下:

代码语言:javascript
复制
.\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,我们把这些流程中添加一些输出的代码。代码如下:

代码语言:javascript
复制
    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;
    }

后面的流程就不写了,太多了,就这样输出就可以。添加的代码并不多,每个分支里面进行一行打印,打印出当前执行了哪些代码,代码中引用的值都是多少。修改完以后编译运行,输出结果如下:

代码语言:javascript
复制
> .\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 异或,就可以得到正确的值。这就是异或的特点。

修改后的代码如下:

代码语言:javascript
复制
    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;
    }

首先,我们输出了正确的值,其次我们不判断,也不退出了。这里只是一个打印。

编译运行一下,结果如下:

代码语言:javascript
复制
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 的正确值了。

继续修改一下代码:

代码语言:javascript
复制
// 增加了全局变量
char szResult[100] = {0};
// 一个下标索引
int i = 0;

在循环中增加一个输出,这个输出增加到打印 opcode 偏移的后边即可,代码如下:

代码语言:javascript
复制
    if (pvm->ip - opcode == 289)
    {
        printf("===> %s\r\n", szResult);
    }

最后,拼接我们想要的字符串,代码如下:

代码语言:javascript
复制
    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;
    }

增加了这样一行代码:

代码语言:javascript
复制
szResult[i++] = (unsigned __int8)*(_DWORD *)(pvm->ip + 1) ^ pvm->r9;

编译运行,输出结果如下:

可以看到,我们 0 ~ 31 位置的结果出来了。

代码语言:javascript
复制
16584abc45baff901c59dde3b1bb6701

0x08:修改输入并测试

继续修改我们的代码,并进行测试,代码如下:

代码语言:javascript
复制
    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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-12-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 源代码010 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档