Introduction to macOS Kernel Debugging
In macOS there is a kernel module named “Don’t Steal Mac OS X” (DSMOS) which registers a function with the Mach-O loader to unpack binaries that have the SG_PROTECTED_VERSION_1
flag set on their __TEXT
segment. Finder, Dock, and loginwindow are a few examples of binaries that have this flag set. As it turns out, this kernel module at one point played a role in the myth that Apple had included a TPM in their Mac hardware.
I started reversing this kernel module because I was getting frustrated trying to reverse parts of the iOS kernel. It occurred to me that I was trying to run before walking so it was time to slow down a little. With that in mind, this post provides an introduction to kernel debugging. My next post will expand on this and go through reversing the DSMOS kernel module.
Kernel Debug Kits
Before we jump in, lets first cover why I switched to macOS when getting frustrated with iOS. The reason is pretty straight forward: macOS and iOS are built from nearly-identical kernel (XNU) source. In fact all of Apple’s operating systems are.
The big advantage to starting with macOS is that Apple provides what are known as Kernel Debug Kits (KDKs). A KDK provides you with Development and Debug builds of the kernel as well as some useful lldb scripts to help with debugging. One thing you may be wondering is what is the difference between the Debug and Development builds of the kernel? The short answer, as I understand it, is that the Development kernel is essentially the Release kernel with symbols while the Debug kernel has symbols as well as additional assertions/checks enabled. For this foray into kernel reversing I used the Development kernel.
KDKs are provided by Apple and you can download them from https://developer.apple.com/download/more/. When selecting a KDK be sure to choose the one with a kernel version that matches your target machine. I highly recommend reading the documentation provided with the KDK and will assume you have.
Configuring the Kernel Debugger
Unlike debugging an application, to debug the kernel you need to use two machines: the target and the host. The host is where you’ll be running lldb and doing all your work. The target is the machine that is running the Development kernel and you will be debugging. For the sake of efficacy, my host is an iMac (Late 2015) running macOS 10.12 (build 16A323) and my target is a Macbook Air (Mid 2011) running macOS 10.12 Beta (16A286aa).
A word on the target machine: it does not need to be physical hardware. You can use a virtual machine. The big difference is that on physical hardware you can easily trigger the kernel debugger using the NMI keys (more on this shortly). There might be a way to do this in a virtual machine but I had hardware so didn’t bother to figure it out.
When you install the KDK on your target machine one of the steps is to set boot-args
in nvram. Mine are set as follows:
airy:~ dean$ nvram boot-args
boot-args debug=0x14e kcsuffix=development kext-dev-mode=1 kdp_match_name=en4 -v
Lets take a moment and go through each of these flags.
kcsuffix
simply instructs the boot loader which kernel it should look for. In my case I used the Development kernel sokcsuffix
should be set todevelopment
.kext-dev-mode
causes kernel module signing requirements to be relaxed.-v
enables verbose mode during boot.
The remaining two variables, kdp_match_name
and debug
, warrant a bit more discussion than a bullet point.
kdp_name_match
Remote kernel debugging, which we are going to do, requires either Ethernet or FireWire. It does not work over WiFi or USB. This obviously begs the question of how one would go about remote debugging with a laptop that has no Ethernet port. Thankfully, Apple has thought this through and the answer is the kdp_match_name
boot-args
variable coupled with a Thunderbolt-to-Ethernet adapter. You can also use an Apple Cinema display if you have one of those.
The kdp_match_name
variable instructs the kernel to bind the remote debugger to the specified interface. You can find the interface name using ifconfig
and checking which interface has the desired IP assigned. You also need to use kdp_match_name
if your Mac has multiple Ethernet interfaces.
debug
The role played by the debug
variable is to configure the kernel debugger. It’s value is an OR of the DB_*
constants defined in osfmk/kern/debug.h
of the XNU source. Table 1 documents many of the interesting and not-so-interesting constants.
Flag | Value | Description |
---|---|---|
DB_HALT |
0x1 | Wait for debugger on boot |
DB_PRT |
0x2 | Send printf() output to the console |
DB_NMI |
0x4 | Activates the kernel debugging facility, including support for NMI |
DB_KPRT |
0x8 | Send kprintf() output to remote console |
DB_KDB |
0x10 | Use KDB instead of GDB |
DB_SLOG |
0x20 | Enable logging system diagnostics to the system log |
DB_ARP |
0x40 | Allows the kernel debugger nub to use ARP and thus support debugging across subnets. |
DB_KDP_BP_DIS |
0x80 | Deprecated, was used for old versions of GDB |
DB_LOG_PI_SCRN |
0x100 | Disable the graphical panic screen. |
DB_KDP_GETC_ENA |
0x200 | Prompt to enter KDB upon panic |
DB_KERN_DUMP_ON_PANIC |
0x400 | Trigger core dump on panic |
DB_KERN_DUMP_ON_NMI |
0x800 | Trigger core dump on NMI |
DB_DBG_POST_CORE |
0x1000 | Wait in debugger after NMI core |
DB_PANICLOG_DUMP |
0x2000 | Send paniclog on panic,not core |
DB_REBOOT_POST_CORE |
0x4000 | Attempt to reboot after post-panic crashdump/paniclog dump. |
DB_NMI_BTN_ENA |
0x8000 | Enable button to directly trigger NMI |
DB_PRT_KDEBUG |
0x10000 | kprintf KDEBUG traces |
DB_DISABLE_LOCAL_CORE |
0x20000 | ignore local core dump support |
Table 1: Summary of constants to use in debug
boot-args variable.
Some of the common values I’ve seen are: 0x141, 0x144, and 0x14e. Note that they all have DB_LOG_PI_SCRN
and DB_ARP
set.
Using the Kernel Debugger
Using the remote kernel debugger essentially boils down to installing the target KDK on the host and then running lldb on the host. You’ll want to install the target KDK on the host so that lldb has access to the symbolicated kernel as well as some handy lldb scripts developed by Apple. One thing to keep in mind about the remote debugger: it is not always waiting for connections. Put differently, you can only connect to it at boot if you set DB_HALT
, during a panic, or an NMI if you set DB_NMI
. Being able to trigger the debugger using the NMI keys (left command, right command, and power all at once) can be very handy if you want to drop into the debugger and inspect the running kernel.
Once you’ve set your boot-args
on the target and restarted your machine you can start debugging. Assuming you’ve set your debug
variable to 0x14e once you’re machine has restarted you can trigger the kernel using the NMI keys (left command, right command, power all at once). When you hit the NMI keys the IP address of the target will be shown on the screen. On your host you can then connect as follows.
(lldb) kdp-remote <target-ip>
Version: Darwin Kernel Version 16.0.0: Fri Aug 5 19:25:15 PDT 2016; root:xnu-3789.1.24~6/DEVELOPMENT_X86_64; UUID=4F6F13D1-366B-3A79-AE9C-44484E7FAB18; stext=0xffffff8006000000
Kernel UUID: 4F6F13D1-366B-3A79-AE9C-44484E7FAB18
Load Address: 0xffffff8006000000
Loading kernel debugging from /Library/Developer/KDKs/KDK_10.12_16A286a.kdk/System/Library/Kernels/kernel.development.dSYM/Contents/Resources/DWARF/../Python/kernel.py
LLDB version lldb-360.1.50
settings set target.process.python-os-plugin-path "/Library/Developer/KDKs/KDK_10.12_16A286a.kdk/System/Library/Kernels/kernel.development.dSYM/Contents/Resources/DWARF/../Python/lldbmacros/core/operating_system.py"
Target arch: x86_64
Instantiating threads completely from saved state in memory.
settings set target.trap-handler-names hndl_allintrs hndl_alltraps trap_from_kernel hndl_double_fault hndl_machine_check _fleh_prefabt _ExceptionVectorsBase _ExceptionVectorsTable _fleh_undef _fleh_dataabt _fleh_irq _fleh_decirq _fleh_fiq_generic _fleh_dec
command script import "/Library/Developer/KDKs/KDK_10.12_16A286a.kdk/System/Library/Kernels/kernel.development.dSYM/Contents/Resources/DWARF/../Python/lldbmacros/xnu.py"
xnu debug macros loaded successfully. Run showlldbtypesummaries to enable type summaries.
Kernel slid 0x5e00000 in memory.
Loaded kernel file /Library/Developer/KDKs/KDK_10.12_16A286a.kdk/System/Library/Kernels/kernel.development
Loading 119 kext modules warning: Can't find binary/dSYM for com.apple.kec.corecrypto (700E1192-8CD6-3F61-ABE9-D27C2CC1F164)
/*-- removed for clarity --*/
. done.
Target arch: x86_64
Instantiating threads completely from saved state in memory.
kernel.development was compiled with optimization - stepping may behave oddly; variables may not be available.
Process 1 stopped
* thread #2: tid = 0x00b6, 0xffffff800639a3de kernel.development`Debugger [inlined] hw_atomic_sub(delt=1) at locks.c:1513, name = '0xffffff8011639cf0', queue = '0x0', stop reason = signal SIGSTOP
frame #0: 0xffffff800639a3de kernel.development`Debugger [inlined] hw_atomic_sub(delt=1) at locks.c:1513 [opt]
(lldb)
At this point you can use lldb as you normally would. You will also have a bunch of additional commands provided throught the KDK. For example you can list all kexts like so:
(lldb) showallkexts
OverflowError: long too big to convert
UUID kmod_info address size id refs TEXT exec size
version name
0490CEBA-045D-344E-AC8B-1449598798F6 0xffffff7f88fdf528 0xffffff7f88fdb000 0x5000 147 0 0xffffff7f88fdb000 0x5000
1.70 com.apple.driver.AudioAUUC
92511291-6B64-35B1-A824-53034DEBDD39 0xffffff7f88fdacb8 0xffffff7f88fd6000 0x5000 146 0 0xffffff7f88fd6000 0x5000
1.9.5d0 com.apple.driver.AppleHWSensor
EDC33E0C-CAA2-3E79-951D-1FC392284B4D 0xffffff7f88fd5508 0xffffff7f88fcd000 0x9000 145 0 0xffffff7f88fcd000 0x9000
3.0 com.apple.filesystems.autofs
CCC57A89-31FE-3D9F-88A3-0EC7D254477B 0xffffff7f88fcb028 0xffffff7f88fc8000 0x5000 144 1 0xffffff7f88fc8000 0x5000
1.0 com.apple.kext.triggers
E6E68296-809B-3884-ADEF-E85831F4B106 0xffffff7f88fc7090 0xffffff7f88fbb000 0xd000 143 0 0xffffff7f88fbb000 0xd000
1 com.apple.driver.pmtelemetry
/*-- removed for clarity --*/
To find all the commands available to lldb just type help
at the prompt. The KDK adds a lot of them rather than going through them in this post your better off taking the time to pick a few interesting ones and seeing what they do.
At this point, you should be able to setup remote kernel debugging in macOS and be able to inspect a running kernel. In the next post I will put some of this to use and look at the DSMOS kernel module.