This page looks best with JavaScript enabled

Linux Kernel 实践(二):劫持系统调用

· ☕ 6 min read

通过劫持系统调用表,将原有系统调用替换成自定义系统调用。

前言

添加系统调用有两种方法

  • 修改内核源代码,并重新编译内核

这种耗时耗力,比较麻烦,但是是在原有的系统调用中插入新的系统调用,不会出现冲突等问题。

  • 通过内核模块重新映射系统调用地址

通过拦截系统调用表,将某个系统调用的地址修改成我们自定义的系统系统调用。

什么是系统调用表

在 Linux 中每个系统调用都有相应的系统调用号作为唯一的标识,内核维护一张系统调用表:sys_call_table

在 64 位系统中,sys_call_table 的定义在 entry/syscall_64.c#L25

1
2
3
4
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
	[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};

其中 #include <asm/syscalls_64.h> 是通过 entry/syscalls/Makefileentry/syscalls/syscall_64.tbl 为源文件编译生成的。

1
2
3
4
5
out := $(obj)/../../include/generated/asm
syscall64 := $(srctree)/$(src)/syscall_64.tbl
systbl := $(srctree)/$(src)/syscalltbl.sh
$(out)/syscalls_64.h: $(syscall64) $(systbl)
	$(call if_changed,systbl)

Makefile 通过 entry/syscalls/syscalltbl.shsyscall_64.tbl 格式化成 __SYSCALL_${abi}($nr, $entry, $compat)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/sh

in="$1"
out="$2"

grep '^[0-9]' "$in" | sort -n | (
    while read nr abi name entry compat; do
	abi=`echo "$abi" | tr '[a-z]' '[A-Z]'`
	if [ -n "$compat" ]; then
	    echo "__SYSCALL_${abi}($nr, $entry, $compat)"
	elif [ -n "$entry" ]; then
	    echo "__SYSCALL_${abi}($nr, $entry, $entry)"
	fi
    done
) > "$out"

生成后的 syscall_64.h 内容截取如下:

1
2
__SYSCALL_COMMON(0, sys_read, sys_read)
__SYSCALL_COMMON(1, sys_write, sys_write)

再看回 entry/syscall_64.c

1
2
#define __SYSCALL_COMMON(nr, sym, compat) __SYSCALL_64(nr, sym, compat)
#define __SYSCALL_64(nr, sym, compat) [nr] = sym,

所以可以得到 sys_call_table 的展开如下:

1
2
3
4
5
6
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    [0 ... __NR_syscall_max] = &sys_ni_syscall,
	[0] = sys_read,
	[1] = sys_write,
	...
};

所以可以把 sys_call_table 看作一个数组,索引为系统调用号,值为系统调用函数的起始地址。

获取 sys_call_table 地址

  1. 通过 /boot/System.map 获取
  2. 通过 /proc/kallsyms 获取
  3. 通过暴力搜索获取

前面两种方式基本一致,都是通过读取文件并过滤的方式获取。

/boot/System.map 包含整个内核镜像的符号表。

/proc/kallsyms 不仅包含内核镜像符号表,还包含所有动态加载模块的符号表。

1
2
3
4
5
6
7
8
# /boot/System.map
root@0xDayServer:~# cat /boot/System.map-$(uname -r) | grep sys_call_table
ffffffff81a001c0 R sys_call_table
ffffffff81a01520 R ia32_sys_call_table
# /proc/kallsyms
root@0xDayServer:~# cat /proc/kallsyms | grep sys_call_table
ffffffff81a001c0 R sys_call_table
ffffffff81a01520 R ia32_sys_call_table

如果要在 LKM 中 使用的话,可以将地址写在宏定义上,再进行调用。

1
#define SYS_CALL_TABLE  ffffffff81a001c0

但在不同的系统上都得进行修改并重新编译,非常麻烦。

暴力搜索

注意:在 Linux 内核 v4.17及之后 sys_close 就不再被导出。

前面提到 sys_call_table 是一个数组,索引为系统调用号,值为系统调用函数的起始地址。

内核内存空间的起始地址 PAGE_OFFSET 变量和 sys_close 系统调用在内核模块中是可见的。系统调用号在同一ABI(x86与x64属于不同ABI)中是高度后向兼容的,可以直接引用(如 __NR_close )。我们可以从内核空间起始地址开始,把每一个指针大小的内存假设成 sys_call_table 的地址,并用 __NR_close 索引去访问它的成员,如果这个值与 sys_close 的地址相同的话,就可以认为找到了 sys_call_table 的地址。

