VMP加壳(一):代码混淆原理

  1、软件的逆向、外挂、破解等,本质上是想办法改变原有代码的执行路径,主要的方式有两种:

  •   改变某些关键数据,比如函数参数(android下有xpose、frida等现成的hook框架,逆向人员只需要找准hook的点就完成了90%的逆向工作)、某些全局变量(比如VX防止多开的mutex);改变这些数据后,会导致原来的if条件走另一个分支,达到改变执行流程的目的;
  •        改变某些关键代码,尤其是涉及到代码跳转的指令,最常见的就是jmp、call,比如改成NOP;或把JE改成JNE等相反的逻辑;

       外挂、软件破解等无不是从这方面入口去寻找关键的call、jmp或数据。为了保护自己的关键代码不被轻易找到并更改,衍生出了很多的防护措施,常见的反调试、代码加密(异或代码,等到执行前才解密,windows下的PG保护就是这么干的)、加壳干扰等;

      先看一段简单的代码:用户输入字符串,然后和内置的字符串做对比,一样就输出ok,不一样就输出fail;这种比对的逻辑一般都用来做密码、license等的判断。稍微懂点底层安全的人都能看出这段代码的两个问题:

    (1)字符串比对这里:从左向右逐个比对,一旦发现有一个不匹配立即返回false;侧信道攻击就可以根据返回时间判断前面的字符是不是正确的。防御的方式也不复杂,比如两个字符串统一用MD5转换后再比对。或则不要单个字节对比,而是把字符串按照4个字节均分成N等份,每份32bit,转换成int型后比较整数的大小,看看是不是一样的;

    (2)main函数里面的if判断逻辑;

  

      反汇编代码如下:00381A9A处有个je,很明显这里的正常逻辑是:eax等于0吗?如果是就跳转到00381AAB这里打印fail,否则继续执行打印ok的代码;

      整个过程的代码逻辑清晰明了,但这也产生了另一个问题:这种代码随便用ODIDAX32DBG就能反汇编,届时改一下关键处的代码(比如00381A9A的je改成jne),或则把对比函数的返回值从0改成1(返回值默认保存在eax寄存器),这种密码、license校验就完全失效了。那么问题来了,该怎么保护了这些关键代码了?

