# OpenBSD:KARL 重链接 / autoconf / pledge·unveil **同一版本的内核,每次启动内部地址都可能不同。** OpenBSD 的 **KARL**(kernel address randomized link)会在重启前用 link kit 里的 `.o` **重新随机链接**内核,使内核内部布局发生变化。于是: ``` 机器 A: /bsd 里 foo() = X 机器 B: /bsd 里 foo() = Y 同一台机器的两次 boot 之间也变 ``` **不能据此判断"内核被 patch 了"或"版本不同"。** 第二条:设备不是"代码里挂上去的",而是**运行时递归匹配/attach 出来的**——`foo_attach()` 没有普通调用者完全正常。 ## 一、KARL:不是 KASLR | | KARL | 普通 KASLR | |---|---|---| | 什么在变 | **二进制内部布局**(对象之间的相对位置) | 同一个二进制被加载到**随机基址** | | 内核加载位置 | 仍在同一 KVA 位置 | 每次不同 | 机制要点: - 内核被链接成:**启动汇编(locore.o)固定**已知位置 → 一个**随机大小的间隙** → **其余 `.o` 随机重排**;`.rodata` / `.data` / `.bss` 之前也各有随机间隙 - 因为**函数与变量之间的距离全新**,泄漏一个指针无法推导出其他对象的位置;嵌套偏移变化也破坏了指令流的多态,可能减少变长架构上的 gadget - 运行时内核可以**反映射(unmap)自己的启动代码**;在因大页映射而无法 unmap 的架构上,改为把这段代码**破坏(trash)掉** - 基础系统里带 **link kit**(构建内核所需的全部 `.o`),由 rc 脚本在**每次启动后后台链接一个新的内核**装给下次启动——每次重启都运行一个新链接出来的内核,并顺手生成再下一个 - 安装时与升级时也会构建一个唯一内核;**若开发者把内核拷到 `/`(例如 `/bsd`)做调试,重链接机制会脱开,直到执行 `make install` 或升级** **RE 判据**: ``` /bsd 逐字节不同 → 先问:是不是 KARL/重链接内核? → 再做 symbol / semantic comparison ``` **不要**把逐字节差异直接当异常。 ## 二、autoconf:设备是运行时匹配出来的 - 设备树是**运行时递归匹配/attach** 出来的:`config_found()` / `config_attach()` 依据配置表调用各驱动的 **match / attach** - `detach` 允许**等待正在使用该设备的进程退出**(不是立即拔掉) - 因此 ``` foo_attach() ← 没有普通 call xref ``` **完全可能正常。** 真正的边是: ``` autoconf 表 → match → config_attach → foo_attach ``` ## 三、用户态侧:pledge / unveil 会让"正常程序"突然失败 这一节是**用户态**机制,但**极易被误判成"EDR/沙箱在拦截"**,所以放在这里。 - **`pledge()` 是单向棘轮**:后续调用**只能削减**可用子系统,**已被撤销的子系统不能重新获得**;试图**提升**权限会以 `EPERM` 失败(另有一种情形——继承 execpromises 的子进程提升会**被忽略并返回成功**) - 违反 promise 时,进程通常**被以不可捕获的 SIGABRT 杀掉**(尽可能留下 core)——**不是可捕获的错误返回** - 注意:**`pledge("everything", NULL)` 与"不调用 pledge"不是一回事**——promise 列表未必覆盖每一个 ioctl - `unveil()` 把文件系统视图缩小到若干路径;**`unveil` 本身也是一个 promise**——移除它(或传入两个空字符串)可禁止后续再调用 - 另有 `error` promise:违反时**不杀进程**,改为返回 `ENOSYS` - 现场线索:运行中的进程在 `ps` 里显示状态 `p`;因 pledge 违规被终止的进程由 `lastcomm` 以 `P` 标志记账 **典型现象**: ``` 程序启动正常 → 过一段时间 open("/etc/foo") 失败 → 甚至直接 SIGABRT ``` **先查是否调用过 pledge() / unveil(),而不是先怀疑外部拦截**——"**启动时能访问、之后突然不能**"恰恰是这类机制的常见设计模式。 ## 决策树 ``` 拿到 OpenBSD 目标 ├─ 比较两份 /bsd(或同一机器的两次 boot) │ ├─ 逐字节不同 → 先按 KARL/重链接解释,别判被 patch │ └─ 要做内核 diff → 走符号/语义比较,而不是字节比较 ├─ 分析环境里内核来源可疑 │ └─ 手工拷到 / 的内核会让重链接脱开(直到 make install/升级)——注意与生产环境不同 ├─ 某个 attach 函数没有调用者 │ └─ autoconf:配置表 → match → config_attach → foo_attach ├─ 用户态程序行为"中途变化" │ ├─ 查 pledge():单向棘轮、不能恢复、违规不可捕获 SIGABRT │ ├─ 查 unveil():文件系统视图被缩小 │ └─ 别先归因于 EDR/沙箱 └─ 进程莫名死掉且无常规错误 └─ ps 状态 p / lastcomm 的 P 标志是线索 ``` ## 工具与验证 - 内核:link kit 与 rc 侧的重链接脚本(判断这份内核怎么来的);符号/语义层面的 diff(而非字节) - 设备:autoconf 配置表与 match/attach 关系;`detach` 的等待语义 - 用户态:进程的 pledge/unveil 调用点与 promise 串;`ps` 状态与 `lastcomm` 记账 - 验证:能同时说清「内核是怎么来的(重链接 vs 手工安装)+ 设备是怎么挂上的(autoconf)+ 权限是怎么被削的(pledge/unveil 时点)」 ## 该平台的坑(汇总) - **拿字节差异当"内核被篡改/版本不同"**:KARL 使同一版本每次 boot 布局都不同 - **把 KARL 当 KASLR**:前者变内部布局,后者变加载基址 - **以为内核加载位置会随机**:KARL 下内核仍在同一 KVA 位置 - **找不到 attach 的调用者就判死代码**:autoconf 在运行时按配置表匹配并 attach - **以为 detach 会立即移除设备**:它可能等待使用者退出 - **把 pledge 违规当外部拦截**:那是不可捕获的 SIGABRT;且撤销的能力无法恢复 - **以为不调用 pledge 与 `pledge("everything")` 等价**:后者仍可能不覆盖某些操作 - **忽略了 unveil 之后文件系统视图已缩小**:"路径存在但打不开"可能是设计 - **在分析环境里用手工安装的内核推断生产行为**:手工拷内核会让重链接脱开