SuperAPI AI Directory

re-nim

Security & auditing

View repository

Updated 10-16 05:06

About this skill

Nim reverse engineering

When to use / when not to use

  • Use: Nim artifacts (NimMain / GC symbols, NimString structural features), need to recover string logic, exception paths, GC/ownership relationships
  • Use: in mixed Nim/C artifacts, distinguish Nim-side code (after grouping by symbol origin, Nim-side goes through this skill path)
  • Do not use: pure C/C++ artifacts (go through the general [[re-cpp-abi]] / [[re-binary-core]] path)
  • Do not use: only need function logic (use decompilation skill directly)

Tool preparation

readelf / llvm-nm (symbol and section analysis)

  • Installation and verification see [[re-cpp-abi]] tool preparation
  • Nim artifacts are mainly ELF (Linux default C backend); on macOS use llvm-nm

Ghidra / IDA (decompilation base)

  • Installation and verification see [[re-ghidra]] / [[re-ida]]; Nim symbols (NimMain/NimStringV2 etc.) are directly readable after import

nim compiler (optional, for comparison compilation)

  • Linux: apt install nim / dnf install nim / pacman -S nim;macOS: brew install nim; Windows: choosenim/official installer
  • Verify: nim -v; purpose: compile a comparison artifact with the same version to verify string layout/GC symbol form (version differences see [[layout]])

xxd + Python struct (byte-level verification)

  • System built-in (xxd); Python 3 built-in struct
  • Purpose: when symbol/layout output is abnormal, parse NimString and object structures directly by offset (examples see [[examples]])

Operation steps

Execute in order, archive each step's output (path + sha256, see [[re-triage]]).

  1. Artifact identification:

    readelf -s sample | grep -iE 'NimMain|nimGC|NimString' | head   # ELF;Mach-O 用 llvm-nm
    readelf -s sample | grep -iE 'nimIncRef|nimDecRef|rawNewString|eqStrings' | head
    
    • Nim features:NimMain(entry chain), GC symbols (refc's nimGC_* / orc's nimIncRefCyclic etc.),NimStringV2/NimStringDesc struct types
    • Note: in release builds GC symbols are often inlined/eliminated (see pitfall 1), fall back on NimMain and string functions
    • Entry chain: C main → NimMain → NimMainInner → NimMainModule(module initialization) → business main
    • Module initialization order follows dependency topology (import relationships): initialization segments withinNimMainModule run dependent modules first — when breaking/drilling down in the initialization segment, compare against the import chain by call order
  2. String and sequence structures (determine GC mode first):

    • orc/arc(2.x default orc):NimStringV2 = {len: int, p: ptr NimStrPayload};NimStrPayload = {cap: int, data: 内联字符数组}— strings are "len + heap payload pointer", cap high bits carry literal marker bits (see [[layout]])
    • refc(old default):NimStringDesc = {len, reserved, data[]}— characters are inlined in the struct
    • Locate:rawNewString(this importc name is the same in both 1.x and 2.x) allocation call sites → struct layout → string operation functions (eqStrings etc.)
    • Analysis: string comparison points are key logic (validation/protocol/command dispatch) —eqStrings call sites are string equality checks
    • Sequences (seq) are isomorphic to string: v2 layout is also len + payload 指针(payload carries cap), refc is also len/reserved + 内联— handle by the same discrimination table
    • Memory view (v2 layout, 64-bit):
      栈/对象内:  len(int64) | p(ptr) ───────────────┐
      堆上 payload:  cap(int64, bit62=字面量标记) | data[cap+1] ←┘
      
      The string body only has a 16-byte handle; the content is in a separate heap block—when analyzing the object structure, jump by the handle; don't look for character data inside the object.
  3. Locating strings in memory (dynamic/dump scenarios):

    # 在内存转储中按 v2 布局找字符串:cap 是 8 字节对齐的容量值,data 后跟可打印 ASCII
    # 候选: 8 字节对齐的 cap + len 匹配的 data → 反推 NimStringV2 起点
    

    The string content is in the payload on the heap; on the stack there is only len+pointer; first locate the payload by the cap prefix signature, then trace back to find the NimStringV2 that references it ([[examples]] has a complete example).

  4. Exceptions and raises paths:

    • Nim 2.2.x (Linux x86-64 default) exceptions use a goto-style exception table,no setjmp symbol; the raise path goes through raiseExceptionEx/raiseExceptionAux
    • The old default (--exceptions:setjmp, Nim 2.0 and earlier) uses setjmp/longjmp:nimSetjmp symbol visible
    • Locate:raiseExceptionEx call site → exception object allocation and message → catch branch (exception-table driven)
    • Analysis: exception paths reveal input validation and failure handling, exposing boundary conditions earlier than normal paths
  5. GC and reference counting (branching by GC mode):

    • refc: reference counting + cycle collection—nimGC_*/nimGCunref/nimIncRef/nimDecRef call site
    • orc/arc: ARC semantics—nimIncRefCyclic/nimDecRefIsLastCyclicDyn etc.; few explicit inc/dec, release is inserted by the compiler
    • Analysis: GC call sites help identify object lifetime and ownership (together with the string structure); under release, after inlining, infer from allocation/free boundaries instead
  6. C mixed-compilation boundary:

    • {.compile:} / {.importc:} When mixed, Nim and C symbols coexist: Nim-side symbols carry a module prefix (hello__u8 form) and the runtime (NimMain/NimString); C-side symbols have no
    • The boundary is the logical entry point: Nim business logic is inside the NimMain call chain, and C library calls go through the import table
  7. Fallback for stripped artifacts:

    strings -n 6 sample | grep -E '@m.*\.nim\.c' | head      # C 生成缓存文件名(含模块名,release 也嵌入)
    strings -n 6 sample | grep -iE 'fatal\.nim|Exception' | head
    
    • NimMain etc., after being stripped, identify by signature strings (@m<模块>.nim.c, std runtime source file names) and runtime behavior ([[re-triage]] initial-triage fallback); business source file names are not embedded in the binary, so don't count on them

