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

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

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

像一个侦探一样,不放过任何蛛丝马迹,去逼近事物的真相。

不知道各位 80、90 后,最近是否看《驱魔龙族马小玲》这部电影,真的是贩卖青春啥的。很多电影电视剧都在翻拍,唯独《僵约》没有被翻拍过,估计是即使拍了也知道无法超越吧。但是出来这么个电影,真是拉胯。看情况好像还有后续。

月底还有《寻秦记》的电影版,这怎么都集中在今年出呢?《寻秦记》被翻拍过一次,第一版评分 8、9 分,翻拍才 2、3 分!不知道这个电影如何。

上面是废话,下面是正文!


提到 VM,我们可能想到的是类似 VMWare 或者 Vbox 这样可以安装操作系统的虚拟机!这两种软件是日常中很多电脑用户都会用到的,或者是经常听说的。不过内容写的不是它。

如果作为一个程序员的话,也可能联想到的是类似 JVM 可以运行字节码的的虚拟机。JVM 的话已经和我们要讨论的比较接近了,当然了,这篇文章的虚拟机没有 JVM 那么复杂。

但是,我们这里提到的 VM 是一种软件保护的机制。如果稍有了解的话,可能听过VMP,它就是一种代码虚拟化的软件保护系统。早期用在二进制中,现在被保护的应用面更广泛了,比如现在还有 JSVMP 等。不过,本篇文章介绍的是二进制相关的 VM。

那么这种 VM 的软件保护机制是什么呢?其实,这种软件保护机制的 VM 执行的也是字节码,它通过将二进制代码转换成自定义的字节码从而保护软件,到软件运行时,由嵌入到可执行文件中的虚拟机来解释这些字节码。

上面就是一些简单的介绍,因为是第一次写这方面的内容,多说几句。

0x01:初识某二进制文件

拿到的样本是前几年比赛的一个二进制文件。这种题目一般就是要求输入一个正确的 “值”,这个 “值” 被称作 “Flag”。

先来看下它是什么情况,如下图所示:

可以看到,是一个 ELF32 的文件,没有外壳。那么就直接分析吧。

0x02:查看源码

打开该文件,然后定位到 main 函数的地址,然后 F5 来查看源码,源码如下:

代码语言:javascript
复制
int main()
{
  int v1; // [esp+18h] [ebp-10h]

  sub_80486BB();
  v1 = sub_8048F45();
  *(_DWORD *)(v1 + 32) = &unk_804B0C0;
  sub_80491C8();
  ((void (__cdecl *)(int))loc_80487A8)(v1);
  return 0;
}

来逐个查看一下相关函数,毕竟这样一样看去有 4 个函数,还是看的过来的。

其中 sub_80486BB 这个函数没有什么作用,就是初始化 I/O 的。

看第 6 行和第 7 行,第 6 行调用函数后的返回值放入了 v1,第 7 行通过 v1 加偏移进行了赋值。

看一下 sub_8048F45 这个函数,代码如下:

代码语言:javascript
复制
_DWORD *sub_8048F45()
{
  _DWORD *v1; // [esp+8h] [ebp-10h]

  v1 = malloc(0x3Cu);
  *v1 = 0;
  v1[1] = 0;
  v1[2] = 0;
  v1[3] = 0;
  v1[4] = 0;
  v1[5] = 0;
  v1[9] = 0;
  v1[10] = calloc(4u, 0x50u);
  v1[6] = v1[10] + 316;
  v1[7] = v1[10] + 316;
  v1[8] = 0;
  return v1;
}

是个初始化的工作,但是从 main 函数的第 7 行看到偏移,这里就处理下吧。把 v1 定义成为一个结构体,修改后的代码如下:

