reverse-skills-max: initial public release (restored, redacted, hardened)

This commit is contained in:
baradok609hecl1pq6qi
2026-10-20 02:17:15 +03:00
commit da24c2bd20
2227 changed files with 443931 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
# Android 内核模块分支(GKI / vendor module)
**前提认知**:Android 内核**不能只当普通 Linux** 处理——模块身份(GKI / vendor)、KMI 符号白名单、分区与加载阶段都会决定"这份模块到底能不能加载、为什么加载失败"。
## 一、先分清模块身份
- **GKI module**(Google 提供、随 GKI kernel 签名) vs **vendor module**(OEM/自建、未签名)
- 三方符号权限不同:**protected GKI module / unprotected GKI module / vendor module** 各有可访问的符号集合;vendor(unsigned)模块**只能使用 KMI 符号**,越界会被拒绝加载
- 构建期检查:ACK `kernel/build` 的 `abi/check_buildtime_symbol_protection.py`——unsigned 模块里所有 undefined symbol 必须在 **KMI 符号清单**内,或被另一个 unsigned 模块定义;否则**即使构建成功,运行时也会被拒绝加载**(构建期只是提前告警)
- 运行时症状:`module: Protected symbol: xxx (err -13)` / `module: exports protected symbol xxx`
→ **不要按普通 Linux"找不到符号 → patch 地址"处理**:这是 GKI 的符号保护策略,先识别 GKI protection / KMI 边界
- KMI 符号清单位置:`android/abi_gki_aarch64.xml` 与各子系统 fragment 文件
## 二、KMI 分支 ≠ 内核版本
- KMI 稳定性**只存在于同一 LTS + 同一 Android 大版本的组合**(例如 `android14-6.1`、`android15-6.6`、`android16-6.12`);`android-mainline` 不保证 KMI 稳定
- 因此 **`android12-5.10` 与 `android13-5.10` 虽然都是 Linux 5.10,KMI 并不兼容**:用某一分支的 `Module.symvers` 构建的模块,与另一分支的 kernel/GKI 模块对不上;GKI 模块由 Google 签名,"只与它构建时对应的 GKI kernel 兼容"
- 分析/移植时的正确判据:精确到 **`androidXX-X.Y` + KMI generation + symbol list + `Module.symvers`/CRC**,而不是 `5.10 == 5.10`
## 三、模块在哪里、会不会被加载
- 查找位置(**不要硬编码 `*_dlkm` backing 路径**;AOSP 建议外部逻辑访问 `/vendor/lib/modules`、`/odm/lib/modules`):
`/vendor/lib/modules`、`/odm/lib/modules`、`/system/lib/modules`(早期模块在 vendor ramdisk 的 `/lib/modules`)
- 实际 backing 分区可能是:`vendor_dlkm` / `odm_dlkm` / `system_dlkm`
- 加载顺序与依赖:`modules.load`、`modules.dep`、`modules.softdep`
- first-stage init 从 vendor ramdisk 的 `/lib/modules/modules.load` 按序加载 early modules,**实际顺序会因 hard/soft dependency 变化**
- 晚期 vendor 模块可能来自 vendor/ODM DLKM
- **文件存在 ≠ 会被加载**;而且依赖的模块**可能位于另一个分区 / 另一个 boot stage**
## 工具与验证
- `modinfo`(vermagic / 依赖 / 签名)、`readelf -S`(`.modinfo` / `.BTF`)、`/proc/kallsyms`、`/sys/module/<name>/`
- GKI 侧:以目标分支的 symbol list 与 `Module.symvers` 做比对;签名侧见 `scripts/sign-file`(GKI 由 Google 签名、vendor 由 OEM 签名,key 需在 AVB 信任链内)
- 验证:能同时给出「模块身份(GKI/vendor)+ KMI 分支 + 所在分区 + 是否在 `modules.load` 计划内」
## 该平台的坑
- **把 `android12-5.10` 与 `android13-5.10` 当同一个内核**:KMI 不兼容
- **见到 `Protected symbol` 就按普通 Linux 去 patch 符号**:那是 GKI 策略,不是符号缺失
- **硬编码 `*_dlkm` 路径去找模块**:按 AOSP 建议走 `/vendor/lib/modules` 等入口
- **认为"文件在 = 已加载"**:要看 `modules.load`、依赖与分区
- **忘了模块可能是 built-in**:Android 侧同样存在(与 Linux 分支同理)

View File

@@ -0,0 +1,99 @@
# FreeBSD 内核:linker sets / SYSINIT / KLD
<CORE RULE>
FreeBSD 表面上和 Linux 很像——**但一个关键差异会让整套"可达性"推理失效**:
```
SYSINIT() 并不是生成 "_start → call init"
而是把一个 struct sysinit 放进 linker set
内核启动 / 模块加载时统一排序后执行
```
所以:
```
foo_init() ← 没有任何 call 交叉引用
```
**可能完全正常。** 应当恢复的是
```
func 地址 → struct sysinit → linker set
```
而不是把它标成 dead code。**"找不到调用者"在 FreeBSD 上首先是一条关于 linker set 的线索,而不是一条关于死代码的线索。**
</CORE RULE>
## 一、SYSINIT / SYSUNINIT:带序号的注册项
- `SYSINIT()` 创建一个 `struct sysinit` 放进**启动 linker set**;`SYSUNINIT()` 放进**关闭 linker set**
- 结构包含四个字段:**子系统标识(`SI_SUB_*`)、子系统内次序(`SI_ORDER_*`)、函数指针、传给它的数据指针**
- 启动时扫描启动 linker set,**生成排好序的初始化例程表**再依次执行
- 排序键:**`SI_SUB_*` 为主键,`SI_ORDER_*` 为次键**,各自升序
- **同一 sub 且同一 order 的两个例程,相对顺序未定义**——不要从"源码里的先后"推断运行顺序
- `SI_SUB_DUMMY` 一类条目会被跳过;条目执行后会被标记(如 `SI_SUB_DONE`)
## 二、linker set 机制本身:识别它,才谈得上识别"注册"
- linker set 利用链接器把**多个编译单元里静态声明的数据聚成一个连续、可寻址的整体**
- **现代 ELF 实现**用 "magic ld symbols" 定界:**`__start_set_<集合名>` / `__stop_set_<集合名>`**,条目落在形如 **`set_<集合名>`** 的 section 里
- **老实现(a.out 时代)**是 `struct linker_set { int ls_length; void *ls_items[...]; }`,**且条目列表以 NULL 结尾**
- **现代实现里列表不再以 NULL 结尾**——**按"结尾是 NULL"去扫描会直接越界**。范围只能由 `__start_set_*` / `__stop_set_*` 边界算出
- 宏层提供遍历/计数(`SET_BEGIN`/`SET_LIMIT`/`SET_FOREACH`/`SET_COUNT`)——**反编译器一侧看到的是纯数据指针数组,没有这些 API 名**
**通用判据(值得跨系统复用)**:
```
大量连续排布的 pointer / object
+ section 名形如 set_*
→ 优先按 linker-set 语义解释,而不是 jump table / 混淆表
```
而且 linker set 不止 `SYSINIT` 一处在用——FreeBSD 内核里有**很多** `set_...._set` 形式的集合。**看到 `set_` 前缀先想 linker set。**
## 三、KLD:运行期还有一层 kernel linker
- **预加载模块**(随内核由 boot loader 载入):其启动 linker set 在 **`SI_SUB_KLD`** 子系统初始化时被扫描、排序并**并入内核的启动表**,随后在启动过程中执行
- **由此产生一条反直觉结论**:预加载模块里**排序在 `SI_SUB_KLD` 之前的初始化例程,实际会在 `SI_SUB_KLD` 之后才运行**——"它声明得很早,为什么跑得晚"有确定的机制解释
- **运行期加载的模块**(`kldload`):加载时扫描其启动 set、排序、执行
- **卸载**:扫描关闭 set、排序、执行,**拆除顺序与初始化顺序相反**
- **内核与已加载模块的拆除例程在系统关机时并不执行**——"关机时应该走某条清理路径"这类推断不成立
## 决策树
```
拿到 FreeBSD 内核 / 模块目标
├─ 某个 init/注册函数没有 call 交叉引用
│ ├─ 查它是否被 SYSINIT/SYSUNINIT 引用(func 地址 → struct sysinit)
│ ├─ 查它落在哪个 linker set(section 名 set_*)
│ └─ 别标 dead code
├─ 一大段连续指针/对象、section 名 set_*
│ └─ 按 linker set 解释(可能有 count 字段、可能没有;现代 ELF 无结尾 NULL)
├─ 顺序问题
│ ├─ 排序键是 SI_SUB_* 主 + SI_ORDER_* 次
│ ├─ 同 sub + 同 order → 相对顺序未定义,别从源码顺序推运行顺序
│ └─ 预加载模块:早于 SI_SUB_KLD 的项实际在 SI_SUB_KLD 之后才跑
├─ 模块相关
│ ├─ 加载:扫描-排序-执行启动 set
│ ├─ 卸载:执行关闭 set,顺序与初始化相反
│ └─ 系统关机不执行内核与已加载模块的拆除例程
└─ 与 Linux 对照的常见误判 → 见下方"坑"
```
## 工具与验证
- 定位注册项:由函数地址回溯到 `struct sysinit`,再由它所在的 section 归属到具体 linker set
- 枚举集合:按 `__start_set_*` / `__stop_set_*` 边界计算范围与条目数(**不要依赖结尾 NULL 或固定 count 字段**)
- 模块生命周期:预加载 vs `kldload` 两条路径分别核对(排序时机不同)
- 验证:能同时说清「注册方式(linker set / 直接调用)+ 所属集合与排序键 + 加载路径(内建/预加载/运行期)」
## 该平台的坑(汇总)
- **找不到 call 交叉引用就判 init 不会运行**:`SYSINIT()` 通过 linker set 注册,源码层面本来就没有调用点
- **按"结尾 NULL"扫描 linker set**:那是 a.out 时代的老结构;现代 ELF 用边界符号定界,**按 NULL 扫会越界**
- **把 `set_*` 的指针数组当 jump table / 混淆表**:它更可能是内核自己的注册集合
- **从源码里的先后推断初始化顺序**:排序由 `SI_SUB_*` + `SI_ORDER_*` 决定,同 sub 同 order 的相对顺序**未定义**
- **以为预加载模块里声明得早的初始化就跑得早**:它们在 `SI_SUB_KLD` 时才被并入内核启动表
- **以为关机时会执行内核/模块的拆除例程**:不会
- **把卸载时的执行顺序当成初始化顺序**:拆除顺序与初始化相反
- **拿 Linux 的 `module_init`/`__initcall` 模型直接套 FreeBSD**:注册机制与排序键都不同(对照见 [[linux-kernel]])

View File

@@ -0,0 +1,107 @@
# Genode:组件树 / session / quota 捐赠 / 局部命名
<CORE RULE>
Genode 的核心不是某个固定微内核 API,而是:
```
component tree + parent 中介的服务 + session + capability + quota
```
两条容易误导的规则:
① **服务名只在局部 parent 层级里成立**——两个程序都请求 `"Foo"`,**不能推出它们连到同一个 server**;
② **capability 整数是 protection-domain 局部的**——数值相同不代表同一对象,数值不同也可能是同一对象。
**与 seL4 的 CPtr、Zircon 的 handle 属于同一类坑**([[sel4-kernel]]、[[zircon-kernel]])。
</CORE RULE>
## 一、递归的组件树
- 每个程序运行在**自己的 sandbox** 里,并可用自己的资源**创建和管理子 sandbox**——形成严格的层级,策略在每一层生效
- 站在 **parent-child 接口**上看,只有两件事受 parent 策略约束:**child 宣告服务**、**child 请求服务**
- parent 接口的操作:
- **session**:child 请求连接到一个(parent 所知的)服务;参数里包含**愿意在 session 生命周期内捐给 server 的资源**
- **close**:child 表示不再需要该 session;parent 应关闭它并**退还捐赠的资源**
- **announce**:child 把本地实现的服务注册到 parent(从而成为 server)
- **transfer_quota**:child 扩大它对提供该 session 的 server 的资源捐赠
## 二、路由与"名字重映射"
- child 宣告服务后,parent 用**本地名字**保存 server 的 root capability(放进自己的 root_list);**root capability 只供 parent 使用与保管**
- parent 调 server child 的 root 接口创建 session,server 返回 session capability,parent 再用本地名字记录(session_list)
- 这一过程**递归**:**服务名不必相同,其含义只延伸到直接 parent,每一层都可以重映射**
- parent 对请求有四种处理:**拒绝**、**委托给自己的 parent**、**本地实现**、**在自己的其他 child 上开 session**
- init 用 XML 的 `<route>` 表描述路由(`<service name=...>` 下可选 `<parent/>` 或 `<child name=.../>`),支持通配(`<any-service>`、`<any-child>`)与 `<default-route>`
- 路由可基于 **session label**:label 在路由途中被逐级加前缀(**最靠近 server 的部分最可信,来自 client 的部分最不可信**);匹配属性包括 `label`、`label_prefix`、`label_suffix`、`unscoped_label`、`label_last`
- **label 可以被重写**——目标节点里的重写可以**对 server 隐藏客户端身份**
**所以恢复"谁连到谁"必须看路由表与 label 链,而不是看服务名。**
## 三、quota 捐赠:路径上的每一级都要"吃一口"
- session 构造参数携带 client 捐出的资源
- **parent 会从 child 捐赠的 session quota 里扣掉本地记账开销**——于是"**session 请求路径上的每个组件都会从配额里咬一小口**"
- 结果:**到达 server 的配额可能已被消耗**,server 可以返回 `QUOTA_EXCEEDED`;客户端一侧随后会**以逐步增大的配额重试**
- capability 预算同理:每个 `<start>` 节点有 `caps` 属性(建一个组件约需 35 个 cap,100 是常见实用值),cap 预算也可以在 client 与 server 之间转移
- 运行时可动态调整:child 可请求追加资源,也可被要求让出资源
**所以**:
```
全系统还有 100 MB 空闲
某 child 只剩 8 KB quota,创建 session 失败
```
**不矛盾**。排查顺序:全局物理内存 → 当前 PD 的 quota → 该 session 捐出的 quota → capability 预算。
## 四、ROM session:配置可以更新,"ROM 内容变了"不是篡改
- 每个由 init 启动的组件通过向 parent **请求名为 `"config"` 的 ROM module** 获得配置;init 拦截该请求,只交付**与本 child 相关的那部分**配置;这一机制递归成立
- **ROM session 在生命周期内支持动态更新**(client 与 server 之间有专门的更新/通知协议),因此长期运行的组件配置可以被动态改变
- 嵌套配置使一棵组件树可以由单份配置描述
**所以**:ROM dataspace 从版本 A 变成版本 B 是**合法机制**,不是"只读内存被改写"。
## 五、健康与生命周期信号
- 组件在 parent 注册信号处理器(资源可用、yield、session、heartbeat 等)
- **heartbeat**:组件初始化时向 parent 注册心跳处理器,parent 触发信号后由默认处理器回应心跳——init 借此监控子组件健康
- init 支持 state report 与 exit 传播(可把子组件的退出事件转发给自己的 parent)
## 决策树
```
拿到 Genode 目标
├─ 谁连到谁
│ ├─ 看 parent 的路由表(<route>)与 label 链,而不是服务名
│ ├─ 同一服务名在两层 parent 下可能指向不同 server
│ └─ label 可能被重写(对 server 隐藏身份)
├─ capability 相关
│ ├─ 数值只在当前 PD 内有效;相同数值可能无关,不同数值可能是同一对象
│ └─ 按 session/对象对齐,而不是按整数对齐
├─ 资源申请失败
│ ├─ 依次查:全局物理内存 → 当前 PD quota → session 捐赠配额 → cap 预算
│ └─ 路径上的每一级都会扣掉本地开销
├─ ROM 内容变化
│ └─ ROM session 支持更新协议,"内容变了"属合法机制
└─ 组件消失/重启
└─ 查 heartbeat 监控与 exit 传播链
```
## 工具与验证
- 拓扑:init 的 XML 配置(`<route>` 表、`<start>` 节点、`<report>`、`<exit>`)与 label 匹配/重写规则
- 资源:各节点的 quota 与 caps 配置;session 构造参数中的捐赠额;运行时的资源请求/让出
- 通信:announce(谁是 server)与 session(谁连谁),以及中间的转发层级
- 验证:能同时说清「服务的真实归属(哪一层提供的)+ capability 在哪个 PD 的命名空间里 + 资源是从哪一级配额里出的」
## 该平台的坑(汇总)
- **用服务名判断"连到同一个 server"**:名字只在局部 parent 层级有意义,且每层可重映射
- **把 capability 整数当全局标识**:它是 PD 局部命名;相同数值可能无关,不同数值可能同源
- **忽略 label 重写**:客户端身份可能被有意隐藏
- **把 root capability 当可自由传播的东西**:它只供 parent 使用与保管
- **用"系统还有内存"否定 OOM**:配额是分级的,且路径上每一级都扣过开销
- **忽略 capability 预算**:cap 与 RAM 一样是受限资源
- **把 ROM 内容变化当内存被篡改**:ROM session 本身支持更新
- **忽略 heartbeat/exit 传播**:组件消失/重启有既定的监控与通知路径

View File

@@ -0,0 +1,133 @@
# Haiku:device_node 树 / 内核对象模型 / KDL / BeOS 兼容边界
<CORE RULE>
Haiku 有**两套容易看错的东西**,而且**逆驱动与逆系统组件的入口完全不同**:
```
驱动侧:device_node 树 + on-demand exploration + driver module / device module 两层
系统侧:team / thread / area / port / semaphore 这套内核对象模型 + 用户态服务
```
**如果目标不是驱动**,别继续在 Device Manager 里找——换到内核对象模型与用户态服务那一层。
</CORE RULE>
## 一、驱动侧:device_node 树与按需发现
- 设备管理器围绕 **device_node** 构建:每个驱动发布一个或多个节点,构成一棵动态表示硬件的树
- 一个 device_node = **module + attributes + resources** + 父/子指针;**module 是唯一必需的部分**
- 启动时只有根节点;主要总线注册上去;**智能总线为自己发现的每个设备创建子节点**
- **按需探索**:需要某功能时系统扫描设备文件系统,按内建规则把设备路径翻译成 device node(智能总线上的节点带 PCI 式类型信息),**只查询匹配的模块目录**
```
boot 时驱动没出现、后来访问某设备时才出现
→ 可能只是 on-demand exploration
```
- 有"只在系统要求时才被搜索"的按需标志
## 二、驱动侧:两层模块(driver module / device module)
**driver module**(名字必须以 `driver_v1` 结尾)实现:`supports_device`、`register_device`、`init_driver`/`uninit_driver`、`register_child_devices`、`rescan_child_devices`、`device_removed`、`suspend`/`resume`(**后两个从不被调用**)
- `register_device` 注册节点并可**认领 I/O 资源**(端口或内存范围)——**同一资源只能被一个节点认领**
**device module**(名字必须以 `device_v1` 结尾,经 publish 一类调用发布到 `/dev`)导出:`init_device`/`uninit_device`/`device_removed`、`open`/`close`/`free`/`read`/`write`/`io`/`control`/`select`/`deselect`
**在 driver module 里找不到 `read`/`write` 很正常**——那是 device module 的接口。
- 节点属性含显示名、唯一 ID、**固定子节点**、厂商/设备/类型/子类型/接口、标志
- **固定子节点与动态子节点不能混用**,注册时机也不同(固定在前)
- 较新版本给节点加了"路径/驱动名"属性,用于显示某设备由哪个驱动、走哪条 `/dev` 路径
## 三、系统侧:内核对象模型
逆一个**非驱动**的目标时,入口在这里:
| 对象 | 说明 |
|---|---|
| **team** | 进程等价物:地址空间与资源容器 |
| **thread** | 执行单位,隶属 team |
| **area** | 内存区域(可共享/映射),是"内存从哪来"的答案 |
| **port** | **内核消息队列**——线程间、team 间通信的基本手段 |
| **semaphore** | 同步原语 |
- **信号量的行为有若干标志位影响**:可中断(即使调用方没要求,也可能被终止信号打断)、**释放全部等待者**、**只在有等待者时才递减**——后两者正是实现条件变量语义所需要的
- **调试 API 与这些对象一一对应**:调试器侧有表示 area / semaphore / image / team / thread / syscall 信息的模型类
**逆向含义**:定位"这块内存在哪、谁在等谁、谁给谁发消息",答案在 area / port / semaphore,而不是驱动框架。
## 四、KDL 与内核调试接口
**KDL** 是内建在内核里的交互式命令调试器,进入方式包括内核 panic、显式的调试器调用、**紧急组合键**、以及硬件异常(缺页、保护违规)。
常用命令:帮助、**栈回溯**、继续执行、**线程列表**、**切换 CPU**、dump 系统日志、**查内存**、重启。
- **命令系统可扩展**:各内核子系统可以注册自己的命令
- 相关输出机制各有适用上下文(在内核调试器上下文里用的打印与常规内核打印不同)
- **调试 API 的细节对分析很关键**:
- 发给调试器的消息**以一个含线程与 team ID 的头结构开头**,后来又加了**调试端点端口**字段,使同步调试消息能带上调试通道
- 存在一组 **team 调试标志**:team 创建事件、线程事件、**image 事件**、停止新线程、以及**系统调用前追踪**
- **系统调用参数缓冲区在 64 位平台上被扩大过**——此前在追踪时有内核内存破坏风险;调试器侧相应按目标架构区分
**注意**:**这个调试接口与 BeOS"概念相似,但源码与二进制都不兼容"**——拿 BeOS 时代的调试器接口假设去套会直接落空。
## 五、BeOS 兼容边界(别把它当"就是 BeOS")
- 目标是**对 BeOS R5(x86)大范围二进制与源码兼容,但反过来不成立**;某些情况下**刻意破坏源码兼容以保住二进制兼容**
- 已知差异举例:头文件路径从 `be` 变为 `os`;部分头文件名不同;类的私有字段改名;一些未文档化的 API 未实现;**私有的设备映射 API 被磁盘设备 API 取代**
- **调试接口不兼容**;**文件系统 API 也变过**(BeOS 时代的文件系统驱动不能用)
- 对更早版本的兼容支持已被放弃
- **内核内部的改动也可能显式破坏兼容**——例如某次为做锁竞争测量把自旋锁改成结构体,就明确标注会破坏二进制兼容
## 六、用户态服务边界
不是所有"Haiku 组件"都在内核里:图形服务器、应用注册服务一类是**用户态服务**,它们有自己的协议与生命周期。逆这类目标时:
```
不要找内核驱动接口
要看用户态服务的通信端点与协议
```
## 决策树
```
拿到 Haiku 目标
├─ 先判:驱动 / 内核子系统 / 用户态服务
│ ├─ 驱动 → device_node 树 + 两层模块
│ ├─ 内核子系统 → team/thread/area/port/semaphore + KDL
│ └─ 用户态服务 → 服务端点与协议(不在内核)
├─ 驱动"不存在"
│ ├─ 按需发现:只有被请求时才会搜索/加载
│ └─ 查总线类型规则与按需标志
├─ 在 driver module 里找不到 read/write
│ └─ 那是 device module(device_v1)的接口
├─ 内存/同步/消息相关异常
│ └─ 落到 area / semaphore(注意其扩展标志位)/ port
├─ 现场调试
│ ├─ KDL 可查栈回溯、线程、内存、系统日志、切换 CPU
│ └─ 调试接口与 BeOS 不兼容,别套旧接口
└─ 兼容性相关判断
├─ R5 二进制/源码兼容是单向的
└─ 调试接口与文件系统 API 属于不兼容的部分
```
## 工具与验证
- 驱动:device_node 树(module/attributes/resources)、两层模块的接口集合与命名约束、资源认领唯一性
- 系统:team/thread/area/port/semaphore 的归属与关系;信号量标志位
- 调试:KDL 命令与调试 API 消息结构(头部字段、team 调试标志、syscall 追踪)
- 兼容:目标是否来自 BeOS 时代(调试接口/文件系统 API 会直接落空)
- 验证:能同时说清「这个目标是驱动还是系统组件 + 它在哪一层、由谁加载 + 它用的对象属于哪套模型」
## 该平台的坑(汇总)
- **把按需发现当驱动缺失**:初始只探索最小集合
- **在 driver module 里找 `read`/`write`**:那是 device module 的接口
- **忽略 `driver_v1` / `device_v1` 命名约束**
- **期待 `suspend`/`resume` 被调用**:当前实现不调用
- **混用固定子节点与动态子节点**
- **逆非驱动目标时仍钻进 Device Manager**:入口应是内核对象模型或用户态服务
- **忽略信号量的扩展标志**:可中断、释放全部、仅在等待时递减会改变行为
- **拿 BeOS 的调试接口或文件系统 API 假设去套**:两者都不兼容
- **以为兼容是双向的**:只保证"旧程序能跑",不保证新接口回退
- **忽略 64 位下调试 API 缓冲区结构的变化**:按旧布局解析会错

