摘要:关于绕过__chkesp函数的一点补充

什么时候会调用__chkesp?

  1. 调用windows提供的API后编译都会安排一段__chkesp。
  2. 在"直接调用地址"返回后,也会被插入__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函数的反汇编为:(注意圈出的为编译器自动添加的 开场白,同理下面是收场白)

QQ截图20200904125345.jpg

首先要说一下函数的堆栈,当发生函数调用时以上面的代码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的汇编代码如下:

__chkesp的原理.jpg

  1. 使用__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函数的反汇编为:

    不加函数开场白.jpg

  2. 手动抵消函数开场白
    该种方法可以手动抵消编译器自动添加的开场白和收场白,代码如下:

    #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函数的反汇编为:

    绕过函数开场白.jpg

    可以看到编译器自动添加的开场白被我们添加的ret给分割了,当执行到我们添加的ret时就会退出Test函数,不会再往下执行系统添加的开场白。

参考链接:
越过 __chkesp 检测的缓冲区溢出 - hoodlum1980 - 博客园
绕过__chkesp堆栈检查_lixiangminghate的专栏-CSDN博客

文章目录