10 Feb 2021 @ModernPwner dropped a surprise on the iOS jailbreak community by sharing an
open source PoC1 of a kernel-level LPE exploit, dubbed cicuta_virosa, based on a vulnerability patched only recently in iOS 14.4. Here I’ll share how I adapted the PoC to work iPhone X on iOS 14.0.1.

Credits

First, I’d like to thank @ModernPwner for sharing the exploit PoC, @_simo36 for releasing great Ghidra tools and information on reverse engineering iOS kernelcache2, and @Morpheus for jtool23 and the incredible *OS Internals book series4.

Update 02-14 - prior work

Offsets I found as a part of this exercise had also been previously discovered by @XsF1re.
@halo_michael opened a pull request to add those and more to the PoC, so you can try it directly thanks to their contribution. I wasn’t aware of that at the time of writing, and would like to also thank
@janseredynski for letting me know!

Trying out the PoC

When the exploit dropped, people in my Twitter timeline started sharing logs of gaining root access using the PoC, so I was eager to try it out myself. I grabbed the sources from github and as I browsed through some of the code in cicuta_virosa.c I noticed the following part:

1
2
3
4
5
6
...
cicuta_log("Established custom r/w primitives!");
cicuta_log("Stage 4 (DEMO): pwn kernel");

// offsets is hardcoded for A12-14!!! Change it for your device!!!
...

My testing device is iPhone X with the A11 chip, so it seemed like I would need to adapt parts of the code to make it work. I still went ahead and tried running it, but as expected, the device has restarted, and I didn’t get to see the satisfying whoami: root log entry.

There were only a few more lines of code following the offsets comment, and further stripping some implementation details, it was relatively straightforward to figure out what was going on.

1
2
3
4
5
6
7
  uint64_t task_pac = buf[0];
  ...
  uint64_t proc_pac = read_64(task + 0x3A0);
  ...
  uint64_t ucred_pac = read_64(proc + 0xf0);
  ...
  cicuta_log("Overwriting kernel credentials :)");

The snippet above is accessing internal kernel structures to overwrite the effective process privileges. The offsets are 0x3A0 and 0xF0, and as they point to fields of internal kernel structures, they can differ between devices and iOS versions.

What offsets?

I’m not intimately familiar with iOS exploits, but knowing XNU has multiple personalities with the Mach and BSD layers, I intuitively connected task and proc identifiers from the PoC sources with the Mach task abstraction and BSD processes.

It became clear what I had to do was figure out what are the offsets of the relevant kernel structure fields for my device. My first step in doing that was to get XNU sources for macOS5. I knew that even if I find the exact structures, they will be most likely different from iOS, but I wanted to use it as a starting point. So I just went straight in started grepping for some keywords related to task, proc and cred, and after a bit of searching, found the information I was looking for:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
                       osfmk/kern/task.h 
          ┌──────────────────────┐       
               struct proc             
          ├──────────────────────┤       
       ┌─▶│ void* task           │──┐    
          kauth_cred_t p_ucred       
                                     
         └──────────────────────┘      
         ┌──────────────────────┐      
              struct task            
         ├──────────────────────┤      
       └──│ void* bsd_info       │◀─┘    
                                       
                                       
          └──────────────────────┘       
bsd/sys/proc_internal.h                  

It wasn’t immediately evident that bsd_info field was the proc structure, but that quickly became clear after further jumping through some XNU code and finding tons of places with
(struct proc*)task->bsd_info type of casts. As I grepped through the code, I also tried to take notice of some APIs that were accessing the specific fields as those could be useful later on trying to find the offsets.

Matching source with iOS kernel

Earlier I mentioned I used macOS XNU sources as an entry point and didn’t expect to use them out of the box. That became even more apparent after seeing how the structures were defined, where some fields were configurable with macros. The next step was to match what I found so far in XNU sources with actual kernel code on iOS.

I knew already iOS uses a single kernelcache file where the core kernel code and kernel extensions are located, so I started looking online for some resources on how to get my hands on those and how they can be reversed. I quickly came across an awesome Ghidra framework2 for reverse engineering iOS kernel cache. The framework provides a straightforward usage guide and integrates jtool23 kernelcache symbolication feature so that some kernel symbols are directly visible in Ghidra.

