结构体缺失的解决办法
1.直接去复制对应内核版本的对应结构体导入
这个方法直接一点,但是不同内核的结构体有时候不太一样,而且得一直复制粘贴,还会遇到嵌套结构体的问题,太jb麻烦了,所以除非结构体比较简单,这种方式会好用一些。而且导入时只需要导入到我们最后用到的那个成员就好了。比如:
1 | struct file_operations { |
这是file_operation结构体,里面有很多函数指针成员(其实这个结构体比较简单,我只是举个例子),比如我们只要使用到read和write成员,那么我们只需写成如下的样子
1 | struct file_operations { |
因为结构体只是用于解析对应内存的,我们使用file_operations->write时,实际上就是访问首地址+偏移,write底下的东西我们用不到,所以也访问不到,所以就无所谓了。但是如果我们要使用到read和release,那就必须至少导入到release,也就是如下的样子
1 | struct file_operations { |
这点应该很好理解。但是有时候你想用的成员之前有结构体嵌套的话,那就得导入很多结构体了,比较麻烦
所以为了解决这个问题,我们需要探讨一下有没有更加简单的方法
是有的,我们刚刚提到:“我们使用file_operations->write时,实际上就是访问首地址+偏移”,首地址很好解决,在kpm模块编写的情景下,首地址就是hook的函数所对应的参数(内存中该结构体的首地址)。那么我们就只要解决偏移量计算的问题了。现在先假设我们已经知道偏移量了,我们该如何使用呢?
这里有两种方法:
1.1char[x]占位
我们知道结构体的内存布局需要遵循内存对齐的原则,即结构体总内存始终是成员中所占内存最大的成员所占的内存 * 成员数,听起来很绕口,其实应该很好理解的,这样规定也很简单,原因就是加快了我们访问成员的速度。ok接下来看看这种方式该如何使用吧。假设还是用file_operations结构体:
当我们想使用flush成员,此时我们已经知道了flush相对结构体首地址的偏移offset,那么我们可以这样定义结构体:
1 | struct file_operations { |
这样使用起来是不是就简洁多了,当然如果你不想定义结构体,那你也可以直接使用,但是可读性就没那么好了。
1.2直接使用
1 | const char *name = *(const char **)(pathname + name_offset); |
但我觉得这样可读性不是很好
2.计算成员偏移量
这里依然有几种方法。但是使用场景不太一样
2.1已有内核源码
假设我们已经有了内核源码,那么我们可以直接编译一个.ko文件,打印对应成员的偏移量。
1 | struct test{ |
然后直接使用就好了。
2.2无内核源码,动态计算
这个操作就比较神奇了,当然我之前没接触过内核开发,所以孤陋寡闻觉得神奇吧。
原理:通过逆向内核,阅读汇编代码逻辑,确定加载指令类型和大概范围。在内核模块中通过遍历函数指令获取对应结构体的偏移量。
比如想要获取如下结构体的context成员:
1 | struct binder_proc { |
于是我们可以找到一个使用了binder_proc.context的函数,去逆向看他的汇编代码,这里选用binder_transaction函数
1 | static void binder_transaction(struct binder_proc *proc, |
我们就可以这么计算:
1 | uint32_t* binder_transaction_src = (uint32_t*)binder_transaction; // 将函数指针 binder_transaction 强转为 uint32_t* 类型指针 |
结语
自己用感觉还是1.1配合2.1用起来比较简单点。要想适配不同内核的话就1.2配合2.1,多定义几个版本的偏移,但是要下载好多版本的内核源码,似乎也不是很方便。在写这篇文章时觉得可能会有简单点的方法,但是写到最后才发现我所能想到的似乎都不是很简单哈哈哈。