各位大佬请指正,一时思考,并未做更多详细研究。
很久没碰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在函数中可能做的防止调用的保护吧。
按照自己思考的防止别人调用函数的思路,其实就是检查调用源,那么肯定是从调用栈入手:
大概实现代码就是:
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//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
的模块中找一个不用(零值)的内存,用来保护临时指令,不细讲了,大家自行探索吧。
(完)
欢迎关注:汉客儿