恶意代码分析-Bootkit初探

碎碎念

第一个Bootkit恶意软件为1971年的1971年的Creeper,在VAX PDP-10上的TENEX网络操作系统下运行的恶意软件,第一个杀毒软件Reaper就是专门删除Creeper而设计的。引导扇区感染者BSI是最早的Bootkit恶意软件之一,第一次被发现是在MS-DOS时代,它们感染磁盘引导扇区(第一个物理扇区)。

第一个感染Apple II的BSI病毒为Rich Skrenta的Elk Cloner病毒(1982~1983),比PC引导扇区病毒早了几年。下一个影响Apple II的BSI恶意软件是1989年的Load Runner,监听Control-Command-Reset触发的Apple reset命令来将其写入磁盘并实现恶意软件持久化。1986年出现第一个PC病毒Brain。在Windows操作系统引入和普及后BSI时代终结。

绕过微软数字签名检查的所有已知技巧有4种。第一种在用户模式下运行,依靠内置的微软Windows方法来合法地禁用签名策略,操作系统提供一个接口以通过使用自定义证书来验证驱动程序的数字签名,从而暂时禁用驱动程序映像身份验证或启用测试签名。第二种利用系统内核漏洞或合法的第三方驱动程序的有效数字签名。第三种修改操作系统内核来禁用内核模式代码签名策略,这方法最常见。第四组损害系统固件,以在操作系统内核执行之前在目标系统上执行以禁用安全检查。

Secure Boot保护功能是一种确保启动过程中涉及的组件完整性和安全标准,使得恶意代码不再针对引导程序,而是以系统固件为目标。

首个新一代Bootkit恶意软件是eEye的PoC BootRoot,于2005年于拉斯维加斯举办的Black Hat会议上提出。有Derek Soeder和Ryan Permeh编写的BootRoot是网络驱动程序接口规范NDSI后门,首次证明原始Bootkit可用作攻击现代操作系统的模型。2007年发现Mebroot威胁,同年Black Hat会议上公布了vBootkit和Stoned两个PoC,表明通过修改引导扇区来攻击Windows Vista内核时可能的。

一些PoC于真实Bootkit攻击的演变过程如下:

PoC BootRoot 对应的真实攻击
eEye BootRoot 2005:第一个适用于Windows的基于MBR的Bootkit Mebroot 2007
Vbootkit 2007:滥用Windows Vista的第一个Bootkit Mebratix 2008
Vbootkit x64 2009:第一个绕过Windows 7数字签名检查的Bootkit Mebroot v2 2009
Stoned 2009 Olmarik(TDL4 ) 2010~2011
Stoned x64 2011 Olmasco(TDL4 modfication) 2011:首款基于VBR感染的Bootkit
Evil Core 2011:一种使用对称多处理结构SMP启动到保护模式的Bootkit思路。 Rovnix 2011:一种基于VBR感染进化的多态代码
DeepBoot 2011:第一个将实模式切换为保护模式的Bootkit Mebromi 2011:对BIOS工具包的首次探索
VGS 2012:第一个基于VGA概念的Bootkit Gapz 2012:VBR感染进化
DreamBoot 2013:公开的第一个UEFI Bootkit概念 OldBoot:针对Android的第一个Bootkit

Windows引导过程

现代引导过程一般流程如下,任何部分都可被Bootkit攻击,常见攻击目标有BIOS初始化、MBR和操作系统引导加载程序Bootloader。

模式 次序
CPU实模式 初始化BIOS 硬件
MBR BIOS服务 硬件
Bootloader BIOS服务 硬件
早期内核初始化 BIOS服务 硬件
CPU保护模式 初始化完整内核 硬件
首个用户模式 内核服务 硬件

Windows Vista及更高版本的启动过程和所涉及组件:

1
BIOS启动代码->MBR->卷引导记录VBR和初始程序加载程序->bootmgr->winload.exe->内核映像和引导启动驱动程序

BIOS执行基本系统初始化和开机自检工作,确保关键系统硬件能正常工作。BIOS还提供了一个专门环境,其中包括与系统设备通信所需的基本服务。这种简化的I/O接口在预引导环境中可用,随后被操作系统的抽象用法所取代。Bootkit暴露许多用于执行磁盘I/O操作的入口点,通过INT 13h的特殊处理程序访问磁盘服务,以修改系统启动期间从硬盘驱动器读取的操作系统和引导组件来禁用或规避操作系统保护。接下来BIOS查找可引导的磁盘驱动器,该驱动器承载要加载的操作系统,可能是硬盘驱动器、USB驱动器或CD驱动器,一旦确定后BIOS引导代码将加载MBR。

MBR结构包含硬盘驱动器分区和引导代码的信息,确定可引导硬盘驱动器的活动分区,包含要加载的操作系统实力。确定后MBR读取并执行其引导代码:

1
2
3
4
5
typedef struct _MASTER_BOOT_RECORD {
BYTE bootCode[0x1BE]; //实际启动代码
MBR_PARTITION_TABLE_ENTRY partitionTable[4];
USHORT mbrSignature; //0xAA55 表示PC MBR格式
} MASTER_BOOT_RECORD, *PMASTER_BOOT_RECORD;