Now that I had the kernelcache disassembled with some symbol locations, my plan of attack was to look into macOS XNU sources for the symbols that jtool2 located and find which ones are accessing directly or are using APIs that access the task.bsd_info and proc_p_ucred fields. Some of my entry points were getter-like APIs such as get_bsdtask_info or kauth_cred_proc_ref, and larger functions that directly accessed the fields.

It took a bit of time to find my way around all the different APIs and effectively guessing at what to look at next, and I stumbled into a trap. Some symbols marked by jtool2 were, what it seemed like, located incorrectly as Ghidra control flow analysis didn’t match the number of function arguments that I saw in XNU sources, and functions logic looked entirely different in several places. One such tricky example was that I had two marked symbols, task_for_pid_posix_check and mac_execv, where the latter called the former, but that didn’t match the macOS source code. Initially, I thought maybe there were some larger differences between iOS and macOS implementation, but then I noticed the symbolication wasn’t totally right.

The disassembled code in Ghidra was:

1
2
3
4
5
6
7
8
// bsd/kern/kern_exec.c

ulong ___mac_execve(long param_1,undefined8 *param_2,undefined8 param_3) {
  ...
  ...
  iVar5 = _task_for_pid_posix_check
          (lVar13,"com.apple.developer.kernel.extended-virtual-addressing");
}

While in XNU source code the extended virtual addressing entitlement was used as an argument of a call to IOTaskHasEntitlement, called indirectly by __mac_execve, like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// bsd/kern/kern_exec.c

static inline void
proc_apply_jit_and_jumbo_va_policies(proc_t p, task_t task)
{
  ...
  IOTaskHasEntitlement(task, "com.apple.developer.kernel.extended-virtual-addressing")
  ...
}

int
__mac_execve(proc_t p, struct __mac_execve_args *uap, int32_t *retval) {
  ...
  proc_apply_jit_and_jumbo_va_policies(p, ...);
  ...
}

In the end, however, I was on the right track. IOTaskHasEntitlement first argument was the task pointer, so I followed in XNU sources what exactly it was doing, and I ended up quickly finding IOUserClient::copyClientEntitlements(task_t task) function, which at the start used the get_bsdtask_info API. I followed the same path in Ghidra starting from the incorrectly marked symbol task_for_pid_posix_check and found the bsd_info offset to be 0x390.

mac_execv also called the other symbol often used through the XNU codebase kauth_cred_proc_ref, and while it wasn’t symbolicated by jtool2 it was the first function called in mac_execv. In Ghidra I saw what I suspected to be kauth_cred_proc_ref return a value at offset 0xf0 from the first (struct proc*) parameter, which is also the offset used in the PoC.

Last step was to try newly found bsd_info offset, and it was a success. 🎉

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Established custom r/w primitives!
Stage 4 (DEMO): pwn kernel
task PAC: 0xffffffe1a3aecc50
PAC decrypt: 0xffffffe1a3aecc50 -> 0xffffffe1a3aecc50
proc PAC: 0xffffffe1a3adb240
PAC decrypt: 0xffffffe1a3adb240 -> 0xffffffe1a3adb240
ucred PAC: 0xffffffe19e387a80
PAC decrypt: 0xffffffe19e387a80 -> 0xffffffe19e387a80
Overwriting kernel credentials :)
getuid() returns 0
whoami: root

The only change required to the PoC was the bsd_info field offset:

1
2
-    uint64_t proc_pac = read_64(task + 0x3A0);
+    uint64_t proc_pac = read_64(task + 0x390);

The idea of exploits and their general complexity can be intimidating, but it is a fun exercise, and there’s no need to fully grasp them to make some additions and learn new things. In the future, I hope to write more about iOS vulnerabilities and jailbreak related subjects.


Thanks for reading! If you’ve got any questions, comments, or general feedback, you can find all my social links at the bottom of the page.


  1. cicuta_virosa ↩︎

  2. Ghidra kernelcache framework by @_simo36 ↩︎

  3. jtool2 by @Morpheus ↩︎

  4. *OS Internals by @Morpheus ↩︎

  5. macOS XNU sources ↩︎