到目前为止,我们已经讨论了 Linux 二进制文件和内存,因为它属于用户域。 然而,如果我们不花一章的时间来讨论 Linux 内核,这本书就不会完整。 这是因为它实际上也是一个 ELF 二进制文件。 与程序加载到内存的方式类似,Linux 内核映像(也称为vmlinux)在引导时加载到内存中。 它有一个文本段和一个数据段,上面覆盖着许多特定于内核的分段头,你不会在用户区可执行文件中看到这些。 在本章中,我们还将简要介绍 lkm,因为它们也是 ELF 文件。
如果您想成为 Linux 内核取证的真正大师,那么学习 Linux 内核映像的布局非常重要。 攻击者可以修改内核内存来创建非常复杂的内核 rootkit。 有相当多的技术可以在运行时感染内核。 列出一些,我们有以下:
- 一种
sys_call_table
感染 - 中断处理程序打补丁
- 函数蹦床
- 调试寄存器 rootkit
- 感染异常表
- Kprobe 仪器
这里列出的技术是内核 rootkit 最常用的主要方法,内核 rootkit 通常以LKM(简称可加载内核模块)的形式感染内核。 了解每一种技术,并知道每个感染驻留在 Linux 内核中的位置,以及在内存中查找的位置,对于能够检测这类潜伏的 Linux 恶意软件至关重要。 然而,首先,让我们后退一步,看看我们需要使用什么。 目前,市场上和开源世界中有许多工具能够检测内核 rootkit 并帮助搜索内存感染。 我们不会讨论这些。 然而,我们将讨论来自 Voodoo 内核的方法。 Kernel Voodoo 是我的一个项目,除了向公众发布了一些组件,比如taskverse,它大部分仍然是私有的。 这将在本章后面讨论,并提供从下载的链接。 它使用一些非常实用的技术来检测几乎任何类型的内核感染。 该软件基于我的原创作品内核侦探的想法,这是 2009 年设计的,好奇的人可以在我的网站http://www.bitlackeys.org/#kerneldetective上找到它。
该软件只能在较旧的 32 位 Linux 内核(2.6.0 到 2.6.32)上工作; 64 位支持只是部分完成。 这个项目中的一些想法是永恒的,然而,我最近提取了它们,并结合了一些新的想法。 其结果是 Kernel Voodoo,一个主机入侵检测系统,以及内核取证软件,它依赖于/proc/kcore 进行高级内存获取和分析。 在本章中,我们将讨论它使用的一些基本技术,在某些情况下,我们将在 GDB 和/proc/kcore 中手工使用它们。
除非您编译了自己的内核,否则您不会有一个容易访问的 vmlinux,它是一个 ELF 可执行文件。 相反,您将在/boot
中拥有一个压缩的内核,通常命名为vmlinuz-<kernel_version>
。 这个压缩的内核映像可以解压,但是结果是一个没有符号表的内核可执行文件。 这给分析人员或使用 GDB 调试内核带来了问题。 在这种情况下,大多数人的解决方案是希望他们的 Linux 发行版有一个特殊的包,其内核版本有调试符号。 如果是这样,那么他们可以从发行库中下载带有符号的内核副本。 然而,在许多情况下,这是不可能的,或者因为这样或那样的原因不方便。 尽管如此,这个问题可以通过我在 2014 年设计并发布的一个自定义实用程序来解决。 这个工具被称为kdress,因为它对内核符号表进行着装。
实际上,它是以迈克尔·扎勒夫斯基的一种旧工具命名的,叫做连衣裙。 该工具将使用符号表修饰静态可执行文件。 这个名称源于这样一个事实,即人们运行一个名为的程序条带来从可执行文件中删除符号,因此“dress”对于重新构建符号表的工具来说是一个合适的名称。 我们的工具 kdress 只从System.map
文件或/proc/kallsyms
文件中获取有关符号的信息,具体取决于哪个更容易获得。 然后,它通过为符号表创建一个 section 头来将该信息重新构造到内核可执行文件中。 这个工具可以在我的 GitHub 配置文件https://github.com/elfmaster/kdress找到。
下面是一个例子,展示了如何使用 kdress 工具构建一个可以通过 GDB 加载的 vmlinux 映像:
Usage: ./kdress vmlinuz_input vmlinux_output <system.map>
$ ./kdress /boot/vmlinuz-`uname -r` vmlinux /boot/System.map-`uname -r`
[+] vmlinux has been successfully extracted
[+] vmlinux has been successfully instrumented with a complete ELF symbol table.
该实用程序创建了一个名为 vmlinux 的输出文件,它有一个完全重构的符号表。 例如,如果我们想在内核中定位sys_call_table
,那么我们可以很容易地找到它:
$ readelf -s vmlinux | grep sys_call_table
34214: ffffffff81801460 4368 OBJECT GLOBAL DEFAULT 4 sys_call_table
34379: ffffffff8180c5a0 2928 OBJECT GLOBAL DEFAULT 4 ia32_sys_call_table
拥有带有符号的内核映像对于调试和取证分析都是非常重要的。 几乎所有关于 Linux 内核的取证都可以用 GDB 和/proc/kcore
来完成。
/proc/kcore
技术是一个访问内核内存的接口,它以 ELF 核心文件的形式方便地呈现出来,可以通过 GDB 轻松地进行导航。
使用 GDB 和/proc/kcore
是一种无价的技术,可以扩展到为熟练的分析师进行非常深入的取证。 下面是一个展示如何导航sys_call_table
的简单示例。
$ sudo gdb -q vmlinux /proc/kcore
Reading symbols from vmlinux...
[New process 1]
Core was generated by `BOOT_IMAGE=/vmlinuz-3.16.0-49-generic root=/dev/mapper/ubuntu--vg-root ro quiet'.
#0 0x0000000000000000 in ?? ()
(gdb) print &sys_call_table
$1 = (<data variable, no debug info> *) 0xffffffff81801460 <sys_call_table>
(gdb) x/gx &sys_call_table
0xffffffff81801460 <sys_call_table>: 0xffffffff811d5260
(gdb) x/5i 0xffffffff811d5260
0xffffffff811d5260 <sys_read>: data32 data32 data32 xchg %ax,%ax
0xffffffff811d5265 <sys_read+5>: push %rbp
0xffffffff811d5266 <sys_read+6>: mov %rsp,%rbp
0xffffffff811d5269 <sys_read+9>: push %r14
0xffffffff811d526b <sys_read+11>:mov %rdx,%r14
在本例中,我们可以查看sys_call_table[0]
中保存的第一个指针,并确定它包含了 syscall 函数sys_read
的地址。 然后我们可以看看该系统调用的前 5 条指令。 这是一个使用 GDB 和/proc/kcore
导航内核内存是多么容易的例子。 如果安装了带有蹦床函数的内核 rootkit,那么显示前几个指令将显示跳转或返回到另一个恶意函数。 如果您知道要查找什么,那么以这种方式使用调试器来检测内核 rootkit 是非常有用的。 Linux 内核的结构上的细微差别以及它是如何被感染的都是高级的话题,对许多人来说似乎是深奥的。 一章不足以完全揭开所有这些神秘的面纱,但我们将涵盖可能被用来感染内核和检测感染的方法。 在下面几节中,我将从一般的角度讨论几种感染内核的方法,并给出一些示例。
仅使用 GDB 和/proc/kcore
,就有可能检测到本章中提到的所有类型的感染。 像内核 Voodoo 这样的工具非常好而且方便,但是对于检测与正常运行的内核的偏差并不是绝对必要的。
传统内核 rootkit,如【显示】喜欢和方阵【病人】,在sys_call_table
通过重写指针,这样他们会指向一个替代函数,然后将根据需要调用原来的系统调用。 这可以通过 LKM 或通过/dev/kmem
或/dev/mem
修改内核的程序来完成。 在当今的 Linux 系统中,出于安全原因,这些到内存的可写窗口被禁用,或者不再能够执行任何操作,只能执行读操作,这取决于内核的配置方式。 还有其他方法试图预防这种类型的感染,例如将sys_call_table
标记为const
,以便将其存储在文本段的.rodata
部分。 可以通过将相应的PTE**(即Page Table Entry)标记为可写,或者禁用cr0
寄存器中的写保护位来绕过这个问题。 因此,即使在今天,这种类型的感染也是一种非常可靠的方法来制造 rootkit,但它也很容易被检测到。**
为了检测sys_call_table
的修改,您可以查看System.map
文件或/proc/kallsyms
来查看每个系统调用的内存地址。 举例来说,如果我们想要检测sys_write
系统调用是否被感染,我们需要学习的合法地址sys_write
和sys_call_table
内的指数,然后验证正确的地址实际上是存储在内存使用 GDB 和/proc/kcore
。
$ sudo grep sys_write /proc/kallsyms
ffffffff811d5310 T sys_write
$ grep _write /usr/include/x86_64-linux-gnu/asm/unistd_64.h
#define __NR_write 1
$ sudo gdb -q vmlinux /proc/kcore
(gdb) x/gx &sys_call_table+1
0xffffffff81801464 <sys_call_table+4>: 0x811d5310ffffffff
记住,在 x86 架构上,数字以小写的形式存储。 sys_call_table[1]
的值相当于/proc/kallsyms
中查找的正确sys_write
地址。 因此,我们成功地验证了sys_write
的sys_call_table
条目没有被篡改。
这种技术最初是由 Silvio Cesare 在 1998 年提出的。 其想法是能够修改系统调用而不需要接触sys_call_table
,但事实上,这种技术允许连接内核中的任何函数。 因此,它是非常强大的。 自 1998 年以来,发生了很多变化; 内核文本片段再也不能被修改,不需要禁用写保护在cr0
或修改 PTE。主要问题,然而,是大多数现代内核使用 SMP,核函数蹦床是不安全的,因为它们使用非原子操作,比如memcpy()
每次补丁函数被调用。 事实证明,也有一些方法可以绕过这个问题,使用一种我在这里不讨论的技术。 真正的要点是,内核函数蹦床实际上仍然在使用,因此理解它们仍然是非常重要的。
它被认为是一种更安全的技术,对调用原始函数的单个调用指令进行修补,以便它们调用替换函数。 这个方法可以用作函数 trampolines 的替代方法,但是查找每一个调用可能会很困难,而且这通常在内核与内核之间发生变化。 因此,这种方法是不可移植的。
假设您想要劫持系统调用SYS_write
,并且不想担心直接修改sys_call_table
,因为它很容易被检测到。 这可以通过用包含跳转到另一个函数的代码的存根覆盖sys_write
代码的前 7 个字节来实现。
#define SYSCALL_NR __NR_write
static char syscall_code[7];
static char new_syscall_code[7] =
"\x68\x00\x00\x00\x00\xc3"; // push $addr; ret
// our new version of sys_write
int new_syscall(long fd, void *buf, size_t len)
{
printk(KERN_INFO "I am the evil sys_write!\n");
// Replace the original code back into the first 6
// bytes of sys_write (remove trampoline)
memcpy(
sys_call_table[SYSCALL_NR], syscall_code,
sizeof(syscall_code)
);
// now we invoke the original system call with no trampoline
((int (*)(fd, buf, len))sys_call_table[SYSCALL_NR])(fd, buf, len);
// Copy the trampoline back in place!
memcpy(
sys_call_table[SYSCALL_NR], new_syscall_code,
sizeof(syscall_code)
);
}
int init_module(void)
{
// patch trampoline code with address of new sys_write
*(long *)&new_syscall_code[1] = (long)new_syscall;
// insert trampoline code into sys_write
memcpy(
syscall_code, sys_call_table[SYSCALL_NR],
sizeof(syscall_code)
);
memcpy(
sys_call_table[SYSCALL_NR], new_syscall_code,
sizeof(syscall_code)
);
return 0;
}
void cleanup_module(void)
{
// remove infection (trampoline)
memcpy(
sys_call_table[SYSCALL_NR], syscall_code,
sizeof(syscall_code)
);
}
这个代码示例用一个push; ret
存根替换sys_write
的前 6 个字节,它将新的sys_write
函数的地址推入堆栈并返回给它。 新的sys_write
函数可以做任何它想做的事情,尽管在本例中我们只打印一条消息到内核日志缓冲区。 在它完成了这些狡猾的操作之后,它必须删除 trampoline 代码,以便能够调用未修改的 sys_write,最后将 trampoline 代码放回原位。
通常,函数 trampolines 会覆盖它们所挂钩的函数的过程序言部分(前 5 到 7 个字节)。 因此,为了检测任何内核函数或系统调用中的函数蹦床,您应该检查前 5 到 7 个字节,并寻找跳转或返回到另一个地址的代码。 这样的代码可以以各种形式出现。 这里有几个例子。
将目标地址推入堆栈,然后返回给它。 当使用 32 位目标地址时,这会占用 6 字节的机器码:
push $address
ret
将目标地址移到寄存器中进行间接跳转。 当使用 32 位目标地址时,需要 7 字节的代码:
movl $addr, %eax
jmp *%eax
计算偏移量并执行一个相对跳转。 当使用 32 位偏移量时,这需要 5 字节的代码:
jmp offset
例如,如果我们想验证 sys_write 系统调用是否已经被函数 trampoline 钩住,我们可以简单地检查它的代码,看看过程的 prologue 是否还在:
$ sudo grep sys_write /proc/kallsyms
0xffffffff811d5310
$ sudo gdb -q vmlinux /proc/kcore
Reading symbols from vmlinux...
[New process 1]
Core was generated by `BOOT_IMAGE=/vmlinuz-3.16.0-49-generic root=/dev/mapper/ubuntu--vg-root ro quiet'.
#0 0x0000000000000000 in ?? ()
(gdb) x/3i 0xffffffff811d5310
0xffffffff811d5310 <sys_write>: data32 data32 data32 xchg %ax,%ax
0xffffffff811d5315 <sys_write+5>: push %rbp
0xffffffff811d5316 <sys_write+6>: mov %rsp,%rbp
前 5 个字节是,实际上用作对齐的 NOP 指令(也可能是 ftrace 探针的空间)。 内核使用特定的字节序列(0x66、0x66、0x66、0x66 和 0x90)。 过程序言代码遵循最初的 5 个 NOP 字节,并且完全完整。 因此,这将验证sys_write
系统调用没有与任何函数 trampolines 挂钩。
感染内核的一个经典方法是,在内核内存中插入一个虚假的系统调用表,并修改负责调用系统调用的上半部分中断处理程序。 在 x86 体系结构中,中断 0x80 已弃用,取而代之的是用于调用系统调用的特殊的syscall/sysenter
指令。 syscall/sysenter 和int 0x80
最终调用同一个名为system_call()
的函数,该函数依次调用sys_call_table
中所选的系统调用:
(gdb) x/i system_call_fastpath+19
0xffffffff8176ea86 <system_call_fastpath+19>:
callq *-0x7e7feba0(,%rax,8)
在 x86_64 上,前面的调用指令发生在system_call()
中的交换之后。 下面是代码在entry.S
中的样子:
call *sys_call_table(,%rax,8)
(r/e)ax
寄存器包含了系统调用号乘以sizeof(long)
,以使索引进入正确的系统调用指针。 很容易想象,攻击者可以将一个伪系统调用表kmalloc()
放入内存(其中包含一些修改,其中包含指向恶意函数的指针),然后修补调用指令,以便使用伪系统调用表。 这种技术实际上是相当隐形的,因为它没有对原来的sys_call_table
进行任何修改。 然而,对入侵者来说不幸的是,这种技术仍然很容易被训练有素的眼睛发现。
检测是否system_call()
常规修补了调用一个假的sys_call_table
与否,只是反汇编代码 GDB 和/proc/kcore
,然后找出是否调用抵消指向的地址sys_call_table
。 正确的sys_call_table
地址可在System.map
或/proc/kallsyms
中找到。
这种特殊类型的内核 rootkit 最初是在我 2010 年写的一篇 Phrack 论文中构思和详细描述的。 全文见http://phrack.org/issues/67/6.html。
这种类型的内核 rootkit 是比较奇特的一种,因为它使用 Linux 内核 Kprobe 调试钩子在 rootkit 试图修改的目标内核函数上设置断点。 这种特殊的技术有其局限性,但它可以相当强大和隐形。 然而,就像任何其他技术一样,如果分析人员知道要查找什么,那么使用 kprobes 的内核 rootkit 就可以很容易地检测到。
通过分析内存来检测 kprobes 的存在是相当容易的。 当设置一个常规的 kprobe 时,一个断点被放置在函数的入口点(参见 jprobes)或任意指令上。 通过扫描整个代码段寻找断点,这是非常容易检测到的,因为除了为了 kprobes,没有理由在内核代码中放置断点。 对于检测优化的 kprobes,使用 jmp 指令代替断点(int3
)指令。 当 jmp 被放置在函数的第一个字节时,这将是最容易检测的,因为这显然是不合适的。 最后,在/sys/kernel/debug/kprobes/list
中有一个活动 kprobes 的简单列表,该列表实际上包含正在使用的 kprobes 的列表。 但是,任何 rootkit,包括我在 phrack 中演示的那个,都将对文件隐藏其 kprobes,所以不要依赖它。 一个好的 rootkit 还可以防止在/sys/kernel/debug/kprobes/enabled
中禁用的 kprobes。
这种类型的内核 rootkit 使用 Intel Debug 寄存器作为劫持控制流的手段。 一篇伟大的 Phrack 论文是由半死不活写的。 可在此查阅:
http://phrack.org/issues/65/8.html。
这种技术通常被称为超隐形,因为它不需要修改sys_call_table
。 然而,同样地,也有检测这种类型感染的方法。
在许多 rootkit 实现中,sys_call_table
和其他常见的感染点没有被修改,但int1
处理程序没有。 对do_debug
函数的调用指令进行修补,以调用另一个do_debug
函数,如前面链接的 phrack 论文所示。 因此,检测这种类型的 rootkit 通常就像分解 int1 处理程序并查看call do_debug
指令的偏移量一样简单,如下所示:
target_address = address_of_call + offset + 5
如果target_address
与System.map
或/proc/kallsyms
中找到的do_debug
地址的值相同,这意味着 int1 处理程序没有被修补,认为是干净的。
感染内核的另一种经典的和强大的方法是通过感染内核的 VFS 层。 这种技术非常神奇,而且相当隐蔽,因为它在技术上修改的是内存中的数据段,而不是文本段,其中的差异更容易检测。 VFS 层是非常面向对象的,包含各种带有函数指针的结构体。 这些函数指针是文件系统操作,比如打开、读、写、readdir 等等。 如果攻击者可以修补这些函数指针,那么他们就可以以任何他们认为合适的方式控制这些操作。
可能有几种检测这种类型感染的技术。 然而,一般的想法是验证函数指针地址,并确认它们是否指向预期的函数。 在大多数情况下,这些函数应该指向内核中的函数,而不是 lkm 中存在的函数。 一种快速的检测方法是验证指针是否在内核的文本段的范围内。
if ((long)vfs_ops->readdir >= KERNEL_MIN_ADDR &&
(long)vfs_ops->readdir < KERNEL_MAX_ADDR)
pointer_is_valid = 1;
else
pointer_is_valid = 0;
黑客还可以使用其他的技术来感染 Linux 内核(我们没有在本章中讨论这些),例如劫持 Linux 页面错误处理程序(http://phrack.org/issues/61/7.html)。 可以通过查找对文本段的修改来检测其中的许多技术,这是一种检测方法,我们将在下一节中进一步研究。
在我看来,唯一最有效的 rootkit 检测方法可以通过在内存中验证内核的代码完整性来总结——换句话说,将内核内存中的代码与预期的代码进行比较。 但是我们可以用什么来比较内核内存代码呢? 那么,为什么不是 vmlinux 呢? 这是我在 2008 年最初探索的方法。 知道 ELF 可执行文件的文本段不会从磁盘更改到内存,除非它是某种奇怪的自修改二进制文件,而内核不是这样的… 我很快就遇到了麻烦,并且发现了内核内存文本段和 vmlinux 文本段之间的各种代码差异。 这在一开始是令人困惑的,因为在这些测试期间我没有安装内核 rootkit。 然而,在检查了 vmlinux 中的 ELF 部分之后,我很快发现了一些引起我注意的地方:
$ readelf -S vmlinux | grep alt
[23] .altinstructions PROGBITS ffffffff81e64528 01264528
[24] .altinstr_replace PROGBITS ffffffff81e6a480 0126a480
Linux 内核二进制文件中有几个部分包含替代指令。 随着的出现,Linux 内核开发人员有了一个聪明的想法:如果 Linux 内核可以在运行时智能地修补它自己的代码段,根据检测到的特定 CPU 改变“内存障碍”的某些指令,会怎么样? 这将是一个很好的想法,因为需要为所有不同类型的 cpu 创建更少的库存内核。 不幸的是,对于那些想要检测内核代码段中任何恶意更改的安全研究人员来说,这些替代指令必须首先被理解和应用。
有两个部分,其中包含了需要知道内核中哪些指令在运行时被修补的大部分信息。 现在有一篇很棒的文章解释了这些部分,在我早期研究内核的这个领域时还没有:
https://lwn.net/Articles/531148/
然而,一般的想法是,.altinstructions
节包含一个由struct alt_instr
结构体组成的数组。 每一个代表一个替代指令记录,给你原始指令的位置和新指令的位置,应该用来修补原始指令。 .altinstr_replace
部分包含了alt_instr->repl_offset
成员引用的实际替代指令。
struct alt_instr {
s32 instr_offset; /* original instruction */
s32 repl_offset; /* offset to replacement instruction */
u16 cpuid; /* cpuid bit set for replacement */
u8 instrlen; /* length of original instruction */
u8 replacementlen; /* length of new instruction, <= instrlen */
};
在较老的内核中,前两个成员给出了新旧指令的绝对地址,但在较新的内核中,使用了相对偏移量。
多年来,我设计了几个工具来检测 Linux 内核代码段的完整性。 这种检测技术显然只适用于修改文本段的内核 rootkit,而且大多数 rootkit 都以某种方式这样做。 然而,也有例外,比如 rootkit 只依赖于修改 VFS 层,它驻留在数据段中,不会通过验证文本段的完整性来检测。 最近,我编写的工具(内核 Voodoo 软件套件的一部分)被命名为 textify,它本质上是将内核内存的文本段(取自/proc/kcore
)与 vmlinux 中的文本段进行比较。 它解析.altinstructions
和各种其他部分,如.parainstructions
,以了解合法修补的代码指令的位置。 这样,就不会出现误报。 虽然 textify 目前还没有对公众开放,但其基本思想已经得到了解释。 因此,任何人如果希望尝试一些费力的编码过程来使它工作,都可以重新实现它。
# ./textify vmlinux /proc/kcore -s sys_call_table
kernel Detective 2014 - Bitlackeys.org
[+] Analyzing kernel code/data for symbol sys_call_table in range [0xffffffff81801460 - 0xffffffff81802570]
[+] No code modifications found for object named 'sys_call_table'
# ./textify vmlinux /proc/kcore -a
kernel Detective 2014 - Bitlackeys.org
[+] Analyzing kernel code of entire text segment. [0xffffffff81000000 - 0xffffffff81773da4]
[+] No code modifications have been detected within kernel memory
在前面的示例中,我们首先检查以确保sys_call_table
没有被修改。 在现代 Linux 系统上,sys_call_table
被标记为只读,因此存储在文本段中,这就是为什么我们可以使用 textify 来验证其完整性。 在下一个命令中,我们使用-a
开关运行 textify,它扫描整个文本段中的每一个字节,以查找非法修改。 我们可以简单地从运行-a
开始,因为sys_call_table
包含在-a
中,但是有时候,也可以通过符号名称来扫描内容。
在 Linux 内核中,有几种方法可以修改内核,使进程隐藏能够工作。 由于本章并不是对所有内核 rootkit 的注释,我将只介绍最常用的方法,然后提出一种检测它的方法,它是在我 2014 年提供的 taskverse 程序中实现的。
在 Linux 中,进程 id 被存储为/proc
文件系统中的目录; 每个目录包含大量关于进程的信息。 /bin/ps
程序在/proc
中执行一个目录列表,以查看哪些 pid 当前正在系统上运行。 Linux 中的目录列表(例如带有ps
或ls
的目录)使用sys_getdents64
系统调用和filldir64
内核函数。 许多内核 rootkit 会劫持其中一个函数(取决于内核版本),然后插入一些代码,跳过包含隐藏进程d_name
的目录条目。 结果,/bin/ps
程序无法找到内核 rootkit 通过跳过目录列表中的进程而认为隐藏的进程。
taskverse 程序是 Voodoo 内核包的一个部分,但我免费发布了一个更基本的版本,只使用一种技术来检测隐藏的进程; 然而,这种技术仍然非常有用。 正如我们刚才讨论的,rootkit 通常将 pid 目录隐藏在/proc
中,这样sys_getdents64
和filldir64
就看不到它们了。 要查看这些进程,最直接、最明显的方法是完全绕过/proc 目录,然后沿着内核内存中的任务列表查看由struct task_struct
项链表表示的每个进程描述符。 列表指针的头可以通过查找init_task
符号找到。 有了这些知识,具有一定技能的程序员就可以打开/proc/kcore
并遍历任务列表。 这段代码的详细信息可以在项目中查看,可以在我的 GitHub 配置文件https://github.com/elfmaster/taskverse上找到。
到目前为止,我们已经涵盖了内存中各种类型的内核 rootkit 感染,但是我认为这一章需要专门用一节来解释内核驱动程序是如何被攻击者感染的,以及如何检测这些感染。
lkm 是 ELF 对象。 更具体地说,它们是ET_REL
文件(目标文件)。 由于它们实际上只是可重新定位的代码,因此感染它们的方式,如劫持函数,是比较有限的。 幸运的是,在装入 ELF 内核对象(在 LKM 中重新定位函数的过程)期间,有一些特定于内核的机制使感染它们变得非常容易。 整个方法和它工作的原因在这篇精彩的文章http://phrack.org/issues/68/11.html中进行了描述,但总体思想很简单:
- 将寄生代码注入或链接到内核模块。
- 更改
init_module()
的符号值,使其与邪恶的替换函数具有相同的偏移/值。
这是攻击者在现代 Linux 系统(2.6 到 3)上最常用的方法。 x 内核)。 还有另一种方法没有在其他任何地方具体描述,我将简要地分享它。
正如前面提到的,LKM 文件是可重定位的代码,因此很容易添加代码,因为可以用 C 编写寄生虫,然后在链接之前将其编译为可重定位的。 在链接了新的寄生代码(可能包含一个新函数(或几个函数))之后,攻击者可以使用函数 trampolines 简单地劫持 LKM 中的任何函数,如本章前面所述。 因此,攻击者将目标函数的前几个字节替换为跳转到新函数。 在调用旧函数之前,新函数的 memcpy 是旧函数的原始字节,而 memcpy 是下次调用钩子时的蹦床。
在较新的系统上,在给文本段打补丁之前,必须禁用写保护位,例如使用memcpy()
调用来实现 trampolines 函数。
基于刚才描述的两种简单的检测方法,这个问题的解决方案应该是显而易见的。 对于符号劫持方法,您可以简单地查找具有相同值的两个符号。 在 Phrack 文章中显示的示例中,init_module()
函数被劫持了,但是该技术应该适用于攻击者想要劫持的任何函数。 这是因为内核处理每一个重定位(尽管我没有测试这个理论):
$ objdump -t infected.lkm
00000040 g F .text 0000001b evil
...
00000040 g F .text 0000001b init_module
注意,在前面的符号输出中,init_module
和evil
具有相同的相对地址。 就在这里,这是一个被感染的 LKM 正如 Phrack 68 #11 所示。 检测被 trampolines 劫持的功能也非常简单,并且已经在 9.6.3 节中描述过了,在那里我们讨论了在内核中检测 trampolines。 只需对 LKM 文件中的函数应用相同的分析即可,可以使用 objdump 等工具对其进行分解。
在良好的时代,黑客能够使用/dev/kmem 设备文件修改内核。 这个文件为程序员提供了到内核内存的原始门户,最终受到各种安全补丁的影响,并从许多发行版中删除。 然而,一些发行版仍然可以读取它,这可能是检测内核恶意软件的强大工具,但只要/proc/kcore 可用,就没有必要这么做。 在给 Linux 内核打补丁方面编写的一些最好的工作是由 Silvio Cesare 构思的,这可以在他从 1998 年开始的早期作品中看到,并且可以在 vxheaven 或以下链接上找到:
- Runtime kernel kmem patch:http://althing.cs.dartmouth.edu/local/vsc07.html
有许多使用/dev/mem 的内核 rootkit,即 phalanx 和 phalanx2,由 Rebel 编写。 这个设备也经历了许多安全补丁。 目前,为了向后兼容,它在所有系统上都存在,但是只有前 1mb 的内存是可访问的,主要用于 X Windows 使用的遗留工具。
在一些操作系统上,例如 FreeBSD, /dev/kmem 设备仍然可用,默认情况下是可写的。 甚至有专门为访问它而设计的 API,有一本叫做Writing BSD rootkits的书展示了它的能力。
在上一章中,我们讨论了ECFS(简称Extended Core File Snapshot)技术。 值得一提的是,在这一章的末尾,我编写了一些内核-ecfs 的代码,它将 vmlinux 和/proc/kcore
合并成一个内核-ecfs 文件。 其结果本质上是一个类似于/proc/kcore 的文件,但它也有节头和符号。 通过这种方式,分析人员可以轻松地访问内核、lkm 和内核内存(例如“vmalloc'd”内存)的任何部分。 这段代码最终将成为公共可用的。
这里,我们将演示如何将/proc/kcore
快照到一个名为kcore.img
的文件中,并给出一组 ELF 节头:
# ./kcore_ecfs kcore.img
# readelf -S kcore.img
here are 6 section headers, starting at offset 0x60404afc:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note NULL 0000000000000000 000000e8
0000000000001a14 000000000000000c 0 48 0
[ 2] .kernel PROGBITS ffffffff81000000 01001afc
0000000001403000 0000000000000000 WAX 0 0 0
[ 3] .bss PROGBITS ffffffff81e77000 00000000
0000000000169000 0000000000000000 WA 0 0 0
[ 4] .modules PROGBITS ffffffffa0000000 01404afc
000000005f000000 0000000000000000 WAX 0 0 0
[ 5] .shstrtab STRTAB 0000000000000000 60404c7c
0000000000000026 0000000000000000 0 0 0
# readelf -s kcore.img | grep sys_call_table
34214: ffffffff81801460 4368 OBJECT 4 sys_call_table
34379: ffffffff8180c5a0 2928 OBJECT 4 ia32_sys_call_table
Linux 内核是一个涉及取证分析和逆向工程的大主题。 有许多令人兴奋的方法可以对内核进行插装,以达到破解、反转和调试的目的,Linux 为其用户提供了许多进入这些领域的入口点。 在这一章中,我已经讨论了一些有用的文件和 api,但我也将给出一个简短的列表,列出可能对你的研究有帮助的东西。
/proc/kcore
/proc/kallsyms
/boot/System.map
/dev/mem
(已弃用)/dev/kmem
(已弃用)- GNU 调试器(与 kcore 一起使用)
- Kprobes
- Ftrace
- Kprobe 仪表:http://phrack.org/issues/67/6.html
- Runtime kernel**kmem patch:http://althing.cs.dartmouth.edu/local/vsc07.html
- LKM 感染:http://phrack.org/issues/68/11.html
- Linux 二进制文件中的特殊部分:https://lwn.net/Articles/531148/
- Voodoo:http://www.bitlackeys.org/#ikore
在本书的最后一章中,我们走出了用户域二进制文件,并大致了解了内核中使用的 ELF 二进制文件的类型,以及如何将它们与 GDB 和/proc/kcore
一起用于内存分析和分析目的。 我们还解释了一些最常见的 Linux 内核 rootkit 技术,以及可以应用哪些方法来检测它们。 这个小章节只是作为理解基本原理的主要资源,但是我们只是列出了一些优秀的资源,以便您可以继续扩展您在这一领域的知识。