Files
reverse-skills-max/vendor-new/rev-skills/.claude/skills/re-kernel/references/sel4-kernel.md

14 KiB
Raw Blame History

seL4 分析分支:capability 系统的逆向

遇到 seL4 系统,**首先恢复的不是"模块调用图",而是「对象图 + capability 分布 + IPC 拓扑 + VSpace/设备映射」**。 原因:seL4 的内核只提供机制,**绝大部分驱动与服务都在用户态**——设备 MMIO、IRQ 等通过 capability 暴露给用户态线程。 所以本分支的核心经验不是"内核模块怎么逆",而是**如何不把 capability 系统的行为误判成普通 OS 行为**。

一、先识别运行模型(决定后续一切)

拿到一个 ELF,别先假设它是"普通进程"。先判断它属于哪一类:

root task / 裸 libsel4 component / capDL-loaded / CAmkES component / MCS / VM(VMM) / user-space driver

  • capDL loader 按静态 specification 创建 kernel objects、CSpace、VSpace,再把 ELF 装进去 → ELF 本身 ≠ 完整系统语义:capability 的关键关系可能根本不在 ELF 的普通数据结构里,而在 capDL spec / CAmkES 生成物中
  • 经验路径:发现 capDL/CAmkES 痕迹 → 优先找 .cdl / 生成的 capDL / CAmkES ADL → 恢复对象图 → 再回 ELF 找业务代码
  • .cdl 对 seL4 RE 的价值可能比完整符号表还大

二、CPtr 是本地地址,不是全局对象 ID(最核心的一条)

seL4_Call(0x12, ...) 里的 0x12 是该线程自己 CSpace 中的 capability address——从它 TCB 里的 root CNode 起解析;另一个 component 里同样的 0x12 完全可能指向另一个对象。

所以下面这种推断是错的:

component A: Call(0x12)
component B: Recv(0x12)
   → "A 和 B 通信"          ← 错

必须恢复各自的 CSpace 映射,再按 kernel object 对齐:

A CSpace: 0x12 → Endpoint E1 [Write]
B CSpace: 0x34 → Endpoint E1 [Read]
即: A:0x12 → E1 ← B:0x34

稳定的 identity 是 kernel object,不是 CPtr 数值。

三、capDL-loaded 程序里不要照搬 root-task 语义

  • 内核为 root task 建立的初始环境是:TCB + 一个 CNode(作为 CSpace)+ VSpace;seL4_CapInitThreadCNode(slot 2)是指向初始 CSpace 的 CNode path/capability(不是它指向的对象),seL4_CapInitThreadVSpace(slot 3)同理;初始 CNode 的 guard 被选成恰好解析 32 位
  • capDL loader 以 root task 身份运行,按 spec 创建各应用的 CSpace/VSpace 并装入 ELF——这些 CSpace 来自 spec 与 loader 安装的 caps
  • 因此 capDL-loaded 应用里的 seL4_CapInitThread* 常量不能机械当成"TCB/CNode/VSpace":先确定"这是 root task 还是 loader 创建的子线程",否则整张 capability graph 会标错

四、IPC 看起来像普通函数调用?先怀疑 CAmkES glue

CAmkES 会在 component/connector 周围生成大量 glue(marshal / selector switch / unmarshal)。反编译出来的东西可能长这样:

foo_rpc_call() → marshal → seL4_Call() → unmarshal     ← 真正的业务逻辑只有 foo_rpc_call()
server: seL4_Recv() → selector switch → unmarshal → 用户实现 → marshal → Reply

不要把几十个 generated stub 当成几十个协议。 处理:识别 CAmkES → 找 connector 类型 → 标识并折叠 glue → 提取 interface / method ID / arguments / endpoint / badge / component pair,最终得到 Client.foo(x) --seL4RPCCall--> Server.foo(x)——比纯 CFG 有意义得多。

五、Badge:可能是"调用者是谁",也可能是一组事件位

  • seL4_Recv(ep, &badge):sender 信息来自 sender 所用 capability 的 badge → 看到 if (x == 4) 未必是消息类型,可能是调用者身份
  • Notification badge 可按 bit OR 聚合 → badge == 0x5 可能表示 event 0x1 + event 0x4,而不是"编号 5 的事件"
  • 判据:switch equality → 偏 identity;badge & MASK → 偏 bitset/events
  • badge 还能被 Mint 重新设置(API 允许),所以同一对象可以带多个不同 badge 的 cap

六、CSpace 查找失败 ≠ 槽为空

CSpace 是多级 CNode,且 CNode capability 自身带 guard / guard_size;寻址 CNode 还要给 depth。

lookup 失败的分类:Invalid Root / Missing Capability / Depth Mismatch / Guard Mismatch。

所以看到 seL4_CNode_Copy(..., index=0x1234, depth=32, ...) 要恢复的是 CSpace radix/guard 路径,而不是简单画一个"slot 0x1234"。

七、错误码是诊断信息,不要一律标成 generic error