更多有关 PAGE_OFFSET 的内容请看:[ARM64 Linux 内核虚拟地址空间](https://geneblue.github.io/2017/04/02/ARM64 Linux 内核虚拟地址空间/)

下面来看搜索 sys_call_table 的函数:

ULONG_MAX 为 0xFFFFFFFFUL,即 unsigned long 的最大值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
unsigned long **get_sys_call_table(void)
{
  unsigned long **entry = (unsigned long **)PAGE_OFFSET;

  for (;(unsigned long)entry < ULONG_MAX; entry += 1) {
    if (entry[__NR_close] == (unsigned long *)sys_close) {
        return entry;
      }
  }

  return NULL;
}

劫持系统调用

写保护

当我们获取到了 sys_call_table 的地址时,并不能直接进行操作,会报错且无法写入,因为在内存中有写保护,这个特性可以通过 CR0 寄存器控制。

CR0 的第16位比特是写保护,设置时,即使权限级别为0(Linux 有4个权限级别,从0到3,0为最高级。等级0也被称为内核模式),也不能写入只读页。

我们可以通过 read_cr0write_cr0 这两个函数,来读取和写入 CR0,同时通过 Linux 内核提供的接口 set_bitclear_bit 来操作比特。

关闭写保护,将第16个比特置为0。

1
2
3
4
5
6
void disable_write_protection(void)
{
  unsigned long cr0 = read_cr0();
  clear_bit(16, &cr0);
  write_cr0(cr0);
}

开启写保护,将第16个比特置为1。

1
2
3
4
5
6
void enable_write_protection(void)
{
  unsigned long cr0 = read_cr0();
  set_bit(16, &cr0);
  write_cr0(cr0);
}

模块代码

  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
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/**
 * @file    nice.c
 * @author  WingLim
 * @date    2020-03-05
 * @version 0.1
 * @brief  读取及修改一个进程的 nice 值,并返回最新的 nice 值及优先级 prio 的模块化实现
*/

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
// 下面这些头文件为自定义系统调用要用到的
#include <linux/pid.h>
#include <linux/sched.h>
#include <linux/syscalls.h>
#include <linux/uaccess.h>
#include <linux/unistd.h>

// 这里是随便挑了一个系统调用来劫持,224 为 timer_gettime
#define the_syscall_num 224

MODULE_LICENSE("GPL");
MODULE_AUTHOR("WingLim");
MODULE_DESCRIPTION("A module to read or set nice value");
MODULE_VERSION("0.1");

// 用于保存 sys_call_table 地址
unsigned long **sys_call_table;
// 用于保存被劫持的系统调用
static int (*anything_saved)(void);

// 从内核起始地址开始搜索内存空间来获得 sys_call_table 的内存地址
unsigned long **get_sys_call_table(void)
{
  unsigned long **entry = (unsigned long **)PAGE_OFFSET;

  for (;(unsigned long)entry < ULONG_MAX; entry += 1) {
    if (entry[__NR_close] == (unsigned long *)sys_close) {
        return entry;
      }
  }
  return NULL;
}

void disable_write_protection(void)
{
  unsigned long cr0 = read_cr0();
  clear_bit(16, &cr0);
  write_cr0(cr0);
}

void enable_write_protection(void)
{
  unsigned long cr0 = read_cr0();
  set_bit(16, &cr0);
  write_cr0(cr0);
}

// 这个是用来获取进程的 prio,代码来自 task_prio
// 因为这个函数没有导出,所以拷贝一份到源码里
int get_prio(const struct task_struct *p)
{
        return p->prio - MAX_RT_PRIO;
}

asmlinkage long sys_setnice(pid_t pid, int flag, int nicevalue, int __user * prio, int __user * nice)
{
    struct pid * kpid;
        struct task_struct * task;
        int nicebef;
    int priobef;
        kpid = find_get_pid(pid); // 获取 pid
        task = pid_task(kpid, PIDTYPE_PID); // 返回 task_struct
        nicebef = task_nice(task); // 获取进程当前 nice 值
    priobef = get_prio(task); // 获取进程当前 prio 值

        if(flag == 1){
                set_user_nice(task, nicevalue);
                printk("nice value edit before:%d\tedit after:%d\n", nicebef, nicevalue);
                return 0;
        }
        else if(flag == 0){
                copy_to_user(nice, (const void*)&nicebef, sizeof(nicebef));
                copy_to_user(prio, (const void*)&priobef, sizeof(priobef));
                printk("nice of the process:%d\n", nicebef);
                printk("prio of the process:%d\n", priobef);
                return 0;
        }

        printk("the flag is undefined!\n");
        return EFAULT;
}

static int __init init_addsyscall(void)
{
    // 关闭写保护
    disable_write_protection();
    // 获取系统调用表的地址
    sys_call_table = get_sys_call_table();
    // 保存原始系统调用的地址
    anything_saved = (int(*)(void)) (sys_call_table[the_syscall_num]);
    // 将原始的系统调用劫持为自定义系统调用
    sys_call_table[the_syscall_num] = (unsigned long*)sys_setnice;
    // 恢复写保护
    enable_write_protection();
    printk("hijack syscall success\n");
    return 0;
}

static void __exit exit_addsyscall(void) {
    // 关闭写保护
    disable_write_protection();
    // 恢复原来的系统调用
    sys_call_table[the_syscall_num] = (unsigned long*)anything_saved;
    // 恢复写保护
    enable_write_protection();
    printk("resume syscall\n");
}

module_init(init_addsyscall);
module_exit(exit_addsyscall);

添加 Makefile

1
2
3
4
5
6
7
obj-m+=nice.o
KDIR = /lib/modules/$(shell uname -r)/build

all:
    make -C $(KDIR) M=$(PWD) modules
clean:
    make -C $(KDIR) M=$(PWD) clean

编译模块并启用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 编译
root@0xDayServer:~/dev/kernel/nice# make
make -C /lib/modules/4.4.0-93-generic/build/ M=/root/dev/kernel/nice modules
make[1]: Entering directory `/usr/src/linux-headers-4.4.0-93-generic'
  CC [M]  /root/dev/kernel/nice/nice.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /root/dev/kernel/nice/nice.mod.o
  LD [M]  /root/dev/kernel/nice/nice.ko
make[1]: Leaving directory `/usr/src/linux-headers-4.4.0-93-generic'
# 插入模块
root@0xDayServer:~/dev/kernel/nice# insmod nice.ko

模块测试代码

 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
/*test.c*/
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>
#define __NR_mysetnice 224 //系统调用号

int main() {
    pid_t tid;
    int nicevalue;
    int prio = 0;
    int nice = 0;
    tid = getpid();
    syscall(__NR_mysetnice,tid,0,-5,&prio,&nice);//read
    printf("pid: %d\nprio: %d\nnice: %d\n", tid, prio,nice);
    syscall(__NR_mysetnice,tid,1,-5,&prio,&nice);//set
    printf("pid: %d\nprio: %d\nnice: %d\n", tid, prio,nice);
    syscall(__NR_mysetnice,tid,0,-5,&prio,&nice);//read
    printf("pid: %d\nprio: %d\nnice: %d\n", tid, prio,nice);
    printf("*******************************\n");
    syscall(__NR_mysetnice,tid,0,-15,&prio,&nice);//read
    printf("pid: %d\nprio: %d\nnice: %d\n", tid, prio,nice);
    syscall(__NR_mysetnice,tid,1,-15,&prio,&nice);//set
    printf("pid: %d\nprio: %d\nnice: %d\n", tid, prio,nice);
    syscall(__NR_mysetnice,tid,0,-15,&prio,&nice);//read
    printf("pid: %d\nprio: %d\nnice: %d\n", tid, prio,nice);
    return 0;
}

编译测试代码并测试

 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
# 编译 test.c
root@0xDayServer:~/dev/kernel/nice# gcc test.c -o test
# 执行
root@0xDayServer:~/dev/kernel/nice# ./test
pid: 12872
prio: 20
nice: 0
pid: 12872
prio: 20
nice: 0
pid: 12872
prio: 15
nice: -5
*******************************
pid: 12872
prio: 15
nice: -5
pid: 12872
prio: 15
nice: -5
pid: 12872
prio: 5
nice: -15
# 查看模块输出信息
root@0xDayServer:~/dev/kernel/nice# tail /var/log/kern.log
Mar  7 03:52:47 0xDayServer kernel: [118009.435431] nice of the process:0
Mar  7 03:52:47 0xDayServer kernel: [118009.435434] prio of the process:20
Mar  7 03:52:47 0xDayServer kernel: [118009.435466] nice value edit before:0	edit after:-5
Mar  7 03:52:47 0xDayServer kernel: [118009.435475] nice of the process:-5
Mar  7 03:52:47 0xDayServer kernel: [118009.435476] prio of the process:15
Mar  7 03:52:47 0xDayServer kernel: [118009.435481] nice of the process:-5
Mar  7 03:52:47 0xDayServer kernel: [118009.435481] prio of the process:15
Mar  7 03:52:47 0xDayServer kernel: [118009.435485] nice value edit before:-5	edit after:-15
Mar  7 03:52:47 0xDayServer kernel: [118009.435494] nice of the process:-15
Mar  7 03:52:47 0xDayServer kernel: [118009.435495] prio of the process:5

尾语

这里提到的劫持系统调用,是 RootKit 中的一部分,RootKit 是一组工具,目标是隐藏它自身存在并继续向攻击者提供系统访问。所以我们可以通过劫持系统调用来做一些更有趣的事情,比如劫持 sys_open 来监视文件的创建。

同时,获取 sys_call_table 也有很多其他方式,比如 IDT(Interrupt Descriptor Table)、MSRs(Model-Specific Registers)在参考三中有它们的实现方式,总之,Linux Kernel 还挺有趣的,接下来再继续探索更多可玩的地方。

参考


WingLim
WRITTEN BY
WingLim
DevOps

What's on this Page