内核中结构体偏移计算

6.6k words

结构体缺失的解决办法

1.直接去复制对应内核版本的对应结构体导入

这个方法直接一点,但是不同内核的结构体有时候不太一样,而且得一直复制粘贴,还会遇到嵌套结构体的问题,太jb麻烦了,所以除非结构体比较简单,这种方式会好用一些。而且导入时只需要导入到我们最后用到的那个成员就好了。比如:

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
36
37
38
39
40
41
42
43
44
45
46
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结构体,里面有很多函数指针成员(其实这个结构体比较简单,我只是举个例子),比如我们只要使用到readwrite成员,那么我们只需写成如下的样子

1
2
3
4
5
6
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底下的东西我们用不到,所以也访问不到,所以就无所谓了。但是如果我们要使用到readrelease,那就必须至少导入到release,也就是如下的样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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,那么我们可以这样定义结构体:

1
2
3
4
struct file_operations {
char[offset];
int (*flush) (struct file *, fl_owner_t id);
};

这样使用起来是不是就简洁多了,当然如果你不想定义结构体,那你也可以直接使用,但是可读性就没那么好了。

1.2直接使用

1
const char *name = *(const char **)(pathname + name_offset);

但我觉得这样可读性不是很好

2.计算成员偏移量

这里依然有几种方法。但是使用场景不太一样

2.1已有内核源码

假设我们已经有了内核源码,那么我们可以直接编译一个.ko文件,打印对应成员的偏移量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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成员:

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
36
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函数

1
2
3
4
5
6
7
8
9
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;
/**/
}

我们就可以这么计算:

1
2
3
4
5
6
7
8
9
10
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,多定义几个版本的偏移,但是要下载好多版本的内核源码,似乎也不是很方便。在写这篇文章时觉得可能会有简单点的方法,但是写到最后才发现我所能想到的似乎都不是很简单哈哈哈。