暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

栈溢出的检测

术道经纬 2019-10-14
542

说到stack(栈),大家很可能就会想起stack overflow(栈溢出),著名的程序问答网站stackoverflow.com 就是以此命名的。因为栈通常是从高地址向低地址增长的,因此"栈溢出"分为两种:超出低地址范围的overrun(上溢)和超出高地址范围的underrun(下溢),"上溢"主要是由过深的函数调用引起(比如递归调用):

而"下溢"则会出现在数组/字符串越界的时候(数组的内存分布是从低地址到高地址的)。

因为"栈溢出"造成的数据破坏很可能不会在被破坏的那一瞬间立刻显现,而是像幽灵一样潜伏着,直到之后的某个时刻,被破坏的数据再次被访问到。为了将这些幽灵扼杀在摇篮里,我们需要一些针对"栈溢出"的检测机制。


RTOS的栈溢出检测


对于那些不使用虚拟内存机制的RTOS,通常采用的做法是在stack创建之初就填充满固定的字符(比如0x5a5a5a5a),如果发生了"上溢",那么stack末端(最低地址处)的填充字符则有可能会被更改,这样操作系统就可以在发生线程切换的时候,通过检测线程栈的末端字符(比如最后16个字节)是否被更改来判断是否有"上溢"发生,当然这会增加一些线程切换的开销。


之所以说是“有可能”,是因为末端的那段字节可能正好被跳过,所以这种检测方法并不是100%有效的。

这种方法除了可以用来检测"栈溢出",还可以用来查看栈的watermark(水位线)。当上游的进水量较大的时候,河流的水面就会升高,而后即便水面下降,之前水位到达的最高点处依然是湿的。同样的道理,在线程运行过程中,栈空间的使用率有起有落,但没有被覆盖过的"0x5a5a5a5a"一定是栈未曾达到过的区域,由此我们可以计算出栈的最大使用率。如果这个最大使用率已经逼近栈的极限(最低地址),那么我们就应该适当增加该线程的栈空间大小,避免在更极端的情况下出现"栈溢出"。


至于"下溢",则可以在将函数的返回地址压栈的时候,加上一个随机产生的整数,如果出现了数组越界,那么这个整数将被修改,这样在函数返回的时候,就可以通过检测这个整数是否被修改,来判断是否有"下溢"发生。这个随机的整数被称为"canary",它的原意是金丝雀,这种鸟对危险气体的敏感度超过人类,所以过去煤矿工人往往会带着金丝雀下井,如果金丝雀死了,矿工便知道井下有危险气体,需要撤离。

那怎么加上这个canary呢,只需要在gcc编译的时候,加入"-fstack-protector"选项即可。一个函数对应一个stack frame,每个stack frame都需要一个canary,这会消耗掉一部分的栈空间。此外,由于每次函数返回时都需要检测canary,代码的整执行时间也势必会增加。

"上溢"或者"下溢"发生的时候,栈顶指针(SP - Stack Pointer)一定会超出栈的范围,所以也可以在发生线程切换的时候,检测SP指向的地址是否超过了栈的内存限定。在线程切换之前,因为需要保存线程的上下文,栈的使用率是比较高的,但这并不一定是发生overflow的时刻,所以这只能作为一种辅助的检测手段。

Linux的栈溢出检测


对于使用页表机制的Linux,借助于MMU提供的各项内存管理功能,对内存越界的检测变得容易了很多,比如vmalloc区域中广泛采用的guard page。这种guard page存在于虚拟地址空间,并不会占用实际的物理内存,但是如果内核线程的栈也想用guard page来检测"栈溢出",情况就另当别论了。


在4.14版本之前,Linux的内核栈所使用的内存位于线性映射的区域,这样的内存可以享受线性映射提供的诸多便利,包括不需要建立页表的映射,分配速度更快,可以更好的利用cache等,但有得必有失,它同时也就无法获得虚拟内存带来的若干好处了。使用线性映射,意味着占据虚拟地址空间的同时也会占用物理内存,本来一个内核栈的尺寸就比较小(8KiB或者16KiB),加上那么多guard pages对物理内存的浪费是不容小视的。


在Linux的4.14版本,内核的配置多了一个"CONFIG_VMAP_STACK"的选项。该选项是默认使能的,其带来的变化在于,内核的栈将使用vmalloc区域来获取内存,这样内核栈可以利用vmalloc现成的guard page机制来检测"栈溢出",但同时,其对应的物理内存也将不再保证是连续的。


在32位系统中,vmalloc区域通常小于128MiB,资源比较紧张,所以这个配置更适合用在64位系统中。不过,4.14是2017年11月发布的,这时大多数运行Linux的处理器应该都已经进入了64位时代。


这个变化对我们的代码会产生什么影响呢?因为栈空间不再是物理连续的,所以你不能再把它作为DMA传输的目标地址,比如函数里定义的一个数组。如果要临时开辟一段内存空间给DMA用,请老老实实的用kmalloc()函数来分配。


来看下Linux是如何制造"栈溢出"来测试这个新的"VMAP_STACK"机制的。对于"上溢",即访问了比栈的最低地址更小的内存部分,采用的guard page叫"leading":

/* Test that VMAP_STACK is actually allocating with a leading guard page */void lkdtm_STACK_GUARD_PAGE_LEADING(void){
const unsigned char *stack = task_stack_page(current);
const unsigned char *ptr = stack - 1;

byte
= *ptr;
pr_info("attempting bad read from page below current stack\n");}
文章转载自术道经纬,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论