代码语言:javascript
复制
VM *VmInit()
{
  VM *v1; // [esp+8h] [ebp-10h]

  v1 = (VM *)malloc(60u);
  v1->r0 = 0;
  v1->r1 = 0;
  v1->r2 = 0;
  v1->r3 = 0;
  v1->r4 = 0;
  v1->r5 = 0;
  v1->r9 = 0;
  v1->r10 = calloc(4u, 80u);
  v1->r6 = v1->r10 + 316;
  v1->r7 = v1->r10 + 316;
  v1->ip = 0;
  return v1;
}

可以看到,把数组初始化变成了结构体成员的初始化,注意,这里没有 r8,因为把 r8 改名为 ip,因为通过 main 函数的第 7 行看,偏移 32 的作用就是用来指向 opcode 的位置的,作用类似 ip 寄存器,这里就命名为 ip 即可。其他的不知道是干什么的,就暂且命名为 r0、r1 这样的名字。

继续看 main 函数调用的 sub_80491C8 这个函数,代码如下:

代码语言:javascript
复制
unsigned int sub_80491C8()
{
  _DWORD v1[4]; // [esp+Ch] [ebp-1Ch] BYREF
  unsigned int v2; // [esp+1Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  v1[0] = 17;
  v1[1] = 34;
  v1[2] = 51;
  v1[3] = 68;
  mprotect(&dword_8048000, 0x2000u, 7);
  sub_8049008(&loc_80487A8, 487, v1);
  return __readgsdword(0x14u) ^ v2;
}

上面代码的流程比较简单,修改内存属性,并调用 sub_8049008 函数。继续看 sub_8049008 函数,该函数有三个参数,第一个是内存地址,第二个是长度,第三个是一个数组。重命名变量后的代码如下:

代码语言:javascript
复制
unsigned int __cdecl sub_8049008(unsigned int *pMem, int iLength, int key)
{
  unsigned int v4; // [esp+14h] [ebp-34h]
  unsigned int v5; // [esp+18h] [ebp-30h]
  int i; // [esp+1Ch] [ebp-2Ch]
  int v7; // [esp+20h] [ebp-28h]
  int v8; // [esp+24h] [ebp-24h]
  unsigned int v9; // [esp+2Ch] [ebp-1Ch]

  v9 = __readgsdword(0x14u);
  v7 = 34 / iLength + 9;
  v5 = 1316499440 * v7;
  v4 = *pMem;
  do
  {
    v8 = (v5 >> 2) & 3;
    for ( i = iLength - 1; i; --i )
    {
      pMem[i] -= (((2 * v4) ^ (pMem[i - 1] >> 4)) + ((v4 >> 3) ^ (32 * pMem[i - 1])) + 40)
               ^ ((v4 ^ v5 ^ 0x77) + (pMem[i - 1] ^ *(_DWORD *)(4 * (v8 ^ i & 3) + key)) - 15);
      v4 = pMem[i];
    }
    *pMem -= (((2 * v4) ^ (pMem[iLength - 1] >> 4)) + ((v4 >> 3) ^ (32 * pMem[iLength - 1])) + 40)
           ^ ((v4 ^ v5 ^ 0x77) + (pMem[iLength - 1] ^ *(_DWORD *)(4 * v8 + key)) - 15);
    v4 = *pMem;
    v5 -= 1316499440;
    --v7;
  }
  while ( v7 );
  return __readgsdword(0x14u) ^ v9;
}

看到了嵌套循环,还有一堆数学运算,问问 chatgpt 看看它见过没有。

chatgpt 告我这是非标准 XXTEA 解密算法。好的,了解了!

结合 XXTEA 解密算法和 main 函数的第 8 行代码中的 loc_80487A8 这个地址可以了解到,是对这块内存进行解密。从 main 的第 8 行代码可以看出 loc_80487A8 是个函数,看 XXTEA 解密的地址我们就知道了,loc_80487A8 这个函数被加密了,需要在内存中还原。

也就是说 sub_80491C8 函数是一个 smc。

0x03:解密 loc_80487A8

解密它有两种方法,第一种方法是动态调试,当这块内存被解密以后,我们把解密的代码拿过来进行分析;第二种方法是写脚本让它执行帮我们还原。

