Debugging macOS Kernel For Fun

Hi there! It’s GeoSn0w. Debugging the damn kernel is a very entertaining thing to do (until you provoke a serious exception, that is, and the kernel crawls into a corner from which it refuses to get out). Unfortunately, it’s not an easy task nowadays and Apple seems to want to make it harder and harder. At first, by hiding under lock and key the documentation about the debug boot arguments, and then by moving the Kernel Debug Kit under the Developer Account-only Downloads section. There are many write-ups on the internet about debugging the kernel on macOS but many of them are outdated as hell and the NVRAM boot arguments they tell you to set are no longer working. Some of them stop at just the “now you should have a working debug session” - so what? what do I do next? I wanna have fun Goddammit! In this write-up I am doing my best to provide the most accurate information for 2019, the right commands, the right boot-args and of course, practical examples you can begin with.

A note for the l33t h4xxors

If you are going to say “well if people don’t know what to do with a kernel debugger they shouldn’t use one”, please segfault. You’ve been a beginner once and wanted to have fun and learn so shut up.

Getting started with Kernel debugging on macOS

Okay, so the first things we need to sort out is the lab. You need to have a device of which kernel you want to debug (in my case I am using my iMac 2011 as a debuggee) and a device from where you do the debugging (I am using my MacBook Pro 2009 for this). You can connect the two in various ways I will discuss in this write-up, but in my case, the best method (and the most reliable) seems to be via a Firewire cable between the two (that is because both my machines have actual firewire ports, not USB-C bullshit).

With the hardware part set up, we need some software. You CAN theoretically debug the RELEASE kernel, but when you’re a beginner the Development one is much better. By default, macOS comes with a RELEASE fused kernel located in /System/Library/Kernels/kernel where kernel is a Mach-O 64-bit executable x86_64. We can get ourselves the Development kernel for our macOS version by navigating to Apple Developer portal and downloading the Kernel Debug Kit. It’s surprising that Apple only put the kit under a normal, free Apple Developer Account lock; I would have expected them to put it under the paid Apple Developer Account downloads by now.

Anyways, once you navigate to this Apple Developer Portal Downloads section, you will see something like this:

VERY IMPORTANT! You should get the appropriate kernel debug kit for your specific macOS version! You will boot the downloaded kernel later and if it doesn’t match your macOS version, it will NOT boot! I am not responsible for any damages to your files, computer, life, cat, whatever. Proceed at your own risk.

Finding the proper Kernel Debug Kit for your macOS version [!]

In order to locate the proper Kernel Debug Kit, you must know your macOS version and the actual build number. You can easily see what macOS version you are running by going to the Apple logo, pressing “About This Mac”, and reading the version in the window that appears, for example, “Version 10.13.6”.

For the actual build number, you can either click once on the “Version” label in that “About This Mac” window, or you can run the terminal command sw_vers | grep BuildVersion. In my case running the command outputs “BuildVersion: 17G65”.

Last login: Sun Dec  2 03:58:16 on ttys000
Isabella:~ geosn0w$ sw_vers | grep BuildVersion
BuildVersion:    17G65
Isabella:~ geosn0w$ 

So, in my case, I am running macOS High Sierra (10.13.6) build number 17G65. Looking in the Downloads section I could immediately find my version listed so I can download the .DMG file containing the installation files. The download is pretty small.

Preparing the debuggee for being debugged by the debugger ;)

With the Debug Kit downloaded on the debuggee (that is the machine whose kernel you wanna debug), mount the DMG file by double-clicking on it. Inside the DMG you will find a file called KernelDebugKit.pkg. Double-click that and follow the installation wizard. It will ask you for your macOS login password. If asked, do not move the installer to trash. You will need it later.

When the installation is complete it will look something like this.

