PoC gdb debugging the Windows 10 secure kernel, with examples of use:
- Tracing secure calls
- Tracing
Skpg*
calls - Debugging
LsaIso.exe
(Credential Guard)
At the time of writing, it works on a Win10 1903, Windows Server 2019 and Win10 January 2020 Insider preview (Build 19550).
Requirements:
- QEMU-KVM: version from the package does not include the possibility to read /
write physical memory (support for
qemu.PhyMemMode
packets). We'll then use the version from the repository - Win10 image
- A kernel supporting some KVM extensions. The current
5.0.0-38
version crashes qemu while booting Win10 with VSM. For the experiments, the4.15.0-1066-oem
is used (available from Ubuntu repository) - Secure-boot, with OVMF. The Ubuntu version (from repository) does not include variables with preconfigured windows Key. The fedora version can be used (or the one in this repository)
Steps:
- Get a working version of Win10 on QEMU-KVM
- Extracts
securekernel.exe
from this installation. One can use qemu-nbd:
rmmod nbd
modprobe nbd max_part=16
qemu-nbd -c /dev/nbd0 drive.vdi
mount /dev/nbd0p1 /mnt
cp /mnt/.../System32/securekernel.exe /dest
qemu-nbd -d /dev/nbd0
- Extracts a few interesting addresses from this binary. It can be done manually, or using an IDA script (
extract_ida.py
) - Run Win10 with QEMU-KVM, enabling secureboot, KVM. Some englightments can be added, but are not required. One can even disable the hypervisor advertising (
hypervisor=off
). Example of command:
qemu/x86_64-softmmu/qemu-system-x86_64 -snapshot -drive file=/path/to/win10.qcow2,format=qcow2,if=none,id=drive-sata0-0-0 -device ide-hd,bus=ide.0,drive=drive-sata0-0-0,id=sata0-0-0,bootindex=1 -cpu host,hv_time,kvm=off,hv_vendor_id=null,hypervisor=off -enable-kvm -m 4096 -global driver=cfi.pflash01,property=secure,value=on -net none -pflash /path/to/OVMF_CODE.secboot.fd -pflash /path/to/OVMF_VARS.secboot.copy.fd -machine pc-q35-3.1,smm=on -global driver=cfi.pflash01,property=secure,value=on -device pcie-root-port,port=0x10,chassis=1,id=pci.1,bus=pcie.0,multifunction=on,addr=0x2 -device pcie-root-port,port=0x11,chassis=2,id=pci.2,bus=pcie.0,addr=0x2.0x1 -device pcie-root-port,port=0x12,chassis=3,id=pci.3,bus=pcie.0,addr=0x2.0x2 -device pcie-root-port,port=0x13,chassis=4,id=pci.4,bus=pcie.0,addr=0x2.0x3 -device qemu-xhci,p2=15,p3=15,id=usb,bus=pci.2,addr=0x0 -s`
The -s
is used to automatically starts the gdb stub
on 127.0.0.1:1234
. One can start it at runtime using the qemu console (command gdbserver
).
- Once the Win10 correctly starts, ensure Virtual Based Security capabilities are enabled. Depending on the experiments, one can enable Device Guard (through the local console), HVCI (through the "Core memory protection"), ...
Once these steps have been done, one can now attach to the secure kernel.
First, once the VM is running, attach to it with gdb
, disable gdb pagination and display the current instruction:
(gdb) target remote 127.1:1234
(gdb) set pagination off
(gdb) display /i $pc
The difficulty is now to retrieve the virtual address of the secure kernel. There are several way to do it:
- Inspect the memory, looking for VMCS structures
- Inspect the memory, looking for the securekernel, then rebuilding back Pagination tables and associated addresses
- Breaking early during the startup, in
hvloader
: the securekernel will report back its address - Breaking in Hyper-V, looking for VTL transition (hint: handling of hypercalls 0x11 and 0x12)
This PoC is based on a more pragmatic / ulgy way to do it.
First, let's load the find_patch_phys.py
script in gdb (after patching the
constants inside, like the securekernel.exe
path):
(gdb) source path/to/find_patch_phys.py
Now, let's find securekernel
in the physical memory, patching the
IumInvokeSecureService
entry point with an infinite loop. This function is
called to perform "secure calls", and is at least called by the normal world
every second to perform "periodic" work.
(gdb) find_patch_ium_ep
Look for 'IumInvokeSecureService' in physical memory...
0x0
0x1000000
0x2000000
0x3000000
0x4000000
0x5000000
0x6000000
Found at 0x6c4a0b0!
Replace with infinite loop...
Once the memory has been patched, we let the VM continue for a while (a few seconds is enough):
(gdb) c
Continuing.
^C
Program received signal SIGINT, Interrupt.
0xfffff80608a130b0 in ?? ()
1: x/i $pc
=> 0xfffff80608a130b0: jmp 0xfffff80608a130b0
We can check we actually ends on an infinite loop. If so, we note the virtual address, and fix the memory:
(gdb) fix_ium_ep
Base VA: 0xfffff804c8a00000
At this stage, we have the virtual address used by the secure kernel. Then, we can break on it from anywhere using hardware breakpoints, or use software breakpoints when we are in the secure kernel context (QEMU gdb stub can support any of them).
If, while using a script, one need to get back in the context of the secure kernel, the following warning will be printed:
(gdb) my_command
Please launch this command in the context of the secure kernel
hint: hbreak *(0xfffff8040d0900b0)
One can do:
(gdb) hbreak *(0xfffff8040d0900b0)
Hardware assisted breakpoint 14 at 0xfffff8040d0900b0
(gdb) c
Continuing.
Breakpoint 14, 0xfffff8040d0900b0 in ?? ()
1: x/i $pc
=> 0xfffff8040d0900b0: mov %rbx,0x18(%rsp)
(gdb) del 14
We'll break on IumInvokeSecureService
and inspect its argument to get the secure call number.
The translation is made thanks to the _SKSECURESERVICE
enum, available in ntoskernel.pdb
.
One can extract it with IDA or WinDbg, and convert it to a JSON file thanks to
the log_securecalls/convert_sknum.py
script.
Then, we can import the script and run it:
(gdb) source log_securecalls/log_securecalls.py
(gdb) log_securecall /path/to/sknums.json
Breakpoint 15 at 0xfffff8040d0900b0
(gdb) c
Continuing.
[IumInvokeSecureService] SECURESERVICE_FAST_FLUSH_RANGE_LIST
[IumInvokeSecureService] SECURESERVICE_FAST_FLUSH_RANGE_LIST
[IumInvokeSecureService] SECURESERVICE_FAST_FLUSH_RANGE_LIST
...
Thanks to the support of many breakpoint, we'll break on all securekernel.exe
functions starting with "Skpg".
To do so:
(gdb) source /path/to/log_skpg.py
(gdb) log_skpg
Breakpoint 19 at 0xfffff8040d0a99f8
Breakpoint 20 at 0xfffff8040d0f35d8
Breakpoint 21 at 0xfffff8040d0a6248
Breakpoint 22 at 0xfffff8040d0f3804
Breakpoint 23 at 0xfffff8040d07e1a0
Breakpoint 24 at 0xfffff8040d089518
Breakpoint 25 at 0xfffff8040d08938c
Breakpoint 26 at 0xfffff8040d07e6e4
Breakpoint 27 at 0xfffff8040d0a52f8
...
(gdb) c
Continuing.
[SkpgPeriodicEvent]
[SkpgPeriodicEvent]
[SkpgPeriodicEvent]
[SkpgPeriodicEvent]
Ium binaries specifies if their are debuggable or not. When attaching to one of
this process, the secure kernel function SkpsIsProcessDebuggingEnabled
is
called to retrieve if the debugging is enabled or not.
Here, we simply always return 1.
(gdb) source /path/to/enable_debugging.py
(gdb) enable_ium_debug
Breakpoint 1 at 0xfffff80608a7961c
(gdb) c
Continuing.
Now, we can attach to LsaIso
in WinDbg and observes the patch in the gdb output:
[SkpsIsProcessDebuggingEnabled] Enter
Breakpoint 2 at 0xfffff80608a133e0
[SkpsIsProcessDebuggingEnabled] Exit - Fix RAX
- Hyper-V debugging for beginners, from @gerhart_x Link
- BlackHat 2015: BATTLE OF SKM AND IUM: HOW WINDOWS 10 REWRITES OSARCHITECTURE, from Ionescu Link
- BlackHat 2017: Fractured Backbone: Breaking Modern OS Defenses with Firmware Attacks, from Bulygin and al. Link
- LiveCloudKd
- Hypervisor Top Level Functional Specification