SuperAPI AI Directory

re-fp-runtime

Security & auditing

View repository

Updated 10-15 21:34

About this skill

Functional runtime reverse engineering (Haskell / OCaml)

When to use / when not to use

  • Use: Haskell/OCaml artifacts (GHC RTS symbols, OCaml block header signatures), need to recover closures/heap objects, evaluation order, pattern-matching branches
  • Use: OCaml native/bytecode artifact discrimination and bytecode analysis (ocamlrun script header signature)
  • Don't use: imperative language artifacts (each goes to its own skill: C++ → [[re-cpp-abi]], Go → [[re-go]], Rust → [[re-rust]])
  • Don't use: when only function logic is needed and control flow is intact (functional artifacts have scattered control flow, direct decompilation yields low returns, see step 4)

Tool preparation

readelf / llvm-nm (symbol analysis)

  • For installation and verification see [[re-cpp-abi]] tool preparation

ghc toolchain (Haskell side, optional)

  • Linux/macOS: GHC installer (apt install ghc / brew install ghc / ghcup);Windows: ghcup(winget install ghcup or the official website installer); verify: ghc --version
  • Purpose: compile comparison artifacts with the same version, verify closure/info table shapes (varies greatly between GHC versions)

ocamlobjinfo / ocamlopt (OCaml side, optional)

  • Linux/macOS: OCaml toolchain (apt install ocaml / brew install ocaml);Windows: opam(winget install OCaml.opam) or the official website installer; verify: ocamlobjinfo handles any .cmx output CRC and import table
  • Purpose: bytecode artifact/object file structure analysis (ocamlobjinfo can read .cmo/.cmx/bytecode executables)

Ghidra / IDA (decompilation base)

  • For installation and verification see [[re-ghidra]] / [[re-ida]]

file / xxd / Python struct (byte-level verification)

  • Bundled with the system (file/xxd); Python 3 ships with struct
  • Purpose: bytecode artifact discrimination (ocamlrun script header), closure first field/block header byte verification (see [[examples]] for examples)

Procedure

