结构体缺失的解决办法
1.直接去复制对应内核版本的对应结构体导入
这个方法直接一点,但是不同内核的结构体有时候不太一样,而且得一直复制粘贴,还会遇到嵌套结构体的问题,太jb麻烦了,所以除非结构体比较简单,这种方式会好用一些。而且导入时只需要导入到我们最后用到的那个成员就好了。比如:
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *, unsigned int flags); int (*iterate) (struct file *, struct dir_context *); int (*iterate_shared) (struct file *, struct dir_context *); __poll_t (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); unsigned long mmap_supported_flags; int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); void (*splice_eof)(struct file *file); int (*setlease)(struct file *, long, struct file_lock **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f);#ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *);#endif ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags); int (*fadvise)(struct file *, loff_t, loff_t, int); int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags); int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *, unsigned int poll_flags);};
这是file_operation结构体,里面有很多函数指针成员(其实这个结构体比较简单,我只是举个例子),比如我们只要使用到read和write成员,那么我们只需写成如下的样子
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);};
因为结构体只是用于解析对应内存的,我们使用file_operations->write时,实际上就是访问首地址+偏移,write底下的东西我们用不到,所以也访问不到,所以就无所谓了。但是如果我们要使用到read和release,那就必须至少导入到release,也就是如下的样子
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *, unsigned int flags); int (*iterate) (struct file *, struct dir_context *); int (*iterate_shared) (struct file *, struct dir_context *); __poll_t (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); unsigned long mmap_supported_flags; int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *);};
这点应该很好理解。但是有时候你想用的成员之前有结构体嵌套的话,那就得导入很多结构体了,比较麻烦
所以为了解决这个问题,我们需要探讨一下有没有更加简单的方法
是有的,我们刚刚提到:“我们使用file_operations->write时,实际上就是访问首地址+偏移”,首地址很好解决,在kpm模块编写的情景下,首地址就是hook的函数所对应的参数(内存中该结构体的首地址)。那么我们就只要解决偏移量计算的问题了。现在先假设我们已经知道偏移量了,我们该如何使用呢?
这里有两种方法:
1.1char[x]占位
我们知道结构体的内存布局需要遵循内存对齐的原则,即结构体总内存始终是成员中所占内存最大的成员所占的内存 * 成员数,听起来很绕口,其实应该很好理解的,这样规定也很简单,原因就是加快了我们访问成员的速度。ok接下来看看这种方式该如何使用吧。假设还是用file_operations结构体:
当我们想使用flush成员,此时我们已经知道了flush相对结构体首地址的偏移offset,那么我们可以这样定义结构体:
struct file_operations { char[offset]; int (*flush) (struct file *, fl_owner_t id);};
这样使用起来是不是就简洁多了,当然如果你不想定义结构体,那你也可以直接使用,但是可读性就没那么好了。
1.2直接使用
const char *name = *(const char **)(pathname + name_offset);
但我觉得这样可读性不是很好
2.计算成员偏移量
这里依然有几种方法。但是使用场景不太一样
2.1已有内核源码
假设我们已经有了内核源码,那么我们可以直接编译一个.ko文件,打印对应成员的偏移量。
struct test{ int a; char b; short c; long d; char e;};
int main() { struct test t; size_t offset = (size_t)((void *)&t.d - (void *)&t); printf("size = %d\n", offset); return 0;}
然后直接使用就好了。
2.2无内核源码,动态计算
这个操作就比较神奇了,当然我之前没接触过内核开发,所以孤陋寡闻觉得神奇吧。
原理:通过逆向内核,阅读汇编代码逻辑,确定加载指令类型和大概范围。在内核模块中通过遍历函数指令获取对应结构体的偏移量。
比如想要获取如下结构体的context成员:
struct binder_proc { struct hlist_node proc_node; struct rb_root threads; struct rb_root nodes; struct rb_root refs_by_desc; struct rb_root refs_by_node; struct list_head waiting_threads; int pid; struct task_struct *tsk; const struct cred *cred; struct hlist_node deferred_work_node; int deferred_work; int outstanding_txns; bool is_dead; bool is_frozen; bool sync_recv; bool async_recv; wait_queue_head_t freeze_wait; struct dbitmap dmap; struct list_head todo; struct binder_stats stats; struct list_head delivered_death; struct list_head delivered_freeze; u32 max_threads; int requested_threads; int requested_threads_started; int tmp_ref; long default_priority; struct dentry *debugfs_entry; struct binder_alloc alloc; struct binder_context *context; spinlock_t inner_lock; spinlock_t outer_lock; struct dentry *binderfs_entry; bool oneway_spam_detection_enabled;};
于是我们可以找到一个使用了binder_proc.context的函数,去逆向看他的汇编代码,这里选用binder_transaction函数
static void binder_transaction(struct binder_proc *proc, struct binder_thread *thread, struct binder_transaction_data *tr, int reply, binder_size_t extra_buffers_size){ /**/ struct binder_context *context = proc->context; /**/}
我们就可以这么计算:
uint32_t* binder_transaction_src = (uint32_t*)binder_transaction; // 将函数指针 binder_transaction 强转为 uint32_t* 类型指针 for (u32 i = 0; i < 0x20; i++) { // 开始一个循环,遍历函数的前 32 条指令(0x20 是 16 进制,等于十进制 32) if (binder_transaction_src[i] == ARM64_RET) { // 检查当前指令是否是返回指令(ARM64_RET = 0xD65F03C0) break; // 如果是返回指令,说明函数结束,退出循环 } else if ((binder_transaction_src[i] & MASK_LDR_64_X0) == INST_LDR_64_X0) { // 检查当前指令是否是特定的 LDR 加载指令 uint64_t imm12 = bits32(binder_transaction_src[i], 21, 10); // 从指令中提取 12 位立即数 imm12(位于第 21 到 10 位) binder_proc_context_offset = sign64_extend((imm12 << 0b11u), 16u); // 将 imm12 左移 3 位并进行符号扩展,计算最终偏移量 break; } }
结语
自己用感觉还是1.1配合2.1用起来比较简单点。要想适配不同内核的话就1.2配合2.1,多定义几个版本的偏移,但是要下载好多版本的内核源码,似乎也不是很方便。在写这篇文章时觉得可能会有简单点的方法,但是写到最后才发现我所能想到的似乎都不是很简单哈哈哈。