View File

@@ -0,0 +1,95 @@
# HelenOS:fibril / 用户态 DDF 驱动 / devman
<CORE RULE>
HelenOS 是 multiserver 微内核,**驱动是用户态 server 任务**。有两层容易看错的东西:
```
kernel 抢占调度的 thread
└─ 用户态协作调度的 fibril(内核完全不知道它存在)
```
于是:
```
一个 OS thread 内发生大量"线程切换"
→ 可能只是用户态 fibril 调度器在切,内核视角什么都没发生
```
**在 thread 视角找"这个 thread 为什么被抢占"会一无所获。**
</CORE RULE>
## 一、fibril:内核不知道的执行实体
- 每个任务(process)含若干**由内核抢占调度的 thread**;每个 thread 又可含若干**由用户态库协作调度的 fibril**
- **内核不知道 fibril 的存在**——调度、切换、同步全在用户态完成
- 驱动框架大量使用 fibril:`libdrv` 用 fibril 互斥量保护设备表与驱动状态
- devman 也会**在单独的 fibril 里跑驱动初始化**,以避免"驱动初始化期间还需要 devman 服务"造成的死锁
**RE 判据**:看到密集的用户态"上下文切换"痕迹却对不上内核调度,先怀疑 fibril;看到 driver 内的互斥/让出调用,注意它们是 fibril 级而非 thread 级。
## 二、DDF:用户态驱动框架
DDF(generic device driver support)实现在**用户态库**(`uspace/lib/drv`,公共头 `ddf/driver.h`)。关键结构:
| 结构 | 作用 |
|---|---|
| `driver_t` | 驱动本体:二进制路径、设备表、fibril 互斥量、名字、phone、状态,以及 **`driver_ops_t *driver_ops`** |
| **`driver_ops_t`** | 通用驱动操作——**由通用连接处理器在有来连接时调用** |
| `ddf_dev_ops_t` | 设备/功能层的操作 |
生命周期围绕 device / function:创建(`ddf_fun_create`)→ 加 match id(`ddf_fun_add_match_id`)→ 绑定(`ddf_fun_bind`)→ 上线(`ddf_fun_online`)一类调用;软状态用 `ddf_dev_data_alloc` / `ddf_fun_data_alloc` 管理。
入口形态:驱动主函数把 `driver_t` 存好(**"driver_ops 将由通用处理器在有来连接时调用"**),经 `devman_driver_register(name, connection)` 向设备管理器注册,设置中断处理,然后进入异步管理器循环。**devman 发来"添加设备"消息时,通用代码调用 `driver->driver_ops->add_device(dev)`**。
**RE 含义**:驱动内没有"主循环分发"很正常——分发在通用层,业务在 `driver_ops` 的回调里。
## 三、devman:设备树与匹配
devman 是**用户态服务**,维护:
- **设备树**(device node / function node)与驱动列表
- **匹配评分**:按 match id 计算得分并选最优驱动
- 自动启动驱动、设备枚举、热插拔处理
相关动作:注册运行中的驱动、分配驱动、启动驱动任务;**子设备由其父驱动的注册动作加入**,function 可注册进 class 供 device mapper 使用。
**RE 主线**:
```
match id / 评分 → devman 选中驱动 → 启动该驱动任务 → DDF 回调 → 设备功能
```
**不要去找内核侧的 driver dispatch table**——它在用户态服务里。
## 决策树
```
拿到 HelenOS 目标
├─ 找驱动
│ ├─ 用户态 server 任务(不是内核模块)
│ ├─ 匹配与启动在 devman;业务在 driver_ops 回调
│ └─ 不要找内核 dispatch table
├─ "线程切换"异常密集
│ ├─ 先区分:内核抢占的 thread vs 用户态协作的 fibril
│ └─ 内核不知道 fibril,别在内核调度里找证据
├─ 驱动初始化看起来卡住
│ └─ 注意 devman 在单独 fibril 里做初始化(避免重入死锁)
└─ 设备与驱动的归属
└─ 走设备树(device node / function node)与 match 评分
```
## 工具与验证
- 执行实体:thread 与 fibril 的区分(用户态调度库、fibril 互斥量的使用点)
- 驱动:`driver_ops_t` 回调集合与 `ddf_*` 生命周期调用
- 拓扑:devman 维护的设备树与 match id/评分
- 验证:能同时说清「执行实体在哪一层(kernel thread / user fibril)+ 驱动在用户态如何被匹配与启动 + 业务回调落在哪个 ops」
## 该平台的坑(汇总)
- **把 fibril 当内核线程**:内核不知道 fibril,调度证据对不上
- **在内核调度里找 fibril 切换的证据**:找不到是正常的
- **找内核 driver dispatch table**:驱动是用户态 server,分发在 DDF 通用层
- **以为驱动有自己的分发主循环**:业务在 `driver_ops` 回调里
- **忽略 devman 的匹配评分**:谁绑定谁由 match id 评分决定
- **把驱动初始化的"卡住"当 bug**:devman 在独立 fibril 里初始化以避免重入死锁

View File

@@ -0,0 +1,169 @@
# HIC:capability + 物理沙箱 + 多版本驱动系统
<CORE RULE>
**在 capability / sandbox / 多版本 / live-update 型驱动系统里,不从 CPL、地址、直接调用或 MMIO 指令推导信任边界。**
先恢复 **domain、capability、mapping、device ownership、version、lifecycle topology**,再解释控制流。
具体到 HIC:**Privileged-1 与 Core-0 处于相同物理特权级、靠 MMU 隔离**;驱动只映射自己被授权的 MMIO/共享内存,同时特定 privileged 服务还存在更宽的物理内存访问通道。所以下面这条老推理
```
Ring 0 → kernel driver → full kernel addressability
```
**必须直接标成 invalid assumption**。
</CORE RULE>
## 一、跨系统对照:这些规则从哪来
| 真实系统 / 机制 | 实际经验 | 迁移到本类的规则 |
|---|---|---|
| Fuchsia DFv2 | 驱动跑在 driver host 内;不同驱动可分处不同进程,也可 **co-locate 到同一 driver host 共享地址空间**;driver manager 负责拓扑/绑定/capability 路由 | **不要用"是否直接 call"判断驱动边界**。两个逻辑独立的驱动可能共享地址空间;同一设备栈也可能跨多个隔离域。先恢复 driver/node/host 拓扑 |
| Fuchsia 崩溃恢复 | 驱动 crash 后可重新 spawn,但**重新 bind 时硬件可能已处于未知状态**,初始化不能假设设备是 reset 状态 | 驱动初始化里那些"看起来多余"的 reset / 状态探测**不要删成 boilerplate**——它可能是 sandbox restart / recovery path |
| Fuchsia 绑定元数据(DFv1) | 可把 bind program 放进 ELF NOTE,让协调者在**不完整加载驱动**的前提下检查绑定条件 | **先解析模块元数据,再反汇编**——模块自带 UUID、版本、端点、资源需求、依赖与签名,这些就是第一层 triage |
| seL4 / CAmkES | MMIO 可表现为 dataport / 共享内存映射;IRQ 可表现为 event / notification;RPC、共享 dataport、直接调用都可能存在 | **不能通过指令形态推断安全边界**:`mov [addr]` 可能是被授权的 MMIO;普通函数调用可能跨逻辑组件;共享页也可能是 capability 中介的 IPC |
| Xen frontend/backend | 设备协议由 shared ring + grant table + event channel 构成;**grant reference 是跨域页面授权,不是普通地址**;迁移后 event-channel 本地端口甚至可能变化 | 看到整数 handle / 页索引 / ring offset 时**不要先类型化成 pointer**。先判断它是 capability、共享页偏移,还是 domain-local 标识 |
| Xen grant table | 本质上承担**类似 paravirtual IOMMU 的角色**;页面所有权与跨域访问权是**独立状态** | 内存取证要区分 **VA / PA / backing frame / capability / owner domain / mapped domain**——只存虚拟地址远远不够 |
| Genode | 驱动可经 IO_MEM、IO_PORT、IRQ 等服务获得**细粒度**硬件资源;platform driver 可把设备资源细粒度隔离给沙箱化驱动 | **"能直接访问 MMIO" ≠ "拥有整机的内核权限"** |
| QNX Neutrino | **用户态驱动同样可以 mmap 物理设备寄存器并直接读写**,IRQ 通过专门机制交付;物理内存映射还受 ability/privilege 控制 | 逆向未知系统时,**看到直接 MMIO 不要自动归类成 monolithic kernel driver**——代码运行位置与硬件访问权限是两个独立变量 |
| Barrelfish | 用户态驱动经 capability 获得设备内存,中断被转发到用户态;**capability space 可以是 core-local,不同核甚至运行不同的 CPU driver** | **不要默认存在单一 global namespace**。必须记录 core / domain / cspace / device 上下文;同一个数字 handle 在不同域可能完全不同 |
| MINIX 3 live update | 更新前必须进入 quiescence;新实例做 state transfer,成功则替换旧实例,**失败则 rollback**;线程尤其麻烦,因为栈/寄存器里的指针与上下文很难安全迁移 | 滚动升级取证中**旧实例 + 新实例同时存在是合法状态**。只有确认迁移完成后旧实例仍在收新请求,才应怀疑 stale routing / UAF / hook |
| MINIX 协议静止 | 有些服务不能只做到"当前没有工作",而必须达到 **request-free / protocol-free** 才能更新 | 对网络/NVMe/GPU 这类驱动,**不能把"线程 idle"当成可安全替换**——还要查 outstanding DMA、descriptor ownership、IRQ、协议事务 |
## 二、本系统特有的四个特殊情况
### 1. 先识别 generation,再恢复语义
同一能力在不同代际的实现模型可能不同——例如 **capability 分配在一代实现里是 per-core / 无全局锁**,而另一代(较大模型)描述的是**受保护的全局 capability table**。
**所以绝对不能**:
```
找到 capability 结构体 → 直接按最新文档套布局
```
**正确顺序**:用**代码签名、数据布局、调用路径**确认目标属于哪一代实现,再套结构。
**这条可推广到所有快速演化的实验系统。**
### 2. IPC 边界有专门的"入口页"形态
典型路径:
```
call → 入口页
→ bt [位图] (测试)
→ jnc fail (失败分支)
→ jmp 业务页
```
而**隔离模式下,后面的跳转甚至可以故意触发 page fault**,再由内核验证来源并完成映射/地址空间切换。
**所以碰到 `call unknown / bt [...] / jnc trap / jmp unmapped` 不要按传统 RE 思维判成**:obfuscation、CFI stub、broken CFG——**它可能就是正常的 IPC 边界**。
### 3. 共享内存指针必须带域语义
共享内存页**页对齐且受 capability 控制**;IPC 传大数据时直接通过**预映射共享内存**传 offset/length。
数据结构应记成:
```
{ domain, shmem_cap, mapping, offset, length, rights }
```
**而不是**
```
void *buffer;
```
否则数据流分析会把**大量合法的跨域数据流错误合并**。
### 4. 驱动生命周期与硬件生命周期分开建模
驱动可动态加载、热插拔、sandbox 销毁重建;滚动更新中旧/新服务并行、迁移后再终止旧实例。而跨系统经验(见上表)说明:**软件实例重建并不意味着硬件状态回到 clean boot**。
所以状态模型要从
```
loaded → initialized → running → unloaded
```
扩展成**分别跟踪**:
```
module instance / driver domain / device ownership / DMA ownership /
IRQ routing / client binding / version binding / migration state
```
## 三、触发条件(命中 / 不要命中)
**这一节的目的:让本分支只在确实是本类系统时被命中,不在 Xen / Fuchsia / seL4 一类系统上误触。**
**命中(任一即可)**:
- 模块元数据里带**自定义模块格式标识**(自描述模块:UUID + 版本 + 端点 + 资源需求 + 依赖 + 签名)
- 出现"**同名特权级进程之间靠 MMU 隔离**"的模型(服务与内核核心同物理特权级,靠页表而非 CPL 区分)
- IPC 快路径出现**入口页 + 位图测试 + 失败分支 + 跳业务页**的固定形态
- 证据显示**两种 capability 记账模型并存**(per-core 无全局锁 ↔ 受保护的全局表)——即代际差异
- 模块可**动态加载/销毁重建**,且存在**旧新实例并行的滚动更新**语义
**不要命中(这些属于别的分支)**:
| 现象 | 该走哪里 |
|---|---|
| 只是"驱动在用户态" | [[re-rtos/qnx]](用户态 server)、[[helenos]](DDF)、[[redox]](scheme)——**用户态驱动本身不足以命中本分支** |
| 只是"有 capability / handle 是本地标识" | [[sel4-kernel]]、[[zircon-kernel]]、[[genode]];多内核类系统(core-local capability space)也各走自己的分支 |
| 只是"共享内存 + 通知" | [[re-hypervisor/xen]](grant/event)、[[re-hypervisor/hyperv-vmbus]](GPADL)、[[re-rtos/qnx]](io-pkt 类) |
| 只是"有滚动更新" | [[minix3]](RS 自愈 / live update)、[[netbsd-kernel]](按需加载) |
| 只是"看到直接 MMIO" | 不足以说明任何事——见上表 QNX / Genode 两条 |
**判据**:**本地分支的标志是"特权级相同 + MMU 隔离 + 模块自描述元数据 + 代际可变的 capability 记账"这一组合**,而不是其中任何单独一条。
## 决策树
```
拿到目标
├─ 先做模块元数据 triage(自描述模块:UUID/版本/端点/资源/依赖/签名)
│ └─ 元数据先于反汇编
├─ 确认代际
│ ├─ capability 记账是 per-core 还是全局表?
│ └─ 用代码签名 / 数据布局 / 调用路径判定,别按最新文档直接套结构
├─ 不要从指令形态推信任边界
│ ├─ 直接 call ≠ 同一组件
│ ├─ 直接 MMIO ≠ 全机内核权限
│ └─ mov [addr] 可能是被授权的 MMIO
├─ IPC 形态
│ ├─ 入口页 + 位图 + 失败分支 + 跳业务页 → 正常 IPC 边界
│ └─ 跳转触发 fault 后由内核验证来源并映射/切换 → 也是正常
├─ 数据流
│ └─ 共享内存指针带 {domain, cap, mapping, offset, length, rights}
└─ 生命周期
├─ 模块实例 / 域 / 设备所有权 / DMA / IRQ 路由 / 客户端绑定 / 版本 / 迁移状态 分别跟踪
└─ 旧新实例并存 = 合法状态;软件重建 ≠ 硬件回到 clean boot
```
## 工具与验证
- 元数据:模块自描述字段(UUID、版本、端点、资源需求、依赖、签名)
- 域模型:谁与谁同特权级、谁靠 MMU 隔离、capability 记账在哪个粒度
- 映射:共享内存的 {domain, cap, mapping, offset, length, rights};物理页的 owner / mapped domain
- 生命周期:模块实例、设备所有权、DMA、IRQ 路由、绑定与版本、迁移阶段的分别记录
- 验证:能同时说清「这是哪一代实现 + 该调用的信任边界由什么决定 + 这块内存在哪些域里可见」
## 该平台的坑(汇总)
- **用 "Ring 0 → 全内核可寻址" 推理**:同特权级进程之间靠 MMU 隔离,这条不成立
- **用"是否直接 call"判断驱动边界**:可能同地址空间,也可能跨隔离域
- **把初始化里的 reset / 状态探测当 boilerplate 删掉**:可能是恢复路径
- **先反汇编后看模块元数据**:顺序反了,元数据是第一层 triage
- **把整数 handle / 页索引 / ring offset 先当指针**
- **内存取证只存虚拟地址**:还要 owner domain / mapped domain / backing frame / capability
- **"能访问 MMIO"就判有内核权限**:访问权限与运行位置是两个变量
- **默认存在单一全局命名空间**:capability space 可能是 core-local
- **按最新文档直接套结构体布局**:先做代际指纹
- **把入口页 IPC 形态判成混淆 / CFI stub / 坏 CFG**
- **共享内存指针只记 `void *`**:会把合法跨域流错误合并
- **把"线程 idle"当可安全替换**:还要看 outstanding DMA / descriptor ownership / IRQ / 协议事务
- **滚动更新中看到旧新实例并存就判异常**:这是合法状态
- **软件实例重建就假设硬件已回到初始状态**

View File

@@ -0,0 +1,179 @@
# IBM i:单一存储层 / MI 层次 / 程序模型 / 指针与 activation group
<CORE RULE>
**两件事决定一切**:所有内容都是**对象**,而一切存储处在**单一地址空间**里。
```
主存与辅存不被区分 → 内存 + 磁盘 = 单一 64 位地址空间
对象按【名字】访问,不按硬件地址
```
再加上**两套指针**与**两种存储模型**的分野,`object address` 就绝不能当成"当前由 DRAM 支撑的指针"。
**不要套 POSIX 文件/进程模型**——这一层的每一个概念在这里都有对应的、但语义不同的东西。
</CORE RULE>
## 一、层次:谁在谁上面
```
MI(Machine Interface) ← 程序与进程在这里是【系统对象】;ILE 支持在此实现
├─ timi(technology-independent 接口)
└─ PASE ← 走另一条路:直接对着 SLIC 的 syscall 接口,绕过 TIMI
SLIC / LIC(System Licensed Internal Code) ← 中断处理、异常路由、部分运行时实现
OS(ILE 运行时 / OPM 支持 / 各类服务)
```
- **MI 层跟踪调用流是按"过程(procedure)"而不是按"程序"**——MI 指令里有外部程序调用与"经过程指针调用"两类
- **进程**在 MI 上是系统对象(Process Control Space);**activation group 不是系统对象**,它是 MI 进程对象的一部分
- **PASE 不经过 TIMI**:它对 SLIC 有 syscall 接口,因此跨 PASE↔SLIC 边界的调用有额外开销
**逆向含义**:见到一份代码,先判它在**哪一层**——MI 层的对象、ILE 运行时生成的代码、还是 PASE 里的 AIX 风格二进制,三者的分析工具和语义都不同。
## 二、程序模型:OPM / EPM / ILE
| 模型 | 特征 |
|---|---|
| **OPM** | 程序对象直接由源成员编译;调用**多在运行时解析**(晚绑定);通常单语言;**不支持模块、过程、服务程序、绑定目录**;**必须运行在默认 activation group** |
| **EPM** | 过渡形态(C/400、Pascal 时代),仍然是运行时解析 |
| **ILE** | 引入模块、绑定器、优化翻译器;支持多语言混合 |
**ILE 的对象三件套**:
| 对象 | 说明 |
|---|---|
| **`*MODULE`** | ILE 编译器的输出;含一个或多个**过程**;**不能直接执行**;被绑定进 `*PGM`/`*SRVPGM` |
| **`*PGM`** | 绑定一个或多个模块并指定**入口模块**;**单一入口点**;**bind-by-copy**——过程在编译时解析,开销小,但同一模块被绑进多个程序时内存里会有多份副本 |
| **`*SRVPGM`** | 多入口点的可执行单元(每个过程一个入口);**bind-by-reference**——客户端存的是符号链接,**在程序被激活时解析**;**没有入口模块** |
- **签名与 binder language**:binder 描述文件管理服务程序**支持的签名与导出列表**(导出符号、程序级别、签名、级别检查);客户端程序激活时,被绑定的服务程序也会被激活,并做**签名匹配**
- **binding directory** 保存绑定信息来源
- 模块还可以**导出数据项**:导出方分配并初始化,其他模块导入并共享该存储
**逆向含义**:`*PGM` 里**内含**它的模块;`*SRVPGM` **不内嵌**在调用者里,而是**首次调用时装入内存**——"改了服务程序不需要重编译调用者"正是这个差异的直接后果。
## 三、activation group:运行期的存储与资源边界
activation group 是为运行一个或多个程序而分配的**运行期工作存储容器**,提供:
```
PSSA(程序静态存储区,全局静态数据)
PASA / 调用返回栈(局部变量)
堆存储(动态数据)
```
- 每个 activation group 有自己的控制信息,因此可以有**不同的保护状态、文件使用、提交控制**
- **一个 job 有两个默认 activation group**:操作系统用一个,应用用一个;**OPM 程序必须跑在默认组里**;ILE 程序也可以(但会失去模块/过程/服务程序等特性)
- 常见选择:
| 选择 | 语义 |
|---|---|
| **命名** | 首个程序创建,后续程序"插进来";**直到 job 结束或显式回收**才消失 |
| **`*NEW`** | 系统命名,调用时创建、程序结束时销毁;**服务程序不能用 `*NEW`** |
| **`*CALLER`** | 与调用者同组,不新建(服务程序通常的推荐值) |
| **`*ENTMOD`** | 由绑定器看入口模块决定 |
- **覆盖(override)与文件打开的作用域也在这一层**:作用域可以是 activation group、调用层级或 job;从默认组发出的覆盖通常按调用栈层级生效,从 ILE activation group 发出的按该组生效
**逆向含义**:定位"这个静态变量属于谁、这个文件覆盖为什么没生效",答案在 activation group 而不是在程序里。
## 四、两套指针与两种存储模型
| | **系统指针(system pointer)** | **空间指针(space pointer)** |
|---|---|---|
| 指向 | **对象**(带类型信息) | 空间内的偏移 |
| 形态 | 16 字节 tagged | 地址 |
| 能做什么 | 解析对象、调用、授权检查 | 直接寻址一块空间内的字节 |
**两种存储模型并存**:
| | **single-level storage** | **teraspace** |
|---|---|---|
| 范围 | 全局(任何拿到指针的 job 都可访问) | **job 本地**(正常只对拥有它的 job 可见) |
| 组成 | 多个 16 MB 单元 | 一个连续地址空间,内含多个独立分配区与未分配空隙 |
| 大小 | —— | 约 100 TB 量级 |
| mmap | **不支持** | 支持 |
| 指针 | —— | **8 字节** |
| 跨 job 共享 | 传指针即可 | **只能经共享内存 API**(`shmat`/`mmap` 一类) |
- **teraspace 不是空间对象**:它不是系统对象,**不能用系统指针引用**,但可用空间指针在同一 job 内寻址;**只在 job 开始到 job 结束之间存在**
- ILE 程序/服务程序可建成 `*TERASPACE` / `*INHERIT` / `*SNGLVL`;**OPM 程序只能用 `*SNGLVL`**
- **`*INHERIT` 采用所在 activation group 的存储模型**,并要求 `ACTGRP(*CALLER)`
- **teraspace 模型的程序不能被激活进"根程序用单级存储"的 activation group**(含 OPM 默认组);`*TERASPACE` 不能配默认组
- 堆默认跟随存储模型;某些语言可以让堆单独选型(例如把 `malloc` 映到 teraspace 版本)
**所以逆向时要问三件事**:这个指针是系统指针还是空间指针?这个地址属于单级存储还是 teraspace?这个程序/服务程序是哪种存储模型?(第三问决定了前两问的合法答案。)
## 五、异常与指针使用限制
- **space addressing violation**:试图在当前空间范围之外操作,或引用 teraspace 内**未分配的页**
- 该异常的**数据里带**:与空间关联的对象的**系统指针**、**空间偏移**、以及**空间类别**——空间类别能区分主/次关联空间、隐式进程空间(自动/静态/activation group 堆/常量/句柄堆)、**teraspace**,以及**供 PASE 使用的 teraspace**
- **tagged(16 字节)指针的使用限制**(做数据流与取证时要记住):
- 指向 teraspace 的 tagged 空间指针**只能由拥有该 teraspace 的进程使用**——被 `fork` 子进程继承、或被别的进程存进共享内存后,**应视为不可用**
- **不得存放进由字节流文件映射来的内存**(`mmap`/`shmat` 那类);即使存在用共享内存对象创建的区域里,**也不能被持有者以外的进程可靠使用**
## 六、job / 线程边界
- **teraspace 随 job 生灭**;跨 job 共享必须走共享内存 API
- 库解析(例如库列表搜索)是**按线程**的
- ILE 运行时**可能按 activation group 各自维护 `errno`**——要判断值,必须在"同一个线程、同一个 activation group"里取
- **并非所有系统 API 都线程安全**(API 参考逐个标注);把非线程安全 API 经 JNI 调用会得到不可预期的结果
## 七、可执行材料识别(拿产物时的第一层)
| 手段 | 用途 |
|---|---|
| **DMPOBJ(Dump Object)** | 把对象内容打成 spool file;可用于两份 `*PGM`/`*SRVPGM` 的**差异比对** |
| **DBGVIEW(`*LIST`/`*ALL`) 编译** | 借调试 API **提取编译清单**;比二进制比对更省事、更可读 |
| 对象压缩(`CPROBJ`/`DCPOBJ`) | `*PGM`/`*SRVPGM`/`*MODULE` 等**可能处于压缩态**;**被使用时系统会自动提供解压版本**——所以"文件看着不像代码"可能只是压缩态 |
| 版本/时间戳 | 编译时刻等内嵌结构会让"几乎同时编译的两次产物"**永远不相同**——别据此判被篡改 |
**注意**:`*MODULE` 通常建在临时库里,**被绑定进程序后就不再需要**——所以在系统上找不到"当初那个模块对象"是正常的,代码在 `*PGM`/`*SRVPGM` 里面。
## 决策树
```
拿到 IBM i 目标
├─ 先判层次
│ ├─ MI 层对象 / ILE 运行时产物 / PASE 里的 AIX 风格二进制
│ └─ 三者工具与语义不同
├─ 拿到的是哪种对象
│ ├─ *PGM(bind-by-copy,内含模块)/*SRVPGM(bind-by-reference,多入口)
│ ├─ *MODULE(不可直接执行;可能已被绑定后丢弃)
│ └─ 压缩态?先用解压/查看工具,别判"不是代码"
├─ 引用与存储
│ ├─ 系统指针(指向对象)还是空间指针(空间内偏移)?
│ ├─ 单级存储(全局、支持传指针共享)还是 teraspace(job 本地、跨 job 需共享内存 API)?
│ └─ 程序的存储模型(*TERASPACE/*INHERIT/*SNGLVL)与目标 activation group 是否相容?
├─ 静态数据/文件覆盖行为异常
│ └─ 查 activation group(PSSA/PASA/堆 与覆盖作用域都在这一层)
├─ 异常
│ ├─ space addressing violation → 看异常数据里的系统指针/偏移/空间类别
│ └─ tagged 指针跨 fork / 跨进程使用 → 按不可用处理
└─ 产物比对
├─ 优先用编译清单(DBGVIEW)
└─ DMPOBJ 比对时排除库名/创建时间/PTF 级别等内嵌差异
```
## 工具与验证
- 层次与对象:对象类型与所属库;MI 层调用流(按过程追踪)
- 程序结构:绑定的模块与入口模块、导出/导入符号、服务程序签名与 binder 语言
- 运行期边界:activation group 的存储区(PSSA/PASA/堆)与覆盖作用域
- 指针与存储:系统指针/空间指针的区分;teraspace 与单级存储的归属
- 验证:能同时说清「这是哪一层的什么对象 + 它的指针是哪种、指向哪种存储 + 它跑在哪个 activation group」
## 该平台的坑(汇总)
- **把对象地址当 DRAM 支撑的指针**:内存与磁盘是一个地址空间
- **用盘号/柱面/扇区理解存储**:物理磁盘被 LIC 隐藏
- **套 POSIX 文件/进程模型**:这里是对象 + 限定名 + 系统指针
- **以为库只是目录**:库本身也是对象、是容器对象
- **混淆两套指针**:系统指针指向对象,空间指针指向空间内偏移
- **混淆两种存储模型**:teraspace 是 job 本地且不是对象;跨 job 只能经共享内存 API
- **把 tagged teraspace 指针存进共享内存或让子进程继承后继续用**
- **期待在系统上找到"当初那个 `*MODULE`"**:绑定后它通常已不需要
- **把压缩态对象判成损坏/非代码**:使用时会自动解压
- **拿二进制比对结果判篡改**:编译时间戳等内嵌字段必然造成差异;优先用编译清单
- **在错误的 activation group 里读 `errno`**:运行时可能按组维护
- **以为 OPM 程序能进任意 activation group 或用 `*TERASPACE`**:不能
- **把 PASE 与 native 环境当同一套调用边界**:PASE 绕过 TIMI,跨边界有额外开销

