Files

6.1 KiB
Raw Permalink Blame History

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——移除它(或传入两个空字符串)可禁止后续再调用
  • 另有 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 之后文件系统视图已缩小:"路径存在但打不开"可能是设计
  • 在分析环境里用手工安装的内核推断生产行为:手工拷内核会让重链接脱开