越过 __chkesp 检测的缓冲区溢出

本文的起源,来自于在学校BBS上的C++版上,有一个人问了一个问题,然后我给他已解答。这个帖子的原文是这样的:

越过 __chkesp 检测的缓冲区溢出越过 __chkesp 检测的缓冲区溢出代码

发信人: lisanbai (李三白), 板面: C++标  题: 这个怎么一直不停输出啊,菜鸟求教发信站: 飘渺水云间 (Mon Sep 20 16:52:30 2010), 转信看了半天找不出毛病   char str1[]="go?";    char str2[]="back.";    int i=0;    int j=0;    int count=0;    while(str1[i])        i++;    while(str2[j])        j++;    for(;count<j;count++)      str1[i++]=str2[count];    str1[i]='';    printf("n%sn",str1);    return 0;--※ 来源:·飘渺水云间 zju88.org·[FROM: lisanbai]

上面的代码看起来,是把 str2 的内容附加到 str1 的尾部(完成 strcat 的功能),很显然,他的错误是 str1 的空间不足够容纳 str2,作者之所以犯了这个错误,可能是因为他对内存管理不够熟悉导致的。下面是我对该贴的回复:

(1)你的str1恐怕不够容纳str2的内容,换句话说,你写内存的时候越界了。

(2)我用IDA看了下在函数的栈上的分布,大致情况如下:

低地址:(栈顶部方向)

-14hj: ....

-10hi: [0][0][0][0]

-0Chstr2: [b][a][c][k][.][0][0][0]

-04hstr1: [g][o][?][0]

00h: 被保护的寄存器值(如果有的话)

ebp

返回跳转地址

高地址:

你附加字符的时候,把函数返回地址那里给覆盖了。换句话说,这个函数的Stack Frame被你给破坏掉了。

(3)假设这些代码放在 main 函数里,用 IDA 运行这个程序,在函数返回前,可以手工把栈里的被破坏掉的ebp复原,但是返回地址好像没法复原(没法直接改栈上的数据),因为返回地址不对(返回地址最低位的字节被改成0)又跳回到 mainCRTStartup 里面的比较靠前的地方去了,正好跳到调用GetVersion的地方。然后过会又执行到调用 main 函数,然后又进入我在 main 函数里设的断点位置(如果反复手工修改被破坏的ebp,就形成了一个死循环状态)。

如果不手工复原 ebp ,回到 mainCRTStartup 里面的时候,在会报一个内存不能写的异常。。。(还好是在自己的进程空间里)因为ebp的原来的值是 mainCRTStartup 里的可能也是用于访问栈的一个指针,总之在 mainCRTStartup 的开头的地方有mov ebp, esp。

这里需强调的是,这个代码是可以通过 __chkesp 的检测。因为 __chkesp 只检测ebp的当前值(函数入口点的栈顶地址)和函数释放栈上空间以后的esp是否一致。这个代码不会影响到 ebp 当前值(函数入口点的栈顶),也不会没有破坏 esp 当前值,而是破坏了 ebp 的原值(在上一个函数中的值)和返回地址。因此这个代码属于缓冲区溢出,__chkesp 对此情况无法检测。

(4)注:改正方法很简单,第一行代码改成例如 char str[32]="go?" 即可。该数字保证大于 str1+str2 的长度即可。

=================================================================


尽管这个问题应该说很容易解答,到这里也基本算可以了。不过我在 IDA 调试的时候发现编译器附加的 __chkesp 函数对这个问题里的代码是不起作用的,这引起了我的注意。通常人们不可能故意的让自己的函数产生缓冲区溢出这样的错误(本文的提问者是无意的),常见的比较底层的方法例如使用 FlushInstructionCache 修改函数入口地址来完成一些 hook。但是如果我们自己故意让我们的代码产生缓冲区溢出则另当别论了,所以我按照这个代码的思路,可修改函数返回时跳转的地址,让函数返回时进入另一个函数,这也是比较有趣的一个事情。为了不能让系统觉察到异常,必须再无痕迹跳转回正确的位置,相当于我们自己hack自己了。

下面我提供一个演示的代码,首先简单介绍以下原理,这里存在一些没有保障的假设,例如在进入函数的时候,我们认为函数的栈是这样的分布:

ebp的原值(通常是上一个函数中的栈指针)

函数返回跳转地址(调用者中的某个地址)

然后我修改函数(NormalFunc)的返回地址,让他跳转到另一个函数(Test2),注意这和常规的函数调用不同!如果这个函数有编译器产生的开场白(prolog),必须手动先添加一个收场白(epilog)去抵消掉开场白的影响(稍后我再介绍这一点)。为了简单起见,我使用 naked 关键字,要求编译器不要添加开场白和收场白,这样进入这个函数的时候可以直接去执行我们自己的代码,执行完用户代码以后再跳回正确的地址(调用者 main 的内部)。这样在系统不知道的情况下,我们就用“神不知鬼不觉的方式”“调用”了另一个函数(Test2)!

下面是范例的代码,使用VC6.0,Console Application:


