危险漫步博客
新鲜的“黑客思维”就是从全新的角度看待黑客技术,从更高的层面去思考;专注于黑客精神及技术交流分享的独立博客。
文章2289 浏览18787941

Rootkit基础

一.rootkit的基本概念

rootkit这个术语已经存在10多年了。它是由有用的小型程序组成的工具包,使得攻击者能够保持访问计算机上具有最高权限的用户“root”换句话说,rootkit是能够持久或可靠地、无法检测地存在于计算机上的一组程序和代码。

在上述rootkit定义中关键词是“无法检测”。rootkit所采用的大部分技术和技巧都用于在计算机上隐藏代码和数据。例如许多rootkit可以隐藏文件和目录。rootkit的其他特性通常用于远程访问和窃听。例如用于嗅探网络上的报文。当这些特性结合起来后,它们会给安全带来毁灭性的打击。

rootkit并非天生邪恶,也并不总是被黑客所使用。rootkit只是一种技术,理解这一点是很重要的。美好或邪恶的意图取决于使用它们的人,大量合法的商用程序提供了远程管理,甚至窃听功能,有些程序甚至使用潜行技术,这些程序在许多方面都可称作rootkit。

rootkit存在的原因和语音窃听一样,人们希望了解或控制其他人的行为,由于对数据处理的巨大且日益增长的依赖程度,计算机自然成为了目标。

只有当希望维持对系统的访问时,rootkit才发挥作用。若要完成的全部功能只是窃取信息然后离开,就不留下rootkit。事实上,留下rootkit总是存在着被检测发现的风险,若窃取了信息并将系统清理干净,就可以不留下任何操作痕迹。

rootkit提供了两个主要功能:远程命令和控制,以及软件窃听。远程命令和控制(简称远程控制)包括对文件进行控制,导致系统重启或“死机蓝屏”,以及访问命令shell(即cmd.exe或/bin/sh)。软件窃听就是观察人们在做什么,它包括嗅探报文、截获击键以及阅读电子邮件。攻击者可以使用这些技术来捕获口令和解密的文件,甚至加密密钥。

为了使各位能够方便的理解本文,接下来我要介绍一些关于操作系统的知识。

二.操作系统(os)

2.1操作系统一概述

现代的人们大多是这样认识计算机的:一个人盯着屏幕敲打着键盘,这就是在操纵计算机了,于是,屏幕和键盘就成了计算机的代表物了。更进一步了解计算机的人们知道,屏幕和键盘只不过是计算机的输出和输入而已,计算机的核心在一个机箱里,包括处理器、内存、主板、硬盘和电源等。所有这些构成了计算机的有形部分(实际上,这些部

件也在越变越小)。再进一步,真正使用过计算机的人们还知道,光是计算机硬件还不够,还需要各种软件,日常用到的软件包括:办公软件、游戏软件、字典软件、上网软件、聊天软件以及防病毒软件,等等。正是有了这些软件,计算机用起来才这么有趣。所以,虽然软件并不有形(不过,存放软件的介质却是看得见摸得着的),但人们知道它们是切实存在的。那么,操作系统( OperatingSystem)是什么呢?

就本质而言,操作系统也是一种软件,只不过,相对于一般的软件而言,有其特殊性。所有的应用软件都建立在操作系统的基础之上,操作系统的发展本身也推动了应用软件的发展。一个众所周知的事实是,Windows操作系统的发展和普及,为大量的应用软件提供了得以存在的平台,各种家用软件也纷纷随之诞生。

操作系统是专门管理硬件资源的软件,计算机硬件本质上只提供计算和存储的能力,而操作系统则利用硬件的计算和存储能力,建壶起一个抽象层。在现代操作系统中,这一抽象层包括任务或进程(或线程)、文件、设备或字节流等诸多概念以及相应的功能。基于操作系统所提供的抽象概念和功能,应用程序能够方便地完成其功能,并且无须

直接操纵计算机的硬件。而且,现代操作系统也提供了多个任务共享硬件资源的能力,所以,应用程序并不独占硬件资源,而是以某些既定的方式来共享,这也为用户使用各种应用软件提供了极大的便

利。

不同种类操作系统的特殊性决定了应用软件(特别是我们这次要谈的RK)的适用范围。比如,UNIX平台的RK转到Windows平台上,便不能正常使用,反之亦然。直接在操作系统上进行软件开发的程序员,必须非常清楚地了解操作系统所提供的抽象层,才能够编写出行之有效的软件来。在现代软件开发领域,其中一个分支是在系统无关的平台上进行软件开发。这里所谓的系统无关的平台是指,一个公共抽象层供上层软件在其上运行,因而上层软件与底下的操作系统平台无关。典型的例子是Java语言及其开发平台,这实际上是软件多层次化的体现。

2.2计算机系统的硬件资源管理

操作系统管理哪些硬件资源,分别又是怎么管理的呢?最主要的资源是计算资源和存储资源。计算资源即CPU(Central Processing Unit,中央处理单元),现在主流的计算机通常有一个或多个CPU,或者一个CPU中有多个核(即多核CPU)。从操作系统的角来看,有多个CPU或一个多核CPU意味着可以同时执行多个任务。所以,操作系统必须合理地安排和调度任务,使得多个CPU或多核尽可能地利用起来,避免出现竞争或闲置的情形。在支持多任务并发的操作系统中,这一职责称为任务调度。在现代操作系统中由于任务是由进程或线程来完成的,操作系统的这部分功能也称为进程调度或线程调度。因为任务的数量可能超过CPU或核的数量,所以,多个任务可能共用同一个CPU或核,这就需要有一种硬件机制能够让操作系统在不同的任务之

间实现切换,这是任务调度的硬件基础。通常,计算机提供的时钟中断可以让操作系统很方便地做到这一点,也就是说,每隔一定的时间,硬件系统会触发一个中断;操作系统截获此中断,按照某神算法暂停当前正在执行的任务,并选择一个新的任务,从而实现任务的切换;到下一个时钟中断到来时,再继续这样的切换过程。因此,多个任务可以在一个CPU或核中被轮流执行。操作系统可以设定时钟中断间隔的长度,也可以选择不同的算法来安排这些任务被先后执行,这样就形成了各种不同的调度算法。

存储资源通常包括内存(RAM,随机访问存储器)和外存(也称为辅助存储器)。外存是通过标准的I/O(输入/输出)来管理的,而内存是CPU直接通过系统总线来访问的。内存是CPU执行一个任务的物质基础,CPU内部的寄存器具备计算的能力,但计算的数据从哪里来呢?除了寄存器(其本身也是一种存储资源)以外,数据的来源是系统内存。在现代操作系统中,每个任务都有其独立的内存空间,从而避免任务之间产生不必要的干扰,所以操作系统有责任为每个任务提供相对独立的内存空间。把连续编址的物理内存划分成独立的内存空间,典型的做法是段式内存寻址和页式虚拟内存管理。不同的硬件体系结构可能支持不同的方案。Intel x86体系结构同时支持段式寻址和页式虚拟内存映射,但是,可在Intel x86上运行的操作系统几乎都选择了虚拟内存映射作为内存管理的硬件基础。

Windows和Linux便是典型的例子。

