7检测
7.1 检测调用挂勾
7.1.1 检测系统调用挂勾
7.2 检测 DKOM
7.2.1 查找隐藏的进程
7.2.2 查找隐藏的端口
7.3 检测运行时补丁
7.3.1 查找嵌入函数挂勾
7.3.2 查找代码字节补丁
7.4 小结
7
DETECTION
检测
We’ll now turn to the challenging world of
detection. In general, you can detect a rootkit in one of two ways:
either by signature or by behavior. Detecting by signature involves
scanning the operating system for a particular rootkit trait (e.g.,
inline function hooks). Detecting by behavior involves catching the
operating system in a “lie” (e.g., sockstat(1) lists two open , but a port scan reveals three).
现
在我们将要进入检测rootkit的极具挑战性的世界。一般说来,你可以两种方式来检测rootkit:要么通过特征码,要么通过行为。通过特征码检测涉
及从操作系统搜索独特的rootkit特征(比如,内嵌函数挂勾)。通过行为检测涉及在操作系统捕捉"谎言"(比如,sockstat(1)列举出来有两
个开放的端口,但是端口扫描却显示有三个开放的端口)
In this chapter, you’ll learn how
to detect the different rootkit techniques described throughout this
book. Keep in mind, however, that rootkits and rootkit detectors are in
a perpetual arms race. When one side develops a technique, the other side develops a countermeasure. In other words, what works today may not work tomorrow.
本章中,你将学会如何检测本书中描述过的各种rootkit技术。记住,但是,rootkit和rootkit检测器处于永久的军事竞赛状态。每当一方开发出一种新的技术,另一方就开发出反制措施。换句话说,今天奏效的技术也许明天就会失效。
7.1 Detecting Call Hooks
7.1 检测调用挂勾
As
stated in Chapter 2, call hooking is really all about redirecting
function pointers. Therefore, to detect a call hook, you simply need to
determine whether or not a function pointer still points to its
original function. For example, you can determine if the mkdir system
call has been hooked by checking its sysent structure’s sy_call member.
If it points to any function other than mkdir, you’ve got yourself a
call hook.
第二章说到,调用挂勾实际上是重定位函数。
因此,为了检测调用挂勾,你只需要简单地确定函数指针是否依然指向它原先的函数。比如,你可以通过检测mkdir对应的sysent结构体内的
sy_call 成员来确认mkdir系统调用是否已经被挂勾了。如果sy_call 成员指向了不是mkdir的任何其他函数,你知道它被挂勾了。
7.1.1 Finding System Call Hooks
7.1.1 检测系统调用挂勾
Listing
7-1 is a simple program designed to find (and uninstall) system call
hooks. This program is invoked with two parameters: the name of the
system call to check and its corresponding system call number. It also
has an optional third parameter, the string “fix,” which restores the
original system call function if a hook is found.
清单 7-1 是个简单的程序,它设计用来检测(和卸载)系统调用挂勾。这个程序调用时需要两个参数:需要检测的系统调用名称,以及它对应的系统调用号。它也有一个可选的第三参数,字符串"fix",如果发现了挂勾,它就恢复原先的系统调用函数。
NOTE
The following program is actually Stephanie Wehner’s checkcall.c; I
have made some minor changes so that it compiles cleanly under 6. I also made some cosmetic changes so that it looks better in print.
提示 下面这个程序实际是 Stephanie Wehner 的checkcall.c。我对它进行了一些小修改,这样它可以在FreeBSD 6.1 下编译。我还做了一些修饰性的修改,这样它的打印比较好看。
-------------------------------------------------------------------------------------
#include
#include
#include
#include
#include
#include
#include
#include
#include
usage();
int
main(int argc, char *argv[])
{
char errbuf[_POSIX2_LINE_MAX];
kvm_t *kd;
struct nlist nl[] = { { NULL }, { NULL }, { NULL }, };
unsigned long addr;
int callnum;
struct sysent call;
/* Check arguments. */
/* 检查参数. */
if (argc < 3) {
usage();
exit(-1);
}
nl[0].n_name = "sysent";
nl[1].n_name = argv[1];
callnum = (int)strtol(argv[2], (char **)NULL, 10);
printf("Checking system call %d: %s\n\n", callnum, argv[1]);
kd = kvm_openfiles(NULL, NULL, NULL, O_RDWR, errbuf);
if (!kd) {
fprintf(stderr, "ERROR: %s\n", errbuf);
exit(-1);
}
/* Find the address of sysent[] and argv[1]. */
/* 查找sysent[] 和 argv[1] 的地址. */
if ( /*1*/ kvm_nlist(kd, nl) < 0) {
fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd));
exit(-1);
}
if (nl[0].n_value)
printf("%s[] is 0x%x at 0x%lx\n", nl[0].n_name, nl[0].n_type,
nl[0].n_value);
else {
fprintf(stderr, "ERROR: %s not found (very weird...)\n",
nl[0].n_name);
exit(-1);
}
if (!nl[1].n_value) {
fprintf(stderr, "ERROR: %s not found\n", nl[1].n_name);
exit(-1);
}
/* Determine the address of sysent[callnum]. */
/* 确定 sysent[callnum] 的地址. */
addr = nl[0].n_value + callnum * sizeof(struct sysent);
/* Copy sysent[callnum]. */
/* 拷贝 sysent[callnum]. */
if ( /*2/ kvm_read(kd, addr, &call, sizeof(struct sysent)) < 0) {
fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd));
exit(-1);
}
/* Where does sysent[callnum].sy_call point to? */
/* sysent[callnum].sy_call 指向哪里? */
printf("sysent[%d] is at 0x%lx and its sy_call member points to "
"%p\n", callnum, addr, call.sy_call);
/* Check if that's correct. */
/* 检查它是否正确. */
/*3*/ if ((uintptr_t)call.sy_call != nl[1].n_value) {
printf("ALERT! It should point to 0x%lx instead\n",
nl[1].n_value);
/* Should this be fixed? */
/* 它应当被修正吗? */
if (argv[3] && strncmp(argv[3], "fix", 3) == 0) {
printf("Fixing it... ");
/*4*/ call.sy_call =(sy_call_t *)(uintptr_t)nl[1].n_value;
if (kvm_write(kd, addr, &call, sizeof(struct sysent))
< 0) {
fprintf(stderr,"ERROR: %s\n",kvm_geterr(kd));
exit(-1);
}
printf("Done.\n");
}
}
if (kvm_close(kd) < 0) {
fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd));
exit(-1);
}
exit(0);
}
void
usage()
{
fprintf(stderr,"Usage:\ncheckcall [system call function] "
"[call number] \n\n");
fprintf(stderr, "For a list of system call numbers see "
"/sys/sys/syscall.h\n");
}
-------------------------------------------------------------------------------------
Listing 7-1: checkcall.c
清单 7-1: checkcall.c
Listing
7-1 first /*1*/ retrieves the in-memory address of sysent[] and the
system call to be checked (argv[1]). Next,/*2*/ a local copy of
argv[1]’s sysent structure is created. This structure’s sy_call member
is then /*3*/ checked to
sure that it still points to its original function; if it does, the
program returns. Otherwise, it means there is a system call hook, and
the program continues. If the optional third parameter is present,
sy_call is /*4*/ adjusted to point to its original function,
effectively uninstalling the system call hook.
清单 7-1
首先获取sysent[]
以及需要检测的系统调用(argv[1])在内存中的地址。接着创建一个argv[1]的sysent结构的本地副本。然后检查这个结构中的
sy_call成员,以确认它依然指向它原先的函数。如果是,程序返回。否则,这意味着存在一个系统调用挂勾,然后程序继续。如果可选的第三个参数存在,
修正sy_call指向它原先的函数,有效地卸掉了系统调用的挂勾。
NOTE The checkcall program
only uninstalls the system call hook; it doesn’t remove it from memory.
Also, if you pass an incorrect system call function and number pair,
checkcall can actually damage your system. However, the point of this
example is that it details (in code) the theory behind detecting any
call hook.
提示 这个checkcall 程序只是卸掉系统调用挂勾;挂勾例程没有从内存中删除掉。还有,如果你传递的一对系统调用函数及调用号不配套,checkcall 实际上会破坏你的系统。然而,本例的出发点是它演示(以代码形式)了检测任何一个调用挂勾的理论。
In
the following output, checkcall is run against mkdir_hook (the mkdir
system call hook developed in Chapter 2) to demonstrate its
functionality.
在下面的输出中,checkcall 被运行来对抗mkdir_hook(mkdir系统调用挂勾在第2章中开发) ,来演示它的功能
-------------------------------------------------------------------------------------
$ kldload ./mkdir_hook.ko
$ mkdir 1
The directory "1" will be created with the following permissions: 777
$ sudo ./checkcall mkdir 136 fix
Checking system call 136: mkdir
sysent[] is 0x4 at 0xc08bdf60
sysent[136] is at 0xc08be5c0 and its sy_call member points to 0xc1eb8470
ALERT! It should point to 0xc0696354 instead
Fixing it... Done.
$ mkdir 2
$ ls –l
. . .
drwxr-xr-x 2 ghost ghost 512 Mar 23 14:12 1
drwxr-xr-x 2 ghost ghost 512 Mar 23 14:15 2
-------------------------------------------------------------------------------------
As you can see, the hook is caught and uninstalled.
可以看到,挂勾被捕获并卸除掉了。
Because checkcall works by referencing the ’s
in-memory symbol table, patching this table would defeat checkcall. Of
course, you could get around this by referencing a symbol table on the
filesystem, but then you would be susceptible to a file redirection
attack. See what I meant earlier by a perpetual arms race?
因为checkcall 通过引用内核在内存中的符号表来工作,所以对这个符号表的修改将会击溃checkcall. 当然,你可以通过引用文件系统中的符号表来克服这点。但你又将容易受到文件的影响。明白我原前所说的,永久的军事竞赛了吧。
7.2 Detecting DKOM
7.2 检测 DKOM
As
stated in Chapter 3, DKOM is one of the most difficult-to-detect
rootkit techniques. This is because you can unload a DKOM-based rootkit
from memory after patching, which leaves almost no signature.
Therefore, in order to detect a DKOM-based attack, your best bet is to
catch the operating system in a “lie.” To do this, you should have a
good understanding of what
is considered normal behavior for your system(s).
就像第3章说明的那样,DKOM是最难检测的一种rootkit技术之一。这是因为你可以在修改了内存之后卸载掉基于KDOM的rootkit,这样就几乎没有留下特征码。因此,为了检测基于DKOM的,你最好的赌注是捕获操作系统的"谎言"。要做到这点,你应当深刻地理解你的系统哪些行为认为是正常的。
NOTE One caveat to this approach is that you can’t trust the APIs on the system you are checking.
注意 关于这种方法的一个警告是,你不能信任被检测系统的APIs.
7.2.1 Finding Hidden Processes
7.2.1 查找隐藏的进程
Recall
from Chapter 3 that in order to hide a running process with DKOM, you
need to patch the allproc list, pidhashtbl, the parent process’s child
list, the parent process’s process-group list, and the nprocs variable.
If any of these objects is left unpatched, it can be used as the litmus
test to determine whether or not a process is hidden.
回忆第3章内容,为了用DKOM隐藏一个运行的进程,你必须修改allproc 链表,pidhashtbl, 父进程的子进程链表,父进程的进程组链表和nprocs 变量。如果这些对象有任何一个没被修改,它就可以用做确定是否有进程被隐藏的试金石。
However,
if all of these objects are patched, you can still find a hidden
process by checking curthread before (or after) each context switch,
since every running process stores its context in curthread when it
executes. You can check curthread by installing an inline function hook
at the beginning of mi_switch.
但是,如果所有这些对象都被修改了,你依然可以通过检查每次上下文切换之前(或之后)的curthread 来查找隐藏的进程。既然每个运行的进程在它运行时都把它的上下文都保存在curthread中,你就可以通过在mi_switch前面一个内嵌函数挂勾来检测curthread。
NOTE Because the code to do this is rather lengthy, I’ll simply explain how it’s done and leave the actual code to you.
提示 因为实现这个目标的代码相当长,我将只是简单地解释它的工作方式,实际的代码留给你完成。
The
mi_switch function implements the machine-independent prelude to a
thread context switch. In other words, it handles all the
administrative tasks required to perform a context switch, but not the
context switch itself. (Either cpu_switch or cpu_throw performs the
actual context switch.)
这个mi_switch 函数实现了线程上下文切换的独立于机器的前期准备工作。换句话说,它处理执行上下文切换必需所有的管理任务,但它不是上下文切换自己本身。( 或者 cpu_throw 执行实际的上下文切换.)
Here is the disassembly of mi_switch:
下面是mi_switch 的反汇编:
-------------------------------------------------------------------------------------
$ nm /boot/kernel/kernel | grep mi_switch
c063e7dc T mi_switch
$ objdump -d --start-address=0xc063e7dc /boot/kernel/kernel
/boot/kernel/kernel: file format elf32-i386-freebsd
Disassembly of section .text:
c063e7dc :
c063e7dc: 55 push %ebp
c063e7dd: 89 e5 mov %esp,%ebp
c063e7df: 57 push %edi
c063e7e0: 56 push %esi
c063e7e1: 53 push %ebx
c063e7e2: 83 ec 30 sub $0x30,%esp
c063e7e5: 64 a1 00 00 00 00 mov /*1*/ %fs:0x0,%eax
c063e7eb: 89 45 d0 mov %eax,0xffffffd0(%ebp)
c063e7ee: 8b 38 mov (%eax),%edi
. . .
-------------------------------------------------------------------------------------
Assuming
that your mi_switch hook is going to be installed on a wide range of
systems, you can use the fact that mi_switch always accesses /*1*/ the
%fs segment register (which is, of course, curthread) as your
placeholder instruction. That is, you can use 0x64 in a manner similar
to how we used 0xe8 in Chapter 5’s mkdir inline function hook.
假设
你的mi_switch 挂勾计划可以安装在大范围的系统,你可以利用一个事实,mi_switch 总是访问%fs
段寄存器(当然,它是curthread),做为你的指令占位符。也就是说,你可以用我们在第5章mkdir内嵌函数挂勾这节那里使用0xe8的类似方式
来使用0x64。
With regard to the hook itself, you can either
write something very simple, such as a hook that prints out the process
name and PID of the currently running thread (which, given enough time,
would give you the “true” list of running processes on your
system) or write something very complex, such as a hook that checks
whether the current thread’s process structure is still linked in
allproc.
至于挂勾本身,你或者可以编写一些非常简单的代码。比如,打印当前进程名称和当前运行线程(只要有足够的时间,它将给你系统中运行进程的"真实"链表)PID的挂勾。或者一些非常复杂的代码,比如检查当前线程的process结构是否仍然链接在allproc的挂勾。
Regardless,
this hook will add a substantial amount of overhead to your system’s
thread-scheduling algorithm, which means that while it’s in place, your
system will become more or less unusable. Therefore, you should also
write an uninstall routine.
无论如何,这个挂勾将在你系统的线程调度算法上增加大量开销,这意味着,只要挂勾存在,你的系统将变得或多或少不可用。因此,你还应当编写一个卸载例程。
Also,
because this is a rootkit detection program and not a rootkit, I would
suggest that you allocate kernel memory for your hook the “proper” way—
with a kernel module. Remember, the algorithm to allocate kernel memory
via run-time patching has an inherent race condition, and you don’t
want to crash your system while checking for hidden processes.
还有,因为这是个rootkit检测程序,而不是一个rootkit,我建议你为你的挂勾以“正当”的方式--使用内核模块--分配内核内存。通过运行时补丁来分配内核内存的算法有个先天性的竞态问题。我想你不希望你的系统在检测隐藏进程时崩溃掉。
That’s
it. As you can see, this program is really just a simple inline
function hook, no more complex than the example from Chapter 5.
就这样了。可以看到,这个程序不过是个简单的内嵌函数挂勾,不比第5章的例子复杂多少。
NOTE
Based on the process-hiding routine from Chapter 3, you can also detect
a hidden process by checking the UMA zone for processes.
First, select an unused flag bit from p_flag. Next, iterate through all
of the slabs/buckets in the UMA zone and find all of the allocated
processes; lock each process and clear the flag. Then, iterate through
allproc and set the flag on each process. Finally, iterate through the
processes in the UMA zone again, and look for any processes that don’t
have the flag set. Note that you’ll need to hold allproc_lock the
entire time you are doing this to prevent races that would result in
false positives; you can use a shared lock, though, to avoid starving
the system too much.1
提示
基于第3章中进程隐藏的例程,你还可以通过检查进程的UMA区域联检测隐藏的进程。首先,在p_flag中选取一个没使用的标志位。接着,遍历UMA中所
有的slabs/buckets,查找出所有已分配的进程;锁住每个进程然后清除该标志。然后,遍历allproc,给每个进程设置该标志。最后,再次遍
历UMA区域中的进程,查看任何一个该标志没被设置的进程。注意,在你做这些事情的整段时间里,你得持有allproc_lock
,这样防止竞态的发生。竞态可以导致错误。然而,你可以使用一个锁来避免系统过度饥饿。
1
Of course, all of this just means that my process-hiding routine needs
to patch the UMA zone for processes and threads. Thanks, John.
1 当然,所有这些仅仅意味着我的进程隐藏例程需要为进程和线程修改UMA域了。谢谢,John。
7.2.2 Finding Hidden Ports
7.2.2 查找隐藏的端口
Recall
from Chapter 3 that we hid an open TCP-based port by removing its inpcb
structure from tcbinfo.listhead. Compare that with hiding a running
process, which involves removing its proc structure from three lists
and a hash table, as well as adjusting a variable. Seems a little
imbalanced, doesn’t it? The fact is, if you want to completely hide an
open TCP-based port, you need to adjust one list (tcbinfo.listhead),
two hash tables (tcbinfo.hashbase and tcbinfo.porthashbase), and one
variable (tcbinfo.ipi_count). But there is one problem.
回忆在第3章中,我
们通过把inpcb结构从tcbinfo.listhead
移除来隐藏一个基于TCP的开放端口。对比一下隐藏运行进程的过程,把它的proc结构从三个链表和一个hash表中移除掉,再调整一个变量。这看起来有
点不平衡,不是吗?实际上,如果你想完全地隐藏一个基于TCP的端口,你得调整一个链表(tcbinfo.listhead),两个hash表
(tcbinfo.hashbase 和 tcbinfo.porthashbase),以及一个变量
(tcbinfo.ipi_count)。但是这会导致一个问题。
When data arrives for an
open TCP-based port, its associated inpcb structure is retrieved
through tcbinfo.hashbase, not tcbinfo.listhead. In other words, if you
remove an inpcb structure from tcbinfo.hashbase, the associated port is
rendered useless (i.e., no one can connect to or exchange data with
it). Consequently, if you want to find every open TCP-based port on
your system, you just need to iterate through tcbinfo.hashbase.
当
对应一个基于TCP端口的数据到达时,与它相关的inpcb结构就通过tcbinfo.hashbase获取到,而不是通过
tcbinfo.listhead.
换句话说,如果你把inpcb结构从tcbinfo.hashbase移除掉,与它相关的端口就导致无效(也就是说,没人能够连接到它,或通过它交换数
据)。因此,如果你想查找出你系统中每个基于TCP的开放端口,就只需遍历tcbinfo.hashbase 就可以了。
7.3 Detecting Run-Time Kernel Memory Patching
7.3 检测内核内存运行时补丁
Ther
没ntially two types of run-time kernel memory patching attacks: those
that employ inline function hooks and those that don’t. I’ll discuss
detecting each in turn.
本质上存在两种类型的运行时内核内存补丁攻击方法:采用内嵌函数挂勾和没有使用内嵌函数挂勾。我将逐一讨论针对每一种类型补丁的检测方法。
7.3.1 Finding Inline Function Hooks
7.3.1 查找嵌入函数挂勾
Finding
an inline function hook is rather tedious, which also makes it somewhat
difficult. You can install an inline function hook just about anywhere,
as long as there is enough room within the body of your target
function, and you can use a variety of instructions to get the
instruction pointer to point to a region of memory under your control.
In other words, you don’t have to use the exact jump code presented in
Section 5.6.1.
查找一个内嵌函数挂勾相当地冗长乏味的,这也使得检测变得有点困难。你几乎可以安装一个内嵌函数挂勾到任何
地方,只要那里有足够的空间放置你的目标函数体。并且你可以使用多种指令来使得指令指针指向受你控制的内存区域。换句话说,你不一定要使用章节5.6.1
所讲严格的jump代码。
What this means is that in order to detect an
inline function hook you need to scan, more or less, the entire range
of executable kernel memory and look through each unconditional jump
instruction.
这意味着,为了检测一个内嵌函数挂勾,你得搜索,或多或少,可执行内核内存的整下区域来查看每一个无条件跳转指令。
In
general, there are two ways to do this. You could look through each
function, one at a time, to see if any jump instructions pass control
to a region of memory outside the function’s start and end addresses.
Alternately, you could create an HIDS that works with executable kernel
memory instead of files; that is, you first scan your memory to
establish a baseline and then periodically scan it again, looking for
differences.
一般,完成这个任务存在两种方法。你可以查看每一个函数。每次,看看是否存在任何一种跳转指令,它把控制转移到
了该函数开始和结束地址以外的内存区域。你也可以创建一个HIDS可执行内核内存,而不是代替文件,一起工作;也就是,你首先扫描你的内存来建立一个基
线,然后周期性地再次扫描,来查找不同之处。
7.3.2 Finding Code Byte Patches
7.3.2 查找代码字节补丁
Finding
a function that has had its code patched is like looking for a needle
in a haystack, except that you don’t know what the needle looks like.
Your best bet is to create (or use) an HIDS that works with executable
kernel memory.
查找代码被打了补丁的函数,就像大海捞针一般,你不知道这个针是什么样子。你最好的赌注是创建(或使用)一个HIDS与可执行内核内存一起工作。
NOTE In general, it’s much less tedious to detect run-time kernel memory patching through behavioral analysis.
提示 一般说来,通过行为分析来检测一个运行时内核内存补丁不那么单调乏味得多。
7.4 Concluding Remarks
7.4 小结
As
you can probably tell by the lack of example code in this chapter,
rootkit detection isn’t easy. More specifically, developing and writing
a generalized rootkit detector isn’t easy, for two reasons. First,
kernel-mode rootkits are on a level playing field with detection
software (i.e., if something is guarded, it can be bypassed, but the
reverse is also true—if something is hooked, it can be unhooked).2
Second, the kernel is a very big place, and if you don’t know
specifically where to look, you have to look everywhere.
由于本章缺少实例
代码,就像你可能会说的那样,rootkit的检测不容易。更明确地说,是开发和编写一个通用的rootkit不简单。有两个原因。第一,
内核模式rootkit和检测软件运行于同一级别极限(也就是说,如果有东西被监视了,它可以被绕过,但反过来也一样--如果有东西给挂勾了,这个挂勾也
可以被卸除掉)。第二,内核是个非常大的地方,如果你不知道该查看哪里,你就得查看所有地方。
This is
probably why most rootkit detectors are designed as follows: First,
someone writes a rootkit that hooks or patches function A, and then
someone else writes a rootkit detector that guards function A. In other
words, most rootkit detectors are of the one-shot fix variety.
Therefore, it’s an arms race, with the rootkit authors dictating the
pace and the anti-rootkit authors constantly playing catch-up.
这可
能就是为什么大多数rootkti检测软件像下面这样开发的原因:首先,有人编写了一个rootkit,它挂勾或修改了函数A,然后别人编写一个
rootkit检测软件来保护函数A。换句话说,大多数rootkit检测软件是属于one-shot
fix类型。因此,它就是军备竞赛,rootkit作者决定了竞赛的步调,anti-rootkit作者要经常地跟进上去进行竞争。
In short, while rootkit detection is necessary, prevention is the best course.
简而言之,虽然rootkit检测是必需的,但防护是最好的策略。
NOTE
I purposely left prevention out of this book because there are pages
upon pages dedicated to the subject (i.e., all the books and articles
about hardening your system), and I don’t have anything to add.
提示 我有意在本书中不介绍防护,是因为致力于这个课题的文档非常多(也就是,关于加固系统的所有的书籍和文章),我就没有什么可以增加的东西的了。
------------
2
There is an exception to this rule, however, that favors detection. You
can detect a rootkit through a service, which it provides, that can’t
be cut off; the inpcb example in Section 7.2.2 is an example. Of
course, this is not always easy or even possible.
2 然而,这个规则有个例外,,它有利于检测一方。你可以利用它提供的服务来检测rootkit,这个服务是不能被切除的;章节7.2.2 的inpcb示例子就是个例子。当然,这个方法不总是易行,或者甚至可行的。
阅读(1287) | 评论(0) | 转发(0) |