一次性进群,长期免费索取教程,没有付费教程。
教程列表见微信公众号底部菜单
进微信群回复公众号:微信群;QQ群:460500587
微信公众号:计算机与网络安全
ID:Computer-network
Rootkit自从诞生之初就是网络安全领域里的核心问题,Rootkit的核心功能就是隐藏恶意行为的,换句话说,Rootkit恰恰就是为免杀服务的。
Rootkit技术的重要性是毋庸置疑的,免杀技术要用它,反病毒软件也要用它,恶意软件的核心技术往往是它,网络游戏的保护系统更加是以它为核心,可以说我们身边所能看到的、接触到的多半安全产品中都有Rootkit技术的身影。
不过也正因如此,Rootkit似乎并不是一项很好掌握的技术,其实Rootkit技术本身就是一个集大成者,从最基础的反汇编、逆向、PE文件结构到系统结构、硬件层的x86结构等都需要毫不含糊地掌握,任何差错所换来的后果都是BSoD(Blue Screen of Death,蓝屏死机)。
一、构建一个Rootkit基础环境
无论学习什么技术,首先都是要构建一个基础环境,Rootkit当然也是如此。由于Rootkit的本质就是一个驱动程序,因此编写Rootkit不同于编写普通程序,它唯一的报错方法就是BSoD。这对于正在写程序的人来说可能是一场灾难,原因很简单,在BSoD之前你可能并未保存自己的工作,这可能使得你丢失辛辛苦苦整理出来的文档,也可能使你丢失熬夜写出来的代码。
所以我们需要构建一个双机调试环境,将开发系统与测试系统分开,这样即便是测试系统崩溃也不会影响到开发系统。当然,由于我们有虚拟机,这一切都可以在一台计算机上完成,而且更加方便。
1、构建开发环境
一个好的开发环境无疑是提高开发效率的有利保障,微软的Visual Studio系列IDE虽然非常好用,但是驱动开发领域似乎一直被其所忽视,幸好这种情况随着2012年9月12日Visual Studio 2012(以下简称VS 2012)的发布而结束了。
微软的VS 2012不但是首个支持Windows 的IDE,还是微软首个支持驱动开发的IDE,这无疑会为我们的驱动开发工作省去很多不必要的操作。
我们可以到微软的官方主页获取想要的VS 2012版本。值得注意的是微软的IDE大多都价格不菲,比如其Ultimate版开价近13万元人民币。
不过幸好微软还提供了免费的Express版,其中比较适合我们的是Visual Studio 2012 Express for Windows Desktop,你可以下载这个版本并获得30天的试用时间,若需要在30天后继续使用,则必须通过在线注册以获得一个免费的产品密钥。这个版本虽然会缺失很多功能,但好在不需要为此投入额外的资金,作为学习使用也已经足够了。
有了IDE后,我们还需要驱动开发包Windows Driver Kit 8(以下简称WDK 8),它相比老版本除了加入了很多新特性外,最重要的就是兼容VS 2012开发环境。在安装WDK 8时如果检测到系统中装有VS 2012,它会自动为VS 2012配置驱动开发环境,以使得其具备驱动开发能力。因此在安装时我们一定要注意先后顺序,要先安装VS 2012,然后再安装WDK 8,否则会导致环境配置失败。如果配置失败必须将WDK 8完全卸载并重新安装才能修复。WDK 8的下载地址为http://msdn.microsoft.com/zh-cn/windows/hardware/gg454513。
在安装完WDK 8后系统会提示你安装WDK 8可再发行组件,如果你不小心错过了这一步,可以访问http://msdn.microsoft.com/zh-cn/windows/hardware/br259104进行下载。
安装完毕后,当我们再次打开VS 2012新建项目界面时,就会看到此时已经可以新建驱动项目了,如图1所示。
图1 VS 2012新建驱动项目的界面
2、构建基于Visual Studio 2012的调试环境
驱动程序的调试环境VS 2012已经帮我们设置好了,但是仍然需要配置一下才能进行正常调试。在默认情况下VS 2012已经为本机配置好了大部分双机调试所需要的环境,我们只需要根据传输介质及调试需求的不同选择一种最适合我们的调试环境。
(1)Visual Studio 2012双机调试介绍
VS 2012为我们提供了5种双机调试的连接方式,分别是网络、USB2.0、USB3.0、1394(Firewire)与串行方式,但是无论我们以何种连接方式进行双机调试,都必须保证调试机与目标机(Target)在一个可以互相访问的局域网内,这是因为VS 2012在自动设置目标机的时候需要有网络的支持,只有在启动调试时才会以我们选择的连接方式传输调试信息。其具体示意如图2所示。
图2 VS 2012双机调试连接方式
需要注意的是,如果目标机为Windows 8以上操作系统,则通过网络就可以完成包括调试连接在内的所有调试工作,如果是低版本的操作系统,则只能在USB2.0、USB3.0、1394与串行这4种调试线缆中选择。
(2)配置Visual Studio 2012双机调试
首先需要配置目标机,以使其符合VS 2012自动配置的环境需求。必须使得调试机能在局域网内以管理员权限访问目标机,并能向目标机中写入文件,这就要求调试机与目标机必须在同一局域网的同一工作组下。要做到这点需要执行以下步骤:
1)在目标机上安装用来运行和测试驱动程序的操作系统,并将目标机加入主机所在的网络域。
2)在目标机上,依次选择“控制面板”→“网络和Internet”→“网络和共享中心”,在“查看活动网络”下验证活动网络类型是否为“域”。
3)在目标机上,依次选择“控制面板”→“网络和Internet”→“网络和共享中心”→“更改高级共享设置”。你将看到网络类型的列表:“专用”、“域”、“来宾或公用”等,展开与网络类型(“公用”或“专用”)匹配的标题,选择“启用网络发现”和“启用文件和打印机共享”。
4)在目标机上,启用管理员账户(如果尚未启用)。依次选择“控制面板”→“系统和安全”→“管理工具”→“计算机管理”,导航到“本地用户和组”→“用户”,并双击“管理员”,取消“账户已禁用”复选框选中状态。
5)在目标机上,以管理员身份登录,如果管理员密码当前为空白,则为管理员账户创建密码。
6)验证你是否以管理员身份登录到目标机。
在与客户及建立连接之后,我们需要使用VS 2012配置远程计算机,依次选择VS 2012菜单栏上的DRIVER→Test→Configure Computers,并在弹出的窗口中单击Add new computer按钮,从而调出计算机配置窗口,并在Computer Name后填入目标机器的机器名或IP地址,如图3所示。
图3 计算机配置窗口
由图3可知,VS 2012为我们提供了以下3种配置远程机器的方式:
第一种方式只适用于Windows 8以上操作系统,它可以设置目标机并自动配置调试程序。
第二种方式适用于使用USB2.0、USB3.0、1394与串行方式连接目标机,且第一次对目标机进行设置的情况,它会设置目标机并选择调试程序设置。
第三种方式适用于已经对目标机执行过基本配置,且用户自己对所要更改的调试选项有清晰认识的情况,因为它不会因为用户修改调试配置而去重新配置目标机。例如我们一开始选择的是以串行方式通信的调试方式,但是发现默认的波特率设置得比较低,需要调高一些,这时就可以选择这种方式,但如果一开始我们选择的连接方式是USB2.0,现在要改为1394,就不能再使用这个选项了。
如果我们选择以第二或第三种方式配置目标机,那么当单击“下一步”按钮时VS 2012会引导我们进入调试配置窗口,在此窗口中可以选择连接类型并填写相应的配置信息。关于配置信息,在一般情况下保持默认即可,如图4所示。
图4 连接方式配置窗口
配置完相关信息并单击“下一步”按钮后系统会提示你输入目标机的凭据。在本例中我们的用户名为WIN-DEBUG\Administrator,密码则输入目标机中Administrator账户的密码。接下来VS 2012将会对目标机进行设置,这一过程通常需要十几分钟的时间,而且中间可能会重启几次。整个设置过程中VS 2012会在目标机上执行以下操作:
将安装文件复制到%SystemDrive%\DriverTest。
创建一个名为WDKRemoteUser的用户并切换到该用户。
安装.NET(如果尚未安装)。
安装测试授权和执行框架(TAEF)(WDK客户端)。
安装调试程序。
安装Windows设备测试框架(WDTF)。
关闭AutoReboot。
启用内核内存崩溃转储。
禁用屏幕保护程序。
禁用工作站锁定策略。
禁用ForceGuest。
将电源策略设置为高电源配置,从而防止系统空闲时进入待机或休眠模式。
启用RTC唤醒计时器。
启用和配置内核调试。
启用驱动程序的测试签署。
必要时重新启动目标机。
创建系统还原点。
以上所有设置只要有任何一项未能设置成功则整个目标机自动设置操作都会失败,如果设置失败可以通过查看VS 2012给出的日志文件检查是哪一步未成功。例如目标机如果未开启系统还原服务的话就会导致整个设置的失败。
设置完成后我们就可以对驱动进行简单的调试工作了,选择指定的编译方式后按F5键开始调试,远程调试器附加成功后的效果如图5所示。
图5 VS 2012远程调试器附加成功
由图5可知,VS 2012已经将WinDbg集成了到了调试界面的右下角,此时在我们按下快捷键Ctrl+Alt+Break全部中断后,右下角就会出现WinDbg的命令提示符,此时就可以执行所有WinDbg的命令进行调试了。
3、构建基于WinDbg的调试环境
基于WinDbg的调试环境一直是Windows下内核研究人员的首选调试环境,也是目前最稳定、兼容性最高的调试环境。这里我们讲解怎样使用WinDbg与VirtualKD以VMware为基础建立双机调试环境。以下是对这三款软件的基本介绍。
WinDbg是完全免费的,在我们安装的最新版本的WDK开发包中就包含此工具。
VMware是威睿公司的桌面虚拟化产品,它能在一台计算机上同时运行多个操作系统,是目前世界上最好的虚拟机解决方案之一,但是它不是免费的。我们可以在其官方网站http://www.vmware.com/cn/try-vmware.html处申请其旗下产品VMware Workstation的免费试用权利。除此之外也可以使用其免费提供的VMware Player,此软件可运行由其他VMware产品产生的客户虚拟机,同时也可以自行创建新的虚拟机,但其功能十分有限。
VirtualKD是一个开源软件,因此只需到http://virtualkd.sysprogs.org/处下载即可免费使用。
首先,需要将VirtualKD解压,在解压后的文件夹中有一个名为target的子文件夹,我们需要将其复制到目标机(虚拟机)中,并在目标机中运行里面的vminstall.exe,在随后弹出的对话框中单击Install按钮安装VirtualKD的虚拟机部分,如图6所示。
图6 在目标机(虚拟机)中安装VirtualKD
按照默认设置安装完毕后,VirtualKD会在目标机上添加一个新启动项Windows 7[VirtualKD],此时我们在调试机(宿主机)中运行vmmon.exe或vmmon64.exe来启动VirtualKD监视工具,单击Debugger path按钮选择WinDbg.exe完成基本配置工作,如图7所示。
图7 配置VirtualKD
然后重新启动虚拟机,此时会发现虚拟机默认以新建的启动项启动,如图8所示。
图8 目标机以VirtualKD新建的启动项启动
最后,在虚拟机启动过程中,我们会发现调试机中的VirtualKD监视工具会自动弹出WinDbg,并断到系统断点上。至此,我们已经完成了基于WinDbg的调试环境的配置,并完成了基本的测试。
4、将Rootkit加载到系统
Windows系统加载驱动的方法有很多,其中最正确的方法就是使用SCM(Server Control Manager,服务控制管理器)来完成此操作。使用SCM加载驱动时需要我们注意两点:首先SCM会操作注册表;其次通过SCM加载驱动是不可分页的,这个机制会使驱动的回调函数、IRP处理函数与其他重要代码不会被页换出,而总是驻留在内存中,以此来保证这个驱动不会因为这些原因导致BsoD。
使用SCM加载并运行驱动分为3步,代码清单1将演示如何加载一个驱动。
代码清单1 使用SCM加载驱动E:\Test\Test.sys
对于这段代码我们只需稍加改动就可以应用在以后的项目中。
当然,如果仅仅为了测试驱动,那么完全可以使用驱动加载工具来完成。A1SysTest是一个用于驱动加载控制与测试的小工具,它弥补了现在市面上流通最广泛的驱动加载工具InstDvr存在的诸多不足。A1SysTest使用非常简便,如图9所示。
图9 A1SysTest的主界面
我们打开要加载的驱动后,A1SysTest会自动根据驱动的类型切换到相应的操作界面,然后只需要依次单击“安装”→“启动”按钮,即可将驱动程序安装到系统中并启动。
5、创建一个简单的驱动并调试
现在我们创建一个驱动并尝试进行第一次调试,以便于验证自己构建的环境是否完善。
(1)创建一个简单的驱动
首先我们打开VS 2012并创建一个名为MyFirstDriver的WDM驱动,如图10所示。
图10 使用Visual Studio 2012创建一个空的WDM驱动项目
然后我们打开“解决方案资源管理器”选项卡,在此工程里的Source File分类下新建一个*.CPP文件,并将其命名为Driver.c(或命名为Driver.cpp),完成后如图11所示。
图11 在工程的Source File分类下新建一个文件Driver.c
接下来我们需要将以下代码写入Driver.c中。
最后选择编译方式为Win7 Debug,并在“类视图”的MyFirstDriver项目上右击,在弹出的快捷菜单中选择“生成”,即可在项目目录下的Win7Debug文件夹下生成我们第一个驱动程序,如图12所示。
图12 生成MyFirstDriver项目
(2)准备调试环境
在生成驱动完成后,首先要在调试机(宿主机)运行VirtualKD监视工具vmmon.exe或vmmon64.exe。
然后启动我们已经配置好的Windows 7虚拟机,并以默认的VirtualKD调试模式启动,在系统启动过程中会触发端点,此时调试机的VirtualKD监视工具将自动弹出WinDbg进入调试模式,我们在WinDbg的命令框中输入命令g(或按F5键)让其继续运行即可。
最后等目标机(虚拟机)启动完毕即可。
(3)开始调试驱动
首先我们需要将生成的驱动文件MyFirstDriver.sys与驱动加载控制程序A1SysTest.exe复制到目标机中。
然后使用A1SysTest加载并运行驱动程序MyFirstDriver.sys,如图13所示。
图13 使用A1SysTest加载驱动
当我们单击“启动”按钮时会触发编译在驱动中的int 3断点,此时调试机上的WinDbg在几秒后会进入源码调试。我们可以发现,现在目标机系统已经断到了int3断点处,如图14所示。
图14 WinDbg源码调试状态
接着按F10键两次就应该执行完打印调试信息的DbgPrint函数了。但是Windows 7默认对于调试信息做了过滤处理,因此在默认的情况下是看不到DbgPrint函数在Windows 7打印出的信息的。
我们可以通过修改注册表设置开启Windows 7的调试信息输出功能,需要在注册表中找到HKLM/SYSTEM/CurrentControlSet/Control/Session Manager项,打开或者创建子项Debug Print Filter,然后新建一个DWORD值DEFAULT,将其设置成0xF,重启即可。也可以新建一个以*.reg结尾的文件,并将以下内容写入文件中,然后双击执行,将其导入注册表中。
此时按F5键运行目标机系统,便可以在WinDbg中看到打印出的信息,如图15所示。
图15 WinDbg打印出来的信息
至此我们已经进行完了从驱动的工程创建到驱动调试的整个过程,如果你能在自己的WinDbg上看到调试信息,就证明已经配置好了开发Rootkit所需的所有基本环境。
二、什么是Ring0层
大多数Rootkit都是运行在Ring0层的,并且也都以编写能在Ring0层运行的程序为目标。但这个Ring0层又代表着什么呢?
其实Ring(环)是x86平台进行访问控制的概念,例如Ring0层也常被称为环0层,这个用于访问控制的环共被分为4个层次,其中权限最高的就是Ring0层,一般称之为内核层,权限最低的为Ring3层,一般称之为用户层。Windows操作系统就利用了这两个层:用内核层权限运行系统内核及驱动程序,例如主板驱动或Rootkit;用用户层权限来运行绝大多数普通应用程序,例如Office或普通木马,如图16所示。
图16 x86平台的访问控制
CPU通过环的概念跟踪管理着程序代码和内存分配情况,并借此实施访问限制。当程序被执行时CPU会给其分配一个环编号,环编号高的程序不能直接访问环编号低的程序,例如Ring3程序不能访问Ring0程序及其内存空间。
除了内存访问控制外,某些汇编指令也是需要Ring0权限的,例如负责读写硬件端口的in、out指令及负责控制CPU状态的cli、sti指令等。
三、关键表
CPU除了使用分层机制进行访问限制外,还需要对正在运行的代码进行许多其他的管控,例如CPU必须保证当发生中断、程序崩溃以及硬件发出注意信号时其管控内的代码还能按照“游戏规则”进行,虽然这些机制看起来像是操作系统在帮我们处理,实质上这些特性当中的绝大部分都是由CPU提供最原始的支持的。
而要想很好地实现这些功能,就要求CPU必须维护管理一大堆的数据,它们一般都以表的形式存在。这些重要的表如下:
全局描述符表(Global Descriptor Table,GDT):此表仅有一个,用于映射内存地址。
本地描述符表(Local Descriptor Table,LDT):用于映射内存地址。
页目录(Page Directory):用于映射内存地址。
中断描述符表(Interrupt Descriptor Table,IDT):用于索引中断处理程序。
除了这些CPU表以外,操作系统为了实现某些功能也会自己维护一些重要的表,这其中对我们最重要的就是系统服务调度表(System Services Descriptor Table,SSDT),这个表主要用于协助Windows系统处理系统调用。
四、内存分页
Windows的内存分页机制是比较难理解的部分之一,但好在编写简单的Rootkit并未完全涉及所有这方面的知识,因此我们只需要了解一些比较基础的概念即可。
我们都知道,在32位的Windows系统中,每个进程都有自己独享的4GB内存空间,虽然随着计算机在摩尔定律的“推动”下正在发生着日新月异的变化,但就在2008年以前1GB内存的计算机还是能算得上主流配置的。那么按照每台计算机同时运行30个进程计算的话,这1GB内存是无论如何也满足不了需求的。那么Windows是怎么解决这个问题的呢?答案就是使用虚拟内存的方式。
在现实环境中运行的程序所用的内存都是非常小的,例如一个体积比较大的1MB的应用程序,运行后其本身所占用的内存应该不会超过4MB。因此虽然操作系统给了它4GB的虚拟内存,但是其运行后真正在物理内存上占用的空间可能仅有4MB。除此之外,每个应用程序所面对的内存地址也是基本相同的。举例来说,进程A的理论可用内存地址范围为0x00000000~0xFFFFFFFE,进程B的理论可用内存地址范围也是如此,这样当进程A在访问0x00401234时得到的可能是一个4字节数据0xAAAAAAAA的起始地址,而进程B在访问0x00401234时得到的则有可能就是一个4字节数据0xEEEEEEEE的起始地址,如图17所示。
图17 两个不同的进程在访问同一虚拟地址时的情况
为了实现这种巧妙的映射,Windows系统引入了页表的概念。
我们可以将使用内存的整个过程想象为一个懒惰的作者在写书,将CPU想象为一个负责任的策划编辑,这个懒惰的作者计划写上万本书(就像每个进程都有4GB的内存空间),但实际上他只能写一小部分(就像实际的物理内存往往都要小很多),但是这个作者可以为这些书都先起好一个名字,并弄个作品列表(相当于页目录),并为每本书拟好章节(页表),最后再定好每个章节所在的页数(字节索引)。因此当这个懒惰的作者想写某本书的某一页时,他需要先通过作品列表(页目录)确定书的名字,再找到相应的章节(页表)以便所引导指定的页数(字节索引),这其中的每一步操作都会受到策划编辑(CPU)的审核,如果策划编辑不允许其写作(描述符检查)或不允许其写某本书(页目录检查)或某一章(页面检查),则该访问都会被拒绝,由此可见,进程必须通过所有的安全检查才能读出一个页面。可见整个内存访问的工作量还是非常大的。
1、地址转译
我们通过图17已经对Windows系统的内存映射建立起一个基本概念,但是图中的“地址经过页表转换”恐怕会令很多人感到莫名其妙。为什么不同进程访问的同一虚拟地址,经过转换后就变成了不同的物理地址呢?这是种什么样的映射关系呢?
其实Windows是通过页目录、页表(Page Table)与页表项(Page Table Entry,PTE)这种二级表的结构将虚拟地址转译成物理地址的。
首先,Windows会给每个进程分配一个不同的页目录用于保存这种二级表结构,正是由于每个页目录中保存的虚拟地址到物理地址的映射关系的不同,才导致了图17所描述的情况。
通常来讲,一个32位的虚拟地址会被解释为3个部分,即页目录索引、页表索引与字节索引。这3个部分的索引值负责在描述内存映射的结构中索引物理地址信息,如图18所示。
图18 虚拟地址0x00401234的示例
页目录索引用来在页目录中索引此虚拟地址的页目录项(Page Directory Entries,PDE)从而找到PTE所在的页表;页表索引用来在正确的页表中索引PTE,从而找到目标内存页;字节索引则负责在目标内存页中索引最终的物理地址。整个索引过程如图19所示。
图19 在x86平台转译一个有效的虚拟地址
下面我们将根据图19详细讲解一下转译一个虚拟地址的步骤。
1)内存管理硬件找到当前进程的页目录,并且每次在进程环境切换时也会得到一个新的进程页目录地址。
2)在进程页目录中使用页目录索引可以找到相应的PDE,此PDE包含了此页表的页面帧编号(Page Frame Number,PFN),通过这些信息即可找到包含此虚拟地址映射信息的页表。
3)在找到的页表中使用页表索引即可找到PTE,它描述了此虚拟地址所在的物理位置的信息。
4)根据PTE索引到页面后,需要判定该页面是否有效,如果该页面是有效的,则它将包含此虚拟页所对应的物理内存页信息的PFN;如果该页面是无效的则内存管理错误处理器会找到该页面,并尝试使之变为有效的;如果该页面不能变成有效的,则错误处理器会产生一个非法访问错误或一个错误检查。
5)如果上一步PTE所指向的页面是有效的,那么在其指向的物理内存页中使用字节索引,即可定位到目标数据的物理地址上。
现在我们实际操作一下,利用这些知识将一个虚拟地址转换为物理地址。我们的实验目的是将保存在记事本内存中的一段文本找出来,并记下其虚拟地址,然后用我们所学的知识将其转换为物理地址,并加以验证。
首先,我们需要运行VirtualKD以及虚拟机,使虚拟机以默认的VirtualKD调试模式启动,并确认虚拟机系统未开启PEA。如果不确定当前系统所处状态,请在命令行下依次执行BCDEdit/set PAE ForceDisable、BCDEdit/set NX AlwaysOff命令,并手动重启系统以完成关闭PEA的操作。
系统准备好后打开记事本程序,并在记事本内输入一段文字"Hello A1Pass!",如图20所示。
图20 在虚拟机的记事本中输入
此时返回宿主机的WinDbg界面,按快捷键Ctrl+Break使虚拟机暂停,并输入以下命令查看记事本程序notepad.exe的进程信息。
WinDbg返回的信息如图21所示。
图21 WinDbg的返回结果
为了节省版面,同时为了使得返回结果更清晰,我们会将WinDbg中显示的结果直接展示给出来,以本例为例,WinDbg返回的结果如下:
其中有下划线的是我们输入的命令,加粗字体则是需要大家注意的地方。通过粗体字我们得知记事本进程的两个关键信息,一个是其进程结构的起始地址0x884ead40,另一个是其页目录的起始地址0x32303000。我们可使用切换当前进程命令.process,并以记事本的进程结构的起始地址为参数,将WinDbg的当前调试进程切换到notepad.exe中,结果如下:
下面我们使用搜索命令s,以搜索Unicode字符串的模式,从notepad.exe内存空间的0x00000000处开始搜索字符串"Hello A1Pass!",搜索长度为0x01000000字节,搜索结果如下:
通过结果我们得知,在虚拟机中记事本里输入的字符串被保存到了内存的0x007C0BD4处。现在我们就是要将这个位于notepad.exe进程内的虚拟地址转换为物理地址。
然后,使用查看Unicode字符串的命令du查看位于本进程虚拟地址0x007C0BD4处的字符串,执行结果如下:
最后,在确认无误后,我们用系统自带的计算器将0x007C0BD4转为二进制,并按照虚拟地址的分割规则将其分割,得到的结果如图22所示。
图22 虚拟地址0x007C0BD4的分割结果
通过图22可知,地址0x007C0BD4被我们分解成3条信息:页目录索引0x1、页表索引0x3C0与字节索引0xBD4。
接下来使用!dd命令查看一下notepad.exe进程页目录0x32303000内的值,结果如下:
由虚拟地址分解出的目录索引值可知,PFN(页面帧编号)为1的地方(由0开始)保存着页表地址(也就是第2个地址0x32103867),由于PDE的前12位保存有此页目录项的状态和保护位信息,因此页表地址应该是0x32103000。
而根据页表索引可知,PFN为0x3C0的地方保存着页地址,且由于地址信息的长度为4字节,因此PTE应该位于0x32103000+0x3C0*4处。用!dd命令查看后结果如下:
PTE信息0x083FF847和PDE类似,PTE的前12位保存页表项的状态和保护位信息,因此页面地址应该是0x083FF000。再用分解出的字节索引值0xBD4与其相加,便得到了最终的物理地址0x083FFBD4。
现在我们用查看Unicode字符串命令du验证一下,看看虚拟地址0x007C0BD4与物理地址0x083FFBD4是否指向同一组数据。执行结果如下:
到此,我们已经完成了虚拟地址到物理地址的转换操作,这种手工将虚拟地址转换为物理地址的操作让我们切实感觉到了整个过程的繁琐,其中几乎每一步操作都涉及对某个表的读取,而且所有这些信息都可以被Rootkit修改并加以利用。
2、内存访问检查
程序访问内存时,x86处理器会依次对其进行描述符检查、页目录检查与页面检查,关于页目录检查与页面检查我们通过前面对手工转译虚拟地址的学习已经有所感悟,PDE与PTE都将前12位用作存储状态和保护位信息,下面我们将逐一讲解x86处理器内存访问检查的关键步骤。
(1)描述符检查
在执行描述符检查时通常要访问全局描述符表(GDT),并检查段描述符中一个名为描述符权限级别(Descriptor Privilege Level,DPL)的值,DPL包含有访问进程所需的环编号(0~3)。若DPL低于当前权限级别(Current Privilege Level,CPL)则终止内存检查,并拒绝此次访问。
我们可以使用显示选择段子命令dg,用Ring0下GDT的公共选择值8查看GDT的相关信息,截至选择段子为30,其结果如下:
由此结果我们发现,前4项(08、10、18、20)已经包含了Ring3的代码数据与Ring0代码数据的所有内存空间,这表明了GDT实质上并没有为系统的安全出力。
(2)页目录检查
内存管理器为每个进程都创建了一个页目录,其中映射了此进程中所有页表的位置。页目录是由页目录项(PDE)构成的,每个PDE长度为4字节(开启了PAE的系统上为8字节长),其结构如图23所示。
图23 未启用PAE系统的页目录项(PDE)的格式
系统在索引到PDE时,首先会检查其U/S位,若该位为0(管理员权限)则只有拥有Ring0~2权限的程序才能访问,若该位为1则任何程序都能访问。
(3)页面检查
每个页面都由一个页表项(PTE)来描述其具体位置、状态和保护位信息,而PDE所指向的页表则正是由1024个PTE所组成的阵列,PTE的结构与PDE相差无几,如图24所示。
图24 未启用PAE系统的页表项(PTE)的格式
系统在索引到PTE时的逻辑与PDE相同,同样会检查其U/S位。
3、Windows对重要表的保护
Windows XP及后续版本的操作系统会将包含有SSDT与IDT信息的内存页属性设置为只读,如果想利用这些关键表制作Rootkit就必须将其改为可写的。目前突破这个限制有两个方法:一个是利用CR0欺骗,另一个就是修改以下注册表键并重启系统。
五、内存描述符表
内存描述符表是一些Rootkit经常会用的关键表,这类描述符表有多种类型,比如GDT、LDT等,它们几乎都可以被Rootkit程序加以利用。
1、全局描述符表与局部描述符表
全局描述符表(GDT)是全局的,一个系统中通常只有一个GDT,供系统中的所有程序和任务使用。而局部描述符表(LDT)则与进程相关,每个进程既可以有一个LDT,也可以与多个进程共享LDT。
GDT使用GDTR寄存器用来标识GDT的位置和边界,SGDT汇编指令与LGDT汇编指令分别负责查看GDT的地址与修改GDT的地址。
Rootkit一般会通过利用GDT或LDT使得某些特定进程可以在用户模式下装入并执行任何内核模式代码。
2、代码段
当CPU要访问一段为代码属性的内存时,就会使用代码段寄存器CS指定的段,代码段可以在描述符表中加以指定,所有程序都可以通过执行远程JMP、CALL等指令修改CS寄存器,而执行这些命令的前提就是引用GDT或LDT中已经安装的一个调用门。
3、调用门
调用门是GDT中的一类拥有4个字段的描述符,其中一个字段负责描述特权级(DPL),每当执行代码试图使用一个调用门时,处理器都会检查其DPL以判断其特权级是否符合要求。如果我们打算安装自己的调用门,就可以将DPL设置为任何值。
六、中断描述符表(IDT)
IDT在内存中的起始地址保存在中断描述符表寄存器(Interrupt Descriptor Table Register,IDTR)中,Windows系统在引导时会在IDTR中读取IDT在内存中的起始地址,并将内核中负责处理每个中断和异常的中断服务例程(Interrupt Service Routines,ISR)指针填充到IDT中,这里的ISR指针通俗地讲就是负责处理中断信息的功能函数指针。
在保护模式下的IDT是一个包含256项8个字节的数组,每项都包含ISR的地址及其他的一些安全信息。除此之外IDT还有一个特性需要我们注意,就是每个处理器都有且必须有一个单独的IDT。在多处理器的环境中,可能在同一时间系统会处理不同的中断请求,因此不同的处理器是可以运行不同的ISR的。
我们可以使用SIDT指令与LIDT指令读取或修改IDTR中保存的IDT内存地址信息。我们可以使用如下结构来保存由SIDT读取出来的内容。
在设计Rootkit时,利用好这个结构可以带给攻击者们很多厉害的功能,例如可以通过修改IDTR创建一个IDT的副本,然后便可隐藏攻击者对真实IDT所做的修改,借此达到对抗反病毒引擎、保护IDT完整性的目的。
而攻击者最终关心的还是IDT本身,他们往往试图通过篡改IDT的ISR信息达到拦截并过滤信息的目的。IDT的结构并不复杂,通常我们可以使用以下结构体来描述它:
此结构有时被称为中断门,它的作用是在内存中定位用于处理某个中断的函数,通过中断门可以使RIng3的应用程序调用处于Ring0的例程,例如我们比较熟悉的进入内核的汇编指令INT 0x2E就是通过触发位于IDT表中0x2E处的ISR来完成的。
为了便于理解,大家可以仔细阅读一下遍历系统IDT信息的例子,部分关键代码如下:
七、系统服务调度表
系统服务调度表(SSDT)可能是Windows操作系统中被恶意程序利用得最广泛的关键表,也正因此,SSDT也就成了很多Rootkit初学者首选的学习对象。
SSDT的作用是满足处于用户模式下的程序执行系统函数这一个需求,处于用户模式下的程序可以通过系统提供的机制借助SSDT查找用户请求与系统服务的对应关系,进而使得将处于用户模式下的程序请求迁移到内核模式下进行处理成为可能,这个过程称为系统服务调度。
需要注意的是,SSDT及与其相关的若干机制并非是在CPU中实现的,而是由操作系统提供的。在Windows操作系统中,应用程序通常使用SYSENTER、SYSCALL或INT 0x2E执行系统服务调度功能。系统接收到相关请求后会在内核中调用KiFastCallEntry()函数,然后在导出的变量KeServiceDescriptorTable中获取当前SSDT的地址。此函数会从eax寄存器中读取系统调用的编号,并在SSDT中查询该调用。
八、控制寄存器
控制寄存器(CR0~CR3)最初出现在低级的286处理器中,用于控制和获取处理器的操作模式及当前执行任务的特性。以前称为机器状态字,随着386处理器系列的发布,重新将其称为控制寄存器。
1、利用CR0禁用内存保护机制
在486系列处理器出现后,CR0中才添加了用户控制写保护的WP位,如果该位被置为0就会禁用内存保护机制,攻击者可以借此篡改操作系统中一些本来为只读属性的关键表,从而达到其监控过滤的目标。
然而就目前来讲,利用CR0禁用或重启系统内存保护机制是非常容易的,其关键代码如下:
2、其他控制寄存器
(1)CR1~CR4
除了CR0以外,还有4个控制寄存器(CR1~CR4)用于控制和管理处理器的其他方面:CR1为在文档中说明用途;CR2用于在保护模式下保存上一次导致页故障的地址;CR3用于存储页目录;CR4用于控制虚拟8086模式的开启。
就目前来讲,这些寄存器对于我们了解Rootkit并没有帮助,因此有需要的朋友可以自行阅读Intel文档。
(2)EFlags寄存器
EFlags寄存器负责处理陷阱标志,如果设置了该标志,处理器将进入单步执行状态,Rootkit可以利用这个特性来检查是否存在调试器或在反病毒引擎扫描器面前将自己隐藏起来。
九、结语
本文紧密围绕着Rootkit讲解了一些必备的环境搭建及硬件、操作系统的底层知识,这些知识是学习Rootkit技术的必备前提。
但是着眼长远,驾驭Rootkit的能力最终还是与我们对CPU机制及操作系统结构的了解程度密切相关的。虽然讲解了一些必备的基础知识,但就整个知识体系来说只是九牛一毛。因此如果大家想深入了解Rootkit技术,不断补充操作系统相关的基础知识是必要的。
微信公众号:计算机与网络安全
ID:Computer-network