Xnuspy
an iOS kernel function hooking framework for checkra1n'able devices
Install / Use
/learn @jsherman212/XnuspyREADME
xnuspy

<sup>Output from the kernel log after compiling and running example/open1_hook.c</sup>
xnuspy is a pongoOS module that installs a new system call, xnuspy_ctl,
which allows you to hook kernel functions from userspace. It supports iOS 13.x,
iOS 14.x, and iOS 15.x on checkra1n 0.12.2 and up. 4K devices are not supported.
This module completely neuters KTRR/KPP and makes it possible to create RWX memory inside EL1. Do not use this on your daily driver.
Requires libusb: brew install libusb
Building
Run make in the top level directory. It'll build the loader and the module.
Build Options
Add these before make.
XNUSPY_DEBUG=1- Send debug output from xnuspy to the kernel log (
kprintf).
- Send debug output from xnuspy to the kernel log (
XNUSPY_SERIAL=1- Send debug output from xnuspy to
IOLog.
- Send debug output from xnuspy to
XNUSPY_LEAKED_PAGE_LIMIT=n- Set the number of pages xnuspy is allowed to leak before its garbage
collection thread starts deallocating them. Default is
64. More info can be found under Debugging Kernel Panics.
- Set the number of pages xnuspy is allowed to leak before its garbage
collection thread starts deallocating them. Default is
XNUSPY_TRAMP_PAGES=n- Set the number of pages xnuspy will reserve for its trampoline structures. Default is 1. More info can be found under Limits.
XNUSPY_DEBUG and XNUSPY_SERIAL do not depend on each other.
Usage
After you've built everything, have checkra1n boot your device to a pongo
shell: /Applications/checkra1n.app/Contents/MacOS/checkra1n -p
In the same directory you built the loader and the module, do
loader/loader module/xnuspy. After doing that, xnuspy will do its thing and
in a few seconds your device will boot. loader will wait a couple more
seconds after issuing xnuspy-getkernelv in case SEPROM needs to be exploited.
Known Issues
Sometimes a couple of my phones would get stuck at "Booting" after checkra1n's KPF
runs. I have yet to figure out what causes this, but if it happens, try again.
Also, if the device hangs after bootx, try again. Finally, marking the
compiled xnuspy_ctl code as executable on my iPhone X running iOS 13.3.1 is
a bit spotty, but succeeds 100% of the time on my other phones. If you panic
with a kernel instruction fetch abort when you execute your hook program,
try again.
xnuspy_ctl
xnuspy will patch an enosys system call to point to xnuspy_ctl_tramp.
This is a small trampoline which marks the compiled xnuspy_ctl code as
executable and branches to it. You can find xnuspy_ctl's implementation at
module/el1/xnuspy_ctl/xnuspy_ctl.c and examples in the example directory.
Inside include/xnuspy/ is xnuspy_ctl.h, a header which defines constants
for xnuspy_ctl. It is meant to be included in all programs which hook
kernel functions.
You can use sysctlbyname to figure out which system call was patched:
size_t oldlen = sizeof(long);
long SYS_xnuspy_ctl = 0;
sysctlbyname("kern.xnuspy_ctl_callnum", &SYS_xnuspy_ctl, &oldlen, NULL, 0);
This system call takes four arguments, flavor, arg1, arg2, and arg3.
The flavor can either be XNUSPY_CHECK_IF_PATCHED, XNUSPY_INSTALL_HOOK,
XNUSPY_REGISTER_DEATH_CALLBACK, XNUSPY_CALL_HOOKME, XNUSPY_CACHE_READ,
XNUSPY_KREAD, XNUSPY_KWRITE, or XNUSPY_GET_CURRENT_THREAD.
The meaning of the next three arguments depend on the flavor.
XNUSPY_CHECK_IF_PATCHED
This exists so you can check if xnuspy_ctl is present. Invoking it with this
flavor will cause it to return 999. The values of the other arguments are
ignored.
XNUSPY_INSTALL_HOOK
I designed this flavor to match MSHookFunction's API.
arg1 is the UNSLID address of the kernel function you wish to hook. If you
supply a slid address, you will most likely panic. arg2 is a pointer to your
ABI-compatible replacement function. arg3 is a pointer for xnuspy_ctl to
copyout the address of a trampoline that represents the original kernel
function. This can be NULL if you don't intend to call the original.
XNUSPY_REGISTER_DEATH_CALLBACK
This flavor allows you to register an optional "death callback", a function xnuspy will call when your hook program exits. It gives you a chance to clean up anything you created from your kernel hooks. If you created any kernel threads, you would tell them to terminate in this function.
Your callback is not invoked asynchronously, so if you block, you're preventing xnuspy's garbage collection thread from executing.
arg1 is a pointer to your callback function. The values of the other arguments
are ignored.
XNUSPY_CALL_HOOKME
hookme is a small assembly stub which xnuspy exports through the xnuspy cache
for you to hook. Invoking xnuspy_ctl with this flavor will cause hookme to
get called, providing a way for you to easily gain kernel code execution without
having to hook an actual kernel function.
arg1 is an argument that will be passed to hookme when it is invoked.
This can be NULL.
XNUSPY_CACHE_READ
This flavor gives you a way to read from the xnuspy cache. It contains many useful
things like kprintf, current_proc, kernel_thread_start, some libc functions,
and the kernel slide so you don't have to find them yourself. For a complete list
of cache IDs, check out example/xnuspy_ctl.h.
arg1 is one of the cache IDs defined in xnuspy_ctl.h and arg2 is a
pointer for xnuspy_ctl to copyout the address or value of what you requested.
The values of the other arguments are ignored.
XNUSPY_KREAD
This flavor gives you an easy way to read kernel memory from userspace without tfp0.
arg1 is a kernel virtual address, arg2 is the address of a userspace buffer,
and arg3 is the size of that userspace buffer. arg3 bytes will be written
from arg1 to arg2.
XNUSPY_KWRITE
This flavor gives you an easy way to write to kernel memory from userspace without tfp0.
arg1 is a kernel virtual address, arg2 is the address of a userspace buffer,
and arg3 is the size of that userspace buffer. arg3 bytes will be written
from arg2 to arg1.
XNUSPY_GET_CURRENT_THREAD
This flavor provides userspace the kernel address of the calling thread.
arg1 is a pointer for xnuspy_ctl to copyout the return value of
current_thread. The values of the other arguments are ignored.
Errors
For all flavors except XNUSPY_CHECK_IF_PATCHED, 0 is returned on success.
Upon error, -1 is returned and errno is set. XNUSPY_CHECK_IF_PATCHED
does not return any errors. XNU's mach_to_bsd_errno is used to convert a
kern_return_t to the appropriate errno.
Errors Pertaining to XNUSPY_INSTALL_HOOK
errno is set to...
EEXISTif:- A hook already exists for the unslid kernel function denoted by
arg1.
- A hook already exists for the unslid kernel function denoted by
ENOMEMif:unified_kallocreturnedNULL.
ENOSPCif:- There are no free
xnuspy_trampstructs, a data structure internal to xnuspy. This shouldn't happen unless you're hooking hundreds of kernel functions at the same time. If you need more function hooks, check out Limits.
- There are no free
ENOTSUPif:- The caller is not from a Mach-O executable or dynamic library.
ENOENTif:mh_for_addrwas unable to determine the Mach-O header corresponding toarg2inside the caller's address space.
EFAULTif:- The determined Mach-O header is not actually a Mach-O header. This will probably never happen.
EIOif:mach_make_memory_entry_64did not return a memory entry for the entirety of the determined Mach-O header's__TEXTand__DATAsegments.
errno also depends on the return value of vm_map_wire_external,
mach_vm_map_external, mach_make_memory_entry_64, copyin, copyout, and
if applicable, the one-time initialization function.
If this flavor returns an error, the target kernel function was not hooked.
If you passed a non-NULL pointer for arg3, it may or may not have been
initialized. It's unsafe to use if it was.
Errors Pertaining to XNUSPY_REGISTER_DEATH_CALLBACK
errno is set to...
ENOENTif:- The calling process hasn't hooked any kernel functions.
If this flavor returns an error, your death callback was not registered.
Errors Pertaining to XNUSPY_CALL_HOOKME
errno is set to...
ENOTSUPif:hookmeis too far away from the memory containing thexnuspy_trampstructures. This is determined inside of pongoOS, and can only happen if xnuspy had to fallback to unused code already inside of the kernelcache. In this case, callinghookmewould almost certainly cause a kernel panic, and you'll have to figure out another kernel function to hook.
If this flavor returns an error, hookme was not called.
Errors Pertaining to XNUSPY_CACHE_READ
errno is set to...
EINVALif:- The constant denoted by
arg1does not represent anything in the cache. arg1wasIO_LOCK, but the kernel is iOS 14.4.2 or below or iOS 15.x.arg1wasIPC_OBJECT_LOCK, but the kernel is iOS 15.x.arg1wasIPC_PORT_RELEASE_SEND, but the kernel is iOS 14.5 or above.arg1wasIPC_PORT_RELEASE_SEND_AND_UNLOCK, but the kernel is iOS 14.4.2 or below.arg1wasKALLOC_CANBLOCK, but the kernel is iOS 14.x or above.arg1wasKALLOC_EXTERNAL, but the kernel is iOS 13.x.arg1wasKFREE_ADDR, but the kernel is iOS 14.x or above.arg1wasKFREE_EXT, but the kernel is iOS 13.x.arg1wasPROC_REF, but the kernel is iOS 14.8 or below.arg1wasPROC_REF_LOCKED, but the kernel is iOS 15.x.arg1wasPROC_RELE, but the kernel is iOS 14.8 or below.arg1wasPROC_RELE_LOCKED, but the kernel is iOS 15.x.arg1wasVM_MAP_UNWIRE, but the kernel is iOS 15.x.arg1wasVM_MAP_UNWIRE_NESTED, but the kernel is iOS 14.8 or below.
- The constant denoted by
errno also depends on the return value of copyout and if applicable, the
return value of the one-time initialization function.
If this flavor returns an error, the point