在支持多任务的系统中,若所有任务的内存需求加起来的总量超过了当前系统的物理内存总量,那么,系统要么停掉一些任务,要么把一些任务转移到外存(如磁盘)中,以后当内存空闲时再把这些任务转换回来。或者系统有选择地把部分不常用的内存转换到外存,并且根据适当的规则将来再慢慢地转换回来。虚拟内存的映射以及物理内存不足时的换出和换入操作,这都是操作系统管理内存资源的重要任务。前者依赖于硬件提供的机制,而后者则更多地由操作系统自己来控制。

除了计算资源和内存资源的管理以外,操作系统对其他资源都通过I/O来管理。例如,上面提到的外存资源,像磁盘,在现代计算机中是不可或缺的部件;另外,键盘和鼠标通常是标准的输入设备,而显示器和打印机往往是标准的输出设备。操作系统为了跟I/O设备打交道,需要三方面的技术保障:CPU通过特定的指令来控制I/O设备、I/O设备通知CPU发生了特定的事情、以及在系统主内存和设备之间传输数据。通常,CPU直接访问设备的寄存器来操作一个设备,在htel x86系统上,CPU通过in和out指令能够做到这一点。设备寄存器是另一个地址空间,CPU通过I/O端口(I/O port)来访问它们。在现代计算机中,I/O端口的分配跟软件和硬件都有关系。不同的硬件设

备会使用不同的端口编号,现代的设备大都可以通过软件方式来设置其端口号,而过去一些老的设备可能需要通过硬件跳线来改变端口号的设置。对硬件设备进行恰当的设置,也是操作系统管理硬件设备的任务之一。另外,CPU怎么知道或检测设备的工作状态呢?一种做法是,通过不停地查询设备的状态寄存器来获知其工作状态;但是,更有效的做法是,当设备的状态发生变化时,它能够主动地通知CPU,从而操作系统可以采取相应的措施。这后者即是设备中断机制。比如说,当键盘设备按收到按键动作时,它产生一个中断,告诉CPU(和操作系统),当前哪个键被按下了。中断也有编号,中断的编号被视为系统全局资源,在早期计算机的中断控制器中,不同设备的中断号不能冲突,否则设备无法正常工作。现代计算机通常允许多个设备共

享中断号,操作系统和设备驱动程序可以协商设备的中断号。

计算机的计算处理能力往往仅限于在CPU内部寄存器和主内存之间进行,但是为了实现基本算术计算以外的其他各种能力,通常有必要让设备中的数据也参与到计算中来,所以,在设备与CPU寄存器或主内存之间传输数据往往是必要的功能。例如,计算机通过磁盘设备可以实现永久存储,通过显示控制器实现彩色显示甚至三维模拟。实现数据传输的方法有多种,如果设备本身的数据量很小,则可以直接通过in指令来读取设备中的数据,或通过out指令输出到设备中。或者也可以映射一段地址范围到设备中,这样,当CPU访问这块地址范围时,实际上是在访问设备的内存,而不是系统的主内存。

另外一种适合于大块数据传输的技术是DMA (Di_rectMemory Access,直接内存访问)。像硬盘控制器和网络控制器就通常采用DMA方式来传输数据,CPU只须设置好数据传输的方向、位置、数量等信息,就可以启动DMA传输了。DMA传输可以与CPU计算同时进行,但是DMA便用的总线不能与CIiU使用的发生冲突,它可以趁CPU不用总线的时刻来传送数据,也可能会因此而阻塞CPU指令的执行。DMA传输影响CPU执行指令的程度取决于DMA控制器的传输策略。

从操作系统的角度来看,考虑到I/O设备的多样性和出现新设备的可能性,操作系统有必要定义一个框架来容纳各种各样的I/O设备,并且允许操作系统发布之后还能够为新的设备提供支持。所以,除了专用操作系统以外,现代操作系统都会提供一个I/O模型,允许设备商按照此模型编写设备驱动程序( Device Driver),并加载到操作系统中。I/O模型通常具有广泛的适用性,能够支持各种类型的设备,包括对硬件设备的控制能力,以及对数据传输的支持。可以这么来概括I/O模型,它对下提供了控制硬件设备的能力,对上为应用程序访问硬件提供了一个标准接口,同时I/O模型也必须能够让操作系统有效地管理设备驱动程序。在Windows系统中,第三方厂商可以使用Windows的I/O模型来编写设备驱动程序。Windows本身在发行时,已经内置了大量主流设备的驱动程序,所以,Windows系统在安装阶段可以自动将识别出来的设备的驱动程序安装到系统中。因此,用户并不需要手工下载或安装这些驱动程序。另一方面,由于驱动程序需要直接访问硬件设备,它执行的许多指令(包括in和out)是特权指令,所以,驱动程序对于系统的稳定性和安全性有至关重要的影响,操作系统有必要对其执行严格的筛查措施,以避免恶意代趁机闯人系统中(但实际上还是不够严格)。    

2.3软件支持

操作系统之所以要管理各种硬件资源,是为了更好地为上层应用程序提供服务。应用程序并不直接与机器的各种硬件设备及资源打交道,而是运行在一个抽象层上,即操作系统提供的功能语义层。操作系统提供什么样的语义层,将决定应用程序应该怎么来构建它们的功能逻辑。尽管不同的操作系统提供的这一功能语义层的接口可能不一致,甚至相距甚远,但现代操作系统的语义层在概念上仍然比较一致,下面我们从应用程序的构建需求来介绍一些核心概念。

首先,对应用程序的任务作最基本的抽象,这就是进程和线程的概念。尽管不同的操作系统对于进程和线程的定义不尽相同,但是每个任务都应该有它自己的执行环境,即该任务的控制流以及函数调用的层次递进痕迹(假设CPU的指令体系中支持函数调用的语义)。比如,在UNIX中,一个进程代表了一个应用任务,它记录了该任务的执行状态:在Windows中,一个线程代表了一个任务,其中也记录了该任务的执行状态。现代操作系统都支持多个任务并发执行,任务的数量可以远远大于CPU或核的数量,所以,每个任务实际上只是分到了一部分CPU执行时间,多个任务可以共享一个CPU。因此,在这种多任务操作系统模型下,操作系统除了为每个任务(进程或线程)维护好其状态以外,还必须有恰当的算法来决定什么时候让哪个任务执行,什么时候暂停一个任务。正如前面所提到的,这便是任务调度,或者称为进程调度或线程调度(取决于操作系统是以进程还是线程的方式来管理任务)。简单地轮流执行每个任务,这种调度算法往往难以满足实际应用程序的各种需求。通常操作系统会采用基于优先级的抢占式调度算法,甚至根据任务的各种特性进行优先级的动态微调,这使得任务调度算法趋向于复杂化。Windows的线程调度方案属于这一类型。

其次,在支持多任务的基础上,如果这些任务是相互独立的,则操作系统总是可以采用适当的方式让它们获得执行机会,但在实践中,应用程序为了实现备种功能逻辑,不同任务之间往往有一些逻辑上的关联性。比如,任务之间必须强加某种时序关系,才能保证每个任务的状态是有效的;当多个任务竞争某些稀有资源(注意,系统的硬件资源,像打印机和显示器等,是共享的)时,系统必须确保这些任务有序执行。所以,凡是存在共享资源的地方,操作系统都必须提供恰当的方法来同步应用程序对它的访问。现代操作系统通常会提供多种同步机制,例如互斥体( mutex)、信号量(semaphore)、临界区(critical section)等。应用程序可以有选择地使用这些同步机制,以确保多个任务有序地共享资源。