View File

@@ -0,0 +1,137 @@
# illumos / Solaris:DDI/DKI 驱动骨架 / dev_ops / cb_ops
<CORE RULE>
illumos 的驱动骨架**非常固定**,所以反而好恢复——**关键是别把它当成 load/unload + file_operations 那一套**:
```
_init / _fini / _info
→ modlinkage
→ modldrv
→ dev_ops (设备级操作:getinfo / probe / attach / detach / power / quiesce …)
→ cb_ops (字符/块级操作:open / close / read / write / ioctl / mmap/devmap …)
```
第二个关键点是**占位符有语义**:
| 占位 | 含义 |
|---|---|
| **`nulldev`** | 合法 no-op(该入口不需要做事) |
| **`nodev`** | **不支持该操作,返回 `ENXIO`** |
**把两者都归成 "unused" 会直接把"这个驱动支持什么"判断错。**
</CORE RULE>
## 一、模块入口三件套
| 入口 | 职责 |
|---|---|
| **`_init`** | 一次性、模块级初始化(如 soft state 初始化、驱动全局互斥);随后调用 **`mod_install(&modlinkage)`**;若安装失败必须**回滚已做的初始化,并返回相应错误** |
| **`_fini`** | 调用 **`mod_remove(&modlinkage)`**;**只有成功时**才释放 `_init` 里分配的资源 |
| **`_info`** | 返回模块信息,通常就是把 `modlinkage` 交给 `mod_info()` |
两条容易忽略的约束:
- **每实例初始化属于 `attach()`,不属于 `_init()`**——在 `_init` 里找"某个设备实例的初始化"会扑空
- **`_init` 调用 `mod_install()` 之后不得再改挂在 `modlinkage` 上的数据结构**(系统可能复制或改动它们)
- **`_fini` 可能在仍有硬件实例 attach 的情况下被调用**:此时 `mod_remove()` 失败,**驱动不得释放资源**——"卸载函数里一定会释放"不成立
## 二、modlinkage / modldrv:从入口到 dev_ops 的桥
- **`modldrv`** 里放:模块类型(`&mod_driverops`)、一句描述、以及**指向该驱动 `dev_ops` 的指针**
- **`modlinkage`** 里放:`MODREV_1` 与指向 `modldrv` 的指针(其后为 NULL 结尾)
- 这个 `modlinkage` 正是传给 `mod_install()` / `mod_remove()` / `mod_info()` 的东西
**RE 含义**:`_init` 的第一个参数/locals 会指向 `modlinkage`——**顺着这条静态引用链,可以一次拿到 dev_ops 与 cb_ops 的位置**,比从 `open` 反着找快得多。
## 三、dev_ops:设备级操作(字段在编译期全部设定)
| 字段 | 说明 |
|---|---|
| `devo_rev` | 驱动构建版本,置为 `DEVO_REV` |
| `devo_refcnt` | 引用计数,置 `0` |
| `devo_getinfo` | 取驱动/设备信息 |
| `devo_identify` | **已废弃**,按约定置 `nulldev` |
| `devo_probe` | 探测设备(自识别设备不需要 probe 时置 `nulldev`) |
| `devo_attach` | 附着/恢复:`int (*)(dev_info_t *dip, ddi_attach_cmd_t cmd)` |
| `devo_detach` | 脱离/挂起:`int (*)(dev_info_t *dip, ddi_detach_cmd_t cmd)` |
| `devo_reset` | 本版本不支持,**置 `nodev`** |
| `devo_cb_ops` | 叶驱动指向 `cb_ops` |
| `devo_bus_ops` | nexus(总线)驱动用;叶驱动为 `NULL` |
| `devo_power` | 电源管理 |
| `devo_quiesce` | 静默设备;可置为"无需 quiesce"的实现 |
外加 `devo_getinfo` / `devo_identify` 一类占位:**`nulldev` 与 `nodev` 的选择本身就是"该驱动支持什么"的编码**。
## 四、`attach` / `detach` 的命令值:不是"第一次附着"那么简单
`attach(9E)` 的语义就是**"附着设备,或恢复设备"**,由 `cmd` 区分:
- **`DDI_ATTACH`**(必需):初始化某个设备实例;**每个实例调用一次**;此时常规内核服务可用;**在 attach 成功之前,驱动能被调用的入口极少**(另有 `getinfo`)
- **`DDI_RESUME`**(可选,支持挂起/恢复才需要):在 `detach(9E)` 以 **`DDI_SUSPEND`** 成功返回之后被调用;必须**恢复硬件状态**(电源可能已被移除)、让挂起的请求继续、并服务新请求;**不得对硬件当前状态作假设**
- 版本差异:更老的 Solaris 资料里还有 **`DDI_PM_RESUME`**(配套 `DDI_PM_SUSPEND`),属于**已废弃的 PM 接口**(`pm(9P)` 已说明原 PM 接口过时)——**见到它说明代码年代较早**,不要把它当"厂商私有扩展"
- 还有一类 `pm-hardware-state` 属性(`needs-suspend-resume` / `no-suspend-resume` / `parental-suspend-resume`)会**影响 `detach(DDI_SUSPEND)` 与 `attach(DDI_RESUME)` 是否被调用**——"挂了但没走到 resume"可能是配置决定的
- 返回值:**`DDI_SUCCESS` / `DDI_FAILURE`**;遇到保留值必须返回失败
所以下面这种 switch **不是奇怪写法**:
```c
switch (cmd) {
case DDI_ATTACH: ...
case DDI_RESUME: ...
}
```
**把 resume 分支判成 vendor 扩展,是这一域典型误判。**
## 五、cb_ops:字符/块操作,占位符语义同样重要
叶驱动经 `devo_cb_ops` 指向 `cb_ops`,其中包含 open / close / read / write / ioctl / devmap / mmap / segmap / chpoll / prop_op / … 以及 `cb_flag`(如 `D_MP`)与版本字段。
典型填写方式(可直接当"这个驱动到底支持什么"的判据用):
```
nodev → 明确不支持该操作(返回 ENXIO)
nulldev → 不需要做事的合法占位
nochpoll / ddi_prop_op 一类 → 专用默认实现
NULL → 例如不使用 STREAMS 时 cb_stream 为 NULL
```
## 决策树
```
拿到 illumos/Solaris 驱动
├─ 先走骨架:_init → modlinkage → modldrv → dev_ops → cb_ops
│ ├─ _init 里 mod_install 的参数即 modlinkage(静态引用链一次拿全)
│ └─ 每实例初始化在 attach(),不在 _init()
├─ dev_ops 逐字段读"支持什么"
│ ├─ nulldev = 合法 no-op
│ ├─ nodev = 不支持(ENXIO)
│ ├─ devo_reset = nodev(本版本不支持)
│ └─ devo_identify = nulldev(已废弃)
├─ attach 里的 switch
│ ├─ DDI_ATTACH 必需;DDI_RESUME 配套 detach 的 DDI_SUSPEND
│ ├─ 见到 DDI_PM_RESUME → 已废弃的 PM 接口,不是私有扩展
│ └─ 没走到 resume 也可能是 pm-hardware-state 属性决定的
├─ cb_ops 逐字段读字符/块能力(注意 nodev / nulldev / NULL 的区别)
└─ 卸载路径
├─ _fini 调 mod_remove,失败时不得释放资源(可能仍有实例附着)
└─ _init 在 mod_install 之后不得再改 modlinkage 上的结构
```
## 工具与验证
- 入口与注册:`_init` / `_fini` / `_info` 三件套与 `modlinkage`→`modldrv`→`dev_ops` 的静态引用链
- 能力清单:逐字段读 `dev_ops` 与 `cb_ops`,**把 `nulldev` / `nodev` / 专用默认 / `NULL` 分别记录**(这是"支持什么"的原始编码)
- 附着路径:`attach` / `detach` 的 `cmd` 分支集合(`DDI_ATTACH` / `DDI_RESUME` / `DDI_SUSPEND`,及历史 `DDI_PM_*`)
- 验证:能同时说清「骨架链(谁指向谁)+ 每个占位符的语义 + attach/detach 支持的命令集」
## 该平台的坑(汇总)
- **把 illumos 驱动当"load/unload + file_operations"**:骨架是 `_init/_fini/_info` → `modlinkage` → `modldrv` → `dev_ops` → `cb_ops`
- **把 `nulldev` 与 `nodev` 都恢复成 unused**:前者是合法 no-op,后者表示不支持并返回 `ENXIO`——混同会判错驱动能力
- **把 `attach()` 理解成"只在第一次附着时调用"**:它还承担**恢复**语义(`DDI_RESUME`),命令值决定行为
- **把 `DDI_RESUME` 分支 / `DDI_PM_RESUME` 当厂商私有扩展**:前者是标准命令值,后者是已废弃的 PM 接口(年代线索)
- **在 `_init` 里找设备实例的初始化**:实例初始化属于 `attach()`
- **以为 `_fini` 一定会释放资源**:`mod_remove()` 失败时(仍有实例附着)不得释放
- **在 `mod_install()` 之后继续改 `modlinkage` 上的结构**:违反约定,且系统可能已复制或改动这些数据
- **忽略 `pm-hardware-state` 一类属性**:它决定挂起/恢复路径是否被调用,"没走到"不一定是缺陷

View File

