Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
xnuspy — 一个适用于 checkra1n 可越狱设备的 iOS 内核函数挂钩框架 | Kitploit
工具/GitHubGitHub/jsherman212/xnuspy
iOS安全漏洞利用调试器二进制分析
GitHubjsherman212/xnuspy

xnuspy

一个适用于 checkra1n 可越狱设备的 iOS 内核函数挂钩框架

查看仓库
595112534年前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

xnuspy

alt text

编译并运行 example/open1_hook.c 后内核日志的输出

xnuspy 是一个 pongoOS 模块,它安装了一个新的系统调用 xnuspy_ctl,允许你从用户空间钩取内核函数。它支持 checkra1n 0.12.2 及以上版本的 iOS 13.x、iOS 14.x 和 iOS 15.x。不支持 4K 设备。

该模块完全禁用 KTRR/KPP,使得在 EL1 内创建 RWX 内存成为可能。请勿在你的主力设备上使用。

需要 libusb:brew install libusb

构建

在顶层目录中运行 make。它将构建加载器和模块。

构建选项

在 make 前添加这些选项。

  • XNUSPY_DEBUG=1
    • 将 xnuspy 的调试输出发送到内核日志(kprintf)。
  • XNUSPY_SERIAL=1
    • 将 xnuspy 的调试输出发送到 IOLog。
  • XNUSPY_LEAKED_PAGE_LIMIT=n
    • 设置 xnuspy 在垃圾回收线程开始释放之前允许泄漏的页数。默认值为 64。更多信息请参见 调试内核恐慌。
  • XNUSPY_TRAMP_PAGES=n
    • 设置 xnuspy 为其跳板结构保留的页数。默认值为 1。更多信息请参见 限制。

XNUSPY_DEBUG 和 XNUSPY_SERIAL 不相互依赖。

使用

构建完成后,让 checkra1n 将你的设备启动到 pongo shell:/Applications/checkra1n.app/Contents/MacOS/checkra1n -p

在构建加载器和模块的同一目录中,运行 loader/loader module/xnuspy。之后,xnuspy 将执行其操作,几秒钟后你的设备将启动。loader 会在发出 xnuspy-getkernelv 后再等待几秒钟,以防需要利用 SEPROM。

已知问题

有时我的几部手机会在 checkra1n 的 KPF 运行后卡在"Booting"。我尚未弄清楚原因,但如果发生这种情况,请重试。此外,如果设备在 bootx 后挂起,请重试。最后,在我的运行 iOS 13.3.1 的 iPhone X 上将编译后的 xnuspy_ctl 代码标记为可执行有点不稳定,但在我的其他手机上 100% 成功。如果你在执行钩子程序时遇到内核指令获取中止导致的 panic,请重试。

xnuspy_ctl

xnuspy 会修补 enosys 系统调用,使其指向 xnuspy_ctl_tramp。这是一个小型跳板,它将编译后的 xnuspy_ctl 代码标记为可执行并跳转到它。你可以在 module/el1/xnuspy_ctl/xnuspy_ctl.c 找到 xnuspy_ctl 的实现,在 example 目录中找到示例。

在 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);

root@kitploit:~
该系统调用接受四个参数:`flavor`、`arg1`、`arg2` 和 `arg3`。
flavor 可以是 `XNUSPY_CHECK_IF_PATCHED`、`XNUSPY_INSTALL_HOOK`、
`XNUSPY_REGISTER_DEATH_CALLBACK`、`XNUSPY_CALL_HOOKME`、`XNUSPY_CACHE_READ`、
`XNUSPY_KREAD`、`XNUSPY_KWRITE` 或 `XNUSPY_GET_CURRENT_THREAD`。
其余三个参数的含义取决于 flavor。

## `XNUSPY_CHECK_IF_PATCHED`
该 flavor 用于检查 `xnuspy_ctl` 是否存在。以此 flavor 调用时,它会返回 `999`。其他参数的值将被忽略。