除了任务和同步的概念,每个应用程序还必须有它自己相对独立的执行空间。在现代操作系统中,进程也代表了一个应用程序和它的执行空间。

不同进程的空间是相互隔离的,这是现代操作系统的基本需求。操作系统必须在处理器的硬件特性基础之上,实现一套行之有效的空间隔离方案。每个进程有它自己的内存空间,并且无法直接访问其他进程的内存空间。进程之间如果要共享数据,则必须通过操作系统提供的机制来进行。Intel x86体系结构上的操作系统基本上都利用硬件的虚拟内存映射机制来隔离每个进程的内存空间。所以,操作系统的职责是为每个进程维护好从虚拟地址到物理地址的映射关系,并且管理好物理内存的分配和回收。

另一方面,除了进程的空间隔离性,操作系统还必须提供相应的机制让不同的进程可以相互通信,毕竟,很多软件需要进程之间的协作来完成一些上层功能。同步机制和跨进程地共享内存是典型的进程间通信(IPC,Inter-Process Communication)手段。

前面提到,现代操作系统往往以统一的框架来管理I/O设备,这也隐含了操作系统向应用程序暴露的I/O接口是统一的这样一层意思。应用程序通过此接口来访问系统的外部设备,而操作系统不仅要管理好应用程序访问外设的各种请求,包括它们的时序,还必须将应用程序的请求发送到对应的设备驱动程序中,最终由设备驱动程序来处理这些请求,而处理的结果也必须以某种方式回送给应用程序。操作系统通常以句柄( handle)来代表一个可访问的抽象设备,抽象设备可能与物理设备连接,也可能并不存在对应的物理设备或资源。操作系统还提供在一个句柄上读( read),写(write)数据,以及发送控制命令的能力。所以,应用程序与系统设备打交道的方式非常简洁明了:打开设备获得句柄、向设备发送命令、读或写设备,以及关闭设备。操作系统的任务是管理这些设备和驱动程序,以及传递或解释应用程序的命令。

一类特殊的设备是磁盘,磁盘具有随机读写的能力,尽管其随机读写的性能比顺序读写要差很多。通常,磁盘的驱动程序负责操纵磁盘控制器,以便从磁盘中读取数据或者向磁盘上写入数据,但磁盘上的数据如何有效地组织起来以便不同的应用程序可以共享同一个磁盘,这一任务是由文件系统(file system)来完成的。在绝大多数操作系统中,文件系统是不可缺少的,它通过一个树状名字空间来管理磁盘上的存储空间,允许应用程序通过名称来访问磁盘上的数据。这种结构奠定了现代计算机的信息存储系统的基础。

所以,应用程序实际上工作在操作系统提供的抽象层上,它无须管理系统的硬件设备和资源,但必须使用操作系统提供的机制或语义来实现其自身的功能逻辑。这也暗示着,操作系统在某种程度上决定了上层应用程序应该如何使用它所提供的服务才是最有效和方便的,同时,应用程序也有足够的灵活性来完成其自身的上层功能。这就形成了在同一个操作系统上存在大量风格迥异的应用程序的局面,并且,由于不同操作系统提供的抽象层在概念上有一定的互通性,所以,很多应用软件能够便捷地从一个操作系统平台移植到另—个上。

我们通常所指的操作系统的范畴包括上面提到的资源管理功能,以及为应用程序提供的各种抽象概念和抽象接口的具体实现。但是,在应用程序和操作系统核心模块之间,往往还存在很多预封装好的模块,以便于应用开发人员可以复用这些模块,从而高效地开发应用软件,缩短应用软件的开发周期。这些模块既可能是操作系统厂商提供的,也可能是独立软件厂商提供的。它们既可能构成一个通用的中间件( middleware),也可能只是随编译器而提供的一个库模块(静态的或动态的)。例如,Windows操作系统的发行版本包含大量这样的中间模块(例如COM/COM+、Winsock2、.NET等),它们也成为了Windows的一部分。

三.内核rootkit

3.1简述

所有类型和规模的计算机上都安装了软件,其中绝大多数都具有操作系统(Operating System.OS)。操作系统足一组核心软件程序,能够为计算机上的其他程序提供服务。许多操作系统都是多任务的,允许多个程序同时运行。

不同的计算设备可以使用不同的操作系统。例如.PC机上应用最广泛的操作系统是微软公司的Windows。Internet上大量服务器都运行Linux或Sun公司的Solaris系统,其他许多机器则运行Windows。嵌入式设备通常运行VXWorks操作系统,许多蜂窝电话则使用S)lmbian系统。

不管操作系统安装在何种设备之上,它们都拥有一个共同目标:为应用软件提供单一、一致的设备访问接口。这些核心服务控制对设备的文件系统、网络接口、键盘、鼠标以及视频/LCD显示器进行访问。

Os的另一种功能是提供关于系统的调试和诊断信息。例如,大多数操作系统能够列出正在运行的或已安装的软件。大多数OS都提供日志机制,因此应用程序可以报告自身发生崩溃的时刻,登录失败的时刻等等。

尽管编写能够绕过OS的应用程序(通过未在文档中公开的、直接访问的方法)是可能的,但大多数开发人员不会这样做。OS提供了“官方的”访问机制,并且坦率地说,单纯使用OS的机制要容易得多。这就是几乎所有应用都使用OS所提供的服务之缘由,也足涉及到修改os的ro otkit会影响几乎所有软件的原因所在。

3.2基本架构

攻击者通常设计rootkit来影响特定的Os和软件集合。若将ro otkit设计成直接访问硬件,则它只能局限于该特定硬件。footkit可以通用于一种OS的不同版本,但仍受限于特定的OS家族。例如,一些公开的rootkit可以影响到全部Windows NT、Windows2000和Windows XP。这只有当一种OS的所有风格都具有类似的数据结构和行为时才是可能的。例如,创建一个能够感染Windows和Solaris两种系统的通用rootkit的可行性是非常小的。

rootkit可以利用多个内核模块或驱动程序。例如,攻击者可叹使用某个驱动程序来处理所有的文件隐藏操作,使用另一个驱动程序来晦藏注册表键。有时将代码分散在多个驱动程序包中是一种好方法,这有助于将代码保持为可管理的一一只要每个驱动程序具有特定用途。

复杂的rootkit可能包含许多组件,一个复杂的rootkit可以使用如下的目录结构:

/My Rootkit

/src/File Hider

隐藏文件的代码可能会很复杂,它应该包含自身的源代码文件集合之中。文件隐藏技术有多种,其中一些可能需要大量代码来实现。例如,确些文件隐藏技术要求钩住(hook)大量的函数。每个钩子都使了相当多的源代码。

/src/Networks Ops在Windows系统上的网络操作需要NDIS和TDI代码。这些驱动程序可能会比较庞大,它们有时链接到外部库上,将这些特征限制在它们自身的源文件中是合适的。

/src/Registry Hider注册表隐藏操作的方法可能不同于文件隐藏特性。它可能会涉及许多钩子,也可能需要跟踪许多句柄表或句柄列表。实际上,注册表键和键值相互关联的方式导致注册表键的隐藏存在着问题。因此footkit开发者对该问题精心设计了非常复杂的解决方案。这类特性也应该限制在它自身的源文件集合之中。

