缓冲区溢出是 Linux 系统中常见且严重的安全漏洞,当程序向缓冲区写入的数据超过其可容纳的数据量时就会发生这种情况。这种溢出可能导致内存损坏,从而使攻击者能够执行任意代码、使程序崩溃或未经授权访问系统。缓冲区溢出也可能发生在 Windows 和 macOS 系统上。
快速回顾一下缓冲区溢出的工作原理
当数据超出内存中分配的空间并覆盖相邻的内存位置时,就会发生缓冲区溢出。这可能会损坏数据、导致意外行为或允许攻击者操纵程序的执行流程。在 Linux 系统中,缓冲区溢出通常利用基于堆栈的缓冲区,其中溢出可以覆盖控制数据,例如返回地址或函数指针。
缓冲区溢出有多种类型:
•基于堆栈的缓冲区溢出:这是最常见的类型,溢出发生在调用堆栈内存中。它可以覆盖局部变量和控制数据(例如返回地址)。
•基于堆的缓冲区溢出:这种情况发生在用于动态内存分配的堆内存区域中。这种情况不太常见,但可利用该漏洞覆盖相邻的内存块。
如何利用缓冲区溢出
利用缓冲区溢出涉及几个步骤:
•发现:通过人工审查或自动扫描识别易受攻击的代码
•有效载荷制作:创建一个将溢出缓冲区并重定向执行流的有效载荷
•注入:通过用户输入或网络载体传递有效载荷
•触发:使程序使用精心设计的有效载荷执行,从而导致内存损坏
步骤1.开始发现:
该过程从发现开始,目标是识别目标系统中的潜在漏洞。此步骤可能涉及手动审查和自动扫描的结合。
人工审查:安全研究人员人工审查代码,以查找可能导致缓冲区溢出的不安全做法,例如未经检查的内存操作(例如,strcpy、sprintf)。
自动扫描:使用静态代码分析器或漏洞扫描器等工具来识别应用程序中的潜在缺陷。
第 2 步。发现有漏洞的代码?
下一步检查是否发现了易受攻击的代码(例如未正确管理缓冲区边界的函数或操作)。如果没有发现漏洞,则该过程结束。如果发现漏洞,则转到有效载荷制作。
步骤3.有效载荷制作:
在此步骤中,攻击者开发一个漏洞利用负载,将其注入到易受攻击的代码中以触发缓冲区溢出。该过程涉及以下部分:
创建有效载荷:攻击者制作一个恶意有效载荷,它将超出缓冲区的容量并覆盖相邻的内存。
测试有效负载:对有效负载进行测试,以确保其在注入易受攻击的代码时能够按预期运行,操纵内存(例如覆盖返回地址)以获得执行控制权。
步骤4.有效载荷是否成功?
测试完payload后,攻击者会检查payload是否成功。如果payload不成功,攻击者会修改payload并再次测试。如果成功,攻击者就会进入注入阶段。
步骤5.开始注射:
此阶段涉及通过不同的方法将恶意负载传递到易受攻击的代码中,这些方法可能是以下之一:
用户输入:如果存在漏洞的应用程序接受用户输入,攻击者就会提供导致缓冲区溢出的恶意输入。
网络向量:如果漏洞通过网络服务暴露,则有效载荷将通过网络协议发送。
步骤6.有效载荷已送达?
在这里,攻击者检查有效载荷是否已成功传递到易受攻击的应用程序。如果没有,他们会修改注入方法并重试。如果有效载荷已传递,他们会继续触发漏洞。
步骤7。开始触发:
一旦注入了有效载荷,其目的就是触发缓冲区溢出。这通常是通过执行一个程序来实现的,该程序会导致易受攻击的函数以导致内存损坏的方式处理有效载荷,通常是通过执行以下操作:
执行程序:存在漏洞的函数通过有效载荷运行。
内存损坏:有效载荷导致溢出,从而破坏内存并可能允许攻击者控制执行流程。
步骤8.漏洞利用成功吗?
攻击者检查漏洞利用是否成功。这通常意味着他们已获得对系统的未经授权的访问或控制权(例如生成 root shell)。
如果漏洞利用不成功,道德黑客/攻击者会修改漏洞利用并通过调整有效载荷、注入方法或触发步骤再次尝试。
如果攻击成功,则该过程结束,攻击者成功利用了缓冲区溢出。
易受攻击的代码示例
考虑一个使用strcpy()而不进行边界检查的简单 C 程序
void vulnerable_function(char *input) {
char buffer[256];
strcpy(buffer, input); // No bounds checking
}
int main(int argc, char *argv[]) {
if (argc > 1) {
vulnerable_function(argv[1]);
}
return 0;
}
#include < stdio.h >和#include <string.h>是标准 C 库。stdio.h提供输入/输出函数,string.h提供处理字符串的函数,例如这里使用的strcpy()。
易受攻击的函数 ( vulnerable_function ) 接受一个参数input,它是指向字符串(字符数组)的指针。在易受攻击的函数内部,名为buffer的局部变量被声明为字符数组,其固定大小为 256 字节。换句话说,buffer 最多可容纳 256 个字符(包括终止空字符“�”)。作为局部变量,buffer 仅存在于易受攻击的函数范围内;一旦函数执行完毕,buffer 使用的内存将被回收。
该函数使用strcpy()将输入的内容(传递给函数的参数)复制到缓冲区中。这是关键漏洞,因为strcpy()不检查源字符串(输入)的长度,并将输入中的所有数据复制到缓冲区中,即使它超出了缓冲区的大小(256 字节)。main ()函数接受命令行参数。argc保存参数的数量,argv[]是一个字符串数组(实际参数)。if (argc > 1)语句检查命令行上是否提供了至少一个参数(因为第一个元素argv[0]是程序名称)。
如果此代码是大型应用程序的一部分,攻击者可以将长字符串(超过 256 个字符)作为参数传递给程序。此长输入将溢出缓冲区,从而可能允许攻击者
• 覆盖重要的内存位置,例如返回地址或函数指针,导致任意代码执行。
• 程序状态损坏,导致崩溃或意外行为。
该程序不会以任何方式验证输入。没有检查以确保输入的大小小于或等于 256 字节(即缓冲区的大小)。您可以通过使用更安全的函数(如strncpy() )轻松避免此问题,该函数会限制复制的字符数以防止缓冲区溢出,或者在复制之前明确检查输入的长度。
一个真实的例子是利用媒体播放器等应用程序中的缓冲区溢出来处理格式错误的输入文件。例如,使用过多数据制作输入文件可能会导致溢出,从而覆盖关键寄存器(如指令指针 (EIP)),从而使攻击者能够将执行重定向到恶意代码。
您应该始终使用安全函数。例如,您可以将不安全的函数(如strcpy())替换为更安全的替代函数(如strncpy())。您还应该启用编译器保护。换句话说,使用编译器选项(如-fstack-protector)并启用 ASLR。
•输入验证:始终验证并清理用户输入,以确保其不超过预期大小。
•定期代码审计:定期进行代码审查并使用静态分析工具检测潜在的漏洞。
void safe_function(char *input) {
char buffer[256];
// Use strncpy to limit the copied input to the size of the buffer
strncpy(buffer, input, sizeof(buffer) - 1);
buffer[255] = '�'; // Ensure null termination
}
int main(int argc, char *argv[]) {
strncpy(buffer, input, sizeof(buffer) - 1);确保复制的字符不超过 255 个,为空终止符留出空间。然后,您可以明确地以空终止符结尾字符串,使用buffer[255] = '�';确保缓冲区是有效的 C 字符串。
什么是地址空间布局随机化 (ASLR)?
ASLR 是操作系统中使用的一种安全机制,用于防御某些类型的基于内存的攻击,特别是缓冲区溢出和面向返回编程 (ROP) 攻击。它的工作原理是随机排列进程关键数据区域的地址空间位置。这使得攻击者更难预测加载特定代码或数据的内存位置,从而使利用变得更加困难。
ASLR 会对进程的几个区域的内存地址进行随机化,包括
1.堆栈:这是存储函数调用数据(例如局部变量,返回地址)的内存区域。
2.堆:这是存储动态分配的内存(例如使用malloc() )的内存区域。
3.共享库: ASLR 会随机化包含关键系统功能的动态加载库(例如 Linux 中的 libc 或 Windows 中的 kernel32.dll)的位置。
4.可执行代码:程序的可执行代码的基地址是随机的,以便函数和指令不会出现在可预测的位置。
5.内存映射文件:用于将文件加载到内存等操作的内存区域也是随机的。
如果没有 ASLR,许多漏洞(例如缓冲区溢出或返回 libc 攻击)将更容易被利用,因为攻击者可以知道或猜测进程的内存布局。ASLR 降低了这种可预测性,因此即使发生缓冲区溢出,攻击者也无法轻易猜测内存中关键区域的位置以注入和执行恶意代码。ASLR 大大增加了编写可靠漏洞的难度,因为攻击者不再能够依赖静态地址来获取关键元素(例如堆栈、堆或库)。ASLR 与其他技术(例如数据执行保护 (DEP))结合使用时效果最佳,DEP 可防止在内存的某些区域执行代码。这些技术共同构成了对基于内存的攻击的强大防御。
一些较旧的操作系统或设计不良的应用程序可能未完全实现 ASLR,导致某些内存区域无法随机化。
使用面向返回编程 (ROP) 绕过无执行 (NX) 堆栈
不可执行 (NX) 位是现代操作系统为防止某些类型的漏洞而实施的安全功能。启用 NX 位后,标记为可写的内存区域将无法执行。此功能可防止攻击者将代码(如 shellcode)注入堆栈或堆,然后直接执行,这是早期缓冲区溢出漏洞利用中的常见攻击方法。
如果没有 NX,攻击者只需将 shellcode 注入缓冲区,让程序跳转到该缓冲区并执行该代码即可。但是,当强制执行 NX 时,这种直接方法会被阻止,因为堆栈或堆等区域(标记为不可执行)中的注入代码无法运行。
为了克服这一限制,攻击者创建了返回导向编程 (ROP),这是一种高级攻击技术,可利用程序的现有代码执行任意操作。ROP 是一种攻击技术,允许攻击者执行任意代码,即使已设置 NX 保护。攻击者不会注入和运行自己的代码,而是使用程序内存或链接库中已存在的代码片段(称为小工具)。
这些小工具通常是以 RET(返回)指令结尾的一小段指令序列。通过将这些小工具链接在一起,攻击者可以控制程序的执行流程,以实现任意操作,例如调用system()等函数来执行命令。
对于 ROP,攻击者的第一步是利用缓冲区溢出漏洞来覆盖程序的返回指令指针 (RIP)。RIP 是一个寄存器,用于保存下一条要执行的指令的地址,通过控制它,攻击者可以重定向程序的流程以执行代码的其他部分(小工具)。
操作 x86 和基于 ARM 的指令指针
RIP 是 x86-64 计算机架构中的关键组件,因为它包含函数返回后要执行的下一条指令的内存地址。调用函数时,当前指令指针保存在堆栈上。函数返回时,保存的地址将重新加载到 RIP 中。这样做是为了确保程序在函数完成后从正确的位置继续执行。RIP 是 x86-64 架构中的 64 位寄存器,而 EIP 是 32 位架构中的等效寄存器。在 ARM 架构中,x86 的 RIP 的等效物是程序计数器 (PC),也称为寄存器 R15。程序计数器的行为因所使用的指令集和模式而异。在 ARM32 模式下,由于处理器的流水线设计,当您读取 PC 时,它实际上会返回一个比当前执行指令提前 8 个字节(或两个 32 位指令)的值。存在这种行为是因为当一条指令读取 PC 时,处理器已经在其管道中获取了接下来的两条指令。
在 Thumb 模式下操作(使用 16 位压缩指令)时,读取 PC 返回的值比当前指令提前 4 个字节。尽管这仍然领先两个指令(因为每个 Thumb 指令都是 16 位),但由于压缩指令大小,偏移量较小。
ARM64 (AArch64) 简化了此行为。在 ARM64 中,PC 直接指向当前正在执行的指令,使其更直观且更易于使用。ARM64 中的这一变化消除了在使用 PC 时考虑与管道相关的偏移量的需要,这在早期的 ARM 架构中是一个常见的混淆原因。
例如,如果您在地址 0x1000 处执行一条指令,在 ARM32 模式下,读取 PC 将返回 0x1008。在 Thumb 模式下,它将返回 0x1004,而在 ARM64 模式下,它将返回 0x1000。
缓冲区溢出攻击通常会尝试操纵已保存的返回地址来劫持程序流。各种安全措施(如堆栈金丝雀和 ASLR)有助于保护返回地址的完整性。
当利用缓冲区溢出时,您(作为道德黑客)将尝试覆盖已保存的 RIP 值以将执行重定向到恶意代码。
ROP 的关键在于攻击者无需注入自己的代码。相反,他们会搜索程序或其链接库(例如 libc)中已经存在的小段代码。这些小段通常由几条指令和一条 RET 指令组成,允许程序在一条指令执行完毕后返回到下一条指令。
每个小工具都会执行一项小任务,例如在寄存器之间移动数据或执行算术运算。通过将这些小工具链接在一起,攻击者可以制作复杂的有效载荷,实现任意操作,尽管 NX 已经到位。
这些小工具使用精心设计的堆栈串联在一起。由于缓冲区溢出可以让攻击者控制堆栈,因此攻击者可以按照他们希望执行的顺序将小工具的地址放在堆栈中。每个小工具都使用 RET 指令完成执行,然后导致程序跳转到下一个小工具(其地址现在位于指令指针中)。
使用 ROP 绕过 NX
您了解到 NX(No-Execute)是一种安全功能,可将内存区域标记为不可执行,以防止代码注入攻击。让我们来看看 ROP 如何允许攻击者绕过 NX 并仍然实现任意代码执行。让我们首先识别程序代码或 libc 等常用链接库中的小工具。ROPgadget、ROPgadget.py 或 Pwntools 等工具可以自动执行此过程,扫描内存以查找以 RET、JMP 或 CALL 结尾的有用指令序列。
# Search for specific gadgets
ROPgadget --binary ./vuln_program --only "pop|ret"
# Generate complete ROP chain
ROPgadget --binary ./vuln_program --rop --chain execve
攻击者找到一个易受攻击的函数,例如允许缓冲区溢出的函数。通过利用此漏洞,攻击者可以覆盖堆栈并控制 RIP,从而控制程序的流程。
攻击者利用已识别的小工具构建 ROP 链。这是放置在堆栈上的一系列小工具地址,这样当执行每个小工具时,它会将下一个小工具的地址弹出到 RIP 中。每个小工具都会执行一个小操作,例如将数据移动到寄存器中或调用函数。
例如:
•小工具 1:将值加载到 RDI(x86-64 中的第一个参数寄存器)。
•小工具 2 :调用类似system()的函数。
攻击者的一个常见目标是通过调用 libc 中的system()之类的函数来执行系统 shell,例如 /bin/sh。为此,攻击者
• 查找将字符串(如/bin/sh)加载到RDI(64 位系统上的第一个参数寄存器)的小工具。
• 找到一个从 libc 调用system()的小工具,其 RDI 已设置为 /bin/sh。
在 64 位二进制文件中,函数参数通过寄存器传递,而不是堆栈,因此需要小工具将正确的参数加载到正确的寄存器中(例如,RDI、RSI 或 RDX)。
一旦构建了 ROP 链并控制了堆栈,程序就会被迫执行小工具链,每个小工具都会执行其小任务。最终,这会导致执行高级函数(如system("/bin/sh") ),从而为攻击者提供 shell 或其他所需的操作。
补充:
缓冲区溢出是一种软件漏洞,当程序试图在缓冲区(临时存储区域)中存储超出其容量的数据时就会发生这种情况。这可能会导致多余的数据溢出到相邻的内存位置,从而可能覆盖重要数据或指令。
以下是 C 语言中缓冲区溢出的另一个示例:
int main(int argc, char* argv[]) {
char buffer[5]; // Declare a buffer with a size of 5 bytes
strcpy(buffer, argv[1]); // Copy the first command line argument into the buffer
printf("%sn", buffer); // Print the contents of the buffer
return 0;
}
在此示例中,程序声明了一个buffer
大小为 5 个字节的函数,并使用该strcpy
函数将第一个命令行参数复制到缓冲区中。但是,如果命令行参数长度超过 5 个字节,则该strcpy
函数会将参数的所有字符复制到缓冲区中,从而导致多余的字符溢出到相邻的内存位置。
恶意攻击者可以通过向程序提供长字符串作为参数来利用此漏洞,这可能导致程序崩溃或执行任意代码。
另一个例子:
void vulnerable_function(char* user_input) {
char buffer[10];
strcpy(buffer, user_input); // copy user input into the buffer
printf("Input: %sn", buffer);
}
int main(int argc, char* argv[]) {
vulnerable_function(argv[1]);
return 0;
}
在此示例中,程序有一个名为的函数vulnerable_function
,它接受一个参数,即用户输入的字符串。然后,该函数声明一个大小为 10 字节的缓冲区,并使用该strcpy
函数将用户输入复制到缓冲区中。
但是,如果用户输入的长度超过 10 个字节,该strcpy
函数会将输入的所有字符复制到缓冲区中,导致多余的字符溢出到相邻的内存位置。
恶意攻击者可以在程序执行时通过向程序提供长字符串作为参数来利用此漏洞,这可能导致程序崩溃或执行任意代码。
修复缓冲区溢出漏洞的方法有很多种。以下是一些示例:
- 使用不同的函数:
strcpy
您可以使用类似这样的函数,而不是使用不检查缓冲区溢出的函数,strncpy
该函数带有一个附加参数,指定要复制的最大字节数。这可以防止缓冲区溢出。
strncpy(buffer, user_input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '�';
- 使用库函数:您可以使用类似的库函数
snprintf
,它可用于将有限数量的字符写入缓冲区,确保缓冲区不会溢出。
snprintf(buffer, sizeof(buffer), "%s", user_input);
- 输入验证:您可以在将用户输入复制到缓冲区之前对其进行验证,检查输入的长度是否小于缓冲区的大小。
if (strlen(user_input) < sizeof(buffer)) {
strcpy(buffer, user_input);
} else {
printf("Error: input too longn");
exit(1);
}
- 使用更安全的数据类型:您可以使用更安全的数据类型,如 C++ 中的 std::string,它会自动处理缓冲区溢出和其他安全问题。
std::string buffer;
buffer = user_input;
原文始发于微信公众号(教父爱分享):高级Linux环境红队攻击手法
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论