|\ __________                          __   __                         __
         | |   __     |          _____ __    __\__/_|  |_ __ ___   _____   ___ |  |\_____     
         | |  /  \    |         /  _  \  \  /  /  |_    _|  /   \ /  _  \ /  _ \  |/  _  \    
         | |  \  /___ |        /  /_\  \  \/  /|  | |  |\|   /\  \  / \  \  / \   |  /_\  \   
         | |__/    _ \|        |  _____||    |\|  | |  | |  |\|  |  |\|  |  |\|   |  _____|\  
         | |___/\  \\_\        \  \____/  /\  \|  | |  | |  | |  |  \_/  /  \_/   |  \___ \|  
         | |    /   \_|         \_____/__/ /\__\__| |__| |__| |__|\_____/ \____/__|\_____/\   
         | |   / / \___|         \____\__\/  \__\__\|\__\|\__\|\__\\____\/ \___\\__\\____\/   
         | |__/_/_____|     
         |/                

Last changed: 05.05.2021

Windows Kernel Debugging with WinDbg


The underlying concepts can be read in the official documentation of the Microsoft WDK and of the distiction between miniport and port drivers

More windbg commands can be found in microsofts official windbg documentation

The Vergilius Project offers a browsable collection of windows kernel structures.

setup kernel debugging


create boot entry for target machine

The tool bcdedit.exe can be used to create a boot entry.

bcdedit /copy {current} /d "Kernel Debugging"
bcdedit /default {<GENERATED_UUID>}
bcdedit /set {default} debug yes

The different connection methods can be configured as follows

bcdedit /dbgsettings serial debugport:1 baudrate:115200
bcdedit /dbgsettings usb targetname:<NAME>
bcdedit /dbgsettings net hostip:10.0.0.1 port:50000 newkey

configure serial connection in virtualbox

Setup serial port in host and target. E.g. select COM1 on both machines and configure them as Host-Pipe with a common filename /tmp/pipe1. In the machine which will get booted first uncheck the option connect to pipe.

disable driver signature checks and patchguard

bcdedit /set testsigning on
bcdedit /set nointegritychecks on

If you want to get rid of the watermark you have to patch the correnponding strings in shell32.dll.mui und basebrd.dll.mui.

connect kernel debugger

The kernel debugger has to be invoked according to the chosen connection method.

kd -k com:port=1,baud=115200
kd -k usb:targetname=<NAME>
kd -k net:port=12345,key=<GENERATED_KEY>,target=<TARGET_IP>

detect kernel debugger

If you use network debugging you can detect the kernel debugger by its interface

systeminfo
wmic nic get caption

connect windbg to ida

If you have IDA Pro running on the same machine as the kernel debugger you can configure IDA Pro to use windbg for kernel debugging. Make sure the folder with the windbg binaries is in the PATH.

Select Kernel mode debugging in the menu Debugger -> Debugger options... -> Set specific options. Then configure the Connection string in Debugger -> Process options... (e.g. net:port=50000,key=<GENERATED_KEY>,target=<TARGET_IP> or com:port=1,baud=115200).

Now you should be able to connect with Debugger -> Attach to process....

ret-sync

Alternatively you can connect IDA Pro with ret-sync. Download the ret-sync plugin and the prebuild binaries for windbg

Check python path in SyncPlugin.py. Probably C:\Python27-x64 on x64.

IDA -> Script file ... -> SyncPlugin.py

In kd load the dll with

.load path\to\sync.dll
!sync

analyse processes


!process 0 0            list all processes
!dml_proc               browsable process list
!process 0 0 calc.exe   show info of calc.exe

To analyse a specific process you will have to switch the context.

!process -1 0           show current process
.process /i /r /p <address>     switch context to process
.reload /user           reload symbols
!peb                    show peb of current process
lmu                     list loaded user modules (dlls)

The current thread is also stored in the KPRCB of the corresponding cpu.

!running                show current cpu
rdmsr c0000101          get current KPCR address
!pcr 0                  print KPCR of cpu 0
dt _KPCR <addr>
dt _KPCR <addr> Prcb.CurrentThread->ApcState.Process
dt _EPROCESS <proc_addr>

process privileges

