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