在IAR Embedded Workbench中实现堆栈保护来提高代码的安全性

随着越来越多的嵌入式产品连接到外部网络(包括直接连接到外部网络,比如通过Wi-Fi,或者间接连接到外部网络,比如汽车中的ECU通过CAN总线与T-box相连,而T-box通过移动网络可以连接到外部网络),嵌入式产品的信息安全性(Security)越来越多地被人们关注。特别是对于一些高功能安全性(Safety)要求的产品(如工业,汽车,医疗产品等),信息安全(Security)是功能安全(Safety)的前提。

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

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

本文主要介绍如何在IAR Embedded Workbench中实现堆栈保护来提高代码的安全性。

堆栈粉碎(Stack Smashing)

在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代码,从而使攻击者获得操作权限。

图:堆栈粉碎示例

堆栈保护(Stack Protection)

堆栈金丝雀可以用于在函数返回执行恶意代码之前检测堆栈缓存溢出。其检测原理是:当调用函数时,将需要保存的临时数据保存到堆栈,然后放置一个堆栈金丝雀,当函数返回时,检查堆栈金丝雀的值是否发生改变,如果发生改变,说明堆栈被篡改,否则说明堆栈没有被篡改。

下面介绍如何在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 Canary)的值是否还是__stack_chk_guard,如果不是,说明堆栈被篡改,会调用__stack_chk_fail函数。

调试

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

在函数出口处打断点,然后运行程序,在函数返回时,会先检测堆栈金丝雀的值是否还是__stack_chk_guard,如果不是,说明堆栈被篡改,会调用__stack_chk_fail函数。

改变堆栈金丝雀的值使之与__stack_chk_guard不一致,然后运行程序,函数返回时将会调用__stack_chk_fail函数:

总结

本文主要介绍了堆栈粉碎利用堆栈缓存溢出来影响代码的安全性。通过在IAR Embedded Workbench中实现堆栈保护可以检测堆栈的完整性,从而提高代码的安全性。

参考文献:

1. https://en.wikipedia.org/wiki/Stack_buffer_overflow

2. https://cwe.mitre.org/data/definitions/121.html

3. https://en.wikipedia.org/wiki/Buffer_overflow_protection

4. IAR C/C++ Development Guide(Stack protection)

We do no longer support Internet Explorer. To get the best experience of iar.com, we recommend upgrading to a modern browser such as Chrome or Edge.