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

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

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

本篇是关于这个话题的最后一篇文章了,前两篇的地址如下:

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

0x01:上次回顾

在前面的文章中,找到了 RunVm 的函数,有些时候也叫做 Dispatch 吧,就是用来解释执行 opcode 的。同时,我们已经拿到了 0 ~ 31 个正确的字符,可以看到后面的字符算法变了,如下图所示。

可以看到,ReadBuf[32] ~ ReadBuf[35] 都赋值给了 r1,然后也都和 r9 进行了异或,其余部分的代码都没有打印输出。但是在 偏移 372 的位置程序退出了。

因此,我们接下来的工作首先要把没有分析的分支分析一下,然后再继续。

0x02:继续分析分支

所谓继续分析分支就是输出后续要执行的分支,按照上图,我们把相关的分支进行输出即可。

具体输出的代码这里就不写了,还是那些代码。输出以后编译运行,上次分析到了偏移 284 的位置,这次从偏移 289 的位置查看,结果如下:

代码语言:javascript
复制
289:  =>c1:  r1 = (unsigned __int8)ReadBuf[32]; // (r1=30, (unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)]=52)
291:  =>80:  *(&pvm->r0 + sub_804875F(pvm, 1)) = *(_DWORD *)(pvm->ip + 2); // (sub_804875F(pvm, 1)=2, *(_DWORD *)(pvm->ip + 2)=24)
297:  =>23:  r1 <<= r2; // (r1=52, r2=24)
298:  =>10:  r9 = r1;  // (r9=47, r1=872415232)
299:  =>c1:  r1 = (unsigned __int8)ReadBuf[33]; // (r1=872415232, (unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)]=52)
301:  =>80:  *(&pvm->r0 + sub_804875F(pvm, 1)) = *(_DWORD *)(pvm->ip + 2); // (sub_804875F(pvm, 1)=2, *(_DWORD *)(pvm->ip + 2)=16)
307:  =>23:  r1 <<= r2; // (r1=52, r2=16)
308:  =>f7:  r9 += r1; // (r9=872415232, r1=3407872)
309:  =>c1:  r1 = (unsigned __int8)ReadBuf[34]; // (r1=3407872, (unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)]=52)
311:  =>80:  *(&pvm->r0 + sub_804875F(pvm, 1)) = *(_DWORD *)(pvm->ip + 2); // (sub_804875F(pvm, 1)=2, *(_DWORD *)(pvm->ip + 2)=8)
317:  =>23:  r1 <<= r2; // (r1=52, r2=8)
318:  =>f7:  r9 += r1; // (r9=875823104, r1=13312)
319:  =>c1:  r1 = (unsigned __int8)ReadBuf[35]; // (r1=13312, (unsigned __int8)ReadBuf[*(unsigned __int8 *)(pvm->ip + 1)]=52)
321:  =>f7:  r9 += r1; // (r9=875836416, r1=52)
322:  =>fe:  r1 = r9; (r1=52, r9=875836468)
323:  =>80:  *(&pvm->r0 + sub_804875F(pvm, 1)) = *(_DWORD *)(pvm->ip + 2); // (sub_804875F(pvm, 1)=2, *(_DWORD *)(pvm->ip + 2)=5)
329:  =>22:  r1 >>= r2;  // (r1=875836468, r2=5)
330:  =>77:  r1 ^= r9; // (r1=27369889, r9=875836468)
331:  =>10:  r9 = r1;  // (r9=875836468, r1=898995605)
332:  =>80:  *(&pvm->r0 + sub_804875F(pvm, 1)) = *(_DWORD *)(pvm->ip + 2); // (sub_804875F(pvm, 1)=2, *(_DWORD *)(pvm->ip + 2)=7)
338:  =>23:  r1 <<= r2; // (r1=898995605, r2=7)
339:  =>80:  *(&pvm->r0 + sub_804875F(pvm, 1)) = *(_DWORD *)(pvm->ip + 2); // (sub_804875F(pvm, 1)=2, *(_DWORD *)(pvm->ip + 2)=-1729005789)
345:  =>31:  r1 &= r2;  // (r1=-892679552, r2=-1729005789)
346:  =>77:  r1 ^= r9; // (r1=-2000666112, r9=898995605)
347:  =>10:  r9 = r1;  // (r9=898995605, r1=-1118447723)
348:  =>80:  *(&pvm->r0 + sub_804875F(pvm, 1)) = *(_DWORD *)(pvm->ip + 2); // (sub_804875F(pvm, 1)=2, *(_DWORD *)(pvm->ip + 2)=24)
354:  =>23:  r1 <<= r2; // (r1=-1118447723, r2=24)
355:  =>80:  *(&pvm->r0 + sub_804875F(pvm, 1)) = *(_DWORD *)(pvm->ip + 2); // (sub_804875F(pvm, 1)=2, *(_DWORD *)(pvm->ip + 2)=904182048)
361:  =>31:  r1 &= r2;  // (r1=-1795162112, r2=904182048)
362:  =>77:  r1 ^= r9; // (r1=352321536, r9=-1118447723)
363:  =>10:  r9 = r1;  // (r9=-1118447723, r1=-1470769259)
364:  =>80:  *(&pvm->r0 + sub_804875F(pvm, 1)) = *(_DWORD *)(pvm->ip + 2); // (sub_804875F(pvm, 1)=2, *(_DWORD *)(pvm->ip + 2)=18)
370:  =>22:  r1 >>= r2;  // (r1=-1470769259, r2=18)
371:  =>77:  r1 ^= r9; // (r1=10773, r9=-1470769259)
372:  =>a0:  if ( pvm->r1 != 1877735783 ) exit(0); // (r1=-1470759552, 1877735783=6febf967)

