我们在讨论栈破坏的时候,通常是数组越界访问和库函数memset/memcpy等函数的错误覆盖,对于这样的错误,通常大家知道原因排查起来很方便,但是实际情况中,还有第三种栈破坏的问题,就是结构体不匹配的类型转换导致的栈破坏。本文讨论这种栈破坏问题。
通常情况下,结构体的类型转换如果出现错误,编译的时候就会提示,例如将两个不同的结构体进行类型转换,如:
struct A a = (struct B)b;
这种错误编译器就告诉你不能这样转换。
但是如果是结构体指针,那么或许可以转换,例如
struct B *b; struct A* a = (struct A*)b;
我们发现可以将struct B的b指针强制类型转换成struct A,这种情况下编译器会提示warning
如果这种强制类型转换出现破坏了,我们查一下编译时的警告就一目了然了。
但是我们还有一种渠道,就是先将结构体指针struct B*
转换成void *
,然后将void*
的变量转换成struct A*
,如下:
struct B *b; void* t = b; struct A* a = (struct A*)t;
这种情况下,我们编译代码,将得不到任何警告或错误,因为对于C而言,这种转换本身就是有意义的。
但是如果我们代码直接这样写,那么当我们出现栈破坏的时候,我们可以通过审查代码的方式定位和解决问题。
如果我们的代码做过封装,封装的时候考虑了解耦,这种情况下,我们可以发现
struct B* 出现在服务层,我们需要在具体实现时构造struct B void* t=b 出现在接口层,为了解耦,我们需要将类型转换成通用的void struct A* 出现在客户端,我们在实现应用的时候,需要实例化一个对象,这个对象是struct A*的结构
就像这样
这种情况下,我们不方便追查问题,因为其真实的调用情况如下:
我们的app程序在调用libxxx.so时,根据的是libxxx_api.h,并不知道libxxx.so内部的实现,而app程序又会自行构造和类型转换结构体。
也就是说,如果libxxx.so的结构体和app的结构体声明存在不一致,那么可能出现问题。
那么有什么情况呢?这里以struct A和struct B为例,struct A在libxxx.so中实现,struct B在application中实现
根据上面所述的,我们存在问题的情况只有在2的时候,可能发生。这里为什么说可能发生,而不是一定发生呢,因为需要满足以下两点
如果A破坏了堆区,那么会出现堆区的异常,这里不做讨论
如果A只是占用了没分配的栈地址,那么程序不会异常
如果A只是修改了某个栈的局部变量,函数参数和临时数据,那么程序可能不会出现异常
所以这里的破坏,一定是破坏了某个栈区的结构体指针。
根据上面提到的,我们想要出现这种错误,那么我们肯定做了void*
的转换,而实际调用so的过程中,我们其实没必要非做void*
的类型转换,直接包含同一个h即可。那么什么场景下,这种转换尤为重要呢?
答案是这个数据来源于内核,因为内核的结构体和应用的结构体没有做好绑定。内核和应用又是隔离的。所以void*
的转换尤为重要。
所以,综上可以知道。 如果我们通过内核拿一个结构体A,应用层的结构体B需要做强制类型转换,如果A和B结构体是完全一致的,那么不会存在问题,假设如下两种情况:
至此,我们将这种栈破坏讲述的比较清楚了。也可以发现,这种栈破坏在满足一定条件下,实际上是很容易出现的。
为了阐述这个问题,我们先编写了一个简单的c程序,该程序直接进行强制类型转换如下:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> struct kernel{ int x; int y; int z; }; struct user{ int x; int y; int z; int o1; int o2; int o3; int o4; }; void test1(struct user* bug) { bug->x = 2; return; } void test(struct kernel k) { struct user* s = (struct user*)&k; s->o4 = 1; test1(s); return ; } int main(int argc, char *argv[]) { struct user arr = {1,2,3}; void* ss = &arr; struct kernel *b = (struct kernel*)ss; test(*b); return 0; }
我们将其编译:
gcc /root/stack_damage.c -o stack_damage
此时运行后。我们拿到一个段错误如下:
# ~/stack_damage 段错误 (核心已转储)
这里值得注意的是如下:
struct user arr = {1,2,3}; 声明了一个user的结构体 void* ss = &arr; 将其转换成void* struct kernel *b = (struct kernel*)ss; 将其强制类型转换成kernel void test(struct kernel k) 函数test会主动构造kernel结构体 struct user* s = (struct user*)&k; 此时强制类型转换给了s s->o4 = 1; 通过s破坏了结构体s自己 bug->x = 2; 这里访问自己的x时,出现了栈破坏的错误
这里还需要两个知识点如下:
上述查看B.4
如果忽略这两个要素,我们不会出现必现的栈破坏问题。
我们通过gdb来调试如下:
# gdb ./starck_damage GNU gdb (Ubuntu 9.1-0kylin1) 9.1 Copyright (C) 2020 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 "aarch64-linux-gnu". 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 ./starck_damage... (gdb)
此时运行如下:
(gdb) r Starting program: /root/stack_panic/starck_damage [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". Program received signal SIGSEGV, Segmentation fault. 0x0000000000400554 in test1 (bug=0x7f00000001) at /root/starck_damage.c:26 26 bug->x = 2;
然后我们得到了一个巨大的疑问
为什么bug->x =2 会出现段错误 此时我们应该现打一个断点test,然后重跑到断点如下:
(gdb) b test Breakpoint 1 at 0x40057c: test. (2 locations) (gdb) r The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /root/stack_panic/starck_damage [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". Breakpoint 1, test (k=...) at /root/starck_damage.c:32 32 struct user* s = (struct user*)&k;
然后定位到s->o4 = 1;之前,
(gdb) b *0x0000000000400588 Breakpoint 1 at 0x400588: file /root/starck_damage.c, line 33.
然后打印s的地址如下:
(gdb) p &s $1 = (struct user **) 0x7fffffeff8
然后计算s保存的值
(gdb) p s $2 = (struct user *) 0x7fffffefe0
我们得到地址0x7fffffefe0,然后查看其值
(gdb) x/3w 0x7fffffefe0 0x7fffffefe0: 0x00000001 0x00000002 0x00000003
此时我们将其定位到s->o4 = 1;运行之后,
(gdb) b *0x0000000000400590 Breakpoint 2 at 0x400590: file /root/starck_damage.c, line 34.
此时我们打印s的值,如下
(gdb) p s $4 = (struct user *) 0x7f00000001
可以发现s的值变成了0x7f00000001。此时我们访问
bug->x = 2;
自然出现段错误。
那么为什么会这样呢?
我们可以看到k的地址如下:
(gdb) p &k $7 = (struct kernel *) 0x7fffffefe0
然后s的地址是0x7fffffeff8
我们拿0x7fffffeff8-0x7fffffefe0=24
24/4+1=7
也就是说我们如果操作user的第7个int,那么就会破坏s的栈。
所以这也就是s->o4 = 1
;的由来。
至此,我们通过直接的示例阐述了破坏栈区的一种情况
根据上面说的,我们为了复现问题编写了c程序,但是真实的情况应该来自内核的结构体和应用的结构体不一致,所以我们先编写内核驱动如下:
struct data { int x; int y; int z; }; static long kylin_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct data user_data; user_data.x = 1; user_data.y = 2; user_data.z = 3; switch(cmd) { case RD_VALUE: if (copy_to_user((void __user *)arg, &user_data, sizeof(user_data))) { pr_err("Data Read : Err!\n"); } break; default: pr_info("Default\n"); break; } return 0; }
此时我们加载ko会在/dev/下出现kylin的字符设备
# ls /dev/kylin /dev/kylin
我们应用上先提供ioctl.c如下
#include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include "libs.h" #define RD_VALUE _IOR('a','b',int32_t*) void load_data(void* d) { struct kdata *b = (struct kdata*)d; test(*b); } int main() { int fd; struct kdata d; fd = open("/dev/kylin", O_RDWR); if(fd < 0) { return 0; } ioctl(fd, RD_VALUE, (struct kdata*) &d); printf("%d %d %d\n", d.x, d.y, d.z); load_data(&d); close(fd); }
值得注意的是这里的头文件如下:
root@kylin:~/stack_panic# cat libs.h kernel.h #include <stdio.h> #include <stdlib.h> #include <string.h> #include "kernel.h" void test(struct kdata k); #ifndef _KERNEL_STRUCT_H #define _KERNEL_STRUCT_H struct kdata { int x; int y; int z; }; #endif
我们知道应用需要调用test,所以实现libs.c如下
#include "libs.h" struct data { int x; int y; int z; int o1; int o2; int o3; int o4; }; void oops(struct data* bug) { bug->x = 2; return; } void test(struct kdata k) { struct data* s = (struct data*)&k; s->o4 = 1; oops(s); return ; }
编译如下:
gcc ioctl.c libs.c -g -o ioctl
此时运行时保存如下:
# ./ioctl 1 2 3 段错误 (核心已转储)
可以发现栈出现了破坏。
这里为了简化,我只是简单抽象了test的实现在libs.c中。但其意思符合上文描述的情况。
也就是说,应用如果调用了库,库的结构体和内核结构体不符合,那么就会出现栈破坏。
本文通过示例的方式演示了一种特殊的栈破坏情况,这种情况在内核和应用之间非常容易出现,也不容易排查。后续如果有其他情况出现这样的问题的时候,我们可以拿这种情况对号入座一下,这样就不用浪费更多的时间来排查栈破坏的问题了。