堆栈溢出检测机制 您所在的位置:网站首页 rust堆栈溢出 堆栈溢出检测机制

堆栈溢出检测机制

2022-12-10 19:35| 来源: 网络整理| 查看: 265

堆栈溢出问题总结

栈溢出所带来的问题往往十分隐蔽,有时很难复现问题,问题出现的现象可能也不一样,导致问题排查十分困难,遇到一些莫名其妙的问题时,我们会倾向于怀疑堆栈溢出,但是却又不能准确地找出问题的根源。

问题现象

最近遇到了两个死机问题,问题排查也比较困难

长时间运行死机:

能够定位问题的信息有死机时候的内核打印crashinfo以及coredump,crashinfo显示有有两种死机原因:一个是由于发生SP Alignment exception异常导致系统崩溃,另一个是unhandled level 1 translation fault (11) at 0x7f8d0347, esr 0x92000005导致系统死机。 coredump显示是死在其他组提供的so库内部函数,但是coredump的函数栈帧被破坏,无法显示完整的调用栈帧链,查看当前函数的局部变量和函数入参,发现局部变量有被破坏的痕迹。 上述现象让我们怀疑是堆栈溢出,导致局部变量和栈帧被破坏从而出现死机。

更换库后启动时死机

根据coredump显示死机位置在mod算法内部函数的memset函数,且函数调用栈显示不完整。

查看代码,memset的变量为局部变量,且该局部变量的结构体大小较大,怀疑是栈大小不足导致栈溢出

上述第一个问题是由于字符串拷贝导致堆栈溢出,第二个问题是局部变量太大导致,这些堆栈溢出问题往往不易排查。如果在代码中加入栈溢出检测机制,在运行时大部分的栈溢出就可以第一时间被发现,不会让问题潜伏。

栈溢出保护机制

gcc提供了栈保护机制stack-protector,开启了栈保护机制后,可检测运行时栈溢出,不过该选项并不是万能的,不是所有的栈溢出都能被检测到。我们平时还是需要注意不要使用体积较大的局部变量,结构体参数尽量使用指针传递,数组拷贝检查溢出,字符串拷贝检查字符串是否有'\0'结尾,尽量使用strncpy等较为安全的拷贝函数等等来避免堆栈溢出问题。

stack-protector:保护函数中通过alloca()分配缓存以及存在大于8字节的缓存。缺点是保护能力有限。 stack-protector-all:保护所有函数的栈。缺点是增加很多额外栈空间,增加程序体积。 stack-protector-strong:在stack-protector基础上,增加本地数组、指向本地帧栈地址空间保护。 stack-protector-explicit:在stack-protector基础上,增加程序中显式属性"stack_protect"空间。 stack-protector测试 #include int main() { char name[10] = {0}; strcpy(name, "stack overflowooooooooooooooooooo"); printf("%s", name); return 0; }

上述代码在执行时,如果不加stack-protector选项,程序能正常执行完成,加了stack-protector-all 选项后,执行会报错

*** stack smashing detected ***: terminated stackoverfloooooooooooooooooooooooooooooooooooooooooooooooooooAborted

分析加和不加编译选项的反汇编结果

不加栈保护选项:

