关于绕过__chkesp的一点补充
摘要:关于绕过__chkesp函数的一点补充
什么时候会调用__chkesp?
- 调用windows提供的API后编译都会安排一段__chkesp。
在"直接调用地址"返回后,也会被插入__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
关于直接调用地址报错的分析
#include <stdio.h>
unsigned int retAddress;
void Test();
void NormalFunc()
{
//dat[0]: 0x0,data[1]: ebp的值; data[2] :函数返回地址
unsigned int data[1] = {0x0};
unsigned int* ptr = data;
ptr+=2;
//保存返回地址
retAddress = *ptr;
*ptr = (unsigned int)Test;
return;
}
void Test()
{
printf("mmp");
}
typedef void (*DirectCallFunc)();
int main()
{
DirectCallFunc dirCallFunc = NormalFunc;
(*dirCallFunc)();
getchar();
return 0;
}
Test函数的反汇编为:(注意圈出的为编译器自动添加的 开场白,同理下面是收场白)
首先要说一下函数的堆栈,当发生函数调用时以上面的代码Test()为例。
正常的调用方式如下:
c语言
Test()
汇编
push 返回地址 call Test
此时Test反汇编的ret语句会pop返回地址并赋值给EIP,然后ESP寄存器的值-4
不正常的调用方式(直接调用地址)如下,相当于直接跳转到Test()函数。
汇编
call Test
此时Test反汇编的ret语句会pop返回地址,但是没有正确的返回地址可以pop,所以会爆错误。
如何避开__chkesp
__chkesp判断堆栈平衡的原理是:当调用函数之前把esp赋值给esi寄存器。当调用函数结束后,比较函数结束后的esp的值和esi寄存器的值是否一样。
__chkesp的汇编代码如下:
使用__declspec(naked)
该种方法会避开编译器自动添加的开场白和收场白,代码如下:#include <stdio.h> unsigned int retAddress; void Test(); void NormalFunc() { unsigned int data[1] = {0x0}; unsigned int* ptr = data; ptr+=2; //保存返回地址 retAddress = *ptr; *ptr = (unsigned int)Test; return ; } __declspec(naked) void Test() { printf("mmp"); //跳回到main函数体中! __asm { push retAddress; ret } } typedef void (*DirectCallFunc)(); int main() { DirectCallFunc dirCallFunc = NormalFunc; (*dirCallFunc)(); getchar(); return 0; }
Test函数的反汇编为:
手动抵消函数开场白
该种方法可以手动抵消编译器自动添加的开场白和收场白,代码如下:#include <stdio.h> unsigned int retAddress; void Test(); void NormalFunc() { //dat[0]: 0x0,data[1]: ebp的值; data[2] :函数返回地址 unsigned int data[1] = {0x0}; unsigned int* ptr = data; ptr+=2; //保存返回地址 retAddress = *ptr; *ptr = (unsigned int)Test; return; } void Test() { printf("mmp"); __asm { pop edi pop esi pop ebx mov esp,ebp pop ebp } //跳回到main函数体中! __asm { push retAddress; ret } } typedef void (*DirectCallFunc)(); int main() { DirectCallFunc dirCallFunc = NormalFunc; (*dirCallFunc)(); getchar(); return 0; }
Test函数的反汇编为:
可以看到编译器自动添加的开场白被我们添加的ret给分割了,当执行到我们添加的ret时就会退出Test函数,不会再往下执行系统添加的开场白。
参考链接:
越过 __chkesp 检测的缓冲区溢出 - hoodlum1980 - 博客园
绕过__chkesp堆栈检查_lixiangminghate的专栏-CSDN博客
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。