前面很多注入相关的文章中都提到为了保证注入后原始程序能恢复正常的执行流,需要在编译器中关闭堆栈检查。为了解决问题,这是个好手段,但是不得不说这是回避问题,不是根本上解决问题。本文旨在解决这个问题。
vs用 __chkesp来实现堆栈检查。__chkesp顾名思义,检查esp的值,检查失败就抱错。什么时候esp会出错?情况很多,如果排除缓冲区溢出的可能,那还有堆栈失衡的情况。比如不正常的退出函数方式。我经常遇到的不正常退出函数引发_chkesp失败一般都发生在修改堆栈上保存的返回地址,使之指向某个裸函数,然后从裸函数执行退出(或者跳转)之后。
要绕过__chkesp检查,首先要知道怎样的函数调用方式会引起堆栈检查。调用Windows提供的API后编译都会安排一段__chkesp,另外在"直接调用地址"返回后,也会被插入这段代码。那么什么是"直接调用地址"?来看一段代码:
typedef int (*funcAddAddress)(int,int);
int add(int a,int b)
{
return a+b;
}
int _tmain(int argc, _TCHAR* argv[])
{
funcAddAddress funcAddAddressPtr = (funcAddAddress)add;
add(1,1); //(1)
printf("======\n");
(*funcAddAddressPtr)(1,1); //(2)
return 0;
}
对同一个函数进行调用,(1)是普通调用方式,不会产生__chkesp,而(2)是我所谓的"直接调用地址"方式,函数返回后会被插入__chkesp。下面用反汇编代码验证一下这个说法:
funcAddAddress funcAddAddressPtr = (funcAddAddress)add;
013513EE mov dword ptr [funcAddAddressPtr],offset add (1351091h)
add(1,1);
013513F5 push 1
013513F7 push 1
013513F9 call add (1351091h)
013513FE add esp,8
printf("======\n");
01351401 mov esi,esp
01351403 push offset string "======\n" (135573Ch)
01351408 call dword ptr [__imp__printf (13582B0h)]
0135140E add esp,4
01351411 cmp esi,esp
01351413 call @ILT+310(__RTC_CheckEsp) (135113Bh)
(*funcAddAddressPtr)(1,1);
01351418 mov esi,esp
0135141A push 1
0135141C push 1
0135141E call dword ptr [funcAddAddressPtr]
01351421 add esp,8
01351424 cmp esi,esp
01351426 call @ILT+310(__RTC_CheckEsp) (135113Bh)
return 0;
0135142B xor eax,eax
此处补发一个相关的pdf:
产生__chkesp的函数调用方式
既然知道了编译器会在什么时候插入__chkesp代码,接下来进入本文的正题,__chkesp检查失败和绕过__chkesp堆栈检查。
先看下__chkesp检查失败的情况(反面教材):
#include "stdafx.h"
unsigned int retAddress;
void Test();
void NormalFunc()
{
//data[1]: ebp的值; data[4] :函数返回地址
unsigned int data[1] = {0x0};
unsigned int* ptr = data;
ptr+=3;
//保存返回地址
retAddress = *ptr;
*ptr = (unsigned int)Test;
return;
}
void Test()
{
//跳回到main函数体中!
__asm
{
lea eax,[ebp+0x04];
mov eax,[eax]
mov retAddress,eax;
push retAddress;
ret
}
}
typedef void (*DirectCallFunc)();
int main()
{
DirectCallFunc dirCallFunc = NormalFunc;
(*dirCallFunc)();
getchar();
return 0;
}
函数调用前ESI/ESP的值:
函数调用后ESI/ESP的值:
很明显,因为esi!=esp所以引起__chkesp检查失败。接着看看引起失败的原因:
函数调用前,编译器保存了当前栈指针:
(*dirCallFunc)();
00411435 mov esi,esp
00411437 call dword ptr [dirCallFunc]
0041143A cmp esi,esp
0041143C call @ILT+300(__RTC_CheckEsp) (411131h)
getchar();
并在函数中通过PROLOG/EPILOG生成/恢复函数帧框架:
void Test()
{
00411A90 push ebp
00411A91 mov ebp,esp
00411A93 sub esp,0C0h
00411A99 push ebx
00411A9A push esi
00411A9B push edi
00411A9C lea edi,[ebp-0C0h]
00411AA2 mov ecx,30h
00411AA7 mov eax,0CCCCCCCCh
00411AAC rep stos dword ptr es:[edi]
}
00411ABF pop edi
00411AC0 pop esi
00411AC1 pop ebx
00411AC2 add esp,0C0h
00411AC8 cmp ebp,esp
00411ACA call @ILT+300(__RTC_CheckEsp) (411131h)
00411ACF mov esp,ebp
00411AD1 pop ebp
00411AD2 ret
原本main调用NormalFunc,NormalFunc有编译器生成函数调用框架并在执行完毕后顺利的返回到main函数中,此时堆栈平衡通过__chkesp检查。但是由于NormalFunc的返回地址被修改成Test中,节外生枝的跳转到Test中执行。如果Test函数正常终止,那么函数调用前后将还是堆栈平衡的,进一步说就是函数调用完成后esp==函数调用前esi的值==函数调用前esp的值,因此程序可以毫无悬念的通过了__chkesp检查。然而,这里的Test并不是这样的普通青年,他只正常的走过了PROLOG代码开辟新堆栈,此时esp已发生了变动,然后他2B的通过push/ret的方式,从函数中间越过EPILOG恢复堆栈的代码返回到main函数中(也只能通过这种方式返回到main函数中)。因此函数调用完成后esp!=函数调用前esi的值==函数调用前esp的值,在__chkesp关卡上被截住了~
现在找到失败的原因了,那找出相应的解决方法也不难:原本Test由编译器自动生成函数栈,大不了取消编译器做这个步骤就行了。这样虽然进入了Test却不额外生成函数帧,Test函数就像main函数的一部分似得,另外由于push/ret是一段esp自平衡的操作,因此堆栈还是维持NormalFunc结束时的样子。
__declspec(naked) void Test()
{
//跳回到main函数体中!
__asm
{
push retAddress;
ret
}
}
一个什么都没干的裸函数(想干啥自己补全吧),程序仍然从函数中间退出,不过至少能通过__chkesp检查。
裸函数是简单粗暴的解决方式,但是裸函数内部不能定义局部变量,完全不好用。这就得提出新的改变方案:1.函数得正常开辟调用堆栈,可以正常使用变量;2.函数不经过编译器生成的EPILOG的洗礼,从函数中间返回main函数;3讲了这么多,最重要的,必须能通过_chkesp的检查,否则,并没有什么卵用~
仔细想想,函数不过是越过EPLLOG代码,然后返回main函数所以才出错的么?大不了在返回前参考vs的代码自己来做EPILOG,来抵消进入Test时PROLOG的影响不就行了?
void Test()
{
__asm
{
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
}
//跳回到main函数体中!
__asm
{
push retAddress;
ret
}
}
来看下程序调用前后的结果:
附注,也可以把retAddress的地址修改为__chkesp后面的地址~
参考文档:越过__chkesp堆栈检查