@@ -0,0 +1,277 @@
# Linux 内核模块分支(.ko / LKM / 运行态内核)
<CORE RULE>
**三个不要**:
① 不要把 `.ko` 当普通 ELF 程序(它是 ET_REL,relocation 才是主线);
② 不要把**磁盘上的代码**当运行态绝对真值(relocation、KASLR、alternatives、jump labels、ftrace/livepatch/paravirt patching、init 段释放都会让两者不同);
③ 怀疑 rootkit 之后,**不要相信由那个内核自己产生的单一枚举结果**。
Linux 比 seL4 更麻烦的地方恰在这里:**大量"看起来像被篡改"的现象其实是内核合法的动态 patching 机制**。不知道 `__ex_table`、jump labels、livepatch、ftrace、BTF、MODVERSIONS、init 释放这些东西,会产生大量事实性误判。
</CORE RULE>
## 一、先判断你拿到的到底是什么
| 目标 | 性质与处理 |
|---|---|
| `vmlinux` | 完整 ELF(最好分析;符号/类型/节齐全) |
| `bzImage` / `Image` / `zImage` | **boot image,不等于可直接分析的 ELF** |
| `*.ko` | 可加载模块,通常 **ET_REL** |
| `*.ko.xz` / `*.ko.zst` | 压缩模块(loader 有解压阶段,不是壳) |
| built-in driver | 根本没有独立运行时 `.ko` |
| livepatch module | 特殊 ELF(专用节,见 §14) |
| eBPF object | 看起来也是 ELF,**模型完全不同** |
| runtime memory image | 运行态(relocation 已应用、init 段已释放) |
- x86 的 **bzImage 本身是压缩 boot image**:拖进 IDA/Ghidra 会看到 boot stub、大量 16-bit code、后面识别不了——**这不是"内核做了混淆"**。正常路径:识别 architecture/boot format → **提取真正的 kernel image** → 得到 vmlinux-style ELF 或 raw Image → 再恢复符号/类型
- 现代内核支持模块解压(`.ko.xz`/`.ko.zst`):loader 流程里 `kernel_read_file → 模块解压 → ELF 处理` 是**独立阶段**;系统报 `failed_decompress` 先查压缩算法/文件损坏,而不是去查符号依赖
## 二、`.ko` 不是共享库:relocation 优先
- `.ko` 的核心性质是 **ELF ET_REL**(不是 ET_EXEC / ET_DYN)——**relocation 是逆向主线**
- stripped 模块的信息价值排序(不是"没符号只能硬读汇编"):
```
relocations / imported symbols → modinfo → BTF/DWARF → 内核 API 调用模式 → 裸反汇编
```
- 例子:反编译器没恢复出函数名,但 `.rela.text` 里 `offset 0x183 / R_X86_64_PLT32 / symbol = pci_register_driver` ——**这条边实际上已经恢复了**。一个 stripped 驱动只要还剩 `pci_register_driver`、`request_irq`、`dma_alloc_coherent`、`copy_from_user`、`misc_register` 这类导入,功能骨架就出来了
## 三、先吃 `.modinfo`(便宜得离谱,却常被跳过)
- 字段:`license` / `author` / `description` / `alias` / `depends` / `vermagic` / `srcversion` / `parm` / `firmware` / `retpoline` / `intree` / `livepatch`
- **`alias` 直接说明设备匹配**:`alias: pci:v00008086d0000....` → 这是给什么设备用的;大量 `alias: usb:...` → 优先沿 `usb_driver` / `probe` / `disconnect` / `usb_control_msg` / `urb` 方向恢复
- **built-in 也有 modinfo**:`modules.builtin.modinfo`(字段带模块名前缀);较新的构建还生成 `modules.builtin.ranges`(`CONFIG_BUILTIN_MODULE_RANGES`)——按 ELF 节给出 built-in 模块的地址偏移范围,配合 `System.map` 可把符号关联回模块名
## 四、"找不到这个 .ko" ≠ 没有这个逻辑
- 可能是 `CONFIG_FOO=y`(链进 vmlinux)而不是 `=m`
- 查证:`modules.builtin` / `modules.builtin.modinfo` / kernel config / `System.map` / `vmlinux`
- built-in 驱动甚至可接受 **kernel command line 参数**(`foo.option=value`)
## 五、磁盘有代码、内存里找不到 → 先想 `__init` 释放
- 初始化完成后内核**释放 init-only 内存**(`__init` / `__initdata` / `.init.text` / `.init.data`);模块 init 成功后同样丢弃相应 initialization memory
- 所以运行态 dump 里找不到 `foo_init` **可能是正常现象**
- 对内存取证尤其重要:rootkit 的关键 setup(`install_hook()` / `patch_table()` / `register_callback()`)若全在 `module_init()` 路径,加载完成后 init code 自身已不存在,**但它制造出的 callback / hook / 改过的指针 / 持久对象仍在**
- 所以运行态逆向要问的不是"恶意 init 函数在哪",而是"**init 最终留下了什么状态改变**"
## 六、模块加载失败:按阶段诊断,不要乱 patch
loader 自己把失败分成阶段(官方 `CONFIG_MODULE_STATS` 的计数器就是按此划分):
```
read(kernel_read_file_from_fd)
→ decompress(vmap 解压缓冲)
→ 早期校验:module_sig_check(签名)/ elf_validity_cache_copy(ELF 校验)
/ early_mod_check(blacklist、节头重写、vermagic、livepatch 要求、已加载)
→ layout_and_allocate(最终落位)
→ 符号解析 → init
```
- 对应计数:`failed_kreads` / `failed_decompress`(官方注释:**除非压缩/解压坏了否则不该出现**)/ `failed_becoming`(读过之后、落位之前失败)/ `failed_load_modules`;debugfs 的 `stats` 文件直接给 "Mods failed on kread / decompress / becoming / load"
- 所以 `modprobe foo → Invalid module format` **不要一看到就去改 vermagic**——可能是 ELF 错、签名错、MODVERSIONS 错、架构错、livepatch 格式错;结合 `dmesg` + `modinfo` + `readelf` 判断
## 七、vermagic 相同 ≠ ABI 相同
- `CONFIG_MODVERSIONS=y` 时,内核把导出符号原型压成 **CRC**;CRC 不匹配即使 `uname -r`/vermagic 看起来一致也会拒绝加载
- 要匹配的是:**release + config + build flags + `Module.symvers` + symbol CRC**(`Module.symvers` 记录全部导出符号与 CRC)
- **格式有两代,别死写旧 parser**:
- basic(`CONFIG_BASIC_MODVERSIONS`):导入符号的 name+CRC 存在模块的 `__versions` 节,符号名上限 64 字节
- extended(`CONFIG_EXTENDED_MODVERSIONS`):名字在 `__version_ext_names`(连续 NUL 结尾字符串),CRC 在 `__version_ext_crcs`,支持长符号名;加载时校验两者**同时存在或同时缺失**,不匹配报 `-ENOEXEC`
- 导出侧:`__ksymtab` / `__ksymtab_gpl`(名字在 `__ksymtab_strings`),CRC 在 `__kcrctab` / `__kcrctab_gpl`
## 八、文件末尾"不是 ELF 的数据":先查模块签名
- Linux 模块签名**直接附加在 ELF 之后**:`ELF` + signature blob + `~Module signature appended~.`
- 见到"节表已结束但文件还有数据"**不要自动判** overlay / packer / 恶意附加数据——先检查 module signature
- **签完名以后再 strip 会破坏签名**(官方称这种签名为 brittle:整个模块文件都是签名 payload)→ 要分析 signed module:**保留原件 → copy → 只对副本做 strip/解压/patch**
## 九、stripped 但有 `.BTF` / `.BTF.ext` —— 可能反而好逆
- BTF 给:struct / union / enum / typedef / 函数原型 / member offset
- **`.BTF.ext`** 还含 function info、line info 与 **CO-RE relocation**
- 所以 `.symtab` 被 strip 不等于类型信息全没:先查 `.BTF` / `.BTF.ext`,再决定怎么恢复类型
## 十、`.BTF` 里 type ID 全错 → 先查 split BTF
- 模块 BTF 可以是 **split BTF**:`vmlinux BTF` 为 base,模块只存新增类型,type ID 可引用 base
- 单独解析模块 `.BTF` 报"type 38291 不存在"**不是损坏**
- 新版 external module 可能带 **`.BTF.base`**:用于 base BTF 改变后重新定位 split BTF 引用(`btf__relocate()` 的用途)
```
.BTF
├── standalone → 直接解析
└── split → 找对应 vmlinux BTF
└── 若有 .BTF.base → relocate 后再解析
```
## 十一、struct offset 与源码不符:按顺序排查
① exact kernel build? ② BTF 是否对应? ③ CONFIG 选项是否相同? ④ **结构布局随机化**(`CONFIG_GCC_PLUGIN_RANDSTRUCT`,此类内核带 taint `T`)⑤ vendor patch?
**运行态 kernel 自带 BTF/DWARF 的可信度高于"网上同版本源码"的 offset**——不是 IDA 错,也不是算错。
## 十二、磁盘代码 ≠ 内存指令:先排除**合法动态 patching**(本分支最重要的一条)
以下机制都会让运行态字节与磁盘不一致,**全部合法**:
| 机制 | 行为 |
|---|---|
| **static keys / jump labels** | `nop` ↔ `jmp target` 运行时改写(几乎零成本分支) |
| **alternatives** | 启动时按 CPU 特性/勘误替换指令 |
| **paravirt** | `pv_ops` 启动时 binary patching,用 native/hypervisor 实现替换通用路径 |
| **ftrace** | 函数跟踪本身修改执行路径;function graph tracer 还处理函数返回路径 |
| **livepatch** | 真正把执行流换到修补函数(symbol lookup + relocation + 注册 patched functions) |
| **kprobes / fprobe** | 动态插桩 |
判定链:
```
runtime bytes != disk bytes
→ 先排除 alternatives / static keys / ftrace / livepatch / paravirt / breakpoints
→ 剩余无法解释的修改
→ 才提高 rootkit 嫌疑
```
## 十三、看到 `.klp.*` 不要判 ELF 损坏(livepatch)
- 标志:`modinfo` 里 `livepatch: Y`;节 `.klp.rela.<object>.<section>`;符号 `.klp.sym.<object>.<symbol>,<pos>`
- 特殊之处:livepatch 需要引用 **unexported global / local / 尚未加载模块内**的符号 → 不能在普通模块加载阶段全部解决
- 因此"目标 driver 当前不存在"也**不代表 livepatch broken**:对应 relocation 可以等 target module load 时再应用
## 十四、`.orc_unwind` / `.orc_unwind_ip` 不是垃圾数组
- 它们是 **objtool 生成的 unwind metadata**,描述每条相关指令位置的 stack state,用于内核栈回溯——不是加密数据 / lookup table / 恶意 blob
- 对逆向有价值:**函数边界、stack state、异常控制流、unwindability**,可辅助恢复 stripped 代码
## 十五、`__ex_table` 制造"不可见控制流"
```
faulting instruction → CPU exception → __ex_table lookup → fixup handler
```
- CFG 里没有传统的 `jmp fixup`,但发生 page fault 后控制流确实会转过去(官方文档以 `get_user()` 为例说明 faulting instruction ↔ `__ex_table` entry ↔ fixup code 的对应)
- 逆向 `copy_from_user` / `get_user` / `put_user` / `probe_kernel_read` 附近的低级代码时,**必须把 exception table 当成 CFG 的一部分**
- 否则会看到"这段 fixup code 没有任何 xref" → **误标成 dead code**(它其实是 exception edge)
## 十六、x86 大量 `ENDBR64` 不是混淆
- 启用 kernel IBT(`CONFIG_X86_KERNEL_IBT`)时,`endbr64` 是**合法的 indirect call/jump target 标志**,函数头出现 `endbr64; push rbp; ...` 很正常
- 反过来在做**函数指针目标恢复**时,ENDBR 可作辅助信号
## 十七、函数没有正常 `call` xref,不代表没被调用
- Linux 内核函数指针密度极高:`file_operations`、`net_device_ops`、`proto_ops`、`bus_type`、`device_driver`、irq callbacks、timer、workqueue、notifier、`security_hook_list`、`seq_operations`、`proc_ops`
- 真实控制流常是 `VFS → function pointer → foo_read`,静态反编译器未必给出漂亮的 call graph
- 所以:**恢复 callback-registration graph 往往比恢复普通 call graph 更重要**。优先追注册点:
```
register_*() / *_register() / proc_create() / misc_register() / register_chrdev()
pci_register_driver() / usb_register() / netlink_kernel_create() / register_netdev()
request_irq() / queue_work()
```
然后反推"谁会调用这些 callback"。
## 十八、`kallsyms` 地址全是 0,不一定是 rootkit
- 系统可能开了 `kernel.kptr_restrict`(限制 `/proc` 等接口暴露内核指针)
- 看到 `0000000000000000 T some_kernel_symbol` → 先 `sysctl kernel.kptr_restrict`,再分析权限与 kernel config,**不要立即判"kallsyms 被 hook"**
## 十九、地址和静态 vmlinux 全对不上 → KASLR
- kernel text base 与 module 加载基址都会被随机化
- 所以 `vmlinux: foo = 0xffffffff81012340` / `runtime: foo = 0xffffffff9ac12340` **不能判"不是同一个 kernel"**——先恢复 runtime slide
- 动态调试时可直接用 `nokaslr` 让 GDB 与 vmlinux 地址一一对应
## 二十、`lsmod` 没看到,不代表模块不存在(cross-view 扩大)
- 经典手法:模块从 kernel module linked list **unlink** → `/proc/modules`、`lsmod` 都看不到,但模块仍在工作
- 当年的做法是 `module list` vs `/sys/module` kset 交叉;**现代分析不要只依赖这两个视图**,比较面应扩大为:
```
/proc/modules · /sys/module · module list · module_kset
内核可执行内存区间 · callback owner · 符号区间 · vmalloc/module 地址范围
```
- 核心:**怀疑内核已失陷后,不要信任"由那个内核自己产生的单一枚举结果"**(离线内存/多视图交叉优先)
## 二十一、"syscall table 没 hook" 也不能排除 rootkit
- 落点远不止 `sys_call_table`:`file_operations`、`proc_ops`、`seq_operations`、`net_device`/`proto_ops`、ftrace、kprobe、inline text、**LSM hook**、notifier、timer/workqueue 都被用过(经典案例会同时改 syscall path 与网络信息展示用的 operation 结构)
- 正确判断不是"sys_call_table 干净 → 内核干净",而是:
```
关键 callback pointer → 是否落入正常 vmlinux/module text 区间
→ owner 是否合理
→ runtime bytes 是否可解释
```
## 二十二、kprobe/ftrace 自己也能让你误判 hook
- 动态取证发现"某函数前几字节变了"→ **先确认 tracing**(kprobes / kretprobes / fprobe / ftrace / tracepoints 都是内核正式支持的机制)
- 完整的 inline hook 检测不能只是 `runtime bytes != disk bytes → malware`,而要解释:谁改的?合法 tracing / livepatch / jump label / alternatives / paravirt?还是无法解释?
## 二十三、行为偶发时,不要第一时间猛上 kprobe
- 若问题位于 interrupt / NMI / locking / timing-sensitive race,**instrumentation 本身可能改变时序**
- 更稳的顺序:**已有 tracepoint → ftrace event → fprobe → kprobe → KGDB/QEMU breakpoint**(已有 tracepoint 是静态 typed probe point,本来就是为观测设计的)
- "加 probe 后 bug 消失"**不一定是 bug 没了**——可能只是观察改变了 timing
- 另外 fprobe 与 kprobes 的 recursion 语义并不完全相同;callback 在 interrupt context 中可能出现嵌套
## 总决策树
```
拿到 Linux kernel/module 样本
├─ 先识别 artifact:vmlinux / bzImage(Image) / .ko / 压缩 .ko / built-in / livepatch / runtime dump
├─ 是 module?
│ ├─ modinfo(alias→设备;built-in 也有 modules.builtin.modinfo/.ranges)
│ ├─ ELF sections / relocations / imported+exported symbols
│ ├─ MODVERSIONS(basic __versions / extended __version_ext_*)
│ └─ BTF(standalone 或 split + .BTF.base)
├─ 找不到代码?
│ ├─ built-in? __init 已释放? stripped? split BTF? runtime relocation?
├─ 地址不一致?
│ ├─ KASLR(kernel text + module base)→ 恢复 slide
├─ runtime bytes != disk bytes?
│ ├─ alternatives / static key jump label / ftrace / livepatch / paravirt → 合法
│ └─ 无法解释的 hook → 提高嫌疑
├─ CFG 很怪?
│ ├─ __ex_table(exception edge)/ ORC unwind metadata / callbacks
│ ├─ IBT ENDBR / 编译器优化与内联
├─ module load 失败?
│ ├─ 按阶段:read → decompress → 签名/ELF/blacklist/vermagic/livepatch → layout → 符号 → init
│ └─ CONFIG_MODULE_STATS 的 failed_kreads/decompress/becoming/load 直接告诉你卡在哪一段
└─ 怀疑 rootkit?
├─ 不信任单一 live view(cross-view 扩大到内核可执行内存/callback owner/符号区间)
├─ module cross-view · callback ownership · code integrity
└─ offline memory > 受感染系统的 userspace output
```
## 工具与验证
- 身份与结构:`file` / `readelf -h`(确认 ET_REL 与 artifact 类型)、`readelf -S`(`.modinfo`/`.BTF`/`.BTF.ext`/`.BTF.base`/`.klp.*`/`.orc_unwind*`/`__ex_table`/`__versions`/`__version_ext_*`)、`modinfo`(vermagic/依赖/签名/livepatch/alias)
- 符号与重定位:`nm`、`objdump -r`(stripped 模块的主要线索)、`Module.symvers`(导出与 CRC)
- 类型:`pahole`、`bpftool btf dump file <ko>`(split BTF 需配合 base)
- 运行时:`/proc/kallsyms`(注意 `kptr_restrict`)、`/sys/module/<name>/sections/`、`/sys/module/<name>/`、`bpftrace`/`perf probe`(kprobe)、`ftrace`、debugfs 的 module `stats`
- 加载失败诊断:`dmesg` + `modinfo` + `readelf` 三者对照,配合 `CONFIG_MODULE_STATS` 的计数定位阶段
- 验证:对目标 `.ko` 能同时给出「artifact/构建归属(vermagic/CRC/KMI 无关但 MODVERSIONS 相关)+ 由导入符号推出的功能骨架 + 磁盘与运行态的差异解释」
## 该平台的坑(汇总)
- **把 .ko 当普通 ELF 可执行分析**:ET_REL,relocation 是信息不是噪音
- **把 bzImage 拖进反编译器就判"内核被混淆"**:那是 boot stub + 压缩载荷
- **为"修好"带签名尾的 .ko 去 truncate/strip**:破坏签名(且应只对副本操作)
- **把 `.ko.xz`/`.zst` 当壳**:loader 的正常解压阶段
- **见到 `.BTF` 里 type ID 对不上就判"文件损坏"**:可能是 split BTF(需 base;有 `.BTF.base` 先 relocate)
- **用同版本源码的 offset 硬套**:先排除 RANDSTRUCT(taint `T`)
- **把 livepatch 的 `.klp.rela` 当损坏的 relocation**
- **用 5.7 之前的经验解释符号解析失败**:`kallsyms_lookup_name()` 已不再导出给模块
- **只跑 `lsmod` 就下"没有 rootkit"结论**:必须 cross-view 且不信任单一枚举
- **把合法动态 patching 当篡改**:alternatives / static keys / ftrace / livepatch / paravirt / kprobe 先排除
- **把 `__ex_table` 的 fixup code 标成 dead code**:它是 exception edge,属于 CFG
- **把 ORC unwind 数据当加密 blob / 把 ENDBR64 当混淆**
- **把 kallsyms 全 0 当 hook**:先看 `kptr_restrict`
- **把地址不一致当"不是同一个 kernel"**:先恢复 KASLR slide
- **只等一个漂亮的 call graph**:内核里 callback 注册图更接近真实控制流
- **在偶发路径上先上 kprobe**:tracepoint → ftrace → fprobe 的阶梯,且注意观测会改变时序

View File

@@ -0,0 +1,286 @@
# macOS 低层分支:KEXT / DriverKit(DEXT) / System Extension / 内核集合
<CORE RULE>
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`
```sh
# 取 __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 可以直接用用户态调试器

View File

@@ -0,0 +1,124 @@
# MINIX 3:RS 自愈 / endpoint 身份 / grants / live update
<CORE RULE>
MINIX 3 的驱动大多是**用户态服务**,由 **Reincarnation Server(RS)** 监控:检测崩溃/挂起并**重启**它们。于是:
```
driver 进程(PID/endpoint)消失
→ 几乎立刻出现一个新的 driver
→ 设备继续工作
```
**这不是 rootkit 持久化,是系统自愈。**
第二条:**服务由 label 标识、由 endpoint 寻址**——重启后 label 可以不变,但 **endpoint 会变**。把 endpoint 当持久服务身份,会得到一连串假异常。
</CORE RULE>
## 一、RS 与 endpoint 身份
- RS 监控服务/驱动,检测失败并重启;相关框架处理 fresh / restart / live-update 三类初始化
- **重启后 label 保持,endpoint 改变**;需要按 label 重新查询才能拿到新 endpoint
- 因此下面这条时间线是**正常恢复**:
```
以前给 endpoint X 发 IPC → 突然失败
新的 endpoint Y 接管
```
- **推论**:任何按 endpoint 缓存/比对的引用都会在重启后失效;做行为分析时要把"endpoint 变化"当作一等公民来解释
## 二、消息是定长的,大数据走 grants
- **消息载荷固定大小(56 字节)**;每种消息类型有各自格式(定义在 `minix/include/minix/ipc.h`)
- 超过这个尺寸的传输使用 **grants**(`minix/include/minix/safecopies.h`)——**指向 granter 导出的一块已分配数据块**
- 三种典型用法:
1. **发送超过 56 字节**:grantee 拿到只读 grant,映射后读出,granter 再撤销
2. **请求超过 56 字节**:读写 grant,grantee 写入后发回,granter 撤销
3. **双向**:对已分配内存建立 grant
**所以**:
```
消息里出现数字 12
→ 不一定是用户 buffer 指针
→ 可能是 grant ID
真正的关系:(endpoint, grant ID, offset, rights) → 一段外部内存
```
## 三、grant 校验失败 → `EPERM`,不是"权限不足"
grant 校验按固定顺序进行:
1. 解析 granter 的 endpoint
2. grant 表已注册且索引在范围内
3. 从 granter 的地址空间把条目读回来——**页表走查失败会被报成 `EPERM` 而非 `EFAULT`**,这是有意的:**避免 grantee 借错误码探测 granter 的地址空间**
4. 条目的有效/已用标志置位
5. **序列号与 grant id 匹配**
6. **请求的访问权限 ⊆ grant 标志**(这就是 "access invalid" 那类报错的来源)
7. 目标 endpoint 等于**调用方由内核盖戳的 endpoint**(防伪造 / 混淆代理规则)
8. `offset + bytes ≤ len`
**错误语义(驱动开发者依赖的约定)**:
| 错误 | 含义 |
|---|---|
| **`EPERM`** | **grant 无效**(未注册、序号不匹配、权限不足、越界,或页表走查失败) |
| **`EFAULT`** | **缓冲区未映射** |
**所以 `sys_safecopy*` 返回 `EPERM` 时,不要套 Unix 的"进程没有特权"直觉**——先按 grant 的六类失败逐一排除。
- grant 类型区分:**direct**(把内存解析到 granter)与 **magic**(解析到消息发送方,并额外要求 granter 属于受信任的特权服务集合)
- grant id 是**打包**的(例如位移 20 位,内含 id / 索引 / 序列号),因此"同一个数字"未必指向同一 grant
## 四、Live update:更新期间可能真的有两个实例
- 更新前服务必须到达合适的**静态点**:部分状态预定义,**其余要由服务开发者自己定义**;管理员在发起更新时须提供状态号
- 更新是**进程级**的:**内核(仅)授予新进程对旧进程地址空间的只读访问**,借此先迁移元数据(新旧元数据都在本地可用,从而能按版本无关的命名重新映射状态对象),再迁移数据
- **出错可以回滚**:终止新版本、让旧版本继续执行——**类似被中止的原子事务**;这种进程级更新也**阻止新版本的更新期错误传播回旧版本**
- **更新过程中旧、新两个实例确实可能同时存在**
**所以内存取证看到两个几乎一样的 FS/驱动镜像,不一定是进程注入或重复的恶意实例**。
- 相关约束:更新要求服务采用事件循环设计,使**更新时刻栈上没有状态对象**
- **活跃的 direct grants 会让更新更复杂**:grant 里保存的指针可能需随状态重定位而调整;文档建议在这种情形下**阻止更新或做定制化的状态迁移**
## 决策树
```
拿到 MINIX 3 目标
├─ 驱动"消失又出现"
│ ├─ 先按 RS 重启解释(系统自愈),不要判持久化
│ └─ 对齐 label(稳定)与 endpoint(会变)
├─ IPC 发送失败
│ ├─ 对端是否已重启、endpoint 是否已变
│ └─ 按 label 重新查询而不是复用旧 endpoint
├─ 消息里出现可疑整数
│ ├─ 56 字节以内的载荷是定长消息
│ └─ 更大的传输走 grant → 该整数可能是 grant ID 与 offset
├─ sys_safecopy* 返回 EPERM
│ ├─ 按 grant 校验顺序逐项排除(序号/权限/越界/页表走查)
│ └─ 别套 Unix 的权限直觉;EFAULT 才表示未映射
└─ 内存里出现两个相同服务镜像
├─ 先考虑 live update 的旧/新实例共存
└─ 再看是否存在活跃 direct grants 影响更新
```
## 工具与验证
- 服务生命周期:RS 的监控与重启记录、服务 label 与当前 endpoint 的对应关系
- 通信:消息类型定义(定长载荷)与 grant 相关接口;`(endpoint, grant ID, offset, rights)` 的还原
- 更新:服务的静态点定义、更新与回滚轨迹、更新期间实例数量
- 验证:能同时说清「服务身份是 label 还是 endpoint + 数据是走消息还是 grant + 当前的更新/重启阶段」
## 该平台的坑(汇总)
- **把驱动重启当持久化/注入**:RS 会重启失败的服务,这是自愈
- **把 endpoint 当持久服务身份**:重启后 label 不变而 endpoint 变
- **把消息里的小整数当 buffer 指针**:可能是 grant ID(配合 offset 与权限)
- **把 `EPERM` 当"权限不足"**:它表示 grant 无效,含序号不匹配/越界/页表走查失败等多种情形
- **把 `EFAULT` 与 `EPERM` 混同**:前者表示缓冲区未映射
- **忽略 grant id 的打包结构**:同一个数字未必是同一个 grant
- **把 live update 期间的两个实例当异常**:旧/新共存是更新过程的正常状态
- **忽略活跃 direct grants 对更新的影响**:可能需要阻止更新或定制状态迁移
- **在更新期进行依赖栈上状态的推理**:更新要求栈上没有状态对象

View File

@@ -0,0 +1,104 @@
# NetBSD:按需自动加载模块 / 兼容性约束 / 模块路径布局
<CORE RULE>
NetBSD 的 loader 与 Linux 很像,但模块模型不同:**内核可以在需要时自动加载模块**。
于是现场出现:
```
没有 modload
没有明显的启动脚本
但 xyz.kmod 突然进了内核
```
**不一定存在隐藏 loader**——可能只是:
```
操作请求 → 内核 module 子系统按需加载(依赖模块会被递归带上)
```
这对**时间线逆向**很关键:模块出现的时间点对应的是**某个操作**,而不是某个进程。
</CORE RULE>
## 一、按需加载与自动卸载
- 内核组件用 `module_autoload(name, class)` 定位并加载可选的系统组件(也用于加载其他模块所需的依赖)
- **典型触发**:第一次挂载某类文件系统时,对应模块被自动带入
- **自动卸载**:自动加载的模块会被标记为可自动卸载——一个内核线程在加载后不久(当前实现约 10 秒)尝试卸载,走 `MODULE_CMD_AUTOUNLOAD`,**模块可以返回非零来拒绝**
- 若设置了 `kern.module.autounload_unsafe`,则返回 `ENOTTY`(表示未审计为可安全自动卸载)的模块**也可能被自动卸载**
- **`noautoload` 属性**(同名 `.plist` 里设为 true)使系统**拒绝加载**该模块
- **手工用 modload 加载的模块永远不会被自动卸载**
## 二、兼容性:模块只与特定版本的内核配套
- loader 会检查模块版本与 `__NetBSD_Version__` 的兼容性;不匹配时打印类似
`module 'X' built for 'N', system 'M'` 的警告
- **不带 force 标志则加载失败**(`EPROGMISMATCH`)
- **`MODCTL_LOAD_FORCE`(`modload -f`)会强制加载**,并记录 `forced load, system may be unstable`——**这是官方明确警告过的路径**,不兼容模块可能造成数据损坏或崩溃
- 同一标志还能强制重新加载**被禁用的内建模块**:卸载内建模块会把它标记为禁用,之后**只有带 force 才能再加载**
- `MODCTL_NO_PROP` 会跳过定位模块的 `.plist` 属性字典(默认会去找同名 `.plist` 并合并属性)
**所以**:
```
模块 ELF 看起来正常、符号也在、load 就失败
→ 先查 exact kernel version/build 与模块兼容性
→ 而不是先怀疑 loader 或文件损坏
```
## 三、模块放在哪:路径不是永久契约
- 传统布局:内核在 `/netbsd`,调试内核 `/netbsd.gdb`,模块在 `/stand/<arch>/<version>/modules/`
- **自 2025-04-28 起(UPDATING 20250427)新增构建选项 `KERNEL_DIR`**:把**内核、其调试内核与内核模块放进同一个以内核命名的目录**——
```
旧: /netbsd(内核)、/netbsd.gdb、/stand/amd64/<版本>/modules
新: /netbsd/kernel、/netbsd/kernel.gdb、/netbsd/modules
```
- 该选项**只针对 i386 与 amd64**,且**完全是可选的**(同一台机器上两种布局都可能有)
- **需要更新过的三级 bootloader**(GPT/EFI 引导需更新 EFI 分区上的引导程序);bootloader 会按序尝试 `/netbsd/kernel`、`/onetbsd/kernel`、`/netbsd.old/kernel`
- 已知问题:用了 `KERNEL_DIR` 但内核仍以文件形式放在 `/netbsd` 时,`dmesg` 会报出错误的 `kern.module.path`
**RE/取证含义**:**硬编码 `/stand/...` 的工具会开始漏样本**;判断"模块在哪"应当读运行时报告(`kern.module.path`)或按两种布局分别探测。
## 四、其他现场约束
- **MODULAR 内核启动失败的常见原因是模块缺失或 bootloader 无法加载**,症状可能表现为 `Cannot mount root, error 79` 一类——不要先当成磁盘/内核损坏
- **模块加载的分寸**:模块成为内核的一部分;只有 **securelevel ≤ 0** 或内核编译时带 `INSECURE` 才允许加载新模块
## 决策树
```
拿到 NetBSD 目标
├─ 模块"自己出现了"
│ └─ 先按 module 按需自动加载解释(含依赖递归),对齐到触发它的操作
├─ 模块不见了
│ └─ 查是否被自动卸载(约 10 秒后的尝试)或 noautoload/禁用状态
├─ 模块加载失败但文件看着正常
│ ├─ 查 exact kernel version/build 与兼容性
│ └─ 别直接上 -f/强制加载(官方警告可能不稳定/损坏数据)
├─ 找不到模块文件
│ ├─ 读运行时的模块路径报告
│ └─ 按 /stand/<arch>/<ver>/modules 与 <kernel>/modules 两种布局分别探测
└─ 启动失败
└─ 先查模块缺失/bootloader 能否加载,再谈内核或磁盘
```
## 工具与验证
- 模块子系统:modctl 一族的工具(加载/卸载/查询)与模块的 `.plist` 属性
- 路径与布局:运行时的模块路径报告、两种布局的目录探测、bootloader 的查找顺序
- 兼容性:模块版本与内核版本报告的比对
- 验证:能同时说清「模块为何出现/消失(自动加载或卸载 vs 手工)+ 版本兼容状态 + 实际路径布局」
## 该平台的坑(汇总)
- **找不到显式加载动作就判"模块没加载"**:可能是按需自动加载(含依赖递归)
- **把自动卸载当"模块被恶意移除"**:自动加载的模块会被尝试卸载,且模块自己可以拒绝
- **忽略 `.plist` 的 `noautoload`**:那会让系统拒绝加载,不是文件损坏
- **加载失败就去改 loader/patch 模块**:先核对内核版本与兼容性
- **随手 `-f` 强制加载**:官方明确警告不兼容模块可能损坏数据或崩溃
- **以为卸载内建模块后就干净了**:它只是被标记禁用,仍需 force 才能再加载
- **硬编码 `/stand/<arch>/<version>/modules`**:新布局把模块放到内核目录下,会漏样本
- **把启动失败直接当内核/磁盘损坏**:先查模块缺失与 bootloader 加载能力

View File

@@ -0,0 +1,117 @@
# OpenBSD:KARL 重链接 / autoconf / pledge·unveil
<CORE RULE>
**同一版本的内核,每次启动内部地址都可能不同。**
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 之后文件系统视图已缩小**:"路径存在但打不开"可能是设计
- **在分析环境里用手工安装的内核推断生产行为**:手工拷内核会让重链接脱开

