Compute iOS XNU offset from kernel cache

Gaining kernel read/write access is just the beginning. This guide details how to identify critical offsets within the XNU Kernelcache to fully leverage iOS vulnerabilities.
You Have a Kernel Read/Write. Not Enough! How to Extract Offsets from XNU Kernelcaches
Foreword
Opa334 recently shared a kernel read and write primitive which is similar to the one used in DarkSword malware. I found that it was a perfect occasion for me to try to make it run on one of my testing devices and actually get my hands dirty with kernel exploration. We always hear about kernel exploitation, but rarely get to walk through what it looks like in practice.
Once you have read and write primitives to the kernel, the first step is to read backward until you find the magic number aka the Mach-O binary signature:
uint64_t magic = early_kread64(kernel_base); if (magic == 0x100000cfeedfacf) { printf("[DEBUG] Found Mach-O magic at 0x%llx!\n", kernel_base);
Then you can compute the kernel slide and you are good to go.
I won't detail this, but feel free to check the blog post of MATTEYEUX on DarkSword.
Now the next difficulty is to find the offsets between this magic value and the kernel objects in memory. It is exactly what this post is about.
Introduction
Kernelcaches extracted from IPSW files come without symbols: just raw ARM64 code. Yet, the internal layout of every kernel data structure is recoverable if you know where to look.
Note: you can use blacktop/symbolicator to recover some symbols and make your life easier.
It reminds me of this sentence from J. Levin in the DisARM book: "In fact, the whole premise of the command line tools I demonstrate is to avoid having to use a debugger."
This guide documents a repeatable methodology for extracting struct offsets from stripped kernelcaches. The techniques here were validated against iOS 16.7.12 (iPhone X, build 20H364) using Binary Ninja.
Prerequisites
- A disassembler with decompiler support (Binary Ninja, IDA Pro + Hex-Rays, or Ghidra)
- A decrypted kernelcache (I used
ipswfor extraction) - The XNU open-source release for the closest matching version
The Core Principle
The key insight behind this entire methodology is that functions like proc_pid(), vnode_mount(), or kauth_cred_getuid() are wrappers that read the field from a struct. When decompiled, they directly reveal the field's offset.
A stripped kernelcache still retains the names of these exported functions.
Phase 1: Cross-Referencing with XNU Source
The XNU kernel source is partially open. While the iOS build may differ from the published source, the struct layouts are usually very close. Use the source as a map, not as ground truth.
- Identify the field name from the accessor function name (e.g.,
_proc_pid→p_pid) - Find the struct definition in the XNU source (e.g.,
bsd/sys/proc_internal.h) - Use the source to predict which fields exist and roughly where they are
- Verify each prediction against the actual binary
Apple frequently adds, removes, or reorders fields between iOS versions. Never assume the open-source layout matches exactly. The source tells you what fields exist; the binary tells you where they are.
Phase 2: Finding Anchor Points
Global variables like allproc, kernproc, and nprocs are stored in the __DATA segment. They are referenced by functions via adrp/ldr instruction pairs. Finding these gives you entry points into the kernel's data structures from a known address.
ARM64 uses page-relative addressing to load these global variables. The offsets between globals remain constant even with KASLR.
Phase 3: Use accessor functions
Search for functions whose names follow the pattern <struct>_<field> or <struct>_get<field>. These are almost always thin accessors.
How to recognize an accessor
An accessor function decompiles to essentially one operation:
return *(arg1 + 0x60);
Or in ARM64 assembly:
ldr w0, [x0, #0x60]
ret
That single load instruction tells you: struct proc has p_pid at offset +0x60, and it is a 32-bit integer.
Phase 4: Iterator and Constructor Functions
When accessor functions do not exist for a field, look at functions that iterate or construct instances of the struct. These functions touch many fields and reveal the overall layout.
Functions named *_iterate, *_foreach, or *_walk traverse linked lists of kernel objects. They reveal the global head pointer, the list entry offset, and various field accesses used for filtering.
Source: Hacker News















