背景
最近在追赶项目节点时,同事遇到了一个crash问题,首先听他的描述:经过添加日志打印定位,在函数执行return之后就出现了crash。
从他的描述中,该问题引起了我的兴趣。(这似乎和我平时遇到的crash并不一样)。
一般情况下,程序段错误是因为访问了非法地址,我的套路是:
- 程序编译时,增加调试信息,即加上
-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
- 通过gdb运行程序,等待问题复现。
- 问题复现后,通过
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
- 结合代码上下文,进行分析。
看到这个现象最初也是有点懵,但是稍微思考一下,也就有了初步思路:
- 从上面gdb调试结果来看,当函数退出时,出现了SIGABORT异常。
- 通过我之前的文章【程序员的自我修养11】栈与函数调用过程可知,函数退出时,会做两件事。
- 函数栈空间释放。但是其实际原理只是更改
$sp
指针的位置,并不会对函数栈中的局部变量做释放操作。理论上并不会导致crash。 - 通过
$bp
指针,返回到上一个函数地址。若该指针被修改,导致访问到了非法地址,似乎也会导致异常。
于是乎,我初步断定:函数栈被破坏,导致的程序异常。
分析思路
根据上面的怀疑,我的排查思路是:
- 在
ITU_JAC3_register
接口中打断点,查看返回地址$lr
。 - 在
ITU_JAC3_register
退出前,打断点。查看返回地址$lr
。 - 对比是否发生了变化,若发生了变化,则说明,还函数的返回地址被异常更改。
流程大致如下:
....
//打断点并运行
(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
....
分析:
- 第一次断点中
$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$
说明返回地址正确。
- 第二次断点中
$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
,满足以下三个条件都会插入保护代码:
- 局部变量的地址作为赋值语句的右值或函数参数;
- 局部变量包含数组类型的局部变量,不管数组的长度;
- 带register声明的局部变量。
而我们的编译环境默认是打开的-fstack-protector-strong
。因此,我的思路就变为:函数被检测到栈溢出,因为抛出异常,程序退出。
为了能够快速解决问题,就需要了解两点:
- gcc实现栈溢出检测的原理
- 在代码中是如何实现的
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机制的理解,思路似乎又清晰了一些:
- 在执行
ITU_JAC3_register
函数时,该函数的canary信息被异常破坏。 - 找到破坏canary信息的代码段,即找到bug。
如何找到修改canary信息的代码段呢?这是一个难点,经过查阅资料和思考,流程大致如下:
- 对函数进行反汇编,找到canary信息所在的栈空间。
- 通过gdb 中的watch指令,监控该内存中的内容。(正常情况下,在函数退出前,该内存地址的内容是不会变化的)
- 若在函数前,该内存中的值被修改,则说明出现了栈溢出。
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>
上述汇编语句含义大致如下:
- 取栈顶指针
$sp
+1220地址上的值,存入r2
寄存器。 - 取
r4
寄存器中的值,存入r5
寄存器。 - 若
r2
和r5
寄存器值不一样,则跳转到<ITU_JAC3_register+0xb0>
。 <ITU_JAC3_register+0xb0>
则是__stack_chk_fail
接口,说明发生了栈溢出。
综上分析,canary信息保存在$sp
+1220地址中。
通过栈分布可知,$sp
+1220地址实际上就是函数刚开始传入栈中r6
寄存器。
gdb调试的思路大致如下:
- 在
ITU_JAC3_register
中打断点。(gdb) b ITU_JAC3_register
- 当执行到
ITU_JAC3_register
时,通过ni
单步执行指令,运行到push {r4, r5, r6, r7, lr}
。(gdb) ni
- 查看寄存器在栈中的值。
(gdb) x/5xw $sp
- 监控
r6
寄存器所在内存地址的值。(gdb) watch *(int*)0xbeaa1b0c
- 继续运行。
(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