## `XNUSPY_INSTALL_HOOK`
我设计此 flavor 以匹配 [`MSHookFunction`](http://www.cydiasubstrate.com/api/c/MSHookFunction/) 的 API。
`arg1` 是你希望 hook 的内核函数的*未滑动*地址。如果你提供了滑动后的地址,很可能导致内核崩溃。`arg2` 是指向你兼容 ABI 的替换函数的指针。`arg3` 是 `xnuspy_ctl` 用于 `copyout` 代表原始内核函数的跳板地址的指针。如果你不打算调用原始函数,该参数可以为 `NULL`。

## `XNUSPY_REGISTER_DEATH_CALLBACK`
此 flavor 允许你注册一个可选的“死亡回调”,即当你的 hook 程序退出时 xnuspy 会调用的函数。它让你有机会清理在内核 hook 中创建的任何东西。如果你创建了任何内核线程,你需要在此函数中通知它们终止。

你的回调不是异步调用的,因此如果你阻塞了,就会阻止 xnuspy 的垃圾回收线程执行。

`arg1` 是指向你的回调函数的指针。其他参数的值将被忽略。

## `XNUSPY_CALL_HOOKME`
`hookme` 是一个小的汇编桩,xnuspy 通过 xnuspy 缓存导出供你 hook。以此 flavor 调用 `xnuspy_ctl` 会导致 `hookme` 被调用,从而让你无需 hook 实际的内核函数即可轻松获得内核代码执行能力。

`arg1` 是一个参数,会在 `hookme` 被调用时传递给它。可以为 `NULL`。

## `XNUSPY_CACHE_READ`
此 flavor 提供了一种从 xnuspy 缓存读取数据的方式。缓存中包含许多有用的东西,例如 `kprintf`、`current_proc`、`kernel_thread_start`、一些 libc 函数以及内核滑动值,这样你就不必自己去查找它们。关于缓存 ID 的完整列表,请参考 `example/xnuspy_ctl.h`。

`arg1` 是 `xnuspy_ctl.h` 中定义的某个缓存 ID,`arg2` 是 `xnuspy_ctl` 用于 `copyout` 所请求内容的地址或值的指针。其他参数的值将被忽略。

## `XNUSPY_KREAD`
此 flavor 提供了一种无需 tfp0 即可从用户空间读取内核内存的简便方法。

`arg1` 是内核虚拟地址,`arg2` 是用户空间缓冲区的地址,`arg3` 是该用户空间缓冲区的大小。将从 `arg1` 向 `arg2` 写入 `arg3` 个字节。

## `XNUSPY_KWRITE`
此 flavor 提供了一种无需 tfp0 即可从用户空间写入内核内存的简便方法。

`arg1` 是内核虚拟地址,`arg2` 是用户空间缓冲区的地址,`arg3` 是该用户空间缓冲区的大小。将从 `arg2` 向 `arg1` 写入 `arg3` 个字节。

## `XNUSPY_GET_CURRENT_THREAD`
此 flavor 向用户空间提供调用线程的内核地址。

`arg1` 是 `xnuspy_ctl` 用于 `copyout` `current_thread` 返回值的指针。其他参数的值将被忽略。

### 错误
对于除 `XNUSPY_CHECK_IF_PATCHED` 之外的所有 flavor,成功时返回 `0`。发生错误时返回 `-1`,并设置 `errno`。`XNUSPY_CHECK_IF_PATCHED` 不会返回任何错误。XNU 的 `mach_to_bsd_errno` 用于将 `kern_return_t` 转换为相应的 `errno`。

#### 与 `XNUSPY_INSTALL_HOOK` 相关的错误
`errno` 设置为...
- `EEXIST` 如果:
  - `arg1` 所表示的未滑动内核函数已存在 hook。
- `ENOMEM` 如果:
  - `unified_kalloc` 返回了 `NULL`。
- `ENOSPC` 如果:
  - 没有空闲的 `xnuspy_tramp` 结构体(这是 xnuspy 内部的数据结构)。除非你*同时* hook 了数百个内核函数,否则不应发生此情况。如果你需要更多函数 hook,请参考 [限制](#limits)。
- `ENOTSUP` 如果:
  - 调用者不是来自 Mach-O 可执行文件或动态库。
- `ENOENT` 如果:
  - `mh_for_addr` 无法在调用者的地址空间中确定与 `arg2` 对应的 Mach-O 头部。
- `EFAULT` 如果:
  - 确定的 Mach-O 头部实际上不是 Mach-O 头部。这很可能永远不会发生。
- `EIO` 如果:
  - `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` 以及(如果适用)一次性初始化函数的返回值。

如果此 flavor 返回错误,则目标内核函数未被 hook。如果你为 `arg3` 传递了非 `NULL` 指针,它可能已被初始化,也可能未被初始化。如果已初始化,使用它是不安全的。

#### 与 `XNUSPY_REGISTER_DEATH_CALLBACK` 相关的错误
`errno` 设置为...
- `ENOENT` 如果:
  - 调用进程尚未 hook 任何内核函数。

如果此 flavor 返回错误,你的死亡回调未被注册。

#### 与 `XNUSPY_CALL_HOOKME` 相关的错误
`errno` 设置为...
- `ENOTSUP` 如果:
  - `hookme` 距离包含 `xnuspy_tramp` 结构体的内存太远。此判断在 pongoOS 内部完成,并且只有在 xnuspy 必须回退到内核缓存中已有的未使用代码时才会发生。在这种情况下,调用 `hookme` 几乎肯定会导致内核崩溃,你需要另寻其他内核函数进行 hook。

如果此 flavor 返回错误,则 `hookme` 未被调用。

#### 与 `XNUSPY_CACHE_READ` 相关的错误
`errno` 设置为...
- `EINVAL` 如果:
  - `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` 的返回值以及(如果适用)一次性初始化函数的返回值。

如果此 flavor 返回错误,你为 `arg2` 传递的指针未被初始化。

#### 与 `XNUSPY_KREAD` 和 `XNUSPY_KWRITE` 相关的错误
`errno` 设置为...
- `EFAULT` 如果:
  - `arg1` 或 `arg2` 的地址转换失败。如果你使用 `XNUSPY_DEBUG=1` 编译,内核日志中会打印一条相关消息。

如果此 flavor 返回错误,则内核内存未被读取/写入。

#### 与 `XNUSPY_GET_CURRENT_THREAD` 相关的错误
如果 `copyout` 失败,`errno` 设置为其返回值。

# 重要信息

### 常见陷阱
在编写替换函数时,很容易忘记自己正在编写内核代码。编写 hook 时需要记住以下几点:

- **你不能执行程序 `__TEXT` 段之外的任何用户空间代码**。例如,如果你不小心调用了 `printf` 而不是 `kprintf`,就会导致崩溃。你需要重新实现你想要调用的任何 libc 函数(如果该函数尚未通过 `XNUSPY_CACHE_READ` 提供)。不过,你可以创建指向其他内核函数的函数指针并调用它们。
- **用户空间代码中常用的许多宏在内核中是不安全的。** 例如,`PAGE_SIZE` 展开为 `vm_page_size`,而不是一个常量。在读取此变量之前,你需要禁用 PAN(在 A10+ 上,我同样不推荐这样做),否则会导致崩溃。
- **请确保使用 `-fno-stack-protector` 和 `-D_FORTIFY_SOURCE=0` 编译你的代码。** 在某些情况下,设备需要通过解引用另一个用户空间指针来读取 `___stack_chk_guard`,这在 A10+ 上会导致崩溃。
- **为了安全起见,不要使用编译器优化编译你的 hook 程序。**

建议也浏览一下 https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/style/style.html。

### 调试内核崩溃
编写代码时 bug 在所难免,所以你最终肯定会遇到内核崩溃。崩溃并不一定意味着 xnuspy 存在 bug,因此在提交 issue 之前,请先确认当你仅调用原始函数并返回其值(如果需要)时是否仍然崩溃。如果仍然崩溃,则可能是 xnuspy 的 bug(请提交 issue),但如果不崩溃,则说明你的替换函数有问题。

由于 xnuspy 实际上不会将执行重定向到 EL0 页面,因此调试崩溃并不简单。打开 `module/el1/xnuspy_ctl/xnuspy_ctl.c`,在 `xnuspy_install_hook` 中唯一调用 `kwrite_instr` 的地方之前,添加一个 `IOSleep` 调用,等待几秒钟。这样做是为了确保设备崩溃前有足够的时间让日志传播。使用 `XNUSPY_DEBUG=1 make -B` 重新编译 xnuspy 并再次加载模块。加载模块后,如果尚未编译,请编译 `klog/` 中的 `klog`。将其上传到设备,然后运行 `stdbuf -o0 ./klog | grep shared_mapping_kva`。再次运行你的 hook 程序,并观察 `klog` 中类似于以下内容的行:

`shared_mapping_kva: dist 0x7af4 uaddr 0x104797af4 umh 0x104790000 kmh 0xfffffff00c90c000`

如果你安装了多个 hook,将会出现多次。在这种情况下,`dist` 和 `uaddr` 会变化,但 `umh` 和 `kmh` 不会。`kmh` 指向内核映射的程序 `__TEXT` 段的起始地址。将你的 hook 程序放入你喜欢的反汇编器中,并将其基址重定位为 `kmh` 的地址。对于 IDA Pro,选择 `Edit -> Segments -> Rebase program...` 并选中 `Image base`。设备再次崩溃并重启后,如果崩溃日志中存在与你替换函数在内核映射中的地址相对应的地址,它们将与反汇编结果匹配。如果没有,那么你的替换函数中可能存在某种微妙的

xnuspy 也无法知道在卸载你的 hook 后,是否仍有内核线程在执行(或将要执行)内核映射的程序 `__TEXT` 段。为了解决这个问题,xnuspy 采取的措施之一是:在你的 hook 程序终止后,不会立即释放此映射,而是将其添加到队列的末尾。当 xnuspy 的垃圾回收线程发现队列中持有的映射页数超过设定限制时,它将开始从队列前端释放,直到不再超出该限制。默认情况下,此限制为 1 MB,即 64 页。

虽然这帮助很大,但你的 hook 程序的 `__TEXT` 和 `__DATA` 段越大,xnuspy 赢得这场竞赛的可能性就越小。如果你经常崩溃且 hook 程序较大,请尝试在 `make` 之前添加 `XNUSPY_LEAKED_PAGE_LIMIT=n` 来增加此限制。这会将限制设置为 `n` 页,而不是 64 页。

### 限制
xnuspy 在 XNU 启动前预留了一个静态内核内存页用于其 `xnuspy_tramp` 结构体,这允许你同时 hook 大约 225 个内核函数。如果你需要更多,可以在 `make` 之前添加 `XNUSPY_TRAMP_PAGES=n`。这将告诉 xnuspy 为 `xnuspy_tramp` 结构体预留 `n` 页静态内存。但是,如果 xnuspy 必须回退到内核缓存中已有的未使用代码,则该值会被忽略。发生这种情况的时机详见[工作原理](#how-it-works)。

### 日志记录
出于某种原因,`os_log_with_args` 的日志不会显示在命令行工具 `oslog` 的输出流中。`kprintf` 的日志也不会出现在那里,但可以通过 `dmesg` 查看。然而,`dmesg` 不是实时流,因此我编写了 `klog`,一个实时显示 `kprintf` 日志的工具。你可以在 `klog/` 中找到它。我强烈建议使用它来代替频繁使用 `dmesg` 来查看 `kprintf` 消息。

如果你在运行 `klog` 后遇到 `open: Resource busy`,请运行命令 `launchctl unload /System/Library/LaunchDaemons/com.apple.syslogd.plist` 并重试。

不幸的是,如果 XNU 的 bootargs 中设置了 `atm_diagnostic_config=0x20000000`,你将无法看到任何 `NSLog` 输出。`klog` 依赖于这个 boot 参数的存在。如果你想要恢复 `NSLog`,请从 `loader.c` 中的 `pongo_send_command` 中移除该 boot 参数。

### 卸载 Hook
xnuspy 会为你管理此事。一旦进程退出,该进程安装的所有内核钩子将在大约一秒内被卸载。

### 可 Hook 的内核函数
大多数函数钩子框架对可钩函数的最小长度有一定要求。xnuspy 只有在你计划调用原始函数*并且*被钩函数的第一条指令不是 `B` 时才有此限制。在这种情况下,最小长度为八个字节。否则,没有最小长度限制。

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 是一个小型 trampoline,将执行重定向到 replacement,而 orig 是一个更大、更复杂的 trampoline,代表原始函数。

xnuspy 首先做的事情之一是确定 EL0 替换存在于调用进程地址空间中的哪个位置。这样做是为了可以从动态库中挂钩内核函数。保存与该替换地址相对应的 Mach-O 头。

之后,会创建该头的 __TEXT 和 __DATA 段(以及这两者之间的任何段)的共享用户-内核映射。共享 __TEXT 是为了你可以从钩子中调用其他函数。共享 __DATA 是为了对全局变量的修改能被 EL1 和 EL0 双方看到。

由于这个映射是 __TEXT 和 __DATA 的一对一拷贝,因此很容易计算出用户替换函数在其上的地址。给定调用进程的 Mach-O 头的地址 u、共享映射起始地址 k 和用户替换函数的地址 r,我们应用以下公式:replacement = k + (r - u)

之后,replacement 是用户替换函数在共享映射上的内核虚拟地址,并被写入函数钩子结构。xnuspy 不会将执行重定向到替换函数的 EL0 地址,因为那极其不安全:这不仅让我们受制于调度器,而且在具有内核钩子的进程死亡而内核线程仍在替换上执行时,我们无法控制局面。

最后,共享映射被标记为可执行,并组装一条无条件立即分支指令(B)。它将执行定向到 tramp 的起始位置,并替换现在被挂钩的内核函数的第一条指令。不幸的是,这限制了我们无法将分支跳转到距给定内核函数超过 128 MB 的钩子结构。xnuspy 会在启动前检查这种情况,如果发现可能发生,则回退到内核缓存中未使用的代码来承载钩子结构。

其他说明

我尽力确保补丁查找器正常工作,因此如果有任何问题,请提交 issue。

下载工具