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

发布者:anhkgg
发布于:2020-02-02 15:04

各位大佬请指正,一时思考,并未做更多详细研究。

 

很久没碰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的模块中找一个不用(零值)的内存,用来保护临时指令,不细讲了,大家自行探索吧。

 

(完)

 

欢迎关注:汉客儿



2020安全开发者峰会(2020 SDC)议题征集 中国.北京 7月!


声明:该文观点仅代表作者本人,转载请注明来自看雪