
本篇是关于这个话题的最后一篇文章了,前两篇的地址如下:
0x01:上次回顾
在前面的文章中,找到了 RunVm 的函数,有些时候也叫做 Dispatch 吧,就是用来解释执行 opcode 的。同时,我们已经拿到了 0 ~ 31 个正确的字符,可以看到后面的字符算法变了,如下图所示。

可以看到,ReadBuf[32] ~ ReadBuf[35] 都赋值给了 r1,然后也都和 r9 进行了异或,其余部分的代码都没有打印输出。但是在 偏移 372 的位置程序退出了。
因此,我们接下来的工作首先要把没有分析的分支分析一下,然后再继续。
0x02:继续分析分支
所谓继续分析分支就是输出后续要执行的分支,按照上图,我们把相关的分支进行输出即可。
具体输出的代码这里就不写了,还是那些代码。输出以后编译运行,上次分析到了偏移 284 的位置,这次从偏移 289 的位置查看,结果如下:
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 版本和逆算法的代码),代码如下:
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;
}编译运行,查看输出结果:
=======>61323534可以看到,输出结果是:61323534,那么对应的字符串的值应该是 a254 了。
继续修改我们代码的输入:
strcpy(ReadBuf, "16584abc45baff901c59dde3b1bb6701a25444445555");可以看到,还有 8 个字节就解决了。
0x04:继续补全代码并测试
在我们填入了 32 ~ 35 的正确字符后,继续运行我们的模拟执行,执行情况如下:

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

继续暴力破解,仍然选择了最慢的那种算法,输出结果:
=======>62303663这个结果对应的字符串是 b06c,我们用它更新代码。
strcpy(ReadBuf, "16584abc45baff901c59dde3b1bb6701a254b06c5555");编译运行,这次运行到了偏移 548 的位置。
这次取 ReadBuf[40] ~ ReadBuf[43] 的位置了,取出以后仍然是位移,和前面一样。

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

理论上就剩四个字节要破解了,接着写一段暴力破解的代码吧(写完发现和上面那个是一样的)。输出结果如下:
=======>64633233同样,字符串是 dc23,修改最开始的模拟代码,修改如下:
strcpy(ReadBuf, "16584abc45baff901c59dde3b1bb6701a254b06cdc23");编译运行,如下图:

可以看到程序这次正常退出了。看一下 0x99 的分支:
else if ( *(_BYTE *)pvm->ip == 0x99 )
{
printf("exit(0)");
break;
}可以看到,里面只有一个 break,也就是跳出了我们 while 循环。
那么,最后,我们要找的字符串就是:
16584abc45baff901c59dde3b1bb6701a254b06cdc23整个分析完成了!
0x05:总结
商业化的 VM 保护要比这个复杂很多,这就是开胃的小菜,就像是玩真人 CS 和真正上战场的区别,是不可以相提并论的。这个 VM 更像是把流程打乱了,依靠 opcode 重建了流程的顺序。所以,只要我们自己跑流程就可以还原它流程的过程,好在它的算法不复杂,所以完成的还是比较顺利的。
涉及文章的资源:https://pan.quark.cn/s/fd8e76c9f4ab