错误码 含义(RE 视角)
seL4_DeleteFirst 目标 CSlot 已经有 capability(Copy/Mint/Move/Retype 要求目标为空);映射类操作表示目标地址已有映射
seL4_RevokeFirst untyped 已被 retype 过,或存在该 cap 的副本(另有 CB / IRQ handler / vspace service cap 已存在等场景)
seL4_FailedLookup index/depth 无效,或 root 是类型不对的 capability
其他 InvalidCapability、IllegalOperation、RangeError、TruncatedMessage、NotEnoughMemory、AlignmentError

这些错误码暴露了作者期望的对象状态——保留具体分支,比统一写成 if (err != 0) 有价值得多。

八、Untyped 分配:不是 malloc

  • 路径:Untyped → Retype → Endpoint / TCB / CNode / Frame / ...;对象大小 2^n 且按 2^n 对齐;retype 要求 untyped 足够大 + 目标 CNode 有足够的连续空 CSlot
  • 看到一个"按 size 降序排列的奇怪 allocator":不要当成自定义 slab 算法——很可能只是为降低 untyped retype 的对齐碎片(小到大顺序会浪费);要分配特定物理地址时,必须从 untyped 起点到目标地址按 2 的幂块全部占掉
  • 删除子 cap ≠ 物理区间立刻可用:往往要 Revoke 父 untyped 清掉派生对象,才能真正重用

九、Device untyped:只能变成 Frame(且限制更多)

  • manual:device untyped 只能 retype 成 frame 或子 untyped——不能创建 TCB / Endpoint / CNode / 页表
  • 额外限制:device frame 不能作为 IPC buffer(seL4_TCB_SetIPCBuffer → IllegalOperation)、不能用于创建 ASID pool、ARM 上不能做成可执行 frame(内核不会对设备内存做 cache 维护)
  • device 属性表示"内核不能写"的内存(MMIO,或内核窗口外的 RAM);属性会被子 untyped 继承,无法更改
  • 逆向主线:Untyped → Frame → Map → volatile MMIO 读写 → 先判"这是设备寄存器映射还是共享内存" → 物理地址 → DTB/平台内存图 → 设备 → 寄存器偏移。这才是 seL4 用户态驱动分析的主线

十、IRQ 不进内核 handler,走用户态驱动

官方路径:

IRQControl → IRQHandler capability → bind Notification
   → 硬件中断 → 内核 signal Notification → 用户驱动 seL4_Wait() → 处理 → seL4_IRQHandler_Ack()
  • 看到 seL4_Wait(ntfn, &badge) + seL4_IRQHandler_Ack(irq_cap) → 基本可判为 IRQ 处理循环
  • 中断只来一次 → 先查有没有 Ack(未 Ack 内核不会继续送后续中断),而不是先怀疑 Notification 坏了或路由配错

十一、MMIO 正常但 DMA 诡异:isolation proof 管不到 DMA

  • seL4 的形式化保证假定 MMU 能完全控制内存;普通 DMA 设备可绕过 CPU MMU 直接写物理内存,因此 DMA 不自动落在核心 isolation 证明内(需要 IOMMU/SystemMMU 才进入相应范围,且并非所有配置都在同一验证范围)
  • 所以安全逆向时:"A 没有 B 的 frame cap" 不能推出 "A 绝对改不了 B 的内存"——还要问:A 是否控制 DMA 设备?IOMMU/SystemMMU 是否启用、如何配置?DMA mapping policy 是什么?
  • 这是 capability graph 分析最容易漏掉的一条旁路

十二、Fault 不等于"线程被内核杀掉"

fault 可以作为 IPC 送给指定 fault-handler 线程(官方教程就是这种设计)——程序 fault 之后继续执行并不奇怪:

thread faults → 内核构造 fault IPC → fault endpoint → handler 收到
   → 修改 mapping/register/capability → reply/resume

所以 Recv(endpoint) → decode_fault() → write_regs() → reply() 很可能是 exception pager / fault handler,而不是普通 RPC server。

同一个 fault 反复出现时:先判断 handler 是否真的修复了 fault 原因(mapping/register/IP)——没修就恢复线程,会立刻再次触发同一个 fault(官方明确说明),不要先怀疑 kernel fault 循环坏了。

十三、MCS 与 non-MCS 必须先分辨(大坑)

MCS 内核引入:SchedulingContext(budget/period)、Reply object、passive server、SC donation、timeout fault,且 IPC ABI 有差异。

  • 拿到 binary 先判断 MCS / non-MCS,否则很容易把 reply object 误判成普通 endpoint/capability 参数
  • SC 由 untyped retype 创建、seL4_SchedControl_Configure 配置(budget == period 即时间片/轮转;budget < period 即周期任务);线程没有 SC(或被捐赠)就不能运行
  • 捐赠机制:seL4_Call 捐赠 SC、seL4_ReplyRecv 归还;server 必须处于"在 endpoint 上等待"状态才会发生捐赠
  • timeout fault 由 seL4_TCB_SetTimeoutEndpoint 注册处理
  • reply object 被撤销会打断调用链:链中间的 reply object 被 revoke/delete → SC 再也回不到发起者(内核刻意不保留这条链)——多级调用链分析时要注意

