18 KiB
macOS 低层分支:KEXT / DriverKit(DEXT) / System Extension / 内核集合
macOS 的坑在于**三套完全不同的低层模型混在一起**:| 模型 | 执行位置 | 时代 |
|---|---|---|
| KEXT + IOKit | 内核态 | 传统 |
| DriverKit / DEXT | 用户态 | 现代驱动 |
| System Extension | 用户态框架(可承载 DriverKit / EndpointSecurity / NetworkExtension) | 现代系统能力 |
Apple 明确:KEXT 在内核态,DriverKit 与 System Extension 在用户态;macOS 11 起能用 System Extension/DriverKit 实现的功能通常不再走 KEXT 路径。所以第一条经验不是"先反汇编",而是先确认你在逆哪一代东西。
四个最容易让整条分析链跑偏的事实:
① codeless KEXT 可能根本没有代码;
② 磁盘上的 KEXT 与当前运行的可能是两个版本(AuxKC);
③ arm64e 指针不能按普通 64 位函数指针解释(PAC);
④ IOServiceOpen 失败可能停在 entitlement/activation 层,根本没走到你的 UserClient。
</CORE RULE>
一、先分类(决定后面全部)
KEXT / codeless KEXT / DEXT(DriverKit) / System Extension —— 四类模型不同,误判一次后面全错。
二、KEXT:Info.plist 先行,不要先反汇编
Foo.kext/Contents/ 下的 Info.plist 价值极高,因为 IOKitPersonalities 直接描述匹配条件:
IOClass / IOProviderClass / IONameMatch / IOPCIMatch / IOPCIPrimaryMatch / IOKitDebug / CFBundleIdentifier / OSBundleLibraries
IOKit 的 matching 模型就是"personality 字典决定哪个 driver 能绑定哪个 provider";PCI personality 甚至直接含 vendor/device ID 匹配规则。
正确顺序:
Info.plist → 确定 IOClass / Provider / hardware match → 确定依赖
→ Mach-O → 恢复 C++ class hierarchy → 找 Start / Stop / newUserClient / externalMethod
而不是从 _start 顺着 CFG 啃。
三、codeless KEXT:没有 Mach-O 不代表文件坏了
Apple 正式支持 codeless KEXT:可以完全没有 executable,只靠 Info.plist 做 hardware matching,把设备交给已有系统 driver。
Foo.kext/ └── Info.plist (没有 Contents/MacOS/Foo —— 合法)
正确分析:IOKitPersonalities → IOProviderClass → matching properties → 最终匹配到哪个 Apple driver → 去分析真正执行的那个 driver。不要浪费时间寻找"不见了的 payload"。
四、磁盘上的 KEXT ≠ 当前正在运行的 KEXT(比 Linux 的 .ko 更严重)
- macOS 11 起第三方 KEXT 不再按需 load,而是进入 Auxiliary Kernel Collection(AuxKC)、随 boot 加载
- 更新 KEXT 后,即使
/Library/Extensions/Foo.kext已是新版,reboot 前机器仍可能执行旧版本 - 卸载同理:
kmutil unload/kextunload不意味着立即从运行中的内核拔掉——新的 kernel collection 要等重启才生效,旧 KEXT 可能一直活到重启 - 所以"我明明 patch 了这个 KEXT,行为为什么还是旧的?"→ 优先怀疑 disk = version B / running AuxKC = version A,而不是"patch 没生效 / 分析错了 / 缓存没刷"
- 运行态逆向至少要区分三份:磁盘 bundle / AuxKC 中的 image / 当前 boot 中真正映射的 image(可对比运行 UUID 与磁盘 UUID)
- 推论:逆向实验若不考虑这点,极易得到假结果——"工具显示已请求 unload" ≠ "代码现在已经不存在"
五、Apple Silicon:arm64e 与 PAC 不是混淆
- Apple Silicon 的 KEXT 必须支持 arm64e,于是频繁出现
pacibsp/autibsp/pacia/autia/braa/blraa/retab,以及"地址高位看起来不对"的指针 - 不要把 64-bit raw value 直接当普通 canonical pointer——尤其是 vtable / function pointer / callback / return address
- 常见误判:"function pointer 看起来不在
__TEXT_EXEC→ pointer 被破坏了" —— 实际可能只是 PAC-signed pointer - 结论:class / vtable 重建必须 PAC-aware
六、KEXT 的重点是 IOKit class graph,不是普通 call graph
- C++ IOKit 典型生命周期:
OSObject → IORegistryEntry → IOService → YourDriver,主要围绕init/probe/start/stop/free - 真正高价值的是:class hierarchy、vtable、OSMetaClass、IOService 继承链,而不是"哪个函数 call 次数最多"
- 单独一个 KEXT 不一定够恢复完整类层级——它依赖 kernel 与其他 IOKit family 的 class/vtable;实践做法是先恢复 kernel/依赖的 class metadata,再把该 KEXT 的 class 接回完整层级
- 所以"单独 KEXT 里大量 unresolved virtual calls"不一定是 stripped 太狠,可能只是你没有它的 kernel / superclass type universe
七、攻击面优先追 newUserClient
IOService → newUserClient() → IOUserClient subclass → externalMethod()
用户态入口:IOServiceOpen → IOConnectCallMethod / IOConnectCallScalarMethod / IOConnectCallStructMethod / IOConnectCallAsyncStructMethod。
实用策略:先恢复 selector → handler 映射,而不是先分析整个硬件实现。
八、selector 不是函数地址;dispatch 会在 handler 之前做 ABI 检查
- DriverKit 保留同一核心模型:
ExternalMethod(selector, arguments, dispatch, target, reference)(在驱动内调用时dispatch为 NULL;非 NULL 时先按 dispatch 校验 arguments 再调用其function) - 字段名注意前缀:dispatch 结构里的字段是
function/checkCompletionExists/checkScalarInputCount/checkStructureInputSize/checkScalarOutputCount/checkStructureOutputSize(带check);不带前缀的scalarInputCount/structureInputSize/ … 是 arguments(IOUserClientMethodArguments)的字段,两者别混 - kext 侧的 dispatch 表是位置式(8 字节函数指针 + 四个 uint32);变长结构用
kIOUCVariableStructureSize kIOReturnBadArgument不一定是 handler 自己返回的:框架层会在 handler 真正执行之前按 dispatch 拒绝(scalar 数量 / 结构大小 / completion)。排查顺序:
selector 对吗 → dispatch entry 对吗 → scalar count 对吗 → structure size 对吗
→ async completion 对吗 → 才进入 handler 本身
- 特别是
checkCompletionExists = true时:completion 需要有效的通知 Mach port(IONotificationPortCreate+IONotificationPortGetMachPort一类);传错端口(例如主/主控端口)同样会得到kIOReturnBadArgument——不是 handler 的逻辑问题 - DriverKit 异步:
IOUserClient::AsyncCompletion取代 kext 的sendAsyncResult64();注意小structureOutput(≤4096 字节)不能异步填充,必须由ExternalMethod立即返回(只有能走structureOutputDescriptor的大缓冲才能延后填)
九、IOServiceOpen 失败:先查授权链,最后才怀疑 selector
service 是否真的存在 → DEXT 是否 active → matching 是否成功
→ entitlement → Team ID / signature → sandbox → UserClient policy
→ 最后才是 selector / 协议问题
- entitlement 名:客户端
com.apple.developer.driverkit.userclient-access(声明允许访问哪些 DriverKit service);驱动侧com.apple.developer.driverkit.allow-any-userclient-access(允许任意应用连接) - 这类失败常见症状:
provider entitlements check failed/IOUserServer(...)::exit(Entitlements check failed)
十、.dext 不要按 KEXT 逆(用户态模型)
DriverKit 驱动本质是用户态扩展,不是 ring-0 模块。分析模型:
Mach-O userspace executable → DriverKit 生成/运行时代码 → IOService subclass
→ IOUserClient → DriverKit IPC → kernel-mediated device access
立刻停止 KEXT 模型的信号:看到 Foundation/普通用户态运行时、普通 Mach ports、SystemExtension host app。
调试模型也随之不同:dext 可以按普通进程调(用户态调试器),不会因为断点把整机内核一起停掉。
十一、System Extension ≠ DriverKit 的同义词
DriverKit / EndpointSecurity / NetworkExtension 都可以作为 System Extension 运行。
看到 *.systemextension 不能推断"硬件驱动"——System Extension 本身只是"高权限 user-space extension"的安装/生命周期框架,业务语义可能属于完全不同的 API。先看:entitlements / linked frameworks / bundle metadata。
十二、Host App 与 DEXT 分离时,两边都要逆
Host.app/Contents/Library/SystemExtensions/Foo.dext
Host app 负责 activation(用 SystemExtensions framework 提交 activation request),DEXT 负责 driver。完整 RE 应同时分析:
- Host app:installation/activation、IOService discovery、
IOConnect调用 - DEXT:matching、ExternalMethod、实际驱动
selector 与输入结构常常在 client app 端更容易恢复。
十三、"DEXT 文件存在" ≠ "DEXT 已经运行"
System Extension 有独立 activation 生命周期,六态要分开:
bundle exists → registered → approved → activated → matched → started
.app 内存在 Foo.dext 只说明"安装包包含它";注册/审批/激活/匹配/启动都要各自核实(bundle identifier、filename、Team ID、签名与公证都要满足规则)。
十四、Apple Silicon 上 KEXT 不能靠 Rosetta
- Apple 文档明确:Rosetta 不翻译 kernel extension(也不翻译虚拟化 x86_64 平台的 VM app)
- x86_64 kext 在 Apple Silicon 上加载失败:
Error Domain=KMErrorDomain Code=71
"Incompatible architecture: Binary is for x86_64, but needed arch arm64e"
- 这不是安全设置 / SIP / 权限问题——只能由开发者重编为 arm64e(或 universal)
- 所以"host app 正常启动但 driver 永远加载不了"不是 UserClient bug,也不是你用错了 API
十五、Kernel Collection:运行态不是"一个 kernel + 一堆随时独立 load 的 bundle"
- 现代 macOS 的运行态涉及 Kernel Collections:kernel / Apple KEXTs / 第三方 KEXTs(第三方走 AuxKC)
- 做地址归属、KEXT inventory、symbolication、runtime-vs-disk 对比时都要把 KC 结构算进去
- 对照:不是 Linux 那种
insmod → runtime relocate → 马上出现的模型
十六、Panic 符号化:__TEXT vs __TEXT_EXEC
- arm64/arm64e 上,panic 里的 KEXT load address 不能直接当代码 image base
- 常见布局:
__TEXT从 0 开始,而可执行代码在__TEXT_EXEC→ 必须把__TEXT_EXEC.vmaddr加到 load address 再交给atos -l
# 取 __TEXT_EXEC 的 vmaddr(例如 0x4000)
otool -arch arm64e -l Foo.kext/Contents/MacOS/Foo | grep -A4 __TEXT_EXEC
atos -arch arm64e -o Foo.kext.dSYM/Contents/Resources/DWARF/Foo \
-l <load_address + __TEXT_EXEC.vmaddr> <panic_addr>
- 不加这个偏移,
atos/lldb image lookup会返回垃圾(例如解析到__TEXT.__cstring而不是指令) - 内核回溯同构:先算
__TEXT_EXEC.vmaddr与首段之差,再从 panic 的 "Kernel text exec base" 里减掉,用得到的基址跑 atos
十七、符号化必须匹配 UUID/build,不只是版本号
- panic/crash report 记录相关 image 的 UUID 与地址范围;UUID 在 crash report 格式里就是"用于匹配对应符号文件/dSYM 的 build identifier"
- 版本号(
Foo.kext 1.2.3)相同仍然不够——要 UUID + architecture + exact build - 症状对照:"函数名大概对、行号很怪、offset 一直飘" → 多半是符号文件不是同一个 build
十八、KEXT 进不了内核:签名合法 ≠ 系统会加载
Apple Silicon 上传统 KEXT 受整套 boot policy 约束:
Reduced Security(1TR 模式降级 + 勾选允许用户管理 kernel extension + 管理员密码)
+ 用户 approval + UAKL + AuxKC rebuild + reboot
- UAKL(User-Approved Kernel Extension List):用户授权的 kext 列表的 SHA384 哈希进 LocalPolicy;内核侧 kmd 校验只有 UAKL 内的 kext 才能进 AuxKC
- SIP 与签名:SIP 开启时每个 kext 的签名在进入 AuxKC 前会被校验;SIP 关闭则不强制(permissive security,用于测试未签名 kext)
- AuxKC 完整性:创建后其测量值由 Secure Enclave 签名进 Image4 结构、由 iBoot 在启动时评估;并生成 kext receipt(可能因禁用 kext 而小于 UAKL),哈希进 LocalPolicy
- 所以"完全加载不了"要分开判断:architecture / code signature / user approval / UAKL / boot security policy / AuxKC inclusion
十九、SIP 关掉 ≠ Apple Silicon 的限制都没了
别把 macOS 当"关掉 SIP 就等于 Linux root"。这一层之上还有:secure boot policy / LocalPolicy / AuxKC measurement / KIP / PAC。
- KIP(Kernel Integrity Protection):内核初始化完成后启用;iBoot 把 kernel 与 kexts 载入受保护的物理内存区,启动完成后内存控制器拒绝对该区的写入,MMU 同时阻止"从区外映射特权代码"与"区内可写映射";使能 KIP 的硬件在启动后被锁定(所以
csrutil disable不等于"内核随便 patch") - 相关:PPL(iOS/iPadOS/watchOS)、SPTM/TXM(A15+/M2+ 及以后)、SCIP(协处理器固件)、KTRR
- 做动态 patch/hook 实验前先搞清楚撞到的是哪一层:SIP / KIP / secure boot / PAC / code signing
二十、rootkit / malware 分析:落点不止 syscall hook,且强年代相关
- 历史上 KEXT rootkit 落点:
syscall、kauth、IOKit、process structures、network stack、inline patch、kernel symbol lookup(公开案例:恶意 KEXT 解析 kernel symbols、修改内核状态做进程隐藏) - 年代差异必须纳入判断:
10.x Intel ≈ 传统 KEXT 世界
11+ Intel ≈ KC / modern policy 过渡
11+ Apple Silicon ≈ arm64e + PAC + AuxKC + 严格 boot policy
- 在现代 Apple Silicon 上,"加载任意 KEXT → inline patch kernel"已比老 Intel 困难得多
- 不能拿 2012 年的 KEXT rootkit 手法直接解释 2026 年的 Apple Silicon——样本行为与年代强相关
二十一、动态调试:KEXT 本来就该双机,DEXT 反而按普通进程调
- Apple 明确:KEXT/kernel 不能像普通用户态进程那样本机调试(debugger 自己也依赖内核)。官方方案:日志 / 双机 KDP+LLDB / panic core dump / KDK(匹配 kernel 符号);需要早期或死锁现场时触发 NMI 让 target 停下等待远程调试器(Apple Silicon 的 NMI 组合键与 boot debug flags 有正式文档)
- 所以"attach lldb 到 kernel 失败"不要沿普通 userspace anti-debug 方向排查
- 反过来这是 DriverKit 最大的不同:dext 在用户态 → 优先 userspace debugger,不要一上来就搭 KDP 双机环境
决策树
拿到 macOS low-level target
├─ 先分类:KEXT / codeless KEXT / DEXT(DriverKit) / System Extension
├─ KEXT
│ ├─ Info.plist / IOKitPersonalities(IOClass/Provider/match)
│ ├─ Mach-O architecture(arm64e?PAC?)
│ ├─ class / vtable / OSMetaClass(需要 kernel/依赖的 type universe)
│ ├─ IOService 生命周期 init/probe/start/stop/free
│ └─ newUserClient → externalMethod(selector 表优先)
├─ 没有 executable → codeless KEXT → 找真正匹配的系统 driver
├─ 磁盘内容与运行行为不一致
│ ├─ AuxKC 仍是旧版本? reboot 完成了吗? disk UUID vs running UUID?
├─ Apple Silicon 约束
│ ├─ arm64e?PAC?KIP?Reduced Security?UAKL / AuxKC / LocalPolicy?
│ └─ x86_64 kext → KMErrorDomain 71(Rosetta 不翻译 kext)
├─ UserClient 调不通
│ ├─ service 是否 matched/started → selector → dispatch(count/size/completion)
│ ├─ entitlement → Team ID → sandbox
├─ symbolication 错
│ ├─ UUID / exact build(KDK) / architecture / __TEXT vs __TEXT_EXEC / runtime load address
└─ 动态调试
├─ KEXT/kernel → KDK + 双机 KDP/LLDB 或 panic core
└─ DEXT → userspace debugger
工具与验证
- 结构:
otool -l(段与__TEXT_EXEC.vmaddr)、vmmap、ioreg(IORegistry:谁绑定了什么设备)、codesign -d --entitlements :- - 集合与生命周期:
kmutil(kext 与 kernel collection)、System Information / 系统设置里的扩展列表(UAKL / 已批准项) - 符号化:
atos -arch arm64e -o <dSYM> -l <调整后基址>、lldb image lookup(先按 §16 调整基址) - 调试:KEXT → KDK + 双机 KDP/LLDB;DEXT → userspace debugger(lldb 直接 attach 进程)
- 验证:能同时说清「哪一代模型 + 哪份版本(磁盘/AuxKC/运行)+ 该管线的授权链状态(signed/approved/activated)」
该平台的坑(汇总)
- 把 dext 当"新版 kext":它跑在用户态,分析模型与调试模型都不同
- 只分析磁盘上的 kext:先对齐 disk / AuxKC / running 三份版本与 UUID
- 以为
kextunload后代码就没了:重启前可能仍在运行 - 把 codeless kext 当坏包:去逆它实际绑定的系统驱动
- 把 arm64e 的 PAC 指令当混淆 / 把 PAC 指针当损坏
IOServiceOpen连不上就怀疑 selector:先查 activated/entitlement/Team ID/sandbox- 把
kIOReturnBadArgument当 handler 逻辑失败:先查 dispatch 的 count/size/completion(含通知端口) - 把
*.systemextension一律当硬件驱动:先看 entitlements 与 linked frameworks - 只逆 .dext 不看 Host app:selector 与输入结构常在 client 端更好恢复
- panic 符号化直接减 KEXT base:漏了
__TEXT_EXEC.vmaddr - 拿版本号当 build 匹配:要 UUID + arch + exact build(KDK)
- 签名合法就以为能加载:还要 UAKL / 用户批准 / Reduced Security / AuxKC 重建 / 重启
- 以为关掉 SIP 就能随便 patch 内核:还有 KIP / secure boot / LocalPolicy / PAC
- 拿老 Intel 的 KEXT rootkit 手法套 Apple Silicon
- 在 KEXT 上硬啃本机调试:KEXT 本就该双机(KDK + KDP);但 DEXT 可以直接用用户态调试器