
Systematic reverse engineering of Cisco ASA's lina binary to discover and analyze memory corruption vulnerabilities, including CVE-2025-20333 and CVE-2025-20362, with a focus on finding remote code execution primitives.
linaCreated: 2026-08-12
Target: lab_asav9-12-4-67/lina (ASAv 9.12(4)67, ELF x86-64, PIE; Ghidra image base 0x100000; 112,687 functions, ~167 MB mapped, 642,707 symbols)
Track goal: systematic, complete RE coverage of the lina binary — complementing the hypothesis-driven targeted RE done so far — to hunt the primitive the lab has not found (wild unauth root RCE on 20362+20333 is proven, so it exists).
All prior RE was targeted (fault-PC decompiles, string-anchor xref walks, binding-range sweeps). It closed every audited scope (H4-1/2/3/4, tostring(userdata) reflection, SSH/SNMP/IKE %p, alt overflow families, survival oracle) — see ../RCE_BLOCKERS.md, . Those closures are per-scope; the of a 112k-function binary is exactly where the APT's missing primitive must live. This track replaces "guess the next surface" with .
../NEW_ROADS_TO_CODE_EXECUTION.mdPosition vs critical path: the known chain's hard gate remains P0 (live remote infoleak); the CIFS readdir over-read (Step 19i, 4-byte window) is the only empirically-confirmed leak and stays the foreground empirical track. LINA_FULL_RE is the systematic hedge: find a better leak/primitive in unaudited code, or prove none exists.
0x55.. (lina) or 0x7f.. (libc/heap) qword: format-string emitters (%p/%lx/%x on pointers), over-reads (length-not-bounded-by-initialized-data), reflection of heap residue, uninitialized-buffer echoes.memcpy/memmove/strcpy/strcat/sprintf/vsprintf/gets-class + custom copy loops), across all protocol surfaces (not just the WebVPN Lua bindings already exhausted — see ../LINA_ALT_OVERFLOW_FAMILIES_RE.md).csco_config) so triage hits can be ranked by actually reachable from the wire.(Deferred by user: CVE-2025-20363 sink hunt — OSINT says it consumes a leak, sibling of 20333; see ../CVE-2025-20363_OSINT.md.)
| Stage | What | Tooling | Output |
|---|---|---|---|
| 1. Corpus | Batch-decompile all 112,687 functions to disk, one .c per function, sharded by address | Ghidra run_script_inline (Java, DecompInterface), chunked + resumable | corpus/<shard>/<addr>.c |
| 2. Mechanical triage | ripgrep the corpus for sink patterns (leak + overflow classes below) | local scripts, triage/ | hit tables (CSV/MD) |
| 3. Reachability filter | keep hits in functions reachable from network entry points | Ghidra call-graph (get_function_call_graph/script) + entry-point seed list | ranked hit list |
| 4. Deep-dive | targeted RE of ranked hits (the existing ghidra-re subagent pattern) | ghidra-mcp | findings docs in this folder |
Stage 1 is pure compute (est. hours–days, unattended, resumable). Stages 2–3 are cheap/local. Stage 4 is the only human/LLM-heavy stage and is bounded by the funnel.
Research notes/LINA_FULL_RE/
README.md — this file (master plan + status)
TRIAGE.md — sink-pattern catalog + reachability plan (stage 2/3 spec)
scripts/ — decompile + triage + driver scripts
corpus/ — stage-1 output: decompiled functions, sharded (corpus/2c/02c6cc80.c)
_progress.txt — resumable progress marker (done count, last address)
_errors.txt — per-function decompile failures
triage/ — stage-2/3 outputs (hit tables, ranked lists)
findings/ — stage-4 deep-dive writeups (one MD per confirmed candidate)
| Stage | Status | Notes |
|---|---|---|
| 1. Corpus | DONE 2026-08-12 (~19:03 UTC-4) | 111,997/111,997 functions, 0 errors, 548 MB in corpus/; took 26,214 s (~7.3 h) at 4.3 fn/s via scripts/decompile_all.py (GhidraMCP batch_decompile, 20 fn/call cap) |
| 2. Triage | DONE 2026-08-13 | scripts/triage_stage2.py; per-class hit tables in triage/*_hits.csv; ranked join in triage/RANKED.md |
| 3. Reachability | DONE 2026-08-13 (re-run, fixed seeds) | corpus call+ref graph (427,198 edges); 26,291/111,997 reachable (23.5%) — preauth 26,278 / admin 13 / post20362 0. Seeds corrected: registrar 0317b4e0, session 0317c0a0 (old 030ab4e0/030ac0a0 were not function starts). Still a lower bound (vtables). |
| 4. Deep-dive | IN PROGRESS 2026-08-17 | L1 CLOSED. L2 C-path GO (static): WebVPN UCTE create_type3_message copies attacker Type-2 TargetInfo with no clamp; leak rides Type-3 Authorization: NTLM back to the attacker HTTP server (findings/L2_NTLM_C_PATH_TYPE3.md). Lua ntlm.parseType2 is the dormant twin. O1/O5 protocol pass CLOSED (static): IKE/SSH/SNMP/EWS/PDTS copies are dest-sized, send-side, or already-known (031af6d0 / 02ff2f30). O1 “other” 725 CLOSED (static): stack/global/param+const filter → no new dest-overflow GO; 015a8b30 is an unclamped addrtype helper whose callers pass AF size 4/0x10 only (findings/O1_OTHER_SWEEP.md). O2 CLOSED (static): 605 reachable copy-loops; no new dest-overflow GO. FUN_02fd7270 = ucte_ntlm_authenticate_connection → same create_type3_message as the L2 C-path GO (findings/O2_SWEEP.md). Next: lab NTLM probe (post-20362) — beats CIFS on window if a / qword shows up. Optional static: O3/O4. |
Resume/control: the driver skips functions whose output file already exists — re-run python3 scripts/decompile_all.py at any time to continue. Graceful stop: touch corpus/STOP. If Ghidra or the MCP plugin restarts, just re-run the driver.
Plugin constraints discovered (2026-08-12): run_script_inline is disabled (GHIDRA_MCP_ALLOW_SCRIPTS unset) → corpus uses the HTTP batch endpoints only; batch_decompile takes ?functions=0x..,0x..,.. as a GET query parameter (not JSON body) and silently truncates responses to 20 functions → driver chunk size = 20.
list_functions (verified: corpus/02/02c6cc80.c = luaH_get = RVA 0x2c6cc80). Note: the GhidraMCP HTTP plugin reports RVA-style addresses, not Ghidra VA = RVA + 0x100000.../RCE_BLOCKERS.md / ../TODO.md when a "new" candidate overlaps a closed route.0x55.. vs 0x7f.. vs heap), and whether it beats the CIFS readdir primitive (Step 19i) on reliability.0x7f..0x55..