[原創]一次美麗的誤會引發對函式呼叫保護的思考

anhkgg發表於2020-02-02

各位大佬請指正,一時思考,並未做更多詳細研究。

 

很久沒碰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月!

相關文章