接下来MBR引导代码bootCode解析分区表partitionTable以查找活动分区,读取第一个扇区中卷引导记录VBR,并将控制权转移给他。

分区表结构如下。任何情况只有一个分区被标记为活动状态,type标记分区类型如(EXTENDED MBR、FAT12、FAT16、FAT32、安装过程时可安装文件系统IFS、逻辑磁盘管理器LDM、NTFS、未使用0)。

1
2
3
4
5
6
7
8
typedef struct _MBR_PARTITION_TABLE_ENTRY {
BYTE status; //活动状态 no=0 yes=128
BYTE chsFirst[3]; //起始扇区号
BYTE type; //操作系统类型指示代码
BYTE chsLast[3]; //结束扇区号
DWORD lbaStart; //相对于硬盘起点的第一个扇区
DWORD size; //分区中扇区数
} MBR_PARTITION_TABLE_ENTRY, *PMBR_PARTITION_TABLE_ENTRY;

WIndows典型启动硬盘驱动器布局如下。其中bootmgr模块确定要加载哪个特定操作系统实例。若安装了多个系统,bootmgr显示一个对话框提示用户选择一个,还提供参数如安全模式、最后一次正确配置、禁用驱动签名等。

序号 含义
1 MBR代码
2 分区表条目1(无效)---指向Bootmgr分区前
3 分区表条目2(操作系统)---指向操作系统分区
4 分区表条目3(空闲)
5 MBR数据
6 Bootmgr分区(bootmgr模块、其他引导组件)
7 操作系统分区(托管操作系统、用户数据卷)

硬盘驱动器可能包含承载不同操作系统的多个实例和几个分区,但通常只有一个分区被标记为活动的。MBR读取并执行分区的第一个扇区VBR。VBR包含分区布局信息,指定正在使用的文件系统类型及参数,以及从活动分区读取初始程序装入器IPL模块的代码。IPL模块实现文件系统解析功能,以便从分区的文件系统中读取文件,并报告硬盘驱动器开始的偏移量。VBR布局如下结构如下,BPB结构布局对应卷文件系统,另外两个结构对应NTFS卷。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
typedef struct _BIOS_PARAMTER_BLOCK_NTFS { //BPB结构
WORD SectorSize;
BYTE SectorsPerCluster;
WORD ReservedSectors;
BYTE Reserved[5];
BYTE MediaId;
BYTE Reserved2[2];
WORD SectosPerTrack;
WORD NumberOfHeads;
DWORD HiddenSectors;
BYTE Reserved3[8];
QWORD NumberOfSectors;
QWORD MFTStartingCluster;
QWORD MFTMirrorStartingCluster;
BYTE ClusterPerFileRecord;
BYTE Reserved4[3];
BYTE ClusterPerIndexBuffer;
BYTE Reserved5[3];
QWORD NTFSSerial;
BYTE Reserved6[4];
} BIOS_PARAMETER_BLOCK_NTFS, *PBIOS_PARAMETER_BLOCK_NTFS;
typedef struct _BOOTSTRAP_CODE{
BYTE bootCode[420]; //引导扇区机器代码 从分区中读取和执行IPL 指定位置时HiddeSectors字段
//代码末尾有一串出现错误时显示给用户的文本字符串
WORD bootSectorSignature; //0x55AA VBR签名
} BOOTSTRAP_CODE, *PBOOTSTRAP_CODE;
typedef struct _VOLUME_BOOT_RECORD {
WORD jmp; //将系统传输控制转换到VBR代码bootCode
BYTE nop;
DWORD OEM_Name;
DWORD OEM_ID: //NTFS
BIOS_PARAMTER_BLOCK_NTFS BPB;
BOOTSTRAP_CODE BootStrap;
} VOLUME_BOOT_RECORD, *PVOLUME_BOOT_RECORD;

IPL从文件系统读取并加载操作系统引导管理器bootmgr模块,后者读取引导配置数据BCD其中包含一些影响安全策略的参数,如内核模式代码签名策略等。Bootkit常试图绕过bootmgr代码完整性验证实现。在Windows 10中bootmgr引导菜单的界面标题为“Startup Settings”。bootmgr管理启动过程直至用户选择启动选项为止,此后winload.exe或winresume.exe(系统从休眠状态启动)将加载内核,启动驱动程序以及一些系统注册表数据。

实模式最大地址只有ffff:ffff,bootmgr在winload.exe和bootmgr接管后将处理器切换到保护模式,在x64下称为长模式。bootmgr由16位实模式代码和一个压缩的PE镜像组成。16位代码提取和解压缩该PE,将处理器切换到保护模式并将控制权传递给该模块。Bootkit必需正确处理处理器执行模式的切换,以保持对引导代码执行的控制。在切换后,整个内存布局被改变,且一线位于一个连续的内存地址集的代码的一部分可以移动到不同的内存段。