After the installation completes, navigate to /Library/Developer/KDKs. There you will have a folder named KDK_YOUR_VERSION_BUILDNUMBER.kdk. In my case the folder is called KDK_10.13.6_17G65.kdk. Open the folder and inside it you will find another folder called “System”. Navigate into the folder, then into “Library” and then into “Kernels”. In that folder you will find a few kernel binaries, some Xcode Debug Symbol files (.dSYM), etc. You are interested in the file called kernel.development.

Copy the kernel.development and paste into /System/Library/Kernels/ alongside your release kernel binary. When you are done, you should have two kernels on your macOS installation, a RELEASE one and a DEVELOPMENT one.

Disabling SIP on the debuggee

For proper debugging, you may need to disable SIP (System Integrity Protection) on the machine whose kernel you wanna debug. To do that, reboot the machine in Recovery Mode. To do that, reboot the machine and when you hear the “BOONG!”, or when the screen turns on, press CMD + R. Wait a few seconds for it to boot into Recovery Mode user interface, and open “Terminal” from the top bar.

In the Recovery Terminal, write csrutil disable. Then reboot the machine and boot it normally to macOS.

Setting the correct NVRAM boot-args as of 2018/2019

The boot-args have been changed during the years by Apple so what you find on the internet may or may not work depending on how old the write-up is. The following boot-args have been tested and are working with macOS High Sierra as of 2018.

NOTE! The following boot-args assume you are doing this over Firewire or via Firewire through Thunderbolt adpter.

If you are using a FireWire cable through a real FireWire port (older Macs):

In the Terminal run the following command:

sudo nvram boot-args="debug=0x8146 kdp_match_name=firewire fwdebug=0x40 pmuflags=1 -v"

If you are using a FireWire through ThunderBolt adapter:

In the Terminal run the following command:

sudo nvram boot-args="debug=0x8146 kdp_match_name=firewire fwkdp=0x8000 fwdebug=0x40 pmuflags=1 -v"

The difference is that fwkdp=0x8000 tells IOFireWireFamily.kext::AppleFWOHCI_KDP to use the non-built-in firewire <-> thunderbolt adapter for the debugging session.

This is pretty much it, the debuggee is ready to be debugged after a reboot, but let me explain you a bit what the boot arguments do.

  • debug=0x8146 -> This enables the debugging and allows us to press the Power button to trigger a NMI This stands for Non-Maskable Interrupt and it is used to allow the debugger to connect.
  • kdp_match_name=firewire -> This allows us to debug via FireWireKDP.
  • fwkdp=0x8000 -> As I explained earlier, this tells the kext to use the thunderbolt to firewire adapter. Don't set it if you use normal Firewire ports.
  • fwdebug=0x40 -> Enables more verbose output from the AppleFWOHCI_KDP driver, it is useful for troubleshooting.
  • pmuflags=1 -> This one disables the Watchdog timer.
  • -v -> The simplest of birds. This one tells the computer to boot verbose instead of the normal Apple logo and progress bar. This is extremely useful for troubleshooting, not only when you debug but also when you have boot loops.

Aside from these boot arguments that we set, macOS supports more args that are defined in /osfmk/kern/debug.h which I am going to list below. These were taken from xnu-4570.41.2.

...
/* Debug boot-args */
#define DB_HALT        0x1
//#define DB_PRT          0x2 -- obsolete
#define DB_NMI        0x4
#define DB_KPRT        0x8
#define DB_KDB        0x10
#define DB_ARP          0x40
#define DB_KDP_BP_DIS   0x80
//#define DB_LOG_PI_SCRN  0x100 -- obsolete
#define DB_KDP_GETC_ENA 0x200

#define DB_KERN_DUMP_ON_PANIC        0x400 /* Trigger core dump on panic*/
#define DB_KERN_DUMP_ON_NMI        0x800 /* Trigger core dump on NMI */
#define DB_DBG_POST_CORE        0x1000 /*Wait in debugger after NMI core */
#define DB_PANICLOG_DUMP        0x2000 /* Send paniclog on panic,not core*/
#define DB_REBOOT_POST_CORE        0x4000 /* Attempt to reboot after
                        * post-panic crashdump/paniclog
                        * dump.
                        */
