返回介绍

16.2 缓冲区溢出

发布于 2025-02-22 14:00:46 字数 5137 浏览 0 评论 0 收藏 0

Array[index]中 index 指代数组索引,仔细观察下面的代码,你可能注意到代码没有 index 是否小于 20。如果 index 大于 20?这是 C/C++经常被批评的特征。 以下代码可以成功编译可以工作:

#!cpp
#include <stdio.h>
int main()
{
    int a[20];
    int i;
    for (i=0; i<20; i++)
        a[i]=i*2;
    printf ("a[100]=%d
", a[100]);
    return 0;
};

编译后 (MSVC 2010):

#!bash
_TEXT   SEGMENT
_i$ = -84                               ; size = 4
_a$ = -80                               ; size = 80
_main       PROC
    push    ebp
    mov     ebp, esp
    sub     esp, 84                 ; 00000054H
    mov     DWORD PTR _i$[ebp], 0
    jmp     SHORT $LN3@main
$LN2@main:
    mov     eax, DWORD PTR _i$[ebp]
    add     eax, 1
    mov     DWORD PTR _i$[ebp], eax
$LN3@main:
    cmp     DWORD PTR _i$[ebp], 20  ; 00000014H
    jge     SHORT $LN1@main
    mov     ecx, DWORD PTR _i$[ebp]
    shl     ecx, 1
    mov     edx, DWORD PTR _i$[ebp]
    mov     DWORD PTR _a$[ebp+edx*4], ecx
    jmp     SHORT $LN2@main
$LN1@main:
    mov     eax, DWORD PTR _a$[ebp+400]
    push    eax
    push    OFFSET $SG2460
    call    _printf
    add     esp, 8
    xor     eax, eax
    mov     esp, ebp
    pop     ebp
    ret     0
_main       ENDP

运行,我们得到: a[100]=760826203

打印的数字仅仅是距离数组第一个元素 400 个字节处的堆栈上的数值。 编译器可能会自动添加一些判断数组边界的检测代码(更高级语言 3),但是这可能影响运行速度。 我们可以从栈上非法读取数值,是否可以写入数值呢? 下面我们将写入数值:

#!cpp
#include <stdio.h>
int main()
{
    int a[20];
    int i;

    for (i=0; i<30; i++)
        a[i]=i;

    return 0;
};

我们得到:

#!bash
_TEXT   SEGMENT
_i$ = -84                                   ; size = 4
_a$ = -80                                   ; size = 80
_main       PROC
    push    ebp
    mov     ebp, esp
    sub     esp, 84 ; 00000054H
    mov     DWORD PTR _i$[ebp], 0
    jmp     SHORT $LN3@main
$LN2@main:
    mov     eax, DWORD PTR _i$[ebp]
    add     eax, 1
    mov     DWORD PTR _i$[ebp], eax
$LN3@main:
    cmp     DWORD PTR _i$[ebp], 30 ; 0000001eH
    jge     SHORT $LN1@main
    mov     ecx, DWORD PTR _i$[ebp]
    mov     edx, DWORD PTR _i$[ebp] ; that instruction is obviously redundant
    mov     DWORD PTR _a$[ebp+ecx*4], edx ; ECX could be used as second operand here instead
    jmp     SHORT $LN2@main
$LN1@main:
    xor     eax, eax
    mov     esp, ebp
    pop     ebp
    ret     0
_main       ENDP

编译后运行,程序崩溃。我们找出导致崩溃的地方。 没有使用调试器,而是使用我自己写的小工具 tracer 足以完成任务。 我们用它看被调试进程崩溃的地方:

#!bash
generic tracer 0.4 (WIN32), http://conus.info/gt

New process: C:PRJ...1.exe, PID=7988
EXCEPTION_ACCESS_VIOLATION: 0x15 (<symbol (0x15) is in unknown module>), ExceptionInformation
[0]=8
EAX=0x00000000 EBX=0x7EFDE000 ECX=0x0000001D EDX=0x0000001D
ESI=0x00000000 EDI=0x00000000 EBP=0x00000014 ESP=0x0018FF48
EIP=0x00000015
FLAGS=PF ZF IF RF
PID=7988|Process exit, return code -1073740791

我们来看各个寄存器的状态,异常发生在地址 0x15。这是个非法地址—至少对 win32 代码来说是!这种情况并不是我们期望的,我们还可以看到 EBP 值为 0x14,ECX 和 EDX 都为 0x1D。 让我们来研究堆栈布局。 代码进入 main()后,EBP 寄存器的值被保存在栈上。为数组和变量 i 一共分配 84 字节的栈空间,即(20+1)*sizeof(int)。此时 ESP 指向_i 变量,之后执行 push something,something 将紧挨着_i。 此时 main() 函数内栈布局为:

#!bash
ESP
ESP+4
ESP+84
ESP+88
4 bytes for i
80 bytes for a[20] array
saved EBP value
returning address

指令 a[19]=something 写入最后的 int 到数组边界(这里是数组边界!)。 指令 a[20]=something,something 将覆盖栈上保存的 EBP 值。 请注意崩溃时寄存器的状态。在此例中,数字 20 被写入第 20 个元素,即原来存放 EBP 值得地方被写入了 20(20 的 16 进制表示是 0x14)。然后 RET 指令被执行,相当于执行 POP EIP 指令。 RET 指令从堆栈中取出返回地址(该地址为 CRT 内部调用 main() 的地址),返回地址处被存储了 21(0x15)。CPU 执行地址 0x15 的代码,异常被抛出。 Welcome!这被称为缓冲区溢出 4。 使用字符数组代替 int 数组,创建一个较长的字符串,把字符串传递给程序,函数没有检测字符串长度,把字符复制到较短的缓冲区,你能够找到找到程序必须跳转的地址。事实上,找出它们并不是很简单。 我们来看 GCC 4.4.1 编译后的同类代码:

#!bash
            public main
main        proc near

a           = dword ptr -54h
i           = dword ptr -4

            push    ebp
            mov     ebp, esp
            sub     esp, 60h
            mov     [ebp+i], 0
            jmp     short loc_80483D1
loc_80483C3:
            mov     eax, [ebp+i]
            mov     edx, [ebp+i]
            mov     [ebp+eax*4+a], edx
            add     [ebp+i], 1
loc_80483D1:
            cmp     [ebp+i], 1Dh
            jle     short loc_80483C3
            mov     eax, 0
            leave
            retn
main        endp

在 linux 下运行将产生:段错误。 使用 GDB 调试:

#!bash
(gdb) r
Starting program: /home/dennis/RE/1

Program received signal SIGSEGV, Segmentation fault.
0x00000016 in ?? ()
(gdb) info registers
eax         0x0                 0
ecx         0xd2f96388      -755407992
edx         0x1d            29
ebx         0x26eff4        2551796
esp         0xbffff4b0      0xbffff4b0
ebp         0x15            0x15
esi         0x0                 0
edi         0x0                 0
eip         0x16            0x16
eflags      0x10202         [ IF RF ]
cs          0x73            115
ss          0x7b            123
ds          0x7b            123
es          0x7b            123
fs          0x0                 0
gs          0x33            51
(gdb)

寄存器的值与 win32 例子略微不同,因为堆栈布局也不太一样。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文