如果计算机程序错误地处理传入数据,则它可能容易受到缓冲区溢出的影响。若程序中支持用户任意输入数据,那么可能导致此类程序崩溃。更糟糕的是,易受攻击的程序可能会执行入侵者提供的代码,并执行相应的程序以达到破坏系统的行为。缓冲区溢出漏洞是由程序员开发错误引起的,这些错误很容易理解,但不容易避免或防范。
缓冲区溢出漏洞(也称为缓冲区溢出)的想法很简单。 以下是具有缓冲区溢出漏洞的C程序的源代码:
char greeting[5];
memcpy(greeting, "Hello, world!\n", 15);
printf(greeting);
当我们编译并运行这个存在漏洞的程序时会发生什么?任何事情都可能发生。 当执行此代码片段时,它将尝试将15个字节放入只有5个字节长的目标缓冲区。 这意味着将十个字节写入阵列外部的内存地址。 稍后会发生什么取决于重写的十个字节的内存的原始内容。 也许重要的变量存储在那里,我们刚刚的做法更改了它们的值。
上面的例子能够很好的解释缓冲区溢出的概念,然而缺乏开发经验的程序员会犯这样的错误。 那么,让我们考虑另一个例子。 我们假设我们需要从文件中读取IP地址。 我们可以使用以下C代码来完成它:
#include <stdio.h>
#define MAX_IP_LENGTH 15
int main(void) {
char file_name[] = "ip.txt";
FILE *fp;
fp = fopen(file_name, "r");
char ch;
int counter = 0;
char buf[MAX_IP_LENGTH];
while((ch = fgetc(fp)) != EOF) {
buf[counter++] = ch;
}
buf[counter] = '\0';
printf("%s\n", buf);
fclose(fp);
return 0;
}
上面例子中的一个错误并不那么明显。 我们假设我们想要从文件中读取的IP地址永远不会超过15个字节。 适当的IP地址(例如,255.255.255.255
)不能超过15个字节。 但是,恶意用户可以准备包含非常长的伪字符串而不是IP地址的文件(例如19222222222.16888888.0.1
)。 该字符串将导致我们的程序溢出目标缓冲区。
如果你认为即使这个bug太明显了也没有程序员会犯这样的错误。那么接下来,我们将看到一个缓冲区溢出错误的真实示例,该错误发生在一个非常重要的项目中,并且不比上面的示例复杂得多。
现在我们知道一个程序可以溢出一个数组并覆盖它不应该覆盖的内存片段,让我们看看它是如何用来挂载缓冲区溢出攻击的。 在一般的攻击场景(称为堆栈缓冲区溢出)中,通过将数据(意图处理或显示)与控制程序执行的命令混合,引起信息安全的许多问题。
在C中,与大多数编程语言一样,程序是使用函数构建的。 函数相互调用,相互传递参数,并返回值。 例如我们的代码从文件中读取IP地址,可以是名为readIpAddress
的函数的一部分,该函数从文件中读取IP地址并对其进行解析。 此函数可以由其他一些函数调用,例如readConfiguration
。 当readConfiguration
调用readIpAddress
时,它会向其传递一个文件名,然后readIpAddress
函数将一个IP地址作为四个字节的数组返回。
图1. readIpAddress函数的参数和返回值
在该函数调用期间,三个不同的信息并排存储在计算机存储器中。 对于每个程序,操作系统维护一个内存区域,其中包括一个称为堆栈或调用堆栈的部分(因此名称堆栈缓冲区溢出)。 调用函数时,会为其分配堆栈的片段。 这个堆栈(称为框架)用于:
记住完成函数执行时程序执行应该从中恢复的代码行(在我们的例子中,readConfiguration
函数中的特定行)
存储由调用者传递给函数的参数(在我们的例子中,例如/home/someuser/myconfiguration/ip.txt
)
将函数返回的返回值存储到其调用者(在我们的例子中,是一个四字节数组,例如192,168,0,1
)
在执行此函数时存储被调用函数的局部变量(在我们的例子中,变量char [MAX_IP_LENGTH] buf
)
因此,如果程序在堆栈帧中分配了缓冲区并尝试在其中放置的数据超出了适合的范围,则用户输入数据可能会溢出并覆盖存储返回地址的内存位置。
图2.调用readIPAddress函数时堆栈帧的内容
如果问题是由随机格式错误的用户输入数据引起的,则很可能新的返回地址不会指向存储任何其他程序的内存位置,因此原始程序将崩溃。 但是,如果数据是攻击者精心准备的,则会产生恶意代码执行的情况。
攻击者的第一步是准备可执行代码的数据,这些数据可以为攻击者带来好处(这种数据称为shellcode)。 第二步是将此恶意数据的地址放在返回地址的确切位置。
图3. ip.txt的内容覆盖了返回地址
实际上,当函数读取IP字符串并将其放入目标缓冲区时,返回地址将被恶意代码的地址替换。 当函数结束时,程序执行会跳转到恶意代码。
自从发现堆栈缓冲区溢出攻击技术以来,操作系统(Linux,Microsoft Windows,macOS
等)的作者尝试了许多预防技术:
堆栈可以是不可执行的,因此即使恶意代码放在缓冲区中,也无法执行。
操作系统可以随机化地址空间(存储空间)的存储器布局。 在这种情况下,当恶意代码放在缓冲区中时,攻击者无法预测其地址。
其他保护技术(例如StackGuard
)以这样的方式修改编译器:每个函数调用一段代码来验证返回地址是否未更改。
实际上,即使这种保护机制使堆栈缓冲区溢出攻击更加困难,它们也不会使它们变得不可能,并且其中一些会影响性能。
编程语言中存在缓冲区溢出漏洞,与C类似,为了提高效率而不进行内存访问,需要进行安全交易。 在通常用于构建Web应用程序的高级编程语言(例如Python,Java,PHP,JavaScript或Perl
)中,缓冲区溢出漏洞不可能存在。 在这些编程语言中,我们不能将多余的数据放入目标缓冲区。 例如,尝试编译并执行以下Java代码:
int[] buffer = new int[5];
buffer[100] = 44;
Java编译器不会产生警告,但运行时Java虚拟机将检测到问题而不是覆盖随机内存,它将中断程序执行。
但是,即使是使用高级语言的程序员也应该知道并关心缓冲区溢出攻击。他们的程序通常在用C编写的操作系统中执行,或者使用用C编写的运行时环境,并且这个C代码可能容易受到这种攻击。为了了解缓冲区溢出漏洞如何影响使用这种高级编程语言的程序员,让我们分析一下CVE-2015-3329,一个真实的安全漏洞,它在2015年的PHP标准库中被发现。
PHP应用程序是* .php文件的集合。为了便于分发此类应用程序,可以将其打包到单个文件存档中 - 作为zip文件,tar文件或使用名为phar的自定义PHP格式。名为phar的PHP扩展包含一个可用于处理此类存档的类。使用此类,我们可以解析存档,列出其文件,提取文件等。使用此类非常简单,例如,从存档中提取所有文件,使用以下代码:
$phar = new Phar('phar-file.phar');
$phar->extractTo('./directory');
当Phar类解析存档(新的Phar('phar-file.phar'))
时,它会从存档中读取所有文件名,将每个文件名与存档文件名连接起来,然后计算校验和。 例如,对于包含文件index.php
和components/hello.php
的名为myarchive.phar
的存档,Phar类计算两个字符串的校验和:myarchive.pharindex.php
和myarchive.pharcomponents/hello.php
。 作者之所以这样实现它并不重要,重要的是他们如何实现它。 到2015年,此操作使用以下函数完成:
phar_set_inode(phar_entry_info *entry TSRMLS_DC) /* {{{ */
{
char tmp[MAXPATHLEN];
int tmp_len;
tmp_len = entry->filename_len + entry->phar->fname_len;
memcpy(tmp, entry->phar->fname, entry->phar->fname_len);
memcpy(tmp + entry->phar->fname_len, entry->filename, entry->filename_len);
entry->inode = (unsigned short)zend_get_hash_value(tmp, tmp_len);
}
如我们所见,此函数创建一个名为tmp
的字符数组。 首先使用以下命令将phar
存档的名称(在我们的示例中为myarchive.phar
)复制到此数组中:
memcpy(tmp, entry->phar->fname, entry->phar->fname_len);
在此命令中:
第一个参数tmp
是应该复制字节的目标。
第二个参数entry-> phar-> fname
是一个来源,应该从中复制字节。在我们的例子中作为文件名(myarchive.phar)
。
第三个参数entry-> phar-> fname_len
是应该复制的字节数。在我们的例子中,它是存档文件名的长度(以字节为单位)。
该函数使用以下命令将文件名(在我们的示例中,index.php
或components/hello.php
)复制到tmp char
数组中:
memcpy(tmp + entry->phar->fname_len, entry->filename, entry->filename_len);
在此命令中:
第一个参数tmp + entry-> phar-> fname_len
是一个应该复制字节的目标。在我们的例子中,它是存档文件名结尾之后的tmp
数组中的一个位置。
第二个参数entry-> filename
是来自应该复制字节的源文件。
第三个参数entry-> filename_len
是应该复制的字节数。
然后调用zend_get_hash_value
函数来计算哈希。
请注意如何声明缓冲区的大小:
char tmp[MAXPATHLEN];
它的大小为MAXPATHLEN
,它是一个常量,定义为当前平台上文件系统路径的最大长度。
作者假设,如果他们将存档的文件名与存档中文件的名称连接起来,它们将永远不会超过允许的最大路径长度。 在正常情况下,这个假设得到满足。如果攻击者使用异常长的文件名准备存档,则缓冲区溢出将会发送。 函数phar_set_inode
将导致tmp数组溢出。
攻击者可以使用它来崩溃PHP(导致拒绝服务)甚至使其执行恶意代码。 问题类似于我们的简单示例,程序员犯了一个错误,过多地信任用户输入,并假设数据总是适合固定大小的缓冲区。 然而此漏洞在2015年被发现并已修复。
程序员必须始终验证用户输入长度,以避免缓冲区溢出攻击。 但是,避免缓冲区溢出漏洞的一般方法是坚持使用包含缓冲区溢出保护的安全功能(与memcpy
不同)。 此类函数可在不同平台上使用,例如strlcpy,strlcat,snprintf(OpenBSD)或strcpy_s,strcat_s,sprintf_s(Windows
)。
本文为翻译文章,来自:https://www.netsparker.com/blog/web-security/buffer-overflow-attacks/