#define DB_NMI_BTN_ENA      0x8000  /* Enable button to directly trigger NMI */
#define DB_PRT_KDEBUG       0x10000 /* kprintf KDEBUG traces */
#define DB_DISABLE_LOCAL_CORE   0x20000 /* ignore local kernel core dump support */
#define DB_DISABLE_GZIP_CORE    0x40000 /* don't gzip kernel core dumps */
#define DB_DISABLE_CROSS_PANIC  0x80000 /* x86 only - don't trigger cross panics. Only
                                         * necessary to enable x86 kernel debugging on
                                         * configs with a dev-fused co-processor running
                                         * release bridgeOS.
                                         */
#define DB_REBOOT_ALWAYS        0x100000 /* Don't wait for debugger connection */
...

Preparing the debugger machine

Okay, now that the debuggee is ready, we need to configure the machine where the debugger will run. For that, I am using another macOS machine running El Capitan, but that matters less. Remember that Kernel Debug Kit we installed on the debuggee? We need to install it on the debugger machine too. The difference is that we will NOT move the kernels and we will not set any boot arguments on the debugger. We need the kernel because we are going to use lldb to perform the debugging. If you’re familiar with GDB instead, don’t worry. There is a GDB -> LLDB command sheet available right here.

Note: You should install the same macOS Kernel Debug toolkit on the debugger even if it doesn’t run the same macOS version as the debuggee because we will not boot any kernel on the debugger.

After you installed the toolkit, it’s time to connect.

Debugging the kernel

To begin, reboot the debuggee. You will see that it boots into a text-mode console which spits out verbose boot information. Wait until you see “DSMOS has arrived!” on the screen and press the Power button once. Don’t hold it pressed. On the debuggee, you will see that it is waiting for a debugger to be connected.

On the debugger machine:

Open a Terminal window and start fwkdp -v, this is the FireWire KDP Tool and will listen to the FireWire interface and redirect the data to the localhost so that you can set the KDP target as localhost or 127.0.0.1. You should get an output similar to this:

MacBook-Pro-van-Mac:~ mac$ fwkdp -v
FireWire KDP Tool (v1.6)
Matched on device 0x00002403
Created plugin interface 0x7f9e50c03548 with result 0x00000000
Created device interface 0x7f9e50c0d508 with result 0x00000000
Opened device interface 0x7f9e50c0d508 with result 0x00000000
Added callback dispatcher with result 0x00000000
Created pseudo address space 0x7f9e50c0d778 at 0xf0430000
Address space enabled.
2018-12-02 05:51:05.453 fwkdp[5663:60796] CFSocketSetAddress listen failure: 102
Created KDP socket listener 0x7f9e50c0d940 with result 0
KDP Proxy and CoreDump-Receive dual mode active.
Use 'localhost' as the KDP target in gdb.
Ready.

Now, WITHOUT closing this window open another Terminal window and start lldb debugger by passing it the kernel.development file you installed on the debugger machine as part of the Kernel Debug Kit. Remember, the kernel can be found at /Library/Developer/KDKs/. There you will have a folder named KDK_YOUR_VERSION_BUILDNUMBER.kdk. In my case the folder is called KDK_10.13.6_17G65.kdk. and my full kernel path that I need is /Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development.

The command in the new terminal window in MY case will be xcrun lldb /Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development

Last login: Sun Dec  2 10:37:51 on ttys000
MacBook-Pro-van-Mac:~ mac$ xcrun lldb /Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development
(lldb) target create "/Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development"
warning: 'kernel' contains a debug script. To run this script in this debug session:
command script import "/Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development.dSYM/Contents/Resources/DWARF/../Python/kernel.py"

To run all discovered debug scripts in this session:

    settings set target.load-script-from-symbol-file true