越过 __chkesp 检测的缓冲区溢出越过 __chkesp 检测的缓冲区溢出code_buf_overflow

// BufferOverflow.cpp : Defines the entry point for the console application.//#include "stdafx.h"//保存函数返回地址(跳回到main)unsigned int retAddress;void Test2();void NormalFunc(){    //data[1]: ebp的值;data[2]:函数返回地址    unsigned int data[1] = { 0x0 };    //保存返回地址    retAddress = data[2];    data[2] = (unsigned int)Test2;    return;}//naked函数(手工指定prolog 和 epilog)__declspec(naked) void Test2(){    printf("Naked: hello world!n");    //跳回到main函数体中!    __asm    {        jmp [retAddress]    }}int main(int argc, char* argv[]){    NormalFunc();    printf("before exitn");    return 0;}

这个函数产生下面的输出,看起来就和调用了 Test2 一样:

Naked: hello world!

before exit

在 main 函数里本质上调用的是 NormalFunc 函数, 在这个函数里我修改了它返回时的跳转地址,同时也把正确的返回时跳转地址保存到了一个全局变量(retAddress)中。然后这个函数返回时进入了 Test2, 在 Test2 里执行了一些代码以后,再通过全局变量跳回到 main 中的正确位置,这个过程对编译器和系统来说是透明的。

在 Test2 里我们使用 naked 关键字防止编译器自动产生那些开场白和收场白。如果没有加这个关键字,函数的开头和结尾会有系统产生的那些开场白和收场白,因为我们并非常规的函数调用,可以理解为我们还位于原来的函数体中,所以在执行我们自己的代码前,需要手工抵消掉函数的开场白(只需要把编译器产生的收场白嵌入到函数的用户代码前面即可,为此,首先观察编译器产生的开场白和收场白(函数主体部分省略):

越过 __chkesp 检测的缓冲区溢出越过 __chkesp 检测的缓冲区溢出asm_test

;函数的开头部分.text:00401070                 push    ebp.text:00401071                 mov     ebp, esp.text:00401073                 sub     esp, 40h.text:00401076                 push    ebx.text:00401077                 push    esi.text:00401078                 push    edi.text:00401079                 lea     edi, [ebp+var_40].text:0040107C                 mov     ecx, 10h.text:00401081                 mov     eax, 0CCCCCCCCh.text:00401086                 rep stosd  ;函数的结尾部分.text:0040109B                 pop     edi.text:0040109C                 pop     esi.text:0040109D                 pop     ebx.text:0040109E                 add     esp, 40h.text:004010A1                 cmp     ebp, esp.text:004010A3                 call    __chkesp.text:004010A8                 mov     esp, ebp.text:004010AA                 pop     ebp.text:004010AB                 retn

现在我们写一个普通的函数(不加 naked 关键字),我们在函数前面嵌入“收场白”的等效汇编代码,则非 naked 的 Test2 函数的代码如下,具体的开场白有可能会依赖编译器,嵌入的收场白代码怎样写,最好还是用反汇编查看一下再确定(本例使用的是VC6.0):

越过 __chkesp 检测的缓冲区溢出越过 __chkesp 检测的缓冲区溢出code_test2_normal

void Test2(){    //普通函数,我们必须手工抵消函数的开头    __asm    {        pop edi        pop esi        pop ebx        mov esp, ebp        pop ebp    }    //现在做一些事情    printf("hello world!n");    //跳回到main函数体中!    __asm    {        jmp [retAddress]    }}

范例中的 Test2 函数很显然是不能直接调用的,因为全局变量 retAddress 的初值是 0,直接调用会导致进程异常终止。但如果我们先调用 NormalFunc 是全局变量(retAddress)被赋正确的值 ,Test2 就可以正常调用了,但是 Test2 返回时是从 NormalFunc 函数调用后面的语句继续执行的,所以这样会产生死循环。所以我们可以少许改造下 Test2,让它最多被调用 5 次以后进程退出(否则因为死循环屏幕将一直输出字符串)。改造后的代码可以在屏幕上打印五行字符串内容:

越过 __chkesp 检测的缓冲区溢出越过 __chkesp 检测的缓冲区溢出code_buf_overflow_2

#include "stdafx.h"#include <stdlib.h>//保存函数返回地址(跳回到main)unsigned int retAddress;void Test2();void NormalFunc(){    //data[1]: 可能是    unsigned int data[1] = { 0x0 };    //保存返回地址    retAddress = data[2];    data[2] = (unsigned int)Test2;    return;}//naked函数(手工指定prolog 和 epilog)__declspec(naked) void Test2(){    static int i;    printf("Naked: hello world!n");    i++;    if(i == 5)        exit(0);    //跳回到main函数体中!    __asm    {        jmp [retAddress]    }}int main(int argc, char* argv[]){    NormalFunc();    Test2();    printf("before exitn");    return 0;}

--hoodlum1980

--2010-9-20
原文链接: https://www.cnblogs.com/hoodlum1980/archive/2010/09/20/1832048.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/15235

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月7日 下午3:09
下一篇 2023年2月7日 下午3:10

相关推荐