上述未压缩的映像从BCD加载引导配置信息,当BCD存储在硬盘驱动器上时,其布局与注册表配置单元相同,可读取HKEY_LOCAL_MACHINE\BCD 000000。bootmgr会将处理器执行的运行环境保存在临时变量中,临时切换到实模式,执行INT 13h磁盘服务,从硬盘驱动器读取数据,然后返回保护模式,还原保存的运行环境。

BCD存储区包含bootmgr加载操作系统所需的所有信息,常用参数有:

变量名 参数类型 参数ID
BcdLibraryBoolean_Disable-IntegrityCheck(Windows 7以上废弃) Boolean 0x16000048
BcdOSLoaderBoolean_WinPEMode Boolean 0x26000022
BcdLibraryBoolean_Allow-PrereleaseSignatures Boolean 0x1600004

变量BcdOSLoaderBoolean_WInPEMode指示系统在Windows预安装环境模式下启动,该模式本质是具有有限服务的最小Win32操作系统,主要用于为Windows安装准备计算机。此模式还禁止内核完整性检查,包括x64下强制指定的内核模式代码签名策略。

变量BcdLibraryBoolean_Allow-PrereleaseSignatures使用测试代码签名证书来加载内核模式驱动程序以进行测试,这些证书可通过Windows驱动程序工具包中包含的工具生成。Necurs Rootkit用此过程将恶意内恶化模式驱动程序安装到系统上,并使用自定义证书签名。

获取引导选项后,bootmgr执行自我完整性验证,检查失败则停止引导并显示错误消息。若上述两个变量任意一个为TRUE则bootmgr不执行自我完整性检查。此后bootmgr选择winload.exe或winresume.exe。winload.exe接收到对操作系统引导的控制时,它启用保护模式下的分页,并加载操作系统内核镜像机器依赖项,包含bootvid.dll(计算机图形图像支持)、ci.dll(代码完整性)、clfs.dll(哦那个用日志文件系统驱动)、hal.dll(硬件抽象层)、kdcom.dll(内核调试器协议通信库)、pshed.dll(特定平台硬件错误驱动)、存储设备驱动、ELAM(早期启动反恶意软件)。系统注册表配置单元启动驱动等。

引导过程安全性

ELAM模块是一种用于Windows系统的检测机制,允许第三方安全软件注册一个内核模式驱动,该驱动保证在启动过程早期执行,在其他第三方驱动加载前。ELAM驱动注册回调例程,内核使用这些例程来评估系统注册表配置单元和引导驱动中数据。如用CmRegisterCallbackExCmUnRegisterCallback注册和注销监视注册表数据回调,用IoRegisterBootDriverCallbackIoUnRegisterBootDriverCallback来注册和注销启动驱动的回调,这些回调函数标准如下。对于Argument1参数为BdCbStatusUpdate向ELAM驱动提供有关驱动程序依赖项或引导驱动程序的加载的状态更新,为BdCbInitializeImage表示ELAM驱动用于对引导驱动及其依赖项进行分类。

1
2
3
4
5
NTSTATUS EX_CALL_FUNCTION(
_In_ PVOID CallbackContext, //从ELAM驱动接收上下文
_In_ PVOID Argument1, //回调类型
_In_ PVOID Argument2 //系统提供的上下文结构
);

ELAM驱动基于映像的名称、注册为引导驱动的注册表位置、文件的证书发布者和所有者、散列值和对应算法名、证书指纹和指纹算法名,上述Argument2参数表示操作系统对引导驱动的分类信息,可以是非恶意、未知和恶意。Windows根据ELAM策略值HKLM\System\CurrentControlSet\Control\EarlyLaunch\DriverLoadPolicy决定是否加载已知的恶意或未知驱动。

策略名 策略值 描述
PNP_INITIALIZE_DRIVERS_DEFAULT 0x00 仅加载已知为非恶意的驱动
PNP_INITIALIZE_UNKNOWN_DRIVERS 0x01 仅加载已知为非恶意、未知的驱动
PNP_INITIALIZE_BAD_CRITICAL_DRIVERS 0x03 默认,加载已知为非恶意、未知的驱动和已知为恶意的非关键驱动
PNP_INITIALIZE_BAD_DRIVERS 0x07 加载所有驱动

ELAM只能防御Rootkit而不能防御Bootkit。

内核模式驱动完整性检查。签名策略从Windows Vista引入,在64为系统上无论类型如何,所有内核模式模块都要进行完整性检查。驱动必需有一个嵌入式软件发布证书SPC数字签名,或一个SPC签名的目录文件。开机启动驱动只能用嵌入式签名,因为启动时存储设备驱动没初始化,目录文件不可访问。

PE文件的嵌入式驱动签名在PE头数据目录的IMAGE_DIRECTORY_DATA_SECURITY条目中指定,枚举和验证证书的API:

1
2
3
4
5
6
7
8
9
10
11
12
13
BOOL ImageEnumerateCertificates(
_In_ HANDLE FileHandle,
_In_ WORD TypeFilter,
_Out_ PDWORD CertificateCount,
_In_opt_ PDWORD Indices,
_In_opt_ DWORD IndexCount
);
BOOL ImageGetCertificateData(
_In_ HANDLE FileHandle,
_In_ DWORD CertificateIndex,
_Out_ LPWIN_CERTIFICATE Certificate,
_Inout_ PDWORD RequiredLength
);

