6.1 KiB
6.1 KiB
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() 没有普通调用者完全正常。
</CORE RULE>
一、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——移除它(或传入两个空字符串)可禁止后续再调用- 另有
errorpromise:违反时不杀进程,改为返回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 之后文件系统视图已缩小:"路径存在但打不开"可能是设计
- 在分析环境里用手工安装的内核推断生产行为:手工拷内核会让重链接脱开