问题描述 进程收到SIGABRT
信号异常退出,异常调用栈显示__stack_chk_fail
原因分析和定位思路 原因分析: __stack_chk_fail
说明发生了缓冲区溢出,canary被破坏 。这说明代码设置GCC编译选项fstack-protector ,开启了栈保护机制canary
定位思路:
先通过反汇编找到canary
在栈上的存放地址。
用GDB对canary
的存放地址打数据断点,定位出导致栈破坏的指令,再结合C代码具体分析。
定位过程 以下给出一个简化案例:一个可执行程序test, 依赖两个.so:libcomp1.so
, libcomp2.so
。执行test
程序后会异常退出,调用栈显示__stack_chk_fail
1 2 3 4 5 6 7 8 9 10 ├── CMakeLists.txt —— 可执行程序 test, 依赖libcomp1.so, libcomp2.so ├── comp1 │ ├── CMakeLists.txt —— libcomp1.so │ ├── comp1.c │ ├── lua.h ├── comp2 │ ├── CMakeLists.txt -- libcomp2.so │ ├── comp2.c │ ├── lua.h ├── main.c
C代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 extern void func1 () ; void main () { func1(); } #include "lua.h" extern func2 (struct lua_Debug *a, char *b, int c) ; void func1 () { struct lua_Debug a = {0 }; char *b = 0x12345678 ; int c = 0xFFFFFFFF ; func2(&a, b, c); return ; } #include "lua.h" void func2 (struct lua_Debug *a, char *b, int c) { a->i_ci = 1 ; return ; }
用GDB调试test程序,出现如下的异常调用栈
1 2 3 4 5 6 7 8 9 10 (gdb) bt #0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50 #1 0x00007ffff7e1c535 in __GI_abort () at abort.c:79 #2 0x00007ffff7e73508 in __libc_message (action=<optimized out>, fmt=fmt@entry=0x7ffff7f7e07b "*** %s ***: %s terminated\n") at ../sysdeps/posix/libc_fatal.c:181 #3 0x00007ffff7f0480d in __GI___fortify_fail_abort (need_backtrace=need_backtrace@entry=false, msg=msg@entry=0x7ffff7f7e059 "stack smashing detected") at fortify_fail.c:28 #4 0x00007ffff7f047c2 in __stack_chk_fail () at stack_chk_fail.c:29 #5 0x00007ffff7fca189 in func1 () at /home/pc/LUA/comp1/comp1.c:13 #6 0x0000555555555143 in main () at /home/pc/LUA/main.c:7
第4帧出现__stack_chk_fail
,这表示程序出现了栈溢出。定位思路如下:
1. 先通过反汇编找到canary
在栈上的存放地址。 用gdb查看出现__stack_chk_fail
的前一个函数帧,即第5帧的汇编代码。通过frame
命令切换函数帧,disassemble
查看反汇编代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 (gdb) frame 5 #5 0x00007ffff7fca189 in func1 () at /home/pc/LUA/comp1/comp1.c:13 13 } (gdb) disassemble Dump of assembler code for function func1: 0x00007ffff7fca115 <+0>: push %rbp 0x00007ffff7fca116 <+1>: mov %rsp,%rbp 0x00007ffff7fca119 <+4>: sub $0x90,%rsp # 栈上共分配144字节空间: (8字节canary + 120字节lua_Debug + 8字节char * + 4字节int, 4字节对齐 = 144字节) 0x00007ffff7fca120 <+11>: mov %fs:0x28,%rax # %fs:0x28说明,canary值是通过段寻址方式从内存中读入的 0x00007ffff7fca129 <+20>: mov %rax,-0x8(%rbp) # 将canary存储到栈中,位于$rbp - 0x8处 0x00007ffff7fca12d <+24>: xor %eax,%eax 0x00007ffff7fca12f <+26>: lea -0x80(%rbp),%rdx # comp1.c/func1(): struct lua_Debug a = {0}; 0x00007ffff7fca133 <+30>: mov $0x0,%eax 0x00007ffff7fca138 <+35>: mov $0xf,%ecx 0x00007ffff7fca13d <+40>: mov %rdx,%rdi 0x00007ffff7fca140 <+43>: rep stos %rax,%es:(%rdi) 0x00007ffff7fca143 <+46>: movq $0x12345678,-0x88(%rbp) # comp1.c/func1(): char *b = 0x12345678; 0x00007ffff7fca14e <+57>: movl $0xffffffff,-0x8c(%rbp) # comp1.c/func1(): int c = 0xFFFFFFFF; 0x00007ffff7fca158 <+67>: mov -0x8c(%rbp),%edx # c 0x00007ffff7fca15e <+73>: mov -0x88(%rbp),%rcx # b 0x00007ffff7fca165 <+80>: lea -0x80(%rbp),%rax # a 0x00007ffff7fca169 <+84>: mov %rcx,%rsi 0x00007ffff7fca16c <+87>: mov %rax,%rdi 0x00007ffff7fca16f <+90>: callq 0x7ffff7fca040 <func2@plt> 0x00007ffff7fca174 <+95>: nop 0x00007ffff7fca175 <+96>: mov -0x8(%rbp),%rax 0x00007ffff7fca179 <+100>: xor %fs:0x28,%rax # 从段寄存器取出canary的值,和栈上$rbp - 0x8处比较,如果发现canary被修改, 调用__stack_chk_fail进行错误处理 0x00007ffff7fca182 <+109>: je 0x7ffff7fca189 <func1+116> 0x00007ffff7fca184 <+111>: callq 0x7ffff7fca030 <__stack_chk_fail@plt> => 0x00007ffff7fca189 <+116>: leaveq # movq %rbp %rsp, popq %rbp 0x00007ffff7fca18a <+117>: retq End of assembler dump.
0x00007ffff7fca119:sub $0x90,%rsp
:表示栈上分配了144 个字节,依次存放三个局部变量a, b, c
0x00007ffff7fca120 <+11>: mov %fs:0x28,%rax
:其中指令参数$fs:0x28
说明canary
通过段寻址从内存中读入。canary值存放在一个特殊的段中,标志为只读,这样攻击者就无法覆盖canary的值。
0x00007ffff7fca129 <+20>: mov %rax,-0x8(%rbp)
:说明canary在栈上存放地址是$rbp - 0x8
。
根据x86_64过程调用的函数参数传递规则,可确定栈上所有局部变量和canary
是如何存放的。func1
的栈组织如下:
2. 用GDB对canary
的存放地址打数据断点,定位出导致栈破坏的指令 对func1
打断点,重新执行程序,接着用watch
命令,对canary
的存放地址打数据断点,操作如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 (gdb) disassemble Dump of assembler code for function func1: 0x00007ffff7fca115 <+0>: push %rbp 0x00007ffff7fca116 <+1>: mov %rsp,%rbp 0x00007ffff7fca119 <+4>: sub $0x90,%rsp 0x00007ffff7fca120 <+11>: mov %fs:0x28,%rax 0x00007ffff7fca129 <+20>: mov %rax,-0x8(%rbp) => 0x00007ffff7fca12d <+24>: xor %eax,%eax 0x00007ffff7fca12f <+26>: lea -0x80(%rbp),%rdx ...... End of assembler dump. (gdb) p $rbp - 0x8 $8 = (void *) 0x7fffffffe4a8 (gdb) watch *0x7fffffffe4a8 # 此处对canary在栈中存放的地址打数据断点! Hardware watchpoint 2: *0x7fffffffe4a8 (gdb) c Continuing. Hardware watchpoint 2: *0x7fffffffe4a8 Old value = 1303192064 New value = 1 (gdb) disassemble Dump of assembler code for function func2: 0x00007ffff7fc50f5 <+0>: push %rbp 0x00007ffff7fc50f6 <+1>: mov %rsp,%rbp 0x00007ffff7fc50f9 <+4>: mov %rdi,-0x8(%rbp) # 第一个入参,lua_Debug *a 0x00007ffff7fc50fd <+8>: mov %rsi,-0x10(%rbp) 0x00007ffff7fc5101 <+12>: mov %edx,-0x14(%rbp) 0x00007ffff7fc5104 <+15>: mov -0x8(%rbp),%rax # 第一个入参,lua_Debug *a 0x00007ffff7fc5108 <+19>: movq $0x1,0x78(%rax) # comp2.c/func2(): a->i_ci = 1; 这句导致canary被破坏 => 0x00007ffff7fc5110 <+27>: nop # 箭头指向的指令表示下一步即将执行的指令,也就是说是上一条指令movq $0x1,0x78(%rax)触发的数据断点! 0x00007ffff7fc5111 <+28>: pop %rbp 0x00007ffff7fc5112 <+29>: retq End of assembler dump.
数据断点被触发,定位出0x00007ffff7fc5108: $0x1,0x78(%rax)
导致canary
破坏,结合汇编上下文和C码确定,这条指令关联的C语句是a->i_ci = 1;
为什么对lua_Debug结构体的成员赋值会导致栈溢出? 由$0x1,0x78(%rax)
可以确定i_ci
成员距离结构体首地址的偏移为0x78,即120字节。事实上,在func2
的函数帧中,lua_Debug
结构体大小为128字节,而func1
中的lua_Debug
结构体只有120字节。
结合func1
的栈帧发现栈溢出的直接原因:a->i_ci = 1; 语句执行后,恰好导致canary的值被改写为1。
通过gdb
也能发现两个.so中的结构体大小不一致问题:
1 2 3 4 5 6 7 8 9 10 (gdb) f 1 #1 0x00007ffff7fca174 in func1 () at /home/pc/LUA/comp1/comp1.c:11 11 func2(&a, b, c); (gdb) p sizeof(struct lua_Debug) $24 = 120 # func1中结构体大小为120 (gdb) f 0 #0 func2 (a=0x7fffffffe430, b=0x12345678 <error: Cannot access memory at address 0x12345678>, c=-1) at /home/pc/LUA/comp2/comp2.c:5 5 return; (gdb) p sizeof(struct lua_Debug) $27 = 128 # func2中结构体大小为128
lua_Debug
结构体在lua.h
定义的,这个错误原因是由于两个.so编译使用的lua.h头文件不一致,导致栈溢出问题。 以下分别给出两个.so的lua.h
:
根据64位结构体对齐规则 ,左边结构体大小为120,右边结构体大小为128,多出的这8个字节恰好覆盖了canary
,导致栈溢出。
总结
参考资料 《深入理解计算机系统 原书第3版》 3.10.4.2 栈破坏检测