/src/Process Hider进程隐藏应该使用直接内核对象操作(DirectKemel Object Manipulation,DKOM)技术。这些文件可售B包含反向工程的数据结构以及其他信息。当计算机重启后,大多数rootkit也需要重新启动。攻击者在此处提供一个微型服务,用于在系统引导对“发动”rootkit。使得rootkitlltf重启是一个难题。虽然简单修改注册表键就能够导致在系统引导时启动文件,但是这种方法很容易被检测到。-些rootkit开发者设计了复杂的引导功能,包括磁盘上内核补丁以及修改系统引导加载程序。/inc该目录下包含公共的被包含文件,其中含有类型定义、枚举以及I,0 ControI(IOCTL)代码。这些文件通常被其他所有文件共享,因此应该位于自己的专有空间之中。/bin该目录下包含所有编译后的文件。/lib由于编译器自身的库集合位于其他目录,因此攻击者可以在该目录下存储自己的附加库或第三方库。

3.3在内核中引入代码

将代码植入内核中的直接方式是使用可加载模块(有时称为设备驱动程序或内核驱动程序)。大多数现代操作系统都允许加载内核扩展模块,以便第三方硬件如存储系统、显卡、主板和网络硬件的制造商能够添加对自己产品的支持。操作系统通常都提供了关于将驱动程序引入内核中的文档和支持。正如其名称所示,设备驱动程序通常是用于设备的。然而,通过驱动程序可以引入任何代码。一旦拥有了在内核中运行的代码,就能够完全访问内核和系统进程的全部特权内存空间。通过内核级访问,可以修改计算机上的所有软件代码和数据结构。

清理例程并非总是必要的,因此Windows设备驱动程序将其设置为可选项。在希望卸载驱动程序时才需要清理例程。在许多情况下,rootkit在置入系统之后可以驻留其中,无需卸载。但在开发过程中包含一个卸载例程是有用的,因为在rootkit的改进过程中可能需要加载最新的版本。

3.4构建Windows设备驱动程序

第一个示例设计成运行于Windows XP和Windows 2000平台上的一个简化的设备驱动程序。它还不是真正的rootkit只是简单的"hello world”设备驱动程序。

上述代码看起来很简单,将其加载到内核中,调试语句会显示出信息。我们的rootkit不例由多项内容构成,以下各节分别对其进行介绍。

3.4.1设备驱动程序开发工具包为了构建我们的Windows设备驱动程序,需要驱动程序开发工具包(Driver Development Kit,DDK)。各个Windows版本的DDK可从微软公司得到。有时需要W/ndows 2003版本的DDK,可以使用这个DDK版本来构建Windows 2000、Windows XP和Windows 2003的驱动程序。

3.4.2构建环境

DDK提供了两种不同的构建环境:检查(checked)构建和(free)构建环境。在开发设备驱动程序时使用检查构建环境,对于发行代码则使用自由构建环境。检查构建将调试检查信息编译到驱动程序中,所生成的驱动程序比自由构建版本大得多,大部分开发工作都应该使用检查构建,只有在测试最终产品时才切换到自由构建。

3.4.3文件驱动程序源代码使用C语言编写,文件的扩展名是“c”。为了开发第一个项目,先生成一个空目录(建议为C:\myrootkit),将mydrivcr.c文件置人其中。然后将前述的“hello world”设备驱动程序代码复制到该文件中。还需要SOURCES文件和MAKEFILE文件。

(1) SOURCES文件该文件应命名为全部由大写字母组成的SOURCES,没有扩展名。

TARGETNAME变量控制驱动程序的命名。该名称可能会嵌入到二进制文件中,因此TARGETNAME不应采取诸如MY_EVII_ROOTKIT IS—GONNA_GET YOU之类的名称。即使以后重新命名该文件,但上述字符串仍可能存在于二进制文件之中,从而被发现。

更好的驱动程序命名方法是使其看上去类似于合法的设备驱动程序,例如MSDIRECTX、MSVIDLH424、IDE -HD41、SOUNDMGR以及H323FON。

计算机上已经加载了许多设备驱动程序,有时只需通过检查这些现有驱动程序的列表并且对它们的名称略加变化,就可以得到极好的方案。

TARGETPATH变量通常设成OBJ,它控制文件在编译时的存储位置,驱动文件通常会放置在当前目录的objchk_xxx/i386子目录下。

TARGETTYPE变量控制所编译的文件类型,创建驱动程序要使用DRIVER类型。

在SOURCES语句行上列出.c文件列表,若使用多行的话,则需要在每行(除了最后一行)结尾处放置“\”符号。

注意;在最后一行没有以反斜线字符结尾。INCLUDES变量是可选的,它指定了include文件所在的多个目录。

若需要将库链接进来,则使用TARGETLIBS变量a一些rootkit驱动程序使用NDIS库,因此该行类似于以下形式:TARGETLIBS=$( BASEDIR) Ylib\w2k\1386\ndis.lib或TARGETLIBS=$(DDK_LIB_ PATH) \ndis.lib在构建NDIS驱动程序时,可能需要在自身系统上寻找ndis.lib文件,并对到达该文件的路径进行硬编码。$(BASEDIR)变量指定了DDK的安装目录。$(DDK_LIB_PATH)指定了默认库的安装位置。路径的其余部分根据所用的系统和DDK版本而有所区别。

(2) MAKEFILE文件

最后创建一个名称全部由大写字母组成且没有扩展名的MAKEFILE文件。

启动检查构建环境,它会打开一个命令shell。检查构建环境可以是“开始”菜单的“程序”中Windows DDK图标组之下的一个链接。打开了构建环境的命令shell后,将当前目录改为驱动程序目录,并输入命令"build”。理想情况下,不会出现任何错误,此时就得到了我们的第一个驱动程序。一个提示:要确保驱动程序目录所在位置的完整路径中不包含任任何空格。例如,可以将驱动程序放在c:\myrootkit中,找到包含了已创建的MAKEFILE和SOtlRCES文件的驱动程序示例。

(4)卸载例程

在创建驱动程序时,将theDriverObject参数传递给驱动程序的主函数。它指向一个包含函数指针的数据结构。这些函数指针之一称为“卸载例程”。若设置了卸载例程,这意味着可以从内存中卸载驱动程序。若不设置该指针的话,则驱动程序可以加载,但不会卸载。需要重启机器以便从内存中删除驱动程序。

当为驱动程序继续开发功能时,需要多次对其进行加载和卸载。所以应该设置卸载例程,以便每次测试新的驱动程序版本时无须重启。

此时就可以在无须重启机器的情况下安全的加载和卸载驱动程序。3.5用户模式和内核模式的融合rootkit很容易同时包含用户模式和内核模式的组件。用户模式部分完成大多数功能,例如联网和远程控制,而内核模式部分则执行潜行和访问。

大多数rootkit都需要内核级别的破坏活动,同时还提供了复杂的特性。由于这些复杂特性可能含有程序缺陷并且还需要使用系统的AP库,因此用户模式的方法是首选方法。用户模式的程序可通过多种方式与内核级驱动程序通信。最常见的一种方式是使用I/O Control(IOCTL)命令。IOCTL命令是可以由程序员定义的命令消息。

