什么是缓冲区溢出,如何引发它?

32

我听说过缓冲区溢出,想知道如何引发它。

有人能展示一个小的缓冲区溢出例子吗?还有它们被用于什么?


3
你为什么要尝试制造缓冲区溢出? - Sophie Alpert
2
请参阅C标准库中的gets函数。 - derobert
看到:在C标准库中获取 - 这是一个玩笑吗? - NTDLS
1
@NTDLS:这可能有点讽刺,但完全是认真的... 在生产代码中使用get是很危险、也是完全不能被辩解的。 - dmckee --- ex-moderator kitten
12个回答

33

缓冲区溢出的经典示例:

// noone will ever have the time to type more than 64 characters...
char buf[64];
gets(buf); // let user put his name
缓冲区溢出通常不是故意发生的,而是由于所谓的 "off-by-one" 错误导致的。意思是您计算数组大小时错误了一位,可能是因为您忘记考虑终止的空字符,或者因为其他一些原因。
但它也可以用于一些恶意行为。事实上,用户早就知道这个漏洞,然后插入70个字符,其中最后几个包含一些特殊的字节,它们覆盖了某些堆栈槽 - 如果用户非常狡猾,他/她将击中堆栈中的返回地址槽,并覆盖它,以便跳转到刚刚插入的缓冲区:因为用户输入的不是他的名字,而是他先前编译和转储的 shell-code。那个代码将被执行。有一些问题。例如,您必须安排不要在该二进制代码中使用 "\n"(因为 gets 会停止在那里读取)。对于与危险的字符串函数混淆的其他方式,二进制零是有问题的,因为字符串函数会停止将其复制到缓冲区。人们已经使用两次相同值的 xor 来生成零,而不需要明确写入零字节。
那是做这件事的经典方法。但是有一些安全块可以说出发生了这种事情以及其他使堆栈不可执行的东西。但我想还有比我刚才解释的更好的技巧。一些汇编语言的人可能会讲述关于它们的长篇故事:)
如何避免它
如果您不确定缓冲区是否足够大,请始终使用带有最大长度参数的函数。不要玩“哦,数字不会超过5个字符”的游戏 - 它总有一天会失败。记住有一个火箭,科学家说数字不会超过某个大小,因为火箭永远不会那么快。但总有一天,它实际上变得更快,结果是整数溢出和火箭坠毁(这是历史上最昂贵的计算机错误之一,与 Ariane 5 中的一个漏洞有关)。
例如,除了 gets 之外,使用 fgets。并且在适当和可用的情况下,使用 snprintf 而不是 sprintf(或者使用 C++ 风格的内容,如 istream 等)

缓冲区溢出 = 缓冲区超限? - Ahmed
我不知道后面这个术语。维基百科似乎说它们意思相同。 - Johannes Schaub - litb

28

缓冲区溢出基本上是指当一个经过设计的内存段(或缓冲区)被写入到其预期界限之外时。如果攻击者能够通过程序外部将此事发生,可能会导致安全问题,因为这可能允许他们操纵任意内存位置,尽管许多现代操作系统防止了最糟糕情况的发生。

虽然读取和写入超出预期边界通常被认为是一个坏主意,但是"缓冲区溢出"一词通常保留给写入超出边界的情况,因为这可能使攻击者轻松修改代码运行方式。有一篇关于缓冲区溢出以及它们被用来进行攻击的各种方式的很好的维基百科文章。

就如何自己编程实现一个缓冲区溢出而言,这只是一个简单的问题:

char a[4];
strcpy(a,"a string longer than 4 characters"); // write past end of buffer (buffer overflow)
printf("%s\n",a[6]); // read past end of buffer (also not a good idea)
无论程序能否编译通过以及运行时会发生什么,可能取决于您的操作系统和编译器。