截图如下:

上图中有三种颜色,最开始是红色部分,逐个取出字符,然后进行位移,然后相加,位移位数分别是 24、16、8,也就是把 4 个 1 字节组合成了 1 个 4 字节的值,先取的在高位,后取的在低位。然后是橙色部分,这部分是把合并的 4 字节的值进行移位、异或相关的操作。最后是蓝色的值,也就是经过上面的运算以后,结果要和蓝色框住的值相等。

0x03:处理 ReadBuf[32] ~ ReadBuf[35]

上面的算法虽然是可逆的,但是代码量不多就不逆了,暴力破解吧!

暴力破解可以使用 z3,也可以不用 z3,我这里选择了不用 z3(z3 的性能高一些,如果是写算法的逆运算,效率比爆破的效率高一些,下面这段代码是性能最差的一段代码,附件中给出了 z3 版本和逆算法的代码),代码如下:

代码语言:javascript
复制
int main()
{
    for (unsigned int r9 = 0x21212121; r9 <=0x7E7E7E7E; r9 ++)
    {
        unsigned int tmp = r9;
        unsigned int r1 = r9;

        r1 >>= 5;
        r1 ^= r9;

        r9 = r1;
        r1 <<= 7;
        r1 &= -1729005789;
        r1 ^= r9;

        r9 = r1;
        r1 <<= 24;
        r1 &= 904182048;
        r1 ^= r9;

        r9 = r1;
        r1 >>= 18;
        r1 ^= r9;

        if (r1 == 0x6febf967)
        {
            printf("=======>%x\r\n", tmp);
            break;
        }

        r9 = tmp;
    }

    return 0;
}

编译运行,查看输出结果:

代码语言:javascript
复制
=======>61323534

可以看到,输出结果是:61323534,那么对应的字符串的值应该是 a254 了。

继续修改我们代码的输入:

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

可以看到,还有 8 个字节就解决了。

0x04:继续补全代码并测试

在我们填入了 32 ~ 35 的正确字符后,继续运行我们的模拟执行,执行情况如下:

可以看到,这次去了 36 ~ 39 四个字符,但是同样是将 4 个 1 字节拼成了 1 个 4 字节,但是后续的算法和之前的是不一样的,这里补齐代码后进行分析。

处理后的代码如下:

继续暴力破解,仍然选择了最慢的那种算法,输出结果:

代码语言:javascript
复制
=======>62303663

这个结果对应的字符串是 b06c,我们用它更新代码。

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

编译运行,这次运行到了偏移 548 的位置。

这次取 ReadBuf[40] ~ ReadBuf[43] 的位置了,取出以后仍然是位移,和前面一样。

后面的算法就不一样了,但是这次比较长。

理论上就剩四个字节要破解了,接着写一段暴力破解的代码吧(写完发现和上面那个是一样的)。输出结果如下:

代码语言:javascript
复制
=======>64633233

同样,字符串是 dc23,修改最开始的模拟代码,修改如下:

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

编译运行,如下图:

可以看到程序这次正常退出了。看一下 0x99 的分支:

代码语言:javascript
复制
else if ( *(_BYTE *)pvm->ip == 0x99 )
{
    printf("exit(0)");
    break;
}

可以看到,里面只有一个 break,也就是跳出了我们 while 循环。

那么,最后,我们要找的字符串就是:

代码语言:javascript
复制
16584abc45baff901c59dde3b1bb6701a254b06cdc23

整个分析完成了!

0x05:总结

商业化的 VM 保护要比这个复杂很多,这就是开胃的小菜,就像是玩真人 CS 和真正上战场的区别,是不可以相提并论的。这个 VM 更像是把流程打乱了,依靠 opcode 重建了流程的顺序。所以,只要我们自己跑流程就可以还原它流程的过程,好在它的算法不复杂,所以完成的还是比较顺利的。

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

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

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

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

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

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