除了内核模式代码签名策略,还有即插即用设备安装签名策略,目的是验证发布者身份和PnP设备驱动安装包完整性,要求驱动程序包目录文件由Windows硬件质量实验室WHQL认证或由第三方SPC签署。。若驱动不符合PnP策略要求,会出现一个警告对话框给出提示,并由用户决定是否在自己的系统上安装该驱动程序包。该策略只映像驱动安装,而不影响驱动加载,加载仍受到内核模式代码签名策略检查。该PnP策略可被禁止,允许没有适当签名的PnP驱动安装。

内核模式代码签名策略中负责执行代码完整性的逻辑在Windows内核映像和内核模式库ci.dll中共享,内核映像通过该库来验证加载到内核地址空间的所有模块的完整性。在Windows Vista和Windows 7中,内核映像中单个变量BOOL nt!g_CiEnabled确定是否强制执行完整性检查,它由启动时内核映像例程NTSTATUS SepInitializeCodeIntegrity()初始化。操作系统检查是否将其引导到Windows预安装模式(WinPE),如果已引导则将该单个变量初始化为FALSE,来禁用完整性检查。攻击者可轻松将该变量设为FALSE来规避完整性检查,这在2011年被Uroburos恶意软件家族(另称Snake和Turla)使用,原理是引入并利用第三方VirtualBox驱动VBoxDrv.sys的漏洞。

若Windows未处于WinPE模式,接下来检查引导选项DISABLE_INTEGRITY_CHECKS和TESTSIGNING的值。前者可由用户在引导菜单或bcdedit.exe中设为TRUE,启用Secure Boot后该值被忽略。后者为TRUE时,不需要证书验证就可一直链接到受信任的根证书颁发机构CA,即任何具有数字签名的驱动都可加载到内核空间中。

负责执行代码完整性策略的内核模式库ci.dll有常用例程:

例程 含义
CiCheckSignedFile 验证摘要和数字签名
CiFindPageHashedInCatelog 验证经过验证的系统目录是否包含PE映像的第一个内存页摘要
CiFindPageHashesInSignedFile 验证摘要并验证PE映像第一个内存页的数字签名
CiFreePolicyInfo 释放由CiVerifyHashInctalogCiCheckSignedFileCiFindPageHashesInCatalogCiFindPageHashesInSignedFile分配的内存
CiGetPEInformation 在调用方和ci.dll之间创建加密的通信通道
CiInitialize 初始化ci.dll的功能来验证PE映像文件的完整性
CiVerifyHashInCatalog 验证那个包含在经过验证的系统目录中PE映像的摘要

其中最重要的CiInitialize例程原型如下,它还进行自我检查,确保自身没被篡改,此后例程继续验证引导驱动列表中所有驱动的完整性。

1
2
3
4
5
NTSTATUS CiInitialize(
_In_ ULONG CiOptions; //完整性选项
PVOID Parameters,
_Out_ PVOID g_CiCallbacks //回调数组
);

一旦ci.dll初始化完成,内核用回调数组缓冲区中的回调来验证模块的完整性。Windows Vista和Windows 7中,SeValidateImageHeader例程决定映像是否通过完整性检查,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NTSTATUS SeValidateImageHeader(Parameters) {
NTSTATUS Status=STATUS_SUCCESS;
VOID Buffer=NULL;
if(g_CiEnable==TRUE){
if(g_CiCallbacks[0]!=NULL)
Status=g_CiCallbacks[0](Parameters);
else
Status=0xC0000428;
}
else{
Buffer=ExAllocatePoolWithTag(PagedPool,1,'hPeS');
*Parameters=Buffer;
if(Buffer==NULL)
Status=STATUS_NO_MEMORY;
};
return Status;
};

Windows 8时弃用内核变量nt!g_CiEnabled,并更改了g_CiCallbacks缓冲区布局:

1
2
3
4
5
6
7
8
9
10
typedef struct _CI_CALLBACKS_WIN8 {
ULONG ulSize;
PVOID CiSetFileCache;
PVOID CiGetFileCache;
PVOID CiQueryInformation;
PVOID CiValidateImageHeader;
PVOID CiValidateImageData;
PVOID CiHashMemory;
PVOID KappxIsPackageFile;
};

Windows 8引入Secure Boot技术,利用同意可扩展固件接口UEFI来阻止任何没有有效数字签名的启动应用程序或驱动程序的加载和执行。启用后BIOS验证启动时执行的所有UEFI和系统引导文件的完整性,以确保他们时合法来源并具有有效的数字签名。系统首次启动时,Secure Boot确保预启动环境和引导加载程序组件不被破坏,引导加载程序反过来验证内核和引导启动驱动的完整性。内核通过完整性验证后,Secure Boot将验证其他驱动和模块。

