Communitygithub.com

aicodeking/leanrea

Reverse engineer shipped software with plain local CLI tools, no MCP needed. Covers Electron/JS bundles, native Mach-O/ELF/PE, Go/Rust/.NET/Flutter/WASM binaries, APKs, SQLite, pcaps, firmware and websites. Use to explain how a feature works, extract versions/deps, compare builds, or rebuild a feature.

What is leanrea?

leanrea is a Claude Code agent skill that reverse engineer shipped software with plain local CLI tools, no MCP needed. Covers Electron/JS bundles, native Mach-O/ELF/PE, Go/Rust/.NET/Flutter/WASM binaries, APKs, SQLite, pcaps, firmware and websites. Use to explain how a feature works, extract versions/deps, compare builds, or rebuild a feature.

Works with~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/aicodeking/leanrea/tree/HEAD/leanrea

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

leanrea: fast path

You already know these tools. This file only lists the fastest command for each target, and the traps.

Rules

  • Work on a copy (cp -c on APFS is instant). Put each extraction in a new dir under $TMPDIR/re/<name>. Reuse an extraction if one exists.
  • Route by file -b T and the magic bytes first. Run the cheapest inventory next. Only then decompile or read code.
  • rg skips hidden and ignored files, and bundlers hide code (Electron: .webpack/, .vite/). Always search extracted trees with rg -uu.
  • Never cat a big or minified file. Slice it: rg -o '.{0,200}NEEDLE.{0,200}' F | head. Dump a disassembly once to a file, then grep that file.
  • Batch independent probes into ONE shell call with ; and echo '--- name' markers (quoted: zsh expands a bare ==x). Cap every output with head or -m.
  • Answer from evidence. In the answer, mark each claim as seen (cite file:offset, or a symbol/string) or inferred. A fact you did not find is unknown, not "absent". Static reading is not observed execution.
  • A missing tool is not a blocker. Use the fallback in the same row, or brew install <x> if the user allows it.
  • Turns cost more than bytes. Aim for 2–4 tool calls: one batched recon call, one or two targeted slices, then answer. Stop when every item you were asked has evidence. Do not re-verify what you already saw.

First move: one call each, paste and run

Native (Mach-O):

T=./bin; D=$TMPDIR/re/$(basename $T); mkdir -p $D; file $T; lipo -info $T; otool -L $T; echo '--- EXPORTS'; nm -gU $T | head -150; echo '--- IMPORTS'; nm -u $T | rg -v '^__Z|napi_' | tr '\n' ' '; echo; echo '--- STRINGS'; strings -a -n 6 $T | rg -v '^[_$.]' | head -400; otool -tV $T > $D/dis.txt; echo '--- FUNCS'; rg '^[_a-zA-Z].*:$' $D/dis.txt | rg -v '^_?_?Z?N?St3' | head -150

Then read one function: awk '/^FUNC_LABEL:$/{p=1;next} p&&/^[_a-zA-Z].*:$/{exit} p' $D/dis.txt | rg 'bl|b\.|cmp|literal|mov\s+w2'.

Electron:

A=./app.asar; X=$TMPDIR/re/asar; rm -rf $X; npx -y @electron/asar extract $A $X && jq '{name,version,main}' $X/package.json; find $X -path '*/node_modules' -prune -o -type f \( -name '*.js' -o -name '*.node' \) -print | head -60; ls ${A}.unpacked/node_modules 2>/dev/null; rg -uu -l --glob '!node_modules' 'exposeInMainWorld' $X

Then slice the needle across the bundle: rg -uu -o --glob '!node_modules' '.{0,250}NEEDLE.{0,250}' $X | head -20.

Go: go version -m T alone usually answers the question.

Router