缓冲区溢出通常在你写入缓冲区末尾之外时比读取时更具破坏性 [例如,char x[2]; strcpy (x,"hello");] - 这是因为它经常会破坏许多其他变量和/或堆栈帧。 - paxdiablo
你不必超出数组的边界才被认为是缓冲区溢出吗?在这种情况下,我认为更好的例子是改变a[200]处的内容。 - MahlerFive
@david 如果你阅读了你引用的维基百科文章的第一段,你会发现缓冲区溢出只会在你“写入”缓冲区范围之外时发生,而你的示例不是一个正确的缓冲区溢出。 - Ismael
那仍然不是缓冲区溢出。a是一个指向字符串的指针,第二行只是改变了该引用。此外,a是一个数组,因此它甚至不是一个有效的左值,你的代码将无法编译。一个绝对可靠的例子是strcpy(a,"一个超过4个字符的字符串"); - wj32

14
在现代Linux操作系统中,如果没有进行一些附加实验,你将无法利用缓冲区溢出漏洞。为什么?因为你会被ASLR(地址空间布局随机化)和GNU C编译器中的堆栈保护机制所阻止。由于ASLR的存在,内存会随机分配到不同的地址,因此很难定位内存。同时,如果你试图溢出程序,你也会被堆栈保护机制所阻止。 要开始这项工作,你需要将ASLR关闭,其默认值为2。
root@bt:~# cat /proc/sys/kernel/randomize_va_space
2
root@bt:~# echo 0 > /proc/sys/kernel/randomize_va_space
root@bt:~# cat /proc/sys/kernel/randomize_va_space
0
root@bt:~#

这种情况与您从互联网上获取的旧式缓冲区溢出教程无关,也不适用于Aleph One教程,因为它们现在可能无法在您的系统中运行。

现在让我们创建一个易受缓冲区溢出攻击的程序。

---------------------bof.c--------------------------
#include <stdio.h>
#include <string.h>

int main(int argc, char** argv)
{
        char buffer[400];
        strcpy(buffer, argv[1]);

        return 0;
}
---------------------EOF-----------------------------

如果没有堆栈保护,看起来strcpy函数很危险,因为该函数不检查我们将输入多少字节。在您的C程序中编译时使用额外选项-fno-stack-protector和-mpreferred-stack-boundary = 2以去除堆栈保护。

root@bt:~# gcc -g -o bof -fno-stack-protector -mpreferred-stack-boundary=2 bof.c
root@bt:~# chown root:root bof
root@bt:~# chmod 4755 bof

具有SUID root访问场景的缓冲区溢出C程序我们已经完成。 现在让我们搜索需要将多少字节放入缓冲区才能使程序发生分段错误。

root@bt:~# ./bof `perl -e 'print "A" x 400'`
root@bt:~# ./bof `perl -e 'print "A" x 403'`
root@bt:~# ./bof `perl -e 'print "A" x 404'`
Segmentation fault
root@bt:~#

你看,我们需要404个字节来造成程序的分段错误(崩溃),那么我们需要多少字节才能覆盖EIP?EIP是指将要执行的指令。黑客利用这一点,把二进制SUID程序中的EIP改为他们想要的恶意指令。如果程序在SUID root下运行,该指令将以root权限运行。

root@bt:~# gdb -q bof
(gdb) list
1       #include <stdio.h>
2       #include <string.h>
3
4       int main(int argc, char** argv)
5       {
6               char buffer[400];
7               strcpy(buffer, argv[1]);
8
9               return 0;
10      }
(gdb) run `perl -e 'print "A" x 404'`
Starting program: /root/bof `perl -e 'print "A" x 404'`

Program received signal SIGSEGV, Segmentation fault.
0xb7e86606 in __libc_start_main () from /lib/tls/i686/cmov/libc.so.6
(gdb) run `perl -e 'print "A" x 405'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/bof `perl -e 'print "A" x 405'`

Program received signal SIGSEGV, Segmentation fault.
0xb7e800a9 in ?? () from /lib/tls/i686/cmov/libc.so.6
(gdb)

程序收到段错误的返回代码。让我们输入更多字节并查看EIP寄存器。

(gdb) run `perl -e 'print "A" x 406'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/bof `perl -e 'print "A" x 406'`

