分享一个在排查中遇到的,因栈溢出导致局部变量被意外修改的经典案例。
问题现象
在某个嵌入式系统中,一个状态码从正常的0x01突然变为了其他值。核心逻辑代码简化后如下:
两次打印之间仅仅调用了read_data()函数,没有任何其他代码对status变量进行赋值,但它的值确实被改变了。经过排查发现,在特定情况下,read_data函数读取到了24字节的数据,而目标缓冲区buffer只分配了16字节的空间,导致了溢出。溢出的数据恰好覆盖了紧邻的status变量。
这类栈溢出篡改数据的问题,在定位到根源后会觉得原理很简单。但当它作为一个偶现问题,且问题代码隐藏在复杂的业务逻辑中时,排查过程往往会耗费大量时间。
原因分析
为了清晰地理解status变量为何会被修改,我们需要审视函数栈帧在内存中的布局。通常,在函数调用时,其局部变量会分配在栈上。
在常见的实现中,栈由高地址向低地址方向增长。函数内的局部变量通常按照声明顺序或编译器优化后的顺序,从高地址向低地址分配。当使用strcpy向仅16字节的buffer写入24字节数据时,多出的8字节便会向上(低地址方向)溢出,覆盖相邻的内存区域。
需要注意两点:
- 同一函数内局部变量的具体布局由编译器决定,不一定严格按照源代码中的声明顺序。即,先声明的变量未必存放在更高地址。
- 栈的生长方向是由CPU架构决定的,无法改变。
可以通过一段测试代码来验证内存布局:
运行结果显示了变量的地址:
可以看到,status的地址0x7ffc8b2a3c4f正好位于buffer起始地址0x7ffc8b2a3c40之后的15字节处(0x40 + 0x0f = 0x4f)。当向buffer写入超过16字节的数据时,从第17字节开始,就会覆盖到status所在的内存。
最直接的解决方案是使用带有长度检查的安全版本字符串函数。
为什么栈会溢出?
C语言标准库中的strcpy、sprintf等函数本身不进行边界检查。当源数据的长度超过目标缓冲区的容量时,函数会继续将多余的数据写入相邻的内存空间。
栈上被溢出的相邻区域可能是:
- 其他局部变量(本例中的情况)。
- 函数的返回地址(这种情况更危险,可能导致程序崩溃或被利用执行恶意代码)。
- 保存的栈帧指针(EBP/RBP)。
利用编译器选项增强检测
现代编译器(如GCC 7+)提供了栈保护机制(Stack Canary),但默认配置通常只保护函数返回地址不被破坏,对于局部变量之间的溢出可能无法检测。可以通过编译选项来增强检测:
# 栈保护(主要检测返回地址破坏)
gcc -fstack-protector-all -o test test.c
# AddressSanitizer(检测多种内存越界错误)
gcc -fsanitize=address -g -o test test.c
使用 AddressSanitizer 重新编译并运行问题程序,会立即得到精确的错误报告:
===================================================================
12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc8b2a3c50
WRITE of size 24 at 0x7ffc8b2a3c40 thread T0
#0 0x... in strcpy
#1 0x... in read_data test.c:6
#2 0x... in data_process test.c:15
栈溢出预防措施
代码规范
首要的是在编码层面建立安全习惯,避免使用不安全的函数。
| 危险函数 |
安全替代 |
原因 |
strcpy |
strncpy / strlcpy (若可用) / snprintf |
无边界检查 |
sprintf |
snprintf |
无边界检查 |
gets |
fgets |
无法限制输入长度 |
scanf(“%s”) |
scanf(“%Ns”) (N为缓冲区大小) |
无边界检查 |
辅助工具
- 静态代码分析:在开发阶段集成静态分析工具,提前发现潜在风险。
# 使用 Cppcheck
cppcheck --enable=all --error-exitcode=1 src/
# 使用 Clang Static Analyzer
scan-build make
- 动态检测工具:在测试环境中默认启用动态检测工具。
总结
- 永不信任外部输入:始终对输入数据的长度、格式等保持警惕,即使数据来自“可信”的传感器或内部模块。
- 善用工具:编译器提供的安全特性需要主动开启,并结合静态分析和动态检测工具构建多道防线。
- 养成安全编码习惯:栈溢出是最常见的内存错误之一,建立“操作缓冲区必须传递其大小”的强制性编码习惯,可以预防绝大多数此类问题。
|