1、构建Rootkit基础环境
1.1、构建开发环境
VS2012+WDK8
1.2、构建基于VS2012的调试环境
- 将目标机、调试机配置在同一个工作组内
- sVS2012配置->DRIVER->Test->Configure Computers,Add New computer按钮。
1.3、构建基于Windbg的调试环境
WinDBG+Vmware+VirtualKD
1.4、将Rootkit加载到系统
- SCM(服务控制管理器)加载驱动
- OpenSCManager():获取SCM句柄
- CreateService():创建一个服务对象,并将其添加到指定的服务控制管理器数据库的函数。
- CloseServiceHandle(handle):关闭SCM句柄
1.5、创建一个简单的驱动并调试
- DriverEntry:驱动的入口函数,相当于用C语言编写用户层程序时的Main()函数
- VirtualKD:运行vmmon.exe,在虚拟机运行书中自带的.reg,加载*.sys驱动程序就可以进行调试了
2、Ring0
Rootkit运行在Ring0层的,Ring是x86平台进行访问控制的概念,这个用于访问控制的Ring被分为4个层次,其中权限最高的是Ring0层,称之为内核层,权限最低的为Ring3层,一般称为用户层。
3、关键表
当发生中断、程序崩溃以及硬件发出注意信号时,CPU必须维护管理一大堆的数据,这些数据以表的形式存在。
- 全局描述符(GDT):用于映射内存地址
- 本地描述符(LDT):用于映射内存地址
- 页目录(Page Directory):用于映射内存地址
- 中断描述符表(IDT):用于索引中断处理程序。
除了这些CPU表以外,操作系统为了实现某些功能也会维护一些重要的表。其中最重要的就是系统服务调度表(SSDT),这个表主要用于协助Windows系统处理系统调用。
4、内存分页
32位的Windows系统中,每个进程都有自己独享的4GB内存空间。为了解决内存不足的问题,Windows通过虚拟内存的方式进行解决。
4.1、地址转译
Windows通过页目录、页表(Page Table)与页表项(Page Table Entry,PTE)这种二级表的结构将虚拟地址转译成物理地址的。
转译一个虚拟地址的步骤:
- 1、内存管理硬件找到当前进程的页目录,并且每次在进程环境切换时也会得到一个新的进程页目录地址。
- 2、在进程页目录中使用页目录索引可以找到相应的PDE,此PDE包含了此页表的页面帧编号(Page Frame Number,PFN),通过这些信息即可找到包含此虚拟地址映射信息的页表。
- 3、在找到的页表中使用页表索引即可找到PTE,它描述了此虚拟地址所在的物理位置的信息。
- 4、根据PTE索引到页面后,需要判定该页面是否有效,如果该页面是有效的,则它将包含此虚拟页所对应的物理内存页信息的PFN;如果该页面是无效的则内存管理错误处理器会找到该页面,并尝试使之变为有效的;如果该页面不能变成有效的,则错误处理器会产生一个非法访问错误或一个错误检查。
- 5、如果上一步PTE所指向的页面是有效的,那么在其指向的物理内存页中使用字节索引,即可定位到目标数据的物理地址上。
4.2、内存访问检查
- 1、描述符检查
在执行描述符检查时通常要访问全局描述符表(GDT),并检查段描述符中一个名为描述符权限级别(DPL)的值,DPL包含有访问进程所需的Ring编号(0-3)。若DPL低于当前权限级别(CPL)则终止内存检查,并拒绝此次访问。
- 2、页目录检查
内存管理器为每个进程都创建了一个页目录,其中映射了此进程中所有页表的位置。页目录是由页目录项(PDE)构成的,每个PDE长度为4字节。
系统在检索到PDE时,先检查其U/S位,若该位为0(管理员权限)则只有拥有Ring0~2权限的程序才能访问,若该位为1则任何程序都能访问。
- 3、页面检查
每个页面都由一个页表项(PTE)来描述其具体位置、状态和保护位信息,而PDE所指向的页表则是1024个PTE所组成的阵列,PTE的结构与PDE相差无几。
系统在索引到PTE时的逻辑与PDE相同,同样会检查其U/S位。
4.3、Windows对重要表的保护
Windows XP及后续版本的操作系统会将包含有SSDT与IDT信息的内存页属性设置为可读。
5、内存描述符表
- 1、全局描述符与局部描述符表
- GDT:全局描述符(GDT)是全局的,一个系统中通常只有一个GDT,供系统中的所有程序和任务使用。
- LDT:局部描述符表(LDT)与进程相关,每个进程即可有一个LDT,也可以与多个进程共享LDT。
- Rootkit:通过利用GDT或LDT使得某些特定进程可以在用户模式下装入并执行任何内核模式代码。
- 2、代码段
CPU访问一段代码属性的内存会使用代码段寄存器CS指定的段。
- 3、调用门
调用门是GDT中的一类拥有4个字段的描述符,其中一个字符按负责描述特权级(DPL)。
6、中断描述符表
IDT在内存中的起始地址保存在中断描述符表寄存器IDTR中,Windows系统在引导时会在IDTR中读取IDT在内存中的起始地址,并将内核中负责处理每个中断和异常的中断服务例程ISR指针填充到IDT中。
在保护模式下的IDT是一个包含256项8个字节的数组,每项都包含ISR的地址及其他的一些安全信息。
每个处理器都有且必须有一个单独的IDT。设计Rootkit时,通过修改IDTR创建一个IDT的副本,然后可隐藏攻击者对真实IDT所做的修改,借此达到反病毒引擎、保护IDT完整性的目的。
7、系统服务调度表
系统服务调度表(SSDT)的作用满足处于用户模式下的程序执行系统函数这一个需求,处于用户模式下的程序可以通过系统提供的机制借助SSDT查找用户请求与系统服务的对应关系。
8、控制寄存器
控制寄存器(CR0~CR3)
8.1、利用CR0禁用内存保护机制
CR0有用户控制写保护的WP位,如果该位置被置为0就会禁用内存保护机制,攻击者可以借此篡改操作系统中一些本来为只读属性的关键表,从而达到其监控过滤的目标。
8.2、其他控制寄存器
- 1、CR1~CR4
CR1在文档中说明用途,CR2用于在保护模式下保存上一次导致页故障的地址,CR3用于存储页目录,CR4用户控制虚拟8086模式的开启。
- 2、EFlags寄存器
EFlags寄存器负责处理陷阱标志,如果设置了标志,处理器将进入单步执行状态,Rootkit可以利用这个特性来检查是否存在调试器或在反病毒引擎扫描器面前将自己隐藏起来。
9、用户模式Rootkit
9.1、DLL远程注入技巧
- 使用注册表注入DLL
REG注入原理是利用在Windows 系统中,当REG以下键值中存在有DLL文件路径时,会跟随EXE文件的启动加载这个DLL文件路径中的DLL文件。当如果遇到有多个DLL文件时,需要用逗号或者空格隔开多个DLL文件的路径。
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows
- 使用Windows钩子注入DLL
SetWindowsHookEx()
- 使用远程线程注入DLL
CreateRemoteThread()执行远程进程空间中的LoadLibrary()函数加载DLL。
原理是基于每个进程都必须加载Kernel32.dll模块,且Kernel32.dll模块中导出LoadLibrary()函数。
9.2、内联钩子
内联钩子:替换目标函数的部分代码,以达到控制其执行流程的目的。为一个系统API添加自定义的功能函数。
原理:因为微软为了便于热补丁操作而刻意将用户层的前5个字节都设计成同样的起始格式,这被称为前同步码(Preamble)。替换这5个字节为指向钩子函数的JMP指令,由于每个系统API函数的前同步码是一样的,在执行完钩子函数后跳转到原函数头5个字节后的代码中即可。
JMPCODE jcode;
jcode.jmp = 0xe9;//jmp
jcode.addr = (DWORD)myMessageBox - (DWORD)RealMessageBox - 5;
9.3、导入地址表钩子
导入地址表钩子(IAT Hook)通过修改PE文件中的IAT部分而达到钩子目的。
9.4、保护文件不被删除
以内联的钩子对CreateFileW函数Hook,对此函数的参数进行判断。当发现打开的文件名称中含有特征字则拒绝执行打开操作。
10、内核模式Rootkit
10.1、驱动设备与请求处理
驱动设备与请求处理是驱动程序与其他组件或应用程序进行交互的基本机制。
IOCTL(I/O Control):内核级驱动通信方式。
驱动程序与外部通信总共由3个部分组成:
-
设备对象
IoCreateDeviceSecure:创建设备对象
-
符号链接
IoCreateSymbolicLink:创建符号连接
-
请求处理
操作系统根据MajorFunction将IRP传送到不同的派遣函数中。
IRP_MJ_DEVICE_CONTROL 能找到派遣函数
10.2、函数调用
大多数内核编程接口都拥有一个前缀。常见前缀Io、Ex、Rtl、Ke、Zw、Nt、Ps与Wdf。
- Io前缀:涉及I/O管理器
- Zw前缀:与NT为前缀的函数具有同样的功能,这些函数都有用户层API函数与之对应。
10.3、内核模式Rootkit-SYSENETR钩子
SYSENTER钩子:
- 读取SYSENTER_EIP_MSR寄存器的信息,并备份系统kiFastCallEntry函数的地址
- 构建一个自己的MyKiFastCallEntry函数用以过滤调用信息。
- 设置SYSENTER_EIP_MSR寄存器指向自己构造函数地址
- 摘除钩子,则将备份的系统kiFastCallEntry函数地址写回SYSENTER_EIP_MSR寄存器。
10.4、SSDT钩子
系统服务调度表(System Services Descriptor Table,SSDT)可以基于系统调用编号索引以定位相应函数的内存地址。
- 找到SSDT地址
- 导入需要勾住的内核函数
- 使SSDT所在的内存页可写
- 替换SSDT中指定内核函数为我们自定义的函数
- 如果需要摘除钩子,将被勾住的函数还原后将SSDT所在的内存设为初始状态。
10.5、内联钩子
- 编写跳板函数
- 获取需要安装钩子的函数地址
- 修改内存属性后安装钩子,将函数执行流程转移到跳板函数(将安装钩子的函数头5个字节改成jmp 模块名)
- 如果有摘除钩子的需要,则摘除完毕后还原内存属性
10.6、IRP钩子
- 1、替换驱动对象中的IRP派遣函数
- 2、替换IRP中的完成例程
- 3、编写自定义的IRP完成例程
- 4、摘除钩子
10.7、LADDR钩子
- 设计设备扩展中保存的数据结构,便于后期附加或摘除过滤设备时使用
- 设置过滤驱动的IRP派遣函数,以便获得IRP的控制权
- 设计自定义的派遣函数与完成例程
- 使用过滤驱动生成过滤设备,并分别附加到指定驱动对象的所有设备上。
- 编写摘除过滤设备的函数
10.7、IDT钩子
IDT属于硬件关键结构,每个CPU核心都拥有一个IDT。
- 获取IDT入口点及需要处理IDT的ISR,并保存ISR留待恢复时使用;
- 准备一个自定义的ISR函数用以处理此IDT的中断请求;
- 替换需要处理IDT的ISR为自定义的ISR函数地址,即可钩住此IDT;
- 摘除钩子,则将保存的原ISR写回即可。
10.8、IOAPIC钩子
I/O高级可编程中断控制器(I/O Advanced Programmable Interupt Controller,IOAPIC)是一个可以用于控制多个CPU核心中断操作的新型中断控制器。
IOAPIC负责控制将IRQ发送给哪个CPU,以何种方式发送,使用好IOAPIC可以用比较底层的方式完美地实现一个支持多核CPU的键盘嗅探器。
- 映射IOAPIC的关键寄存器至虚拟内存空间
- 修改中断处理向量
- 获取并保存原键盘中断的ISR
- 赋值原键盘IDT到未被占用的IDT中,并将ISR修改为自己的处理函数
- 如果需要摘除钩子,恢复IRQ1后,将找到的空中断向量重新置为空即可。