找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

478

积分

0

好友

62

主题
发表于 3 天前 | 查看: 9| 回复: 0

分享一个在排查中遇到的,因栈溢出导致局部变量被意外修改的经典案例。

问题现象

在某个嵌入式系统中,一个状态码从正常的0x01突然变为了其他值。核心逻辑代码简化后如下:

64029.png

两次打印之间仅仅调用了read_data()函数,没有任何其他代码对status变量进行赋值,但它的值确实被改变了。经过排查发现,在特定情况下,read_data函数读取到了24字节的数据,而目标缓冲区buffer只分配了16字节的空间,导致了溢出。溢出的数据恰好覆盖了紧邻的status变量。

这类栈溢出篡改数据的问题,在定位到根源后会觉得原理很简单。但当它作为一个偶现问题,且问题代码隐藏在复杂的业务逻辑中时,排查过程往往会耗费大量时间。

原因分析

为了清晰地理解status变量为何会被修改,我们需要审视函数栈帧在内存中的布局。通常,在函数调用时,其局部变量会分配在栈上。

64030.png

在常见的实现中,栈由高地址向低地址方向增长。函数内的局部变量通常按照声明顺序或编译器优化后的顺序,从高地址向低地址分配。当使用strcpy向仅16字节的buffer写入24字节数据时,多出的8字节便会向上(低地址方向)溢出,覆盖相邻的内存区域。

需要注意两点:

  1. 同一函数内局部变量的具体布局由编译器决定,不一定严格按照源代码中的声明顺序。即,先声明的变量未必存放在更高地址。
  2. 栈的生长方向是由CPU架构决定的,无法改变。

可以通过一段测试代码来验证内存布局:

64031.png

运行结果显示了变量的地址:

64032.png

可以看到,status的地址0x7ffc8b2a3c4f正好位于buffer起始地址0x7ffc8b2a3c40之后的15字节处(0x40 + 0x0f = 0x4f)。当向buffer写入超过16字节的数据时,从第17字节开始,就会覆盖到status所在的内存。

最直接的解决方案是使用带有长度检查的安全版本字符串函数。

64033.png

为什么栈会溢出?

C语言标准库中的strcpysprintf等函数本身不进行边界检查。当源数据的长度超过目标缓冲区的容量时,函数会继续将多余的数据写入相邻的内存空间。

栈上被溢出的相邻区域可能是:

  1. 其他局部变量(本例中的情况)。
  2. 函数的返回地址(这种情况更危险,可能导致程序崩溃或被利用执行恶意代码)。
  3. 保存的栈帧指针(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为缓冲区大小) 无边界检查

辅助工具

  1. 静态代码分析:在开发阶段集成静态分析工具,提前发现潜在风险。
    # 使用 Cppcheck
    cppcheck --enable=all --error-exitcode=1 src/
    # 使用 Clang Static Analyzer
    scan-build make
  2. 动态检测工具:在测试环境中默认启用动态检测工具。
    • Valgrind:强大的内存调试和分析工具。
      valgrind --leak-check=full ./test
    • AddressSanitizer (ASan):如前所述,能高效检测内存越界、使用释放后内存等问题。在网络与系统编程和底层开发中,这类工具是必不可少的调试利器。
      ASAN_OPTIONS=detect_stack_use_after_return=1 ./test

总结

  • 永不信任外部输入:始终对输入数据的长度、格式等保持警惕,即使数据来自“可信”的传感器或内部模块。
  • 善用工具:编译器提供的安全特性需要主动开启,并结合静态分析和动态检测工具构建多道防线。
  • 养成安全编码习惯:栈溢出是最常见的内存错误之一,建立“操作缓冲区必须传递其大小”的强制性编码习惯,可以预防绝大多数此类问题。



上一篇:渗透测试实战:自动化漏洞挖掘与批量扫描技巧详解
下一篇:Redis高并发性能机制深度解析:单线程模型与多路复用原理
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区(YunPan.Plus) ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-7 01:45 , Processed in 0.076491 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 CloudStack.

快速回复 返回顶部 返回列表