*** stack smashing detected ***,彻底搞懂GCC栈保护的实现原理以及问题分析套路

背景

最近在追赶项目节点时,同事遇到了一个crash问题,首先听他的描述:经过添加日志打印定位,在函数执行return之后就出现了crash
从他的描述中,该问题引起了我的兴趣。(这似乎和我平时遇到的crash并不一样)。

一般情况下,程序段错误是因为访问了非法地址,我的套路是:

  1. 程序编译时,增加调试信息,即加上-g选项,并且不需要strip
out/bin/stTsp: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.3, for GNU/Linux 2.6.32, BuildID[sha1]=f53f9bd98376e6467303c7fe16ad20ec8a7ba427, with debug_info, not stripped
  1. 通过gdb运行程序,等待问题复现。
  2. 问题复现后,通过bt指令查看堆栈信息。如下:
Program received signal SIGABRT, Aborted.
0xb6a1cee4 in raise () from /lib/libc.so.6
(gdb) bt
#0  0xb6a1cee4 in raise () from /lib/libc.so.6
#1  0xb6a21570 in abort () from /lib/libc.so.6
#2  0xb6a55290 in __libc_message () from /lib/libc.so.6
#3  0xb6adde4c in __fortify_fail () from /lib/libc.so.6
#4  0xb6adde08 in __stack_chk_fail () from /lib/libc.so.6
#5  0x7f5f8dc8 in ITU_JAC3_register ()
    at /home/xieyihua/3503-MPU/soc/stTsp/jac3/st_tsp_jac3_manager.cpp:40
#6  0x7f5f9054 in HanedleJAC3Platform ()
    at /home/xieyihua/3503-MPU/soc/stTsp/jac3/st_tsp_jac3_manager.cpp:439
#7  0x7f5c2594 in Itu_808_MainApp ()
    at /home/xieyihua/3503-MPU/soc/stTsp/src/st_tsp_gbt32960_protocol.cpp:3132
#8  0x7f5b5f28 in main (argc=<optimized out>, argv=<optimized out>)
    at /home/xieyihua/3503-MPU/soc/stTsp/src/st_tsp_main.cpp:311
  1. 结合代码上下文,进行分析。

看到这个现象最初也是有点懵,但是稍微思考一下,也就有了初步思路:

  1. 从上面gdb调试结果来看,当函数退出时,出现了SIGABORT异常。
  2. 通过我之前的文章【程序员的自我修养11】栈与函数调用过程可知,函数退出时,会做两件事。
  • 函数栈空间释放。但是其实际原理只是更改$sp指针的位置,并不会对函数栈中的局部变量做释放操作。理论上并不会导致crash。
  • 通过$bp指针,返回到上一个函数地址。若该指针被修改,导致访问到了非法地址,似乎也会导致异常。

于是乎,我初步断定:函数栈被破坏,导致的程序异常

分析思路

根据上面的怀疑,我的排查思路是:

  1. ITU_JAC3_register接口中打断点,查看返回地址$lr
  2. ITU_JAC3_register退出前,打断点。查看返回地址$lr
  3. 对比是否发生了变化,若发生了变化,则说明,还函数的返回地址被异常更改。

流程大致如下:

....
//打断点并运行
(gdb) b st_tsp_jac3_manager.cpp:20
Breakpoint 1 at 0xa3d30: file /home/xieyihua/3503-MPU/soc/stTsp/jac3/st_tsp_jac3_manager.cpp, line 20.
(gdb) b st_tsp_jac3_manager.cpp:37
Breakpoint 2 at 0xa3d90: file /home/xieyihua/3503-MPU/soc/stTsp/jac3/st_tsp_jac3_manager.cpp, line 37.
(gdb) r

....
//断点一,并查看相关寄存器
Breakpoint 1, ITU_JAC3_register ()
    at /home/xieyihua/3503-MPU/soc/stTsp/jac3/st_tsp_jac3_manager.cpp:20
20      /home/xieyihua/3503-MPU/soc/stTsp/jac3/st_tsp_jac3_manager.cpp: No such file or directory.
(gdb) info reg
r0             0x0      0
r1             0x7fd19600       2144441856
r2             0x7f86af5c       2139533148
r3             0xb66e6000       3060686848
r4             0xb6f24f10       3069333264
r5             0x7f81a690       2139203216
r6             0xb6f24f10       3069333264
r7             0x7f81a690       2139203216
r8             0x7f8de824       2140006436
r9             0x7f81e038       2139217976
r10            0x7f8bd980       2139871616
r11            0x0      0
r12            0xfffffa4c       4294965836
sp             0xbed60638       0xbed60638
lr             0x7f5f9054       2136969300
pc             0x7f5f8d30       0x7f5f8d30 <ITU_JAC3_register()+28>
cpsr           0x60070010       1611071504