十四、两条容易被判成"配置坏了"的正常现象

  1. server 没有 SchedulingContext 却能运行:MCS 下这是合法的 passive server——它没有自己的 SC,阻塞在 endpoint 上,用 client 捐赠的 SC 执行,reply 后 SC 归还。看到"有 TCB 但没有 SC"不要判"这线程不可能运行 / capDL 配置损坏"
  2. 线程"卡住"不一定是锁:MCS 下即使优先级最高且未 blocked,仍需要有可用 budget 才能运行;budget 耗尽会等 replenishment。排查顺序:blocked? → SC bound? → budget remaining? → replenishment? → passive/donated SC?——不要只套 POSIX 的 mutex/deadlock/starvation
    • 同理:server 突然跑不动,也可能是 client 预算不足 / SC 捐赠链断了 / server 没正确 ReplyRecv

十五、capDL snapshot:静态图 vs 运行时图的 cross-view

  • 调试构建(CONFIG_DEBUG_BUILD / SEL4_DEBUG_KERNEL)下 seL4_DebugSnapshot() 输出当前内核状态的 capDL dump;seL4_DebugCapIdentify(cap) 返回 capability 的类型号
  • 注意形态:据 devel 邮件列表,seL4_DebugSnapshot 不是普通文本转储——它等待命令字(0xa0–0xff)并回传二进制数据(实现见 src/arch/*/machine/capdl.c、include/machine/capdl.h);syscall ID 随架构不同(如 AArch64 与 RISC-V32 不同)
  • 用法(很强的 cross-view):把静态推导的 capability graph 与运行时 capDL snapshot 对比——差异即运行时发生过的 cap transfer / Mint / Copy

十六、capability 图必须带 rights

同一个 endpoint,三种 cap 能力完全不同:

A: Endpoint E [Write]
B: Endpoint E [Read]
C: Endpoint E [Read, Write, Grant]

capDL 记录 Read / Write / Grant / GrantReply + badge + guard 等参数;Grant / GrantReply 直接关系 IPC 中的 capability transfer 与 reply 权限。

所以安全分析要画 object graph + rights,而不是 component ↔ component——否则会高估或低估攻击面。

逆向决策树(压缩版)

发现 seL4 binary/system
├─ 1 先识别运行模型:root task / 裸 app / capDL / CAmkES / MCS / VM(VMM)
├─ 2 重建对象图:TCB / Endpoint / Notification / CNode / Frame / Untyped / IRQHandler / SchedulingContext
├─ 3 重建 capability graph:CPtr 是本地地址 + rights + badge + guard/depth + derivation
├─ 4 重建通信:Send/Recv、Call/Reply、capability transfer、Notification、fault IPC
├─ 5 行为异常时按症状查:
│     FailedLookup → depth/guard/path/cap 类型
│     DeleteFirst  → 目标 slot 非空
│     RevokeFirst  → 存在派生/子对象或副本
│     fault 循环   → handler 没修 fault
│     线程不跑     → MCS 的 SC/budget/捐赠链
│     passive server 不跑 → SC donation
│     IRQ 只来一次 → 没 Ack
└─ 6 涉及设备时:device untyped → MMIO frame → Notification↔IRQ → Ack → DMA 另查 IOMMU/SystemMMU

工具与验证

  • 静态:capDL-tool(解析 .cdl 得到对象与 cap 分布)、CAmkES 生成的 glue 与 ADL、readelf(ELF 层)
  • 动态:QEMU 仿真目标系统;调试构建下用 seL4_DebugSnapshot(二进制 capDL 协议)/ seL4_DebugCapIdentify
  • 验证:能同时给出「运行模型判定 + 对象图 + 带 rights 的 capability 图」——只给"两个 component 通过 Endpoint 通信"是不够的

该平台的坑(汇总)

  • 把 seL4_Call(0x12) 里的数值当全局 ID:CSpace 本地地址,跨 component 无意义
  • 对 capDL-loaded 应用照搬 root-task 的 slot 语义:整张 capability graph 会错
  • 把 CAmkES glue 当协议:先折叠生成代码
  • 把 badge 当 enum:可能是身份,也可能是 bit 聚合的事件集
  • 把 FailedLookup 当"槽为空":还可能是 depth/guard/path/类型问题
  • 把错误码统一成 generic error:丢掉作者期望的对象状态
  • 把 device untyped 当普通内存:它只能变 Frame,还不能当 IPC buffer / ASID pool / 可执行 frame
  • IRQ 只来一次就怀疑内核:先看有没有 Ack
  • "A 没有 B 的 cap" 就断定隔离成立:忘了 DMA 旁路(查 IOMMU)
  • fault 后程序继续跑 = 见鬼:那是 fault handler 在按设计工作
  • 把 MCS 的 reply object 当普通 cap 参数;把 passive server 当配置损坏;把 budget 耗尽当死锁