动态调试要么在 Linux 下,要么就要双机调试了,反正怎么都需要启动一个 Linux 系统,算了!

第二种方法的话,先请 chatgpt 给我们写吧,它写不了再自己写,自己写不了再动态调试。

看下解密前的部分代码:

代码语言:javascript
复制
.text:080487A8 loc_80487A8:                            ; CODE XREF: main+4B↓p
.text:080487A8                                         ; DATA XREF: smc+11↓o
.text:080487A8 ; __unwind {
.text:080487A8                 xchg    cl, [ecx+ecx*2-0B81C2A0h]
.text:080487AF                 in      eax, 1          ; DMA controller, 8237A-5.
.text:080487AF                                         ; channel 0 current word count
.text:080487B1                 outsb
.text:080487B2                 mov     edi, 8CAB9D50h
.text:080487B7                 and     al, 0D9h
.text:080487B9                 mov     edi, 0AD3B3BB8h
.text:080487BE                 pop     eax
.text:080487BF                 setz    dh
.text:080487C2                 mov     word ptr [ebp-35h], gs
.text:080487C5                 xrelease xchg    eax, edx
.text:080487C7                 adc     esp, edx
.text:080487C9                 outsb
.text:080487CA                 in      eax, dx
.text:080487CB                 xchg    eax, ebp
.text:080487CC                 rcl     dword ptr [ecx+39h], cl
.text:080487CF                 xchg    eax, esi
.text:080487D0                 enter   0FFFF99ACh, 9Ch
.text:080487D4                 jns     short loc_80487D8
.text:080487D6                 pop     ebp
.text:080487D8
.text:080487D8 loc_80487D8:                            ; CODE XREF: .text:080487D4↑j
.text:080487D8                 cmp     edi, [esp-6143B921h]
.text:080487DF                 fbstp   tbyte ptr [esi]
.text:080487E1                 jge     short loc_8048811
.text:080487E3                 loopne  near ptr loc_8048777+2
.text:080487E5                 push    eax
.text:080487E6                 lahf
.text:080487E6 ; ---------------------------------------------------------------------------
.text:080487E7                 db 0C6h
.text:080487E8                 dd 0FF7249ADh, 0D82BCA7Dh, 870F38A2h, 0EE79FC2Dh, 0B86D3157h
.text:080487FC                 dd 84E039E1h, 4CA9A96Fh, 762DE590h, 0E0C4A1D2h, 8CDE380h
.text:08048810                 db 6Eh

可以看到入口不像常规函数的入口。至少编译器生成的函数入口函数不是这样。

让 chatgpt 给我们写一个解密的脚本,代码如下:

代码语言:javascript
复制
import ida_bytes
import idaapi
import struct

# ===============================
# 参数(按你的代码来)
# ===============================

START_ADDR = 0x80487A8   # loc_80487A8
DWORD_COUNT = 487
KEY = [17, 34, 51, 68]
DELTA = 1316499440       # 0x4E67C6F0

# ===============================
# 读取 DWORD 数组
# ===============================

def read_dwords(addr, count):
    data = []
    for i in range(count):
        b = ida_bytes.get_bytes(addr + i * 4, 4)
        if not b:
            raise RuntimeError("Read failed at 0x%X" % (addr + i * 4))
        data.append(struct.unpack("<I", b)[0])
    return data

# ===============================
# 写回 DWORD 数组
# ===============================

def write_dwords(addr, data):
    for i, v in enumerate(data):
        ida_bytes.patch_dword(addr + i * 4, v & 0xFFFFFFFF)

# ===============================
# sub_8049008 解密逻辑
# ===============================

def decrypt_xxtea_variant(buf, key):
    n = len(buf)
    v7 = 34 // n + 9
    v5 = (DELTA * v7) & 0xFFFFFFFF

    v4 = buf[0]

    while v7 > 0:
        v8 = (v5 >> 2) & 3

        for i in range(n - 1, 0, -1):
            buf[i] = (buf[i] - (
                (((2 * v4) ^ (buf[i - 1] >> 4)) +
                 ((v4 >> 3) ^ (32 * buf[i - 1])) + 40)
                ^
                ((v4 ^ v5 ^ 0x77) +
                 (buf[i - 1] ^ key[(v8 ^ (i & 3)) & 3]) - 15)
            )) & 0xFFFFFFFF

            v4 = buf[i]

        buf[0] = (buf[0] - (
            (((2 * v4) ^ (buf[n - 1] >> 4)) +
             ((v4 >> 3) ^ (32 * buf[n - 1])) + 40)
            ^
            ((v4 ^ v5 ^ 0x77) +
             (buf[n - 1] ^ key[v8 & 3]) - 15)
        )) & 0xFFFFFFFF

        v4 = buf[0]
        v5 = (v5 - DELTA) & 0xFFFFFFFF
        v7 -= 1

# ===============================
# 主流程
# ===============================

def main():
    print("[*] Reading encrypted data...")
    data = read_dwords(START_ADDR, DWORD_COUNT)

    print("[*] Decrypting...")
    decrypt_xxtea_variant(data, KEY)

    print("[*] Patching decrypted data back...")
    write_dwords(START_ADDR, data)

    idaapi.auto_wait()
    print("[+] Done! SMC restored at 0x%X" % START_ADDR)

main()

解密后的代码如下:

代码语言:javascript
复制
.text:080487A8 ; __unwind {
.text:080487A8                 push    ebp
.text:080487A9                 mov     ebp, esp
.text:080487AB                 push    ebx
.text:080487AC                 sub     esp, 34h
.text:080487AF                 mov     eax, [ebp+arg_0]
.text:080487B2                 mov     [ebp+var_2C], eax
.text:080487B5                 mov     eax, large gs:14h
.text:080487BB                 mov     [ebp+var_C], eax
.text:080487BE                 xor     eax, eax
.text:080487C0
.text:080487C0 loc_80487C0:                            ; CODE XREF: RunVm+743↓j
.text:080487C0                                         ; RunVm+780↓j
.text:080487C0                 mov     eax, [ebp+var_2C]
.text:080487C3                 mov     eax, [eax+20h]
.text:080487C6                 movzx   eax, byte ptr [eax]
.text:080487C9                 cmp     al, 71h ; 'q'
.text:080487CB                 jnz     short loc_80487FC
.text:080487CD                 mov     eax, [ebp+var_2C]
.text:080487D0                 mov     eax, [eax+18h]
.text:080487D3                 lea     edx, [eax-4]
.text:080487D6                 mov     eax, [ebp+var_2C]
.text:080487D9                 mov     [eax+18h], edx
.text:080487DC                 mov     eax, [ebp+var_2C]
.text:080487DF                 mov     eax, [eax+18h]
.text:080487E2                 mov     edx, [ebp+var_2C]
.text:080487E5                 mov     edx, [edx+20h]
.text:080487E8                 mov     edx, [edx+1]
.text:080487EB                 mov     [eax], edx
.text:080487ED                 mov     eax, [ebp+var_2C]

解密后的代码入口看起来就是一个函数了,在入口处创建一个函数名。

既然解密了,就省下自己写脚本或者动态调试了。

0x04:整理后的 main 函数

通过上面的代码,已经整理完了 main 函数,来看一下 main 函数吧。代码如下:

代码语言:javascript
复制
int main()
{
  VM *pVm; // [esp+18h] [ebp-10h]

  InitIO();
  pVm = (VM *)VmInit();
  pVm->ip = &opcode;
  smc();
  RunVm(pVm);
  return 0;
}

对于我们来说,关键就是要分析 RunVm 了,只有分析它,才能得到我们想要的值。

未完待续……!

涉及文章的资源:https://pan.quark.cn/s/fd8e76c9f4ab

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

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

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

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

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