首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >babyvm 逆向分析(一)

babyvm 逆向分析(一)

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

之前写过几篇关于 VM 分析的文章,再接着写点。篇幅比较啰嗦,因为记录得比较完整。有时候看别人的文章,觉得很神奇,他们是怎么做到的,其实是把很多中间环节省略了。

我整理的时候想着是尽可能的详细吧,尽量把每一步都说清楚!

0x01:初探二进制信息

拿到二进制文件要分析,先看看它是一个怎样的文件再继续。这就是常规的操作。不能上来就开药,那是结果;起码先做个化验,看看情况。

可以看到,ELF64 文件,也是 Linux 平台下的可执行程序。没有外壳,直接分析吧。

0x02:分析源码找关键点

先定位到主函数,然后切换到翻译的 C 代码位置:

代码语言:javascript
复制
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 函数的代码,代码如下:

代码语言:javascript
复制
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 函数的代码,代码如下:

代码语言:javascript
复制
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 函数,那就顺便看一下这个函数吧,代码如下:

代码语言:javascript
复制
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 函数的代码,代码如下:

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

代码语言:javascript
复制
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 函数,代码如下:

代码语言:javascript
复制
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 函数,代码如下:

代码语言:javascript
复制
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 函数,代码如下:

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

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

代码语言:javascript
复制
.data:0000000000202040 EncCodeFlag     db 'Fz{aM{aM|}fMt~suM !!',0

0x04:分析初始化中的函数

初始化中的函数一共有 7 个,这些函数我们暂时还没有给它们命名,因为尚不知它们的作用。我们逐个来看一下。

代码语言:javascript
复制
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 函数,代码如下:

代码语言:javascript
复制
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 函数,代码如下:

代码语言:javascript
复制
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 函数,代码如下:

代码语言:javascript
复制
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 函数,代码如下:

代码语言:javascript
复制
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 函数,代码如下:

代码语言:javascript
复制
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 函数,代码如下:

代码语言:javascript
复制
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 函数,代码如下:

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

这个相当于完成了一个算术运算吧。

完善后的初始化代码如下:

代码语言:javascript
复制
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,本篇文章就到这里。

未完待续!

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

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

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

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

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