View File

@@ -0,0 +1,162 @@
# OpenVMS:AST / $QIO / 条件处理 / 镜像激活与共享镜像
<CORE RULE>
OpenVMS 有**两条异步/异常控制流**,逆向时都要补进图里:
```
AST(Asynchronous System Trap) → 外部事件驱动的异步控制流
条件处理(Condition Handling) → 异常/状态传播的控制流
```
再加一层**镜像激活**语义(逻辑名、共享镜像、全局节)——三者叠起来,才解释得了"这段代码谁在调、这份映像是哪来的"。
</CORE RULE>
## 一、$QIO 与事件标志
`SYS$QIO` 的关键参数:
| 参数 | 作用 |
|---|---|
| `efn` | 完成时**置位**的事件标志号 |
| `astadr` | 完成时执行的 **AST 例程地址** |
| `astprm` | 传给 AST 例程的参数 |
| `iosb` | 接收最终状态的 I/O 状态块 |
- **发起时事件标志会被清除**;完成时置位
- `iosb` 技术上可选,但**不应当作可选**——并发的异步事件要各用一个独立的 IOSB
## 二、完成顺序与控制流
```
① 写 IOSB → ② 置事件标志 → ③ 触发 AST
```
- 指定了 `astadr` 时,系统在完成时**中断进程并把控制交给 AST 例程**;例程结束**控制返回到被中断的那个点**
- **事件标志置位 ≠ 这个 I/O 完成**:等待服务等的是标志,别的操作也可能置位它;判断完成要用同步等待 + IOSB
- **`W` 后缀 = 同步版本**
## 三、AST 的语义
- AST 像子程序调用,但**无视主流程上下文异步投递**;主线与 AST 共享的代码**必须可重入**
- **AST 不会中止进行中的系统调用**
- 进程休眠或在等待服务上等待时:AST 投递会**把进程暂时移出等待**去执行 AST,完成后**原来的等待调用会被再次执行**
**所以**:`wait 看起来"返回后又自动等待"不是 loop corruption**——这是该模型的正常行为。跳出这个循环只能靠唤醒或置位事件标志。
- 同一访问模式下,AST **不能被同访问模式的另一个 AST 抢占**,但**可被更高访问模式的 AST 抢占**
- **特殊内核模式 AST** 是获得进程上下文的标准机制:在下一次该进程被调度时以该进程最高优先级执行
## 四、屏蔽 AST
- 专门的系统服务可关闭/开启**当前及所有更低特权访问模式**的 AST 投递
- 极短临界区可用**每进程 P1 空间里的两个标志位**代替,避免服务调用开销:投递代码置"已禁用"位,若第一位已置则再置第二位并**不投递**;用户代码事后检查第二位并重新开启
## 五、条件处理:异常的控制流
**信号与处理器**:
- 例程可以**返回完成码**(在 R0 里、按 bit 0 判断)或**显式 signal** 一个条件
- **条件处理设施(CHF)按顺序调用已启用的条件处理器**;signal 会复制参数列表并构造传给处理器的信号参数向量
- **处理器链的行为细节**(做控制流恢复时要记住):
- 当处理器仍在活动时又发生第二次异常,**CHF 会跳过已搜索过的帧**,并调整传给处理器的深度值
- **经异常向量建立的处理器每次异常都会被调用(不被跳过)**——多次信号并发时用它建立处理器要谨慎
- **处理器返回后 CHF 依据返回状态动作**:再 signal 一个条件 / **继续** / **重新 signal(继续搜索下一个处理器)** / **unwind**
**所以**:异常流不是"抛出—捕获"两段,而是**一条可以继续、可以重新投递、可以展开的链**。
## 六、建立处理器的两种方式(含语言差异)
- 一种方式把处理器地址**移到调用者栈帧的 longword 0**(即 `SF$A_HANDLER`),并返回原值;**直到显式恢复或控制返回到调用该建立例程的例程之前都有效**
- 它**修改调用者的栈帧**,主要为**没有内建错误处理的语言**提供;**不该与有预定义处理器的语言混用**(会得到不可预期的结果);手工汇编里等价于把处理器地址写进帧指针偏移 0
- **在 Alpha 与 I64 的原生程序上不可用**,但多数语言编译器会**识别并映射**它:
- 每个过程可用**过程描述符**里的处理器字段标识处理器(Alpha),I64 上则由**展开信息块**的处理器字段标识
- I64 的运行时里没有它;各语言分别处理(有的当内建、有的转成运行时入口点、有的提供自己的建立/恢复内建)
- **32 位信号数组格式在 VAX 与 I64 相同,但条件码与机制数组格式不同**(四字元素、额外寄存器、深度值)
- **C 程序必须用 C 运行时专有的建立接口**:它会捕获与 C 运行时相关的异常并把你不想处理的转给你的处理器;**直接用通用接口配 `setjmp`/`longjmp` 是未定义行为**
- 要让某个异常生成 UNIX 风格信号,用户处理器必须返回"重新投递";返回"正常"则**阻止**生成 UNIX 风格信号
- 用空指针调用可取消该例程中已有的处理器
## 七、镜像启动时建立的默认处理器
**每次启动新映像时,操作系统都会建立默认条件处理器**(三件套,顺序有意义):
1. **traceback 处理器**——在 catchall **之后**建立,因此**先拿到控制**;经消息输出与格式化服务给出错误消息与符号化回溯,并按条件严重性决定继续或退出
2. **catchall 处理器**——建在**第一个栈帧**,**最后被调用**
3. **last-chance 处理器**——经系统异常向量建立;**仅当栈无效、或所有栈处理器都重新投递时**才被调用;**若调试器在场,调试器自己的 last-chance 处理器会替换系统那个**
**只有这三个会输出错误消息。**
## 八、镜像激活与共享镜像
- **重定向共享镜像**:**用与该共享镜像文件名相同的逻辑名**即可——镜像激活器会用这个逻辑名去找目标文件,于是**无需重新链接**就能替换实现
- 共享镜像的默认目录是 **`SYS$SHARE:`**(不是 `SYS$LIBRARY:`,尽管当前二者指向同一批目录)
- 逻辑名必须与"链接时使用的输入文件规格"以及"安装时使用的名字"一致
- 有**动态激活把一个共享镜像装进进程的 P0 空间**的例程
- **特权/仅执行(execute-only)镜像的限制**(安全分析常撞到):
- 激活特权镜像时,**只考虑系统逻辑名表里带执行属性的逻辑名**
- **被激活的镜像、以及它引用的共享镜像与消息节,都必须作为"已知镜像"安装**
- 仅执行(有执行权无读权)的镜像受**与特权镜像相同**的限制;专用限定符(只对主程序有意义)允许激活"用户有执行权但无读权"的共享镜像
- 这种受限模式下,**RMS 只使用"可信"逻辑名**(exec/内核模式创建的);**用户模式与超级visor模式的逻辑名不被信任**;进程/组逻辑名表里的重定向被忽略
## 九、节(sections)与调用标准
- 安装镜像时指定**共享**会为其建立**全局节**(global sections);否则以**私有节**映射
- 共享镜像可安装为让全局节**驻留多端口内存**(需要相应特权)
- **已知镜像(及其全局节)的删除,要等所有使用它的进程释放之后才发生**;可写全局节在移除时写回磁盘
- 调用标准体现在参数与返回值约定上:**返回值是条件值**;参数可以是**按描述符**、按引用或按值;描述符有类别要求(VAX 上某些必须是特定类别);符号查找一类的例程**返回符号的虚拟地址时会加上共享镜像的起始地址**
## 十、RMS 与文件规格解析
- 很多功能**把文件名与默认名分开解析**(而不是拼成一条完整规格)
- 默认设备/目录、默认文件类型(如 `SYS$SYSTEM` / `EXE`)由调用方或例程提供
- **已知镜像的查找忽略版本号**,总是用现存最高版本;**安装命令的规格里不允许指定版本号**
## 决策树
```
拿到 OpenVMS 目标
├─ 例程没有调用者却有行为
│ ├─ 查它是否作为 astadr 传给异步服务 → 补 AST 边
│ └─ 查它是否是条件处理器 → 补条件处理边
├─ 完成路径不完整
│ └─ 补:写 IOSB → 置事件标志 → 触发 AST
├─ 异常处理不完整
│ ├─ 处理器链:继续 / 重新投递 / 展开 三种去向
│ ├─ 第二次异常时 CHF 跳过已搜索帧
│ └─ 三个默认处理器(traceback / catchall / last-chance)
├─ 等待"返回后又继续等待"
│ └─ AST 投递后原等待被重新执行,属正常
├─ 共享镜像行为与预期不符
│ ├─ 逻辑名是否与文件名一致(重定向机制)
│ ├─ 默认目录 SYS$SHARE
│ └─ 特权/仅执行镜像下:必须安装为已知镜像,且只认可信逻辑名
├─ 参数/返回值解析错位
│ └─ 按调用标准:条件值 + 描述符类别 + 按引用/按值
└─ 文件打不开 / 找到的不是那份
└─ RMS 的文件名与默认名分开解析;已知镜像查找忽略版本号
```
## 工具与验证
- 异步登记点:异步服务的 `astadr`/`astprm`/`efn`/`iosb` 参数
- 条件处理:处理器建立点(栈帧字段 / 过程描述符 / 展开信息块)、处理器返回值、默认三件套
- 镜像:镜像激活器与逻辑名(默认目录、重定向规则)、已知镜像安装状态、全局/私有/消息节
- 调用标准:描述符类别、参数传递方式、返回条件值
- 验证:能同时说清「这条 I/O 的完成经哪条 AST 边回来 + 异常会走哪一层处理器 + 这个映像从哪里被激活、节是全局还是私有」
## 该平台的坑(汇总)
- **把 AST 例程当无调用者的死代码**
- **用同步调用图解释完成路径**:必须补 AST 边
- **把"等待返回后又自动等待"当循环被破坏**
- **把事件标志置位当该 I/O 完成**
- **忽略 IOSB**:并发异步事件必须各用一个
- **以为 AST 会中止正在进行的系统调用**
- **把异常当"抛出—捕获"两段**:实际是可继续、可重投、可展开的链
- **忽略"第二次异常跳过已搜索帧"**:处理器的 depth 值会变
- **把经异常向量建立的处理器当只调用一次**:每次异常都会调用
- **在有内建错误处理的语言里混用栈帧式处理器建立方式**:行为不可预期
- **在 C 里用通用接口配 setjmp/longjmp**:未定义行为,必须用 C 运行时专有接口
- **按文件名找共享镜像却忽略逻辑名重定向**:看到的可能不是你以为的那份
- **在特权/仅执行镜像下期待普通逻辑名生效**:只认可信逻辑名,且被引用的共享镜像必须安装
- **按 ASCII/字节流理解参数与记录**:调用标准与 RMS 规格解析各有自己的规则

View File

@@ -0,0 +1,130 @@
# Plan 9 / 9front:进程私有命名空间 / /srv / 9P
<CORE RULE>
**pathname 在 Plan 9 里不是全局标识。**
每个进程(或 namespace group)可以有自己的文件名空间:
```
Process A: open("/foo") → 可能是某个 file server 的树
Process B: open("/foo") → 可能是完全不同的对象
```
**不能只看路径就推断"访问的是同一个对象"**——必须先恢复各自的 namespace 与绑定历史。
</CORE RULE>
## 一、bind / mount:修改的是"当前进程及其 namespace group"的视图
- `bind` 与 `mount` 修改**当前进程及同一 namespace group 内其他进程**的文件名空间
- `bind` 之后,`old` 成为 `new` 所命名对象的**别名**;**`new` 在 bind 时求值**(不是使用时)
- `mount` 的 `servename` 是"**打开后返回一个已存在的到某 file server 的连接**"的文件——**几乎总是 `/srv` 下的文件**;`spec` 参数随 attach 消息传给 server,用于在 server 提供的多棵树中选择
- 目录与文件的绑定类型必须匹配(两边都是目录,或都不是)
## 二、union directory:是**单层**叠加,不是真正的目录树合并
选项决定叠加方式:
| 选项 | 效果 |
|---|---|
| (无) | 用 `new` **替换** `old`;对目录而言,`old` 变成"只含一个目录的 union" |
| **`-b`** | 把新目录加到 union 的**开头** |
| **`-a`** | 把新目录加到 union 的**末尾** |
| **`-c`** | 允许在 union 目录中创建——新文件落在**第一个以 `-c` 绑定**的元素里 |
| `-C`(mount) | 允许内核使用本地缓存,每次 open 校验时效 |
| `-q` | 失败时静默退出 |
**关键限制**:Plan 9 的 union **不是完整的目录树 union**——查找按顺序检查绑定目标并**返回第一个匹配**,只有一层被叠加。例如把 `b` 绑到 `a` 前面时,`b` 中不存在的名字**不会**回落到 `a` 的深层树里——上层目录里"被遮住"的深层项仍然不可见。
**所以**:
```
readdir("/bin") 的结果
→ 不能简单归属到某一个文件系统
```
## 三、`/srv` 不是 socket 目录,而是服务注册表
- `/srv` 是**服务注册设备**,作为可挂载服务的**汇合点**;引导后 `/srv/boot` 里是"到系统加载来源文件系统"的通信端口
- 典型流程(lib9p):建立管道 → 把 fd **以名字发布到 `/srv/<name>`** → fork 一个子进程运行服务循环 → 需要时把它挂载到某个挂载点
**所以**:
```
一个新文件系统出现
→ 完全不需要内核里有对应的文件系统模块
→ 它可能就是一个用户态 server 发布到 /srv 后被挂载的结果
```
## 四、9P:fid 是 session 内的 client 侧句柄
- 请求-响应模型:client 发 **T-message**,server 回 **R-message**,各有类型与 tag
- 标准操作:`Tversion/Rversion`、`Tattach/Rattach`、**`Twalk/Rwalk`**、`Topen/Ropen`、`Tread/Rread`、`Twrite/Rwrite`、`Tclunk/Rclunk`、`Tstat/Rstat`、`Twstat/Rwstat`
- **`fid` / `newfid` 是当前 9P session 内的 client 侧句柄命名空间**:`Twalk` 从已有 fid 出发遍历路径,并把结果绑定给 `newfid`
- 库层(lib9p)里,请求对象与 fid 与"未完成的请求""活跃的 fid"一一对应;**服务循环会拒绝重用已存在的 fid,或打开一个已经打开的 fid**
- 若服务用"文件树"方式实现,walk 由库内部处理,不回调驱动的 walk
**所以**:
```
连接 A 的 fid 5
连接 B 的 fid 5
→ 没有任何必然关系
即使同一连接,fid 也会随 walk/clunk 生命周期变化
→ 不能当 inode 那样的持久标识
```
恢复 9P 通信应当对齐:**connection/session + fid + walk 历史 + qid**。
## 五、`open`/`read`/`write` 很可能实际是用户态 RPC
lib9p 的服务循环:从输入 fd 读请求 → 分发到函数指针(auth / attach / open / create / read / write / remove / flush / stat / wstat / walk / walk1 / clone / destroyfid / destroyreq / start / end)→ 把响应写到输出 fd;**服务循环是单线程的**。
**所以**:
```
open("/net/tcp/clone"); write(...); read(...)
→ 不能直接套 "syscall → 内核 TCP 栈"
→ 实际路径可能是:namespace → 已挂载的 file server → 9P → 用户态处理程序
```
这是 Plan 9 逆向最根本的心智模型。
## 决策树
```
拿到 Plan 9 / 9front 目标
├─ 两个进程访问同名路径
│ ├─ 先恢复各自的 namespace 与 bind/mount 历史
│ └─ 不能因路径相同就认定同一对象
├─ 目录内容异常
│ ├─ 查是否 union(-b/-a 顺序、-c 创建位置)
│ └─ union 只有单层叠加:上层目录不存在的名字不会落到下层深树
├─ 出现"新的文件系统"
│ ├─ 查 /srv 下是否有对应服务被发布
│ └─ 不需要内核文件系统模块
├─ 分析 9P 流量
│ ├─ 按 connection/session 对齐,而不是裸比较 fid 整数
│ └─ 结合 walk 历史与 qid
└─ 文件操作行为与预期不符
└─ 先确认它到底 hit 到哪个 server(namespace → 9P → 用户态处理程序)
```
## 工具与验证
- 命名空间:每个进程/namespace group 的绑定与挂载记录、bind 选项与顺序
- 服务发现:`/srv` 下的条目与它们的发布者;挂载点与 `spec` 参数
- 协议:9P 会话(连接、fid、walk 历史、qid)与消息类型
- 验证:能同时说清「路径在谁的 namespace 里解析 + 最终落到哪个 file server + 数据经过哪些用户态处理程序」
## 该平台的坑(汇总)
- **把 pathname 当全系统唯一资源**:命名空间是每进程/每组的
- **因为路径相同就认定同一对象**:必须先恢复各自的绑定历史
- **把 union 目录当真正的树合并**:只有单层叠加,查找返回第一个匹配
- **忽略 `-b` / `-a` 的顺序与 `-c` 的创建位置**:结果差异很大
- **把 `/srv/foo` 当 socket 文件**:它是服务注册表里的一个连接
- **以为新文件系统必须由内核模块提供**:用户态 server 发布后挂载即可
- **把 9P 的 fid 当 inode/全局文件 ID**:它是 session 内的 client 侧句柄
- **跨连接比较 fid 整数**:不同 session 之间没有可比性
- **把文件操作当 syscall → 内核栈**:通常要经过 namespace → file server → 9P → 用户态处理程序

View File

@@ -0,0 +1,149 @@
# Redox OS:scheme / 启动链 / relibc / 驱动即用户态 daemon
<CORE RULE>
Redox 的核心抽象不是"VFS + 内核驱动",而是 **scheme**:
```
普通程序 read()/write()/mmap()
→ 内核翻译成 SQE 消息
→ scheme provider(通常是用户态 daemon)处理
→ CQE 消息 → 内核转成系统调用返回值
```
所以 **`read()` 不必然意味着文件系统**——它可能是网络、设备、IRQ 抽象或任意系统服务,**含义由目标 scheme 决定**。
第二条:**scheme 侧的 handle 描述符 ≠ 客户端的 fd**,两者由内核映射,**不能跨侧比较整数**。
第三条(分析定位):**syscall ABI 是刻意不稳定的**——稳定层被推到了用户态库。所以你手上的二进制**未必对应当前内核的 syscall 号**,版本对不上不要急着判"被改过"。
</CORE RULE>
## 一、scheme 的形态
| 类型 | 提供者 |
|---|---|
| **用户态 scheme** | 用户态程序(通常是 daemon)——**可能实现的都放用户态** |
| **内核 scheme** | 内核直接实现(只保留关键部分) |
- 根 scheme(`:`)是内核提供的特殊 scheme,作为**所有其他 scheme 名字的容器**
- provider 通过创建 `":myscheme"` 建立 scheme,**返回的文件描述符就是它与内核之间的消息通道**
- **调度分类**:涉及路径的文件系统调用被标为**路径类**或**文件类**——内核**按路径前缀匹配 scheme 名**,或者**记住打开该 fd 时用的是哪个 scheme**;两套匹配方式决定了"换了个路径为什么行为变了"
- scheme 在内核侧是一个 trait(对应用户态 daemon 的 `UserScheme` 实现)
- **其他 IPC 不走 scheme**:管道一类用普通系统调用;另有专门的 IPC daemon 提供共享内存/通道风格的 IPC
## 二、内核侧的转换与映射
- 普通程序的文件操作被内核**翻译成 SQE 消息**交给 provider;provider 用 **CQE 消息**回应,内核再把它转成系统调用的结果
- SQE/CQE 是双向队列条目:**opcode(Open/Read/Write…)、flags、tag(请求 ID)、caller(调用方进程 ID)、args(操作相关参数)**
- **描述符映射是关键**:`open` 请求里带待打开项的名字,scheme 分配自己的编号描述符;**这个描述符与客户端的文件描述符不是同一个**——内核在 `(客户进程, fd 号)` 与 `(provider 进程, handle 号)` 之间做映射,并把它们放进消息包
**RE 含义**:trace 里两侧各有一个整数句柄,**它们本来就不该相等**;建立关系要经内核的映射,而不是比较整数。
- **阻塞语义**:客户端做阻塞 read/write 时被挂起;内核对 provider 的事件描述符发事件包把它唤醒;完成后客户端被标记就绪并放入运行队列
- **缓冲区捕获**:对 `read(fd, buf, len)` 这类操作,内核把调用方缓冲区暴露给 provider——头部拷贝、**中间页零拷贝**(pin 住)、尾部处理
- **FD 传递**:scheme 可以当 IPC broker,把描述符插进调用方的文件表
- **内存映射**:用户态 scheme 经专用 opcode 提供 mmap 区域
- **命名空间**:进程继承父命名空间;较新的架构把命名空间管理移到**用户态 manager 守护进程**,scheme 在内核里匿名创建,请求经 `openat` + 命名空间描述符分发
## 三、daemon 侧的固定套路(决定了它的行为边界)
一个 scheme provider 的典型初始化:
1. 创建 scheme(拿到 fd)
2. 打开它需要的资源 fd(中断、定时器、事件等)
3. **进入 null namespace——此后无法再按名字打开普通资源**
4. 把要监听的事件 fd 登记到事件 scheme
5. 进入循环:等事件 → 读事件判断是定时器 / 资源事件(如设备中断)/ scheme 请求 → 处理请求 → 按 tag 回响应
**所以**:
```
driver 初始化后进入 null namespace、随后打不开普通资源
→ 不是 sandbox 异常,是安全设计
```
## 四、启动链:从引导到第一个用户态程序
```
bootloader(BIOS 版 / EFI 版,后者覆盖 x86_64、aarch64、riscv64)
→ 载入 kernel 与 initfs 到内存、建立初始页表
→ 跳到内核入口(架构相关的汇编入口)
→ Rust 入口:校验 BSS/data、建初始栈、带"内核参数"结构进入
→ 初始化调试输出、内存管理器、分页与堆、(x86)GDT/IDT 与 syscall 指令
→ 主初始化
→ 进入用户态调度循环
```
- 那个"内核参数"结构携带:内核映像、栈、环境、**硬件描述**(x86 上是 ACPI RSDP,aarch64/riscv64 上是设备树)、引导器提供的内存区、以及 **bootstrap / initfs 区域**
- 多核的从核走**另一条入口路径**
**bootstrap(第一个用户态程序)**:
- 内核载入 initfs blob、为它建立地址空间、跳到引导器给的偏移
- bootstrap 在汇编桩里分配栈、**对自己做内存保护设置**、然后 exec `init`,并启动 initfs scheme daemon
- 它本身就是一个**极小的 ELF 加载器**(Rust 编写、带每架构的自定义链接脚本、静态链接)
**initfs 是压缩归档**,含两级启动所需的最小二进制与配置:架构相关的驱动(块设备、输入等)与核心 daemon。**这样系统就能支持多种文件系统驱动而不把它们编进内核。**
## 五、relibc 与 C 运行时
- **relibc** 是 Rust 写的 C 库(连头文件也是,宏除外),**同时支持 Redox 与 Linux,两套后端**:Linux 上走原始 syscall,Redox 上走运行时库
- **稳定 ABI 层在用户态**:应用面向一个稳定 ABI,不稳定部分(syscall ABI、IPC、进程管理)被收进运行时库
- **C 启动序列**:`_start`(crt0)→ 运行时启动函数解析**辅助向量**、建立 argc/argv/env、初始化线程控制块 → `.init_array` 构造 → `main()`
- 辅助向量里有平台特有的项(例如进程 fd、线程 fd、继承的信号屏蔽),**还有动态链接器要用的程序头指针**——所以"这个二进制是静态还是动态"可以从辅助向量侧确认
- 线程控制块用**架构寄存器**承载;线程经 `clone` 创建,**在 Redox 上每个线程用一个文件描述符表示**,共享地址空间并继承文件表
- syscall 包装层负责**可重启语义**(被信号打断后重试)与把内核错误映射成 errno
## 六、驱动与 daemon 的实际形态
- **驱动是独立的用户态程序**(driver schemes,表现为文件风格的服务)
- 它们在文件系统里有固定位置,**PCI 相关的设备配置也以数据文件形式存放**(按厂商/设备组织)
- **驱动是两遍构建**:initfs 里用**体积优化 + panic-abort** 的版本,主文件系统里用**标准 release** 版本
**逆向含义**:同一个驱动在两个位置**不是同一份产物**——不要拿 initfs 里的版本推断主系统里的行为。
## 决策树
```
拿到 Redox 目标
├─ 先判:内核 / 用户态 daemon / 应用
│ ├─ 内核很小,syscall ABI 刻意不稳定 → 版本对不上别判被改
│ └─ 用户态 daemon 是最可能的目标
├─ 一次 read/write 背后是什么
│ ├─ 先定位它是哪个 scheme(路径的 scheme 名即"类型")
│ ├─ 匹配方式:路径前缀 还是 "记住打开时的 scheme"
│ └─ 别默认它是文件系统
├─ 两侧句柄对不上
│ ├─ 客户端 fd 与 provider 描述符本就不相等
│ └─ 经内核映射建立关系
├─ 某个进程之后打不开新资源
│ └─ 可能是 provider 主动进入 null namespace
├─ 想看消息内容
│ └─ 按 SQE/CQE 条目解析:opcode / flags / tag / caller / args
├─ 启动/加载相关
│ ├─ 找 bootstrap 与 initfs(两级启动、极小 ELF 加载器)
│ └─ 设备接线来自 initfs 与用户态 daemon,不在内核
└─ 同款驱动两份产物
└─ initfs 版与主系统版构建方式不同
```
## 工具与验证
- 服务拓扑:路径里的 scheme 名 → provider daemon;scheme 的创建点(`:name`)
- 请求流:SQE/CQE 条目字段(opcode / tag / caller / args)与响应匹配
- 句柄:客户端 fd 与 provider 描述符的内核映射关系
- 启动:引导器类型、内核参数里的硬件描述来源(ACPI vs 设备树)、initfs 内容
- 运行时:辅助向量项(静态/动态、fd 承载的线程与进程句柄)
- 验证:能同时说清「这次 I/O 落到哪个 scheme + provider 是谁 + 两个句柄怎么对应 + 这份产物属于哪一遍构建」
## 该平台的坑(汇总)
- **把 read/write 默认当文件系统**:含义由 scheme 决定
- **跨侧比较句柄整数**:客户端 fd 与 provider 描述符由内核映射,不等价是常态
- **把进入 null namespace 当沙箱异常**:那是 provider 的安全设计
- **在内核里找设备驱动**:驱动多为用户态 daemon
- **忽略 scheme 的两种匹配方式**:路径前缀 vs 记住打开时的 scheme
- **把 syscall 号写死**:ABI 刻意不稳定,稳定层在用户态运行时
- **把 initfs 里的驱动当主系统的同一份产物**:两遍构建,优化与 panic 策略不同
- **把阻塞归因于死锁**:客户端阻塞是设计,由 provider 的事件唤醒机制驱动
- **忽略 fd 传递能力**:scheme 可以充当 IPC broker
- **把启动时的硬件描述来源搞错**:x86 与 aarch64/riscv64 不同(ACPI vs 设备树)

View File

@@ -0,0 +1,201 @@
# seL4 分析分支:capability 系统的逆向
<CORE RULE>
遇到 seL4 系统,**首先恢复的不是"模块调用图",而是「对象图 + capability 分布 + IPC 拓扑 + VSpace/设备映射」**。
原因:seL4 的内核只提供机制,**绝大部分驱动与服务都在用户态**——设备 MMIO、IRQ 等通过 capability 暴露给用户态线程。
所以本分支的核心经验不是"内核模块怎么逆",而是**如何不把 capability 系统的行为误判成普通 OS 行为**。
</CORE RULE>
## 一、先识别运行模型(决定后续一切)
拿到一个 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 耗尽当死锁**

View File

@@ -0,0 +1,145 @@
# Unikraft / MirageOS:库操作系统与 unikernel
<CORE RULE>
这一族的模型是:
```
应用 + 被选中的 OS 库 + 驱动 + 运行时
→ 链接成单一 image / 单一地址空间
```
于是:
```
不要硬找 "user → syscall → kernel process" 边界
```
**"系统调用"可能只是函数调用,"内核"可能就在同一个保护域里。**
第二类坑:**同一份应用代码可以构建成完全不同的目标**,运行时的 I/O、boot、设备模型、地址结构随之全变——看到同一个应用名,**不能假定 binary/运行时架构相同**。
</CORE RULE>
## 一、Unikraft:库操作系统
- 把 OS 功能拆成细粒度**微库**,用 Kconfig 式构建系统选择,**与应用链接成单一二进制**;支持 POSIX 兼容
- 单地址空间、单一保护域,**组件之间没有保护边界**;内核函数可以被**直接执行**——专门的调用机制使进入"内核"**不需要 trap/中断/特权级切换**
- **调度是可选的**:可以构建单线程或 run-to-completion 的 unikernel
两条构建路线(区别对 RE 很关键):
| 路线 | 做法 | 结果 |
|---|---|---|
| **Native(API 兼容)** | 应用**源码**与 Unikraft 一起编译链接;系统调用经 shim 微库解析为处理函数 | 运行期 shim"让开",**syscall 实际变成廉价函数调用** |
| **Binary-compatible(ABI 兼容)** | 直接运行**未修改的 Linux ELF**,无需重编译 | 系统调用被**捕获**并经 shim 重定向到各子系统;缺的 syscall 默认落到 ENOSYS 桩 |
- 二进制兼容路线需要:ELF 格式、二进制兼容的函数接口与数据表示、Linux 系统调用、以及兼容的运行时环境
- **PIE 与否影响地址空间布局**:非 PIE 可执行文件**决定布局并限制为单个非 PIE 应用**;PIE 提供布局灵活性、允许单地址空间内多应用、且**上下文切换时不需要切换地址空间**
**RE 含义**:
- 看到"系统调用"别自动假设特权级切换——**同一保护域里可能只是函数调用**
- 看到"内核函数"与"应用函数"混在一起也正常
- 先判是 native 还是 binary-compatible 构建:两者的调用路径不同
## 二、MirageOS:把配置当接线图
- 用 OCaml 编写,构建成**独立的 unikernel**;同一份源码可构建成多种目标:**Unix 进程**(本地开发)、**Xen**(PVH 模式)、**Solo5 系列**(hvt 硬件虚拟化、spt 严格 seccomp 沙箱、virtio、muen、genode)、Unikraft 目标等
- 构建流程是"**先写配置清单,再生成构建**":
1. 写配置描述组件(**接线图,不是应用本身**)——这里表达的是"应用 functor 的类型"与"把哪个具体实现插进去"
2. 按目标执行配置,生成 Makefile/Dune 文件、**主胶水代码**与**按目标区分的包描述**
3. 构建
- **同一份源码换目标,差别只在一个被替换的模块**(平台主模块)+ 目标对应的包集合;应用代码不变
**关键推论**:
```
源码里没有明确的 "new Driver()"
但 runtime 里出现了设备
→ 不奇怪:设备接线由构建期配置决定
```
- 设备驱动按目标打包(网络、控制台、块设备、时钟各有对应包);某些目标支持多网络/多块设备,另一些限于单个
- 代码跨目标的行为差异(I/O、boot、设备模型、地址结构)来自这一层,而不是应用逻辑
## 三、Unikraft 的启动表与 syscall 符号
**多级初始化表**(这是"0 xref 却在启动时执行"的正解):
| 表 | 阶段 | 说明 |
|---|---|---|
| 早期表 | 最早 | 在**分配器可用之前**,处理 bootinfo |
| 构造表 | 紧随其后 | 同样在分配器之前,库级构造函数 |
| 初始化表 | 之后 | **调度器可用之后**的标准库初始化 |
**初始化层级(按序)**:构造 → 早期 → 平台 → 库 → rootfs → 系统 → 晚期;**每级内有 10 个优先级(0 最高)**,条目放在**按名字排序的专用链接段**里,由起止符号界定。
**所以**:`init 函数 0 xref` **不等于 dead code**。
**syscall shim 的多个符号**:同一操作往往同时存在 **libc 风格**与 **raw-error 风格**的包装(外加 shim 本体)——**不要判"三份重复实现"**,它们是 ABI 包装层。
**native 构建里没有特权转换**:syscall 样式的 API **直接落进库实现**(例如最终走到 VFS 写路径),`write() → 普通调用` 完全可能。
**binary-compatible 构建里真的会出现 Linux syscall ABI**:所以要**先用"native 链接还是加载 Linux ELF"分流**——两者不能共用同一套 CFG 心智模型。
**缺 syscall 返回 ENOSYS 而应用照常工作**:文档明确指出这种情况存在——不少应用会调用自己实际不依赖的 syscall。**"syscall 缺失"不能直接判"应用不兼容"。**
## 四、MirageOS 的 Lwt 与目标指纹
- **没有 OS 线程却像有几百个"线程"**:Mirage 大量使用 **Lwt**——**协作式轻量线程 + 事件循环**,不是抢占式原生线程
- **一个函数进入死循环会让整个 unikernel 像死了一样**:Lwt 线程会一直执行直到主动让出;CPU 密集路径不让出就能**饿死同一事件循环上的全部任务**——**不需要互斥量,也不需要内核死锁**:
```
所有网络定时器停 / 所有异步任务停 / CPU 100%
→ 先查有没有协作式让出
```
- **后台任务失败但主程序行为奇怪**:Lwt 的后台异常交给 **async 异常处理器**,是否终止 unikernel 取决于该处理器的配置——**既不是必然崩溃,也不是必然被忽略**;必须恢复 async 异常策略
- **目标指纹**:不同目标的隔离机制完全不同(硬件虚拟化 / 沙箱进程 / virtio 兼容 hypervisor / Xen PVHv2)——**"这是 MirageOS unikernel"不足以决定你该找什么**(VM-exit?Linux syscall?Xen grant?virtio 队列?),**必须先确定目标**
## 五、这一族的共同 RE 策略
1. **先确认构建目标/构建类型**(native vs binary;Unix vs hvt vs Xen…)——它决定 syscall 路径、地址布局与设备模型
2. **不要假设存在特权边界**:单一地址空间是设计
3. **设备的出现与接线来自构建期配置**,不在应用源码里
4. 现象"应用跑得好好的但找不到它调用内核的痕迹"——在 native 构建里是正常的
## 决策树
```
拿到 unikernel 目标
├─ 先判构建类型/目标
│ ├─ Unikraft:native(源码编译)还是 binary-compatible(跑 Linux ELF)
│ ├─ MirageOS:unix / xen / hvt / spt / virtio / …
│ └─ 目标决定 I/O、boot、设备模型、地址布局
├─ 找不到 syscall 的 trap/特权切换
│ └─ 单保护域下可能就是函数调用(native 构建)
├─ 设备的接线找不到来源
│ └─ 来自构建期配置(配置清单/DSL),不是应用源码
├─ 地址布局相关
│ └─ 非 PIE 决定布局且限单应用;PIE 允许单地址空间内多应用
└─ 同一个应用名、行为不同
└─ 别假定 binary/运行时架构相同,先确认目标
```
## 工具与验证
- 构建类型:构建系统的配置项、是否有 ELF 加载器(binary-compatible 路线)、包集合
- 调用路径:shim 微库的使用点;native 路线的"让开"行为
- 设备接线:构建期配置清单/DSL 与生成的主胶水代码
- 验证:能同时说清「这是哪种构建/哪个目标 + 调用进入内核走的是函数调用还是捕获 + 设备接线来自哪份配置」
## 该平台的坑(汇总)
- **硬找 user/kernel 边界**:单地址空间、单保护域是设计
- **把"syscall"默认当特权级切换**:native 构建下可能只是函数调用
- **假定同一应用名对应同一运行时架构**:构建目标决定一切
- **在应用源码里找设备初始化**:接线来自构建期配置
- **忽略 PIE 与否对地址布局的限制**:非 PIE 决定布局并限单应用
- **忽略 binary-compatible 路线的桩语义**:缺失的系统调用可能落到 ENOSYS 桩,行为与会返回错误不同
- **按"动态库/动态链接器"模型理解 image**:这一族通常是静态整链
- **把 0 xref 的启动函数当死代码**:多级初始化表通过专用链接段注册
- **把 shim 的多个符号当重复实现**:libc 风格 / raw-error 风格 / shim 本体是三层包装
- **把 ENOSYS 当"应用不兼容"**:不少应用会调用自己并不依赖的 syscall
- **把"所有任务一起卡死"当内核死锁**:协作式线程不让出就会饿死整个事件循环
- **把后台任务异常当必然崩溃或必然被忽略**:取决于 async 异常处理策略
- **不确认目标就去找 VM-exit / syscall**:不同构建目标的隔离机制完全不同

View File

@@ -0,0 +1,78 @@
# 内核逆向决策树与证据分级
## 场景决策树(入口 → 分支)
### 1. 目标类型判定(第一级)
```
拿到 .sys 或内核相关样本
├─ 单独 .sys(无用户态样本) → 静态主线(第 2 节)
├─ .sys + 用户态样本 → 静态主线 + 交互闭环(第 2 节 + IOCTL 分支)
├─ rootkit 嫌疑(隐藏/钩子行为报告) → rootkit 特征分支(第 3 节)
├─ 崩溃 dump / 蓝屏定位 → [[re-windbg]] !analyze -v 直接走
└─ 加载器/服务安装器含驱动 → 先还原安装链(服务注册),再进静态主线
```
### 2. 静态主线(驱动分析)
```
DriverEntry 定位(EP 或 stub 跳转)
├─ 注册 MajorFunction(28 项数组赋值/RtlCopyMemory)
│ ├─ 有 IRP_MJ_DEVICE_CONTROL(0x0E)/INTERNAL_DEVICE_CONTROL(0x0F) → IOCTL 分支
│ ├─ 有 IRP_MJ_READ/WRITE → 数据流 handler 分支
│ └─ 全默认 handler → 可能靠 minifilter/回调工作(下查 Flt*/Ps* 导入)
├─ IoCreateDevice/IoCreateSymbolicLink → 设备名 → 用户态入口 \\.\名
├─ IoAttachDevice* 出现 → filter 驱动(文件/键盘/网络过滤)→ 查挂载点与回调
├─ FltRegisterFilter 出现 → minifilter → 查 FLT_REGISTRATION 与 Instances 配置
└─ PsSet*/CmRegister/ObRegister 回调注册 → 回调分支(第 3 节)
```
### 3. rootkit 特征分支(按技术归类)
```
rootkit 嫌疑特征
├─ 函数头被改写(jmp rel32 / mov rax;jmp rax)→ inline hook → 逐字节比原始镜像(≥16 字节)
├─ 表项地址异常(!ssdt 对照)→ SSDT hook → 注意 Win8 起符号不导出 + x64 PatchGuard
├─ 进程/驱动枚举差集 → DKOM 摘链表 → 调试对照 + 取证插件复核
├─ 回调数组异常地址 → 回调滥用 → 查回调类型与注册点
├─ 文件操作被劫持 → minifilter 挂载 → 查 FLT_REGISTRATION 回调表
└─ 无静态特征但有行为异常 → 动态:内核调试断点定位 + 内存取证对照
```
### 4. IOCTL 交互分支
```
DeviceIoControl 调用点(用户态样本)
├─ 取 IOCTL 码 → 解码 CTL_CODE(高 16 位设备类型, bit14-15 访问, bit2-13 功能, bit0-1 方法)
│ ├─ Method 0 (BUFFERED) → SystemBuffer 处理
│ ├─ Method 3 (NEITHER) → Type3InputBuffer(需自己探针/ProbeForWrite)
│ └─ Method 1/2 (DIRECT) → MDL 映射
├─ 驱动 handler 反编译 → 输入结构逐字段对齐(类型传播复核)
└─ 闭环验证:用户态触发 → 内核断点命中 → 参数核对
```
## 证据分级表
| 级别 | 证据形态 | 示例 | 用途 |
|---|---|---|---|
| A 强 | 动态 + 静态闭合 | 内核断点命中 handler 且参数与静态分析一致;hook 字节与原始镜像 diff 落证 | 可下"确定"结论 |
| B 中 | 静态链完整 | DriverEntry→IRP→handler 全链还原、IOCTL 结构对齐 | 可下"高可信"结论 |
| C 弱 | 静态特征 | 仅导入表含 Flt*/回调 API、可疑字符串 | 只支持"疑似",不下结论 |
| 反证 | 良性解释 | 回调为合规过滤功能(EDR/备份)、hook 是调试器断点 | 记录并存档 |
- 内核证据必须带版本上下文:结构偏移、符号可用性、PatchGuard 行为都随系统版本变——结论标注目标系统版本
- rootkit 结论要两条独立证据线(调试会话 + 取证转储)互相印证,单线证据降一级
## 实现教训(内化)
- 先符号后偏移:有符号服务器/PDB 时全部用符号字段名,裸偏移只在无符号时用并标注版本
- IRP 表是驱动的心脏:从 28 项分发表出发比从 DriverEntry 顺序读代码更能覆盖全行为
- 交互闭环比单向静态可信:IOCTL 结构是否猜对,用用户态触发 + 内核断点验证,比反复读反编译快
- rootkit 特征按"改了什么"分类:改表项(SSDT)/改代码(inline)/改链表(DKOM)/挂回调/挂过滤——每类有对应的对照方法,别混用
- PatchGuard 是分析边界也是安全边界:被保护结构的修改行为只在调试 VM 验证,报告里标注"该手法在目标版本是否仍可行"
- 每次驱动加载前打快照,崩溃即回滚——蓝屏是常态不是意外
## 使用注意
- 全部动态环节在沙箱调试 VM 内([[re-sandbox]],[[re-analyze/platform-tips]] 最高原则);测试签名只开在分析 VM(见 [[windows-gotchas]])
- 内核分析结论涉及系统完整性(rootkit 手法),报告限定在授权分析范围,不提供可用做真实环境对抗的完整载荷

View File

@@ -0,0 +1,29 @@
# 内核逆向方法论坑与边界
## 版本差异组(内核分析的最大变量)
- **结构布局随版本变**:EPROCESS/DRIVER_OBJECT/_KPCR 等内部字段偏移随 Windows 版本变化(Win10 各 build 之间也有差异)——有符号用符号名,无符号按 `ntddk.h`/公开结构手工布局并标注目标版本;反例:旧版本文档偏移直接套新系统全错
- **导出符号随版本变**:`KeServiceDescriptorTable` Win8 起不再导出(SSDT 定位需按索引/内存特征找);`!ssdt` 命令在新版 WinDbg 输出也不同——按版本查工具能力
- **签名要求随版本变**:x64 一直强制驱动签名;Win11 新版引入 attestation 签名要求——分析 VM 用测试签名(bcdedit,关 Secure Boot)只覆盖部分场景,某些驱动加载不了是环境限制,不是分析错误
- **PatchGuard 随版本变强**:Win7 时代有效的 SSDT/关键函数 inline hook,在 x64 新版触发 bugcheck 0x109(CRITICAL_STRUCTURE_CORRUPTION)——"历史手法"与"当前可用手法"要分开标注,别在真实环境验证受保护结构的修改
## rootkit 技术边界组(手法识别反例)
- **inline hook 不改地址**:函数指针/表项全正常 ≠ 无 hook——inline hook 改写函数头指令,必须逐字节比对原始镜像(从 `lmv m nt` 拿路径读原始文件),比对长度 ≥16 字节才覆盖 `mov rax,imm64;jmp rax` 形态
- **SSDT hook 不是唯一表**:新版系统还有 IDT/irp/回调等 hook 面——只查 SSDT 会漏;回调数组(PsSet*/CmRegister/ObRegister)与 minifilter 是更常见的现代手法
- **隐藏 ≠ 不存在**:DKOM 摘链表后 `!process` 看不到,但对象仍存活(句柄/引用可定位)——取证复核用 `psxview` 的交叉视图而非单视图
- **minifilter 伪良性**:`FltRegisterFilter` 本身是合规过滤框架(EDR/备份/杀软都在用)——判断恶意性要看回调做什么(读写内容篡改?),不是看有没有注册
- **调试器 vs 恶意 hook 混淆**:内核调试下的断点(`ba`/`bu`)也会改写指令/寄存器上下文——采集 hook 证据前先停用调试器断点,避免把调试痕迹当样本行为
## 环境与工具组
- **蓝屏即回滚**:驱动 bugcheck 后调试会话可能不可用(重试时驱动已加载一半)——快照回滚是最快的恢复路径,别在坏状态里继续验证
- **断点未解析**:`bp <地址>` 在模块未加载时无效——用 `bu <符号>`(延迟绑定);`ba` 硬件断点受数量限制(4 个)且要求地址对齐
- **符号服务器可达性**:内网/离线环境 `!analyze -v` 无符号输出变裸地址——预下载符号缓存(`srv*C:\symbols*...` 提前 `.symfix`+`.reload`),分析前确认符号加载
- **测试签名环境差异**:测试签名只影响驱动加载校验,不影响 PatchGuard/结构校验——"能加载"不等于"能 hook",边界分开判断
- **VM 内核调试链路**:串口 named pipe 比 KDNET 稳(不依赖网卡);调试机与目标机时间/性能差异会影响时序类检测行为——记录环境参数
## 使用注意
- 驱动加载/触发/验证全部在调试 VM 内([[re-sandbox]] + [[re-analyze/platform-tips]] 最高原则),生产环境不加载分析样本
- 结论按 [[windows-decision-tree]] 证据分级标注版本上下文;rootkit 手法描述限定在授权分析场景

View File

@@ -0,0 +1,506 @@
# Windows 驱动分支(模型识别 → callback / 设备栈 / I/O 路径)
<CORE RULE>
**先识别 driver model,再恢复真正的 callback / 设备栈 / I/O 路径。**
看到 `.sys` 就按「DriverEntry → MajorFunction[] → IOCTL」一条线往下走,会漏掉 KMDF、NDIS miniport、StorPort miniport、文件系统 minifilter 等一大批真实驱动——它们的 WDM dispatch 由 framework/port driver 持有,真正的业务入口是**注册给 framework 的 callback**。
七个最容易产生事实性错误的点:① 把所有 `.sys` 都当 WDM;② 把 `GsDriverEntry` 当业务入口;③ 把 `MajorFunction` 为空解释成"无 I/O";④ 不知道 KMDF/NDIS/minifilter 的 callback registration;⑤ 不知道 **IRP completion 是异步控制流**;⑥ 不知道 **IOCTL 的 Method 决定 buffer 语义**;⑦ 2026 年仍用旧的 Windows 驱动签名假设。
</CORE RULE>
## 一、先确认到底是哪一种 Windows driver
```
.sys
├─ raw WDM / NT driver
├─ KMDF
├─ PnP function driver
├─ upper / lower filter
├─ file-system minifilter
├─ legacy file-system filter
├─ NDIS miniport / filter
├─ StorPort / storage miniport
├─ AVStream / PortCls / 其他 miniport
└─ 特殊安全 / 虚拟化类 driver
```
另外:**UMDF driver 通常不是内核 `.sys` 业务主体**,而是由 `Wudfhost.exe` 装载的**用户态 DLL**。
WDM 直接处理 IRP;KMDF 则由 framework 接住 IRP,再调用驱动注册的 event callback。
> **经验**:`MajorFunction[]` 看起来什么都没有 **≠** 这个 driver 没有 I/O 接口——它可能压根不是 raw WDM。
## 二、DriverEntry 不一定是真正的 DriverEntry
PE header 的 entry point 可能落到 **`GsDriverEntry`**(工具链自动生成的初始化 wrapper),之后才调用开发者写的 `DriverEntry`:
```
AddressOfEntryPoint → GsDriverEntry → security/runtime init → 真实 DriverEntry
```
反编译器把入口函数命名为 `DriverEntry` 也别盲信:看它**有没有再调用一个** `NTSTATUS f(PDRIVER_OBJECT, PUNICODE_STRING)` 并完成 callback 初始化。否则容易把编译器包装层当成业务代码分析。
## 三、WDM:真正重要的是 `DRIVER_OBJECT` 赋值图
传统 WDM 的关键初始化:
```
DriverObject->DriverUnload / DriverObject->DriverExtension->AddDevice
DriverObject->MajorFunction[IRP_MJ_CREATE / _CLOSE / _READ / _WRITE
/ _DEVICE_CONTROL / _INTERNAL_DEVICE_CONTROL / _PNP ...]
```
`MajorFunction` 是 "IRP major code → dispatch routine" 的函数指针数组。所以 stripped WDM 最有效的逆向方法是**找赋值点**:
```asm
mov [DriverObject + xx], handler
```
然后恢复成映射表:
```
IRP_MJ_DEVICE_CONTROL → sub_140003820
IRP_MJ_CREATE → sub_140001A00
IRP_MJ_CLOSE → sub_140001A00
IRP_MJ_PNP → sub_140004C10
```
这比普通 call graph 有意义得多。逐项看 handler 时的关注点:
- `IRP_MJ_DEVICE_CONTROL`(0x0E,用户态交互)、`_INTERNAL_DEVICE_CONTROL`(0x0F)、`_CREATE`(0x00)/`_CLOSE`(0x02)、`_READ`(0x03)/`_WRITE`(0x04)
- 每个 handler 用 `IoGetCurrentIrpStackLocation(irp)` 取参数 → 分支处理(IOCTL 码分派)
- **参数校验**(InputBufferLength/OutputBufferLength)——长度校验缺失是驱动类漏洞常见成因(漏洞面分析转 [[re-vuln]] 思路)
**设备对象与符号链接**:
- `IoCreateDevice` 的 DeviceName(`\Device\MyDriver`);`IoCreateSymbolicLink` 的 SymbolicLinkName(`\DosDevices\MyDriver` → 用户态 `\\.\MyDriver`)
- 没符号链接时用户态可直接 `DeviceIoControl` 打 `\\.\` 名(或无符号名只能内部引用)
- 动态核对:`!drvobj <名>` 看设备对象链、`!devobj <设备>` 看设备名与 AttachedDevice——与静态字符串对照,确认运行期没被改
- 多层设备栈(filter 驱动):看到 `IoAttachDevice` / `IoAttachDeviceToDeviceStack` 即为拦截型驱动(文件/键盘过滤等)
**服务注册与加载入口**:
- SCM 注册:`CreateServiceW`(Type=SERVICE_KERNEL_DRIVER、Start=2 自动/3 手动/0 引导)或 INF 安装;恶意加载器常用 `Start=3` + 手动启动
- 注册表:`HKLM\SYSTEM\CurrentControlSet\Services\<驱动名>` 的 `ImagePath` 指向 `.sys`
- 启动失败码:577 = 签名错误、1275 = 未签名驱动被拒(x64)——先查测试签名状态再看代码
- 从用户态样本([[re-binary-core]])的创建服务调用反推驱动名,与静态 `.sys` 对应
## 四、KMDF:千万别按 WDM 找
KMDF 中 **framework 自己注册 WDM dispatch**,再把 IRP 转成 `WDFREQUEST` / `WDFDEVICE` / `WDFQUEUE`,调用驱动注册的:
`EvtDriverDeviceAdd` / `EvtIoRead` / `EvtIoWrite` / `EvtIoDeviceControl` / `EvtIoInternalDeviceControl` / `EvtDevicePrepareHardware` / `EvtDeviceD0Entry` …
典型路径:
```
DriverEntry → WdfDriverCreate → EvtDriverDeviceAdd → WdfDeviceCreate → WdfIoQueueCreate → EvtIoDeviceControl / EvtIoRead / ...
```
> **特殊情况**:`!drvobj` 显示的 MajorFunction **大部分指向 `Wdf01000.sys`** —— 这完全正常。
> **不要得出"驱动逻辑在 Wdf01000.sys"**:真正的业务入口是它交给 WDF 的 callback。
## 五、KMDF stripped 后找不到明显 callback 赋值
WDF 初始化大量使用 `WDF_*_CONFIG` / `WDF_OBJECT_ATTRIBUTES` 结构体:
```c
WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&cfg, ...);
cfg.EvtIoDeviceControl = Handler;
WdfIoQueueCreate(..., &cfg, ...);
```
优化器可能把这一过程拆得很碎:stack struct 初始化 → 某 offset 写函数地址 → `WdfIoQueueCreate`。
所以**不要只搜 `xref → WdfIoQueueCreate`**,要向前做 dataflow:
```
WDF_IO_QUEUE_CONFIG → 哪个 offset 被写了函数地址 → 对应哪个 EvtIo*
```
否则看到 `sub_140017260` 却不知道它实际是 `EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL`。
## 六、`.sys` 没有漂亮的 DriverEntry → IRP 路径,可能是 **miniport**
Windows 有大量「**miniport + Microsoft port/framework driver**」结构:
- **NDIS**:`vendor.sys` + `ndis.sys` 共同形成一个驱动层——vendor NDIS miniport 的 AddDevice、部分 IRP dispatch **都是 ndis.sys 提供的**,vendor 通过 miniport callback 参与工作
- 所以 `DriverObject->MajorFunction[]` 几乎都不是 vendor 的,**不等于 vendor driver 没功能**
- 要找的是 `NdisMRegisterMiniportDriver` / `NdisFRegisterFilterDriver` 及注册结构里的 callback
- 同样思路适用于 **StorPort / PortCls / AVStream / 其他 class or port-miniport 架构**
## 七、NDIS:别当普通 IRP 网络驱动
要恢复的是 miniport / filter 的 callback 图:
```
miniport: MiniportInitializeEx / MiniportHaltEx / MiniportPause / MiniportRestart
MiniportOidRequest / MiniportSendNetBufferLists / MiniportReturnNetBufferLists …
filter: FilterAttach / FilterDetach / FilterRestart / FilterPause
FilterSendNetBufferLists / FilterReceiveNetBufferLists / FilterOidRequest …
```
数据主体是 `NET_BUFFER_LIST` / `NET_BUFFER`,且 filter module **堆叠在 miniport 之上**:
```
protocol → NDIS → filter A → filter B → miniport → NIC
```
> 所以**看不到 `IRP_MJ_READ` / `IRP_MJ_WRITE` 对网卡驱动完全不奇怪**;不要强行找 `ReadFile()` 对应的 handler。
## 八、device stack 必须恢复(行为可能来自任何一层)
PnP 世界:`Upper Filter → Function Driver (FDO) → Lower Filter → Bus Driver (PDO)`;I/O Manager 把 IRP 交给**最上面的 device object**,再由驱动继续向下传。
典型误判:
> 应用打开 Device X → 某行为发生 → 一定是 X 的 function driver 做的
**不一定**——行为可能来自 upper filter / lower filter / class filter / bus filter。动态分析真正应恢复:
```
DeviceObject → AttachedDevice chain → DriverObject → image owner
```
而不只是 `\Device\Foo → foo.sys`。
## 九、INF 往往比 `.sys` 更快解释驱动是什么
驱动包通常是 `.sys + .inf + .cat (+ coinstaller/DLL/firmware)`。INF 直接给出:
`Hardware ID` / `Compatible ID` / `service name` / `ServiceBinary` / `StartType` / `class` / `AddService` / **`UpperFilters` / `LowerFilters` / `AddFilter`**
- Win10 1903+ 的设备 filter 可用 `AddFilter` 声明 upper/lower/filter level;更老的驱动大量通过注册表 `UpperFilters` / `LowerFilters` 插入设备栈
- 只有 `foo.sys` 时,你丢失的不只是安装文件——**而是 `foo.sys` 在整个设备栈中的位置**
## 十、文件系统 minifilter:完全是另一套世界
看到 `FltRegisterFilter` 应立即切换模型:
```
FLT_REGISTRATION → FLT_OPERATION_REGISTRATION[]
IRP_MJ_CREATE → PreCreate / PostCreate
IRP_MJ_READ → PreRead / PostRead
IRP_MJ_WRITE → PreWrite / PostWrite
IRP_MJ_SET_INFORMATION → ...
```
**FltMgr 按 altitude 排序 minifilter**:pre-operation 从高 altitude 往低走,post-operation 返回时反向执行。所以:
```
PreRead A → PreRead B → filesystem → PostRead B → PostRead A
```
**不是奇怪的 recursion,而是正常的 Filter Manager pipeline。**
## 十一、Altitude 不是随便写的版本号
minifilter 的 `Altitude = "320000"` 不是 build ID、优先级数值或版本号——它决定该 filter 在 minifilter stack 中的**位置**,且 Microsoft 分配 altitude 范围。
因此分析多 filter 相互作用("A 抢先看到文件""B 为什么看到的是修改后的请求")**必须考虑 altitude**。
`CmRegisterCallbackEx`(注册表回调)同样带 altitude,并用于 callback ordering。
## 十二、IOCTL 先解 `CTL_CODE`
一个 IOCTL(如 `0x222004`)不要一直当 magic integer——拆成 `DeviceType` / `Access` / `Function` / **`Method`**:
| Method | 数据在哪 |
|---|---|
| `METHOD_BUFFERED` | `Irp->AssociatedIrp.SystemBuffer` |
| `METHOD_IN_DIRECT` / `METHOD_OUT_DIRECT` | `MdlAddress` 与锁定/映射的页 |
| `METHOD_NEITHER` | **可能直接携带原始用户态虚拟地址** |
Microsoft 明确规定 `DeviceIoControl` 的 transfer method 来自 **IOCTL 自身的 TransferType**。所以最好把 `switch (IoControlCode)` 恢复成带语义的注释再逆 handler:
```
IOCTL_800: Function = 0x800, Method = BUFFERED, Access = ANY
IOCTL_801: Function = 0x801, Method = NEITHER
```
## 十三、"内核直接解引用一个很低的地址":先确认是不是 `METHOD_NEITHER` 的用户指针
- BUFFERED 关注 `AssociatedIrp.SystemBuffer`;DIRECT 涉及 `MdlAddress`
- **NEITHER 甚至可能直接携带原始用户态虚拟地址**,I/O Manager 不负责替你锁定或验证(Microsoft 明确指出 neither I/O 的用户地址只在合适的调用线程上下文中才能安全访问)
所以反编译看到内核 dereference 一个"很低"的地址:
> **别立即判断"内核指针损坏"** —— 先确认是不是 METHOD_NEITHER 的用户指针。
安全审计时这也应立即提高优先级:`ProbeForRead` / `ProbeForWrite`、`try/except`、`RequestorMode`、thread context 变得关键。
## 十四、IRP 控制流不是普通函数调用图(completion edge)
WDM 最大的坑之一是 **asynchronous completion**:
```
Dispatch → IoCopyCurrentIrpStackLocationToNext → IoSetCompletionRoutine
→ IoCallDriver → STATUS_PENDING → ……下层异步完成 → CompletionRoutine
```
- **`IoCallDriver` 返回了 ≠ 这次 I/O 已结束**
- completion routine 返回 `STATUS_MORE_PROCESSING_REQUIRED` 会**阻止 I/O Manager 继续完成原 IRP**,让当前 driver 接管后续 processing(Microsoft 有明确规定)
逆向 CFG 应增加一类边:**completion edge**——否则很多关键 cleanup / copy-back / retry 代码在普通静态 call graph 中像"没人调用"。
## 十五、`STATUS_PENDING` 不是失败
做驱动协议 RE 时,`status = IoCallDriver(...); if (status == STATUS_PENDING) ...` **不要**按用户态直觉理解成"请求没完成/调用失败"——这是正常 asynchronous I/O。
真正终态在 `IRP->IoStatus.Status` 与 completion path。记录协议时应区分:
- **dispatch return status**
- **final I/O completion status**
## 十六、`PAGE` section 是语义,不只是 section 名
所有以 `PAGE` 开头的 section(`PAGE` / `PAGEABC` / `PAGEDATA` …)都被当作 **pageable section**。
- 函数在 `PAGE` → 设计上它应只在**允许 page fault 的 IRQL** 上运行
- 相反:ISR / DPC / spinlock-held path / `DISPATCH_LEVEL+` 对应的代码**必须 resident**
所以 **section 本身就是调用上下文线索**。
## 十七、偶发 0xD1 不一定是随机内存破坏
`DRIVER_IRQL_NOT_LESS_OR_EQUAL`(0xD1)优先检查:当前 IRQL、fault address、读/写/执行、faulting instruction、**目标代码/数据是否 pageable**(典型原因:DISPATCH_LEVEL 或更高 IRQL 访问 pageable memory / 执行 pageable code,或访问无效、已释放的指针)。
特别阴的一类:
> 99.99% 都没问题,压力测试突然炸
可能只是那个 `PAGE` section 平时一直 resident,**直到内存压力把它真正换出去**。所以「平时运行正常」≠「IRQL/pageability 正确」。
## 十八、Driver Verifier 开着时行为可能**故意"不正常"**
Verifier 可主动做:Special Pool / Force IRQL Checking / Pool Tracking / I/O Verification / Deadlock Detection / DMA Verification / **Low Resources Simulation** / **Force Pending I/O Requests** / IRP Logging / 同步延迟模糊 / Code Integrity checking …
也就是说它会故意让 allocation 失败、强制 pending、改变时序、更激进地驱逐 pageable memory。因此:
- 实验机一开 Verifier 就大量 `STATUS_INSUFFICIENT_RESOURCES` → **不要误判成"malware 正在耗尽资源"**,这可能是 verifier 的 fault injection
- "只有开 Verifier 才 BSOD" 通常反而说明**潜伏的 driver bug 被放大出来了**
## 十九、Verifier 抓到 IRP 错误:别只看 crash 栈顶部
I/O Verification 追踪 IRP 生命周期,检测非法 `IoCallDriver` 参数、重复/错误 complete、IRP 还挂着 cancel routine 却 complete、IRQL 变化错误、已释放的 IRP、stack-lifetime 已结束的 event/IOSB……违反时触发 **0xC9 `DRIVER_VERIFIER_IOMANAGER_VIOLATION`**。
这类 crash 的**最后一个函数可能只是 verifier 发现问题的位置**,真正犯错的地方可能更早几十步。要跟完整条链:
```
allocation → forward → pending → cancel → completion → free
```
## 二十、静态地址 ≠ 运行态地址:先想 PE relocation
`.sys` 仍是 PE image;不能加载到 preferred `ImageBase` 时,loader 按 `.reloc` 应用 base relocation。
所以 `IDA: sub_140012340` vs `runtime: fffff806'72b12340` 是正常的。做 memory-vs-disk 对比前必须先:
```
runtime VA → 减 runtime image base → RVA → 对比对应 section
```
否则会得到大量假 mismatch。
## 二十一、PDB 找到了也可能不是完整符号
Windows RE 的巨大优势是 PDB,但要区分 **public symbols** 与 **private/full symbols**:public 通常不给完整 local variables、static functions、source lines/type universe。
所以 `nt!KeSomething` 有名字、但大量内部 `sub_xxx` 没名字,**不代表符号加载失败**——可能只是你拿到的是 public symbols。
## 二十二、PDB 同名不代表匹配(signature + age)
symbol store 用 **signature + age** 选择版本——文件名相同没有任何意义。遇到:
```
*** WARNING: Unable to verify timestamp / symbols could not be loaded
```
先检查 exact binary/PDB matching;一般直接 `.symfix` → `.reload /f` → `!sym noisy`,比手工硬套一个"看起来版本一样"的 PDB 可靠。
## 二十三、`!analyze -v` 是入口,不是结论
`Probably caused by : foo.sys` **不能直接写成"foo.sys 一定是根因"**——它可能只是:
- 最后解引用坏指针的人(受害者)
- 下层已经把内存写坏
- filter stack 中最后一个可识别模块
pool corruption / UAF / DMA corruption 场景下,**真正写坏内存的 driver 往往早就离开栈了**。继续结合 `lm` / `locks` / `process` / `memory` / bugcheck parameters。
## 二十四、UMDF:看到 DLL + Wudfhost,立刻换思路
UMDF driver 是 DLL,由 **`Wudfhost.exe`** 加载;Wudfhost 负责 I/O dispatch、驱动加载、驱动分层、线程池、与 kernel reflector 通信——**一个 Wudfhost 还可能同时承载多个 UMDF driver/device stack**。
所以 `Wudfhost.exe` 崩了**不能推出"Wudfhost 自己有 bug"**——先确定:哪个 UMDF DLL、哪个 device stack、哪个 callback。
`!wdfkd.wdfumdevstacks` 可直接枚举 UMDF 设备栈(kernel 调试或 attach 到 wudfhost.exe 的用户态调试都可用):
```
!process 0 0 wudfhost.exe # 找 host 进程
.process /P <addr> # 切到该进程
!wdfkd.wdfumdevstacks # device stack / PDO 名 / UMDriver image path / transfer mode
```
其他有用命令:`!wdfkd.wdfumtriage`、`!wdfkd.wdfldr`、`!wdfkd.wdfumirps`、`!wdfkd.wdfumfile`、`!wdfkd.wdflogdump <drivername.dll> -m`。
## 二十五、UMDF 崩溃 ≠ kernel 崩溃
UMDF 跑在用户态 host 里,所以 driver access violation **通常先杀 Wudfhost**,而不是立即整个 Windows bugcheck。
- UMDF dump 位于 **`%ProgramData%\Microsoft\WDF`**(UMDF 2.15 / Win10 1507 起;此前在 `%windir%\system32\LogFiles\WUDF`);用 `WinDbg -z <dmp>` 打开,**`!analyze` 对它同样有效**
- WER 报告有三类事件:`WUDFHostProblem` / `WUDFUnhandledException` / `WUDFVerifierFailure`(含 ExitCode、Operation、Message(编码 IRP 的 Major/MinorFunction)、HardwareId)
- 所以确认是 UMDF 后:**userspace dump / 进程上下文**通常比一上来抓 kernel dump 更直接
## 二十六、"驱动存在但加载不了":先查签名与策略,不要先怀疑 DriverEntry
Win10 1607 起,新的 release kernel driver 原则上要通过 Microsoft Hardware Dev Center(WHCP)签名路径;PnP package、catalog、boot driver 又各有细节。而**现在(2026 年)还有一条更硬的变化**:
> Microsoft 的 Windows Driver Policy 明确:**2026 年 4 月安全更新后,旧式 cross-signed 驱动默认不再受信任**,内核默认只接受 WHCP 签名的驱动(另有 Microsoft 维护的 allow list)。适用于 Win11 24H2 / 25H2 / 26H1 与 Server 2025 及后续版本。
实施方式是分阶段的:**先进入评估模式**(只审计不拦截;系统需满足约 100 小时运行 + 2–3 次启动相关条件才转入强制执行;期间若发现不合规的 cross-signed 驱动,会继续留在评估模式并重置计数);企业可用 **Application Control for Business**(原 WDAC)策略覆盖(策略须由 Secure Boot PK/KEK 中的权威签名)。
所以遇到:
> 十年前的 `.sys`、签名看起来完全存在、以前能加载、现在新系统突然 block
**不要直接判断"签名损坏"**——很可能是 **legacy signing policy 已经过期**。
## 二十七、Test Mode 也不是"未签名什么都能跑"
- 即使 `TESTSIGNING ON`,在启用 **HVCI / Memory Integrity** 的环境中,测试驱动**仍要求有数字签名**(只是可以用自建 test certificate)
- 且 HVCI **仍可能否掉** test-signed 驱动(实践中需临时关掉 Memory Integrity 再装/测)
- 另外 Secure Boot 开启时 `bcdedit /set testsigning on` 可能被阻止或失败(多数场景要关 Secure Boot)
所以 `TESTSIGNING=ON + unsigned .sys + 加载失败` **不能说"Test Mode 没生效"**——还要看 HVCI / Memory Integrity、Secure Boot 与签名。加载失败的典型码:**0xC0000428(STATUS_INVALID_IMAGE_HASH)**,CodeIntegrity 日志 **Event ID 219**。
## 二十八、HVCI 下老驱动的奇怪失败:可能是**设计本身不兼容**
HVCI(Memory Integrity)的核心:**内核内存页永远不能同时可写+可执行(W+X)**,可执行代码不能被直接修改,内核内存变可执行只能经由 CI 验证。官方兼容要求:
- 默认 NX、用 NX 分配(`NonPagedPoolNx`)
- **不用既写又可执行的 section**;**不直接修改可执行系统内存**
- **不在内核使用动态代码**(自解密 stub、JIT、运行时生成代码)
- 不把数据文件当可执行加载;section 对齐为 0x1000 的整数倍
因此老驱动里若有:分配 RWX pool / 生成 trampoline / runtime patch kernel text / JIT code——**HVCI off 时也许能跑,on 时可能直接失败**。
> 不要第一时间认为"新 Windows 把 undocumented API 改了"——**可能撞的是 CI/VBS 策略**。
补充时间点:Microsoft 计划自 **2026-10-13** 起在符合条件的 Win11 设备上**默认启用 Memory Integrity**(前提之一正是"没有不兼容的内核驱动")。
## 二十九、x64 上"老教程的 kernel patching"思维尤其危险
Microsoft 明确要求 x64 driver **不应**修改 kernel code、IDT/GDT、undocumented kernel structures——这类行为可能触发 `CRITICAL_STRUCTURE_CORRUPTION`(历史的 KPP 就是为保护内核代码与关键结构)。
所以分析现代合法驱动时,如果**没看到** SSDT hook / inline ntoskrnl patch / IDT hook,**不是"它功能少"**——现代驱动更应该通过 documented callback、filter model、WFP、minifilter、Ob callbacks、process/thread/image notify、registry callback 实现观察与控制。
## 三十、安全产品不 hook SSDT 也完全可以深度监控进程
Windows 正式提供 `PsSetCreateProcessNotifyRoutineEx*`、`ObRegisterCallbacks` 等;Microsoft 自己的 sample 就演示「进程创建通知 + 进程句柄 access restriction」的组合。
所以看到 EDR/AV driver **没有 SSDT hook,绝对不能得出"它没监控进程"**。应搜 **callback-registration graph**:
```
PsSetCreateProcessNotifyRoutine* / PsSetCreateThreadNotifyRoutine*
PsSetLoadImageNotifyRoutine* / ObRegisterCallbacks / CmRegisterCallbackEx
FltRegisterFilter / Ndis*Register* / StorPort / WDF 注册点 ...
```
这比找 inline hook 更适合现代 Windows。
## 三十一、一个函数没有任何 xref,也可能是 callback
Windows 内核与 Linux 一样高度 callback-driven:DriverObject dispatch、PnP、ISR/DPC、timer、work item、completion routine、KMDF event、NDIS callback、FltMgr callback、Ob callback、registry callback、process notify……
所以反编译器显示 **0 callers 不能自动标 dead code**。先看它的地址是否:
```
写进 registration structure / 传给 registration API
写进 DriverObject / 写进 WDF config
```
Windows driver RE 应同时维护 **call graph 与 callback-registration graph**。
## 三十二、同一个 IRP 被很多 driver 碰过是正常的
```
app → upper filter → function driver → lower filter → bus
```
每层都可能:修改 stack location、设置 completion routine、forward、complete。所以看到「同一个 MajorFunction 在四个 `.sys` 里都有 handler」**不是重复实现**——这正是 Windows layered I/O model。
逆向时最好恢复 **IRP flow graph**,而不只是每个 driver 单独的 CFG。
## 三十三、Minifilter unload 后还有 completion 行为,不一定 UAF
Filter Manager 专门负责 minifilter 生命周期,并能协调 unload 时仍未完全结束的操作(也允许 minifilter 安全动态卸载)。
所以捕获 trace 时看到「开始 unload → 还有旧 I/O completion → 最后才彻底消失」**并不自动意味着"已卸载代码仍被调用"**。需要精确区分四态:
```
unload initiated → instance detach → outstanding operations drain → image actually gone
```
## 决策树
```
拿到 Windows driver
├─ 先确定模型:WDM / KMDF / UMDF / minifilter / NDIS / miniport / filter driver
├─ entry 看起来奇怪
│ ├─ GsDriverEntry? framework wrapper? 真正 DriverEntry 在下一层?
├─ 找不到 IRP handler
│ ├─ KMDF callback? NDIS/miniport callback? minifilter callback? UMDF?
├─ 恢复安装上下文:INF / service / hardware ID / UpperFilters·LowerFilters / AddFilter
├─ 恢复 I/O
│ ├─ DriverObject MajorFunction / IOCTL CTL_CODE / buffering method
│ ├─ IRP stack / pending / completion edge
├─ 文件系统:FltRegisterFilter / FLT_REGISTRATION / pre-post callback / altitude
├─ 地址·符号怪
│ ├─ PE relocation? runtime image base? PDB exact match? public vs private PDB?
├─ 随机 crash
│ ├─ IRQL? PAGE section? UAF/pool? pending/completion lifetime? DMA?
│ └─ 是否开着 Driver Verifier?
├─ driver 加载失败
│ ├─ architecture? signing? 2026 legacy cross-sign policy? Secure Boot? HVCI? INF/PnP 绑定?
└─ 怀疑 rootkit
├─ 不要只找 SSDT → callback ownership / filter·device stack / registered notify callbacks
├─ loaded image · executable memory · runtime code integrity
└─ cross-view + dump analysis
```
## 工具准备
参考 [[re-analyze/platform-tips]]——驱动加载/内核调试属动态执行,默认沙箱(调试机 VM)内进行;静态分析部分免沙箱。
- **反编译与驱动类型**:[[re-ghidra]](默认;Data Type Manager 导入 Windows 内核类型,字段名直接参与反编译)/ [[re-ida]](FLIRT 内核签名 + 类型库)。验证:导入 `.sys` 后能反编译出带参数签名的函数
- **WinDbg 内核调试**([[re-windbg]]):双机/VM 串口或 KDNET。验证:`lm` 看到目标驱动、`.reload /f <驱动名>` 加载符号
- **符号**:Microsoft 公共符号 `srv*C:\symbols*https://msdl.microsoft.com/download/symbols`;驱动自带 PDB 同名放置;遇到匹配问题用 `.symfix` + `!sym noisy`。验证:`!process 0 0` 输出带 `nt!` 前缀
- **测试签名**(仅调试 VM,且注意 HVCI):`bcdedit /set testsigning on` 重启(多数场景需关 Secure Boot);验证 `bcdedit /enum {current}` 为 Yes
- **WDF/UMDF 扩展**:`!wdfkd.wdfumdevstacks` / `wdfumtriage` / `wdfldr` / `wdfumirps` / `wdflogdump`;UMDF dump 在 `%ProgramData%\Microsoft\WDF`
- **Verifier**:`verifier`(分析目标 bug 时先确认它开没开,见 §18–19)
## 该平台的坑(汇总)
- **把所有 `.sys` 当 WDM**:KMDF/NDIS/miniport/minifilter 的 WDM dispatch 由 framework 持有
- **把 `GsDriverEntry` 当业务入口**;**把 `MajorFunction` 为空当"无 I/O"**
- **在 KMDF 上找 `IoCallDriver` 链路**:`!drvobj` 指向 Wdf01000.sys 是正常的,去找 Evt* callback(含 `WDF_*_CONFIG` 的 dataflow)
- **在 miniport 上找 IRP handler**:去找 `Ndis*/StorPort/PortCls` 注册 callback;网卡驱动没有 READ/WRITE 也正常
- **把设备行为只归给 function driver**:先恢复 AttachedDevice 链与 image owner
- **漏看 INF**:`UpperFilters`/`LowerFilters`/`AddFilter` 决定它在设备栈中的位置
- **把 minifilter 的 pre/post 流水线当 recursion**;**把 altitude 当版本号**
- **把 IOCTL 当 magic integer**:先解 `CTL_CODE`(尤其 Method)
- **把 `METHOD_NEITHER` 的用户指针当"内核指针损坏"**
- **把 `IoCallDriver` 返回当 I/O 结束**、**把 `STATUS_PENDING` 当失败**
- **忽略 `PAGE` section 的 IRQL 语义**,把偶发 0xD1 当随机破坏
- **把 Driver Verifier 的故障注入当恶意行为**;**只看 0xC9 栈顶**
- **忘了 PE relocation** 就做内存-磁盘对比;**拿文件同名当 PDB 匹配**
- **把 `!analyze -v` 的 "Probably caused by" 当结论**
- **把 Wudfhost 崩溃当它自己的 bug**、**把 UMDF 崩溃按 kernel crash 分析**
- **2026 年仍按旧签名假设判断"加载不了"**(legacy cross-sign 已默认不受信;Test Mode 在 HVCI 下也要签名)
- **把 HVCI 下的失败当"API 变了"**(是 W+X/动态代码等设计不兼容)
- **只找 SSDT hook 判断安全产品有没有监控**;**把 0 xref 当 dead code**
## 附:Windows 专属的两份附录
- [[windows-gotchas]] —— **版本差异组**:结构布局(EPROCESS/DRIVER_OBJECT/_KPCR 偏移随 build 变)、导出符号(`KeServiceDescriptorTable` Win8 起不导出)、签名要求(attestation 等)、PatchGuard 随版本变强——同一条经验在不同 Windows 版本可能给出相反结论
- [[windows-decision-tree]] —— Windows 专属场景决策树与**证据分级**(目标类型 → 静态主线 / 交互闭环 / rootkit 分支)

