我最近一直在学习 rootkit,并注意到hooking
内核级 rootkit 使用这种技术来执行恶意操作。
典型的挂钩操作是挂钩合法系统调用,然后在实际调用合法操作之前先将合法操作替换为恶意操作。
但如果是这样,为什么不让系统调用表从一开始就不可修改呢?
我最近一直在学习 rootkit,并注意到hooking
内核级 rootkit 使用这种技术来执行恶意操作。
典型的挂钩操作是挂钩合法系统调用,然后在实际调用合法操作之前先将合法操作替换为恶意操作。
但如果是这样,为什么不让系统调用表从一开始就不可修改呢?
您可以通过查找内核符号来检查它们是否是只读的。“R”表示只读。*
$ grep sys_call_table /proc/kallsyms
0000000000000000 R sys_call_table
0000000000000000 R ia32_sys_call_table
0000000000000000 R x32_sys_call_table
所以它们是只读的,并且从内核 2.6.16 开始。但是,内核 rootkit 能够使它们再次可写。它所需要做的就是在内核模式下执行这样的函数†(直接或通过足够灵活的 ROP 小工具,其中有很多),每个地址作为参数:
static void set_addr_rw(const unsigned long addr)
{
unsigned int level;
pte_t *pte = lookup_address(addr, &level);
if (pte->pte &~ _PAGE_RW)
pte->pte |= _PAGE_RW;
}
这会更改系统调用表的权限,并可以对其进行编辑。如果由于某种原因这不起作用,可以使用以下 ASM 全局禁用内核中的写保护:
cli
mov %cr0, %eax
and $~0x10000, %eax
mov %eax, %cr0
sti
这将禁用中断,禁用CR0 中的WP(写保护)位,然后重新启用中断。尽管由于写入 CR0 的预定义函数现在固定敏感位而失败,但使用汇编仍然可以工作。不过,请确保您重新启用 WP!write_cr0(read_cr0() & ~0x10000)
那么,如果它很容易禁用,为什么它会被标记为只读呢?原因之一是存在允许修改内核内存但不一定直接执行代码的漏洞。通过将内核的关键区域标记为只读,在没有发现额外漏洞将页面标记为可写(或完全禁用写保护)的情况下,利用它们变得更加困难。现在,这并不能提供非常强大的安全性,因此将其标记为只读的主要原因是为了更容易阻止意外覆盖导致灾难性和不可恢复的系统崩溃。
* 给出的特定示例是针对 x86_64 处理器的。第一个表用于本机 64 位模式 (x64) 中的系统调用。第二个是用于 32 位模式 (IA32) 的系统调用。第三个是很少使用的x32 系统调用 ABI,它允许程序在使用 32 位指针和值的同时使用 64 位模式的所有特性(例如 SSE 而不是 x87 进行浮点运算)。
† 内核的内部 API 一直在变化,因此这个确切的功能可能不适用于较旧的内核或较新的内核。CR0.WP
但是,无论内核版本如何,都可以保证在 ASM 中全局禁用所有 x86 系统。
正如 forest 所指出的,现代 Linux 不允许这样做,但它很容易被覆盖。
然而,从历史上看,它对于安全目的是有用的(也许现在仍然是):针对漏洞进行热补丁。早在 1990 年代和 2000 年代初期,每当针对我并不绝对需要的系统调用宣布新漏洞(ptrace
当时是一个非常常见的漏洞)时,我都会编写一个内核模块来覆盖系统调用表中的函数地址刚刚执行的函数的地址return -ENOSYS;
。这消除了攻击面,直到升级的内核可用。对于一些我不需要的可疑系统调用,它们反复存在漏洞,我只是先发制人地为它们做了这个,并让模块一直处于启用状态。