很久没碰 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
的模块中找一个不用(零值)的内存,用来保护临时指令,不细讲了,大家自行探索吧。
(完)
1 zhensjoke 2019-09-28 10:24:57 +08:00 我就服会汇编的人。 |
![]() | 2 Chaos11 2019-09-28 10:40:52 +08:00 翻了下 po 主 blog,有点东西 |
![]() | 3 ech0x 2019-09-28 12:23:51 +08:00 via iPhone 这个有点意思,但是这样做没有法律风险吗? |
![]() | 4 May725 2019-09-28 12:36:07 +08:00 via iPhone 硬核 hack |
5 meeken 2019-09-28 12:40:59 +08:00 via iPhone 这边都是 po 主的帖子,太硬核了爱了爱了 |
![]() | 6 janxin 2019-09-28 22:29:21 +08:00 新版本的微信都加 vmp 了? |
7 Chenamy2017 2019-09-29 09:20:37 +08:00 Orz |
8 azcvcza 2019-09-29 10:03:03 +08:00 好 HACK |