View File

@@ -0,0 +1,136 @@
# Fuchsia / Zircon:handle 模型 / DFv2 驱动框架 / FIDL
<CORE RULE>
两条会把整条分析链带错的事实:
① **Zircon 的 handle 整数值只在当前进程内有意义**——它和 seL4 的 CPtr 是同一类坑(见 [[sel4-kernel]]);
② **驱动是用户态 component**——不在内核里,不是"另一种 kext/`.sys`"。
```
handle(进程本地数值)
→ 该进程的 handle 表
→ 内核对象
→ rights
```
这三者必须一起恢复。**只拿 handle 数值做跨进程对齐,一切结论都不可信。**
</CORE RULE>
## 一、handle 语义:数值不是对象的全局 ID
- handle 的整数**只对当前进程有意义**:**同一个数值在另一个进程里可能根本不对应任何 handle,也可能对应完全不同的内核对象**
- **handle 被关闭后,其数值允许被重用**
- 因此下面这条推理是**无效**的:
```
A: zx_channel_write(0x4f, ...)
B: zx_channel_read(0x4f, ...)
→ "同一个 channel"
```
正确做法是分别恢复两个进程各自的 handle 表,**按底层的 kernel object 对齐**,而不是按数值对齐。
## 二、handle 可以"随消息搬家":in-transit 与 transfer
- channel 不只是传字节,**还能传 capability**:handle 写进 channel 后,会**从发送进程移除**并挂到消息上,接收方读到后拿到**新的 handle**
- **传输途中属于 in-transit 状态**:只绑定在进程上或只绑定在内核中;**绑定在内核中的 handle 对用户态不可见**——"in-transit 的 handle 在用户态看不到"是设计如此,不是丢失
- 所以 trace 里出现下面这种序列**可能是完全正常的**,不是 UAF:
```
A 曾持有 handle 0x57
→ channel_write 把它送走
A 再访问 0x57 → BAD_HANDLE
```
- 权限约束:channel 自身需 `ZX_RIGHT_WRITE`;**被发送的每个 handle 需 `ZX_RIGHT_TRANSFER`**
- **写入失败不等于 handle 还在**:失败时 handle 被**丢弃而非转移**(写操作被改成"始终消费"语义)。所以"调用失败了,那它应该还持有这个 handle"是错的
- 原子性:一次写入中的 handle **要么全部进入 channel,要么全部被丢弃**
- 变体 `zx_channel_write_etc`:每个 handle 包在 `zx_handle_disposition_t` 里,操作为 **`ZX_HANDLE_OP_MOVE`**(等价于 `handle_replace` + write,源 handle 总是被关闭)或 **`ZX_HANDLE_OP_DUPLICATE`**(源 handle 保留)
- **rights 只能收窄不能放大**:可以在传输时降权,甚至可以置为 `ZX_RIGHT_NONE`;**若移除了 `ZX_RIGHT_TRANSFER`,接收方拿到的 handle 就无法再转手**——这是"为什么这一段传不下去了"的常见解释
- 特例:**in-transit 且它所在的 channel/socket 被销毁 → 该 handle 被关闭**
**推论**:reachability / security 关系图必须同时记录 **object + handle + rights**——两个 handle 可以指向同一个对象却持有不同的 READ / WRITE / MAP / TRANSFER 权限,"同一个对象"不等于"同样的能力"。
## 三、DFv2:驱动是用户态 component,绑定靠 bind rules 不是 probe
DFv2 的实体分工:
| 实体 | 职责 |
|---|---|
| **driver manager** | 启动/停止驱动、为驱动路由 FIDL capability、**维护全部节点(node)的拓扑** |
| **driver host** | 承载驱动实例的进程(独立地址空间与线程);**一个 driver host 可以同驻多个驱动** |
| **driver index** | 登记全部可用驱动及其元数据(component URL + **bind rules**),按请求对节点做匹配 |
| **driver runtime** | 同驻驱动之间的本地通信机制 |
| **FIDL** | 驱动之间、驱动与框架、驱动与非驱动组件之间的主要通信方式 |
绑定序列:
```
父驱动创建子节点
→ driver manager 向 driver index 发 MatchDriver
→ driver index 用各驱动的 bind rules(字节码)比对节点属性
→ 返回最匹配驱动的 URL
→ driver manager 创建/复用 driver host,启动驱动实例(Start())
→ 驱动可以再创建子节点,循环继续
```
**所以不要去找 Linux 式的 `bus probe()`**:应恢复的是
```
node properties → bind rules → driver index → driver component → driver host → FIDL services
```
驱动还有层级之分(boot driver / base driver / universe driver)——**"系统里没有这个驱动"先按加载来源分层看**,universe driver 是开发期手动登记的。
## 四、同驻驱动:syscall trace 里找不到 channel 调用,不一定是分析错了
- **同驻(co-located)**:驱动可以要求被放进父驱动所在的 driver host,从而**共享同一地址空间**
- 同驻驱动之间,**driver runtime 提供进程内通信路径**——比走内核 channel 更快,代价是**不产生 `zx_channel_write()` 这类 syscall 痕迹**
所以:**功能上明显发生了 driver A → driver B 的 IPC,但 syscall trace 里干干净净**——先怀疑 driver-runtime local transport,而不是"看漏了"或"样本在隐藏调用"。
- 相关的还有 node "symbols"(键值对,值可含虚拟地址):**仅在父子同驻同一个 driver host 时用于进程内通信**;不同驻时走 FIDL
- 反向推论:**看到大量直接内存地址交换,往往说明这两个驱动同驻**——这本身是一条拓扑证据,可用于恢复 driver host 划分
## 五、`/dev/foo` 不是 Unix 设备文件
- 打开 devfs 里的 entry 后,**拿到的通常是一个 FIDL channel**,之后的通信走 FIDL,而不是 read/write/ioctl 那套设备文件语义
- 新工具链还在**逐步从 devfs 转向 service discovery**:设备能力的暴露方式随年代变化,**"我在 `/dev` 里没找到它"不等于它不存在**
## 决策树
```
拿到 Fuchsia 目标
├─ 先问:这是用户态 component 还是内核?
│ ├─ 用户态(绝大多数驱动与系统服务)→ DFv2 模型
│ └─ 内核(Zircon 本体)→ 内核对象与 handle 表
├─ handle 相关
│ ├─ 跨进程比对 handle 数值 → 无效!改成按 kernel object 对齐
│ ├─ 曾经有 handle、现在 BAD_HANDLE → 先查是否被 channel_write 送走(in-transit/transfer)
│ └─ 传不过去 → 查 ZX_RIGHT_TRANSFER 是否被移除(rights 收窄过)
├─ 设备/驱动相关
│ ├─ 别找 bus probe() → 恢复 node properties → bind rules → driver index → driver host
│ ├─ 驱动"没加载" → 分辨 boot/base/universe 来源
│ ├─ 有 IPC 但无 syscall → 查是否同驻同一 driver host(driver runtime local transport)
│ └─ 找设备 → 别只翻 devfs,现代路径可能走 service discovery
└─ 权限/能力异常 → 画 object + handle + rights 三方关系图,而不是只看对象图
```
## 工具与验证
- 拓扑:driver manager 维护的 node 拓扑、driver index 的匹配结果(node → bind rules → driver URL)
- 组件与进程:driver host 是进程——**同驻与否可以从 driver host 划分直接读出来**,也是解释"无 syscall IPC"的关键证据
- 通信:FIDL 调用链(驱动与框架/其他组件的主路径)
- 验证:能同时说清「对象归属(哪个进程的哪个 handle)+ rights + 通信路径(FIDL / 同驻本地通道 / 内核 channel)」
## 该平台的坑(汇总)
- **把 handle 数值当全局对象 ID**:数值只在进程内有效,跨进程比对无意义,关闭后数值还会被重用
- **把 in-transit 当丢失**:绑定在内核中的 handle 对用户态不可见是正常状态
- **把"送走后再访问报 BAD_HANDLE"当 UAF**:transfer 成功后发送方本来就不再持有
- **以为写入失败 handle 就还在**:失败时是被丢弃而不是转移
- **忽略 rights**:同一对象上的两个 handle 能力可以不同;`ZX_RIGHT_TRANSFER` 被移除会让能力无法继续传递
- **按 Linux 思路找 bus probe()**:Fuchsia 的绑定是 node properties + bind rules 由 driver index 匹配
- **在 syscall trace 里找不到 IPC 就认为分析错了**:同驻驱动走 driver runtime 的进程内通道,本来就不产生内核调用
- **用 Unix 设备文件模型理解 `/dev/foo`**:打开后通常得到 FIDL channel;新代码还在转向 service discovery
- **把同驻驱动的内存地址交换当信息泄露或异常**:那是父子同驻 driver host 时的正常进程内通信机制