dt _EPROCESS poi(nt!PSInitialSystemProcess) UniqueProcessId ImageFileName Token.
!token @@C++(<token_addr> & ~f)
dt _TOKEN @@C++(<token_addr> & ~f)

system calls (64bit)

uf ntdll!NtCreateFile

When the syscall instruction (x64) is executed the value from the IA32_LSTAR MSR (System Call Target Address, 0xc0000082) gets loaded into RIP.

rdmsr c0000082
ln <address>
uf nt!KiSystemCall64Shadow
uf nt!KiSystemServiceUser
u nt!KiSystemServiceRepeat L10
dqs nt!KeServiceDesctiptorTable

Calculate function pointer address

ln nt!KiServiceTable + ((poi(nt!KiServiceTable + (0x55*4)) & 0xffffffff) >>4)
ln @@C++(*(void **)@@(nt!KeServiceDescriptorTable)) + (@@C++((*(int **)@@(nt!KeServiceDescriptorTable))[0x55]) >>>4)

You can do this dynamically

bp /p $proc nt!KiSystemServiceRepeat
ln nt!KiServiceTable + ((poi(nt!KiServiceTable + (@eax*4)) & 0xffffffff) >>4)

page table entries

You can list all allocated pages and translate their page table entries to virtual addresses.

!pte <address>
!ptetree
!pte2va <pte>

loading kernel drivers


To load a driver from cmd.exe create a driver service.

sc create driver_debug binPath= c:\path\to\driver.sys type= kernel
sc query driver_debug
sc qc driver_debug
sc start driver_debug

In windbg break on the unresolved driver entry.

bu $iment(driver_name)  set unresolved breakpoint

If this does not work you can create a debug driver which breaks on entry and look at the stacktrace.

#include <ntifs.h>
NTSTATUS DriverEntry(PDRIVER_OBJECT drv, PUNICODE_STRING reg) {
    UNREFERENCED_PARAMETER(drv);
    UNREFERENCED_PARAMETER(reg);
    __debugbreak();
    return STATUS_UNSUCCESSFUL;
}

driver info

lm                      list modules (drivers)
!dh -f <driver_name>    show driver headers (incl. entry point)
lm m nt                 show kernel base
!object \Driver
dt nt!_DRIVER_OBJECT <address>
!drvobj <DRIVER>
!devobj <DEVICE>

kdfiles

If you are developing a driver on the debugging machine you can create a mapping to automatically copy the most recent file to the debuggee.

map
\??\c:\path\to\driver.sys
C:\Users\user\Source\Repos\test\Release\test.sys

driver I/O


Normally a driver creates a device in the appropriate device stack and registers the needed major functions. To be accessable from user space the driver needs to create a symbolic link (e.g. in the GLOBAL/??) namespace.

major functions

Major functions have the following prototype

NTSTATUS DriverDispatch(_DEVICE_OBJECT *DeviceObject, _IRP *Irp)

The names of the 28 different major functions can be resolved in windbg.

da @@(((char**)@@(nt!IrpMajorNames))[0])

I/O request packets (IRP)

Input and output to the major function is contained in the _IRP. The function code and the parameters can be found in the Tail.Overlay.CurrentStackLocation (0xb8). Depending on the configured method the output gets copied to the UserBuffer (0x70) (METHOD_NEITHER) or AssociatedIrp.SystemBuffer (0x18) (METHOD_BUFFERED).

dt nt!_IRP @rdx
dt nt!_IRP @rdx AssiciatedIrp->*
dt nt!_IRP @rdx Tail.Overlay.CurrentStackLocation->*
dt nt!_IRP @rdx Tail.Overlay.CurrentStackLocation->Parameters.DeviceIoControl.

IOCTLs

Comands to the device itself get send to the major functions IRP_MJ_DEVICE_CONTROL or IRP_MJ_INTERNAL_DEVICE_CONTROL. The _IO_STACK_LOCATION contains the IoControlCode (0x18).

#define IOCTL_Device_Function CTL_CODE(DeviceType, Function, Method, Access)

driver signing


signtool sign /f Verisign.pfx /p t-span /ac MSCV-VSClass3.cer driver.sys

bypass

The Kernel Driver Utility exploits vulnerable drivers to load unsigned code.