攻击者可能攻击完整性机制本身,因此WIndows 10引入虚拟安全模式VSM和设备保护Device Guard,两者都基于硬件辅助的内存隔离。这种技术被称为二级地址转换。包括在Intel(扩展页表EPT)和AMD(快速虚拟化索引RVI)CPU中。

Hyper-V使用二级地址转换SLAT对虚拟机执行内存管理,并减少将guest用户的物理地址(由虚拟化技术隔离的内存)转换为实际物理地址的开销。SLAT为虚拟机监控程序提供虚拟地址与物理地址转换的中间缓存,这减少了虚拟机管理程序为主机物理地址的转换请求提供服务所需的时间。

VSM从Windows 10出现,建立在Hyper-V之上,将在隔离的虚拟机管理程序保护的容器中执行操作系统和关键系统模块。即使内核遭到破坏,其他虚拟环境中执行的关键组件仍然安全,攻击者无法从一个收到破坏的虚拟空间转移到另一个虚拟空间。潜在的易受攻击的驱动和代码完整性库位于单独的虚拟容器中,攻击者无法关闭代码完整性保护。换句话说,VSM将代码完整性关键系统组件HVCI与系统内核地址空间隔离开。

Device Guard将VSM代码完整性保护与平台和UEFI Secure Boot进行了结合,防止不受信任的代码在系统上运行。Device Guard从启动过程开始,到加载系统内核驱动和用户应用程序,都强制执行代码完整性策略。

1
2
3
4
5
6
7
                                    VSM:
安全内核<->HVCI
| Device Guard:
Secure Boot: | HVGI:
BIOS->UEFI->bootmgr(winload.exe)->系统内核->ELAM->内核驱动
| | |
防Bootkit加载 防Bootkit破坏系统模块 防Bootkit将代码注入内核地址空间

在Device Guard机制下,驱动程序开发时需要注意一些事项:从非执行NX非分页池中分配所有非分页内存,驱动的PE模块不能同时具有可写和可执行的部分;不能尝试直接修改可执行系统内存;不能再内核模式下使用动态的或自修改的代码;不要加载任何可执行数据。

Bootkit感染技术

Bootkit感染技术分为MBR感染技术和VBR/初始程序加载器IPL感染技术,MBR感染例子有TDL4 Bootkit,VBR感染例子有Rovnix和Gapz Bootkit。MBR感染技术直接修改MBR代码或MBR数据。MBR代码修改指的是以某种方式保存MBR原始内容,用恶意代码覆盖系统MBR代码。MBR数据修改方法涉及更改MBR分区表,但分区表内容因系统而异。

MBR代码修改的例子是TDL4,它重用了TDL3的高级逃避和反司法鉴定技术,增加绕过内核代码签名策略的能力,并感染x64的Windows系统。

TDL4在磁盘末端创建了一个隐藏的存储区域,将原始MBR和自己的一些模块写入其中,以便在概然发生后可加载,系统看起来会正常启动。Bootkit在引导时使用MBR、LDR16、LDR32和LDR64模块来绕过Windows完整性检查,并加载未签名的恶意驱动。

模块名 描述
mbr 受感染的硬盘驱动引导扇区的原始内容
ldr16 16位实模式加载的代码
ldr32 x86系统的虚假kdcom.dll
ldr64 x64系统的虚假kdcom.dll
drv32 x86的Bootklit驱动
drv64 x64的Bootkit驱动
cmd.dll 注入x86进程的有效负载
cmd64.dll 注入x64进程的有效负载
cfg.ini 配置信息
bckfg.tmp 加密的C&C服务器链接

TDL4用DeviceIoControl直接将I/O控制代码IOCTL_SCSI_PASS_THROUGH_DIRECT直接发送到磁盘微型端口驱动,第一个参数传递符号链接打开的句柄\??\PhysicalDriveXX,其中XX为被感染的硬盘的数字。用写访问打开这个句柄需要管理特权,TDL4用MS10-092漏洞来提升特权。安装所有组件后,TDL4用NtRaiseHardError强制系统重启:

1
2
3
4
5
6
7
8
NTSYSAPI NTSTATUS NTAPI NtRaiseHardError(
_In_ NTSTATUS ErrorStatus,
_In_ ULONG NumberOfParameters,
_In_opt_ PUNICODE_STRING UnicodeStringParameterMask,
_In_ PVOID* Parameters,
_In_ HARDERROR_RESPONSE_OPTION ResponseOption, //传参OptionShutdownSystem 使系统变为BSoD状态并自动重启
_Out_ PHARDERROR_RESPONSE Response
);

TDL4 Bootkit引导过程如下:

过程 从上一步的实现途径
加载受感染的MBR
从隐藏文件系统中加载ldr16 加载并执行受感染的MBR
挂载BIOS的INT 13h处理程序并还原原始MBR ldr16被加载和执行
加载VBR 加载并执行原始MBR代码
加载bootmgr VBR被加载和执行
读取BCD bootmgr已加载并接受控制
加载winload.exe 用WInPE替换EmsEnabled选项
加载ntoskrnl.exe、hal.dll、kdcom.dll、bootid.dll 篡改/MININT选项
从kdcom.dll调用kdDebuggerInitialize1 使用ldr32或ldr64欺骗kdcom.dll
继续进行内核初始化 加载drv32或drv64

