# BabyKitDriver
## Description
Pwnable, 7 solved, 647 Pts.
qemu+macOS ventura 13.6.1
upload your exploit binary and the server will run it for you, you can get the output of your exploit.
the flag is at /flag
Just pwn my first IOKit Driver!
nc baby-kit-driver.ctf.0ops.sjtu.cn 20001
[attachment](https://github.com/mephi42/ctf/tree/master/2023.12.09-0CTF_TCTF_2023/BabyKitDriver/BabyKitDriver.kext_D5ED88B9E517723BF57C28E742D3AE49.zip)
## Summary
macOS 13.6.1 kernel module pwn. Solve quite an expensive PoW, send a binary, andget its output back.
## TL;DR
* Install [OSX-KVM](https://github.com/kholia/OSX-KVM) using the [offline]( https://github.com/kholia/OSX-KVM/blob/master/run_offline.md) method.* Boot with `./OpenCore-Boot.sh`.  * Add `-s` to `OpenCore-Boot.sh` for enabling the [qemu gdbstub]( https://wiki.qemu.org/Features/gdbstub) for kernel debugging.  * If dropped to the [UEFI shell]( https://chriswayg.gitbook.io/opencore-visual-beginners-guide/advanced-topics/opencore-uefi-shell    ), do:    ```    fs0:    cd efi/boot    bootx64.efi    ```* Use [docker-osxcross](https://github.com/crazy-max/docker-osxcross) for  [compiling the exploit](https://github.com/mephi42/ctf/tree/master/2023.12.09-0CTF_TCTF_2023/BabyKitDriver/Makefile).* Build [gdb](https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git) with  `--target=x86_64-apple-darwin13` for kernel debugging.* [Enable SSH](https://support.apple.com/lt-lt/guide/mac-help/mchlp1066/mac).* Copy and load the vulnerable kernel module with `kextload`. The first time an  approval via GUI and a reboot are needed.* Copy the kernel from `/System/Library/Kernels/kernel` to the host, load it  into the favorite decompiler. The kernel sources can be found [here]( https://github.com/apple-oss-distributions/xnu).* Check `/Library/Logs/DiagnosticReports/Kernel-*.panic` for panic messages.* After gdb is attached, the VM is normally stopped in [`machine_idle()`]( https://github.com/apple-oss-distributions/xnu/blob/xnu-10002.41.9/osfmk/i386/pmCPU.c#L175  ). This makes it possible to [infer the KASLR base](https://github.com/mephi42/ctf/tree/master/2023.12.09-0CTF_TCTF_2023/BabyKitDriver/gdbscript) for debugging  purposes.* Breakpoints work reliably. Single-stepping is only possible with `stepi`.  Other commands, like `nexti`, more often than not, cause gdb to lose control.  When using `si`, disable interrupts by clearing IF using  `set $eflags=$eflags&~0x200`, and set it back before `continue`.* The kernel module allows reading and writing a buffer, which is stored in the  following format:  ```  union message {    struct {      void (*output)(void *dest, const void *src);      char buf[0x100];    } v1;    struct v2 {      size_t size;      void (*output)(void *dest, const void *src, size_t size);      char buf[0x200];    };    char raw[0x300];  };  ```* Leak the kernel stack contents by reading -1 bytes from a v2 buffer.* Obtain the RIP + RSI + *RSI control by racing reading vs the v2 -> v1  transition. Stabilize the race by storing the *RSI payload in advance.* Pivot stack to RSI using JOP.* Disable SMEP/SMAP and jump to the userspace shellcode using ROP.* Get root by calling `proc_update_label()` and `kauth_cred_setresuid()`. The  creds are stored in read-only pages, so a direct write is not an option.* Restore the original RSP by scanning for leaked data starting at  `current_cpu_datap()->cpu_kernel_stack`. Tweak a few other registers and  values in memory, and return to the kernel module.* Try reading the flag, retry the race on failure.* `flag{7ac21f7848a39f0aea63fa29d304226a}`
## Setup
### OS
As the description states, the challenge runs in QEMU, so it should be possibleto recreate the remote setup on the local Linux host. OSX-KVM turned up insearch and was quite usable. The installation steps from the front page workedlike a charm, but resulted in macOS 13.5.2, which was not what the challengedescription said. Using `softwareupdate -i 'macOS Ventura 13.6.1-22G313'`failed with `MSU_ERR_STAGE_SPLAT_FAILED` with no further explanations. Theattempt to do a manual upgrade by downloading the image with [`./macrecovery.pydownload -b Mac-B4831CEBD52A0C4C -m 00000000000000000`](https://github.com/acidanthera/OpenCorePkg/tree/0.9.7/Utilities/macrecovery)bricked the system. The offline installer worked, though.
Booting the system with `./OpenCore-Boot.sh` sometimes dropped me to the scaryUEFI shell instead of the nice graphical menu. To get to the menu, I needed torun `fs0:/efi/boot/bootx64.efi` manually.
### Building
It should be possible to build the exploit natively, but I preferred to use thehost as much as possible, so I used the `crazymax/osxcross:13.1-ubuntu` dockerimage with the cross-compilers and the SDK. Somewhat counter-intuitively, theimage comes only with the cross-toolchain and nothing else. So I had to createa [`Dockerfile`](image/Dockerfile) that added the base system.
### Testing
Enabling SSH in the guest helped create automation for the other steps, such asloading the kernel module, uploading and running the exploit, and reading crashlogs, which I put into the [Makefile](https://github.com/mephi42/ctf/tree/master/2023.12.09-0CTF_TCTF_2023/BabyKitDriver/Makefile). The port forwarding wasalready set up in `OpenCore-Boot.sh`, so I only had to upload my public keyand put the following into `~/.ssh/config` on the host:
```Host OSX-KVM        Hostname localhost        Port 2222        IdentityFile ~/.ssh/id_ecdsa        IdentitiesOnly yes```
### Debugging
The kernel is stored in `/System/Library/Kernels/kernel` inside the guest andcan be easily copied to the host with `scp`. There are symbols inside it, so Ihoped I could use it with GDB. Unfortunately, my distro `gdb-multiarch` didn'tsupport Mach-O binaries, [Apple GDB](https://opensource.apple.com/source/gdb/)could not be built on Linux, and building GDB from source for the`x86_64-apple-darwin` target did not produce a GDB binary. After staring at theGDB's `configure` script for a while, I realized that I had to target`x86_64-apple-darwin13`! The resulting GDB accepted the kernel, but did notload any symbols anyway, so I had to proceed with debugging the raw asm.
Knowing the randomized kernel base is important for debugging. It can be easilycalculated using the observation that attaching the debugger almost alwaysinterrupts the VM at the `*fc6: cli` instruction, which can be then found inthe kernel binary like this:```$ llvm-objdump -d kernel|grep fc6.*cliffffff80004dafc6: fa                   	cli```
The randomized kernel module base is also important. The `kextstat-b keen.BabyKitDriver` command shows the non-randomized base, which then needsto be adjusted by the value of `vm_kernel_slide`. Yes, kernel and modules usethe same slide value.
With that, it's possible to put breakpoints on the kernel and on the module.
## Analysis
As the description states, the module is written using the IOKit framework.That's the first time I've seen it; the [old Apple docs](https://developer.apple.com/library/archive/documentation/DeviceDrivers/Conceptual/IOKitFundamentals/Introduction/Introduction.html) read like marketing materials, and the [new Apple docs](https://developer.apple.com/documentation/kernel/iokit_fundamentals) are quiteconcise, but in general it's not that different from Windows or Linux driverframeworks.
The driver provides the following two `externalMethods`, which are quitesimilar to ioctls:
```kern_return_t BabyKitDriverUserClient::baby_read(void *ref, IOExternalMethodArguments *args) {  BabyKitDriver *drv;  kern_return_t ret;  char buf_v1[256];  char buf_v2[512];  size_t size;  bool is_v2;
  drv = (BabyKitDriver *)getProvider();  is_v2 = drv->is_v2;  IOLog("BabyKitDriverUserClient::baby_read\n");  IOLog("version:%lld\n", is_v2);  if (!drv->message)    return 0;  if (is_v2) {    size = args->scalarInput[1];    if ((int64_t)size > (int64_t)drv->message->v2.size)      size = drv->message->v2.size;    memset(buf_v2, 0, sizeof(buf_v2));    drv->message->v2.output(buf_v2, drv->message->v2.buf, drv->message->v2.size);    ret = copyout(buf_v2, args->scalarInput[0], (size - 1) & 0xFFF);  } else {    memset(buf_v1, 0, sizeof(buf_v1));    drv->message->v1.output(buf_v1, &drv->message->v1.buf);    ret = copyout(buf_v1, args->scalarInput[0], 0x100);  }  return ret;}
kern_return_t BabyKitDriverUserClient::baby_leaveMessage(void *ref, IOExternalMethodArguments *args) {  BabyKitDriver *drv;  kern_return_t ret;  __int64 is_v2;  size_t size;
  drv = (BabyKitDriver *)getProvider();  is_v2 = args->scalarInput[0];  IOLog("BabyKitDriverUserClient::baby_leaveMessage\n");  if (!drv->message) {    drv->message = (struct message *)IOMalloc(sizeof(struct message));    if (is_v2)      drv->message->v2.output = output2;    else      drv->message->v1.output = output1;  }  if (is_v2) {    drv->message->v2.output = output2;    size = args->scalarInput[2];    if ((int64_t)size > 0x200)      size = 0x200LL;    drv->message->v2.size = size;    ret = copyin(args->scalarInput[1], &drv->message->v2.buf, size);  } else {    drv->message->v1.output = output1;    ret = copyin(args->scalarInput[1], &drv->message->v1.buf, 0x100uLL);  }  drv->is_v2 = is_v2;  return ret;}```
One vulnerability stands out, since there is no good reason for doing`(size - 1) & 0xFFF`. It draws attention to the `(int64_t)size >(int64_t)drv->message->v2.size` comparison, which also has no good reason to besigned. And indeed, storing an empty v2 message and then reading -1 bytes leaks0xFFF bytes of stack.
A similar problem exists for writing v2 messages.
Finally, the code does not use synchronization, so there are various dataraces.
## Exploitation
### Breaking KASLR
In the `baby_read(-1)` leak, one can find the address for returning to the`is_io_connect_method()` at a fixed offset, which makes it possible to computethe kernel base.
### Controlling RIP
It's tempting to do `baby_leaveMessage(v2, -1)`, making `copyout()` write asmany bytes as we want to the heap - arranging it to stop by putting theuserspace buffer right before a `PROT_NONE` page. Unfortunately, `copyout()`[panics](https://github.com/apple-oss-distributions/xnu/blob/xnu-10002.41.9/osfmk/x86_64/copyio.c#L182) on large sizes, downgrading this bug to a mere DoS.
Race, on the other hand, is quite viable. The goal is to overwrite `v2.output`with a controlled value by racing reads with writes that switch from v1 to v2:
```writer                               reader-----------------------------------  ----------------------------------------------                                     if (is_v2) {                                     ...                                     memset(buf_v2, 0, sizeof(buf_v2));ret = copyin(args->scalarInput[1],             &drv->message->v1.buf,             0x100uLL);                                     drv->message->v2.output(buf_v2,                                                             drv->message->v2.buf,                                                             drv->message->v2.size);```
Missing the race is not dangerous in most situations, except when the writerthread updates `drv->message->v1.output` and stalls. In this case the readerthread will use a very large size, causing a panic. But the probability of thishappening doesn't seem to be significant enough for a CTF challenge.
As a result, we can jump anywhere, with RDI (`buf_v2`) pointing to stack, andRSI (`drv->message->v2.buf`) pointing to up to 0x200 bytes of controlled data.
### Pivoting stack
Since we control the data at RSI, we can perform JOP. It's hard to do manyuseful things with JOP alone, so we need to convert it to ROP like this:
```gadget_1: add rsi, 0x58 ; call raxgadget_2: mov rsp, rsi  ; call rdi```
Unfortunately there are not enough gadgets for setting up RAX and RDI that donot conflict with each other. So we need to go for an even simpler chain thatloads a constant into RSI, and for that we need to leak the message address,which is as easy as using a single `mov [rdi+8], rsi; ret` gadget.
With that, we can build a JOP chain that [pivots to RSI + 0x70](https://github.com/mephi42/ctf/tree/master/2023.12.09-0CTF_TCTF_2023/BabyKitDriver/pwnit.c#L238).
### Executing shellcode
It should be possible to achieve privilege escalation with ROP alone, but Ideemed it to be too hard to debug, therefore I wanted to go for jumping touserspace shellcode that would do all the heavy lifting. The [chain](pwnit.c#L270) for that is quite simple: we only need to disable SMEP and SMAPin CR4, for which the good gadgets exist.
I've decided to write as much [shellcode](https://github.com/mephi42/ctf/tree/master/2023.12.09-0CTF_TCTF_2023/BabyKitDriver/pwnit.c#L147) in C as possible. Ineeded assembly only for [switching](https://github.com/mephi42/ctf/tree/master/2023.12.09-0CTF_TCTF_2023/BabyKitDriver/pwnit.c#L172) to the statically allocatedstack in order to avoid damaging the kernel heap, and then for [switching](pwnit.c#L159) back to the kernel stack.
### Escalating privileges
While kernel functions cannot be called from shellcode directly, since it'snot linked with the kernel, calculating their addresses from the known kernelbase and casting them to strongly typed function pointers is easy. The kernelbase is stored by the userspace portion of the exploit in a global variable,and this variable is available from the shellcode.
The kernel allocates credentials in read-only memory, so they cannot be updateddirectly. Instead, there are a couple kernel functions that need to be used:[`proc_update_label()`](https://github.com/apple-oss-distributions/xnu/blob/xnu-8796.101.5/bsd/kern/kern_proc.c#L1813) and [`kauth_cred_setresuid()`](https://github.com/apple-oss-distributions/xnu/blob/xnu-8796.101.5/bsd/kern/kern_credential.c#L3924). The usage example can be found in the [`setuid()` implementation](https://github.com/apple-oss-distributions/xnu/blob/xnu-8796.101.5/bsd/kern/kern_prot.c#L679).
One difficulty is that `proc_update_label()` takes a closure as an argument.For the purpose of making one in the exploit, a closure is just an array of3 pointers, the last one being a function pointer.
With that, we can safely set all kinds of uids to 0.
### Resuming the kernel module
Now we need to return to userspace. There is `return_to_user()` for that,however, using it directly results in all sorts of nasty consequences.
For instance, IOKit takes some lock, which is then not released. There is asanity check that causes a panic if `current_thread()->rwlock_count` is not 0when returning to userspace. It can be fooled by manually decrementing thatvalue, but of course this causes a deadlock sooner or later.
Therefore, we better return back to the kernel module. For that, the exploitneeds to switch back to the kernel stack. We've lost the original value whenpivoting, so we need to calculate it. The kernel stack base is stored in`current_cpu_datap()->cpu_kernel_stack`. We can then scan it for known pointersthat have to be there, such as the return address to `is_io_connect_method()`,and subtract a fixed offset from that.
We also need to restore a handful of values that were damaged by the exploit:* RBP, which is at the fixed offset from RSP.* The saved length, which was set to a very large value by the race, and needs  to be changed to something more reasonable for `copyout()` to not panic.* R14, which is used by `is_io_connect_method()` and has to be 0.
With that, we happily return to userspace as root, and can read the flag and/orspawn the shell.
# Conclusion
That was quite an interesting introduction to the macOS kernel pwn. Definitelynot the easiest possible, but at least no knowledge of heap feng-shui wasrequired.
OSX-KVM + GDB provides a reasonable debugging environment, which couldnevertheless be improved, first and foremost, by adding or fixing the GDBsupport for parsing Mach-O symbols.
The amount of learning materials on the web is limited, comparing to the Linuxkernel pwn, but still enough for getting started. The availability of the XNUkernel source code is a godsend. I found the following links especially useful:
* https://www.usenix.org/system/files/conference/woot17/woot17-paper-xu.pdf* https://github.com/A2nkF/macOS-Kernel-Exploit
The macOS kernel mitigations and sanity checks are somewhat different fromthose in the Linux kernel, but the challenge author was merciful enough to giveus the tools to circumvent them with reasonable effort.
Overall, this was quite hard as a first macOS kernel pwn experience - it tookme about 20 hours to solve it (including install and reinstall times) - but I'mstill glad I looked into it.