
编译和运行example/open1_hook.c后,内核日志的输出
Xnuspy是一个安装新系统调用xnuspy_ctl Pongoos模块,它允许您从用户空间挂起内核函数。它支持iOS 13.X,iOS 14.X和iOS 15.x在CheckRA1N 0.12.2及以上。不支持4K设备。
该模块完全中绝化了KTRR/KPP,并可以在EL1内部创建RWX内存。不要在日常驾驶员上使用它。
需要libusb : brew install libusb
在顶级目录中运行make 。它将构建加载程序和模块。
在make之前添加这些。
XNUSPY_DEBUG=1kprintf )。XNUSPY_SERIAL=1IOLog 。XNUSPY_LEAKED_PAGE_LIMIT=n64 。在调试内核恐慌下可以找到更多信息。XNUSPY_TRAMP_PAGES=n XNUSPY_DEBUG和XNUSPY_SERIAL不依赖彼此。
建造了所有内容后,请checkra1n将设备启动到pongo shell: /Applications/checkra1n.app/Contents/MacOS/checkra1n -p
在同一目录中,您构建了加载程序和模块,请执行loader/loader module/xnuspy 。执行此操作后,Xnuspy将完成它的事情,并在几秒钟内您的设备启动。如果需要利用SEPROM,则loader将在发出xnuspy-getkernelv之后再等待几秒钟。
有时,在Checkra1n的KPF运行后,我的几个电话会陷入“启动”。我还没有弄清楚原因是什么,但是如果发生这种情况,请重试。另外,如果设备在bootx后悬挂,请重试。最后,在我的iPhone X运行iOS 13.3.1上标记xnuspy_ctl代码为可执行文件是有点斑点的,但在其他手机上有100%的时间成功。如果您在执行钩程序时使用内核指令进行暂停,请重试。
Xnuspy将修补一个enosys系统调用,以指向xnuspy_ctl_tramp 。这是一个小型蹦床,将编译的xnuspy_ctl代码标记为可执行文件并分支到它。您可以在module/el1/xnuspy_ctl/xnuspy_ctl.c和example目录中的示例中找到xnuspy_ctl的实现。
内部include/xnuspy/是xnuspy_ctl.h ,该标头定义了xnuspy_ctl的常数。它旨在将其包含在所有连接内核功能的程序中。
您可以使用sysctlbyname弄清楚修补了哪个系统调用:
size_t oldlen = sizeof(long);
long SYS_xnuspy_ctl = 0;
sysctlbyname("kern.xnuspy_ctl_callnum", &SYS_xnuspy_ctl, &oldlen, NULL, 0);
该系统呼叫需要四个论点, flavor , arg1 , arg2和arg3 。味道可以是XNUSPY_CHECK_IF_PATCHED , XNUSPY_INSTALL_HOOK ,Xnuspy_register_death_callback, XNUSPY_REGISTER_DEATH_CALLBACK , XNUSPY_CALL_HOOKME ,xnuspy_kread,xnuspy_kread,, XNUSPY_CACHE_READ , XNUSPY_KREAD ,或XNUSPY_GET_CURRENT_THREAD XNUSPY_KWRITE接下来的三个论点的含义取决于风味。
XNUSPY_CHECK_IF_PATCHED存在此问题,因此您可以检查是否存在xnuspy_ctl 。用这种风味调用它会导致它返回999 。其他参数的值被忽略了。
XNUSPY_INSTALL_HOOK我设计了这种风味以匹配MSHookFunction的API。 arg1是您希望挂接的内核函数的不便地址。如果您提供滑动地址,您很可能会恐慌。 arg2是指向您与ABI兼容的替换功能的指针。 arg3是xnuspy_ctl copyout代表原始内核函数的蹦床地址的指针。如果您不打算调用原件,则可以NULL 。
XNUSPY_REGISTER_DEATH_CALLBACK这种口味使您可以注册可选的“死亡回调”,当您的挂钩程序退出时,功能Xnuspy会打电话。它使您有机会清理从内核钩中创建的任何东西。如果您创建了任何内核线程,则会告诉他们在此功能中终止。
您的回调不会异步调用,因此,如果阻止,则可以防止Xnuspy的垃圾收集线程执行。
arg1是指您回调功能的指针。其他参数的值被忽略了。
XNUSPY_CALL_HOOKME hookme是一个小型组件存根,Xnuspy通过Xnuspy Cache出口供您挂钩。使用这种风味调用xnuspy_ctl会导致hookme被调用,为您提供一种轻松获得内核代码执行而无需连接实际内核函数的方法。
arg1是一个论点,将在调用Hookme时传递给hookme 。这可能是NULL 。
XNUSPY_CACHE_READ这种味道为您提供了一种从Xnuspy Cache中阅读的方法。它包含许多有用的东西,例如kprintf , current_proc , kernel_thread_start ,一些libc函数和内核幻灯片,因此您不必自己找到它们。有关缓存ID的完整列表,请查看example/xnuspy_ctl.h 。
arg1是xnuspy_ctl.h和arg2中定义的缓存ID之一,是xnuspy_ctl copyout请求的地址或值的指针。其他参数的值被忽略了。
XNUSPY_KREAD这种口味为您提供了一种简单的方法,可以在没有TFP0的情况下从用户空间读取内核内存。
arg1是一个内核虚拟地址, arg2是用户空间缓冲区的地址,而arg3是该用户空间缓冲区的大小。 arg3字节将从arg1到arg2编写。
XNUSPY_KWRITE这种口味为您提供了一种简单的方法,可以在没有TFP0的情况下从用户空间写入内核内存。
arg1是一个内核虚拟地址, arg2是用户空间缓冲区的地址,而arg3是该用户空间缓冲区的大小。 arg3字节将从arg2到arg1编写。
XNUSPY_GET_CURRENT_THREAD这种风味为用户空间提供了调用线程的内核地址。
arg1是xnuspy_ctl copyout current_thread的返回值的指针。其他参数的值被忽略了。
对于除XNUSPY_CHECK_IF_PATCHED以外的所有口味,成功返回0 。错误后,返回-1并设置errno 。 XNUSPY_CHECK_IF_PATCHED不会返回任何错误。 XNU的mach_to_bsd_errno用于将kern_return_t转换为适当的errno 。
XNUSPY_INSTALL_HOOK有关的错误errno设置为...
EEXIST if:arg1表示的不便内核函数已经存在一个钩子。ENOMEM if:unified_kalloc返回NULL 。ENOSPC如果:xnuspy_tramp结构,Xnuspy内部的数据结构。除非您同时挂起数百个内核功能,否则这不应该发生。如果您需要更多功能挂钩,请查看限制。ENOTSUP如果:ENOENT :mh_for_addr无法确定与呼叫者地址空间内arg2相对应的MACH-O标头。EFAULT如果:EIO if:mach_make_memory_entry_64没有返回确定的Mach-O标题的__TEXT和__DATA段的整个内存条目。 errno还取决于vm_map_wire_external , mach_vm_map_external , mach_make_memory_entry_64 , copyin , copyout和如果适用的vm_map_wire_external的返回值。
如果此口味返回错误,则目标核函数不会挂钩。如果您通过了arg3的NULL指针,则可能已经初始化了也可能没有初始化。如果是的话,它是不安全的。
XNUSPY_REGISTER_DEATH_CALLBACK有关的错误errno设置为...
ENOENT :如果此口味返回错误,则您的死亡回调未注册。
XNUSPY_CALL_HOOKME有关的错误errno设置为...
ENOTSUP如果:hookme离包含xnuspy_tramp结构的内存太远。这是在Pongoos内部确定的,只有在Xnuspy不得不退回到kernelcache内部的未使用的代码中,才能发生。在这种情况下,几乎可以肯定会引起hookme恐慌,您必须找出另一个内核功能要钩。如果这种口味返回错误,则不会调用hookme 。
XNUSPY_CACHE_READ有关的错误errno设置为...
EINVAL if:arg1表示的常数并不表示缓存中的任何内容。arg1是IO_LOCK ,但内核是iOS 14.4.2或以下或iOS 15.x。arg1是IPC_OBJECT_LOCK ,但内核是iOS 15.x。arg1是IPC_PORT_RELEASE_SEND ,但内核是iOS 14.5或更高。arg1是IPC_PORT_RELEASE_SEND_AND_UNLOCK ,但内核是iOS 14.4.2或以下。arg1是KALLOC_CANBLOCK ,但内核是iOS 14.x或更高。arg1是KALLOC_EXTERNAL ,但内核是iOS 13.x。arg1是KFREE_ADDR ,但内核是iOS 14.x或更高。arg1是KFREE_EXT ,但内核是iOS 13.x。arg1是PROC_REF ,但内核是iOS 14.8或以下。arg1是PROC_REF_LOCKED ,但内核是iOS 15.x。arg1是PROC_RELE ,但内核是iOS 14.8或以下。arg1已被PROC_RELE_LOCKED ,但内核是iOS 15.x。arg1是VM_MAP_UNWIRE ,但内核是iOS 15.x。arg1为VM_MAP_UNWIRE_NESTED ,但内核是iOS 14.8或以下。 errno还取决于copyout的返回值,如果适用,则是一次性初始化函数的返回值。
如果此口味返回错误,则您通过arg2传递的指针不会初始化。
XNUSPY_KREAD和XNUSPY_KWRITE有关的错误errno设置为...
EFAULT如果:arg1或arg2的地址翻译失败。如果您使用XNUSPY_DEBUG=1编译,则将有关它的消息打印到内核日志。如果此口味返回错误,则不会读取/编写内核内存。
XNUSPY_GET_CURRENT_THREAD有关的错误如果copyout失败,则将errno设置为其返回值。
在编写替换功能时,很容易忘记我正在编写内核代码。当您写钩子时,这是要记住的几件事:
__TEXT段之外的用户空间代码。例如,如果您不小心致电printf而不是kprintf ,则会感到恐慌。如果该功能尚未通过XNUSPY_CACHE_READ可用,则需要重新实现您要调用的任何LIBC函数。但是,您可以为其他内核函数创建功能指针并将其称为。PAGE_SIZE扩展到vm_page_size ,而不是常数。在阅读此变量之前,您需要禁用PAN(在A10+上,我也不建议这样做),否则您会感到恐慌。-fno-stack-protector编译代码,并且-D_FORTIFY_SOURCE=0在某些情况下,该设备必须通过确定另一个用户空间指针来读取___stack_chk_guard ,这会在A10+上恐慌。还建议浏览https://developer.apple.com/library/archive/documentation/darwin/conceptual/kernelprogramming/style/style/style.html。
编写代码时,错误是不可避免的,因此最终您将引起内核恐慌。恐慌并不一定意味着Xnuspy有一个错误,因此在打开问题之前,请确保您在拨打原始功能并返回其值(如果需要的话)时仍然恐慌。如果您仍然慌张,那么它可能是一个Xnuspy错误(请打开问题),但是如果没有,则替换有问题。
由于Xnuspy实际上并未将执行重定向到EL0页,因此调试恐慌并不那么简单。打开module/el1/xnuspy_ctl/xnuspy_ctl.c ,然后在xnuspy_install_hook中唯一呼叫kwrite_instr之前,将呼叫添加到IOSleep几秒钟。这样做是为了确保设备恐慌之前有足够的时间进行日志传播。重新编译Xnuspy用XNUSPY_DEBUG=1 make -B并再次加载模块。加载模块后,如果还没有,请从klog/中编译klog 。将其上传到您的设备并执行stdbuf -o0 ./klog | grep shared_mapping_kva 。再次运行您的钩程序,并观看klog的一行,看起来像这样:
shared_mapping_kva: dist 0x7af4 uaddr 0x104797af4 umh 0x104790000 kmh 0xfffffff00c90c000
如果您要安装多个钩子,则会发生多个挂钩。在这种情况下, dist和uaddr会有所不同,但是umh和kmh不会。 kmh指向了内核映射您程序__TEXT段的开始。将您的挂钩程序扔进您喜欢的拆卸器中并重新置换,以便其Mach-O标头处于kmh的地址。对于IDA Pro,这是Edit -> Segments -> Rebase program... Image base燃烧。在设备再次重新启动后,如果有与恐慌日志中内核映射相对应的地址,则它们将与拆卸匹配。如果没有,那么您的替换内部可能会有某种微妙的内存损坏。
Xnuspy也无法知道内核线程是否仍在卸载挂钩后,在插入程序的__TEXT段的内核映射上是否仍在执行(或将执行)。 Xnuspy所做的一件事是,在您的挂钩程序死亡之后,不要立即对此映射进行处理。相反,它被添加到队列的尽头。一旦Xnuspy的垃圾收集线程注意到在该队列中保存价值多少页映射的设定限制,它将开始从队列的前部进行交易,并将继续持续到该限制不再超过该限制。默认情况下,此限制为1 MB或64页。
尽管这确实有帮助,但钩程序的__TEXT和__DATA段就越大,Xnuspy赢得这场比赛的可能性就越小。如果您定期感到恐慌并且有一个大挂钩程序,请尝试通过在make前添加XNUSPY_LEAKED_PAGE_LIMIT=n来增加此限制。这将把此限制设置为n页,而不是64页。
Xnuspy保留其xnuspy_tramp structs XNU靴子之前的一页静态内存存储器,使您同时挂在225个内核函数上。如果需要更多,则可以在make前添加XNUSPY_TRAMP_PAGES=n 。这将告诉Xnuspy为xnuspy_tramp结构保留静态内存的n页。但是,如果Xnuspy必须退回到内核内部已经未使用的代码,那么这将被忽略。当这种情况发生时,它的工作方式将详细介绍。
由于某种原因,来自os_log_with_args的日志不会显示在命令行工具oslog输出的流中。来自kprintf的日志也不会这样做,但是可以用dmesg看到它们。但是, dmesg不是现场供稿,因此我写了klog ,该工具是实时显示kprintf登录的工具。在klog/中找到它。我强烈建议您使用它,而不是将dmesg发送给您的kprintf消息。
如果您获得open: Resource busy运行klog后的资源忙碌,请运行此命令launchctl unload /System/Library/LaunchDaemons/com.apple.syslogd.plist ,然后重试。
不幸的是,如果在XNU的Bootargs中设置了atm_diagnostic_config=0x20000000 ,则将无法看到任何NSLog 's。 klog取决于存在此引导参数。如果您想要NSLog返回,请从pongo_send_command inide loader.c中删除该启动参数。
Xnuspy将为您管理。一旦流程退出,该过程安装的所有内核挂钩都将在一秒钟左右的时间内卸载。
大多数功能挂钩框架具有一定的最小长度,使给定的功能可钩。仅当您打算调用原始功能并且挂钩功能的第一个指令不是B ,Xnuspy才有此限制。在这种情况下,最小长度为八个字节。否则,没有最小长度。
Xnuspy使用X16和X17作为其蹦床,因此,期望这些函数的内核函数无法钩住函数呼叫(没有多少人期望这一点)。如果您要挂钩的功能以BL开头,并且您打算调用原始功能,则只有执行原始函数不修改X17 ,才可以这样做。
xnuspy_ctl将在新启动后首次进行一次性初始化。这是Xnuspy的唯一一部分,它是可种族的,因为我无法静态地初始化我使用的读/写锁。在第一个呼叫返回后,任何将来的呼叫都将是线程安全的。
这是简化的,但它很好地捕捉了主要思想。 Xnuspy中的函数钩是一个位于可写的可执行内核内存上的结构。在大多数情况下,这是由Pongoos内部的alloc_static返回的内存。它可以归结为:
struct {
uint64_t replacement;
uint32_t tramp[2];
uint32_t orig[10];
};
如果replacement是替换功能的内核虚拟地址(在后面详细说明), tramp是一个小型蹦床,将执行重新引导到replacement中,而orig是代表原始功能的较大,更复杂的蹦床。
Xnuspy所做的第一件事之一是确定EL0替换位于呼叫过程的地址空间内的位置。这样做是为了使内核函数可以从动态库中挂钩。与该替换的地址相对应的MACH-O标头被保存。
之后,创建了该标题的__TEXT和__DATA段(以及这些之间的任何段(如果有)之间的任何段)的共享用户内核映射。共享__TEXT ,因此您可以从钩子中调用其他功能。 __DATA共享,因此EL1和EL0都可以看到全局变量的变化。
由于此映射是__TEXT和__DATA的一对一副本,因此很容易找出用户替换功能的地址。给定调用过程的MACH -O标头u的地址,共享映射k的开始的地址以及用户替换功能r的地址r,我们应用以下公式: replacement = k + (r - u)
之后, replacement是用户在共享映射上替换功能的内核虚拟地址,并写入功能钩结构。 Xnuspy不会将执行重新指导到替换功能的EL0地址,因为这是极其不安全的:这不仅使我们受到调度程序的摆布,而且无法控制kernel挂钩dies的过程,而kernel挂钩dies的过程仍在替换率上仍在执行。
最后,共享映射标记为可执行文件,并组装无条件的直接分支( B )。它将执行力定向到tramp的开始,是替代现在钩的内核功能的第一个指令。不幸的是,这限制了我们从分支到钩结构远离给定核函数超过128 MB的钩子。 Xnuspy在启动之前确实检查了此方案,并落入了kernelcache中的未使用的代码,以便如果发现可能发生这种情况,则可以将钩子结构固定在上面。
我会尽力确保补丁程序工作,因此,如果某件事不起作用,请打开问题。