C Programming for Kernel Exploitation
The XNU kernel is written in C. Understanding C at the memory layout level, not just at the syntax level, is a mandatory requirement.
Why You Need Deep C Knowledge
- Kernel source code (XNU) is C – you will read it daily
- Exploit development requires precise understanding of the binary layout of structs, unions, and pointers
- Heap exploitation depends on understanding memory allocator behavior
- Type confusion exploits leverage differences in how the compiler lays out structs
Key Knowledge
1. Pointers & Memory
// Pointer arithmetic — each +1 jumps sizeof(*ptr) bytes
uint64_t *p = (uint64_t *)0x1000;
p + 1; // = 0x1008, NOT 0x1001
// Void pointer — no type info, requires manual cast
void *raw = malloc(64);
uint32_t val = *(uint32_t *)((char *)raw + 0x10); // read 4 bytes at offset 0x10
// Function pointer — the foundation for vtable exploitation
typedef int (*handler_fn)(void *ctx, int cmd);
handler_fn fn = (handler_fn)0xFFFFF00001234000; // assume a kernel address
fn(ctx, 0); // indirect call
// Double pointer — commonly seen in kernel APIs
kern_return_t task_for_pid(mach_port_t target, int pid, mach_port_t *task);
// task is an output parameter — the kernel writes to *task
2. Struct Layout & Padding
// The compiler adds padding to align fields
struct example {
uint8_t a; // offset 0x00, size 1
// 7 bytes padding
uint64_t b; // offset 0x08, size 8
uint32_t c; // offset 0x10, size 4
uint16_t d; // offset 0x14, size 2
// 2 bytes padding
}; // total size = 0x18 (24 bytes)
// Packed struct — no padding (used in protocol/file format parsing)
struct __attribute__((packed)) header {
uint8_t magic; // offset 0x00
uint64_t size; // offset 0x01 (unaligned!)
}; // total size = 9 bytes
Why this matters:
- Exploits need to know the exact offset of each field in a kernel struct
- Type confusion exploits replace object A with object B – field X of A must be at the same offset as field Y of B
offsetof(struct, field)gives you the exact offset
3. Union – Type Punning
// Unions allow interpreting the same memory in multiple ways
union isa_t {
uintptr_t bits;
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
// ...
};
};
// Used in exploitation to reinterpret kernel objects
union {
uint64_t raw;
struct {
uint32_t lo;
uint32_t hi;
};
} kaddr;
kaddr.raw = 0xFFFFF00012345678;
// kaddr.lo = 0x12345678, kaddr.hi = 0xFFFFF000
4. Bitwise Operations
// Masking — extract specific bits
uint64_t addr = ptr & 0x0000FFFFFFFFFFFF; // strip PAC bits (upper 16)
uint64_t page = addr & ~0x3FFF; // page-align (16KB pages on iOS)
uint64_t offset = addr & 0x3FFF; // offset within page
// Setting/clearing flags
uint32_t flags = pte;
flags |= PTE_VALID; // set bit
flags &= ~PTE_READ_ONLY; // clear bit
flags ^= PTE_ACCESSED; // toggle bit
// Checking flags
if (pte & PTE_VALID) { ... } // check single bit
if ((pte & MASK) == EXPECTED) { ... } // check bit pattern
// Shifts — building addresses, extracting fields
uint64_t phys = (pte & 0x0000FFFFFFFFF000ULL); // extract physical address from PTE
uint64_t ppn = phys >> 14; // physical page number (16KB pages)
5. Memory Management Patterns in the Kernel
// Zone allocation (XNU pattern)
zone_t my_zone = zone_create("my_objects", sizeof(struct my_obj), ZC_NONE);
struct my_obj *obj = zalloc(my_zone);
zfree(my_zone, obj);
// BUG: obj still points to freed memory → UAF
// Kalloc (general purpose)
void *buf = kalloc(256); // allocated from kalloc.256 zone
kfree(buf, 256); // must pass the correct size!
// Reference counting pattern
void obj_retain(struct obj *o) { os_ref_retain(&o->ref_count); }
void obj_release(struct obj *o) {
if (os_ref_release(&o->ref_count) == 0)
obj_free(o); // BUG if someone still holds a pointer
}
// Mach port lifecycle — common exploitation target
struct ipc_port {
struct ipc_object ip_object; // header
struct ipc_mqueue ip_messages; // message queue
union {
struct ipc_kmsg *data;
struct task *task; // for task ports
struct semaphore *sema;
} kdata;
// ...
};
6. Volatile & Memory Barriers
// Volatile — compiler does not optimize away reads/writes
volatile uint32_t *mmio_reg = (volatile uint32_t *)0x200000000;
uint32_t val = *mmio_reg; // actually reads the hardware register
// Memory barriers — important for multi-core exploitation
__asm__ volatile("dmb sy" ::: "memory"); // Data Memory Barrier
__asm__ volatile("dsb sy" ::: "memory"); // Data Synchronization Barrier
__asm__ volatile("isb" ::: "memory"); // Instruction Synchronization Barrier
XNU Kernel Source Code Patterns to Recognize
// Kernel function error handling pattern
kern_return_t
some_kernel_function(args...)
{
kern_return_t kr = KERN_SUCCESS;
// ... validation ...
if (bad_input) {
kr = KERN_INVALID_ARGUMENT;
goto out;
}
// ... do work ...
out:
// cleanup
return kr;
}
// IOKit C++ pattern (restricted C++)
class IOSurfaceRootUserClient : public IOUserClient {
virtual IOReturn externalMethod(uint32_t selector,
IOExternalMethodArguments *args,
IOExternalMethodDispatch *dispatch,
OSObject *target,
void *reference) override;
};
Resources
- K&R – The C Programming Language (classic, old but provides a solid foundation)
- Expert C Programming: Deep C Secrets – Peter van der Linden
- XNU Source Code – read directly
- Compiler Explorer (godbolt.org) – see C compiled to ARM64 assembly
Exercises
- Manually compute offsets for 5 different structs with mixed types, verify with
offsetof() - Write type-punning code using unions to extract fields from a 64-bit packed value
- Implement a simple zone allocator in C (fixed-size allocations, freelist)
- Read XNU source: open
osfmk/kern/ipc_kobject.cand trace 1 function call path - Compile C to ARM64, read the assembly output, map each C statement to the corresponding instructions