早在 2020 年 2 月, LKML上就有一些关于kallsyms_lookup_name()
从内核取消导出的骚动。造成这种情况的主要原因是,不道德的模块开发人员通常会简单地添加MODULE_LICENSE('GPL')
到他们的代码中(而没有实际许可他们的模块)。然后,通过使用kallsyms_lookup_name()
,他们可以随心所欲地使用任何其他导出的内核函数。内核开发人员不喜欢这样,因为它使树外模块能够调用非导出函数。
显然,这对我们来说是一个问题!特别是,ftrace_helper.h
用于kallsyms_lookup_name()
获取我们想要挂钩的函数的地址。事实上,从内核版本 5.7 开始,我们不能再使用这个函数了(diff)。
除了这一更改之外,5.6 版本的发布还对 procfs 系统进行了一些其他更改。虽然这些修复相对较小,但为了在ftrace_helper.h
更新的内核上工作,还有更多的工作要做。
值得注意的是,截至撰写本文时,可用于 Ubuntu 20.04 的最新内核是
5.4.0-60-generic
,因此如果您使用 LTS,这些更改实际上不会影响您。但能够走在时代前沿真是太好了!
让我们先处理一下更改proc_create()
。这是一个非常简单的修复,但说明了一种在不破坏现有支持的情况下处理此类更改的好方法。查看v5.5.19中的声明,我们看到:
struct proc_dir_entry *proc_create(const char *name,
umode_t mode,
struct proc_dir_entry *parent,
const struct file_operations *proc_fops);
复制
我们在《Privileged Container Escapes with Kernel Modules》escape.c
中使用了该声明。然而,从 5.6 版本开始,现在看起来像这样:proc_create()
struct proc_dir_entry *proc_create(const char *name,
umode_t mode,
struct proc_dir_entry *parent,
const struct proc_ops *proc_ops);
复制
请注意,最后一个参数已从file_operations
结构体更改为proc_ops
结构体?我们需要在代码中考虑这一变化。我们关心的这些结构体之间有两个主要区别:
.owner
字段proc_ops
.read
/字段现在分别称为/.write``.proc_read``.proc_write
那么,处理这些变化的最佳方法是什么?使用预处理器!特别是,
为我们提供了LINUX_VERSION_CODE
和KERNEL_VERSION
宏。这些让我们非常简单地实现这些更改:
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,6,0)
// proc_ops version
static const struct proc_ops proc_file_fops_escape = {
.proc_write = escape_write,
};
static const struct proc_ops proc_file_fops_output = {
.proc_write = output_write,
.proc_read = output_read,
};
#else
// file_operations version
static const struct file_operations proc_file_fops_escape = {
.owner = THIS_MODULE,
.write = escape_write,
};
static const struct file_operations proc_file_fops_output = {
.owner = THIS_MODULE,
.write = output_write,
.read = output_read,
};
#endif
复制
至此,第一个问题就解决了!docker escape 现在可以编译并按5.10.6-arch1-1
预期工作。相同的代码仍然可以5.4.0-60-generic
在 Ubuntu 20.04 上编译。
此修复已合并到存储库中。您可以在此处查看上面提到的更改。
现在,我们遇到一个稍微困难一点的问题。如果没有kallsyms_lookup_name()
,我们无法轻松地将符号名称解析为内存地址,这意味着我们无法使用 ftrace 挂钩函数(回想一下,我们使用 ftrace 来注册当等于$rip
我们想要挂钩的函数的内存地址时触发的回调) )。
我最初的想法是寻找一个不同的内核函数(仍然是导出的),它可以用来无意中解析符号名称。我决定与sprint_symbol()
执行相反的操作kallsyms_lookup_name()
,即给定一个内存地址,它返回该地址处的函数名称。
使用这个,我决定从基地址向上循环地址,sprint_symbol()
每次调用并strncmp()
ing 直到找到我想要的函数。虽然有点不雅观,但效果却出奇的好。它看起来像这样:
/*
* kaddr is an unsigned long which holds the memory address being looped over
* fname_lookup is a kernel buffer which stores the name of the function at kaddr
* fname is a kernel buffer storing the function we're searching for
*/
/*
* Trick to get the kernel base address
* sprint_symbol() is less than 0x100000 bytes from the base address, so
* we can just AND-out the last 3 bytes from it's address to obtain the address
* of startup_64 (the kernel load address)
*/
kaddr = (unsigned long) &sprint_symbol;
kaddr = &= 0xffffffffff000000;
/* During testing, all the interesting functions were found below this limit */
for ( i = 0x0 ; i < 0x100000 ; i++ )
{
sprint_symbol(fname_lookup, kaddr);
if (strncmp(fname_lookup, fname, strlen(fname)) == 0)
{
/* Match! Clean up and exit */
kfree(fname_lookup);
return kaddr;
}
/* Kernel function addresses are all aligned, so we skip 0x10 bytes */
kaddr += 0x10;
}
kfree(fname_lookup);
复制
如果我最终没有使用这种技术,为什么我还要费心告诉你呢?有两个原因;首先是为了说明剥猫皮的方法总是不止一种。其次是因为我上面使用的技巧来获取内核基地址。我面临的问题是如何知道从哪里开始暴力破解。加载内核的地址被称为startup_64
(您可以在 中找到它/proc/kallsyms
),但是内核地址空间布局随机化意味着该地址将在每次启动时发生变化。然而,即使我们不能使用,我们仍然可以通过使用运算符来获取任何导出的kallsyms_lookup_name()
内核函数的地址。&
sprint_symbol
如果您检查系统上的和的地址startup_64
,您会发现只有最后 3 个字节不同。这是因为距内核开头sprint_symbol
不到字节。0x100000
这种差异在重新启动之间不会改变。因此,我们只需删除最后三个字节即可获得基地址!尽管它已经在上面的代码片段中,但我会再次将其放在这里,因为我认为它非常酷:
/* Get the address of sprint_symbol() */
kaddr = (unsigned long) &sprint_symbol;
/* Set the last 3 bytes of the address to 0x00 */
kaddr &= 0xffffffffff000000;
复制
当我致力于完善这项技术时,@f0lg0在 GitHub 上提出了一个问题,提出了这个问题,并提出了一种使用 kprobes 的很酷的技术。
Kprobe系统允许您动态地将断点插入正在运行的内核中。我们将使用它来完成kallsyms_lookup_name()
查找自身的工作!
经过一番反复讨论后,他们想出了一个非常巧妙的解决方案。他们在该评论中的代码很好地说明了主要思想。我们只需声明一个kprobe
结构体,并将.symbol_name
字段预设为kallsyms_lookup_name
。一旦注册了kprobe,我们就可以取消引用该.addr
字段来获取内存地址!
为了有效且整齐地实施这项技术,我希望所有的更改都在ftrace_helper.h
only 中。这里的技巧是使用
上面提到的宏来检查内核版本,然后在kallsyms_lookup_name()
像平常一样使用之前手动解析。
最初,我们只是包含
并声明该kprobe
结构:(请参阅此处):
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,7,0)
#define KPROBE_LOOKUP 1
#include
static struct kprobe kp = {
.symbol_name "kallsyms_lookup_name"
};
#endif
复制
就位后,在尝试使用之前kallsyms_lookup_name()
,我们只需添加以下代码片段。需要做的就是注册 kprobe,将.addr
字段分配给一个名为的符号kallsyms_lookup_name
(在适当地转换它之后),然后在完成后取消注册 kprobe(请参阅此处)。
#ifdef KPROBE_LOOKUP
/* typedef for kallsyms_lookup_name() so we can easily cast kp.addr */
typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
kallsyms_lookup_name_t kallsyms_lookup_name;
/* register the kprobe */
register_kprobe(&kp);
/* assign kallsyms_lookup_name symbol to kp.addr */
kallsyms_lookup_name (kallsyms_lookup_name_t) kp.addr;
/* done with the kprobe, so unregister it */
uregister_kprobe(&kp);
#endif
复制
当然,如果我们不是在内核 5.7+ 上进行编译,那么这些都不会触发,并且kallsyms_lookup_name()
将由内核头解析(就像之前的情况一样)。这样,我们就不必对现有代码进行任何更改ftrace_helper.h
- 并且 5.7 之前的内核版本不受影响!
最后,还有另一个小补丁修复了一直困扰我的问题。ftrace_helper.h
尽管共享相同的名称,但存储库中实际上有两个略有不同的文件。原因是我使用宏来添加__x64_
到系统调用名称,但问题是没有一种简单的方法(据我所知)仅添加__x64_
到以sys_
. ftrace_helper.h
为了解决这个问题,我只是在没有挂接系统调用时删除了相应的宏。
这是非常不优雅的,所以我决定完全删除宏,并简单地手动添加__x64_
到任何rootkit.c
挂钩系统调用的 s 中。缺点是不再自动支持 32 位内核(您必须__x64_
从HOOK()
宏中删除rootkit.c
并重新编译),但现在 32 位并没有太多问题(我实际上没有测试过任何内容) 32 位,所以我什至不知道哪些模块已损坏以及哪些模块可以工作!)。
现在,存储库中的 Rootkit 技术可与最新内核配合使用!再次感谢@f0lg0他们使用 kprobes 来解析的想法kallsyms_lookup_name()
- 绝对比暴力破解地址更简洁。
直到下一次…
阅读其他帖子
←Janus:BGGP 2021 的多语言二进制文件Fancy Bear 是一名伐木工人,没关系 - 深入了解 Drovorub 的内核组件→
哈维菲利普斯 2020 - 伦敦, 英国:: panr制作的主题
该网站是闹鬼网络的一部分
Drovorub 的内核组件→](https://xcellerator.github.io/posts/linux_rootkits_10/)
哈维菲利普斯 2020 - 伦敦, 英国:: panr制作的主题
该网站是闹鬼网络的一部分
<<< 随机 >>>