Execute in order; reverse engineering focuses ondata flowrather than control flow (functional artifacts have scattered control flow, see pitfall 4). Archive each step's artifacts (path + sha256, see [[re-triage]]).

  1. Runtime identification:

    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 entry) + RTS runtime symbols (stg_*/hs_*) + business symbols Main_main_closure/Main_main_info(模块_名字_closure/info shape)
    • OCaml native:main → caml_main → caml_startup_common → caml_start_program → caml<模块>__entry;caml_startup/caml_startup_pooled is the equivalent entry for C embedding calls (signature is also void (char_os **argv), the difference lies in the pooling flag and exception behavior), do not mistake it for the main chain;caml_* runtime symbols (caml_alloc/caml_apply2/3 etc.)
    • Bytecode vs native:caml_start_program exists only in the native runtime library (measured in 4.14.2 libasmrun.a), is a native signature; bytecode criteria use caml_interprete(only libcamlrun.a has it); bytecode artifact discrimination uses file(ocamlrun script executable)/xxd header (#!...ocamlrun\n script header + T/C magic number + sectioning)
    • Entry chain is consistent across versions: runtime/main.c defines main and calls caml_main(argv)(caml_main defined in startup_byt.c/startup_nat.c; native chain main → caml_main → caml_startup_common → caml_start_program, see [[examples]] for measured addresses in 4.14.2); the bytecode runtime entry is caml_main → caml_startup_aux → caml_interprete
    • Discrimination quick reference: GHC = stg_* mechanical symbol group + 模块_名_closure/info Yes; OCaml native = caml_* group + caml<模块>__<名>_<id>; OCaml bytecode = #!ocamlrun script header
  2. Closures and heap objects:

    • GHC: heap object layout for thunks (unevaluated closures) and evaluated values—the first field of a closure is the info table pointer (for byte-level verification see [[examples]]); CAFs are statically allocated as thunks and are only evaluated on first reference (lazy);Main_main_closure is a CAF, and its info points to the thunk evaluation code
    • GHC value forms: unevaluated (thunk, info points to evaluation code) vs evaluated WHNF (info points to constructor header/function header)—two states in the same address space; after evaluation, the closure contents are overwritten
    • OCaml: block header (tag + size; on 64-bit, header = (size<<10)|(color<<8)|tag); tagged int determination uses the value's least significant bit (bit 0; odd = integer, even = pointer/block)
    • Analysis: fields and constructors are the main clues (data flow first)
  3. Calling convention:

    • GHC: arguments are passed via the stack; return values are in registers R1-R3 (boxed values in R1); entry code uses the info table as the hub (_info symbol = entry code)
    • OCaml: arguments are passed via registers (the first N), closure calls go through caml_applyN; native code calling a closure = loading registers + caml_apply2/3 or jumping directly to entry
    • Analysis: first identify the runtime wrappers (caml_apply / stg entry) before entering user logic; tail-call optimization turns recursion into jumps (no growing stack frames), so read it as loops
    • Common point between GHC and OCaml: functions are not "called", they are "jumped to entry"—the jmp target address in the decompilation is the function entry; do not read it with a call/ret pairing mindset
  4. Analysis strategy (data flow first):

    • Control flow is fragmented: lazy evaluation makes evaluation order unpredictable—static control flow analysis has low value
    • Data flow clues: closure field initialization points (constructor arguments), pattern-matching branches (constructor tag dispatch), string/constant references
    • Output: a data flow graph (constructor → field → use site) replaces the control flow graph (connecting with the [[re-analyze/analysis-contract]] data contract)
    • Pattern-matching recovery: branches dispatch by constructor tag (OCaml) or by info table pointer comparison (GHC)—tag/pointer value → constructor index; recovering the constructor set means recovering the data type
    • Output format (for consumption by the analysis report and the next stage):
      构造器 C1 (tag 0, 2 字段) ← 分配点 A (caml_alloc2 / info 表)
        字段0 ← 函数参数/常量(数据来源)
        字段1 ← 字符串池引用
      使用点: tag 比较 → 分支 B(业务逻辑)
      
  5. Bytecode artifacts (OCaml-specific):

    head -c 64 sample | xxd          # #!...ocamlrun 脚本头 + 魔数 T/C + 长度
    ocamlobjinfo sample              # 直接解析字节码可执行文件(导入单位/CRC)
    
    • Bytecode exe = script header + magic number T + code section + per-section data; the section names (CODE/PRIM/DATA/SYMB/CRCS, etc., 4 characters) and the big-endian length table are in the TOC at the end of the file, and the file ends with Caml1999X031 (for the structure see [[layout]], for byte examples see [[examples]])
    • Bytecode disassembly is not conventional decompilation (the instruction set is OCaml bytecode-specific); the analysis entry point uses the structural view of ocamlobjinfo

Cross-domain combinations

  • [[re-binary-core]] gateway: this skill belongs to it (select the tree's "Haskell/OCaml artifacts" branch)
  • [[re-analyze/analysis-contract]]: the data flow graph is passed according to the data contract
  • [[re-cpp-abi]]: vtable/info table comparison approach (table pointer dispatch is isomorphic)

Common pitfalls and traps

  • RTS version differences: Symptom — closure layout interpretation fails; Cause — GHC/OCaml version evolution; Countermeasure — confirm the layout according to the target version (this skill's field table is based on actual testing of 9.14/4.14, see [[layout]])
  • thunk lazy evaluation misleads: Symptom — an unevaluated closure is treated as evaluated data; Cause — lazy evaluation; Countermeasure — distinguish a thunk header (info points to evaluation code) from an evaluated value (info points to a WHNF header); CAFs are thunks until first referenced
  • OCaml bytecode is not native: Symptom — decompilation is all runtime wrappers; Cause — bytecode artifact; Countermeasure — after identifying the ocamlrun script header/bytecode section characteristics, analyze according to bytecode structure (not conventional decompilation;caml_start_program exists only in the native runtime library and is a native characteristic; the bytecode criterion uses caml_interprete)
  • control flow scattering causes static analysis to fail: Symptom — the function body has no continuous logic; Cause — functional compilation artifact; Countermeasure — switch to data flow analysis (step 4), do not stubbornly chase control flow
  • tagged int misread: Symptom — an integer is treated as a pointer/a pointer is treated as an integer; Cause — OCaml value tag bits; Countermeasure — distinguish by the lowest bit (1=integer, 0=pointer), decode the tag before access (int >> 1 to get the true value)
  • GHC module names carry z encoding: Symptom — symbol GHCziInternalziTopHandler_runMainIO1_info is hard to read; Cause —z+lowercase escapes special characters (GHC mangling:zi=. zu=underscore zz=z zc=: zh=# etc.); Countermeasure — manually restore the module name according to the escape rules (GHCziInternal → GHC.Internal), and after restoration it corresponds to the source module structure
  • info table and entry code are two views of the same pointer: Symptom — disassembling at the info pointer yields field table data rather than code; Cause — the info table pointer points to entry code, and the table fields are before the entry code; Countermeasure — disassemble starting from the info pointer (i.e., entry), and read the field table by negative offset
  • tail call optimization turns recursion into jumps: Symptom — analyzing recursive logic by call tree breaks down; Cause — tail call optimization (TCO) by functional compilers; Countermeasure —jmp returning to the function's own address = recursion, read it with loop semantics, do not look for growing stack frames
  • lazy evaluation order is unpredictable: Symptom — single-step dynamic analysis in source order does not match; Cause — a thunk is evaluated only on first reference, and the evaluation trigger point is "where a value is needed" rather than "where a value is produced"; Countermeasure — dynamic analysis focuses on data dependencies (which closure is forced to evaluate), static analysis focuses on field initialization points, do not assume execution order
  • cross-runtime misjudgment (GHC mechanical symbols treated as business code): Symptom —stg_ap_*/stg_upd_frame_info etc. are treated as business logic for analysis; Cause — STG mechanical symbols are the evaluation mechanism; Countermeasure — first use the stg_/hs_ prefix to exclude RTS mechanical symbols, business code is concentrated in 模块_名_info and fragments that call caml_applyN/caml_alloc* fragment of