Cross-domain joint

  • [[re-binary-core]] gateway: this skill belongs here (selection tree "Nim artifacts" branch)
  • [[re-analyze/analysis-contract]]: symbol table passed according to the data contract
  • [[re-cpp-abi]]: reference for the C mixed side and RTTI-free discrimination
  • [[re-triage]]: initial-triage fallback (stripped artifacts identified by signature strings)

Common pitfalls and traps

  • GC version differences (refc/orc): symptom—reference counting calls cannot be found; cause—orc has no explicit inc/dec (ARC semantics) and under release the GC symbols are inlined; countermeasure—first confirm by Nim version and GC mode before analyzing; debug/refc builds have more complete symbols
  • NimString layout changes with GC/version: symptom—using the len/reserved/data handwritten parser reads 2.x artifacts all wrong; cause—2.x orc default is NimStringV2{len, p}(payload has cap),len/reserved/data inline is refc's NimStringDesc (1.x default); countermeasure—first confirm the artifact's GC mode (see the [[layout]] discrimination table) then choose the layout: for debug builds look at GC symbols (refc has nimGCunref, orc has nimIncRefCyclic), for release builds look at the allocation function naming and whether the cap field appears
  • Exported symbols get stripped: Symptom—no NimMain/NimString symbols; Cause—strip processing; Countermeasure—identify by characteristic strings/runtime behavior (step 6 fallback)
  • C mixed compilation: Symptom—mixed Nim/C symbols; Cause—{.compile:} mixed; Countermeasure—group by symbol origin, the Nim side goes through this skill path
  • Misjudgment at string comparison points: Symptom—key checks treated as ordinary comparisons; Cause—eqStrings wrapper; Countermeasure—trace Nim string function call sites to locate the comparison logic
  • Misjudgment of exception implementation generation: Symptom—looking for nimSetjmp the old way finds nothing; Cause—2.2+ Linux amd64 defaults to goto-style exceptions (no setjmp); Countermeasure—when nimSetjmp is absent, locate catch along raiseExceptionEx and the exception table, don't treat it as "no exception handling"
  • release inlining causes sparse symbols: Symptom—nimGC_*/eqStrings visible in debug disappear in release; Cause—-d:release inlining; Countermeasure—analyze release artifacts by behavioral characteristics (allocation/deallocation boundaries, string function call patterns); the symbol table is only a clue, not the basis
  • String content is on the heap, only a handle is on the stack: Symptom—searching for a string by content on the stack finds nothing; Cause—in the v2 layout the character data is in a separate payload heap block, and on the stack there are only len + p two fields; Countermeasure—in dynamic/dump scenarios, first locate the payload block by the cap prefix characteristic, then trace back to the handle referencing it ([[examples]] memory location example)
  • 32-bit artifact structure sizes are halved: Symptom—applying the 64-bit layout table to a 32-bit artifact makes all offsets wrong; Cause—NI is 4 bytes on 32-bit platforms (NimStringV2 is 8 bytes instead of 16, NimStrPayload cap is 4 bytes); Countermeasure—confirm the artifact's bitness before parsing, and choose structure sizes based on 4/8-byte NI
  • Non-PIE old build entry is a fixed address: Symptom—readelf -h 's e_entry is an absolute address (e.g. 0x401xxx), which matches directly in memory; Cause—non-PIE build (old default); Countermeasure—use symbols for entry chain location (the NimMain triplet) regardless of PIE, but distinguish by e_type when converting addresses