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,110 @@
---
name: re-fp-runtime
type: atomic
description: >
函数式语言运行时逆向(Haskell/OCaml):闭包/堆对象模型、调用约定、数据流优先策略。
触发词:Haskell逆向、OCaml逆向、GHC RTS、thunk、STG、OCaml runtime、闭包、函数式产物。
capabilities: [lang-runtime-analysis]
---
# 函数式运行时逆向(Haskell / OCaml)
## 何时使用 / 何时不用
- 用:Haskell/OCaml 产物(GHC RTS 符号、OCaml block 头特征),需要还原闭包/堆对象、求值顺序、模式匹配分支
- 用:OCaml 原生/字节码产物判别与字节码分析(ocamlrun 脚本头特征)
- 不用:命令式语言产物(各归各技能:C++ → [[re-cpp-abi]]、Go → [[re-go]]、Rust → [[re-rust]])
- 不用:只需函数逻辑且控制流完整(函数式产物控制流打散,直接反编译收益低,见步骤 4)
## 工具准备
### readelf / llvm-nm(符号分析)
- 安装与验证见 [[re-cpp-abi]] 工具准备
### ghc 工具链(Haskell 侧,可选)
- Linux/macOS: GHC 安装包(`apt install ghc` / `brew install ghc` / ghcup);Windows: ghcup(`winget install ghcup` 或官网安装器);验证: `ghc --version`
- 用途: 同版本编译对照产物,验证 closure/info table 形态(GHC 版本差异大)
### ocamlobjinfo / ocamlopt(OCaml 侧,可选)
- Linux/macOS: OCaml 工具链(`apt install ocaml` / `brew install ocaml`);Windows: opam(`winget install OCaml.opam`)或官网安装器;验证: `ocamlobjinfo` 处理任意 .cmx 输出 CRC 与导入表
- 用途: 字节码产物/对象文件结构分析(ocamlobjinfo 可读 .cmo/.cmx/字节码可执行文件)
### Ghidra / IDA(反编译底座)
- 安装与验证见 [[re-ghidra]] / [[re-ida]]
### file / xxd / Python struct(字节级核对)
- 系统自带(`file`/`xxd`);Python 3 自带 `struct`
- 用途: 字节码产物判别(ocamlrun 脚本头)、closure 首字段/block 头字节验证(示例见 [[examples]])
## 操作步骤
按顺序执行;逆向着重**数据流**而非控制流(函数式产物控制流被打散,见坑 4)。每步产物存档(路径 + sha256,见 [[re-triage]])。
1. **运行时识别**:
```sh
readelf -s sample | grep -iE 'ghc|stg_|RTS|HsMain|_closure|_info' | head # GHC 特征
readelf -s sample | grep -iE 'caml_' | head # OCaml 特征
file sample # 字节码产物判别(脚本头)
```
- GHC:`main`(C RTS 入口)+ RTS 运行时符号(`stg_*`/`hs_*`)+ 业务符号 `Main_main_closure`/`Main_main_info`(`模块_名字_closure/info` 形态)
- OCaml 原生:`main` → `caml_main` → `caml_startup_common` → `caml_start_program` → `caml<模块>__entry`;`caml_startup`/`caml_startup_pooled` 是供 C 嵌入调用的等价入口(签名同为 void (char_os **argv),区别在 pooling 标志与异常行为),勿误当主链;`caml_*` 运行时符号(caml_alloc/caml_apply2/3 等)
- **字节码 vs 原生**:`caml_start_program` 仅存在于 native 运行库(4.14.2 libasmrun.a 实测),是 native 特征;字节码判据用 `caml_interprete`(仅 libcamlrun.a 有);字节码产物判别用 `file`(`ocamlrun script executable`)/`xxd` 头(`#!...ocamlrun\n` 脚本头 + `T`/`C` 魔数 + 分节)
- 入口链各版本一致:runtime/main.c 定义 `main` 并调用 `caml_main(argv)`(`caml_main` 定义于 startup_byt.c/startup_nat.c;原生链 `main → caml_main → caml_startup_common → caml_start_program`,4.14.2 实测地址见 [[examples]]);字节码运行库入口为 `caml_main → caml_startup_aux → caml_interprete`
- 判别速查:GHC = `stg_*` 机械符号群 + `模块_名_closure/info` 对;OCaml 原生 = `caml_*` 群 + `caml<模块>__<名>_<id>`;OCaml 字节码 = `#!ocamlrun` 脚本头
2. **闭包与堆对象**:
- GHC:thunk(未求值闭包)与已求值值的堆对象布局——closure 首字段即 info table 指针(实测字节验证见 [[examples]]);CAF 以 thunk 形式静态分配,首次引用才求值(惰性);`Main_main_closure` 是 CAF,其 info 指向 thunk 求值代码
- GHC 值形态:未求值(thunk,info 指向求值代码)vs 已求值 WHNF(info 指向构造器头/函数头)——同地址空间的两种状态,求值后 closure 内容被覆写
- OCaml:block 头(tag + 大小,64 位下 header = (size<<10)|(color<<8)|tag);tagged int 判定用值的最低位(bit 0,奇数=整数,偶数=指针/block)
- 分析:字段与构造器是主要线索(数据流优先)
3. **调用约定**:
- GHC:参数经栈传递;返回值在寄存器 R1-R3(盒值在 R1);entry 代码以 info table 为枢纽(`_info` 符号 = entry code)
- OCaml:参数经寄存器(前 N 个)传递,闭包调用经 `caml_applyN`;原生代码调用闭包 = 寄存器装载 + `caml_apply2/3` 或直接跳 entry
- 分析:先识别运行时包装(`caml_apply` / stg 入口)再进用户逻辑;尾调用优化使递归变跳转(无增长栈帧),按循环读
- GHC 与 OCaml 共点:函数不是"被 call",而是"跳到 entry"——反编译里的 `jmp` 目标地址即函数入口,别按 call/ret 配对思维读
4. **分析策略(数据流优先)**:
- 控制流打散:惰性求值导致求值顺序不可预测——静态控制流分析价值低
- 数据流线索:闭包字段初始化点(构造器参数)、模式匹配分支(构造器标签分发)、字符串/常量引用
- 产出:数据流图(构造器 → 字段 → 使用点)替代控制流图(与 [[re-analyze/analysis-contract]] 数据契约衔接)
- 模式匹配还原:分支按构造器 tag 分发(OCaml)或 info 表指针比较(GHC)——tag/指针值 → 构造器序号;还原出构造器集合即还原出数据类型
- 产出格式(供分析报告与下一环节消费):
```
构造器 C1 (tag 0, 2 字段) ← 分配点 A (caml_alloc2 / info 表)
字段0 ← 函数参数/常量(数据来源)
字段1 ← 字符串池引用
使用点: tag 比较 → 分支 B(业务逻辑)
```
5. **字节码产物(OCaml 特有)**:
```sh
head -c 64 sample | xxd # #!...ocamlrun 脚本头 + 魔数 T/C + 长度
ocamlobjinfo sample # 直接解析字节码可执行文件(导入单位/CRC)
```
- 字节码 exe = 脚本头 + 魔数 `T` + 代码区 + 各分节数据;分节名(CODE/PRIM/DATA/SYMB/CRCS 等 4 字符)与大端长度表在文件尾部 TOC,文件以 `Caml1999X031` 收尾(结构见 [[layout]],字节样例见 [[examples]])
- 字节码反汇编不是常规反编译(指令集为 OCaml bytecode 自定),分析入口用 ocamlobjinfo 的结构视图
## 跨域联合
- [[re-binary-core]] 网关:本技能归属(选择树「Haskell/OCaml 产物」分支)
- [[re-analyze/analysis-contract]]:数据流图按数据契约传递
- [[re-cpp-abi]]:vtable/info table 对照思路(表指针分派同构)
## 常见坑与陷阱
- **RTS 版本差异**:现象——closure 布局解读失败;原因——GHC/OCaml 版本演进;对策——按目标版本确认布局(本技能字段表基于 9.14/4.14 实测,见 [[layout]])
- **thunk 惰性求值误导**:现象——未求值闭包被当已求值数据;原因——惰性求值;对策——区分 thunk 头(info 指向求值代码)与已求值值(info 指向 WHNF 头);CAF 首引用前都是 thunk
- **OCaml 字节码非 native**:现象——反编译全是运行时包装;原因——字节码产物;对策——识别 ocamlrun 脚本头/字节码段特征后按字节码结构分析(非常规反编译;`caml_start_program` 仅 native 运行库有,是 native 特征;字节码判据用 `caml_interprete`)
- **控制流打散导致静态分析失效**:现象——函数体无连续逻辑;原因——函数式编译产物;对策——转数据流分析(步骤 4),不硬追控制流
- **tagged int 误读**:现象——整数被当指针/指针被当整数;原因——OCaml 值标记位;对策——按最低位区分(1=整数,0=指针),访问前先解标记(int >> 1 取真值)
- **GHC 模块名带 z 编码**:现象——符号 `GHCziInternalziTopHandler_runMainIO1_info` 难读;原因——`z`+小写转义特殊字符(GHC mangling:`zi`=`.` `zu`=下划线 `zz`=z `zc`=: `zh`=# 等);对策——按转义规则手工还原模块名(`GHCziInternal` → `GHC.Internal`),还原后与源码模块结构对应
- **info table 与 entry code 是同一指针的两个视图**:现象——info 指针处反汇编出的是字段表数据而非代码;原因——info table 指针指向 entry code,表字段在 entry code 之前;对策——反汇编从 info 指针处开始(即 entry),字段表按负偏移读
- **尾调用优化把递归变跳转**:现象——按 call 树分析递归逻辑断裂;原因——函数式编译器的尾调用优化(TCO);对策——`jmp` 回函数自身地址 = 递归,按循环语义读,别找增长栈帧
- **惰性求值顺序不可预测**:现象——按源码顺序单步动态分析对不上;原因——thunk 首次引用才求值,求值触发点在"需要值的地方"而非"产生值的地方";对策——动态分析聚焦数据依赖(哪个闭包被强制求值),静态聚焦字段初始化点,别假设执行顺序
- **跨运行时误判(GHC 机械符号当业务代码)**:现象——`stg_ap_*`/`stg_upd_frame_info` 等被当成业务逻辑分析;原因——STG 机械符号是求值机制;对策——先按 `stg_`/`hs_` 前缀把 RTS 机械符号排除,业务代码集中在 `模块_名_info` 与调用 `caml_applyN`/`caml_alloc*` 的片段

View File

@@ -0,0 +1,146 @@
# 函数式运行时最小解析示例
示例以本机 GHC 9.14.1(ghcup)与 OCaml 4.14.2(opam)编译的真实产物为标本,readelf/nm/xxd 输出与字节验证可逐字段对照。布局规则见 [[layout]]。
## 0. 标本构建
```haskell
-- hello.hs
main :: IO ()
main = putStrLn "hi from ghc"
```
```ocaml
(* hello.ml *)
let add x y = x + y
let () = print_endline (string_of_int (add 40 2))
```
```sh
ghc -O1 hello.hs -o hello_ghc
ocamlopt -o hello_ocaml hello.ml # 原生
ocamlc -o hello_bc hello.ml # 字节码
```
## 1. GHC 符号对照(readelf/nm 实测)
```
$ nm hello_ghc | grep -E 'Main_main|newCAF|hs_init'
0000000000407c12 T main ← C RTS 入口
00000000004da4f0 D Main_main_closure ← 顶层 main 的 closure(CAF,数据段)
0000000000407bc8 T Main_main_info ← entry code
00000000004da500 D Main_main4_closure
0000000000407be8 T Main_main4_info
0000000000494190 T newCAF ← CAF 分配(RTS)
000000000047f780 T hs_init_ghc ← RTS 初始化
$ nm hello_ghc | grep -c stg_ # RTS STG 机械符号(实测 334 个)
334
$ nm hello_ghc | grep -E 'stg_upd_frame_info|stg_enter_info'
000000000049bba0 T stg_upd_frame_info ← 更新帧(thunk 求值写回)
00000000004980e0 T stg_enter_info ← 进入求值
```
对照要点:`模块_名字_closure`(数据)+ `模块_名字_info`(代码)成对;`newCAF`/`hs_init_ghc` 是 RTS 锚点;`stg_*` 机械符号数量级(数百个)是 GHC 特征。
## 2. GHC closure 字节验证(首字段 = info 指针)
```python
import struct
# hello_ghc 非 PIE 时 vaddr == file offset;PIE 需先按 PT_LOAD 换算(见 re-format-elf 思路)
d = open('hello_ghc', 'rb').read()
# 用 readelf 的节映射换算 0x4da4f0 的文件偏移(此处示意:已换算)
off = 0x4da4f0 # Main_main_closure vaddr
v = struct.unpack_from('<Q', d, off)[0]
print(hex(v)) # 0x407bc8 == Main_main_info 地址
# 前 16 字节: info 指针 + 8 字节字段(CAF 载荷)
print(d[off:off+16].hex())
# c87b400000000000 0000000000000000
# └─ info 指针(0x407bc8)┘ └─ 载荷区(CAF 空载荷)┘
# 反汇编 entry code(0x407be8 = Main_main4_info):加载 closure 跳转 runMainIO
# leaq 0xd28da(%rip), %r14 ; 装载 Main_main1_closure(thunk)
# jmp GHCziInternalziTopHandler_runMainIO1_info ; 求值链(z 编码: GHC.Internal)
```
对照要点:closure 头部 8 字节 = info 指针(分派点);entry code 从 info 指针处开始;`GHCziInternal` = `GHC.Internal`(z 编码)。
## 3. OCaml 原生符号对照(nm 实测)
```
$ nm hello_ocaml | grep -E 'camlHello__|caml_apply|caml_start|caml_alloc'
0000000000022a00 T caml_apply3 ← 闭包调用分派(3 参数)
0000000000022a50 T caml_apply2 ← 闭包调用分派(2 参数)
0000000000022ac0 T camlHello__add_267 ← 模块级函数:caml<模块>__<名>_<id>
00000000000239d0 T camlCamlinternalAtomic__entry ← 各模块 entry
0000000000025ec0 T caml_startup_common ← 入口链:caml_main → caml_startup_common
0000000000026150 T caml_startup ← 等价 C API 入口(caml_startup(argv))
0000000000026180 T caml_main ← 入口链:main → caml_main(4.14.2 实测)
000000000004a990 T caml_start_program ← 仅 native 运行库(libasmrun.a),是 native 特征
000000000004df68 D camlHello ← 模块全局数据
$ nm hello_ocaml | grep -wE 'main|_start'
00000000000261f0 T main ← C 入口(call caml_main)
0000000000022540 T _start
```
对照要点:`caml_applyN` 是闭包部分应用分派(参数个数驱动);`caml_main` 存在于 4.14.2(0x26180,反汇编见 `main → call caml_main` → `caml_startup_common`),runtime/main.c 定义 `main` 并调用 `caml_main`(`caml_main` 定义于 startup_byt.c/startup_nat.c);模块 entry 符号 `caml<模块>__entry`。
## 4. OCaml 字节码产物判别(file + xxd 实测)
```
$ file hello_bc
hello_bc: a /home/user/.opam/default/bin/ocamlrun script executable (binary data)
↑ file 直接识别脚本头(示例路径为脱敏占位)
$ xxd -l 64 hello_bc
00000000: 2321 2f68 6f6d 652f 7573 6572 2f2e 6f70 #!/home/user/.op
00000010: 616d 2f64 6566 6175 6c74 2f62 696e 2f6f am/default/bin/o
00000020: 6361 6d6c 7275 6e0a 5400 0000 df02 0000 camlrun.T.......
00000030: 0000 0000 5700 0000 0100 0f00 1000 0000 ....W...........
└ 脚本头 "#!...ocamlrun\n"(脱敏占位路径)┘ └魔数 T┘ └代码区(分节表在尾部 TOC)┘
$ ocamlobjinfo hello_bc | head -8 # 结构视图(导入单位/CRC)
File hello_bc
Imported units:
-------------------------------- Stdlib__Weak
-------------------------------- Stdlib__Unit
...
```
对照要点:字节码 exe 头 = `#!<ocamlrun 路径>\n` + 魔数 `T` + 代码区;分节(CODE/PRIM/DATA/SYMB/CRCS 等)与长度表在文件尾部 TOC(大端,以 `Caml1999X031` 收尾);`ocamlobjinfo` 可直接解析字节码可执行文件。示例路径为脱敏占位(本机实路径含用户名)。
## 5. OCaml 对象文件(.cmx)结构(ocamlobjinfo 实测)
```
$ ocamlobjinfo hello.cmx
File hello.cmx
Name: Hello
CRC of implementation: 63b3383284e2773bc5d450ad0fa510d5
Globals defined: Hello
Interfaces imported: Stdlib / CamlinternalFormatBasics ...
Clambda approximation:
(0: function camlHello__add_267 arity 2 (closed) (inline) -> ...)
```
对照要点:`.cmx` 携带 CLambda 近似(函数名/元数/是否内联)——元数信息可直接用于调用点参数布局判断。
## 6. tagged int 与 block 头速查(反汇编判读用)
```
整数 42 → 内存值 0x55(42<<1|1),最低位=1
指向 block 的指针 → 最低位=0(8 字节对齐)
block 头(3 字段,tag 0) → (3<<10)|(0<<8)|0 = 0xC00
```
## 实现教训(内化)
- 一切从符号锚点出发:GHC `模块_名_closure/info` 对、OCaml `caml<模块>__<名>_<id>`
- closure 首字段 = info 指针是分派核心;模式匹配/闭包调用都经它跳转
- 字节码判别用 `file`/头字节;`caml_start_program` 仅 native 运行库有,是 native 特征,字节码判据用 `caml_interprete`
## 使用注意
- 本机 9.14.1 / 4.14.2 实测;用户产物以自身版本为准(入口符号/魔数随版本变,见 [[layout]] 版本差异)
- 与 [[re-analyze/analysis-contract]](数据流图传递)、[[re-triage]](初勘兜底)配合使用

View File

@@ -0,0 +1,106 @@
# 函数式运行时布局:GHC 堆对象 / OCaml 值表示 / 调用约定
Haskell(GHC)与 OCaml 的产物共性:堆对象头部携带"分派指针"(GHC info table / OCaml block 头),函数调用经闭包间接跳转。本文件字段基于 GHC 9.14.1 与 OCaml 4.14.2 实测,版本差异单独标注。
## GHC 堆对象(closure)
### closure 布局(实测)
```
closure:
+0 info table 指针(StgInfoTable 的 entry code 地址)← 分派点
+8 字段 0(构造器参数/捕获变量/求值结果)
+16 字段 1 ...
```
- **closure 首字段 = info 指针**(实测:`Main_main_closure` 内容 = `Main_main_info` 地址,字节验证见 [[examples]])
- 分派:调用/求值 = 读 info 指针 → 跳到 entry code;entry code 末尾通常 `jmp` 到后续处理(更新帧/返回)
### info table 与 entry code
- `_info` 符号 = entry code 地址;info table 字段(layout/type/srt)在 entry code 之前的负偏移区
- 符号形态:`<模块>_<名字>_closure`(数据)+ `<模块>_<名字>_info`(代码);模块名 z 编码(`zi`=`.` 等)
- 求值链:thunk 的 entry code 求值后写回 closure 并跳更新帧(`stg_upd_frame_info` 相关)——`stg_upd_frame_info`/`stg_enter_info` 出现处 = 惰性求值机制
### CAF(常量应用形式)
- 顶层常量/无参函数以 CAF 形式静态分配(.data 区),首次引用才求值(thunk)
- 定位:`newCAF` 调用点(RTS 符号);`Main_main_closure` 即典型 CAF
### GHC 调用约定
| 项 | 规则 |
|---|---|
| 参数 | 经栈传递(STG 栈,非硬件栈) |
| 返回值 | R1-R3 寄存器(盒值在 R1) |
| 分派 | 经 info 指针(闭包调用 = 读 info → 跳 entry) |
| 求值触发 | thunk entry → 求值 → 更新 closure(WHNF) |
## OCaml 值表示
### tagged int 与 block(64 位,原生代码)
| 值 | 表示 | 判定 |
|---|---|---|
| 整数 | `value = 真值 << 1 \| 1` | 最低位=1(奇数) |
| block 指针 | 指向 header 的 8 字节对齐指针 | 最低位=0(偶数) |
| 未装箱 double | 特殊 tag(250)block 或寄存器 | — |
### block 头(header,64 位)
```
header = (size << 10) | (color << 8) | tag
size: 字段数(字) color: GC 三色 tag: 构造器标签
```
- tag 语义:0-246 构造器标签(模式匹配按 tag 分发);247-255 特殊(250=double,248=string 等)
- 双字段加速:`caml_alloc2/alloc3` 一次分配多字段 block(`caml_alloc` 单字段通用)
### 闭包(函数值)
- 闭包 = 带 tag 的 block,字段 = 环境(捕获变量)+ 代码指针(首字段)
- 调用:装载参数到寄存器 → `caml_apply2`/`caml_apply3`(按参数个数分派)或直接跳闭包代码
- 运行时函数符号:`caml_apply1..N`(部分应用)、`caml_alloc*`(分配)、`caml_main`/`caml_startup_common`/`caml_startup`/`caml_start_program`(入口链)
### OCaml 入口链(原生,实测 4.14.2)
```
main (C 入口,runtime/main.c)
└─ caml_main(argv) ← runtime/main.c 定义 main 并调用 caml_main(caml_main 定义于 startup_byt.c/startup_nat.c)
└─ caml_startup_common
└─ caml_start_program ← 仅 native 运行库(4.14.2 libasmrun.a 实测);字节码入口 caml_main → caml_startup_aux → caml_interprete(无此符号)
└─ caml<模块>__entry(各模块初始化,含全局数据分配)
└─ 业务入口(camlHello__entry)
```
- 主链是 `caml_main`;`caml_startup`/`caml_startup_pooled` 是供 C 嵌入方调用的等价入口(签名同为 void (char_os **argv),区别在 pooling 标志与异常行为),勿混淆。4.14.2 实测地址:caml_main=0x26180、caml_startup_common=0x25ec0、caml_startup=0x26150、caml_start_program=0x4a990(反汇编见 [[examples]])
- 模块级函数命名:`caml<模块>__<名字>_<数字id>`(如 `camlHello__add_267`)
### OCaml 字节码产物结构
```
可执行文件:
#!<ocamlrun 路径>\n 脚本头(file 报 "ocamlrun script executable")
T 前导魔数(4.x 实测)
代码区(无长度字段,长度见尾部 TOC)
各分节数据 + 尾部 TOC: 分节名(4 字符)+大端长度逐条列出,末为分节数与 "Caml1999X031"
分节: CODE 代码 / PRIM 原语表 / DATA 数据 / SYMB 全局符号 / CRCS CRC 串 / DLLS 动态库
```
- 判别:`file sample` 输出 `ocamlrun script executable`;`head -c 32 | xxd` 见 `#!` 头
- 结构分析:`ocamlobjinfo sample`(.cmo/.cmx/字节码 exe 均可,输出导入单位/CRC/CLambda 近似)
## 版本差异要点
| 项 | 老版本 | 实测版本(本文档) |
|---|---|---|
| OCaml 入口 | `main → caml_main → caml_startup_common → caml_start_program`(各版本一致;runtime/main.c 定义 `main` 并调用 `caml_main`,`caml_main` 定义于 startup_byt.c/startup_nat.c) | 同左(4.14.2 实测,地址见 [[examples]]) |
| 字节码魔数 | 魔法串在文件尾 TOC(`Caml1999X`+版本号,exec.h 定义) | 前导 `T` + 尾部 `Caml1999X031`(4.14 实测) |
| GHC 版本 | RTS 符号命名随版本微调 | 9.14.1(stg_ap_*/stg_upd_frame_info 稳定) |
- 跨版本稳定的锚点:OCaml tagged int 最低位规则、block 头 tag 低字节、GHC closure 首字段 info 指针
## 使用注意
- 静态分析无需沙箱;跨平台(Linux/Windows/macOS)产物结构一致(C 运行时移植)
- 与 [[re-analyze/analysis-contract]](数据流图传递)、[[re-cpp-abi]](表指针分派对照)配合使用