zoukankan      html  css  js  c++  java
  • 越过 __chkesp 检测的缓冲区溢出

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

    代码
    发信人: 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]
    ='\0';
    printf(
    "\n%s\n",str1);
    return 0;
    --
    ※ 来源:·飘渺水云间 zju88.org·[FROM: lisanbai]

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

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

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

          低地址:(栈顶部方向)

          -14h j:         ....
          -10h i:       [0][0][0][0]
            -0Ch str2:  [b][a][c][k][.][0][0][0]
            -04h str1:  [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:

     

     

    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 exit\n");
    return 0;
    }

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

        Naked: hello world!
        before exit

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

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

    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):

    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 次以后进程退出(否则因为死循环屏幕将一直输出字符串)。改造后的代码可以在屏幕上打印五行字符串内容:

    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 exit\n");
    return 0;
    }

        --hoodlum1980

        --2010-9-20

  • 相关阅读:
    前端性能优化 —— reflow(回流)和repaint(重绘)
    前端性能优化 —— 文档在浏览器中的加载和渲染
    前端性能优化 —— 起步篇(一)
    zepto源码研究
    zepto源码研究
    zepto源码研究
    zepto源码研究
    zepto源码研究
    zepto源码研究
    zepto源码研究
  • 原文地址:https://www.cnblogs.com/hoodlum1980/p/1832048.html
Copyright © 2011-2022 走看看