3.5.1 VO请求报文

一个需要理解的设备驱动程序概念是I/O请求报文(I/O Request Packet,IRP),它只是包含数据缓冲区的数据结构。为了与用户模式的程序进行通信,Windows设备驱动程序通常需要处理IRP。处于用户模式的程序可以打开一个文件句柄并向其中写入信息。在内核中,这个写入操作表示为一个IRP。因此若用户模式的程序向文件句柄中写入字符串“HELLO DRIVER!",则内核将会创建一个包含了缓冲区和字符串“HELLO DRIVER!”的IRP;用户模式和内核模式之间的通信可以通过这些IRI,进行。

为了操作IRP,内核驱动程序必须包含IRP的处理函数。和卸载倒程的安装工作一样。

在实际的驱动程序中,很可能会为每个主函数都创建一个单独的函数。例如,假定要处理READ和WRITE事件。当用户模式的程序使用驱动程序句柄来调用ReadFile或WriteFile时,会激活这些事件。更完整的驱动程序还会处理其他功能,例如关闭文件或发送IOCTL命令等。

对于每个被处理的主函数,驱动程序都需要指定一个将要调用的函数。例如,驱动程序可能包含以下函数。

3.5.2创建文件句柄另一个需要理解的概念是文件句柄。为了从用户模式的程序中使用内核驱动程序,用户模式的程序必须打开一个驱动程序句柄。这只有当驱动程序已经注册了一个指定的设备之后才能进行。一旦注册完成,用户模式的程序就可以将指定设备像文件一样打开。这非常类似于许多UNIX系统上的设备工作方式一一将所有设备都像文件一样对待。

对于我们的示例,内核驱动程序使用下面的代码来注册设备。

在上述的代码片段中,DriverEntry例程创建了一个名为MyDevice的设备,注意在函数调用中使用了完全限定(fully quaMcd)路径:

前缀“L”表示以UNICODE编码定义字符串,这是API调用所需的。一旦创建了设备,用户模式的程序可以将其像文件一样打开:

一旦打开了文件句柄,它就可以在用户模式的函数如ReadFile和WriteFile中充当参数,也可以用于进行IOCTL调用,这些操作会导致生成要在驱动程序中处理的IRP。

从用户模式中很容易打开和使用文件句柄,下面分析如何通过符号链接使文件句柄更加易用。

3.5.3添加符号链接

第三个关于设备驱动程序的重要概念是符号链接,为了便于用户模式的程序打开文件句柄,一些驱动程序使用了符号链接,这个步骤不是必需的,却是有益的:符号名更便于记忆。这种驱动程序会创建一个设备,然后调用IoCreateSymbolicLink来创建符号链接,有些rootkit采用了这种技术。

在创建了符号链接之后,用户模式的程序可以使用字符串“\VYMyDevice”来打开设备句柄。是否创建符号链接实际上并不重要,它只是使得用户模式的代码更容易找到驱动程序,但这不是必需的。

前面已经讨论了如何使用文件句柄在用户模式和内核模式之间进行通信,下面介绍如何加载设备驱动程序。

3.6加载rootki

不可避免地,需要从用户模式的程序中加载驱动程序。例如,在侵入计算机系统时会希望复制某个部署程序,该程序在运行时将rootkit加载到内核中。加载程序通常会将.sys文件的副本解压缩到硬盘上,然后发出命令将其加载到内核中。当然,要使得这些操作有效,程序必须以“管理员”权限运行。将驱动程序加载到内核中有许多方式。下面具体介绍两种有效的方法:草率方式”(quick and dirty)”和正确方式”(The Right Way)“。

3.6.1草率方式

通过未在文档中说明的API调用,可以在无须创建任何注册表键的情况下将驱动程序加载到内核中。该方法的问题在于驱动程序是可分页的,可分页的是指可以交换到磁盘上的内存,若驱动程序是可分页的,则它的任何组成部分都可以页换出(即从内存交换到磁盘上)。有时当内存被页换出时,就再无法访问它,尝试对其进行访问会导致一种声名狼藉的系统崩溃——蓝屏死机(Blue Screen ofDeath,BSOD)。这种加载方法真正安全的唯一方法是围绕分页问题专门对其进行设计。

使用这种加载方法的rootkit示例是migbot,它可以从rootkit.com网站上得到。migbot非常简单,它将全部操作代码都复制到不分页的内存中,因此驱动程序是分页的这个事实不会影响migbot所执行的动作。

migbot的源代码可以从网上下载。加载方法通常称为SYSTEM LOAD AND CALLIMAGE,因为这是在文档中未说明的API调用的名称。

上述代码在用户模式下运行,所用的.sys文件是C:Ymigbot.sys。migbot没有提供卸载特性。加载它之后,只有在系统重启时才能卸载它。可以将其看作是“发后不理”(fire-and-forget)操作。该方法的优点是它比较完备的协议更加隐秘,缺点在于它使得rootkit的设计复杂化。对于migbot来说,这是一种好的解决方法:但对于具有许多钩子的复杂rootkit来说,支持该方法需要太多的开销。

3,6.2正确方式

已建立的驱动程序正确加载方法是使用服务控制管理器(Service Control Manager,SCM),使用SCM需要创建注册表键,当驱动程序通过SCM加载时,它是不可分页的。这意味着回调函数、IRP处理函数以及其他重要代码将不会从内存中消失,不会被页换出,也不会导致蓝屏死机,这是一个好的特性。

下面的示例代码通过SCM方法基于名称来加载任意驱动程序。它注册并启动驱动程序。

至此已经学习了两种将驱动程序或rootkitjjD载到核心内存中的方法,OS的全部权限都已掌握在手。

3.7系统重启后的考验

rootkit驱动程序必须在系统启动时加载。对这个问题进行-般性的考虑时,可以发现系统启动时会加载许多不同的软件组件,只要rootkit-下文中某个启动事件相关联,它也会加载进来。

(1)使用注册表键“run”(古老的可靠方法)注册表键“run”(及其派生键)可用于在启动时加载任意程序,它也可以解压缩rootkit并加载,rootkit加载后可以隐藏注册表键“run”的值,使自身无法被检测。所有的病毒扫描器都检查该键,因此这种方法具有高度风险。然而,一旦加载了rootkit,就可以将该键的值隐藏起来。

(2)使用特洛伊木马或被感染的文件

类似于病毒感染文件的方式,在启动动时加载的任何.sys文件或可执行文件都可以以被替换,加载程序的代码也可以被插入。具有讽刺意味的是,感染的最佳目标之一就是病毒扫描或安全产品。安全产品通常在系统启动时开始运行,特洛伊木马DLL则可以被插入到搜索路径中,或者只是简单地替换或“感染”现有的DLL。

(3)使用.1ni文件可以修改.ini文件来执行程序,许多程序都具有初始化义件,它们能够在启动时运行命令或指定要加载的DLL,通过这种方式使用的文件示例是win11U。

(4)注册成为驱动程序

rootkit可以将自身注册为在启动时加载的驱动程序,这需要创建注册表键,一旦加载了rootkit之后,也可以将表键隐藏起来。