0000000000400590 : 400590: a9be7bfd stp x29, x30, [sp, #-32]! //sp-32位置开始依次存放x29(sp) x30(pc),保存caller函数的返回地址和sp指针,并将sp = sp-32(分配栈空间) 400594: 910003fd mov x29, sp //保存当前栈顶sp到x29寄存器 400598: f9000bbf str xzr, [x29, #16] 40059c: 790033bf strh wzr, [x29, #24] 4005a0: 910043a2 add x2, x29, #0x10 4005a4: 90000000 adrp x0, 400000 4005a8: 911a2001 add x1, x0, #0x688 4005ac: aa0203e0 mov x0, x2 4005b0: a9400c22 ldp x2, x3, [x1] 4005b4: a9000c02 stp x2, x3, [x0] 4005b8: a9410c22 ldp x2, x3, [x1, #16] 4005bc: a9010c02 stp x2, x3, [x0, #16] 4005c0: f9401022 ldr x2, [x1, #32] 4005c4: f9001002 str x2, [x0, #32] 4005c8: b9402821 ldr w1, [x1, #40] 4005cc: b9002801 str w1, [x0, #40] 4005d0: 910043a1 add x1, x29, #0x10 4005d4: 90000000 adrp x0, 400000 4005d8: 911ae000 add x0, x0, #0x6b8 4005dc: 97ffff95 bl 400430 4005e0: 52800000 mov w0, #0x0 // #0 4005e4: a8c27bfd ldp x29, x30, [sp], #32 4005e8: d65f03c0 ret 4005ec: 00000000 .inst 0x00000000 ; undefined

加了栈保护选项的反汇编结果

0000000000400670 : 400670: a9bd7bfd stp x29, x30, [sp, #-48]! //sp-32位置开始依次存放x29(sp) x30(pc),保存caller函数的返回地址和sp指针,并将sp = sp-48(分配栈空间) 400674: 910003fd mov x29, sp //保存当前栈顶sp到x29寄存器 //diff:增加的部分 400678: 90000080 adrp x0, 410000 //获取保护数所在的页的基址,装入寄存器x0 40067c: 9137c000 add x0, x0, #0xdf0 //将x0+0xdf0,获得保护数的地址,偏移量为0xdf0 400680: f9400001 ldr x1, [x0] //将x0指向的保护数存入x1 400684: f90017a1 str x1, [x29, #40] //将保护数放入sp+40的位置,该位置就是返回地址的前一个字节 400688: d2800001 mov x1, #0x0 // #0 40068c: f9000fbf str xzr, [x29, #24] 400690: 790043bf strh wzr, [x29, #32] 400694: 910063a2 add x2, x29, #0x18 400698: 90000000 adrp x0, 400000 40069c: 911e6001 add x1, x0, #0x798 4006a0: aa0203e0 mov x0, x2 4006a4: a9400c22 ldp x2, x3, [x1] 4006a8: a9000c02 stp x2, x3, [x0] 4006ac: a9410c22 ldp x2, x3, [x1, #16] 4006b0: a9010c02 stp x2, x3, [x0, #16] 4006b4: f9401022 ldr x2, [x1, #32] 4006b8: f9001002 str x2, [x0, #32] 4006bc: b9402821 ldr w1, [x1, #40] 4006c0: b9002801 str w1, [x0, #40] 4006c4: 910063a1 add x1, x29, #0x18 4006c8: 90000000 adrp x0, 400000 4006cc: 911f2000 add x0, x0, #0x7c8 4006d0: 97ffff90 bl 400510 4006d4: 52800000 mov w0, #0x0 // #0 //增加的部分 4006d8: 90000081 adrp x1, 410000 //找到保护数的所在页的基址 4006dc: 9137c021 add x1, x1, #0xdf0 //获取保护数的地址,存入x1 4006e0: f94017a2 ldr x2, [x29, #40] //取出sp+40位置上的值 4006e4: f9400021 ldr x1, [x1] //保护数放入x1 4006e8: ca010041 eor x1, x2, x1 //将 取出的值x2和x1异或,将结果存入x1 4006ec: f100003f cmp x1, #0x0 //检查x1是否为0--即检查堆栈上的保护数是否被篡改 4006f0: 54000040 b.eq 4006f8 // b.none //相等则正常返回返回 4006f4: 97ffff7b bl 4004e0 //不等则说明堆栈有溢出,跳转执行__stack_chk_fail,进程退出 4006f8: a8c37bfd ldp x29, x30, [sp], #48 4006fc: d65f03c0 ret

通过对比加了堆栈保护选项和没加保护选项的汇编结果,可以看出在函数的开头和结尾处分别多了几条汇编语句,上述汇编结果中对于多出来的汇编语句进行了标注和注释,通过这几句汇编代码就在函数栈框中插入了一个 Canary,并实现了通过这个 canary 来检测函数栈是否被破坏。

函数栈的局部变量布局

我们来看下下面C代码的输出结果

int main() { int i = 0; char name[10] = {0}; i = 11; strcpy(name, "stack over"); printf("%s %p, %p", name,&i, name); return 0; }

不加堆栈保护编译选项:stack over 0x7fc80b9d9c, 0x7fc80b9d90

加了堆栈保护编译选项:stack over 0x7ff14e66c4, 0x7ff14e66c8

可以看出,加了编译保护选项后影响函数内的局部变量布局。堆栈的增长方向是高地址->低地址,不加编译选项的时候,变量i的地址大于变量name的地址,说明变量i在name上方;加了编译选项后,变量i变成了在name的下方。这样的内存布局在一定程度上可以减轻堆栈溢出带来的风险,因为有时候局部数组的溢出长度短的话,并不一定会触发堆栈检测,但是局部变量有可能被数组溢出篡改值,这会导致程序存在一定风险。

总结

建议在开发过程中增加堆栈溢出保护编译选项-fstack-protector-all,虽然会稍稍增加程序体积,但是带来的收益确是很客观的,很大一部分栈溢出问题就会被探测到,通过结合coredump的函数栈帧信息可以定位发生溢出的函数,这样可以大大缩小问题的范围。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有