V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
anhkgg
V2EX  ›  程序员

一次美丽的误会引发对函数调用保护的思考

  •  
  •   anhkgg ·
    anhkgg · 2019-09-28 10:08:52 +08:00 · 2627 次点击
    这是一个创建于 1912 天前的主题,其中的信息可能已经有所发展或是发生改变。

    很久没碰 wx 了,最近想写个东西,就重新拿了起来,最新版本 2.6.8.65 (此时已经 2.6.8.68 )。

    找到以前分析过的发送文本消息接口,发现函数大变样,很明显的 vm 痕迹。

    .vmp0:1131CE33 000                 push    2493AC03h
    .vmp0:1131CE38 004                 call    sub_1134AEB3
    .vmp0:1131CE3D 000                 mov     cx, [ebp+0]
    .vmp0:1131CE42 000                 test    bp, 373Dh
    .vmp0:1131CE47 000                 shl     ah, cl
    .vmp0:1131CE49 000                 mov     dx, [ebp+2]
    .vmp0:1131CE4E 000                 cmovnb  eax, edi
    .vmp0:1131CE51 000                 lea     ebp, [ebp-2]
    ...
    .vmp0:1131CE9C                     bswap   eax
    .vmp0:1131CE9E                     inc     eax
    

    当时也没在意,仔细看接口参数并没有变化,就直接拿来用了。

    结果发现接口不能用了,并没有成功发送文本信息。

    擦,难道 vm 里面藏了什么玄机,做了防止函数调用的保护??

    ...

    正整备大干一场的时候,重新测试给别人发送消息是 ok 的。

    这是一次美丽的误会,测试时是给自己的微信发送消息,结果证明该接口是不能给自己发的,所以没成功。

    ...

    然后就继续说说先前自以为的 wx 在函数中可能做的防止调用的保护吧。

    按照自己思考的防止别人调用函数的思路,其实就是检查调用源,那么肯定是从调用栈入手:

    1. 在函数内部回溯调用堆栈,检查返回地址
    2. 返回地址为微信模块则正常调用,否则拒绝执行
    3. 可能检查一层( wechatwin.dll ),或者多层
    4. 可能检测返回地址在模块范围,或者是准确的返回地址
    5. vm 相关逻辑,增加分析难度

    大概实现代码就是:

    void TestAntiCall(DWORD a1)
    {
    //vmstart
        DWORD retAddr = *((DWORD*)((char*)&a1 - 4));//
        if(retAddr > wxModuleBase && retAddr < wxModuleEnd) {
          //do things
        } else {
           //anti
          //do nothing
        }
    //vmend
    }
    

    所以能够想到的对抗方式就是在调用 TestAntiCall 的时候,修改调用栈返回地址,让 TestAntiCall 误以为确实是正常调用。

    这里分析只考虑检查一层返回地址。

    比如如下正常调用代码,00003 就是返回地址,在合法模块内,即可正常调用。

    //正常调用代码
    void Right_TestAntiCall()
    {
    00001 push a1
    00002 call TestAntiCall
    00003 add esp, 4
    }
    

    而我的调用 TestAntiCall 函数(在我的模块内)如下,add esp, 4;为 TestAntiCall 拿到的返回地址,这个地址肯定在我的模块内,调用失败。

    pfnTestAntiCall = 原始 TestAntiCall 地址;
    pfnTestAntiCall_RetAddr = 000003;//调用 TestAntiCall 返回地址
    //这个会失败
    void MyTestAntiCall(DWORD a1)
    {
     __asm {
        push a1;
        call pfnTestAntiCall;
        add esp, 4; //返回地址
      }
    }
    

    然后尝试欺骗TestAntiCall,我们修改一下调用栈的返回地址(本来应该是 MyRetAddr )。

    通过push+jmp来替换通常的call,这样返回地址由我们自己压入,这里压入正常调用的返回地址g_SendTextMsgRetAddr

    //这个会成功
    void MyTestAntiCall(DWORD a1)
    {
        __asm {
            push a1;
            push g_SendTextMsgRetAddr;//压入原始 retaddr
            jmp pfnWxSendTextMsg; //调用函数,这样函数内部检测就是正常的
            add esp, 4; //MyRetAddr
        }
    }
    

    当然,就这么简单的调用,肯定会出问题的,因为jmp pfnWxSendTextMsg之后,就会返回到Right_TestAntiCall00003,如此显然导致栈破坏,会出现崩溃。

    所以为了让程序正常执行,还需要多两个处理步骤。

    1. Right_TestAntiCall的 00003 处修改指令为 jmp MyRetAddr。让执行流返回到 MyTestAntiCall1
    2. 恢复 00003 处原始指令。
    //1. `Right_TestAntiCall`的 00003 处修改指令为 jmp MyRetAddr。让执行流返回到 MyTestAntiCall1
    void fakeAntiTestCall(DWORD retaddr1, DWORD retaddr2, char OrigCode[5])
    {
        DWORD MyRetAddr = retaddr1 - 24;
        DWORD ShellCode[5] = { 0xe9, 0x00, 0x00, 0x00, 0x00 };
        *((DWORD*)(&ShellCode[1])) = MyRetAddr;
        memcpy(OrigCode, (char*)retaddr2, 5);
        Patch((PVOID)retaddr2, 5, ShellCode);
    }
    
    //2. 恢复 00003 处原始指令。
    void fakeAntiTestCall1(DWORD retaddr2, char OrigCode[5])
    {
        Patch((PVOID)retaddr2, 5, OrigCode);
    }
    
    //这个会成功
    void MyTestAntiCall(DWORD a1)
    {
        DWORD MyRetAddr = 0;
        char OrigCode[5] = { 0 };
        __asm {
            jmp RET1;
        INIT:
            pop eax;//retAddr
            mov MyRetAddr, eax;
            lea eax, OrigCode;
            push eax;
            push g_SendTextMsgRetAddr;
            push MyRetAddr;
            call fakeAntiTestCall; //在原始 g_SendTextMsgRetAddr 处跳入 MyTestAntiCall1 的 MyRetAddr
            push a1;
            push g_SendTextMsgRetAddr;//压入原始 retaddr
            jmp pfnWxSendTextMsg; //调用函数,这样函数内部检测就是正常的
            add esp, 4; //MyRetAddr
            lea eax, OrigCode;
            push eax;
            push g_SendTextMsgRetAddr;
            call fakeAntiTestCall1;//恢复 g_SendTextMsgRetAddr 数据
            ret;
        RET1:
            call INIT;
            nop;
        }
    }
    

    为了拿到 MyRetAddr 的地址,通过 call+pop 的方法完成,如下:

    __asm {
        jmp RET1:
        WORK:
            pop eax; //eax = retaddr
            mov retaddr, eax;
            //do thing
            add esp, 4;//MyRetAddr
        RET1:
            call WORK;//push retaddr; jmp WORK;
            nop;//retaddr
    }
    

    上面拿到 retaddr 和 MyRetAddr 明显不是同一个,所以在fakeAntiTestCall中减去一个偏移 24 拿到MyRetAddr

    偏移值通过下面的字节码可以计算出来10024E1E - 10024E06 = 24。

    .text:10024DDF EB 37                             jmp     short RET1
    .text:10024DE1                   INIT:   
    .text:10024DE1 58                                pop     eax
    .text:10024DE2 89 45 F4                          mov     MyRetAddr, eax
    .text:10024DE5 8D 45 F8                          lea     eax, OrigCode
    .text:10024DE8 50                                push    eax
    .text:10024DE9 FF 35 00 D0 25 10                 push    pfnTestAntiCall_RetAddr
    .text:10024DEF FF 75 F4                          push    MyRetAddr
    .text:10024DF2 E8 C9 00 00 00                    call    fakeAntiTestCall; 
    .text:10024DF7 FF 75 E0                          push    a1
    .text:10024DFA FF 35 00 D0 25 10                 push    pfnTestAntiCall_RetAddr
    .text:10024E00 FF 25 D4 A4 28 10                 jmp     pfnTestAntiCall; 
    .text:10024E06 83 C4 04                          add     esp, 4
    .text:10024E09 8D 45 F8                          lea     eax, OrigCode
    .text:10024E0C 50                                push    eax
    .text:10024E0D FF 35 00 D0 25 10                 push    MyRetAddr
    .text:10024E13 E8 88 00 00 00                    call    fakeAntiTestCall1; 
    .text:10024E14 C3                                ret;
    .text:10024E19
    .text:10024E19                   RET1:    
    .text:10024E19 E8 C4 FF FF FF                    call    INIT
    .text:10024E1E 90                                nop
    

    如此可以正常完成一次调用,但是还有问题,因为会反复修改Right_TestAntiCall的指令,可能在多线程中执行时出现问题。

    所以更好的方法时在Right_TestAntiCall的模块中找一个不用(零值)的内存,用来保护临时指令,不细讲了,大家自行探索吧。

    (完)

    8 条回复    2019-09-29 10:03:03 +08:00
    zhensjoke
        1
    zhensjoke  
       2019-09-28 10:24:57 +08:00
    我就服会汇编的人。
    Chaos11
        2
    Chaos11  
       2019-09-28 10:40:52 +08:00
    翻了下 po 主 blog,有点东西
    ech0x
        3
    ech0x  
       2019-09-28 12:23:51 +08:00 via iPhone
    这个有点意思欸,但是这样做没有法律风险吗?
    May725
        4
    May725  
       2019-09-28 12:36:07 +08:00 via iPhone
    硬核 hack👍
    meeken
        5
    meeken  
       2019-09-28 12:40:59 +08:00 via iPhone
    这边都是 po 主的帖子,太硬核了爱了爱了
    janxin
        6
    janxin  
       2019-09-28 22:29:21 +08:00
    新版本的微信都加 vmp 了?
    Chenamy2017
        7
    Chenamy2017  
       2019-09-29 09:20:37 +08:00
    Orz
    azcvcza
        8
    azcvcza  
       2019-09-29 10:03:03 +08:00
    好 HACK
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   986 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 18:53 · PVG 02:53 · LAX 10:53 · JFK 13:53
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.