在IAR Embedded Workbench开发工具中如何实现堆栈保护来提高代码的安全性

本文作者:IAR Systems       点击: 2022-05-27 15:39
前言:
 
随着越来越多的嵌入式产品连接到外部网络,嵌入式产品的信息安全性(Security)越来越多地被人们关注。其中既包括直接连接到外部网络,比如通过Wi-Fi连接;也包括间接连接到外部网络,比如汽车中的ECU通过CAN总线与T-box相连,而T-box通过移动网络可以连接到外部网络。特别是对于一些高功能安全性(Safety)要求的产品,如工业,汽车,医疗产品等,信息安全成为了功能安全的前提(There Is No Safety Without Security)。

在C/C++中,堆栈缓存溢出(Stack Buffer Overflow)是一种常见的错误:当程序往堆栈缓存(Stack Buffer)写数据时,由于堆栈缓存通常采用固定长度,如果需要写的数据长度超过堆栈缓存的长度时,就会造成堆栈缓存溢出。堆栈缓存溢出会覆盖堆栈缓存临近的堆栈数据,其中可能包含函数的返回地址,就会造成函数返回时异常。如果堆栈缓存溢出是攻击者利用代码的漏洞蓄意造成的,它就称为堆栈粉碎(Stack Smashing)。堆栈粉碎是常用的一种攻击手段。

堆栈金丝雀(Stack Canaries), 因其类似于在煤矿中使用金丝雀来感测瓦斯等气体而得名,它可以用于在函数返回之前检测堆栈缓存溢出来实现堆栈保护(Stack Protection),从而提高代码的安全性。

相对于很多更加关注发挥器件性能的原厂开发工具,一些在行业中被广泛使用的商用开发工具更加关注性能和安全性的平衡性和完整性。本文以过去数十年来在行业中被广泛采用的商用工具链IAR Embedded Workbench为例,介绍如何在工具中实现堆栈保护,从而提高代码的安全性。
 
堆栈粉碎
在C/C++中,堆栈(Stack)用于保存程序正常运行(比如函数调用或者中断抢占)的临时数据,可能包含如下数据:
没有存储在寄存器中的函数参数和局部变量
没有存储在寄存器中的函数返回值和函数返回地址
CPU和寄存器状态

由于堆栈保存的是保证程序正常运行的临时数据,堆栈缓存溢出会覆盖堆栈缓存临近的堆栈数据,这些数据可能包含函数的返回地址,如果发生时一般会造成程序运行异常。攻击者经常利用这一点来进行堆栈粉碎攻击。

下面通过一个简单的例子来说明堆栈粉碎攻击:
void foo(char *bar)
{
   char c[12];

   strcpy(c, bar);  // no bounds checking

}

foo()函数将函数参数输入复制到本地堆栈变量c。如下图B所示:当函数参数输入小于12个字符时,foo()函数会正常工作。如下图C所示:当函数参数输入大于11个字符时,foo()函数会覆盖本地堆栈的数据,将函数返回地址覆盖为0x80C03508,当foo()函数返回时,会执行地址0x80C03508对应的代码A,代码A有可能包含攻击者提供的shell代码,从而使攻击者获得操作权限。         
           A数据复制前     B "hello" 作为函数参数输入    C"AAAAAAAAAAAAAAAAAAAA\x08\x35\xC0\x80"作为函数参数输入
图:堆栈粉碎示例
_
堆栈保护
因其功能类似于在煤矿中用来发现瓦斯的金丝雀而得名的堆栈金丝雀(Stack Canaries),可以用于在函数返回执行恶意代码之前检测堆栈缓存溢出。其检测原理是:当调用函数时,将需要保存的临时数据保存到堆栈,然后放置一个堆栈金丝雀,当函数返回时,检查堆栈金丝雀的值是否发生改变;如果发生改变,说明堆栈已被篡改,否则说明堆栈没有被篡改。

下面介绍如何在IAR Embedded Workbench这种广受欢迎的商用工具链中实现堆栈保护,从而提高代码的安全性:

在IAR Embedded Workbench中,会使用启发模式(Heuristic)来决定函数是否需要堆栈保护: 如果函数局部变量包含数组类型或者结构体成员包含数组类型,或者局部变量的地址在该函数外被使用,该函数需要堆栈保护。

IAR Embedded Workbench安装目录下面\src\lib\runtime包含stack_protection.c,里面包含了__stack_chk_guard变量和__stack_chk_fail函数,可以作为模板使用:其中__stack_chk_guard变量就是堆栈金丝雀的值,在函数返回时,如果检测到堆栈金丝雀的值被篡改,就会调用__stack_chk_fail函数。

1. 将IAR Embedded Workbench安装目录下面\src\lib\runtime文件夹的stack_protection.c拷贝并添加到工程。

2. 在IAR Embedded Workbench中启用堆栈保护。 

 
3. 在代码中声明堆栈保护相关的__stack_chk_guard变量和__stack_chk_fail函数。
extern uint32_t __stack_chk_guard;
__interwork __nounwind __noreturn void __stack_chk_fail(void);

4. 编译工程。编译器会在需要堆栈保护的函数中添加如下操作:在函数入口处先入栈(Push),然后再额外保存堆栈金丝雀,具体的值用户可以在stack_protection.c中更改__stack_chk_guard;在函数出口,会检测堆栈金丝雀的值是否还是__stack_chk_guard,如果不是,说明堆栈被篡改,会调用__stack_chk_fail函数。

调试
将断点打到需要堆栈保护的函数反汇编(Disassembly)入口,暂停后发现编译器在函数入口处入栈操作之后额外将堆栈金丝雀保存:
 

在函数出口处打断点,然后运行程序,在函数返回时,会先检测堆栈金丝雀的值是否还是__stack_chk_guard,如果不是,说明堆栈被篡改,会调用__stack_chk_fail函数。
 
 
改变堆栈金丝雀的值使之与__stack_chk_guard不一致,然后运行程序,函数返回时将会调用__stack_chk_fail函数:
 
 
总结
本文主要介绍了堆栈粉碎攻击如何利用堆栈缓存溢出来影响代码的安全性。通过在IAR Embedded Workbench中实现堆栈保护可以检测堆栈的完整性,从而提高代码的安全性。

参考文献:
5. IAR C/C++ Development Guide (Stack protection)

欢迎您关注IAR Systems 微信公众号