(5)注册为现有程序的附加件间谍件常用的一种方法是向Web浏览应用程序中添加扩展(例如,伪装成搜索栏的形式)。在应用程序加载时也加载了该扩展,这种方法需要启动相应的应用程序,但如果在必须激话rootkitZ前能够启动应用程序的话,则该方法对于加载rootkit来说是有效的。它的缺陷是已存在着许多的广告件扫描器,它们可能检测到应用程序的这些扩展。

(6)修改磁盘上的内核

可以直接修改内核并将其保存到磁盘上。对启动引导程序必须进待多处修改,以便内核能够通过校验和的完整性检查。这种方法非常有效,因为内核被永久地修改,并且无须注册驱动程序。

(7)修改启动引导程序

可以修改启动引导程序,在内核加载之前对其打补丁。该方法的优点是对系统进行离线分析时内核文件本身看不出修改。然而,使用恰当的工具仍可以检测到对启动引导程序进行的修改。

启动时加载的方法有许多种,以上仅仅是其中的一部分,只要发挥创造力去思考,一定能想到更好的加载方法。

四,硬件相关问题

软件和硬件是形影不离的。没有软件,硬件只是毫无生命力的硅晶体:而没有硬件,软件也无法存在,软件最终控制着计算机,但在表象之下却是由硬件实现了软件代码。

另外,硬件是软件安全的最终实施部件。没有硬件支持,软件将是彻底不安全的,许多资料在介绍软件开发时都不曾提及过底层硬件,这对于企业应用的开发者来说也许已足够,但对于rootkit开发者来说还不行,rootkit开发者需要面对逆向工程问题、手工编码的汇编语言,以及对系统上的软件工具的高技术性攻击,理解底层硬件有助于解决这些难题。

所有的访问控制最终都是由硬件实现的,例如,流行的进程隔离(process separation)概念在Intel x86硬件上通过“环(ring)”机制来实施。若Intel的CPU没有访问揎制机制,则所有在系统上执行的软件都会被信任,这意味着任何程序的崩溃都可能导致整个系统随之崩溃,任何程序都能够读写硬件、访问任意文件,或修改其他进程的内存。这听起来很熟悉,即使Intel系列处理器具有访问控制能力的历史已有多年,但是微软公司直到发布Windows NT之后才利用了这些能力。

4.1环级

Intel x86微芯片系列使用环概念来实施访问控制,环有4个级别:环0是最高权限的,环3是最低权限的。每个环都内部保存为一个数字,微芯片上实际并没有真正的物理环:  Windows操作系统中的所有内核代码都在环O级别上运行。因此,在内核中运行的rootkit-被看作是在环O级别上:@i行。不在内核中运行的用户模式程序(例如电子制表软件程序)有时称为环3级程序。包括Windows和Linux在内的许多操作系统在Intel x86微芯片上只使用环O和环3,而不使用环1和环2。由于系统中环0的权限最高,能力最强大,因此对于rootkit开发者来说,宣称自己的代码在环0级别上运行是一种自豪的展示。

CPU负责跟踪为软件代码和内存分配环的情况,并在各环之间实施访问限制。通常,每个软件程序都会获得一个环编号,它不能访问任何具有更小编号的环。例如,环3的程序不能访问环0的程序,若环3的程序试图访问环o的内存,则CPU将发出一个中断。在多数情况下,操作系统将不会允许这种访问,该访问尝试甚至会导致攻击程序的终止。

大量代码在幕后控制着这种访问限制,也有一些代码允许程序存特定环境下访问更低编号的环。

例如,为了将打印机驱动程序加载到内核中,管理员程序(环O级)需要访问被加载的设备驱动程序(在环O级的内核中)。然而,内核模式的rootkitljU载后,它的代码在环0级别上抗行,这些访问限制将不再是问题。

许多可能检测到rootkit的工具都作为管理员程序在环3级别上运行,rootkit开发者应该理解如何利用rootkit的权限高于管理工具这个事实。例如,rootkit可以使用该事实来向工具隐藏自身或导致工具失效。另外,rootkit通常是由加载程序来安装,这些加载程序是环3级的应用程序,为了将rootkit加载到内核中,这些加载程序使用了特殊的函数调用,使得它们能够访问环0级别。

除了内存访问限制之外,还存在着其他安全机制。某些指令是具有特权的,只能在环O级使用。这些指令通常用于更改CPU的行为,或者直接访问硬件。

在环0级别上执行rootkit有许多好处,这样良rootkit不仅能够操纵硬件,还能够操纵其他软件刮运行环境,这对于在计算机上进行潜行操作是关铡的。

在讨论了CPU如何实施访问控制之后,下面分额CPU如何对重要数据进行跟踪。

4.2 CPU表和系统表

CPU除了负责跟踪环的信息之外,还负责制定其他许多决策。例如,CPU必须决定当中断发生、软件程序崩溃、硬件发出注意信号、用户模式的程序试图与内核模式的程序通信,以及多线程程序切换线程时需要执行的动作。显然操作系统代码必须处理这类事情,但总是CPU最先处理它们。

对于每个重要事件,CPUDZ,须指出处理该事件所用的软件例程。由于所有软件例程都存在于内存中,因此CPU有必要存储重要软件例程的地址。更具体地,CPU需要知道在什么位置可以找到重要软件例程的地址。CPU内部无法存储所有地址,因此它必须对其取值进行查询,这个操作通过地址表完成。当中断等事件发生时,CPu在-Al表中查询该事件,并寻找处理该事件的某个软件的相应地址,CPU需要的唯一信息是这些表在内存中的基地址。有许多重要的CPU表,包括:全局描述符表(Global Descriptor Table.GDT),用于映射地址。本地描述符表(Local Descriptor Table,LDT),用于映射地址。页目录(Page Directory),用于映射地址。间断描述符表(lntemlpt Descnptor Table,IDT),用于寻找中断处理程序。

除了CPU表之外,操作系统本身也保存一批表。CPU不直接支持这些由OS实现的表,因此0S通过特殊功能和代码来管理它们。其中一个重要的表是:系统服务调度表(System Service Dispatch Table,SSDT)Windows os用于处理系统调用这些表具有多种使用方式,以下各节介绍这些表并分析它们是如何工作的,并且针Xjrootkit开发者为了提供潜行操作或 捕获数据如何修改或钩住这些表给出了一些建议。

4.3内存页

全部内存都划分成页面,如同在一本中一样。一个页面只能存储固定数目的字符,每个进程都可以拥有一个单独的查询表来寻找这些内存页。

将内存想像为一个巨大的图书馆,其中每个过程都单独拥有自己的卡片目录以进行查询,不同的查询表会导致每个进程查看的内存视图完全不同。

因此,一个进程可以读出内存地址Ox00401122的内容为“GREG”,而另一个进程读出同一内存地划的内容可以为“JAMIE”,每个进程都有唯一的内存“视图”。

访问控制机制作用于内存页面,继续使用关于图书馆的比喻,假设CPU是一个专横的图书管理员,一个进程只允许查看馆内的数本书。为了读出或写入内存,进程首先必须为正在处理的内存找到正确的“图书”,然后找到精确“页面”,若CPU不批准所请求的图书或页面,则该访问被拒绝。

通过这种方式寻找页面的查询过程是耗时且复杂的,这个过程中多个阶段实施了访问控制。首先,CPU检查进程是否能够打开正在被处理的图书(描述符检查),然后CPU检查进程是否能够读出书中的特定章节(页目录检查),最后CPU检查进程是否能够读出该章节中的特定页(页面检查),这是想当巨大的工作量。