在BSoD和随后的系统重启后,BIOS将受感染的MBR读取到内存中并执行它。接下来受感染的MBR将Bootkit文件系统放在可启动硬盘驱动器的末尾,加载和执行ldr16模块。ldr16模块包含的代码负责挂载BIOS的13h终端处理程序,重载原始MBR并将执行结果传递给它,这样开机过程照常继续,但13h中断处理程序是被挂载的。一旦控制权转到原MBR,引导过程照常进行,加载VBR和bootmgr,但主流内存的Bootkit控制进出硬盘的所有I/O操作。

上述引导过程从硬盘读取数据的代码依赖BIOS的13h磁盘服务,这意味着Bootkit可围在引导过程中从硬盘读取的任何数据。此时Bootlit利用此功能将kdcom.dll和隐藏文件中ldr32或ldr64(取决于操作系统)进行替换,并在读取操作期间将内容替换到内存缓冲区。

模块ldr32或ldr64导出与原kdcom.dll库相同的符号。除kdcom!KdDebuggerInitialize1以外,其他恶意版本导出的都只返回0。该函数在内核初始化期间由Windows内核调用,用于在系统上加载Bootkit驱动代码。每当创建或销毁线程时,将用PsSetCreateThreadNotifyRoutine来注册回调函数CreateThreadNotifyRoutine。触发回调时,会创建恶意DRIVER_OBJECT来挂在系统事件,直到引导中为硬盘设备建立驱动栈为止。

为了替换kdcom.dll,恶意软件需要禁用内核代码完整性检查,若不禁用,winload.exe将报告一个错误并拒绝据徐引导过程。Bootkit告知winload.exe以预安装模式加载内核来关闭这些检查。winload.exe将BcdLibraryBoolean_EmsEnabled元素(BCD编码为16000020)替换为BcdOSLoaderBoolean_WinPEMode(BCD编码为26000022),其中BcdLibraryBoolean_EmsEnabled是可继承对象,指示是否启用全局应急管理服务重定向,默认TRUE。

一旦kdcom.dll被加载,恶意软件就禁用WinPE模式,即在从硬盘驱动器读取映像时损坏winload.exe映像中的/MININT字符串选项。winload.exe映像用/MININT选项通知内核已启用WinPE模式,但由此一来内核接收到无效的/MININT选项并继续初始化。这是Bootkit感染的引导过程的最后一步,绕过代码完整性检查后,恶意内核驱动成功加载到了系统中。

为避免TDL4引导程序包中恶意MBR代码被使用静态签名的静态分析检测到,恶意代码被使用ror循环右移加密。

TDL4的一个变种Olmasco修改分区表而不是MBR代码。Olmasco在可启动硬盘驱动器末尾创建一个未分配的分区,再通过此u该MBR分区表中空闲分区表项#2在同一位置创建一个隐藏分区。MBR包含一个分区表,该表条目以偏移量0x1BE开始,在此之前为MBR代码。分区表由4个16字节的条目组成,每个条目描述硬盘上相应分区,为MBR_PARTITION_TABLE_ENTRY结构。硬盘驱动器最多只能有4个主分区,只有一个分区标记为活动分区,操作系统从活动分区启动。Olmasco用自己的恶意分区的参数覆盖分区表中的空条目,将分区标记为活动状态,并初始化新创建的分区的VBR。

VBR/IPL感染技术分为IPL修改和BIOS参数块BPB修改,前者例如Rovnix Bootkti,后者例如Gapz Bootkit。Rovnix修改可引导硬盘的活动分区上IPL和NTFS引导程序代码。Rovnix读取VBR之后的15个扇区(即原IPL),对其进行压缩。将原IPL起始位置替换为恶意代码,再将压缩后的原IPL附着在恶意代码之后。下次系统启动时,恶意引导代码将获得控制权。恶意引导代码挂载INT 13h,以便对bootmgr、winload.exe和内核进行修补,以便一旦加载引导程序组件,便可获得控制权。最后Rovnix解压缩原始IPL代码并将控制权返回给它。它还控制调试寄存器DR0~DR7,以再没有实际补丁的情况下对系统代码挂在,绕过内核代码完整性检查,保持被挂载代码的完整性。

1
2
3
4
5
Rovnix
感染前: NTFS引导代码15扇区:
MBR|VBR|原IPL |文件系统数据 |
感染后:
MBR|VBR|恶意代码|压缩后原IPL|文件系统数据|隐藏分区|未签名恶意驱动|

Gapz用存储在硬盘上的恶意代码扇区中偏移量值覆盖活动分区VBR的HiddenSectors字段,VBR和IPL中所有其他数据和代码不变。当VBR再次运行时,将加载并执行Bootkit而不是合法IPL。Gapz Bootkit映像再第一个分区前或硬盘驱动器最后一个分区之后。

