一次美麗的誤會引發對函數調用保護的思考

很久沒碰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的模塊中找一個不用(零值)的內存,用來保護臨時指令,不細講了,大家自行探索吧。

(完)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章