Current executable set to '/Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development' (x86_64).

As you can see, lldb says that the “kernel” contains a debug script. In the lldb window that is now open, run settings set target.load-script-from-symbol-file true to run the script.

Last login: Sun Dec  2 10:37:51 on ttys000
MacBook-Pro-van-Mac:~ mac$ xcrun lldb /Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development
(lldb) target create "/Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development"
warning: 'kernel' contains a debug script. To run this script in this debug session:
command script import "/Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development.dSYM/Contents/Resources/DWARF/../Python/kernel.py"

To run all discovered debug scripts in this session:

    settings set target.load-script-from-symbol-file true

Current executable set to '/Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development' (x86_64).
(lldb) settings set target.load-script-from-symbol-file true

Loading kernel debugging from /Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development.dSYM/Contents/Resources/DWARF/../Python/kernel.py
LLDB version lldb-360.1.70
settings set target.process.python-os-plugin-path "/Library/Developer/KDKs/KDK_10.13.6_17G65.kdk/System/Library/Kernels/kernel.development.dSYM/Contents/Resources/DWARF/../Python/lldbmacros/core/operating_system.py"
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.13.6_17G65.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.

settings set target.process.optimization-warnings false
(lldb)

Now we can finally connect lldb to the live kernel by writing kdp-remote localhost. If you did everything right, the kernel should connect and you should have an output like this. A LOT of text will start to pour into your lldb window initially, then it should come to a rest state.

(lldb) kdp-remote localhost
Version: Darwin Kernel Version 17.7.0: Wed Oct 10 23:06:14 PDT 2018; root:xnu-4570.71.13~1/DEVELOPMENT_X86_64; UUID=1718D865-98B4-3F6E-97CF-42BF0D02ADD7; stext=0xffffff802e800000
Kernel UUID: 1718D865-98B4-3F6E-97CF-42BF0D02ADD7
Load Address: 0xffffff802e800000
Kernel slid 0x2e600000 in memory.
Loaded kernel file /Library/Developer/KDKs/KDK_10.13.6_17G3025.kdk/System/Library/Kernels/kernel.development
Loading 152 kext modules warning: Can't find binary/dSYM for com.apple.kec.Libm (BC3F7DA4-03EA-30F7-B44A-62C249D51C10)
.warning: Can't find binary/dSYM for com.apple.kec.corecrypto (B081B8C1-1DFF-342F-8DF2-C3AA925ECA3A)
.warning: Can't find binary/dSYM for com.apple.kec.pthread (E64F7A49-CBF0-3251-9F02-3655E3B3DD31)
.warning: Can't find binary/dSYM for com.apple.iokit.IOACPIFamily (95DA39BB-7C39-3742-A2E5-86C555E21D67)
[...]
.Target arch: x86_64
.. done.
Target arch: x86_64
Instantiating threads completely from saved state in memory.
Process 1 stopped
* thread #2: tid = 0x0066, 0xffffff802e97a8d3 kernel.development`DebuggerWithContext [inlined] current_cpu_datap at cpu_data.h:401, name = '0xffffff80486a2338', queue = '0x0', stop reason = signal SIGSTOP
    frame #0: 0xffffff802e97a8d3 kernel.development`DebuggerWithContext [inlined] current_cpu_datap at cpu_data.h:401 [opt]

Now we are connected to the live kernel. You can see that the process is stopped, this means that the kernel is frozen, this is why the boot stopped right where you left it, but now that the debugger has been attached, we can safely continue the boot process into the normal macOS desktop. To do that we just have to unfreeze (continue) the process. To do that, type “c” for continue and press enter until the boot continues (more text is poured on the debuggee screen)