....
//断点二,并查看相关寄存器
Breakpoint 2, ITU_JAC3_register ()
    at /home/xieyihua/3503-MPU/soc/stTsp/jac3/st_tsp_jac3_manager.cpp:40
40      in /home/xieyihua/3503-MPU/soc/stTsp/jac3/st_tsp_jac3_manager.cpp
(gdb) info reg
r0             0x2      2
r1             0x0      0
r2             0x7fd19600       2144441856
r3             0xbed60638       3201697336
r4             0xb6f24f10       3069333264
r5             0xbed60640       3201697344
r6             0xbed6063c       3201697340
r7             0xbed606fc       3201697532
r8             0x7f8de824       2140006436
r9             0x7f81e038       2139217976
r10            0x7f8bd980       2139871616
r11            0x0      0
r12            0x0      0
sp             0xbed60638       0xbed60638
lr             0x7f5c34a8       2136749224
pc             0x7f5f8d90       0x7f5f8d90 <ITU_JAC3_register()+124>
cpsr           0x20000010       536870928
....
//问题复现
*** stack smashing detected ***: /oemdata/stTsp terminated
[2024.04.29-17:50:50][04:stTsp  ][Debug]| msg.event=b045
[2024.04.29-17:50:50][04:stTsp  ][Info ]| AccStatus: 0
[2024.04.29-17:50:50][04:stTsp  ][Info ]| OnRtnK15Inform
[New Thread 0xab2ff440 (LWP 4679)]

Program received signal SIGABRT, Aborted.
0xb6a00ee4 in raise () from /lib/libc.so.6

//查看进程mappings
(gdb) info proc mappings
process 4212
Mapped address spaces:

        Start Addr   End Addr       Size     Offset objfile
        0x7f555000 0x7f851000   0x2fc000        0x0 /oemdata/stTsp
        0x7f851000 0x7f86c000    0x1b000   0x2fc000 /oemdata/stTsp
        0x7f86c000 0x7f911000    0xa5000        0x0 [heap]
        0xaab00000 0xaab01000     0x1000        0x0
....

分析:

  1. 第一次断点中$lr寄存器值为0x7f5f9054。结合进程mappings分析,0x7f5f9054-0x7f555000=0xa4054。再通过addr2line分析如下:
xieyihua@xieyihua:~/3503-MPU$ addr2line -e out/bin/stTsp a4054
/home/xieyihua/3503-MPU/soc/stTsp/jac3/st_tsp_jac3_manager.cpp:440
xieyihua@xieyihua:~/3503-MPU$

说明返回地址正确。

  1. 第二次断点中$lr寄存器值为0x7f5c34a8,得到返回地址为:
xieyihua@xieyihua:~/3503-MPU$ addr2line -e out/bin/stTsp 0x6E4A8
/home/xieyihua/3503-MPU/soc/stTsp/src/st_tsp_net.cpp:165

$lr的确发生了变化,但是经过分析,似乎并不是因为这个原因导致的(和调用关系有关)。因此我上述的思路就不太正确了。

栈溢出

通过gdb打印,发现了日志输出:

*** stack smashing detected ***: /oemdata/stTsp terminated

通过搜索,了解到这是栈溢出的提示打印。一般是gcc开启了栈溢出保护,当程序检测到栈溢出时,则会抛出该异常。

打开方式:

编译过程中添加编译选项-fstack-protector-fstack-protector-strong-fstack-protector-all,它们的区别为:

  • -fstack-protector,只为局部变量中包含长度超过8-byte(含)的char数组的函数插入保护代码。
  • -fstack-protector-strong,满足以下三个条件都会插入保护代码:
  1. 局部变量的地址作为赋值语句的右值或函数参数;
  2. 局部变量包含数组类型的局部变量,不管数组的长度;
  3. 带register声明的局部变量。

而我们的编译环境默认是打开的-fstack-protector-strong。因此,我的思路就变为:函数被检测到栈溢出,因为抛出异常,程序退出

为了能够快速解决问题,就需要了解两点:

  1. gcc实现栈溢出检测的原理
  2. 在代码中是如何实现的

CANARY

gcc的栈溢出检测技术,也被称作为CANARY。它的命名背景也非常有趣:

Canary 的意思是金丝雀,来源于英国矿井工人用来探查井下气体是否有毒的金丝雀笼子。工人们每次下井都会带上一只金丝雀。如果井下的气体有毒,金丝雀由于对毒性敏感就会停止鸣叫甚至死亡,从而使工人们得到预警。