while (1)
00381A68 B8 01 00 00 00       mov         eax,1  
00381A6D 85 C0                test        eax,eax  
00381A6F 74 49                je          main+7Ah (0381ABAh)  
    {
        scanf("%s", buf);
00381A71 68 38 A1 38 00       push        offset buf (038A138h)  
00381A76 68 30 7B 38 00       push        offset string "%s" (0387B30h)  
00381A7B E8 B7 F5 FF FF       call        _scanf (0381037h)  
00381A80 83 C4 08             add         esp,8  
        if (mystrcmp(buf, "123"))
00381A83 68 34 7B 38 00       push        offset string "123" (0387B34h)  
00381A88 68 38 A1 38 00       push        offset buf (038A138h)  
00381A8D E8 AE F6 FF FF       call        mystrcmp (0381140h)  
00381A92 83 C4 08             add         esp,8  
00381A95 0F B6 C0             movzx       eax,al  
00381A98 85 C0                test        eax,eax  
00381A9A 74 0F                je          main+6Bh (0381AABh)  
            printf("ok
");
00381A9C 68 38 7B 38 00       push        offset string "ok
" (0387B38h)  
00381AA1 E8 31 F6 FF FF       call        _printf (03810D7h)  
00381AA6 83 C4 04             add         esp,4  
00381AA9 EB 0D                jmp         main+78h (0381AB8h)  
        else
            printf("fail
");
00381AAB 68 3C 7B 38 00       push        offset string "fail
" (0387B3Ch)  
00381AB0 E8 22 F6 FF FF       call        _printf (03810D7h)  
00381AB5 83 C4 04             add         esp,4  
    }
00381AB8 EB AE                jmp         main+28h (0381A68h)  
    system("pause");
00381ABA 8B F4                mov         esi,esp  
00381ABC 68 44 7B 38 00       push        offset string "pause" (0387B44h)  
00381AC1 FF 15 70 B1 38 00    call        dword ptr [__imp__system (038B170h)]  
00381AC7 83 C4 04             add         esp,4  
00381ACA 3B F4                cmp         esi,esp  
00381ACC E8 7D F7 FF FF       call        __RTC_CheckEsp (038124Eh)  
}
00381AD1 5F                   pop         edi  
00381AD2 5E                   pop         esi  

  2、保护方式原理也不复杂:见招拆招,既然逆向人员大都通过IDAODX32DBG等工具反编译后调试汇编代码,那防御人员是不是可以在汇编代码层面考虑干扰方式了?现在来看看另一种比对方式:

#include <iostream>
#include <stdlib.h>
#include <Windows.h>

#define IS_ZERO(x) (1-((x)|(-x))>>31)

typedef void (*CALL)();

void f1() {
    printf("ok
");
}

void f2() {
    printf("fail
");
}
/*
如果a==b调用f1,否则调用f2,不能出现jcc指令
*/
void nojcc(int a, int b, CALL f1, CALL f2) {
    _asm {
        mov eax, a
        sub eax, b; 如果结果是0,ZF = 1,否则相反;这里也可以用CMP,只不过不把结果写回eax

        pushfd
        pop eax; eflags存入eax

        and eax, 0x40; 0x40 = 1000000取eflags的ZF位
        shr eax, 6; ZF位放末尾
        mov ecx, 1
        sub ecx, eax; 如果ZF = 1,那么ecx = 0,否则ecx = 1.总之ecx和ZF刚好相反
                    ; 注意:这里的两个数相减,会继续影响ZF。比如ecx=0,会让ZF=1,这刚好和开始的ZF保持一致;同时这里的ZF已经不用了,不影响最后的结果
        neg eax; 按位取反后加1.如果ZF = 1,那么eax = 0xFFFFFFFF;如果ZF = 0,那么eax = 0x100000000(注意最高位溢出了,实际上eax = 0)
        neg ecx; 按位取反后加1.如果ZF = 1,那么ecx = 0xFFFFFFFF;如果ZF = 0,那么ecx = 0x100000000(注意最高位溢出了,实际上ecx = 0)

        and eax, f1; 如果eax = 0,那么f1就不保留,执行f2
        and ecx, f2; 如果ecx = 0,那么f2就不保留,执行f1

        or eax, ecx; 上面在f1和f2已经二选一了(只保留1个,另一个是0),这里综合一下结果
        call eax
    }
}

int main()
{
    nojcc(1,2,f1,f2);
    system("pause");
}

  先看看效果:nojcc函数前面的两个参数不等,那么打印fail,否则打印ok;

    

   老规矩:重要的逻辑都在注释了。这里大致解释一下逻辑:

  •   既然两个数要比较,常见的就是CMP了。但这个很容易被逆向人员发现,这里用sub替代,隐蔽性更好;
  •        两个数相减,怎么知道结果了?看eflgas的ZF位呗;
  •        最后有f1和f2两个函数,也就是有2个分支,应该选哪一个了?因为sub ecx,eax,所以ecx和eax有一个必定是0,这就是个很好的二选一“开关”,所以先neg一下,让两个数分别是0xFFFFFFFF和0x00000000,再分别和两个地址and,这时只有一个分支的地址不为0了,最后or一下,得到最终的跳转地址,完美撒花!

      整个过程完全没用到CMP、JE、JNE等跳转指令,并且用了and、or、shr等操作,给人第一感觉是用来加解密的!整个过程需要逆向人员逐步调试每行指令才有可能理解背后的逻辑,安全性大幅提升!

     3、接着用专业的加壳工具VMP加壳;这里把第一个原始的exe用VMP针对entryPoint加壳,结果对比如下:

  (1)体积:从40K膨胀到了584K,翻了14倍;

      

       增加的部分集中在这了:vmp的段,一共有89*4KB=356KB;

       

  (2)再看看入口,已经面目全非;第一行代码就是跳转;

       

  跟着刚才的jmp语句,追踪到这里:先是push一个magic number,再通过call继续跳转,这是典型进VM的标志特征!

      

  继续追踪:发现很多“莫名其妙”的代码,函数的风格和VS完全不同(32位最常见的风格就是push ebp,mov ebp,esp开头),VM加壳后完全变了

      

 注意事项:

         1、这里用了scanf函数接受用户的输入,这个函数比较久远了,貌似没有边界检查,容易导致栈溢出攻击,不安全,所以VS2019默认是编译不通过的,这里要做如下设置才行;

       

       2、这次知道为啥VX这种国民级别的APP为啥至今不加壳了,加壳的“坏处”:

     (1)壳的特征也很明显,比如“内存布局”中能找到vmp的段;

     (2)exe的体积增加几十倍

     (3)增加了大量的混淆指令,降低了运行效率;增加的这些混淆指令还有可能导致原APP逻辑出错

     (4)VMP会不会在加壳的同时留后门了? VX这种装机量几十亿的APP,一旦官方发布的exe被装上了病毒/木马,后果不堪设想!

 参考:1、https://www.bilibili.com/video/BV1oT4y1K7Vi  VM保护攻防

 VMP版本:3.5.0,在吾爱破解的网盘上下载的;

原文地址:https://www.cnblogs.com/theseventhson/p/14224501.html