From 3f263a9f628511df36f429706cc8140fd61ba16b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Neusch=C3=A4fer?= Date: Wed, 29 Oct 2025 09:23:41 +0100 Subject: [PATCH 01/14] Fix a few typos --- include/xnuspy/xnuspy_ctl.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/xnuspy/xnuspy_ctl.h b/include/xnuspy/xnuspy_ctl.h index 5489f67..488945d 100644 --- a/include/xnuspy/xnuspy_ctl.h +++ b/include/xnuspy/xnuspy_ctl.h @@ -34,7 +34,7 @@ enum { COPYINSTR, COPYOUT, - /* Idential to XNU's implementation */ + /* Identical to XNU's implementation */ CURRENT_MAP, CURRENT_PROC, From 063f9f18d9711278ee52bff6c8a996620430bdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Neusch=C3=A4fer?= Date: Wed, 29 Oct 2025 09:24:09 +0100 Subject: [PATCH 02/14] klog: Fix potential out-of-bounds write If read returns the full buffer size, then buf[r] = '\0' will write one byte past the buffer. Prevent this by reading at most sizeof(buf)-1 bytes. --- klog/klog.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/klog/klog.c b/klog/klog.c index 355d73a..f651061 100644 --- a/klog/klog.c +++ b/klog/klog.c @@ -28,7 +28,7 @@ int main(int argc, char **argv){ char buf[1024]; memset(buf, 0, sizeof(buf)); - ssize_t r = read(klog_fd, buf, sizeof(buf)); + ssize_t r = read(klog_fd, buf, sizeof(buf) - 1); if(r < 0){ printf("read failed: %s\n", strerror(errno)); From 90e1cb2afa8e2adf6fb744507bb7bd50ddc8f05e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Neusch=C3=A4fer?= Date: Wed, 29 Oct 2025 20:27:24 +0100 Subject: [PATCH 03/14] loader: Specify C files before LD flags Some linkers require that symbols are used first, and then defined. This resulted in libusb symbols not being found while building on Alpine Linux. --- loader/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loader/Makefile b/loader/Makefile index fd7429f..a714261 100644 --- a/loader/Makefile +++ b/loader/Makefile @@ -11,4 +11,4 @@ TARGET = loader SOURCES = loader.c $(TARGET) : $(SOURCES) - $(CC) $(CFLAGS) $(LDFLAGS) $(SOURCES) -o $(TARGET) + $(CC) $(CFLAGS) $(SOURCES) $(LDFLAGS) -o $(TARGET) From 77da65a3cbbc6d31aad09d9d4de9c1c27a2d8827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Neusch=C3=A4fer?= Date: Thu, 30 Oct 2025 09:32:50 +0100 Subject: [PATCH 04/14] module/pf: Add offline patchfinder (for quicker testing) --- include/pf/pf_common.h | 4 + module/pf/offline/.gitignore | 1 + module/pf/offline/Makefile | 5 + module/pf/offline/offline.c | 235 +++++++++++++++++++++++++++++++++++ 4 files changed, 245 insertions(+) create mode 100644 module/pf/offline/.gitignore create mode 100644 module/pf/offline/Makefile create mode 100644 module/pf/offline/offline.c diff --git a/include/pf/pf_common.h b/include/pf/pf_common.h index ef63625..8896bcb 100644 --- a/include/pf/pf_common.h +++ b/include/pf/pf_common.h @@ -26,6 +26,7 @@ struct pf { #define LISTIZE(...) __VA_ARGS__ +#ifndef PF_DECL32 #define PF_DECL32(name, matches, masks, mmcount, callback, seg) \ { \ .pf_name = name, \ @@ -39,7 +40,9 @@ struct pf { .pf_section = NULL, \ .pf_unused = 0, \ } +#endif +#ifndef PF_DECL_FULL #define PF_DECL_FULL(name, matches, masks, mmcount, access, callback, kext, seg, sect) \ { \ .pf_name = name, \ @@ -53,6 +56,7 @@ struct pf { .pf_section = sect, \ .pf_unused = 0, \ } +#endif #define PF_UNUSED { .pf_unused = 1 } diff --git a/module/pf/offline/.gitignore b/module/pf/offline/.gitignore new file mode 100644 index 0000000..faa62b1 --- /dev/null +++ b/module/pf/offline/.gitignore @@ -0,0 +1 @@ +offline diff --git a/module/pf/offline/Makefile b/module/pf/offline/Makefile new file mode 100644 index 0000000..7035e82 --- /dev/null +++ b/module/pf/offline/Makefile @@ -0,0 +1,5 @@ +CC = clang +CFLAGS = -g -O2 -I../../../include + +offline: offline.c ../../../include/pf/pfs.h + $(CC) $(CFLAGS) offline.c -o offline diff --git a/module/pf/offline/offline.c b/module/pf/offline/offline.c new file mode 100644 index 0000000..1d9aafc --- /dev/null +++ b/module/pf/offline/offline.c @@ -0,0 +1,235 @@ +// Offline patchfinder. +// +// This program exists to test the patchfinder offline (without booting into +// pongoOS), which enables quick iteration on changes and testing with +// different kernel versions. +// +// It is far from perfect, though: The callbacks in module/pf/1*/pf.c are not +// executed, so whatever result you get from pf/offline is only part of the +// story. +// +#include +#include +#include +#include +#include +#include +#include + +#ifndef XNU_PF_ACCESS_32BIT +#define XNU_PF_ACCESS_32BIT 0 +#endif + +#define PF_DECL32(name, matches, masks, mmcount, callback, seg) \ + { \ + .pf_name = name, \ + .pf_matches = matches, \ + .pf_masks = masks, \ + .pf_mmcount = mmcount, \ + .pf_access_type = XNU_PF_ACCESS_32BIT, \ + .pf_callback = NULL /*callback*/, \ + .pf_kext = NULL, \ + .pf_segment = seg, \ + .pf_section = NULL, \ + .pf_unused = 0, \ + } + +#define PF_DECL_FULL(name, matches, masks, mmcount, access, callback, kext, seg, sect) \ + { \ + .pf_name = name, \ + .pf_matches = matches, \ + .pf_masks = masks, \ + .pf_mmcount = mmcount, \ + .pf_access_type = access, \ + .pf_callback = NULL /*callback*/, \ + .pf_kext = kext, \ + .pf_segment = seg, \ + .pf_section = sect, \ + .pf_unused = 0, \ + } + +#include + +uint64_t g_sysent_addr = 0; +uint64_t g_kalloc_canblock_addr = 0; +uint64_t g_kfree_addr_addr = 0; +uint64_t g_sysctl__kern_children_addr = 0; +uint64_t g_sysctl_register_oid_addr = 0; +uint64_t g_sysctl_handle_long_addr = 0; +uint64_t g_name2oid_addr = 0; +uint64_t g_sysctl_geometry_lock_addr = 0; +uint64_t g_lck_rw_done_addr = 0; +uint64_t g_h_s_c_sbn_branch_addr = 0; +uint64_t g_h_s_c_sbn_epilogue_addr = 0; +uint64_t g_lck_grp_alloc_init_addr = 0; +uint64_t g_lck_rw_alloc_init_addr = 0; +uint64_t g_exec_scratch_space_addr = 0; +/* don't count the first opcode */ +uint64_t g_exec_scratch_space_size = -sizeof(uint32_t); +uint32_t *g_ExceptionVectorsBase_stream = NULL; +uint64_t g_bcopy_phys_addr = 0; +uint64_t g_phystokv_addr = 0; +uint64_t g_copyin_addr = 0; +uint64_t g_copyout_addr = 0; +uint64_t g_IOSleep_addr = 0; +uint64_t g_kprintf_addr = 0; +uint64_t g_vm_map_unwire_addr = 0; +uint64_t g_vm_deallocate_addr = 0; +uint64_t g_kernel_map_addr = 0; +uint64_t g_kernel_thread_start_addr = 0; +uint64_t g_thread_deallocate_addr = 0; +uint64_t g_mach_make_memory_entry_64_addr = 0; +uint64_t g_offsetof_struct_thread_map = 0; +uint64_t g_current_proc_addr = 0; +uint64_t g_proc_list_lock_addr = 0; +uint64_t g_proc_ref_locked_addr = 0; +uint64_t g_proc_list_mlock_addr = 0; +uint64_t g_lck_mtx_lock_addr = 0; +uint64_t g_lck_mtx_unlock_addr = 0; +uint64_t g_proc_rele_locked_addr = 0; +uint64_t g_proc_uniqueid_addr = 0; +uint64_t g_proc_pid_addr = 0; +uint64_t g_allproc_addr = 0; +uint64_t g_lck_rw_lock_shared_addr = 0; +uint64_t g_lck_rw_lock_shared_to_exclusive_addr = 0; +uint64_t g_lck_rw_lock_exclusive_addr = 0; +uint64_t g_vm_map_wire_external_addr = 0; +uint64_t g_mach_vm_map_external_addr = 0; + +/* Only for <14.5 */ +uint64_t g_ipc_port_release_send_addr = 0; + +/* Only for >=14.5 */ +uint64_t g_ipc_port_release_send_and_unlock_addr = 0; + +uint64_t g_lck_rw_free_addr = 0; +uint64_t g_lck_grp_free_addr = 0; +int g_patched_doprnt_hide_pointers = 0; +uint64_t g_copyinstr_addr = 0; +uint64_t g_thread_terminate_addr = 0; +int g_patched_pinst_set_tcr = 0; +int g_patched_all_msr_tcr_el1_x18 = 0; +uint64_t g_snprintf_addr = 0; +uint64_t g_strlen_addr = 0; +uint64_t g_proc_name_addr = 0; +uint64_t g_strncmp_addr = 0; +uint64_t g_memset_addr = 0; +uint64_t g_memmove_addr = 0; +uint64_t g_panic_addr = 0; +uint64_t g_mach_to_bsd_errno_addr = 0; +uint64_t g_xnuspy_sysctl_mib_ptr = 0; +uint64_t g_xnuspy_sysctl_mib_count_ptr = 0; +uint64_t g_xnuspy_ctl_callnum = 0; + +/* Only for >=14.5 && <15.0 */ +uint64_t g_io_lock_addr = 0; + +uint64_t g_vm_allocate_external_addr = 0; +uint64_t g_vm_map_deallocate_addr = 0; +uint64_t g_offsetof_struct_vm_map_refcnt = 0; +uint64_t g_IOLog_addr = 0; + + +struct mapping { + void *data; + size_t size; +}; + +struct mapping *map_file(const char *path) +{ + struct mapping *m = malloc(sizeof(*m)); + if (!m) return NULL; + + int fd = open(path, O_RDONLY); + if (fd < 0) return NULL; + + struct stat statbuf; + int res = fstat(fd, &statbuf); + if (res < 0) return NULL; + + m->size = statbuf.st_size; + m->data = mmap(NULL, (m->size + 0xffff) & ~0xffff, PROT_READ, MAP_PRIVATE, fd, 0); + if (m->data == MAP_FAILED) return NULL; + + close(fd); + return m; +} + +bool find_version(struct mapping *m, int *major, int *minor, int *patch) +{ + const char *needle = "Darwin Kernel Version "; + + void *eureka = memmem(m->data, m->size, needle, strlen(needle)); + if (!eureka) return false; + + sscanf(eureka + strlen(needle), "%d.%d.%d", major, minor, patch); + return major != 0; +} + +int main(int argc, char **argv) +{ + if (argc != 2) { + fprintf(stderr, "Usage: %s kernel\n", argv[0]); + exit(1); + } + + struct mapping *m = map_file(argv[1]); + if (!m) { + fprintf(stderr, "Failed to open %s\n", argv[1]); + exit(1); + } + + int major, minor, patchlevel; + if (!find_version(m, &major, &minor, &patchlevel)) { + fprintf(stderr, "Failed to find kernel version\n"); + exit(1); + } + + printf("[!] Kernel version %d.%d.%d\n", major, minor, patchlevel); + int version; + switch (major) { + case 19: version = 0; break; + case 20: version = 1; break; + case 21: version = 2; break; + default: + fprintf(stderr, "Unsupported kernel version\n"); + exit(1); + } + + + for (size_t pf_idx = 0; pf_idx < MAXPF; pf_idx++) { + struct pf *pf = &g_all_pfs[pf_idx][version]; + if (!pf->pf_name) + continue; + + printf("[?] %s\n", pf->pf_name); + //for (int i = 0; i < pf->pf_mmcount; i++) + // printf("[.] %08lx %08lx\n", pf->pf_matches[i], pf->pf_masks[i]); + + int matches = 0; + bool matched = false; + const uint32_t *code = m->data; + for (size_t k = 0; k < m->size / sizeof(uint32_t); k++) { + if ((code[k] & pf->pf_masks[matches]) == pf->pf_matches[matches]) { + if (++matches == pf->pf_mmcount) { + // Print fake address that's kind of works in the kernel's __TEXT segment + printf("[+] > %016llx ->", k * sizeof(uint32_t) + 0xfffffff007003ff4ull); + for (int i = 0; i < 4; i++) + printf(" %08x", code[k - (matches-1) + i]); + printf("\n"); + + matched = true; + matches = 0; + } + } else { + matches = 0; + } + } + + if (!matched) + printf("[-] === NOPE ===\n"); + + } + + return 0; +} From 64104e1778c079ec234c20fd3768f2b0ab46d4ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Neusch=C3=A4fer?= Date: Thu, 30 Oct 2025 09:40:01 +0100 Subject: [PATCH 05/14] pf/15: Port lck_rw_alloc_init to iOS 15.4+ The old pattern broke with 15.4, the new callside (in OSSymbol::initialize) is stable across all iOS 15.x versions. --- include/pf/pfs.h | 12 +++++++----- module/pf/15/pf.c | 20 ++++---------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/include/pf/pfs.h b/include/pf/pfs.h index 02cbf5a..15eca30 100644 --- a/include/pf/pfs.h +++ b/include/pf/pfs.h @@ -589,16 +589,18 @@ struct pf g_all_pfs[MAXPF][NUM_SUPPORTED_VERSIONS] = { 7, lck_rw_alloc_init_finder_14, "__TEXT_EXEC"), PF_DECL_FULL("lck_rw_alloc_init finder iOS 15", LISTIZE({ - 0xd2800016, /* mov x22, #0 */ - 0xf9400260, /* ldr x0, [x19] */ - 0xd2800001, /* mov x1, #0 */ + 0xf9400000, /* ldr x0, [xn, n] */ + 0xd2800001, /* mov x1, #0 */ + 0x94000000, /* bl _lck_rw_alloc_init */ + 0xf9000e60, /* str x0, [x19, #0x18] */ }), LISTIZE({ + 0xffc0001f, /* ignore all but Rt */ 0xffffffff, /* match exactly */ - 0xffffffff, /* match exactly */ + 0xfc000000, /* ignore branch target */ 0xffffffff, /* match exactly */ }), - 3, XNU_PF_ACCESS_32BIT, lck_rw_alloc_init_finder_15, + 4, XNU_PF_ACCESS_32BIT, lck_rw_alloc_init_finder_15, "com.apple.filesystems.lifs", "__TEXT_EXEC", NULL), }, { diff --git a/module/pf/15/pf.c b/module/pf/15/pf.c index d3144d2..8c2fc0a 100644 --- a/module/pf/15/pf.c +++ b/module/pf/15/pf.c @@ -222,26 +222,14 @@ bool proc_ref_rele_finder_15(xnu_pf_patch_t *patch, void *cacheable_stream){ return true; } -/* Confirmed working 15.0 */ +/* Confirmed working 15.0 - 15.8 */ bool lck_rw_alloc_init_finder_15(xnu_pf_patch_t *patch, void *cacheable_stream){ - /* We landed inside lifs_req_hashtbl_init. The branch to - * lck_rw_alloc_init is three instructions down if we see a - * lsl w8, w0, #1 less than 20 instructions before where we are */ - uint32_t *opcode_stream = cacheable_stream; - uint32_t *saved_stream = opcode_stream; - uint32_t limit = 20; - - while(*opcode_stream != 0x531f7808){ - if(limit-- == 0) - return false; - - opcode_stream--; - } - + /* We landed inside OSSymbol::initialize. */ xnu_pf_disable_patch(patch); - uint32_t *lck_rw_alloc_init = get_branch_dst_ptr(saved_stream + 3); + uint32_t *opcode_stream = cacheable_stream; + uint32_t *lck_rw_alloc_init = get_branch_dst_ptr(opcode_stream + 2); g_lck_rw_alloc_init_addr = xnu_ptr_to_va(lck_rw_alloc_init); From 6f33e439905666621fd2831b43fb67142d8d650b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Neusch=C3=A4fer?= Date: Thu, 30 Oct 2025 10:19:34 +0100 Subject: [PATCH 06/14] pf/15: Port kalloc_external finder to 15.4+ In 15.4, the first instruction changed to "add x25, x23, #8", so ignore the destination register there. For good measure, match on the allocation size (0x400), though. --- include/pf/pfs.h | 8 +++++--- module/pf/14/pf.c | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/include/pf/pfs.h b/include/pf/pfs.h index 15eca30..2c58546 100644 --- a/include/pf/pfs.h +++ b/include/pf/pfs.h @@ -93,16 +93,18 @@ struct pf g_all_pfs[MAXPF][NUM_SUPPORTED_VERSIONS] = { "__TEXT_EXEC", NULL), PF_DECL_FULL("kalloc_external finder iOS 15", LISTIZE({ - 0x910022f7, /* add x23, x23, #0x8 */ + 0x910022e0, /* add xNN, x23, #0x8 */ 0x910043a8, /* add x8, x29, #0x10 */ 0xf9000fe8, /* str w8, [sp, #0x18] */ + 0x52808008, /* mov w8, #0x400 */ }), LISTIZE({ + 0xffffffe0, /* ignore Rd */ 0xffffffff, /* match exactly */ - 0xffffffff, /* match exactly */ + 0xffffffff, /* ignore offset */ 0xffffffff, /* match exactly */ }), - 3, XNU_PF_ACCESS_32BIT, kalloc_external_finder_14, + 4, XNU_PF_ACCESS_32BIT, kalloc_external_finder_14, "com.apple.driver.AppleMobileFileIntegrity", "__TEXT_EXEC", NULL), }, diff --git a/module/pf/14/pf.c b/module/pf/14/pf.c index adba232..a6a76ff 100644 --- a/module/pf/14/pf.c +++ b/module/pf/14/pf.c @@ -12,7 +12,7 @@ uint64_t g_kalloc_external_addr = 0; uint64_t g_kfree_ext_addr = 0; -/* Confirmed working 14.0 - 15.0 */ +/* Confirmed working 14.0 - 15.8 */ bool kalloc_external_finder_14(xnu_pf_patch_t *patch, void *cacheable_stream){ /* We've landed somewhere inside AMFI, kalloc_external is the * branch six instructions down */ From c9fb4c29638d962f3ee0193e110703ac516a70db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Neusch=C3=A4fer?= Date: Thu, 30 Oct 2025 11:02:32 +0100 Subject: [PATCH 07/14] pf/15: Port kernel_map finder to 15.1+ The old finder broke with 15.1, but the new one is stable across all 15.x versions. --- include/pf/pfs.h | 20 ++++++++++++++------ module/pf/15/pf.c | 26 +++++++++++--------------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/include/pf/pfs.h b/include/pf/pfs.h index 2c58546..2484279 100644 --- a/include/pf/pfs.h +++ b/include/pf/pfs.h @@ -1065,18 +1065,26 @@ struct pf g_all_pfs[MAXPF][NUM_SUPPORTED_VERSIONS] = { PF_UNUSED, PF_DECL32("kernel_map finder iOS 15", LISTIZE({ - 0x94000000, /* bl n */ + 0xf9400340, /* ldr x0, [x26] */ + 0x94000000, /* bl _vm_map_page_mask */ 0xaa0003f4, /* mov x20, x0 */ - 0x0, /* ignore this instruction */ - 0x9ba87c00, /* umull Xn, w0, w8 */ + 0xf9400340, /* ldr x0, [x26] */ + 0x94000000, /* bl _vm_map_page_mask */ + 0xaa0003f9, /* mov x25, x0 */ + 0xf9400340, /* ldr x0, [x26] */ + 0x94000000, /* bl _vm_map_page_mask */ }), LISTIZE({ + 0xffffffff, /* match exactly */ 0xfc000000, /* ignore immediate */ 0xffffffff, /* match exactly */ - 0x0, /* ignore this instruction */ - 0xffffffe0, /* ignore Rd */ + 0xffffffff, /* match exactly */ + 0xfc000000, /* ignore immediate */ + 0xffffffff, /* match exactly */ + 0xffffffff, /* match exactly */ + 0xfc000000, /* ignore immediate */ }), - 4, kernel_map_finder_15, "__TEXT_EXEC"), + 8, kernel_map_finder_15, "__TEXT_EXEC"), }, { PF_UNUSED, diff --git a/module/pf/15/pf.c b/module/pf/15/pf.c index 8c2fc0a..2d02516 100644 --- a/module/pf/15/pf.c +++ b/module/pf/15/pf.c @@ -107,26 +107,22 @@ bool vm_map_unwire_nested_finder_15(xnu_pf_patch_t *patch, return true; } -/* Confirmed working 15.0 */ +/* Confirmed working 15.0-15.8 */ bool kernel_map_finder_15(xnu_pf_patch_t *patch, void *cacheable_stream){ - /* Will land in panic_kernel, the first PC relative addressing pair - * we see from this point on is for kernel_map */ + /* Will at a sequence of four calls to _vm_map_page_mask. + * Just prior to the calls, _kernel_map is loaded into x0: + * adrp x26, 0xfffffff006d4e000 + * ldr x26, [x26, #0x460] + * ldr x0,[x26] + */ uint32_t *opcode_stream = cacheable_stream; - uint32_t limit = 50; - - /* adrp or adr */ - while((*opcode_stream & 0x1f000000) != 0x10000000){ - if(limit-- == 0) - return false; - - opcode_stream++; - } - xnu_pf_disable_patch(patch); - uint64_t *kernel_mapp = (uint64_t *)get_pc_rel_target(opcode_stream); + /* get constant pool entry */ + uint64_t *kernel_mapp = (uint64_t *)get_pc_rel_target(opcode_stream - 2); - g_kernel_map_addr = xnu_ptr_to_va(kernel_mapp); + /* get address in constant pool entry (already a kernel virtual address) */ + g_kernel_map_addr = *kernel_mapp + kernel_slide; puts("xnuspy: found kernel_map"); From c3955b656691cc42522222a39efcdcd0760d8d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Neusch=C3=A4fer?= Date: Thu, 30 Oct 2025 11:06:21 +0100 Subject: [PATCH 08/14] pf/15: Port current_proc finder to 15.4+ The previous code pattern disappeared in 15.4, this pattern is stable across all 15.x versions. --- include/pf/pfs.h | 23 ++++++++++++++++------- module/pf/15/pf.c | 7 +++---- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/include/pf/pfs.h b/include/pf/pfs.h index 2484279..10fe348 100644 --- a/include/pf/pfs.h +++ b/include/pf/pfs.h @@ -1314,18 +1314,27 @@ struct pf g_all_pfs[MAXPF][NUM_SUPPORTED_VERSIONS] = { PF_UNUSED, PF_DECL32("current_proc finder iOS 15", LISTIZE({ - 0x39402a88, /* ldrb w8, [x20, #0xa] */ - 0x35000008, /* cbnz w8, n */ - 0x94000000, /* bl current_proc */ - 0xf9000e80, /* str x0, [x20, #0x18] */ + 0xaa0003e0, /* mov x0, xN */ + 0xd2800001, /* mov x1, #0 */ + 0xd2800002, /* mov x2, #0 */ + 0x52800043, /* mov w3, #2 */ + 0x94000000, /* bl n */ + 0xf9400000, /* ldr x0, [Xn, n] */ + 0x94000000, /* bl n */ + 0x94000000, /* bl n */ + }), LISTIZE({ + 0xffe0ffff, /* ignore Rm */ 0xffffffff, /* match exactly */ - 0xffc0001f, /* ignore signed offset */ - 0xfc000000, /* ignore immediate */ 0xffffffff, /* match exactly */ + 0xffffffff, /* match exactly */ + 0xfc000000, /* ignore immediate */ + 0xffc0001f, /* ignore Rn & immediate */ + 0xfc000000, /* ignore immediate */ + 0xfc000000, /* ignore immediate */ }), - 4, current_proc_finder_15, "__TEXT_EXEC"), + 8, current_proc_finder_15, "__TEXT_EXEC"), }, { PF_DECL_FULL("proc stuff finder 1 iOS 13", diff --git a/module/pf/15/pf.c b/module/pf/15/pf.c index 2d02516..bb161aa 100644 --- a/module/pf/15/pf.c +++ b/module/pf/15/pf.c @@ -68,15 +68,14 @@ bool proc_name_snprintf_strlen_finder_15(xnu_pf_patch_t *patch, return true; } -/* Confirmed working 15.0 */ +/* Confirmed working 15.0-15.8 */ bool current_proc_finder_15(xnu_pf_patch_t *patch, void *cacheable_stream){ - /* This matches four places inside _eval, all of which have a branch - * to current_proc two instructions down */ + /* The next instruction after the pattern is a call to current_proc */ xnu_pf_disable_patch(patch); uint32_t *opcode_stream = cacheable_stream; - uint32_t *current_proc = get_branch_dst_ptr(opcode_stream + 2); + uint32_t *current_proc = get_branch_dst_ptr(opcode_stream + 8); g_current_proc_addr = xnu_ptr_to_va(current_proc); From c42293a7a73c3d2d7451fd7cb4316a8a94d2b1dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Neusch=C3=A4fer?= Date: Thu, 30 Oct 2025 11:43:15 +0100 Subject: [PATCH 09/14] pf/15: Port vm_allocate_external finder to 15.4+ --- include/pf/15/pf.h | 1 + include/pf/pfs.h | 22 ++++++++++++++++++---- module/pf/15/pf.c | 16 ++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/include/pf/15/pf.h b/include/pf/15/pf.h index 11770d9..39f4ba3 100644 --- a/include/pf/15/pf.h +++ b/include/pf/15/pf.h @@ -15,5 +15,6 @@ bool proc_list_mlock_lck_mtx_lock_unlock_finder_15(xnu_pf_patch_t *, void *); bool lck_grp_free_finder_15(xnu_pf_patch_t *, void *); bool proc_ref_rele_finder_15(xnu_pf_patch_t *, void *); bool lck_rw_alloc_init_finder_15(xnu_pf_patch_t *, void *); +bool vm_allocate_external_finder_15(xnu_pf_patch_t *, void *); #endif diff --git a/include/pf/pfs.h b/include/pf/pfs.h index 10fe348..2547fad 100644 --- a/include/pf/pfs.h +++ b/include/pf/pfs.h @@ -2267,16 +2267,30 @@ struct pf g_all_pfs[MAXPF][NUM_SUPPORTED_VERSIONS] = { 0xfc000000, /* ignore immediate */ }), 2, vm_allocate_external_finder_13, "__TEXT_EXEC"), - PF_DECL32("vm_allocate_external finder iOS 15", + PF_DECL_FULL("vm_allocate_external finder iOS 15", LISTIZE({ - 0x53187C64, /* lsr w4, w3, #0x18 */ - 0x14000000, /* b n */ + 0x2a2003e8, /* mvn w8, w0 */ + 0x93407d08, /* sxtw x8, w8 */ + 0x8a170117, /* and x23, x8, x23 */ + 0xf9400340, /* ldr x0, [x26] */ + 0xd10163a1, /* sub x1, x29, #0x58 */ + 0xaa1703e2, /* mov x2, x23 */ + 0x52800023, /* mov w3, #1 */ + 0x94000000, /* bl vm_allocate */ }), LISTIZE({ + 0xffffffff, /* match exactly */ + 0xffffffff, /* match exactly */ + 0xffffffff, /* match exactly */ + 0xffffffff, /* match exactly */ + 0xffffffff, /* match exactly */ + 0xffffffff, /* match exactly */ 0xffffffff, /* match exactly */ 0xfc000000, /* ignore immediate */ }), - 2, vm_allocate_external_finder_13, "__TEXT_EXEC"), + 8, XNU_PF_ACCESS_32BIT, vm_allocate_external_finder_15, + "com.apple.iokit.IONetworkingFamily", + "__TEXT_EXEC", NULL), }, { PF_DECL32("vm_map_deallocate, offsetof(vm_map_t, refcnt) finder iOS 13", diff --git a/module/pf/15/pf.c b/module/pf/15/pf.c index bb161aa..32b913b 100644 --- a/module/pf/15/pf.c +++ b/module/pf/15/pf.c @@ -232,3 +232,19 @@ bool lck_rw_alloc_init_finder_15(xnu_pf_patch_t *patch, return true; } + +/* Confirmed working 15.0 - 15.8 */ +bool vm_allocate_external_finder_15(xnu_pf_patch_t *patch, + void *cacheable_stream){ + /* We landed somewhere in IONetworkingFamily */ + xnu_pf_disable_patch(patch); + + uint32_t *opcode_stream = cacheable_stream; + uint32_t *vm_allocate = get_branch_dst_ptr(opcode_stream + 7); + + g_vm_allocate_external_addr = xnu_ptr_to_va(vm_allocate); + + puts("xnuspy: found vm_allocate_external"); + + return true; +} From b99b7b6a7c5569de54fb5f118b1da70e11ee5ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Neusch=C3=A4fer?= Date: Thu, 30 Oct 2025 15:58:48 +0100 Subject: [PATCH 10/14] pf/15: Port proc_ref,proc_rele finder to 15.4+ The previous pattern broke with iOS 15.4, but the new pattern works on all versions 15.x. --- include/pf/pfs.h | 18 ++++++++++++++---- module/pf/15/pf.c | 10 +++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/include/pf/pfs.h b/include/pf/pfs.h index 2547fad..e970dfb 100644 --- a/include/pf/pfs.h +++ b/include/pf/pfs.h @@ -2388,16 +2388,26 @@ struct pf g_all_pfs[MAXPF][NUM_SUPPORTED_VERSIONS] = { PF_UNUSED, PF_DECL32("proc_ref,proc_rele finder iOS 15", LISTIZE({ - 0xaa1903f8, /* mov x24, x25 */ - 0xaa1803e0, /* mov x0, x24 */ - 0x52800021, /* mov w1, #1 */ + 0xaa1403e0, /* mov x0, x20 */ + 0x94000000, /* bl _proc_rele */ + 0xf9400eb5, /* ldr x21, [x21, #0x18] */ + 0xb4000000, /* cbz x21, n */ + 0xf94012a8, /* ldr x8, [x21, #0x20] */ + 0x9356fd08, /* asr x8, x8, #22 */ + 0xf9400d14, /* ldr x20, [x8, #0x18] */ + 0xaa1403e0, /* mov x0, x20 */ }), LISTIZE({ + 0xffffffff, /* match exactly */ + 0xfc000000, /* ignore immediate */ + 0xffffffff, /* match exactly */ + 0xfc000000, /* ignore immediate */ + 0xffffffff, /* match exactly */ 0xffffffff, /* match exactly */ 0xffffffff, /* match exactly */ 0xffffffff, /* match exactly */ }), - 3, proc_ref_rele_finder_15, "__TEXT_EXEC"), + 8, proc_ref_rele_finder_15, "__TEXT_EXEC"), }, { PF_DECL_FULL("lck_mtx_lock finder iOS 13", diff --git a/module/pf/15/pf.c b/module/pf/15/pf.c index 32b913b..2018d1c 100644 --- a/module/pf/15/pf.c +++ b/module/pf/15/pf.c @@ -197,16 +197,16 @@ bool lck_grp_free_finder_15(xnu_pf_patch_t *patch, void *cacheable_stream){ return true; } -/* Confirmed working 15.0 */ +/* Confirmed working 15.0 - 15.8*/ bool proc_ref_rele_finder_15(xnu_pf_patch_t *patch, void *cacheable_stream){ - /* We landed inside proc_rebootscan. A call to proc_ref is three - * instructions down and a call to proc_rele is 14 instructions down */ + /* We landed inside memorystatus_update_vm_pressure, at one of two + * equivalent pieces of code, and use the first one we find. */ xnu_pf_disable_patch(patch); uint32_t *opcode_stream = cacheable_stream; - uint32_t *proc_ref = get_branch_dst_ptr(opcode_stream + 3); - uint32_t *proc_rele = get_branch_dst_ptr(opcode_stream + 14); + uint32_t *proc_ref = get_branch_dst_ptr(opcode_stream + 9); + uint32_t *proc_rele = get_branch_dst_ptr(opcode_stream + 1); g_proc_ref_addr = xnu_ptr_to_va(proc_ref); g_proc_rele_addr = xnu_ptr_to_va(proc_rele); From aa30792f11e97db9c949c635f508723dbc82d568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Neusch=C3=A4fer?= Date: Thu, 30 Oct 2025 16:21:25 +0100 Subject: [PATCH 11/14] pf/15: Port vm_map_t->refcnt finder to 15.5+ The code in vm_map_deallocate changed slightly in iOS 15.5, but the updated finder should work with all versions of 15.x. --- module/pf/13/pf.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/module/pf/13/pf.c b/module/pf/13/pf.c index 54747a1..e9c2d63 100644 --- a/module/pf/13/pf.c +++ b/module/pf/13/pf.c @@ -1302,7 +1302,7 @@ bool vm_allocate_external_finder_13(xnu_pf_patch_t *patch, return true; } -/* Confirmed working on all kernels 13.0 - 15.0 */ +/* Confirmed working on all kernels 13.0 - 15.8 */ bool vm_map_deallocate_offsetof_vm_map_refcnt_finder_13(xnu_pf_patch_t *patch, void *cacheable_stream){ /* vm_map_reference does not exist on release kernels because it was @@ -1318,11 +1318,14 @@ bool vm_map_deallocate_offsetof_vm_map_refcnt_finder_13(xnu_pf_patch_t *patch, g_vm_map_deallocate_addr = xnu_ptr_to_va(vm_map_deallocate); - /* Now get the offset of the reference count. Searching - * for add xn, x19, #n */ + /* Now get the offset of the reference count. Searching for + * add x0, xn, #n + * ldxr w8, [x0] + */ uint32_t instr_limit = 100; - while((*vm_map_deallocate & 0xffc003e0) != 0x91000260){ + while((vm_map_deallocate[0] & 0xffc0001f) != 0x91000000 && + (vm_map_deallocate[1] & 0xffffffff) != 0x885f7c08){ if(instr_limit-- == 0) return false; From 335c053de2fb51fd4cde5baab641a4b00a5efc79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Neusch=C3=A4fer?= Date: Thu, 30 Oct 2025 16:45:58 +0100 Subject: [PATCH 12/14] pf/15: Port ipc_port_release_send finder to 15.4+ In 15.4, the register allocation changed slightly, so detect both variants. --- include/pf/pfs.h | 12 ++++++------ module/pf/15/pf.c | 12 ++++++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/include/pf/pfs.h b/include/pf/pfs.h index e970dfb..8a4be9b 100644 --- a/include/pf/pfs.h +++ b/include/pf/pfs.h @@ -1627,21 +1627,21 @@ struct pf g_all_pfs[MAXPF][NUM_SUPPORTED_VERSIONS] = { 5, ipc_port_release_send_finder_13, "__TEXT_EXEC"), PF_DECL32("ipc_object_lock/ipc_port_release_send_and_unlock finder iOS 15", LISTIZE({ - 0x910006e8, /* add x8, x23, #1 */ + 0x91000408, /* add x8, xn, #1 */ 0xf100091f, /* cmp x8, 2 */ 0x540000a3, /* b.lo 0x1c */ - 0xaa1703e0, /* mov x0, x23 */ + 0xaa0003e0, /* mov x0, xn */ 0x94000000, /* bl _ipc_object_lock */ - 0xaa1703e0, /* mov x0, x23 */ + 0xaa0003e0, /* mov x0, xn */ 0x94000000 /* bl _ipc_port_release_send_and_unlock */ }), LISTIZE({ + 0xfffffc1f, /* ignore Rn */ 0xffffffff, /* match exactly */ 0xffffffff, /* match exactly */ - 0xffffffff, /* match exactly */ - 0xffffffff, /* match exactly */ + 0xffe0ffff, /* ignore Rd */ 0xfc000000, /* ignore immediate */ - 0xffffffff, /* match exactly */ + 0xffe0ffff, /* ignore Rd */ 0xfc000000, /* ignore immediate */ }), 7, ipc_port_release_send_finder_15, "__TEXT_EXEC"), diff --git a/module/pf/15/pf.c b/module/pf/15/pf.c index 2018d1c..1bee448 100644 --- a/module/pf/15/pf.c +++ b/module/pf/15/pf.c @@ -14,7 +14,7 @@ uint64_t g_proc_ref_addr = 0; uint64_t g_proc_rele_addr = 0; uint64_t g_ipc_object_lock_addr = 0; -/* Confirmed working 15.0 */ +/* Confirmed working 15.0 - 15.8 */ bool ipc_port_release_send_finder_15(xnu_pf_patch_t *patch, void *cacheable_stream){ /* will land in _exception_deliver in iOS 15. There is a sequence @@ -23,11 +23,15 @@ bool ipc_port_release_send_finder_15(xnu_pf_patch_t *patch, * resolving the branches. We get about 26 hits for these matches * and masks, so let's make sure we're actually in _exception_deliver. * If we are, then the two instructions behind where we landed will be - * mov x27, #0 and mov x26, x0 */ + * (15.0 - 15.3) (15.4 - 15.8) + * mov x26, x0 or mov x27, x0 + * mov x27, #0 mov x26, #0 + */ uint32_t *opcode_stream = cacheable_stream; - if(opcode_stream[-1] != 0xd280001b && opcode_stream[-2] != 0xaa0003fa) - return false; + if (opcode_stream[-2] == 0xaa0003fa && opcode_stream[-1] == 0xd280001b) {} + else if(opcode_stream[-2] == 0xaa0003fb && opcode_stream[-1] == 0xd280001a) {} + else return false; xnu_pf_disable_patch(patch); From 4b2946211bdc19aa5d9cdaf690ab8d71ec39040a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Neusch=C3=A4fer?= Date: Thu, 6 Nov 2025 13:52:04 +0100 Subject: [PATCH 13/14] pf/15: Use more reliable pattern for ipc_port_release_send --- include/pf/pfs.h | 26 ++++++++++++++------------ module/pf/15/pf.c | 21 ++++----------------- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/include/pf/pfs.h b/include/pf/pfs.h index 8a4be9b..860f3c4 100644 --- a/include/pf/pfs.h +++ b/include/pf/pfs.h @@ -1627,24 +1627,26 @@ struct pf g_all_pfs[MAXPF][NUM_SUPPORTED_VERSIONS] = { 5, ipc_port_release_send_finder_13, "__TEXT_EXEC"), PF_DECL32("ipc_object_lock/ipc_port_release_send_and_unlock finder iOS 15", LISTIZE({ - 0x91000408, /* add x8, xn, #1 */ - 0xf100091f, /* cmp x8, 2 */ - 0x540000a3, /* b.lo 0x1c */ - 0xaa0003e0, /* mov x0, xn */ - 0x94000000, /* bl _ipc_object_lock */ - 0xaa0003e0, /* mov x0, xn */ - 0x94000000 /* bl _ipc_port_release_send_and_unlock */ + 0xaa0003f3, /* mov x19, x0 */ + 0x94000000, /* bl ipc_object_lock */ + 0xaa1303e0, /* mov x0, x19 */ + 0x94000000, /* bl ipc_port_release_send_and_unlock */ + 0xa9417bfd, /* ldp x29, x30, [sp, #0x10] */ + 0xa8c24ff4, /* ldp x20, x19, [sp], #0x20 */ + 0x52800000, /* mov w0, #0 */ + 0xd65f03c0, /* ret */ }), LISTIZE({ - 0xfffffc1f, /* ignore Rn */ - 0xffffffff, /* match exactly */ 0xffffffff, /* match exactly */ - 0xffe0ffff, /* ignore Rd */ 0xfc000000, /* ignore immediate */ - 0xffe0ffff, /* ignore Rd */ + 0xffffffff, /* match exactly */ 0xfc000000, /* ignore immediate */ + 0xffffffff, /* match exactly */ + 0xffffffff, /* match exactly */ + 0xffffffff, /* match exactly */ + 0xffffffff, /* match exactly */ }), - 7, ipc_port_release_send_finder_15, "__TEXT_EXEC"), + 8, ipc_port_release_send_finder_15, "__TEXT_EXEC"), }, { PF_DECL32("lck_rw_free finder iOS 13", diff --git a/module/pf/15/pf.c b/module/pf/15/pf.c index 1bee448..af766c1 100644 --- a/module/pf/15/pf.c +++ b/module/pf/15/pf.c @@ -17,26 +17,13 @@ uint64_t g_ipc_object_lock_addr = 0; /* Confirmed working 15.0 - 15.8 */ bool ipc_port_release_send_finder_15(xnu_pf_patch_t *patch, void *cacheable_stream){ - /* will land in _exception_deliver in iOS 15. There is a sequence - * where they lock/release 4 IPC ports if they are non-null. This - * patchfinder will take us here, then it's just a matter of - * resolving the branches. We get about 26 hits for these matches - * and masks, so let's make sure we're actually in _exception_deliver. - * If we are, then the two instructions behind where we landed will be - * (15.0 - 15.3) (15.4 - 15.8) - * mov x26, x0 or mov x27, x0 - * mov x27, #0 mov x26, #0 - */ + /* We land in IOUserClient::releaseNotificationPort, which does almost + * nothing but call ipc_object_lock and ipc_port_release_send_and_unlock */ uint32_t *opcode_stream = cacheable_stream; - - if (opcode_stream[-2] == 0xaa0003fa && opcode_stream[-1] == 0xd280001b) {} - else if(opcode_stream[-2] == 0xaa0003fb && opcode_stream[-1] == 0xd280001a) {} - else return false; - xnu_pf_disable_patch(patch); - uint32_t *ipc_port_release_send_and_unlock = get_branch_dst_ptr(opcode_stream + 6); - uint32_t *ipc_object_lock = get_branch_dst_ptr(opcode_stream + 4); + uint32_t *ipc_object_lock = get_branch_dst_ptr(opcode_stream + 1); + uint32_t *ipc_port_release_send_and_unlock = get_branch_dst_ptr(opcode_stream + 3); g_ipc_port_release_send_and_unlock_addr = xnu_ptr_to_va(ipc_port_release_send_and_unlock); g_ipc_object_lock_addr = xnu_ptr_to_va(ipc_object_lock); From 4ea9f8903c0642eca402fa129f2cf158b6b3c809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=2E=20Neusch=C3=A4fer?= Date: Thu, 30 Oct 2025 18:23:03 +0100 Subject: [PATCH 14/14] pf/15: Port vm_dealloc finder to 15.5+ Sometime after iOS 15.0, two additional arguments were added to the function called at the detected location, both set to zero, which means that this call site doesn't call vm_dealloc anymore but a different, related function. The easiest way out is to find another vm_dealloc call site. The callsite that I picked works on iOS 15.0 and 15.5+. In versions 15.1 - 15.4, direct calls to vm_deallocate apparently didn't exist. Originally, I included three more instructions in the pattern, before the bl, but these turned out not to exist in the iphone8,1 kernel, for example. The instructions: 0x5280002a, /* mov w10, #1 */ 0x1ac92149, /* lsl w9, w10, w9 */ 0x8b29c102, /* add x2, x8, w9, sxtw */ --- include/pf/pfs.h | 26 ++++++++++---------------- module/pf/15/pf.c | 11 ++--------- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/include/pf/pfs.h b/include/pf/pfs.h index 860f3c4..f6674a1 100644 --- a/include/pf/pfs.h +++ b/include/pf/pfs.h @@ -1091,26 +1091,20 @@ struct pf g_all_pfs[MAXPF][NUM_SUPPORTED_VERSIONS] = { PF_UNUSED, PF_DECL32("vm_deallocate finder iOS 15", LISTIZE({ - 0x94000000, /* bl n */ - 0xf900001f, /* str xzr, [Xn, n] */ - 0x3900001f, /* strb wzr, [Xn, n] */ - 0xb4000000, /* cbz Xn, n */ - 0x0, /* ignore this instruction */ - 0x0, /* ignore this instruction */ - 0xaa0003e1, /* mov x1, Xn */ - 0xaa0003e2, /* mov x2, Xn */ + 0x94000000, /* bl _vm_deallocate */ + 0xf900007f, /* str xzr, [x19, n] */ + 0xb8000008, /* ldr w8, [x19, n] */ + 0x12147908, /* and w8, w8, #0xfffff7ff */ + 0xb8000008, /* str w8, [x19, n] */ }), LISTIZE({ 0xfc000000, /* ignore immediate */ - 0xffc0001f, /* ignore Rn & immediate */ - 0xffc0001f, /* ignore Rn & immediate */ - 0xff000000, /* ignore Rn & immediate */ - 0x0, /* ignore this instruction */ - 0x0, /* ignore this instruction */ - 0xffe0ffff, /* ignore Rn */ - 0xffe0ffff, /* ignore Rn */ + 0xffff00ff, /* ignore immediate */ + 0xfc00001f, /* ignore immediate */ + 0xffffffff, /* match exactly */ + 0xfc00001f, /* ignore immediate */ }), - 8, vm_deallocate_finder_15, "__TEXT_EXEC"), + 5, vm_deallocate_finder_15, "__TEXT_EXEC"), }, { PF_DECL_FULL("kernel_thread_start,thread_deallocate finder iOS 13", diff --git a/module/pf/15/pf.c b/module/pf/15/pf.c index af766c1..00252da 100644 --- a/module/pf/15/pf.c +++ b/module/pf/15/pf.c @@ -119,20 +119,13 @@ bool kernel_map_finder_15(xnu_pf_patch_t *patch, void *cacheable_stream){ return true; } -/* Confirmed working 15.0 */ +/* Confirmed working 15.0, 15.5 - 15.8 */ bool vm_deallocate_finder_15(xnu_pf_patch_t *patch, void *cacheable_stream){ - /* will land in ipc_kmsg_clean_partial. we can only - * search for 8 intructions at a time, so we check - * for the 9th instruction (bl _vm_deallocate) */ xnu_pf_disable_patch(patch); uint32_t *opcode_stream = cacheable_stream; - if ((opcode_stream[8] & 0xfc000000) != 0x94000000){ - return false; - } - - uint32_t *vm_deallocate = get_branch_dst_ptr(opcode_stream + 8); + uint32_t *vm_deallocate = get_branch_dst_ptr(opcode_stream + 0); g_vm_deallocate_addr = xnu_ptr_to_va(vm_deallocate);