用IDA分析Bootkit MBR

IDA加载MBR二进制文件后,Loading offset字段设为0x7c00,这是BIOS引导代码加载MBR的固定地址,然后选择16位实模式代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
seg000:7C00 31 C0                                       xor     ax, ax
seg000:7C02 8E D0 mov ss, ax ;初始化存根
seg000:7C04 BC 00 7C mov sp, 7C00h ; <suspicious>
seg000:7C07 8E C0 mov es, ax
seg000:7C09 8E D8 mov ds, ax
seg000:7C0B FB sti
seg000:7C0C 60 pusha
seg000:7C0D B9 CF 00 mov cx, 0CFh
seg000:7C10 BD 19 7C mov bp, 7C19h ; <suspicious>
seg000:7C13
seg000:7C13 loc_7C13: ; CODE XREF: seg000:7C17↓j
seg000:7C13 D2 4E 00 ror byte ptr [bp+0], cl ;解密
seg000:7C16 45 inc bp
seg000:7C17 E2 FA loop loc_7C13
seg000:7C17 ; ---------------------------------------------------------------------------
seg000:7C19 44 db 44h ; 加密后代码
seg000:7C1A 85 db 85h
seg000:7C1B 1D db 1Dh

编写解密脚本:

1
2
3
4
5
6
7
8
import idaapi
start_ea = 0x7C19
for ix in range(0xCF):
print(ix)
byte_to_decr = idaapi.get_byte(start_ea + ix)
to_rotate = (0xCF - ix) % 8
byte_decr = (byte_to_decr >> to_rotate) | (byte_to_decr << (8 - to_rotate))
idaapi.patch_byte(start_ea + ix, byte_decr)

解密后部分:

1
2
3
4
5
seg000:7C19 88 16 E8 7C                                 mov     ds:drive_no, dl ;dl寄存器包含执行MBR的硬盘驱动器数量
seg000:7C1D 83 2E 13 04 10 sub word ptr ds:413h, 10h ;BIOS维护的可用内存 千字节
seg000:7C22 A1 13 04 mov ax, ds:413h
seg000:7C25 C1 E0 06 shl ax, 6
seg000:7C28 A3 63 7C mov ds:buffer_segm, ax

BIOS磁盘服务哦那个过INT 13h访问,I/O操作码或标识符在ah寄存器中传递,dl寄存器传递磁盘索引,cf标志指示执行服务期间是否发生错误,cf为1则发生错误,并在ah寄存器中返回详细错误代码。常用操作码如下:

操作码 操作描述 扩展操作码 扩展操作描述
41h 扩展安装检查
2h 将扇区读入内存 42h 扩展读
3h 写磁盘扇区 43h 扩展写
8h 获取驱动参数 48h 扩展获取驱动器参数

遗留操作和扩展操作的区别是扩展操作可用基于逻辑块寻址LBA的寻址方案,而遗留操作仅依赖基于柱面磁头扇区CHS的寻址方案。在基于LBA的方案中,扇区在磁盘上进行线性枚举。在基于CHS的方案中,每个扇区用(柱面,磁头,扇区)进行寻址。

定位隐藏存储的驱动器参数:

1
2
3
4
seg000:7C2B B4 48                                       mov     ah, 48h ; 'H'
seg000:7C2D BE F9 7C mov si, 7CF9h ; <suspicious>
seg000:7C30 C7 06 F9 7C 1E 00 mov ds:drive_param_bResultSize, 1Eh
seg000:7C36 CD 13 int 13h ; DISK - IBM/MS Extension - GET DRIVE PARAMETERS (DL - drive, DS:SI - buffer) 执行时该例程填充EXTENDED_GET_PARAMS结构 提供驱动器参数 存储在si寄存器中

检查返回参数:

1
2
3
4
5
6
7
8
9
typedef struct _EXTENDED_GET_PARAMS {
WORD bResultSize; //结果大小
WORD InfoFlags; //信息标识
DWORD CylNumber; //物理柱面数
DWORD HeadNumber; //物理磁头数
DWORD SectorsPerTrack; //每个磁道的扇区数
QWORD TotalSectors; //扇区总数
WORD BytesPerSector; //每个扇区字节数
} EXTENDED_GET_PARAMS, *PEXTENDED_GET_PARAMS;

Bootkit通过将TotalSectors和BytesPerSector相乘来计算硬盘驱动器总大小,单位字节,使用结果定位驱动器末端的隐藏存储。下列代码读取隐藏数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
seg000:7C4C                             read_loop:                              ; CODE XREF: seg000:7C5D↓j
seg000:7C4C E8 16 00 call read_sector ;从硬盘驱动器读扇区 存储在先前分配的内存缓冲区中
seg000:7C4F BE 1D 7D mov si, 7D1Dh ; <suspicious>
seg000:7C52 8B 0E 1B 7D mov cx, ds:word_7D1B
seg000:7C56 F3 A4 rep movsb
seg000:7C58 A1 19 7D mov ax, ds:word_7D19
seg000:7C5B 85 C0 test ax, ax
seg000:7C5D 75 ED jnz short read_loop
seg000:7C5F 61 popa
seg000:7C60
seg000:7C60 loc_7C60: ; DATA XREF: seg000:7C28↑w
seg000:7C60 ; seg000:7C45↑r
seg000:7C60 EA 00 00 00 00 jmp far ptr 0:0 ;bootloader 将控制权转移给恶意引导加载程序