(lldb) c
Process 1 resuming
Process 1 stopped
* thread #2: tid = 0x0066, 0xffffff802e97a8d3 kernel.development`DebuggerWithContext [inlined] current_cpu_datap at cpu_data.h:401, name = '0xffffff80486a2338', queue = '0x0', stop reason = EXC_BREAKPOINT (code=3, subcode=0x0)
    frame #0: 0xffffff802e97a8d3 kernel.development`DebuggerWithContext [inlined] current_cpu_datap at cpu_data.h:401 [opt]
(lldb) c

Once the debuggee has fully booted into the macOS and you are on your desktop, you can pretty much do whatever debugging you want. To run a debugger command you will have to trigger again a NMI, to do that you press the Power button once. The debuggee screen will freeze but your debugger’s lldb screen will be active and you can read/write registers, read/write memory, disassemble at address, disassemble functions, etc. on the live kernel. To unfreeze it back you type again “c” and press enter on the lldb screen.

Practical examples of Kernel debugging

Example 1: Reading all the registers with lldb and writing “AAAAAAAA” to one of them

Okay, to read all the registers, trigger a NMI by pressing the Power button and in the open lldb window type register read --all

(lldb) register read --all
General Purpose Registers:
      rax = 0xffffff802f40ba40  kernel.development`processor_master
      rbx = 0x0000000000000000
      rcx = 0xffffff802f40ba40  kernel.development`processor_master
      rdx = 0x0000000000000000
      rdi = 0x0000000000000004
      rsi = 0xffffff7fb1483ff4
      rbp = 0xffffff817e8ccd50
      rsp = 0xffffff817e8ccd10
       r8 = 0x0000000000000000
       r9 = 0x0000000000000001
      r10 = 0x00000000000004d1
      r11 = 0x00000000000004d0
      r12 = 0x0000000000000000
      r13 = 0x0000000000000000
      r14 = 0x0000000000000000
      r15 = 0xffffff7fb1483ff4
      rip = 0xffffff802e97a8d3  kernel.development`DebuggerWithContext + 403 [inlined] current_cpu_datap at cpu.c:220
 kernel.development`DebuggerWithContext + 403 [inlined] current_processor at debug.c:463
 kernel.development`DebuggerWithContext + 403 [inlined] DebuggerTrapWithState + 46 at debug.c:537
 kernel.development`DebuggerWithContext + 357 at debug.c:537
   rflags = 0x0000000000000046
       cs = 0x0000000000000008
       fs = 0x0000000000000000
       gs = 0x0000000000000000
 
Floating Point Registers:
      fcw = 0x0000
      fsw = 0x0000
      ftw = 0x00
      fop = 0x0000
       ip = 0x00000000
       cs = 0x0000
       dp = 0x00000000
       ds = 0x0000
    mxcsr = 0x00000000
 mxcsrmask = 0x00000000
    stmm0 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm1 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm2 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm3 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm4 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm5 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm6 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm7 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm0 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm1 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm2 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm3 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm4 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm5 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm6 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm7 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm8 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm9 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm10 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm11 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm12 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm13 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm14 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm15 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
 
Exception State Registers:
3 registers were unavailable.
(lldb)

Now let’s write to one of the registers. DO NOT write to a register that is not set to 0x0000000000000000 because you will overwrite something. Find one that is empty. In my case, R13 is empty (r13 = 0x0000000000000000) so I can write garbage to it to prove my point. To write a string of AAAs to the register I can replace it’s value to 0x4141414141414141 where 0x41 is the hex representation for ASCII character “A”. To overwrite the register I can use the command register write r13 0x4141414141414141. Sure enough, if we read the registers again the change is in place:

(lldb) register write R13 0x4141414141414141
(lldb) register read --all
General Purpose Registers:
      rax = 0xffffff802f40ba40  kernel.development`processor_master
      rbx = 0x0000000000000000
      rcx = 0xffffff802f40ba40  kernel.development`processor_master
      rdx = 0x0000000000000000
      rdi = 0x0000000000000004
      rsi = 0xffffff7fb1483ff4
      rbp = 0xffffff817e8ccd50
      rsp = 0xffffff817e8ccd10
       r8 = 0x0000000000000000
       r9 = 0x0000000000000001
      r10 = 0x00000000000004d1
      r11 = 0x00000000000004d0
      r12 = 0x0000000000000000
      r13 = 0x4141414141414141 <-- Yee overwritten this.
      r14 = 0x0000000000000000
      r15 = 0xffffff7fb1483ff4
      rip = 0xffffff802e97a8d3  kernel.development`DebuggerWithContext + 403 [inlined] current_cpu_datap at cpu.c:220
 kernel.development`DebuggerWithContext + 403 [inlined] current_processor at debug.c:463
 kernel.development`DebuggerWithContext + 403 [inlined] DebuggerTrapWithState + 46 at debug.c:537
 kernel.development`DebuggerWithContext + 357 at debug.c:537
   rflags = 0x0000000000000046
       cs = 0x0000000000000008
       fs = 0x0000000000000000
       gs = 0x0000000000000000
 
Floating Point Registers:
      fcw = 0x0000
      fsw = 0x0000
      ftw = 0x00
      fop = 0x0000
       ip = 0x00000000
       cs = 0x0000
       dp = 0x00000000
       ds = 0x0000
    mxcsr = 0x00000000
 mxcsrmask = 0x00000000
    stmm0 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm1 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm2 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm3 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm4 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm5 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm6 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    stmm7 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm0 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm1 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm2 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm3 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm4 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm5 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm6 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm7 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm8 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
     xmm9 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm10 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm11 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm12 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm13 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm14 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
    xmm15 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
 
Exception State Registers:
3 registers were unavailable.
 
(lldb)

NOTE: Of course, when you wanna read a single register you don’t have to run register read --all, you can simply specify the register with register read [register] for example register read r13.

Example 2: Changing the Kernel version and name when running uname -a

Time to do some real memory R/W to the kernel because we can. As you probably know, the command uname -a in the Terminal lists the name of the kernel, the version, the, and the build date. What if we change that to whatever we want?

At first, we have no idea where the kernel stores that information so we need to find that. To do that we can use any Disassembler like IDA Pro, Hopper Disassembler, Jtool, Binary Ninja, etc.

I will use IDA Pro for this task. What we’re going to do is to load the kernel. development file into IDA Pro and let IDA analyze it. The analysis may take a while so please be patient. The Kernel ain’t small. When IDA finishes, the output should look like this, more or less. You will know when IDA finished because it will say “AU: idle” in the left bottom corner.

Now, we have to find that string. We know that the kernel name is Darwin when the uname -a command is executed in Terminal so in order to look for it in IDA, we go to the top bar -> View -> Open subviews -> Strings. A new Strings window will appear and if you press CTRL + F inside it a search box will appear at the bottom where we can search for Darwin. And what do you know? The whole string is there.

Double-click that and you will be redirected to a constant called _version. So now we know. The constant is called “version” and that is what we have to look for. You may be inclined to copy the address of the constant from the IDA disassembly but WRONG! The Kernel uses KASLR or Kernel Address Space Layout Randomization so the address will not be the same, it will be slid. But you don’t need to know the address anyways, you can get it easily with lldb on the debugger machine.

Let’s get the address of the “version” constant.

It’s actually very simple. Trigger a NMI by pressing the Power Button (if you continued the process) and write print &(version).

(lldb) print &(version)
(const char (*)[101]) $8 = 0xffffff802f0f68f0
(lldb)

AHAM! So in my case the const char version is at address 0xffffff802f0f68f0. Sure enough, if we list the character array it shows like this:

(lldb) print version
(const char [101]) $9 = {
  [0] = 'D'
  [1] = 'a'
  [2] = 'r'
  [3] = 'w'
  [4] = 'i'
  [5] = 'n'
  [6] = ' '
  [7] = 'K'
  [8] = 'e'
  [9] = 'r'
  [10] = 'n'
  [11] = 'e'
  [12] = 'l'
  [13] = ' '
  [14] = 'V'
  [15] = 'e'
  [16] = 'r'
  [17] = 's'
  [18] = 'i'
  [19] = 'o'
  [20] = 'n'
  [21] = ' '
  [22] = '1'
  [23] = '7'
  [24] = '.'
  [25] = '7'
  [26] = '.'
  [27] = '0'
  [28] = ':'
  [29] = ' '
  [30] = 'W'
  [31] = 'e'
  [32] = 'd'
  [33] = ' '
  [34] = 'O'
  [35] = 'c'
  [36] = 't'
  [37] = ' '
  [38] = '1'
  [39] = '0'
  [40] = ' '
  [41] = '2'
  [42] = '3'
  [43] = ':'
  [44] = '0'
  [45] = '6'
  [46] = ':'
  [47] = '1'
  [48] = '4'
  [49] = ' '
  [50] = 'P'
  [51] = 'D'
  [52] = 'T'
  [53] = ' '
  [54] = '2'
  [55] = '0'
  [56] = '1'
  [57] = '8'
  [58] = ';'
  [59] = ' '
  [60] = 'r'
  [61] = 'o'
  [62] = 'o'
  [63] = 't'
  [64] = ':'
  [65] = 'x'
  [66] = 'n'
  [67] = 'u'
  [68] = '-'
  [69] = '4'
  [70] = '5'
  [71] = '7'
  [72] = '0'
  [73] = '.'
  [74] = '7'
  [75] = '1'
  [76] = '.'
  [77] = '1'
  [78] = '3'
  [79] = '~'
  [80] = '1'
  [81] = '/'
  [82] = 'D'
  [83] = 'E'
  [84] = 'V'
  [85] = 'E'
  [86] = 'L'
  [87] = 'O'
  [88] = 'P'
  [89] = 'M'
  [90] = 'E'
  [91] = 'N'
  [92] = 'T'
  [93] = '_'
  [94] = 'X'
  [95] = '8'
  [96] = '6'
  [97] = '_'
  [98] = '6'
  [99] = '4'
  [100] = '\0'
}
(lldb)

Actually, using the x <address> command we can dumpt the memory contents at that address. Let’s do it.

(lldb) x 0xffffff802f0f68f0
0xffffff802f0f68f0: 44 61 72 77 69 6e 20 4b 65 72 6e 65 6c 20 56 65  Darwin Kernel Ve
0xffffff802f0f6900: 72 73 69 6f 6e 20 31 37 2e 37 2e 30 3a 20 57 65  rsion 17.7.0: We
(lldb)

It looks like it continues to 0xffffff802f0f6900. Let’s dump that too.

(lldb) x 0xffffff802f0f6900
0xffffff802f0f6900: 65 72 73 69 6f 6e 20 36 39 2e 30 30 20 57 65 65  rsion 17.7.0: We
0xffffff802f0f6910: 64 20 4f 63 74 20 31 30 20 32 33 3a 30 36 3a 31  d Oct 10 23:06:1
(lldb)

Nice! See the 44 61 72 77 69 6e? That is the hexadecimal representation of the word Darwin. If we change that to let’s say “GeoSn0w” in HEX, we can pretty much change the kernel name. Same goes for the version. Let’s do it.

So, we need a Text to Hex converter. Many are available online. I used this one. And we need to keep in mind that we CANNOT write a longer string without overwriting something else. The word can be smaller and we can pad it with NOPs (0x90) but not longer because it will overwrite stuff. I crafted my text to remove some stuff and add some stuff but I stayed in the same boundary. Don’t go past the character limit in the existing string.

My final hex looks like this:

47 65 6f 53 6e 30 77 20 4b 65 72 6e 65 6c 20 56 = "GeoSn0w Kernel V"

