很久沒碰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在函數中可能做的防止調用的保護吧。
防
按照自己思考的防止別人調用函數的思路,其實就是檢查調用源,那麼肯定是從調用棧入手:
- 在函數內部回溯調用堆棧,檢查返回地址
- 返回地址爲微信模塊則正常調用,否則拒絕執行
- 可能檢查一層(wechatwin.dll),或者多層
- 可能檢測返回地址在模塊範圍,或者是準確的返回地址
- 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_TestAntiCall
的00003
,如此顯然導致棧破壞,會出現崩潰。
所以爲了讓程序正常執行,還需要多兩個處理步驟。
Right_TestAntiCall
的00003處修改指令爲jmp MyRetAddr。讓執行流返回到MyTestAntiCall1- 恢復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
的模塊中找一個不用(零值)的內存,用來保護臨時指令,不細講了,大家自行探索吧。
(完)