Program received signal SIGSEGV, Segmentation fault.
0xb7004141 in ?? ()
(gdb)

(gdb) run `perl -e 'print "A" x 407'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/bof `perl -e 'print "A" x 407'`

Program received signal SIGSEGV, Segmentation fault.
0x00414141 in ?? ()
(gdb)

更少的

(gdb) run `perl -e 'print "A" x 408'`
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/bof `perl -e 'print "A" x 408'`

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb)

(gdb) i r
eax            0x0      0
ecx            0xbffff0b7       -1073745737
edx            0x199    409
ebx            0xb7fc9ff4       -1208180748
esp            0xbffff250       0xbffff250
ebp            0x41414141       0x41414141
esi            0x8048400        134513664
edi            0x8048310        134513424
eip            0x41414141       0x41414141 <-- overwriten !!
eflags         0x210246 [ PF ZF IF RF ID ]
cs             0x73     115
ss             0x7b     123
ds             0x7b     123
es             0x7b     123
fs             0x0      0
gs             0x33     51
(gdb)

现在你可以进行下一步操作...


10

缓冲区溢出就是在缓冲区的末尾之后进行写入:

int main(int argc, const char* argv[])
{
    char buf[10];
    memset(buf, 0, 11);
    return 0;
}

5
除了已经提到的内容外,请记住当缓冲区溢出发生时,您的程序可能会或可能不会“崩溃”。 它应该崩溃,并且您应该希望它崩溃-但是如果缓冲区溢出“溢出”到应用程序也分配的另一个地址-您的应用程序可能会表现出正常运行更长一段时间的情况。
如果您正在使用较新版本的Microsoft Visual Studio,则建议使用stdlib中的新安全替代品,例如sprintf_s而不是sprintf等。

还有snprintf,它具有标准化的优点(ISO C 99)。还有asprintf(GNU和BSD libc),g_strdup_printf(Glib)。 - sleske

1

这应该足以复现它:

void buffer_overflow() 
{
    char * foo = "foo";
    char buffer[10];

    for(int it = 0; it < 1000; it++) {
        buffer[it] = '*';
    }

    char accessViolation = foo[0];
}

1

“经典”的缓冲区溢出例子是:


int main(int argc, char *argv[])
{
    char buffer[10];
    strcpy(buffer, argv[1]);
}

这让你可以玩弄缓冲区溢出参数并调整它们以满足你的需求。书籍 "Hacking - The Art of Exploitation"(链接指向亚马逊)详细介绍了如何玩弄缓冲区溢出(显然仅作为一种智力锻炼)。


1
这是对你所收到答案的一般性评论。例如:
int main(int argc, char *argv[])
{
    char buffer[10];
    strcpy(buffer, argv[1]);
}

并且:

int main(int argc, const char* argv[])
{
    char buf[10];
    memset(buf, 0, 11);
    return 0;
}
在现代Linux平台上,由于FORTIFY_SOURCE安全特性的存在,这可能无法按预期或意图工作。FORTIFY_SOURCE使用高风险函数(如memcpy和strcpy)的“更安全”变量。编译器在可以推断目标缓冲区大小时使用更安全的变量。如果复制超过目标缓冲区大小,则程序会调用abort()。为了在测试期间禁用FORTIFY_SOURCE,您应该使用-U_FORTIFY_SOURCE或-D_FORTIFY_SOURCE = 0编译程序。

1

如果您想检查程序是否存在缓冲区溢出问题,可以使用Valgrind等工具运行它。它们会为您找到一些内存管理错误。


0

缓冲区溢出是指插入超出分配内存容量的字符。


这个回答是否与已有的内容相比有所创新?请不要重复,点赞好的回答。 - slfan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接