高级Bootkit:Tophet.a
摘要:
本文揭示了一种新型的Bootkit技术:Tophet,以及其第一代范本Tophet.a使用的一些新颖的技术。Tophet.a并非病毒或木马,只用来演示高级的启动、穿透与隐身技术。
Bootkit是更高级的Rootkit,该概念最早于2005年被eEyeDigital公司在他们的“BootRoot"项目中提及,该项目通过感染MBR(磁盘主引记录)的方式,实现绕过内核检查和启动隐身。可以认为,所有在开机时比Windows内核更早加载,实现内核劫持的技术,都可以称之为Bootkit,例如后来的BIOSRootkit , VBootkit,SMMRootkit等。
在现在MBR\BootSector\Nt OS Loader这些众所周之的位置都被HIPS监视软件、检查软件严防死守,而BIOS,SMM, ROM firmware之类的启动位置又存在被锁定或通用性不够好的时候,如何简单、通用,又有效地进行Windows内核启动劫持呢?Tophet.a使用了一种新的方式:NtBootdd.sys。
同时,Tophet.a揭示了一些磁盘级的穿透、隐藏技术,可以穿透目前所有防御软件,进行安装,同时在目前任何Rootkit文件检测技术下隐身。
关键字:Bootkit 内核劫持磁盘穿透与隐藏
第一章:启动
最早的Bootkit使用MBR(主引导记录)感染或BootSector(启动引导扇区)感染手段来实现早于Windows内核启动,一般考虑MBR或BootSector中的代码负荷有限,会在感染代码中,挂钩Windows内核,再进一步实现具体功能,常用手段是挂钩INT0x13中断,干涉ntos loader对内核文件的读取,实现 Windows内核挂钩。
现在看来,这种手段不免过于原始,对MBR/BootSector这些关键位置,只要严格防御或者定时检查,即使Bootkit在系统启动获得控制权后对MBR或BootSector扇区的感染进行隐藏,也难逃使用低级磁盘读写的检查恢复方式。
感染ntldr和osloader的方式,存在和上面的感染方式同样的问题。
至于BIOSRootkit , SMM Rootkit ,VBootkit,由于其过于依赖硬件,除了有通用性的难题,再则硬件厂商也开始逐步注意安全问题,BIOS和SMM的写入锁定,启动时抢占VMroot等方式,也限制了这类Rootkit的应用。
如何不感染系统关键文件、位置和硬件,来实现内核劫持呢?
boot.ini是一个很好的突破点。
boot.ini中的很多选项都可以用来实现内核劫持,例如启动条目的/KERNEL,/HAL参数可以用来进行内核文件的替换和劫持,这已经是众所周之的了。另外,DEBUG选项和VGA选项都可以用来做劫持或辅助劫持
Vista操作系统已不再使用boot.ini这个文件作为启动配置,其启动配置保存于SystemPartition\boot\bcd这个HIVE文件中,而这个HIVE的注册表项中的GUID即是相关的启动选项,与以往的boot.ini并没有本质上的区别,本文将不为Vista操作系统做特别讨论,下面的相关细节都以WindowsXP 操作系统为准。
比起简单的/KERNEL参数替换内核,这里tophet.a用的是更为隐蔽的方式。
我们知道,WindowsNT的启动过程简单来说是这样的:
BIOS进行上电引导自检等操作后,将控制权交给主引导记录(MBR)的引导代码,引导代码读取硬盘分区表(DPT)后,找到引导分区,并将控制权移交启动扇区(BootSector)的启动代码,启动代码使用其自带的只读FAT32或NTFS的代码,读出其根目录下的NTLDR文件,并将控制权交给NTLDR中的代码,NTLDR首先将系统切换到保护模式,并开启分页机制,然后控制权被交给NTLDR体内的OSLoader.exe,OSLoader读入boot.ini文件,根据配置的启动条目显示启动菜单,等待用户选择(如果只有一个启动项,则直接启动那个启动项, 不显示启动菜单),用户选择指定的启动项目后,便会引导相应的项目。
下面是一个普通的boot.ini中的一条启动项:
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="MicrosoftWindows XP Professional" /noexecute=optin /fastdetect
boot.ini中启动项是符合ARC(AdvancedRISC Computing)命名规范的。
请注意前面的multi(0),Windows有三种语法:multi, scsi 和signature。当引导卷所在的磁盘控制器支持BIOS的INT0x13中断读写磁盘时,就会使用multi语法,这也是绝大部分系统上使用的语法。而scsi语法则是当磁盘控制器不支持INT0x13指令时使用的,此时,NTLDR在确定了引导项后,会从根目录加载一个名为NtBootdd.sys的文件,将其绑定到OSLoader上,OSLoader中包含的只读NTFS/FAT32代码会使用该驱动来读写磁盘,实现Windows内核文件(ntoskrnl.exe)、硬件抽象层文件(hal.dll)和BOOT驱动文件的加载。
那么,我们只需要将boot.ini中当前系统的启动条目中的multi语法改为scsi语法,系统就会在进行内核加载前,加载我们放置在根目录下的NtBootdd.sys文件了。
这里介绍一个小技巧:如何知道当前系统是由boot.ini中的哪个启动条目引导的呢?
在WindowsXP及以后的操作系统中很简单,注册表的HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control键下的SystemBootDevice键值就保存着当前系统引导的启动项目的内容,用该键值的内容同boot.ini的条目对比,不难发现引导当前的系统的启动条目。
在Windows2000操作系统上则稍微复杂点,该系统上并没有这么一个键值,但HKEY_LOCAL_MACHINE\System\Setup键下有个名为"SystemPartition"的键值,该值指明了当前启动卷的设备名,例如:\Device\HarddiskVolumeX“(X为磁盘分区号),而在系统对象目录的\ArcName目录下保存了这些磁盘设备名对应的ARC名,形如multi(0)disk(X)rdisk(Y),这些Arc名实际是一些符号链接(SymbolicLink),在对象目录中匹配取得启动卷的设备名对应的ARC名,再在Boot.ini中匹配ARC名,即可确定当前的启动条目。
言规正传,OSLoader在读入boot.ini到其数据段的一块缓存并确定了启动条目后,首先会检查该条目的前5个字符是不是:"scsi(",如果是,就会调用名为AEInitializeIo()的函数,该函数会调用BlLoadImage函数加载根目录下的NtBootdd.sys到内存中,然后使用BlAllocateDataTableEntry函数为该模块分配一个名为"NTBOOTDD.SYS"的DataTableEntry(该表类似于Windows内核使用的PsLoadedModuleList,是OSLoader用来管理加载模块使用的,在引导Windows内核过程中,会被复制到PsLoadedModuleList中)。
接着它会扫描OSLoader的引入表,将NtBootdd.sys所引出的ScsiPort读写函数绑定到OSLoader的引入表中,然后会调用NtBootdd.sys的入口点,以便NtBootdd.sys完成自己的初始化工作,如果初始化返回成功,则继续进行名为HardDiskInitialize的函数,让以后的磁盘读写操作都通过NtBootdd.sys来实现,若返回失败,则结束AEInitializeIo函数,继续正常的引导过程。
那么我们用来劫持的NtBootdd.sys要完成的工作就是:
-
不导出任何函数,这样就不会影响OSLoader的引入表
-
在初始化函数中完成相关的钩子例程及处理例程,然后返回失败,让系统仍然使用INT0x13来读写磁盘
-
需要注意的是,我们需要在初始化例程中将ntldr中已读入的boot.ini进行修改,将scsi语法重新改回multi语法,否则在后面的引导过程中ntldr会认为我们使用了错误的引导语法,导致系统无法启动。读入的boot.ini是保存在OSLoader的数据段的一个全局变量中的,我们扫描整个OSLoader镜像,即可找到需要修改的数据。
-
为了劫持内核,Tophet.a进行了这样的工作:因为调用NtBootdd.sys的初始化例程的函数是AEInitializeIo,那么我们在初始化例程中通过堆栈就可以轻松找到该函数callDriverEntry的下一条指令位置,然后我们从这条指令向上搜索,就可以准确稳定地找到BlLoadImage函数的地址,OSLoader正是通过这个函数来加载包括Windows内核文件、HAL和boot驱动的模块的,我们挂钩该函数,就可以准确地知道Windows内核文件加载的瞬间,并对其内存镜像进行修改和劫持。
下面是Tophet.a的NtBootdd.sys的DriverEntry函数
NTSTATUS __declspec(naked)DriverEntry(ULONG xx , ULONG xx1)
{
__asm
{
//将esp保存,接下来的domywork函数会根据该值找到堆栈中callDriverEntry的下一条指令,并搜索、挂钩BlLoadImage
mov SavedEsp , esp
push eax
push ebx
//NTLDR会将osloader.exe重定位到它的PE头的ImageBase域指示的地址
//因此我们从0x400000开始搜索
//这个地址可以根据osloader的ImageBase不同改变(安装时自修改)
mov eax , 0x400000
//搜索范围0x80000字节
mov ebx , 0x7FFFA
LoopFind:
//寻找字符串'scsi'
cmp dword ptr[eax + ebx], 0x69736373
jnz NotCatch
//寻找字符串'(('
cmp word ptr[eax + ebx + 4], 0x2828
//osloader.exe会读入boot.ini并将其放到自己的全局变量中
jnz NotCatch
//将其改为multi,否则系统将无法正常启动
mov dword ptr[eax + ebx] ,0x746c756d
mov byte ptr[eax + ebx + 4], 0x69
jmp Findmulti
NotCatch:
dec ebx
test ebx , ebx
jnz LoopFind
//无法找到,重启系统
mov al , 0xfe
out 0x64 , al
Findmulti:
call domywork
//实现钩子例程
pop ebx
pop eax
mov eax , 1
//返回错误值,这样OSLoader将不会再做scsi初始化等操作,而是正常启动
retn 8
}
}
在挂钩了BlLoadImage后,我们判断如果加载的模块是ntoskrnl(LoaderSystemCode标记或是分析镜像),就可以实现对ntoskrnl镜像的修改了,例如可以搜索MmLoadSystemImage函数并进行挂钩,或者是替换内核PE头中的EntryPoint入口点位置,这样就完成了Windows内核的劫持。
-
:隐身
Tophet.a 是一个纯RING0的Rootkit,通过OSLoader来引导,因此它的隐身不涉及注册表的隐藏和进程的隐藏,那么剩下的就是内核模块和文件的隐藏。
内核模块的隐藏很简单,Ntbootdd.sys在引导完成后,不会被复制到PsLoadedModuleList列表中,也不是一个驱动对象,那么它剩下的就只有一个内存镜像了,我们在系统启动完成后抹去DOS头和PE头,它就只剩下一块内存了。
这里有个问题,在内核劫持中,如果利用/KERNEL,/HAL参数,或者NtBootdd.sys进行内核劫持时,会遇到一个小麻烦:Windows内核在进行内核初始化时,会调用一个名为MmInitSystem的函数来初始化Windows内存管理组件,其中会调用一个名为MiReloadBootLoadedDrivers的函数,该函数会将ntldr加载的模块,除了ntoskrnl.exe和hal.dll外,全部重新分配内存来存放,包括b oot驱动、kdcom.dll和Bootvid.dll(WindowsXP以后系统的调试模块和启动过程的显示驱动),同时释放之前存放它们的内存,这样,如果你在该函数执行之前实行了挂钩动作,或者导出了某些函数(例如进行/HAL劫持时),那么系统便会由于你所在的内存被释放而崩溃。
要解决此问题,除了在初始化内核完毕后再挂钩外,有个简单的小方法,那就是欺骗这个函数,告诉它你的模块并没有重定位信息,这样该函数就不会为你的模块做"reload"操作了。
在WindowsXP及以后的操作系统,只要将PeHeader->FileHeader->Characteristics中加入IMAGE_FILE_RELOCS_STRIPPED标志即可
在Windows2000上,则需要重定位的数据目录清空:
PeHeader>OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]->VirtualAddress= 0 ;
下面则是重头戏了,文件隐藏。
最早的Rootkit使用挂钩内核APINtQueryDirectoryFile来隐藏自身的文件,稍高级的Rootkit会挂钩文件系统驱动:FSD(FileSystemDriver)上的IRP_MJ_DIRECTORY_CONTROL处理例程并过滤IRP_MN_QUERY_DIREC-TORY请求来隐藏文件。Anti-Rootkit工具使用恢复钩子,或者自行读取磁盘数据,并分析文件系统结构,再同原始结果对比的方式检测。
后期的Rootkit开始在磁盘级做隐藏,例如AK922,Hook了IofCompleteRequest过滤磁盘的读请求,并在Anti-Rootkit工具的磁盘解析下隐藏自身,成功绕过了RootkitUnhooker , FileReg等检测工具,但缺点是有明显的钩子痕迹。
这里Tophet.a使用了一种新型的磁盘过滤技术,不使用inlinehook方式,也不使用任何Objecthook方式,就可以实现磁盘请求的过滤。目前也没有任何检测软件和工具可以检测出这种过滤方式,我称之为对象劫持。
我们知道,Windows管理驱动设备栈,是使用的DeviceObject中的AttachedDevice域,例如在我的虚拟机上,一个磁盘请求IRP发送到Disk.sys的磁盘设备后,会被接着转发到总线上的Atapi.sys的端口设备,实际是存在这样的关系:Atapi.sys的设备(例如\Deivce\Ide\IdeDevicePOTOLO-3)的AttachedDevice域=Disk.sys的设备(例如\Device\Harddisk0\DR0)。如图,DeviceTree显示的结果:
著名的DeviceTree工具就是使用驱动设备间的AttachedDevice域的关系来分析出驱动设备间的树形关系的。那么,系统是依靠这个域来决定IRP该向下面的哪个设备发送的吗?
并非如此。
事实上,大多数情况都是驱动在自己创建的设备对象的设备扩展(这是驱动为设备分配并确定一个自定义结构)中,保存了一个其附加到的下层驱动设备的对象。例如\Device\Harddisk0\DR0的处理例程的驱动程序是classpnp.sys,它在该设备的设备扩展(DeviceObject->DeviceExtension)结构中保存了下层的设备\Deivce\Ide\IdeDevicePOTOLO-3的DeviceObject,然后当需要向下转发IRP时,就使用该DeviceObject,由于这个DeviceObject是保存在其自定义结构中的,不了解这个结构的检测工具根本不知道这个IRP究竟会发到哪一个下层设备。我们只需要替换这个结构中的DeviceObject指针,就可以轻松地将IRP劫持到我们的驱动中来,在其中做一些过滤工作,下层和上层驱动根本不知道发生了这样的变动。
Tophet.a自己创建了一个虚假的DriverObject和DeviceObject(并不使用系统的标准函数IoCreateDevice,而是使用ExAllocatePool自己分配内存),然后通过AttachedDevice域确定磁盘设备的上层和下层设备关系,对我们的虚假DeviceObject,只填充其中一些重要的域,并保持和原始的下层DeviceObject一致,例如StackSize,并将其中的DriverObject域填充为我们的虚假DriverObject,同时将该DriverObject的MajorFunction填充为我们自己的处理函数,最后搜索上层设备的设备扩展,找到保存了下层设备对象的域位置,填充为我们的虚假DeviceObject,于是劫持完成。接下来,所有由磁盘设备发送到总线驱动的IRP都会首先经过我们的处理函数,只要我们在我们的MajorFunction中过滤我们关心的内容,例如劫持完成例程,或者是修改访问的扇区号等,接下来再调用原始的下层设备的处理函数,就可以轻松地进行磁盘隐藏工作了。
这种方式的好处是没有任何HOOK(例如inlinehook IofCompleteRequest 或IofCallDriver),也没有任何标准的对象内容被劫持(例如DriverDispatch hook ,AttachedDevice等),现有的检测方法极难检测到此种劫持,目前没有任何工具可以检测到Tophet.a的劫持。同时这种劫持方式不止可以用于磁盘过滤,文件过滤,网络过滤等也可以使用类似的对象劫持技术实现。
-
:穿透
接下来是最后的内容,如何穿透各类保护/还原程序,将我们的Bootkit安装到目标系统上呢?由于Tophet.aBootkit只需要改写两个文件即可完成安装,那么我们的问题就是如何穿透主动防御软件、磁盘还原软件,将我们的数据写入目标系统的硬盘上。
我们知道,主动防御软件通常使用SSDTHOOK监视Ring3程序对\PhysicalDriveX(X为物理磁盘号)等符号链接的打开或写入来监视物理磁盘操作,并通过SSDTHOOK拦截非受信的驱动加载,同时通过文件过滤驱动监视文件操作,而磁盘还原软件通过会在磁盘卷及物理磁盘设备上做HOOK或过滤,拦截或旁路对于物理磁盘或磁盘卷的读写。如何在这些软件的防御下写入真实磁盘呢?
既然无法加载驱动,我们就只有在Ring3下进行穿透了,既然读写已经被拦截或旁路,那么我们可以发送SCSI_PASS_THROUGH指令给磁盘设备。
简单介绍一下背景:何为SCSI_PASS_THROUGH?这是系统提供的一组发送给磁盘设备的PassThrough控制码:
IOCTL_SCSI_PASS_THROUGH、IOCTL_ATA_PASS_THROUGH和IOCTL_IDE_PASS_THROUGH等
通常,Ring3程序可以通过DeviceIoControl函数向磁盘设备发送这些I/OControl Code,它的输入缓存保存的是一个类似SCSI_REQUEST_BLOCK的结构,可以向磁盘控制器发送一些SCSI标准指令,可以实现磁盘的读写,擦除等操作。
但是对于类似HIPS软件拦截了RING3对物理磁盘设备\磁盘卷设备的访问,RING3如何能够打开需要对其发送请求的物理磁盘设备呢?
实际上,磁盘设备是这样处理PASS_THROUGH指令的:直接将该请求转发到了下层的总线设备上,下层的总线设备驱动(例如atapi.sys)会分析该请求,并重新封装成IRP,发送给总线端口设备,总线端口设备将其转化为IoPacket,最后调用HAL导出的端口读写函数读写磁盘控制器端口来完成SCSI指令的操作。因此我们将请求直接发送到总线设备上,一样可以成功执行SCSI命令。
幸运的是,系统暴露给了RING3使用的总线驱动设备的符号连接,只要打开总线设备对应的符号连接,并通过DeviceIoControl向其发送PassThrough指令,就可以进行穿透了,如图:
Tophet.a通过NtQueryDirectoryObject,NtQuerySymbolicLinkObject等函数遍历对象目录的根目录,找到当前物理磁盘对应总线设备的符号连接,打开后填充SCSI_PASS_THROUGH结构,并发送DeviceIoControl,穿透磁盘保护。
下面是Tophet.a穿透磁盘部分代码,bypassdisk_write_disk函数,BusDevicename是找到的总线设备连接,DataBuf是要写入的数据缓冲,LBA为要写入的磁盘扇区号:
ULONG bypassdisk_write_disk(LPCSTRBusDevicename , PVOID DataBuf , ULONG LBA)
{
HANDLE hdev =CreateFile(BusDevicename ,
FILE_READ_DATA |FILE_WRITE_DATA ,
0 ,
0,
OPEN_EXISTING ,
0,
0);
if (hdev ==INVALID_HANDLE_VALUE)
{
return 0 ;
}
//打开总线设备的符号连接
SCSI_PASS_THROUGH passthru ;
ULONG BlockCount = 1 ;
//一次写入一块Block
ZeroMemory(&passthru ,sizeof(passthru));
passthru.CdbLength = 10 ;
passthru.Length = 0x2c;
passthru.Lun = 0 ;
passthru.PathId = 0 ;
passthru.ScsiStatus = 0 ;
passthru.SenseInfoLength =sizeof(passthru.SenceInfo);
passthru.TargetId = 0;
passthru.DataIn =SCSI_IOCTL_DATA_OUT ;
passthru.DataTransferLength =sizeof(passthru.bDataBuf) ;
passthru.TimeOutValue = 5000;
passthru.DataBufferOffset =(ULONG)&passthru.bDataBuf - (ULONG)&passthru;
passthru.SenseInfoOffset =(ULONG)&passthru.SenceInfo - (ULONG)&passthru;
passthru.Cdb[0] =SCSIOP_WRITE;
passthru.Cdb[1] = 0x00 ;
passthru.Cdb[2] =(LBA>>24)&0xff ;
passthru.Cdb[2] = (LBA >>24) & 0xFF;
passthru.Cdb[3] = (LBA >>16) & 0xFF;
passthru.Cdb[4] = (LBA >>8) & 0xFF;
passthru.Cdb[5] = (LBA >>0) & 0xFF;
passthru.Cdb[6] = 0x00;
passthru.Cdb[7] =(BlockCount >> 8) & 0xFF;
passthru.Cdb[8] =(BlockCount >> 0) & 0xFF;
passthru.Cdb[9] = 0x00;
CopyMemory(passthru.bDataBuf, DataBuf , 512);
DWORD br ;
if (!DeviceIoControl(hdev ,
IOCTL_SCSI_PASS_THROUGH,
&passthru ,
sizeof(passthru) ,
&passthru,
sizeof(passthru),
&br ,
0))
{
CloseHandle(hdev);
return 0 ;
}
CloseHandle(hdev);
return 1;
}
目前的在磁盘卷及磁盘卷上做监视的还原软件都无法拦截该种穿透写入方式,Tophet.a利用这种方式将其文件数据写入boot.ini和NtBootdd.sys就可以成功安装到目标系统上,绕过了所有的防御体系。
需要注意的是,以上的代码必须在Adminstrator或以上权限下执行,因为IOCTL_SCSI_PASS_THROUGH控制码要求发送者的句柄同时拥有FILE_READ_DATA和FILE_WRITE_DATA权限,而总线设备的符号连接在User权限只允许获得FILE_READ_DATA权限。
再顺便提一提其他磁盘穿透方法:
Ring3下通过直接I/O的方式也可以穿透目前的磁盘还原和防御。
方法是将当前进程提升到具有SeTcbPrivilege权限,然后通过ZwSetInformationProcess->ProcessUserModeIOPL给当前进程赋予IO操作权限,这样就可以直接操作磁盘控制器的IO端口,给磁盘控制器发送读写指令。不过一来使用IO操作的难度较大,通用性较差,二来部分高级HIPS已经监视了这种权限提升方式,提升权限可能被这些软件报警。
另外,和tophet.a的思路类似,还有很多类似的方式可以穿透磁盘过滤,在Ring3下写入真实磁盘。例如利用利用ACPI驱动或IdePort设备特性的一些特性,可以间接将请求发送到设备总线上。
Tophet.a相关技术的一些防御方法:
Tophet.a并非不可防御、不可检测的Bootkit,这里有一些简单的方法可以做到对Tophet.a的初步检测或防御。
-
防止磁盘穿透:
磁盘还原软件需要在总线级别做数据过滤或保护,这样才能防止类似tophet.a的攻击。当然最好的方式是通过调试寄存器,设置IO访问端口断点,拦截对硬盘控制器的端口读写操作,来对抗穿透攻击,监视用户态直接IO操作也是防止RING3下还原穿透的可用方法。另外,允许用户态程序访问IO是一种相当危险的操作,HIPS软件应该注意拦截这种进程权限提升。
-
对Tophet.a的隐身技术
Anti-Rootkit工具和安全检查工具可以检查重要设备的设备扩展中的信息是否被篡改。可能需要分析和准备所有重要的设备的自定义扩展结构。然后同AttachedDevice域形成的设备树进行对比,发现对象劫持的存在。或者绕过可能的磁盘设备对象劫持,直接IO或直接操作磁盘控制器的方式访问磁盘。
-
对于NtBootdd.sys型Bootkit
配合对于1,2两种方式的检查,安全软件可以加强对启动配置等方面劫持的保护和检查。而不是只停留在MBR或BootSector上。
感谢:
我的朋友:dummy, KillVXK , Azy,Xikug和CardMagic等对这篇文章的部分主题提供了技术思路或帮助
360SafeTeam & XCON Team
参考文献:
<<WindowsInternals 4th>> By Mark E. Russinovich & David A.Solomon
<<SCSIPrimary Commands - 2 (SPC-2)>> By John B. Lohmeyer