View File

@@ -0,0 +1,163 @@
# z/OS:TCB·SRB / 三个地址空间 / load module 与 LE / AMODE·RMODE·XPLINK
<CORE RULE>
z/OS **不能直接套 process + thread 模型**。可调度的工作单位有两类,地址空间不止一个,**而且程序自身还有一套"我能在哪种寻址模式下跑、我住在哪里"的属性**:
```
可调度单位:TCB / SRB
地址空间:home / primary / secondary
程序属性:AMODE / RMODE(+ XPLINK 与否)
```
三者叠在一起,才会出现:
```
当前执行地址属于另一个地址空间 → 不能自动判控制流劫持
同一个模块在两种调用约定下栈行为相反 → 不能自动判栈损坏
```
</CORE RULE>
## 一、两种可调度单位
| | TCB | SRB |
|---|---|---|
| 代表 | 在地址空间中执行的任务 | 运行系统服务例程的请求 |
| 典型产生 | 用户程序与支持它们的系统程序 | 一个地址空间检测到影响**另一个**地址空间的事件 |
| 归属 | home 地址空间包含该 TCB | 与 ASCB(局部 SRB)或主调度器(全局 SRB)对齐 |
| 能力 | 常规任务语义 | **总在 supervisor state**;**不能调用 SVC(除 ABEND)或 WAIT**;可跨内存/AR 模式运行、发出 PC、调度另一个 SRB |
- **SRB 提供地址空间间通信的异步机制**(同步对应物是带访问寄存器的 cross-memory services)
- **PC(PROGRAM CALL)指令要求可调度工作单位以 TCB 或 SRB 形式存在**
**所以**:没有传统"线程创建图"的执行代码不一定是异常。
## 二、三个地址空间:home / primary / secondary
- 程序首次被调度时,**home 与 primary 相同**
- **home 是该 TCB/SRB 最初被调度的地址空间,在整个执行期间保持不变**;**`PSAAOLD` 总是指向 home**(硬件不会改它)
- **primary**:取其段表来取指令的地址空间;**secondary** 用于 secondary ASC 模式下的数据访问
- 取指规则:primary/secondary/AR 模式从 **primary** 取指;home 模式从 **home** 取指;real 模式从实存取指
## 三、PC 指令与空间切换
- **PC-SS(空间切换)**:被切到的地址空间成为 **primary**,secondary 通常设为原 primary,**home 不变**
- **PC 例程始终运行在发出 PC 的那个 TCB/SRB 之下**
- 两种链接方式:**basic PC**(手动机保存/恢复调用者环境,用 PT 返回)与 **stacking PC**(系统把环境保存在 linkage stack 上,用 PR 返回)
- PC 通过入口表与链接索引定义;PC 号由链接索引与入口表索引拼接
## 四、AR(访问寄存器)模式
- 每个通用寄存器配一个 AR;放进 AR 的 **ALET 标识目标地址空间**
- **特殊 ALET:0 = primary、1 = secondary、2 = home**
- AR 模式可与最多 16 个地址空间通信,**并且可以调用 SVC**(这是它优于 basic cross-memory 的地方)
## 五、约束与经典故障
- **cross-memory services 需要 APF 授权**;远端地址空间**必须 non-swappable**
- **跨地址空间使用 TCB 指针是经典的 S0C4-11 成因**——TCB 在不同地址空间中**不在同一虚拟地址**
- 在跨内存模式(primary ≠ home)下,工作单位的用户身份取自 **home ASCB 或 TCB/SRB**
## 六、可执行材料:load module / program object
| 形态 | 说明 |
|---|---|
| **load module** | 传统链接编辑产物 |
| **program object** | 更新的形态,可住在 **PDSE** 里;**z/OS UNIX 的 load module 住在 PDS 里** |
| 链接 | 链接编辑 / program binder;链接库列表决定解析来源 |
**AMODE / RMODE(每个程序对象都带)**:
- **AMODE**:程序支持哪种硬件寻址模式——24 / 31 / **ANY** / **64**
- **RMODE**:程序对象**住在哪里**——24(16 MB 以下)/ **ANY**(以下或以上)
- **程序对象与运行中的程序各自都有 AMODE 属性**;语言运行时选项与编译器选项共同影响 AMODE/RMODE 行为与存储位置
- 某些场合下 AMODE 24 执行受限,必须用 31
**XPLINK 与非 XPLINK 的差异(判栈/调用约定时的关键)**:
| | XPLINK | 非 XPLINK |
|---|---|---|
| 栈增长方向 | 向上 | 向下 |
| 栈展开 | **BACKCHAIN** 编译子选项 | backchain 指针 |
| 栈指针 | GPR4,**带 0x800 偏置** | 常规 |
| 参数传递 | 参数在寄存器/调用者的 DSA 中(STOREARGS) | R1 指向参数列表 |
- 运行时会在 XPLINK ↔ 非 XPLINK 之间**自动插入衔接代码**;**两者之间的转换仅支持 31 位**情形
- 转换可用专门的跟踪选项观察(跟踪项类型区分入口/出口)
- **不支持**的路径包括:COBOL 动态调用 XPLINK 函数、某些语言的 FETCH、部分加载服务
**逆向含义**:**同一份代码在两种约定下栈行为相反**——看到"栈向上长"或"栈指针带偏置"不要判损坏,先确认它是不是 XPLINK。
## 七、supervisor services 与 SVC
- 系统服务经 **SVC** 与中断处理程序提供;**SVC 表**是定位系统服务入口的落点
- 与 SVC 相关的是 `SVC` 类型的 dump(与 standalone dump 并列)
- **PC 表**(程序调用入口表)同样是可定位的静态结构
## 八、诊断与 dump
- **dump 类型多种**(含 SVC dump、standalone dump),由 dump 管理机制产生
- **IPCS** 是**离线分析 dump 的工具**:定义/选择 dump、一系列 dump 命令;配合 **SLIP**(自动化的诊断动作)与 **GTF**(跟踪)使用
- **XPLINK 下控制块比 31 位更少**,因此 **IPCS 的 LEDATA 视图尤其重要**:它能给出 traceback 与 DSA 层次(与语言运行时提供的 dump 服务同类)
- 语言运行时另提供 dump/traceback/SNAP 类服务与跟踪表工具
- **跟踪表内容以 EBCDIC 呈现**;运行时也提供 ASCII/EBCDIC 字符模式切换服务
**逆向含义**:拿到一个现场,先确认 **dump 类型**与"是否 XPLINK"——后者决定你该去找哪些控制块、以及 traceback 从哪里取。
## 九、字符与记录模型
- **字符是 EBCDIC**(不是 ASCII)——字符串常量、日志、跟踪表都按此处理
- **记录模型**:数据集结构、记录格式、VSAM 属于 z/OS 数据管理的独立一套概念,**不要套 POSIX 字节流文件**
## 决策树
```
拿到 z/OS 目标
├─ 执行单位是什么
│ ├─ TCB(地址空间内的任务)还是 SRB(系统服务请求)
│ ├─ SRB 不能调 SVC(除 ABEND)、不能 WAIT
│ └─ 没有线程创建图 ≠ 异常
├─ 地址对不上
│ ├─ 区分 home / primary / secondary
│ ├─ PC 之后 primary ≠ home 属正常,home 不变
│ └─ 取指来自 primary(home 模式除外)
├─ 栈行为反常
│ ├─ 先确认是不是 XPLINK(栈向上长、栈指针带 0x800 偏置)
│ ├─ XPLINK ↔ 非 XPLINK 转换仅限 31 位,且需要衔接代码
│ └─ 别把两种约定的差异判成栈损坏
├─ 程序装不进/跑不起来
│ ├─ 查 AMODE(24/31/ANY/64)与 RMODE(24/ANY)
│ └─ 程序对象与运行中程序的属性都要看
├─ 跨地址空间传数据
│ ├─ basic cross-memory(需 APF 授权,远端须 non-swappable)
│ └─ 或 AR 模式(ALET 0/1/2;可调 SVC)
├─ 出现 S0C4-11 一类故障
│ └─ 查是否跨地址空间用了 TCB 指针
├─ 现场诊断
│ ├─ 确认 dump 类型;XPLINK 下控制块更少 → 依赖 LEDATA
│ └─ 字符按 EBCDIC 读
└─ 数据访问
└─ 记录模型(数据集/记录格式/VSAM)≠ POSIX 字节流
```
## 工具与验证
- 执行单位:TCB 与 SRB 的区分;SRB 的调度路径(局部/全局)
- 地址空间:home / primary / secondary 的当前值与切换点(PC / PR / PT / SSAR)
- 程序属性:load module / program object 的 AMODE、RMODE、是否 XPLINK;所在库类型(PDS vs PDSE)
- 诊断:dump 类型、IPCS 视图(XPLINK 下重点看 LEDATA)、跟踪表
- 验证:能同时说清「工作单位是 TCB 还是 SRB + 当前 primary/secondary/home 各是谁 + 这段代码在哪种寻址模式与调用约定下运行」
## 该平台的坑(汇总)
- **把 TCB/thread 模型直接套上去**:SRB 是另一类可调度单位,约束完全不同
- **以为所有执行都有线程创建图**:SRB 由事件触发
- **把 primary 与 home 混同**:PC 之后它们不同,且 home 永不改变
- **用 primary 的地址解释跨内存模式下的权限**:身份取自 home ASCB 或 TCB/SRB
- **跨地址空间复用 TCB 指针**:经典 S0C4-11 成因
- **忘记 SRB 不能 WAIT/不能调 SVC**
- **把 XPLINK 的栈行为判成损坏**:方向相反、栈指针带偏置、展开机制不同
- **忽略 XPLINK 与非 XPLINK 之间转换只支持 31 位**
- **只看程序对象忽略运行中程序的 AMODE**:两者都有该属性
- **在 XPLINK 下按 31 位的控制块清单找现场**:控制块更少,要看 LEDATA 视图
- **按 ASCII 处理字符串与跟踪表**:这里是 EBCDIC
- **把数据集/记录格式当字节流文件**