进程只有通过了所有的安全检查,才可以读出一个页面。即使CPU检查都通过,但页面还可能标记为只读性质。这意味着进程能够读出页面,但是无法向其中写入。数据的完整性通过这种方式得以维护,rootkit开发者就像图书馆里面的野蛮人一样,试图在所有这些书上胡乱涂写一一因此我们必须尽可能地学会关于操作访问控制的知识。

4.3.1内存访问检查

为了访问内存贞面,x86处理器依次执行以下检查:

描述符(或段)检查:通常要访问全局描述符表(GDT),并检查段描述符(segment descriptor)。段描述符包含一个称为描述符权限级别(DescriptorPrivilege Level,DPL)的值。DPL包含了调用进程所需的环编号(O到3)。若DPL需求低于调用进程的环级别(有时称为当前权限级别(Current PrivilegeLevel,CPL)),则访问被拒绝且内存检查终止。页目录检查:对整个页表(即整个内存页范围)需要检查用户/超级用户位。若该位设置为0,则只有“超级用户”程序(环O、1和2)能够访问该内存页范围。若调用进程不是“超级用户”,则内存检查终止。若该位置设置为1,则任何程序都能访问该内存页范围。

页面检查:对单个内存页面执行该检查。若已经成功通过页目录检查,则对正在处理的单个页面进行页检查。类似于页目录,对每个页面都要检查一个用户/超级用户位。若该位设置为0,则只有“超级用户”程序(环O、1和2)能够访问该页面:若该位设置为1,则任何程序都能访问该页面。只有当进程能够到达并通过本检查并且没有任何访问禁止,它才可以访问内存页。

Windows系列操作系统并不真正使用描述符检查。相反,Windows只依赖于环0和环3(有时称为内核模式和用户模式)。因此可以只使用页表检查中的用户/超级用户位束控制对内存的访问。内核模式的程序运符往环0上,总是能够访问内存。用户模式的程序则运行在环3上,只能访问标记为“user”的内存。

4.3.2分页和地址转换

内存保护机制的用途不仅限于安全。大多数现代操作系统都支持虚存,这使得系统上的每个程序都拥有自己的地址空间。它还允许程序使用远比实际可用“内存”多得多的内存。例如,RAM大小为256 MB的计算机不会限制每个程序只能使用256MB内存。程序如果需要的话,能够很容易地使用1GB内存。额外的内存只需存储在磁盘上的分页文件(pagingrile)中。虚存允许多个进程在所用的内存总量大于已安装的物理RAM时同时执行,并且每个进程都拥有自己的内存空问。

内存页可以标记为页换出(paged out),即存储在磁盘上而不是RAM卒。当搜索这种内存贞时,处理器将会中断,中断处理程亭将该页读回到内存之中,并将其标记为页旗人(paged in)。在任一特定时刻,大多数系统只允许将全都可用内存的一小部分进行页换入。物理RAM较少的计算机需要一个经常被访问的很大的分页文件。反之,物理RAM越多意味着访问分页文件越少。

每当程序读取内存时,都必须指定一个地址。对于每个进程,该地址必须转换为实际的物理内存地址。这是重要的:进程使用的地址与数据驻留的实际物理地址并不一致。需要通过转换例程来标识正确的物理存储位置。

例如,若NOTEPAD.EXE搜寻虚地址Ox0041FFl0的内存内容,则实际的物理地址可以转换成例如OxOIEE2Fl0。若NOTEPAD.EXE执行指令“mov eax,Ox0041FFl0”,则读入到EAX中的值实际会存储在物理地址OxOIEE2Fl0。该地址从虚地址转换成物理地址。

4.3.3多个进程使用多个页目录

理论上,操作系统可以通过单个页目录来维护多个进程、进程间的内存保护以及磁盘上的分页文件。但是,如果只存在着一个页目录,则虚存将只有一个转换映射。这意味着所有进程都必须共享一内存空间。在Windows NT/2000/XP/2003系统中,每个进程都有自己的内存空间——它们并不共享。

大多数可执行文件的起始地址是Ox00400000。多个进程如何能够使用同一个虚拟地址,而不会在物理内存中发生冲突?答案是使用多个页目录。

系统上的每个进程都维护唯一的页目录,都拥有自己私有的CR3寄存器的值,这意味着每个进程都有一个独立且唯一的虚存映射。因此,两个不同的进程可以同时访问内存地址Ox00400000,但将其转换成两个独立的物理内存地址。这也是为何一个进程无法查看另一个进程内存的原因。

尽管每个进程都有唯一的页表,但通常所有进程对Ox7FFFFFFF上的内存空间都进行同样的映射。该内存范围是为内核保留的,而内核内存不管哪个进程在运行都是一致的。

即使在环O级别上运行时,也会有一个活动进程上下文,它包括该进程的机器状态(例如保存的寄存器值)、进程的环境、进程的安全令牌以及其他参数。为了便于讨论,它还包含CR3寄存器,从而也包含了活动进程的页目录。rootkit开发者应该考虑到对进程页表的改动不仅会在用户模式中影响该进程,还会在该进程处于上下文中时影响到内核,这可以用于高级潜行技术。

4.3.4进程和线程

rootkit开发者应该知道,管理运行中代码的主要机制是线程,并非进程,Windows内核基于线程而不是进程的数量对进程进行调度。假如存在两个进程:一个是单线程的,另一个具有9个线程,则系统将为每个线程分配10%的处理时问,单线程的进程只能获得10%的CPU时间,而具有9个线程的进程会得到90%的时间。当然,这是一个人为示例,因为其他因素(例如优先级)也在调度过程中发挥作用,但以下事实仍存在:当其他所有因素相同时,调度工作完全基于线程数目,而不是讲程数日。

rootkit出于多种目的(如潜行和代码注入)必须处理线程和线程结构。它不必创建新进程,而是创建新线程并将其分配给现有进程,需要创建一个全新进程的情况很少。

当切换到一个新线程的上下文时,旧线程的状态会保存起来,每个线程都有自己的内核堆栈,因此将线程状态推入到线程内核堆栈的顶部,若新线程属于另一个进程,则将新进程的页目录地址加载到CR3中。页目录地址可以在进程的KPROCESS结构中找到,一旦发现了新线程的内核堆栈,就从该堆栈的项部弹出新线程的上下文,并开始执行新线程a若rootkit修改了进程的页表,该修改将作用于该进程中的所有线程,因为这些线程都共享同_AI CR3值。

4.4内存描述符表

CPU用束跟踪信息的一种表中可以包含描述符。描述符有多种类型,它们可以由rootkit插入或修改。

4.4.1全局描述符表

通过GDT(Global Descriptor Table)全局描述符表,可以实现大量技巧。GDT可以用于映射不同的地址范围,也可导致任务切换,通过SGDT指令可以发现GDT的基地址,通过LGDT指令便以更改GDT的位置。

4.4.2本地描述符表

LDT(Local Descriptor Table)本地描述符表,允许每个任务拥有唯一的描述符集合。当指定一个段时,表指示符位(table-indicator bit)可以在GDT和LDT之间进行选择,LDT可以包含和GDT相同类型的描述符。

