9.7 KiB
9.7 KiB
name, type, description, capabilities
| name | type | description | capabilities | |
|---|---|---|---|---|
| re-fp-runtime | atomic | 函数式语言运行时逆向(Haskell/OCaml):闭包/堆对象模型、调用约定、数据流优先策略。 触发词:Haskell逆向、OCaml逆向、GHC RTS、thunk、STG、OCaml runtime、闭包、函数式产物。 |
|
函数式运行时逆向(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(反编译底座)
file / xxd / Python struct(字节级核对)
- 系统自带(
file/xxd);Python 3 自带struct - 用途: 字节码产物判别(ocamlrun 脚本头)、closure 首字段/block 头字节验证(示例见 examples)
操作步骤
按顺序执行;逆向着重数据流而非控制流(函数式产物控制流被打散,见坑 4)。每步产物存档(路径 + sha256,见 re-triage)。
-
运行时识别:
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脚本头
- GHC:
-
闭包与堆对象:
- 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)
- 分析:字段与构造器是主要线索(数据流优先)
- GHC:thunk(未求值闭包)与已求值值的堆对象布局——closure 首字段即 info table 指针(实测字节验证见 examples);CAF 以 thunk 形式静态分配,首次引用才求值(惰性);
-
调用约定:
- 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 配对思维读
- GHC:参数经栈传递;返回值在寄存器 R1-R3(盒值在 R1);entry 代码以 info table 为枢纽(
-
分析策略(数据流优先):
- 控制流打散:惰性求值导致求值顺序不可预测——静态控制流分析价值低
- 数据流线索:闭包字段初始化点(构造器参数)、模式匹配分支(构造器标签分发)、字符串/常量引用
- 产出:数据流图(构造器 → 字段 → 使用点)替代控制流图(与 re-analyze/analysis-contract 数据契约衔接)
- 模式匹配还原:分支按构造器 tag 分发(OCaml)或 info 表指针比较(GHC)——tag/指针值 → 构造器序号;还原出构造器集合即还原出数据类型
- 产出格式(供分析报告与下一环节消费):
构造器 C1 (tag 0, 2 字段) ← 分配点 A (caml_alloc2 / info 表) 字段0 ← 函数参数/常量(数据来源) 字段1 ← 字符串池引用 使用点: tag 比较 → 分支 B(业务逻辑)
-
字节码产物(OCaml 特有):
head -c 64 sample | xxd # #!...ocamlrun 脚本头 + 魔数 T/C + 长度 ocamlobjinfo sample # 直接解析字节码可执行文件(导入单位/CRC)
跨域联合
- 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=zzc=: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*的片段