IMCAFS

Home

a beautiful misunderstanding leads to the thinking of function call protection

Posted by lipsius at 2020-04-03
all

I haven't touched Wx for a long time. I wanted to write something recently, so I picked it up again. The latest version is 2.6.8.65 (now it is 2.6.8.68).

Find the previously analyzed send text message interface, and find that the function changes greatly, which is a very obvious VM trace.

.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

At that time, I didn't care about it. After careful observation, the interface parameters didn't change, so I used them directly.

It was found that the interface could not be used and the text message was not sent successfully.

Wipe, is there any mystery hidden in VM, which protects against function calls??

...

It's OK to retest sending messages to others while preparing for a big job.

This is a beautiful misunderstanding. During the test, I sent a message to my wechat, and it turned out that the interface could not be sent to myself, so it failed.

...

Then, let's continue to talk about the call prevention protection that Wx previously thought might do in functions.

Prevention

According to the thought of preventing others from calling functions, it is actually to check the call source, so it must start from the call stack:

Backtrack the call stack within the function and check the return address

If the return address is wechat module, it will be called normally. Otherwise, it will be rejected

You may check one layer (wechatwin. DLL), or multiple layers

It is possible to detect that the return address is within the scope of the module, or the exact return address

VM related logic increases analysis difficulty

The implementation code is as follows:

void TestAntiCall(DWORD a1)
{
//vmstart
DWORD retAddr = *((DWORD*)((char*)&a1 - 4));//
if(retAddr > wxModuleBase && retAddr < wxModuleEnd) {
//do things
} else {
//anti
//do nothing
}
//vmend
}

attack

So the way to counter this is to modify the return address of the call stack when calling testanticall, so that testanticall mistakenly thinks it is a normal call.

The analysis here only considers checking the return address of one layer.

For example, the following normal calling code, 00003 is the return address, which can be called normally in the legal module.

//正常调用代码
void Right_TestAntiCall()
{
00001 push a1
00002 call TestAntiCall
00003 add esp, 4
}

My call to testanticall function (in my module) is as follows: add ESP, 4; the return address obtained for testanticall must be in my module, and the call failed.

add esp, 4; pfnTestAntiCall = 原始TestAntiCall地址;
pfnTestAntiCall_RetAddr = 000003;//调用TestAntiCall返回地址
//这个会失败
void MyTestAntiCall(DWORD a1)
{
__asm {
push a1;
call pfnTestAntiCall;
add esp, 4; //返回地址
}
}

Then we try to cheat testanticall. We modify the return address of the call stack (which should have been myretaddr).

TestAntiCall

The normal call is replaced by push + JMP, so that the return address is pushed in by ourselves. Here, the return address of the normal call, G ﹣ sendtextmsgretaddr, is pushed in.

push+jmp call g_SendTextMsgRetAddr //这个会成功
void MyTestAntiCall(DWORD a1)
{
__asm {
push a1;
push g_SendTextMsgRetAddr;//压入原始retaddr
jmp pfnWxSendTextMsg; //调用函数,这样函数内部检测就是正常的
add esp, 4; //MyRetAddr
}
}

Of course, such a simple call is bound to cause problems, because after JMP pfnwxsendextmsg, it will return to 00003 of right'testanticall, which obviously leads to stack damage and crash.

jmp pfnWxSendTextMsg Right_TestAntiCall 00003

So in order for the program to execute normally, two more processing steps are needed.

Right'testanticall was modified to JMP myretaddr at 00003.

Right_TestAntiCall

Return execution flow back to mytestanti Call1

Restore 00003 original instructions.

//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;
}
}

//2. Restore 00003 original instructions. void fakeAntiTestCall1(DWORD retaddr2, char OrigCode[5]){ Patch((PVOID)retaddr2, 5, OrigCode);}

//This will succeed in void mytestanticall (DWORD A1) {DWORD myretaddr = 0; char origcode [5] = {0}; \\; //Jump in myretaddr push A1; push g ﹣ sendtextmsgretaddr; / / press in the original retaddr JMP pfnwxsend textmsg; / / call the function at the original g ﹣ sendtextmsgretaddr, origcode; push eax; push g ﹣ sendtextmsgretaddr; Call fakeantitestcall1; / / recover the data ret; ret1: call init; NOP;}}}

To get the address of myretaddr, call + pop is used, as follows:

__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
}

The retaddr obtained above is obviously not the same as the myretaddr, so subtract an offset of 24 from the fakeantitestcall to get the myretaddr.

fakeAntiTestCall MyRetAddr

The offset value can be calculated as 10024e1e - 10024e06 = 24 by the following bytecode.

10024E1E 10024E06 .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

So a better way is to find a memory in the right'testanticall module that is not used (zero value) to protect temporary instructions. Let's explore it by ourselves.

Right_TestAntiCall

This article is only pseudo code thinking, no actual verification, but it should not be a big problem (escape)

(end)