同理,Linux中栈溢出的实现原理与之类似:函数开始执行的时候会先往栈里插入 canary 信息,当函数返回时验证插入的 canary 是否被修改,如果是,则说明发生了栈溢出,程序停止运行。

demo演示

我们以下面的demo作为示例:

#include <stdio.h>
void main(int argc, char **argv) {
    char buf[10];
    scanf("%s", buf);
}

我们先开启 CANARY,来看看执行的结果:

$ gcc -m32 -fstack-protector canary.c -o f.out
$ python -c 'print("A"*20)' | ./f.out
*** stack smashing detected ***: ./f.out terminated
Segmentation fault (core dumped)

接下来关闭 CANARY:

$ gcc -m32 -fno-stack-protector canary.c -o fno.out
$ python -c 'print("A"*20)' | ./fno.out
Segmentation fault (core dumped)

可以看到当开启 CANARY 的时候,提示检测到栈溢出和段错误,而关闭的时候,只有提示段错误。

接下来我们分析一下反汇编上的差异:
开启 CANARY 时:

gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x000005ad <+0>:    lea    ecx,[esp+0x4]
   0x000005b1 <+4>:    and    esp,0xfffffff0
   0x000005b4 <+7>:    push   DWORD PTR [ecx-0x4]
   0x000005b7 <+10>:    push   ebp
   0x000005b8 <+11>:    mov    ebp,esp
   0x000005ba <+13>:    push   ebx
   0x000005bb <+14>:    push   ecx
   0x000005bc <+15>:    sub    esp,0x20
   0x000005bf <+18>:    call   0x611 <__x86.get_pc_thunk.ax>
   0x000005c4 <+23>:    add    eax,0x1a3c
   0x000005c9 <+28>:    mov    edx,ecx
   0x000005cb <+30>:    mov    edx,DWORD PTR [edx+0x4]
   0x000005ce <+33>:    mov    DWORD PTR [ebp-0x1c],edx
   0x000005d1 <+36>:    mov    ecx,DWORD PTR gs:0x14                ; 将 canary 值存入 ecx
   0x000005d8 <+43>:    mov    DWORD PTR [ebp-0xc],ecx            ; 在栈 ebp-0xc 处插入 canary
   0x000005db <+46>:    xor    ecx,ecx
   0x000005dd <+48>:    sub    esp,0x8
   0x000005e0 <+51>:    lea    edx,[ebp-0x16]
   0x000005e3 <+54>:    push   edx
   0x000005e4 <+55>:    lea    edx,[eax-0x1940]
   0x000005ea <+61>:    push   edx
   0x000005eb <+62>:    mov    ebx,eax
   0x000005ed <+64>:    call   0x450 <__isoc99_scanf@plt>
   0x000005f2 <+69>:    add    esp,0x10
   0x000005f5 <+72>:    nop
   0x000005f6 <+73>:    mov    eax,DWORD PTR [ebp-0xc]                ; 从栈中取出 canary
   0x000005f9 <+76>:    xor    eax,DWORD PTR gs:0x14                    ; 检测 canary 值
   0x00000600 <+83>:    je     0x607 <main+90>
   0x00000602 <+85>:    call   0x690 <__stack_chk_fail_local>
   0x00000607 <+90>:    lea    esp,[ebp-0x8]
   0x0000060a <+93>:    pop    ecx
   0x0000060b <+94>:    pop    ebx
   0x0000060c <+95>:    pop    ebp
   0x0000060d <+96>:    lea    esp,[ecx-0x4]
   0x00000610 <+99>:    ret
End of assembler dump.

关闭 CANARY 时:

gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x0000055d <+0>:    lea    ecx,[esp+0x4]
   0x00000561 <+4>:    and    esp,0xfffffff0
   0x00000564 <+7>:    push   DWORD PTR [ecx-0x4]
   0x00000567 <+10>:    push   ebp
   0x00000568 <+11>:    mov    ebp,esp
   0x0000056a <+13>:    push   ebx
   0x0000056b <+14>:    push   ecx
   0x0000056c <+15>:    sub    esp,0x10
   0x0000056f <+18>:    call   0x59c <__x86.get_pc_thunk.ax>
   0x00000574 <+23>:    add    eax,0x1a8c
   0x00000579 <+28>:    sub    esp,0x8
   0x0000057c <+31>:    lea    edx,[ebp-0x12]
   0x0000057f <+34>:    push   edx
   0x00000580 <+35>:    lea    edx,[eax-0x19e0]
   0x00000586 <+41>:    push   edx
   0x00000587 <+42>:    mov    ebx,eax
   0x00000589 <+44>:    call   0x400 <__isoc99_scanf@plt>
   0x0000058e <+49>:    add    esp,0x10
   0x00000591 <+52>:    nop
   0x00000592 <+53>:    lea    esp,[ebp-0x8]
   0x00000595 <+56>:    pop    ecx
   0x00000596 <+57>:    pop    ebx
   0x00000597 <+58>:    pop    ebp
   0x00000598 <+59>:    lea    esp,[ecx-0x4]
   0x0000059b <+62>:    ret
