绕过__chkesp堆栈检查

63 篇文章 4 订阅

    前面很多注入相关的文章中都提到为了保证注入后原始程序能恢复正常的执行流,需要在编译器中关闭堆栈检查。为了解决问题,这是个好手段,但是不得不说这是回避问题,不是根本上解决问题。本文旨在解决这个问题。

   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堆栈检查

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值