对于read_sector例程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
seg000:7C65     read_sector     proc near               ; CODE XREF: seg000:read_loop↑p
seg000:7C65 ; sub_7CA9+3↓p
seg000:7C65 000 pusha
seg000:7C66 010 mov byte ptr ds:disk_address_packet.PacketSize, 10h
seg000:7C6B 010 mov byte ptr ds:disk_address_packet.SectorsToTransfer, 1
seg000:7C70 010 push cs
seg000:7C71 012 pop ds:disk_address_packet.TargetBuffer+2
seg000:7C75 010 mov word ptr ds:disk_address_packet+TargetBuffer, 7D17h ; <suspicious>
seg000:7C7B 010 push large [ds:drive_param.TotalSectors_l]
seg000:7C80 014 pop large [ds:disk_address_packet.StartLBA_l]
seg000:7C85 010 push large [ds:driver_param.TotalSectors_h]
seg000:7C8A 014 pop large [ds:disk_address_packet.StartLBA_h]
seg000:7C8F 010 inc eax
seg000:7C91 010 sub ds:disk_address_packet.StartLBA_l, eax
seg000:7C96 010 sbb ds:disk_address_packet.StartLBA_h, 0
seg000:7C9C 010 mov ah, 42h ; 'B'
seg000:7C9E 010 mov si, 7CE9h ; <suspicious> 该结构地址
seg000:7CA1 010 mov dl, byte ptr ds:drive_no
seg000:7CA5 010 int 13h ; DISK - IBM/MS Extension - EXTENDED READ (DL - drive, DS:SI - disk address packet)
seg000:7CA7 010 popa
seg000:7CA8 000 retn
seg000:7CA8 read_sector endp

其中BIOS磁盘服务用DISK_ADDRESS_PACKET唯一标识要从硬盘驱动器读取的扇区。

1
2
3
4
5
6
7
typedef struct _DISK_ADDRESS_PACKET {
BYTE PacketSize; //结构大小
BYTE Reserved;
WORD SectorsToTransfer; //要读写的扇区数量
DWORD TargetBuffer; //段 数据缓冲区偏移量
QWORD StartLBA; //起始扇区LBA地址
} DISK_ADDRESS_PACKET, *PDISK_ADDRESS_PACKET;

分区表位于MBR偏移0x1BE的位置,结构这里不再重复。0x7DBE处的BYTE表示激活状态,0x7DC2处BYTE表示NTFS类型,0x7DC5处DWORD表示初始偏移量部分,0x7DCA处DWORD表示分区大小,单位字节。其他条目为0的即为N/A。

1
2
3
4
7DBE:80 01 01 00 07 FE F8 FF 38 00 00 00 88 BD 7F 02
7DCE:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
7DDE:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
7DEE:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

之前说过BPB中HiddenSectors+1为0x2000字节的IPL,例如BPB结构如下:

1
2
3
4
5
6
7
8
9
10
seg000:000B 00 02                       word_B          dw 200h                 ; SectorSize
seg000:000D 08 byte_D db 8 ; SectorsPerCluster
seg000:000E 00 00 00 byte_E db 3 dup(0) ; reserved
seg000:0011 00 00 word_11 dw 0 ; RootDirectoryIndex
seg000:0013 00 00 dw 0 ; NumberOfSectorsFAT
seg000:0015 F8 db 0F8h ; MediaId
seg000:0016 00 00 byte_16 db 2 dup(0) ; Reserved2
seg000:0018 3F 00 dw 3Fh ; SectorsPerTrack
seg000:001A FF 00 dw 0FFh ; NumberOfHeads
seg000:001C 00 08 00 00 dword_1C dd 800h ; HiddenSectors

仿真与联动

这里用VMware Workstation与IDA Pro进行联动。将虚拟机配置文件.vmx内容修改或添加:

1
2
3
4
debugStub.listen.guest64 = "TRUE" //允许从本地主机进行来宾调试 启用了VMware GDB存根 允许将支持GDB协议的调试器附加到已调试的虚拟机上
debugStub.hideBreakpoints = "TRUE" //允许使用硬件断点 而不是软件断点
monitor.debugOnStartGuest64 = "TRUE" //GDB在执行CPU第一个指令时 即启动虚拟机后 终端调试器
//上述后缀32或64位不影响 因为调试的是16位实模式的预引导代码

IDA启动动调,选项为“Remote GDB debugger”,参数localhost:8832。IDA动调时将创建一个32位内存段,右键属性把它改成16位的。用F2在0000:7c00h上设置一个断点,在Location区域填写0x7c00,Settings框选Enabled和Hardware,并激活硬件断点模式Hardware breakpointmode为Execute,Size设为0x1即可,F9跑。