End of assembler dump.

通过对比可知:
GCC在开启CANARY时,的确会向栈空间插入canary信息,并且在函数结束时,验证是否有被修改

再思考

通过上面对CANARY机制的理解,思路似乎又清晰了一些:

  1. 在执行ITU_JAC3_register函数时,该函数的canary信息被异常破坏。
  2. 找到破坏canary信息的代码段,即找到bug。

如何找到修改canary信息的代码段呢?这是一个难点,经过查阅资料和思考,流程大致如下:

  1. 对函数进行反汇编,找到canary信息所在的栈空间。
  2. 通过gdb 中的watch指令,监控该内存中的内容。(正常情况下,在函数退出前,该内存地址的内容是不会变化的)
  3. 若在函数前,该内存中的值被修改,则说明出现了栈溢出。

ITU_JAC3_register函数的反汇编如下:

000a3d14 <ITU_JAC3_register>:
   a3d14:	e59f20ac 	ldr	r2, [pc, #172]	; a3dc8 <ITU_JAC3_register+0xb4>
   a3d18:	e59fc0ac 	ldr	ip, [pc, #172]	; a3dcc <ITU_JAC3_register+0xb8>
   a3d1c:	e08f2002 	add	r2, pc, r2
   a3d20:	e92d40f0 	push	{r4, r5, r6, r7, lr}
   a3d24:	e24ddd13 	sub	sp, sp, #1216	; 0x4c0
   a3d28:	e792400c 	ldr	r4, [r2, ip]
   a3d2c:	e24dd00c 	sub	sp, sp, #12
   a3d30:	e28d6004 	add	r6, sp, #4
   a3d34:	e28d5008 	add	r5, sp, #8
   a3d38:	e5943000 	ldr	r3, [r4]
   a3d3c:	e1a00006 	mov	r0, r6
   a3d40:	e58d34c4 	str	r3, [sp, #1220]	; 0x4c4
   a3d44:	ebfffd35 	bl	a3220 <iTU_get_jac3_msg_body>
   a3d48:	e3500000 	cmp	r0, #0
   a3d4c:	1a000016 	bne	a3dac <ITU_JAC3_register+0x98>
   a3d50:	e59fe078 	ldr	lr, [pc, #120]	; a3dd0 <ITU_JAC3_register+0xbc>
   a3d54:	e28d70c4 	add	r7, sp, #196	; 0xc4
   a3d58:	e3a00001 	mov	r0, #1
   a3d5c:	e08f100e 	add	r1, pc, lr
   a3d60:	eb0007ba 	bl	a5c50 <uv_log>
   a3d64:	e1a01006 	mov	r1, r6
   a3d68:	e2453008 	sub	r3, r5, #8
   a3d6c:	e1a02007 	mov	r2, r7
   a3d70:	e3a00c01 	mov	r0, #256	; 0x100
   a3d74:	ebffff90 	bl	a3bbc <Jt808_JAC3_Register>
   a3d78:	e3500000 	cmp	r0, #0
   a3d7c:	1a000003 	bne	a3d90 <ITU_JAC3_register+0x7c>
   a3d80:	e1a00007 	mov	r0, r7
   a3d84:	e59d1000 	ldr	r1, [sp]
   a3d88:	e3a02003 	mov	r2, #3
   a3d8c:	ebff907a 	bl	87f7c <_Z15Handle808PacketPhi9TSP_SRV_T>
   a3d90:	e59d24c4 	ldr	r2, [sp, #1220]	; 0x4c4
   a3d94:	e5945000 	ldr	r5, [r4]
   a3d98:	e1520005 	cmp	r2, r5
   a3d9c:	1a000008 	bne	a3dc4 <ITU_JAC3_register+0xb0>
   a3da0:	e28ddd13 	add	sp, sp, #1216	; 0x4c0
   a3da4:	e28dd00c 	add	sp, sp, #12
   a3da8:	e8bd80f0 	pop	{r4, r5, r6, r7, pc}
   a3dac:	e59f1020 	ldr	r1, [pc, #32]	; a3dd4 <ITU_JAC3_register+0xc0>
   a3db0:	e3a00003 	mov	r0, #3
   a3db4:	e08f1001 	add	r1, pc, r1
   a3db8:	eb0007a4 	bl	a5c50 <uv_log>
   a3dbc:	e3e00000 	mvn	r0, #0
   a3dc0:	eafffff2 	b	a3d90 <ITU_JAC3_register+0x7c>
   a3dc4:	ebfedf3d 	bl	5bac0 <__stack_chk_fail@plt>
   a3dc8:	00272238 	.word	0x00272238
   a3dcc:	fffffa4c 	.word	0xfffffa4c
   a3dd0:	00225a90 	.word	0x00225a90

分析:

a3d90:	e59d24c4 	ldr	r2, [sp, #1220]	; 0x4c4
a3d94:	e5945000 	ldr	r5, [r4]
a3d98:	e1520005 	cmp	r2, r5
a3d9c:	1a000008 	bne	a3dc4 <ITU_JAC3_register+0xb0>

上述汇编语句含义大致如下:

  1. 取栈顶指针$sp+1220地址上的值,存入r2寄存器。
  2. r4寄存器中的值,存入r5寄存器。
  3. r2r5寄存器值不一样,则跳转到<ITU_JAC3_register+0xb0>
  4. <ITU_JAC3_register+0xb0>则是__stack_chk_fail接口,说明发生了栈溢出。
    综上分析,canary信息保存在$sp+1220地址中。

通过栈分布可知,$sp+1220地址实际上就是函数刚开始传入栈中r6寄存器。

gdb调试的思路大致如下:

  1. ITU_JAC3_register中打断点。(gdb) b ITU_JAC3_register
  2. 当执行到ITU_JAC3_register时,通过ni单步执行指令,运行到push {r4, r5, r6, r7, lr}(gdb) ni
  3. 查看寄存器在栈中的值。(gdb) x/5xw $sp
  4. 监控r6寄存器所在内存地址的值。(gdb) watch *(int*)0xbeaa1b0c
  5. 继续运行。(gdb) c

整体调试log如下:

/oemdata # gdb stTsp
GNU gdb (GDB) 7.9.1
Copyright (C) 2015 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "arm-oe-linux-gnueabi".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from stTsp...done.
(gdb) b ITU_JAC3_register
Breakpoint 1 at 0xa3d14: file /home/xieyihua/3503-MPU/soc/stTsp/jac3/st_tsp_jac3_manager.cpp, line 15.
(gdb) r
...
Breakpoint 1, ITU_JAC3_register ()
    at /home/xieyihua/3503-MPU/soc/stTsp/jac3/st_tsp_jac3_manager.cpp:15
15      /home/xieyihua/3503-MPU/soc/stTsp/jac3/st_tsp_jac3_manager.cpp: No such file or directory.
...
(gdb) ni
...
0x7f5f8d18      15      in /home/xieyihua/3503-MPU/soc/stTsp/jac3/st_tsp_jac3_manager.cpp
(gdb) ni
...
0x7f5f8d1c      15      in /home/xieyihua/3503-MPU/soc/stTsp/jac3/st_tsp_jac3_manager.cpp
(gdb) ni
...
0x7f5f8d20      15      in /home/xieyihua/3503-MPU/soc/stTsp/jac3/st_tsp_jac3_manager.cpp
(gdb) ni
...
0x7f5f8d24      15      in /home/xieyihua/3503-MPU/soc/stTsp/jac3/st_tsp_jac3_manager.cpp
(gdb) x/5xw $sp
0xbeaa1b04:     0x7f821b38      0x7f81a690      0xb6fadf10      0x7f81a690
0xbeaa1b14:     0x7f5f9054
(gdb) watch *(int*)0xbeaa1b0c
Hardware watchpoint 2: *(int*)0xbeaa1b0c
(gdb) c
Continuing.
Old value = -1225072880
New value = 0
operator() (__closure=0x7f8f3350)
    at /home/xieyihua/3503-MPU/soc/stTsp/CarFriend/StatisticData.cpp:323
323     /home/xieyihua/3503-MPU/soc/stTsp/CarFriend/StatisticData.cpp: No such file or directory.
(gdb)

最终定位到是另一个线程因为编码问题,导致访问到了该线程的栈空间,并持续修改,触发了栈溢出。
完结,撒花🎉🎉🎉

总结

本文通过工作中的案例,了解到gcc实现栈保护的原理,以及分享遇到相关问题的排查思路及技巧。希望能够帮助到您。

若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途

在这里插入图片描述

参考文章:https://applink.feishu.cn/client/message/link/open?token=AmX27V1AQAADZjdT9KRAgAQ%3D

  • 18
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谢艺华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值