4.4.3代码段在访问代码内存时,CPU使用在代码段(CodeSegment,cs)寄存器中指定的段,代码段可以在描述符表中加以指定。任何程序,包括rootkit,都可以通过执行far call、far jump或far return指令来修改CS寄存器,其中cs从堆栈顶部弹出。令人感兴趣的是,只需将描述符中的R位设置为O就可以执行代码。

4.4.4调用门

在LDT和GDT中可以放置一种特殊的描述符,称为调用门(call gate)。如果将描述符设量为调用门,程序可以执行段间调用(far call),当该调用发生时,可以指定一个新的环级别。调用门允许用户模式程序通过执行函数调用进入内核模式,这对于rootkit程序来说是一个令人感兴趣的后门。同样的机制可用于段间跳转(far jump),但只有当调用门的权限级别等于或低于执行跳转的进程时才能进行这种操作。

当使用了调用门时,会忽略地址——只有描述符编号起作用。调用门的数据结构会将被调用函数的代码位置告知CPU,可以选择性的从堆栈中读取参数。例如,可以创建一个调用门,使得调用者将秘密命令参数放到堆栈上。

4.5中断描述符表

中断描述符表寄存器(Interrupt Descriptor TableRegister,IDTR)可以存储中断描述符表(InterruptDescriptor Table,IDT)在内存中的基地址(起始地址)。IDT用于查找处理中断所用的软件函数,它是非常重要的,计算机中大量的低层功能都使用了中断。例如,每当键盘上进行击键就产生中断信号。

IDT是一个由256项组成的数组——每个中断对应其一项,这意味着每个处理器至多支持256个中断。另外,每个处理器拥有自己的IDTR,因此也拥有自己的中断表。若计算机包含多个CPU,则部署在该计算机上的rootkit必须考虑到每个CPU都有自己的中段表。

当中断发生时,可以从中断指令或可编程中断控制器(Programmable Interrupt Controller,PIC)中获取中断编号。在这两种情况下,都通过中断表寻找要调用的软件函数,该函数有时称为向量(vector)或中断服务例程(Interrupt Service Routine,ISR)。

如果处理器处于保护模式中,则中断表是由256个8字节项组成的数组,每项包含ISR她址以及其他一些与安全相关的信息。

为了获得中断表的内存地址,必须读取IDTR。这由SIDT(Store Interrupt Descr/ptor Table)指令完,也可通过LIDT(Load Interrupt Descriptor Table)指令修改IDTR的内容。

rootkit所用的一种技巧是创建一个新的中断表,通过它隐藏对原始中断表进行的修改。病毒扫描仪能够检查原始IDT的完整性,但rootkit可以制作IDT的副本,修改IDTR,然后可以惬意地在无法检测到的情况下,对复制的IDT进行修改。

攻击者通过SIDT指令所提供的数据能够找到IDT的基地址并转储它的内容。要记住IDT最多包含256项,其中每项都包含一个中断服务例程的指针。

该数据结构有时称为中断门(interrupt gate),用于在内存中定位处理中断事件的函数。通过中断门,用户模式的程序可以调用内核模式的例程。例如,一个系统调用中断的目标位于lDT表中的偏移量Ox2E处。

系统调用在内核模式中处理,尽管可以从用户模式中对其进行初始化,其他的中断门可以作为后门由rootkit放置,rootkit也可以钩住现有的中断门。

IDT中的最大项数是256。#define MAX_IDT_ENTRIES OxFF出于示例目的,在示例rootkit的DriverEntry例程中实现了分析器。

使用SIDT指令返回的数据来获取IDT的基地址,然后遍历每一项,并将一些数据显示到调试输出信息中。

上述代码示例解释了如何分析IDT。对IDT并没有进行实际修改。然而,这些代码很容易演化成更  复杂工具的基础。

除了中断门之外,IDT还可以包含任务门(taskgate)和陷阱门(trap gate)。陷阱门与中断门的区别只在于它能够被可屏蔽中断所中断,而中断门则不能。另一方面,任务门是一个非常过时的处理器特性,它可以用于强制x86处理器的任务切换。由于Windows并不使用该特性,因此这里不对其进行示例讲解。

在Windows操作系统中不应将任务与进程相混淆。x86 CPU的任务是通过任务切换段(Task SwitchSegment,TSS)-种最初用于通过硬件执行任务管理的工具来管理的。Linux、Windows以及其他许多OS都通过软件实现任务切换,大多数情况下并不使用底层硬件机制。

4.6系统服务调度表

系统服务调度表用于查询处理特定系统调用的函数。该工具在操作系统而不是CPU中实现。程序有两种执行系统调用的方式:使用中断Ox2E,或者使用SYSENTER指令。在Windows XP及其后的系统上,程序通常使用SYSENTER指令,而较老的平台则使用中断Ox2E。这两种机制是完全不同的,尽管它们能够得到相同的结果。

执行系统调用会导致在内核中调用KiSystemService函数。该函敖从EAX寄存器中读取系统调用的编号,并在SSDT中查询该调用。该函数还将由EDX寄存器所指向的系统调用参数从用户模式堆栈中复制到内核模式堆栈。某些rootkit会钩人这个处理过程之中以嗅探数据,更改数据参数或重定向系统调用。

4.7控制寄存器

除了系统的各个表之外,还存在着一些控制CPU重要特性的特殊寄存器。rootkit可对它们加以利用。

4.7.1控制寄存器

控制寄存器包含一些控制处理器如何运作的数据位。例如,禁用内核-fJ内存访问保护机制的一种常见方法就是修改控制寄存器CRO。

控制寄存器最初出现于低级的286处理器中,以前称为机器状态字(machine status word)。随着386处理器系列的发布,重新将其命名为控制寄存器O(Control Register O,CRO)。直到486系列的处理器出现后,CRO中才添加了写保护(Write Protect,WP)位,WP位控制是否允许处理器写入标记为只读属性的内存页。该位设置为0的话,会禁用内存的保护机制。这对于打算向OS数据结构中写入信息的内核rootkit来说是非常重要的。

下列代码显示了如何使用CRO技巧来禁用和重新启用内存保护机制。

4.7.3 EFlags寄存器

EFlags寄存器也是重要的。前先,它处理陷阱标志(trap flag)。如果设置了该标志,处理器将步进执行。rootkit可以使用诸妞步进执行之类的特性来检测调试器是否正在运行或向病毒扫描软件隐藏自身;通过清除中断标志(interrupt flag),就可以禁用中断。另外,还可通过I/O Privilege Level来修改大多数基于Intel的操作系统所采用的基于环的保护系统。

五,备注

5.1附言

至此关于rootkit的基础介绍就告一段落了。本来是想写一篇关于RK的完全解读的,但是由于最近rootkit网的被攻击?无法获得一些重要的资料和代码,只能用手上的资料写了这篇文章,所以《详述RK》就变成了《RK基础》了。等到rootkit网恢复后,我会写一篇《RK高级技巧》和本篇补成一套。

正如前面说的:“rootkit并非天生邪恶,也并不总是被黑客所使用。rootkit只是一种技术,理解这一点是很重要的。美好或邪恶的意图取决于使用它们的人。”危险漫步希望大家能过记住这一点,那么这篇文章便是有意义的。

相关推荐