65 72 73 69 6f 6e 20 36 39 2e 30 30 20 57 65 65 = "ersion 69.00 Wee"

Now, we cannot write it to the two addresses like that. We have to add “0x” in front of all characters. The final result looks like this:

0x47 0x65 0x6f 0x53 0x6e 0x30 0x77 0x20 0x4b 0x65 0x72 0x6e 0x65 0x6c 0x20 0x56 = "GeoSn0w Kernel V"

0x65 0x72 0x73 0x69 0x6f 0x6e 0x20 0x36 0x39 0x2e 0x30 0x30 0x20 0x57 0x65 0x65 = "ersion 69.00 Wee"

Now we can write the bytes to the memory. Let’s start with the first address. In my case the command looks like this:

(lldb) memory write 0xffffff802f0f68f0 0x47 0x65 0x6f 0x53 0x6e 0x30 0x77 0x20 0x4b 0x65 0x72 0x6e 0x65 0x6c 0x20 0x56
(lldb) x 0xffffff802f0f68f0
0xffffff802f0f68f0: 47 65 6f 53 6e 30 77 20 4b 65 72 6e 65 6c 20 56  GeoSn0w Kernel V
0xffffff802f0f6900: 72 73 69 6f 6e 20 31 37 2e 37 2e 30 3a 20 57 65  rsion 17.7.0: We
(lldb)

Now for the 0xffffff802f0f6900 address to complete the string nicely:

(lldb) memory write 0xffffff802f0f6900 0x65 0x72 0x73 0x69 0x6f 0x6e 0x20 0x36 0x39 0x2e 0x30 0x30 0x20 0x57 0x65 0x65
(lldb) x 0xffffff802f0f6900
0xffffff802f0f6900: 65 72 73 69 6f 6e 20 36 39 2e 30 30 20 57 65 65  ersion 69.00 Wee
0xffffff802f0f6910: 64 20 4f 63 74 20 31 30 20 32 33 3a 30 36 3a 31  d Oct 10 23:06:1
(lldb)

Now let’s unfreeze the kernel on the debugee:

(lldb) c
Process 1 resuming
(lldb) Loading 1 kext modules warning: Can't find binary/dSYM for com.apple.driver.AppleXsanScheme (79D5E92F-789E-3C37-BE0E-7D1EAD697DD9)
. done.
Unloading 1 kext modules . done.
Unloading 1 kext modules . done.
(lldb)

And let’s run the uname -a command in the Terminal of the debugee:

And what do you know? It shows our string:

Last login: Sun Dec  2 07:12:19 on ttys000
Isabella:~ geosn0w$ uname -a
Darwin Isabella.local 17.7.0 GeoSn0w Kernel Version 69.00 Weed Oct 10 23:06:14 PDT 2018; root:xnu-4570.71.13~1/DEVELOPMENT_X86_64 x86_64
Isabella:~ geosn0w$ 

And there you have it. Kernel debugging on macOS with some practical examples. I hope you enjoyed it. Do not forget that, after you’re done debugging stuff you should set the boot-args back again to stock to boot the normal RELEASE kernel. You do that by running the following command in Terminal on the debugee: sudo nvram boot-args="". Then go to /System/Library/Kernels/ and remove the kernel.development file.

Isabella:~ geosn0w$ sudo nvram boot-args=""
Password:
Isabella:~ geosn0w$ 

Now in the Terminal write the following two commands to invalidate the kextcache:

sudo touch /Library/Extensions

And

sudo touch /System/Library/Extensions

After this, perform a reboot and the computer will boot the normal RELEASE kernel.

Errare humanum est

If you found any horrible mistakes in this write-up, let me know on Twitter. My handle is @FCE365 (GeoSn0w). Thank you a lot for reading this!

Contact me

  • Twitter: https://twitter/FCE365
  • YouTube Channel: http://youtube.com/fce365official
Written on December 2, 2018 by GeoSn0w (@FCE365)