Target (detect)Fastest path
Electron app (*.app/Contents/Resources/app.asar)npx -y @electron/asar extract app.asar X (also see app.asar.unpacked/ for .node addons). Entry point: jq .main X/package.json. Surface: `rg -n -o '.{0,120}(ipcMain.(handle
Web bundle / minified JSFind sourceMappingURL. If the map has sourcesContent, dump the sources: jq -r '.sources[]'. For webpack, grep the module ids, then slice around the hit. For pretty-print: npx -y prettier --parser babel F > F.pretty.js, then read with line ranges.
Hermes / bytenode / V8 snapshotHermes magic is c6 1f bc 03 (index.android.bundle): use hermes-dec / hbc-decompiler. .jsc (bytenode) cannot be decompiled: use strings and runtime hooks.
Mach-O (file: Mach-O)lipo -info (thin first: lipo -thin arm64 -o T1 T). Then: otool -L (libs); xcrun dyld_info -exports -imports; nm -gU (exports) / nm -u (imports = the capability map); strings -a -n 6; otool -l | rg -A3 'LC_(RPATH|BUILD_VERSION)'; codesign -d --entitlements :- T; otool -ov (ObjC classes/methods); xcrun swift-demangle --compact.
Native code logicotool -tV T > $D/dis.txt (or llvm-objdump -d --no-show-raw-insn -C). Find the function: rg -n '^_?.*Name.*:$'. Read the call/string trail: awk '/^FUNC:/{p=1} p&&/^[_a-zA-Z].*:$/&&!/FUNC/{exit} p' dis.txt | rg 'bl|call|literal|cmp'. otool labels string literals and stubs inline, which is often enough. Only for real pseudocode: Ghidra analyzeHeadless $D p -import T -postScript DecompileAll.java -deleteProject or r2 -qc 'aaa; pdc @ sym.X' T.
ELF / PEreadelf -hlSW, readelf -dW (NEEDED), nm -D --defined-only, objdump -d -M intel. PE: objdump -x, pefile/wrestool for resources. checksec: pwn checksec.
Go (__go_buildinfo, .go.buildinfo)go version -m T gives the toolchain, path, mod, deps and build flags in one call. Without Go: strings -a T | rg -m1 -A200 'Go buildinf' . Symbols survive stripping: go tool nm T | rg main\..
RustDeps (cargo-auditable): cargo audit bin T, or extract .dep-v0 / __DATA,.dep-v0 and zlib-inflate it. Compiler: strings -a T | rg -o '/rustc/[0-9a-f]{40}' | sort -u. Crates from paths: strings -a T | rg -o '[a-z0-9_-]+-[0-9]+\.[0-9]+\.[0-9]+/src' | sort -u.
.NET (file: Mono/.Net)ilspycmd -p -o X T.dll (install: dotnet tool install -g ilspycmd), then rg. P/Invoke: rg DllImport X. NativeAOT/IL2CPP are native: use the native rows.
Android APKjadx -d X --show-bad-code a.apk then rg. Resources/manifest only: apktool d --no-src -o X a.apk. Quick look: unzip -l a.apk. lib/*/libflutter.so means Flutter (Dart AOT: blutter); libhermes.so means React Native (Hermes row).
Flutter snapshot hashIn libapp.so, find magic f5 f5 dc dc; the 32 hex chars at +20 are the snapshot hash. Map it to a Dart version.
WASMwasm-objdump -x, wasm2wat, wasm-decompile (WABT).
SQLiteCopy the db, plus the -wal file if present. sqlite3 'file:C?mode=ro' '.schema', then SELECT * FROM t LIMIT 20.
plist / nib / carplutil -p F. Info.plist holds the bundle id, URL schemes and privacy strings. Asset catalogs: assetutil --info Assets.car.
pcap / HARtshark -r C -q -z conv,tcp; tshark -r C -T fields -e dns.qry.name -e tls.handshake.extensions_server_name -e http.request.full_uri | sort -u. HAR: jq -r '.log.entries[].request | .method+" "+.url'.
Firmwarebinwalk -e F or unblob -e X F. Then route each extracted binary through this table.
Websitecurl -sL URL for the HTML and script srcs, then fetch the JS and slice it. For runtime behavior, use a headless browser with a network log (Playwright page.on('request')) or Chrome --remote-debugging-port.
Running app / runtimemacOS: lldb -p PID, dtrace/fs_usage/log stream --predicate 'process=="X"'. Electron main: --inspect (only if the RunAsNode/inspect fuses allow it). Android: adb logcat, frida -U -f pkg -l hook.js.

Speed patterns

  • Strings and imports first. On a small binary, nm -u + strings + the export list usually answer "what does it do" with no disassembly.
  • To find who uses a string: get its address from otool -tV "literal pool for" comments, or rg -n 'literal pool for: "X'. Then find the enclosing function label above that line.
  • A C++ name in a bl target already tells you the protocol (for example getpeereid, write, std::hash). Read the call sequence, not every instruction.
  • For JS, go from the native/JS boundary inward: find the native call, then its caller class, then the IPC registration, then the renderer/preload.
  • Comparing versions: diff the inventories (nm, the module list, go version -m) before you diff any code.
  • Rebuilding: write the minimal reimplementation, then test it against the original (same inputs, same outputs).

Install (only on request)

brew install jadx apktool radare2 binwalk wabt wireshark ghidra · npm i -g @electron/asar · dotnet tool install -g ilspycmd · cargo install cargo-audit.

Related Skills