Skip to main content

rsleigh_cli/
cli.rs

1//! rsleigh CLI — decompile any binary to C-like pseudocode.
2//!
3//! Usage:
4//!   rsleigh <binary>                    # list functions
5//!   rsleigh <binary> <func>             # decompile one function
6//!   rsleigh <binary> --all              # decompile all functions
7//!   rsleigh <binary> --json             # list functions as JSON
8//!   rsleigh <binary> <func> --json      # decompile as JSON
9//!   rsleigh <binary> --disasm <func>    # disassemble (P-code)
10
11use crate::wasm;
12
13use std::path::Path;
14use std::sync::atomic::{AtomicBool, Ordering};
15
16static ANNOTATE_CRYPTO: AtomicBool = AtomicBool::new(false);
17
18fn maybe_annotate_crypto(s: String) -> String {
19    if ANNOTATE_CRYPTO.load(Ordering::Relaxed) {
20        rsleigh_decompile::crypto_constants::rewrite_text(&s)
21    } else {
22        s
23    }
24}
25
26/// Demangle a Swift symbol name to a human-readable form.
27/// Returns None if not a Swift symbol.
28fn demangle_swift_symbol(name: &str) -> Option<String> {
29    let s = name
30        .strip_prefix("$s")
31        .or_else(|| name.strip_prefix("$S"))?;
32
33    // Parse module name: <length><name>
34    let (module, rest) = parse_swift_id(s)?;
35
36    // Try class + method/property
37    if let Some((class_name, after_class)) = parse_swift_id(rest) {
38        if after_class.starts_with('C') {
39            let after_c = &after_class[1..];
40            if after_c.starts_with("ACycfC") || after_c.starts_with("ACycfc") {
41                return Some(format!("{}.init", class_name));
42            }
43            if after_c == "fd" || after_c == "fD" {
44                return Some(format!("{}.deinit", class_name));
45            }
46            if after_c == "Ma" {
47                return Some(format!("{}.__metadata", class_name));
48            }
49            if after_c == "MF" {
50                return Some(format!("{}.__fields", class_name));
51            }
52            if after_c == "Mm" || after_c == "Mf" || after_c == "N" {
53                return Some(format!("{}.__metadata", class_name));
54            }
55
56            if let Some((prop_name, after_prop)) = parse_swift_id(after_c) {
57                if after_prop.contains("vg") {
58                    return Some(format!("{}.{}.getter", class_name, prop_name));
59                }
60                if after_prop.contains("vs") {
61                    return Some(format!("{}.{}.setter", class_name, prop_name));
62                }
63                if after_prop.contains("vM") {
64                    return Some(format!("{}.{}.modify", class_name, prop_name));
65                }
66                if after_prop.contains("Wvd") {
67                    return Some(format!("{}.{}", class_name, prop_name));
68                }
69                return Some(format!("{}.{}", class_name, prop_name));
70            }
71            return Some(class_name.to_string());
72        }
73        // Free function
74        if after_class.ends_with('F') || after_class.contains("yS") {
75            return Some(class_name.to_string());
76        }
77    }
78
79    // stdlib ($ss prefix)
80    if module == "s" {
81        if let Some((entity, _)) = parse_swift_id(rest) {
82            return Some(format!("Swift.{}", entity));
83        }
84    }
85
86    None
87}
88
89fn parse_swift_id(s: &str) -> Option<(&str, &str)> {
90    let mut len_end = 0;
91    while len_end < s.len() && s.as_bytes()[len_end].is_ascii_digit() {
92        len_end += 1;
93    }
94    if len_end == 0 {
95        return None;
96    }
97    let len: usize = s[..len_end].parse().ok()?;
98    if len_end + len > s.len() {
99        return None;
100    }
101    Some((&s[len_end..len_end + len], &s[len_end + len..]))
102}
103
104pub fn entrypoint() {
105    let args: Vec<String> = std::env::args().collect();
106    if args.len() < 2 {
107        eprintln!("rsleigh — pure Rust decompiler");
108        eprintln!("Usage:");
109        eprintln!("  rsleigh <binary>                  List functions");
110        eprintln!("  rsleigh <binary> <func> [func2..] Decompile functions");
111        eprintln!("  rsleigh <binary> --all             Decompile all functions");
112        eprintln!("  rsleigh <binary> --json             List functions as JSON");
113        eprintln!("  rsleigh <binary> <func> --json     Decompile as JSON");
114        eprintln!("  rsleigh <binary> --disasm <func>   Disassemble with P-code");
115        eprintln!("  rsleigh <binary> --sigs <file.json> Load extra signatures");
116        eprintln!("  rsleigh <binary> --yara             Generate YARA detection rule");
117        eprintln!("  rsleigh <binary> --imphash          Compute imphash (Mandiant) for PE");
118        eprintln!("  rsleigh <binary> --hashes           Print sha256, md5, imphash, size");
119        eprintln!("  rsleigh old.bin --diff new.bin      Diff decompilation (show changes)");
120        eprintln!(
121            "  rsleigh <binary> --taint            Taint analysis (trace user input to sinks)"
122        );
123        eprintln!("  rsleigh <binary> --summary          AI summary (one-line per function)");
124        eprintln!("  rsleigh <binary> --xrefs <func>     Cross-references (callers + callees)");
125        eprintln!("  rsleigh <binary> --search <query>   Find functions by string/pattern");
126        eprintln!("  rsleigh <binary> --search --api <name>  Find functions calling API");
127        eprintln!("  rsleigh <binary> --search --const <hex> Find functions with constant");
128        eprintln!("  rsleigh <binary> --seh-fixpoint      Apply SEH-driven SMC patches until fixpoint, report new functions");
129        eprintln!("  rsleigh <binary> --vulnscan          Scan for vulnerability patterns");
130        eprintln!("  rsleigh <binary> --ioc [--json]      Extract IOCs (URLs, IPs, paths, registry keys)");
131        eprintln!("  rsleigh <binary> --xor-strings [--json]  Brute single-byte XOR string recovery");
132        eprintln!("  rsleigh <binary> --sigcheck [--json] Parse Authenticode signature (signer, timestamp, chain)");
133        eprintln!("  rsleigh <binary> --resources [--dump DIR] [--json]  Walk PE resource directory; --dump extracts blobs");
134        eprintln!(
135            "  rsleigh <binary> --all --compact     Token-efficient output (no decls/blanks)"
136        );
137        eprintln!("  rsleigh <binary> --all --brief       Calls + strings only (minimal tokens)");
138        eprintln!("  rsleigh <binary> --all --min-complexity 10  Skip trivial functions");
139        eprintln!("  rsleigh <binary> --callgraph         Export call graph as JSON");
140        eprintln!("  rsleigh <binary> --classes           Recover C++ classes from RTTI");
141        eprintln!(
142            "  rsleigh <binary> --raw <arch>       Load raw binary (mips32/arm32/x86-64/...)"
143        );
144        std::process::exit(1);
145    }
146
147    let binary_path = &args[1];
148    let json_mode = args.iter().any(|a| a == "--json");
149    let all_mode = args.iter().any(|a| a == "--all");
150    let disasm_mode = args.iter().any(|a| a == "--disasm");
151    let pcode_json_mode = args.iter().any(|a| a == "--pcode-json");
152    let ssa_json_mode = args.iter().any(|a| a == "--ssa-json");
153    let yara_mode = args.iter().any(|a| a == "--yara");
154    let imphash_mode = args.iter().any(|a| a == "--imphash");
155    let hashes_mode = args.iter().any(|a| a == "--hashes");
156    let summary_mode = args.iter().any(|a| a == "--summary");
157    let xrefs_mode = args.iter().any(|a| a == "--xrefs");
158    let search_mode = args.iter().any(|a| a == "--search");
159    let vulnscan_mode = args.iter().any(|a| a == "--vulnscan");
160    let ioc_mode = args.iter().any(|a| a == "--ioc");
161    let xor_strings_mode = args.iter().any(|a| a == "--xor-strings");
162    let sigcheck_mode = args.iter().any(|a| a == "--sigcheck");
163    let resources_mode = args.iter().any(|a| a == "--resources");
164    let classes_mode = args.iter().any(|a| a == "--classes");
165    let compact_mode = args.iter().any(|a| a == "--compact");
166    let brief_mode = args.iter().any(|a| a == "--brief");
167    let annotate_crypto_mode = args.iter().any(|a| a == "--annotate-crypto");
168    ANNOTATE_CRYPTO.store(annotate_crypto_mode, Ordering::Relaxed);
169    let min_complexity: usize = args
170        .iter()
171        .position(|a| a == "--min-complexity")
172        .and_then(|i| args.get(i + 1))
173        .and_then(|s| s.parse().ok())
174        .unwrap_or(0);
175    let callgraph_mode = args.iter().any(|a| a == "--callgraph");
176    let seh_fixpoint_mode = args.iter().any(|a| a == "--seh-fixpoint");
177    let sections_mode = args.iter().any(|a| a == "--sections");
178
179    // VM-helper flags. All take a comma-separated list of hex addresses
180    // (or a single address) and emit one line per handler.
181    let vm_classify_arg = args
182        .iter()
183        .position(|a| a == "--vm-classify-handlers")
184        .and_then(|i| args.get(i + 1))
185        .cloned();
186    let tag_dispatch_arg = args
187        .iter()
188        .position(|a| a == "--tag-dispatch")
189        .and_then(|i| args.get(i + 1))
190        .cloned();
191    let summarise_arg = args
192        .iter()
193        .position(|a| a == "--summarise-handlers")
194        .and_then(|i| args.get(i + 1))
195        .cloned();
196    let vm_dispatch_arg = args
197        .iter()
198        .position(|a| a == "--vm-dispatch")
199        .and_then(|i| args.get(i + 1))
200        .cloned();
201    let vm_bytecode_arg = args
202        .iter()
203        .position(|a| a == "--vm-bytecode")
204        .and_then(|i| args.get(i + 1))
205        .cloned();
206    let vm_handlers_arg = args
207        .iter()
208        .position(|a| a == "--vm-handlers")
209        .and_then(|i| args.get(i + 1))
210        .cloned();
211
212    if sections_mode {
213        let data = match std::fs::read(binary_path) {
214            Ok(d) => d,
215            Err(e) => {
216                eprintln!("Error: {}", e);
217                std::process::exit(1);
218            }
219        };
220        run_section_scan(binary_path, &data);
221        return;
222    }
223
224    // --vm-classify-handlers / --tag-dispatch / --summarise-handlers:
225    // VM-RE helper flags. Each takes a comma-separated list of hex
226    // addresses (or single address). Emit one line per handler and
227    // exit.
228    if vm_classify_arg.is_some()
229        || tag_dispatch_arg.is_some()
230        || summarise_arg.is_some()
231        || vm_dispatch_arg.is_some()
232        || vm_bytecode_arg.is_some()
233    {
234        let data = match std::fs::read(binary_path) {
235            Ok(d) => d,
236            Err(e) => {
237                eprintln!("Error: {}", e);
238                std::process::exit(1);
239            }
240        };
241        let obj = match goblin::Object::parse(&data) {
242            Ok(o) => o,
243            Err(e) => {
244                eprintln!("Error: cannot parse binary: {}", e);
245                std::process::exit(1);
246            }
247        };
248        let parse_addrs = |s: &str| -> Vec<u64> {
249            s.split(',')
250                .filter_map(|t| {
251                    let t = t.trim();
252                    let t = t.trim_start_matches("0x").trim_start_matches("0X");
253                    u64::from_str_radix(t, 16).ok()
254                })
255                .collect()
256        };
257        if let Some(arg) = vm_classify_arg.as_ref() {
258            let addrs = parse_addrs(arg);
259            let encs = rsleigh_decompile::vm_handler_classify::classify_all(&obj, &data, &addrs);
260            for line in rsleigh_decompile::vm_handler_classify::render(&encs) {
261                println!("{}", line);
262            }
263            return;
264        }
265        if let Some(arg) = tag_dispatch_arg.as_ref() {
266            let addrs = parse_addrs(arg);
267            for &a in &addrs {
268                let cases = rsleigh_decompile::tag_dispatch::scan_function(&obj, &data, a);
269                println!("=== {:#x} — {} cases ===", a, cases.len());
270                for line in rsleigh_decompile::tag_dispatch::render(&cases) {
271                    println!("  {}", line);
272                }
273            }
274            return;
275        }
276        if let Some(arg) = summarise_arg.as_ref() {
277            let addrs = parse_addrs(arg);
278            let summaries = rsleigh_decompile::handler_summary::summarise_all(&obj, &data, &addrs);
279            for line in rsleigh_decompile::handler_summary::render(&summaries) {
280                println!("{}", line);
281            }
282            return;
283        }
284        if let Some(arg) = vm_dispatch_arg.as_ref() {
285            let addrs = parse_addrs(arg);
286            for &a in &addrs {
287                if let Some(info) = rsleigh_decompile::vm_dispatch_extract::extract(&obj, &data, a)
288                {
289                    for line in rsleigh_decompile::vm_dispatch_extract::render(&info) {
290                        println!("{}", line);
291                    }
292                } else {
293                    println!("dispatcher @ {:#x}: extraction failed", a);
294                }
295            }
296            return;
297        }
298        if let Some(arg) = vm_bytecode_arg.as_ref() {
299            // Format: <bc_va>:<size> e.g. 0x180018000:0x400
300            let parts: Vec<&str> = arg.split(':').collect();
301            if parts.len() != 2 {
302                eprintln!("--vm-bytecode expects <bc_va>:<size> (hex), got {arg}");
303                std::process::exit(1);
304            }
305            let parse_hex = |s: &str| -> Option<u64> {
306                let s = s.trim().trim_start_matches("0x").trim_start_matches("0X");
307                u64::from_str_radix(s, 16).ok()
308            };
309            let bc_va = match parse_hex(parts[0]) {
310                Some(v) => v,
311                None => {
312                    eprintln!("--vm-bytecode: bad VA {}", parts[0]);
313                    std::process::exit(1);
314                }
315            };
316            let bc_size = match parse_hex(parts[1]) {
317                Some(v) => v as usize,
318                None => {
319                    eprintln!("--vm-bytecode: bad size {}", parts[1]);
320                    std::process::exit(1);
321                }
322            };
323            let handlers_path = match vm_handlers_arg.as_ref() {
324                Some(p) => p,
325                None => {
326                    eprintln!("--vm-bytecode requires --vm-handlers <path.json>");
327                    std::process::exit(1);
328                }
329            };
330            let json = match std::fs::read_to_string(handlers_path) {
331                Ok(s) => s,
332                Err(e) => {
333                    eprintln!("--vm-handlers: cannot read {handlers_path}: {e}");
334                    std::process::exit(1);
335                }
336            };
337            let vtable = match rsleigh_decompile::vm_bytecode_disasm::parse_handlers_json(&json) {
338                Ok(v) => v,
339                Err(e) => {
340                    eprintln!("--vm-handlers: parse error: {e}");
341                    std::process::exit(1);
342                }
343            };
344            // Resolve bc_va → file slice via PE sections.
345            let bytecode = if let goblin::Object::PE(pe) = &obj {
346                let mut found: Option<&[u8]> = None;
347                for sec in &pe.sections {
348                    let svaddr = pe.image_base as u64 + sec.virtual_address as u64;
349                    let sv = sec.virtual_size as u64;
350                    if bc_va >= svaddr && bc_va < svaddr + sv {
351                        let raddr = sec.pointer_to_raw_data as usize;
352                        let rsize = sec.size_of_raw_data as usize;
353                        let off_in_section = (bc_va - svaddr) as usize;
354                        if off_in_section >= rsize {
355                            // VA is in virtual_size but past size_of_raw_data —
356                            // BSS-style uninitialised tail, no file bytes.
357                            break;
358                        }
359                        let off = raddr + off_in_section;
360                        if off >= data.len() {
361                            break;
362                        }
363                        let avail = (rsize - off_in_section).min(data.len() - off);
364                        let end = off + bc_size.min(avail);
365                        found = Some(&data[off..end]);
366                        break;
367                    }
368                }
369                found
370            } else {
371                None
372            };
373            let bytecode = match bytecode {
374                Some(b) => b,
375                None => {
376                    eprintln!("--vm-bytecode: VA {:#x} not in any PE section", bc_va);
377                    std::process::exit(1);
378                }
379            };
380            let insts =
381                rsleigh_decompile::vm_bytecode_disasm::disassemble(bytecode, bc_va, &vtable);
382            for line in rsleigh_decompile::vm_bytecode_disasm::render(&insts) {
383                println!("{}", line);
384            }
385            return;
386        }
387    }
388
389    if seh_fixpoint_mode {
390        let data = match std::fs::read(binary_path) {
391            Ok(d) => d,
392            Err(e) => {
393                eprintln!("Error: {}", e);
394                std::process::exit(1);
395            }
396        };
397        // Full-discovery fixpoint: at each step, re-run the CLI's complete
398        // function-discovery pipeline against the mutated image.  This
399        // picks up not just SEH handlers and scope-table filters but also
400        // PyMethodDef registrations, RIP-relative function pointers in
401        // .rdata, and the prologue / CALL-descent passes that run in
402        // `discover_pe_functions`.
403        let result = rsleigh_decompile::seh_static::smc_fixpoint(&data, 16, |img| {
404            let Ok(obj) = goblin::Object::parse(img) else {
405                return vec![];
406            };
407            let Some((arch, segs, mut symbols)) = parse_binary(&obj, img) else {
408                return vec![];
409            };
410            if symbols.is_empty() {
411                if let goblin::Object::PE(pe) = &obj {
412                    let base = pe.image_base as u64;
413                    if let Some(optional) = pe.header.optional_header {
414                        let entry = base + optional.standard_fields.address_of_entry_point as u64;
415                        symbols = discover_pe_functions(entry, &segs, img, arch);
416                    }
417                }
418            }
419            if let goblin::Object::PE(pe) = &obj {
420                if pe.is_64 {
421                    for (addr, _) in scan_pymethoddef(&segs, img) {
422                        symbols.push((addr, String::new()));
423                    }
424                    let seh = rsleigh_decompile::seh_static::parse_pe64_seh(img);
425                    for a in rsleigh_decompile::seh_static::handler_addresses(&seh) {
426                        symbols.push((a, String::new()));
427                    }
428                    for a in rsleigh_decompile::seh_static::scope_table_addresses(img) {
429                        symbols.push((a, String::new()));
430                    }
431                }
432            }
433            let mut addrs: Vec<u64> = symbols.into_iter().map(|(a, _)| a).collect();
434            addrs.sort_unstable();
435            addrs.dedup();
436            addrs
437        });
438        println!(
439            "iterations: {}  converged: {}",
440            result.iterations, result.converged
441        );
442        println!("patches applied: {}", result.patches.len());
443        for p in &result.patches {
444            let preview: String = p
445                .bytes
446                .iter()
447                .take(16)
448                .map(|b| format!("{:02x}", b))
449                .collect::<Vec<_>>()
450                .join(" ");
451            let more = if p.bytes.len() > 16 { " .." } else { "" };
452            println!(
453                "  patch @ {:#x}  len={:4}  from handler {:#x}  [{}{}]",
454                p.target_va,
455                p.bytes.len(),
456                p.handler_va,
457                preview,
458                more
459            );
460        }
461        println!(
462            "newly discovered functions: {}",
463            result.newly_discovered_fns.len()
464        );
465        for va in &result.newly_discovered_fns {
466            println!("  {:#x}", va);
467        }
468        return;
469    }
470
471    if yara_mode {
472        let data = match std::fs::read(binary_path) {
473            Ok(d) => d,
474            Err(e) => {
475                eprintln!("Error: {}", e);
476                std::process::exit(1);
477            }
478        };
479        generate_yara_rule(binary_path, &data);
480        return;
481    }
482
483    if imphash_mode {
484        let data = match std::fs::read(binary_path) {
485            Ok(d) => d,
486            Err(e) => {
487                eprintln!("Error: {}", e);
488                std::process::exit(1);
489            }
490        };
491        match compute_imphash(&data) {
492            Some(h) => println!("{}", h),
493            None => {
494                eprintln!("imphash: not a PE binary with imports");
495                std::process::exit(1);
496            }
497        }
498        return;
499    }
500
501    if hashes_mode {
502        let data = match std::fs::read(binary_path) {
503            Ok(d) => d,
504            Err(e) => {
505                eprintln!("Error: {}", e);
506                std::process::exit(1);
507            }
508        };
509        let sha256 = compute_sha256(&data);
510        let md5 = compute_md5(&data);
511        let imphash = compute_imphash(&data);
512        println!("file:    {}", binary_path);
513        println!("size:    {}", data.len());
514        println!("md5:     {}", md5);
515        println!("sha256:  {}", sha256);
516        if let Some(h) = imphash {
517            println!("imphash: {}", h);
518        }
519        return;
520    }
521
522    // C++ class recovery
523    if classes_mode {
524        let data = match std::fs::read(binary_path) {
525            Ok(d) => d,
526            Err(e) => {
527                eprintln!("Error: {}", e);
528                std::process::exit(1);
529            }
530        };
531        // Try MSVC RTTI first, then GCC RTTI
532        let mut classes = rsleigh_decompile::cpp_class::recover_msvc_classes(&data);
533        if classes.is_empty() {
534            classes = rsleigh_decompile::cpp_class::recover_gcc_classes(&data);
535        }
536        if classes.is_empty() {
537            eprintln!("No C++ RTTI classes found (binary may not have RTTI, or is stripped)");
538        } else {
539            eprintln!("{} C++ classes recovered from RTTI", classes.len());
540            if json_mode {
541                println!("{}", serde_json::to_string_pretty(&classes).unwrap());
542            } else {
543                print!("{}", rsleigh_decompile::cpp_class::format_classes(&classes));
544            }
545        }
546        return;
547    }
548
549    // Summary/Xrefs/Search/Vulnscan/Callgraph modes
550    if summary_mode || xrefs_mode || search_mode || vulnscan_mode || callgraph_mode || ioc_mode || xor_strings_mode || sigcheck_mode || resources_mode {
551        let data = match std::fs::read(binary_path) {
552            Ok(d) => d,
553            Err(e) => {
554                eprintln!("Error: {}", e);
555                std::process::exit(1);
556            }
557        };
558        let bp = binary_path.clone();
559        let args_clone = args.clone();
560        let t = std::thread::Builder::new()
561            .stack_size(256 * 1024 * 1024)
562            .spawn(move || {
563                if summary_mode {
564                    run_summary(&bp, &data);
565                } else if xrefs_mode {
566                    let target = args_clone
567                        .iter()
568                        .position(|a| a == "--xrefs")
569                        .and_then(|i| args_clone.get(i + 1))
570                        .cloned()
571                        .unwrap_or_default();
572                    run_xrefs(&bp, &data, &target);
573                } else if vulnscan_mode {
574                    run_vulnscan(&bp, &data);
575                } else if ioc_mode {
576                    let json = args_clone.iter().any(|a| a == "--json");
577                    run_ioc(&bp, &data, json);
578                } else if xor_strings_mode {
579                    let json = args_clone.iter().any(|a| a == "--json");
580                    run_xor_strings(&bp, &data, json);
581                } else if sigcheck_mode {
582                    let json = args_clone.iter().any(|a| a == "--json");
583                    run_sigcheck(&bp, &data, json);
584                } else if resources_mode {
585                    let json = args_clone.iter().any(|a| a == "--json");
586                    let dump_dir = args_clone
587                        .iter()
588                        .position(|a| a == "--dump")
589                        .and_then(|i| args_clone.get(i + 1))
590                        .cloned();
591                    run_resources(&bp, &data, json, dump_dir.as_deref());
592                } else if callgraph_mode {
593                    run_callgraph(&bp, &data);
594                } else {
595                    // Search mode
596                    let search_idx = args_clone.iter().position(|a| a == "--search").unwrap();
597                    let api_mode = args_clone.iter().any(|a| a == "--api");
598                    let const_mode = args_clone.iter().any(|a| a == "--const");
599                    let tag_mode = args_clone.iter().any(|a| a == "--tag");
600                    let decompile_results = args_clone.iter().any(|a| a == "--decompile");
601                    let json_output = args_clone.iter().any(|a| a == "--json");
602                    let query = args_clone
603                        .iter()
604                        .skip(search_idx + 1)
605                        .find(|a| !a.starts_with("--"))
606                        .cloned()
607                        .unwrap_or_default();
608                    if query.is_empty() {
609                        eprintln!("Usage: rsleigh <binary> --search <query>");
610                        eprintln!("       rsleigh <binary> --search --api <func_name>");
611                        eprintln!("       rsleigh <binary> --search --const <hex_value>");
612                        eprintln!("       rsleigh <binary> --search --tag network,crypto");
613                        eprintln!("       rsleigh <binary> --search <query> --json");
614                        eprintln!("       rsleigh <binary> --search <query> --decompile");
615                        return;
616                    }
617                    run_search(
618                        &bp,
619                        &data,
620                        &query,
621                        api_mode,
622                        const_mode,
623                        tag_mode,
624                        decompile_results,
625                        json_output,
626                    );
627                }
628            })
629            .unwrap();
630        if let Err(e) = t.join() {
631            eprintln!("Panic: {:?}", e);
632        }
633        return;
634    }
635
636    // Diff mode: compare two binaries
637    if let Some(diff_idx) = args.iter().position(|a| a == "--diff") {
638        let new_path = args.get(diff_idx + 1).cloned().unwrap_or_else(|| {
639            eprintln!("Usage: rsleigh old.bin --diff new.bin [func_name]");
640            std::process::exit(1);
641        });
642        // Optional: specific function to diff
643        let func_filter: Vec<String> = args
644            .iter()
645            .enumerate()
646            .filter(|(i, a)| {
647                *i >= 2
648                    && !a.starts_with("--")
649                    && a.as_str() != new_path
650                    && a.as_str() != binary_path
651            })
652            .map(|(_, a)| a.clone())
653            .collect();
654        let old_path = binary_path.clone();
655        let t = std::thread::Builder::new()
656            .stack_size(256 * 1024 * 1024)
657            .spawn(move || diff_binaries(&old_path, &new_path, &func_filter))
658            .unwrap();
659        if let Err(e) = t.join() {
660            eprintln!("Panic: {:?}", e);
661        }
662        return;
663    }
664
665    // Load external signature database if --sigs provided
666    if let Some(pos) = args.iter().position(|a| a == "--sigs") {
667        if let Some(sigs_path) = args.get(pos + 1) {
668            match rsleigh_decompile::signatures::load_json_file(std::path::Path::new(sigs_path)) {
669                Ok(n) => eprintln!("Loaded {} signatures from {}", n, sigs_path),
670                Err(e) => eprintln!("Warning: {}", e),
671            }
672        }
673    }
674
675    let t = std::thread::Builder::new()
676        .stack_size(64 * 1024 * 1024)
677        .spawn({
678            let binary_path = binary_path.clone();
679            let args = args.clone();
680            move || run(&binary_path, &args, json_mode, all_mode, disasm_mode)
681        })
682        .unwrap();
683
684    match t.join() {
685        Ok(()) => {}
686        Err(_) => {
687            eprintln!("Error: stack overflow during decompilation");
688            std::process::exit(1);
689        }
690    }
691}
692
693/// Hidden GCC runtime symbols to exclude from listing.
694const HIDDEN: &[&str] = &[
695    "deregister_tm_clones",
696    "register_tm_clones",
697    "frame_dummy",
698    "__do_global_dtors_aux",
699    "__libc_csu_init",
700    "__libc_csu_fini",
701    "_dl_relocate_static_pie",
702    "__do_global_ctors_aux",
703];
704
705/// Compact pseudocode for token efficiency: strip declarations, blank lines, reduce indent.
706fn compact_output(output: &str) -> String {
707    output
708        .lines()
709        .filter(|l| {
710            let t = l.trim();
711            // Skip empty lines
712            if t.is_empty() {
713                return false;
714            }
715            // Skip variable declarations (type varN;)
716            if t.ends_with(';')
717                && !t.contains('=')
718                && !t.contains('(')
719                && (t.starts_with("int ")
720                    || t.starts_with("long ")
721                    || t.starts_with("uint")
722                    || t.starts_with("char ")
723                    || t.starts_with("float ")
724                    || t.starts_with("double ")
725                    || t.starts_with("bool "))
726            {
727                return false;
728            }
729            true
730        })
731        .map(|l| {
732            // Reduce indent: 4 spaces → 2 spaces
733            let indent = l.len() - l.trim_start().len();
734            let new_indent = indent / 2;
735            format!("{}{}", " ".repeat(new_indent), l.trim())
736        })
737        .collect::<Vec<_>>()
738        .join("\n")
739}
740
741/// Brief mode: show only calls, comparisons, strings, returns — skip assignments.
742fn brief_output(output: &str) -> String {
743    let mut result = Vec::new();
744    for line in output.lines() {
745        let t = line.trim();
746        // Keep function signature
747        if t.contains("func_") && t.contains('(') && t.ends_with('{') {
748            result.push(line.to_string());
749            continue;
750        }
751        // Keep closing brace
752        if t == "}" {
753            result.push(line.to_string());
754            continue;
755        }
756        // Keep calls (lines with function_name() pattern)
757        if t.contains('(')
758            && t.contains(')')
759            && !t.starts_with("//")
760            && (t.ends_with(';') || t.ends_with('{'))
761        {
762            // Skip pure assignments: var = expr; (no function call)
763            if t.contains(" = ") {
764                let rhs = &t[t.find(" = ").unwrap() + 3..];
765                if !rhs.contains('(') {
766                    continue;
767                } // pure assignment
768            }
769            result.push(line.to_string());
770            continue;
771        }
772        // Keep control flow
773        if t.starts_with("if (")
774            || t.starts_with("} else")
775            || t.starts_with("while (")
776            || t.starts_with("for (")
777            || t.starts_with("switch (")
778            || t.starts_with("return ")
779            || t.starts_with("break")
780            || t.starts_with("case ")
781        {
782            result.push(line.to_string());
783            continue;
784        }
785        // Keep string references
786        if t.contains('"') {
787            result.push(line.to_string());
788            continue;
789        }
790        // Keep comments (annotations, crypto, taint)
791        if t.starts_with("//")
792            && (t.contains("TAINT")
793                || t.contains("XOR")
794                || t.contains("stack string")
795                || t.contains("AES")
796                || t.contains("SHA"))
797        {
798            result.push(line.to_string());
799        }
800    }
801    result.join("\n")
802}
803
804fn run(binary_path: &str, args: &[String], json_mode: bool, all_mode: bool, disasm_mode: bool) {
805    let data = match std::fs::read(binary_path) {
806        Ok(d) => d,
807        Err(e) => {
808            eprintln!("Error: cannot read {}: {}", binary_path, e);
809            std::process::exit(1);
810        }
811    };
812
813    // WebAssembly detection: magic bytes \0asm
814    if data.len() >= 4 && &data[0..4] == b"\0asm" {
815        run_wasm(&data, args, all_mode);
816        return;
817    }
818
819    // Raw binary mode: --raw <arch> [--base <addr>]
820    let raw_arch_idx = args.iter().position(|a| a == "--raw");
821    if let Some(idx) = raw_arch_idx {
822        let arch_str = args.get(idx + 1).map(|s| s.as_str()).unwrap_or("mips32");
823        let base_idx = args.iter().position(|a| a == "--base");
824        let base = base_idx
825            .and_then(|i| args.get(i + 1))
826            .and_then(|s| {
827                if let Some(hex) = s.strip_prefix("0x") {
828                    u64::from_str_radix(hex, 16).ok()
829                } else {
830                    s.parse::<u64>().ok()
831                }
832            })
833            .unwrap_or(0);
834        let arch = match arch_str {
835            "x86-64" | "x86_64" | "x64" => rsleigh_api::Architecture::X86_64,
836            "x86-32" | "x86" | "i386" => rsleigh_api::Architecture::X86_32,
837            "arm32" | "arm" | "ARM32" => rsleigh_api::Architecture::ARM32,
838            "aarch64" | "arm64" | "AArch64" => rsleigh_api::Architecture::AArch64,
839            "mips32" | "mips" | "MIPS32" => rsleigh_api::Architecture::MIPS32,
840            "riscv64" | "riscv" | "RISCV64" => rsleigh_api::Architecture::RiscV64,
841            _ => {
842                eprintln!(
843                    "Unknown arch: {}. Use: x86-64, x86-32, arm32, aarch64, mips32, riscv64",
844                    arch_str
845                );
846                std::process::exit(1);
847            }
848        };
849        run_raw(&data, arch, base, args, all_mode);
850        return;
851    }
852
853    let path = Path::new(binary_path);
854    let obj = match goblin::Object::parse(&data) {
855        Ok(o) => o,
856        Err(e) => {
857            eprintln!("Error: cannot parse binary: {}", e);
858            std::process::exit(1);
859        }
860    };
861
862    // VM-packer family fingerprint. Currently detects PyVMProtect via PE
863    // section-table layout. Emits a one-shot advisory banner so the
864    // analyst knows what scheme they're up against before they sink hours
865    // into manual reversing.
866    if let Some(fp) = rsleigh_decompile::vm_fingerprint::detect(&obj) {
867        eprint!("{}", rsleigh_decompile::vm_fingerprint::banner(&fp));
868    }
869
870    // JMP <reg> tail-call trampolines: 1- or 2-byte gadgets every IAT
871    // call routes through. PyVMProtect uses one at `0x180040770` etc.
872    let trampolines = rsleigh_decompile::jmp_rax_trampoline::scan(&obj, &data);
873    if !trampolines.is_empty() {
874        eprintln!(
875            "// [trampoline] found {} `JMP <reg>` gadget(s):",
876            trampolines.len()
877        );
878        for t in trampolines.iter().take(8) {
879            eprintln!("//   - {:#x}: JMP {}", t.addr, t.reg);
880        }
881        if trampolines.len() > 8 {
882            eprintln!("//   ... and {} more", trampolines.len() - 8);
883        }
884    }
885
886    // XOR-encoded vtable dispatch — VM packers route every handler
887    // through a single CALL [trampoline] preceded by a key+vtable XOR
888    // chain. Detect those dispatchers given the trampoline gadgets we
889    // already found.
890    if !trampolines.is_empty() {
891        let tramp_vas: Vec<u64> = trampolines.iter().map(|t| t.addr).collect();
892        let iat_slots =
893            rsleigh_decompile::xor_vtable::iat_slots_for_trampolines(&obj, &data, &tramp_vas);
894        if !iat_slots.is_empty() {
895            let dispatchers = rsleigh_decompile::xor_vtable::scan(&obj, &data, &iat_slots);
896            if !dispatchers.is_empty() {
897                eprintln!(
898                    "// [xor-vtable] found {} XOR-encoded dispatcher site(s):",
899                    dispatchers.len()
900                );
901                for d in dispatchers.iter().take(8) {
902                    eprintln!(
903                        "//   - call@{:#x} → trampoline_slot={:#x}, key/table slots={:?}",
904                        d.call_site_va,
905                        d.trampoline_slot,
906                        d.data_slots
907                            .iter()
908                            .take(4)
909                            .map(|s| format!("{:#x}", s))
910                            .collect::<Vec<_>>(),
911                    );
912                }
913                if dispatchers.len() > 8 {
914                    eprintln!("//   ... and {} more", dispatchers.len() - 8);
915                }
916                eprintln!(
917                    "// hint: emulate init chain to extract runtime values \
918                     of the listed slots; XOR them to recover the cleartext \
919                     vtable base + handler key."
920                );
921            }
922        }
923    }
924
925    // Hash-resolved API resolver classifier — combines PEB walk with
926    // hash-multiply detection (ROR13 / DJB2 / FNV-1) to label the
927    // resolver function with its hash variant.
928    let resolvers = rsleigh_decompile::api_resolver::scan(&obj, &data);
929    if !resolvers.is_empty() {
930        eprintln!(
931            "// [api-resolver] found {} hash-resolved API resolver(s):",
932            resolvers.len()
933        );
934        for line in rsleigh_decompile::api_resolver::render(&resolvers)
935            .iter()
936            .take(8)
937        {
938            eprintln!("//   - {}", line);
939        }
940        if resolvers.len() > 8 {
941            eprintln!("//   ... and {} more", resolvers.len() - 8);
942        }
943    }
944
945    // PEB-walk anti-debug + API-resolver pattern.
946    let peb_hits = rsleigh_decompile::peb_walk_detect::scan(&obj, &data);
947    if !peb_hits.is_empty() {
948        eprintln!("// [peb-walk] found {} PEB-access site(s):", peb_hits.len());
949        for line in rsleigh_decompile::peb_walk_detect::render(&peb_hits)
950            .iter()
951            .take(8)
952        {
953            eprintln!("//   - {}", line);
954        }
955        if peb_hits.len() > 8 {
956            eprintln!("//   ... and {} more", peb_hits.len() - 8);
957        }
958    }
959
960    // Anti-debug timing probes: RDTSC/RDPMC/RDTSCP pairs within ~256B.
961    if let goblin::Object::PE(pe) = &obj {
962        const IMAGE_SCN_MEM_EXECUTE: u32 = 0x2000_0000;
963        let mut all_probes = Vec::new();
964        for sec in &pe.sections {
965            if sec.characteristics & IMAGE_SCN_MEM_EXECUTE == 0 {
966                continue;
967            }
968            let raddr = sec.pointer_to_raw_data as usize;
969            let rsize = sec.size_of_raw_data as usize;
970            if raddr + rsize > data.len() {
971                continue;
972            }
973            let base_va = pe.image_base as u64 + sec.virtual_address as u64;
974            let (_reads, probes) = rsleigh_decompile::antidebug_timing::scan_region(
975                &data[raddr..raddr + rsize],
976                base_va,
977            );
978            all_probes.extend(probes);
979        }
980        if !all_probes.is_empty() {
981            eprintln!(
982                "// [anti-debug] found {} timing-counter probe(s):",
983                all_probes.len()
984            );
985            for p in all_probes.iter().take(8) {
986                eprintln!(
987                    "//   - {}",
988                    rsleigh_decompile::antidebug_timing::render_probe(p)
989                );
990            }
991            if all_probes.len() > 8 {
992                eprintln!("//   ... and {} more", all_probes.len() - 8);
993            }
994        }
995    }
996
997    // SHA-256 implementation detection via H0/K constant density.
998    let sha_hits = rsleigh_decompile::sha256_func_detect::scan(&obj, &data);
999    if !sha_hits.is_empty() {
1000        eprintln!("// [sha256] found {} SHA-256 region(s):", sha_hits.len());
1001        for line in rsleigh_decompile::sha256_func_detect::render(&sha_hits)
1002            .iter()
1003            .take(8)
1004        {
1005            eprintln!("//   - {}", line);
1006        }
1007        if sha_hits.len() > 8 {
1008            eprintln!("//   ... and {} more", sha_hits.len() - 8);
1009        }
1010    }
1011
1012    let (arch, segs, mut symbols) = match parse_binary(&obj, &data) {
1013        Some(r) => r,
1014        None => {
1015            eprintln!("Error: unsupported binary format");
1016            std::process::exit(1);
1017        }
1018    };
1019
1020    // Apply FID databases (if --fid passed) to rename anonymous funcs.
1021    apply_fid_to_symbols(&data, arch, &segs, &mut symbols, args);
1022
1023    // Go `.gopclntab` name recovery. Stripped Go binaries carry full
1024    // runtime symbol info in this section; merge into symbols list so
1025    // anonymous func_* entries get their real names (main.main, etc.).
1026    {
1027        let go_syms = rsleigh_decompile::go_pclntab::parse(&data);
1028        if !go_syms.is_empty() {
1029            eprintln!("[go] .gopclntab: {} symbols", go_syms.len());
1030            let existing: std::collections::HashSet<u64> =
1031                symbols.iter().map(|(a, _)| *a).collect();
1032            let pclntab_set: std::collections::HashSet<u64> = go_syms.keys().copied().collect();
1033            for (pc, name) in &go_syms {
1034                if !existing.contains(pc) {
1035                    symbols.push((*pc, name.clone()));
1036                }
1037            }
1038            // Drop anonymous FUN_* / func_* entries that sit inside a
1039            // Go function's stack-check preamble. Go funcs begin with
1040            //   4 bytes: CMP RSP, [R14+0x10]
1041            //   6 bytes: JBE rel32 morestack
1042            // so real body starts at entry+10. The prior function-
1043            // discovery pass treats the body as a separate function via
1044            // CALL-target scan. Remove those spurious entries.
1045            symbols.retain(|(a, n)| {
1046                let is_anon =
1047                    n.starts_with("FUN_") || n.starts_with("func_") || n.starts_with("sub_");
1048                if !is_anon {
1049                    return true;
1050                }
1051                // Check any pclntab entry E where E + 1..=16 == a.
1052                let base = a.saturating_sub(16);
1053                !(base..*a).any(|candidate| pclntab_set.contains(&candidate))
1054            });
1055        }
1056    }
1057
1058    // For stripped PE binaries: discover functions from entry point + CALL targets
1059    if symbols.is_empty() {
1060        if let goblin::Object::PE(pe) = &obj {
1061            let base = pe.image_base as u64;
1062            let entry = base
1063                + pe.header
1064                    .optional_header
1065                    .unwrap()
1066                    .standard_fields
1067                    .address_of_entry_point as u64;
1068            symbols = discover_pe_functions(entry, &segs, &data, arch);
1069        }
1070    }
1071
1072    // Always run PyMethodDef scan for PE64 — even when the export table is
1073    // non-empty, Python C-extensions register most of their methods through
1074    // PyMethodDef arrays rather than direct exports.
1075    if let goblin::Object::PE(pe) = &obj {
1076        if pe.is_64 {
1077            let mut seen: std::collections::HashSet<u64> =
1078                symbols.iter().map(|(a, _)| *a).collect();
1079            for (addr, name) in scan_pymethoddef(&segs, &data) {
1080                if seen.insert(addr) {
1081                    symbols.push((addr, name));
1082                }
1083            }
1084            // PE64 SEH handlers live in .text but are never reached by CALL
1085            // descent, vtable scans, or prologue heuristics — they are only
1086            // visible to the OS exception dispatcher. Enumerate them from
1087            // UNWIND_INFO and register as functions.
1088            let seh = rsleigh_decompile::seh_static::parse_pe64_seh(&data);
1089            for addr in rsleigh_decompile::seh_static::handler_addresses(&seh) {
1090                if seen.insert(addr) {
1091                    symbols.push((addr, format!("seh_handler_{:x}", addr)));
1092                }
1093            }
1094            // Filter functions and __except resumption blocks from
1095            // SCOPE_TABLE — these are reached only by the exception
1096            // dispatcher, never by CALL descent.
1097            for addr in rsleigh_decompile::seh_static::scope_table_addresses(&data) {
1098                if seen.insert(addr) {
1099                    symbols.push((addr, format!("seh_scope_{:x}", addr)));
1100                }
1101            }
1102        }
1103    }
1104
1105    // For stripped ELF binaries: discover functions via entry point, CALL scanning, prologues
1106    // Also trigger for ELF with only import symbols (dynsym but no symtab)
1107    let is_elf_stripped = if let goblin::Object::Elf(elf) = &obj {
1108        elf.syms.len() == 0 || symbols.iter().all(|(_, n)| n.starts_with("FUN_"))
1109    } else {
1110        false
1111    };
1112    if is_elf_stripped || (symbols.is_empty() && matches!(&obj, goblin::Object::Elf(_))) {
1113        if let goblin::Object::Elf(elf) = &obj {
1114            let discovered = discover_elf_functions(elf, &segs, &data, arch);
1115            // Merge: keep existing named symbols, add discovered ones
1116            let existing: std::collections::BTreeSet<u64> =
1117                symbols.iter().map(|(a, _)| *a).collect();
1118            for (addr, name) in discovered {
1119                if !existing.contains(&addr) {
1120                    symbols.push((addr, name));
1121                }
1122            }
1123        }
1124    }
1125
1126    // Scratch-buffer leak detector — alloc + write + return-Py_None
1127    // pattern (PyVMProtect v5 anti-emu trick).
1128    if let goblin::Object::PE(pe) = &obj {
1129        if pe.is_64 {
1130            let iat = rsleigh_decompile::handler_summary::build_iat_map(&obj, &data);
1131            if !iat.is_empty() {
1132                // Sweep CALL rel32 targets in executable sections — symbol
1133                // discovery on PyVMProtect-style binaries is sparse, so the
1134                // raw call-graph is the better candidate set.
1135                let mut call_targets: std::collections::HashSet<u64> =
1136                    symbols.iter().map(|(a, _)| *a).collect();
1137                const IMAGE_SCN_MEM_EXECUTE: u32 = 0x2000_0000;
1138                for sec in &pe.sections {
1139                    if sec.characteristics & IMAGE_SCN_MEM_EXECUTE == 0 {
1140                        continue;
1141                    }
1142                    let raddr = sec.pointer_to_raw_data as usize;
1143                    let rsize = sec.size_of_raw_data as usize;
1144                    if raddr + rsize > data.len() {
1145                        continue;
1146                    }
1147                    let base_va = pe.image_base as u64 + sec.virtual_address as u64;
1148                    let body = &data[raddr..raddr + rsize];
1149                    let mut k = 0;
1150                    while k + 5 <= body.len() {
1151                        if body[k] == 0xe8 {
1152                            let d32 = i32::from_le_bytes([
1153                                body[k + 1],
1154                                body[k + 2],
1155                                body[k + 3],
1156                                body[k + 4],
1157                            ]);
1158                            let next_rip = base_va.wrapping_add((k + 5) as u64);
1159                            let target = next_rip.wrapping_add(d32 as i64 as u64);
1160                            call_targets.insert(target);
1161                        }
1162                        k += 1;
1163                    }
1164                    // Prologue sweep — PyVMProtect indirect-only callees
1165                    // (e.g. const-pool resolver) are missed by call-graph
1166                    // alone. Catch common 64-bit prologues at 16B align.
1167                    let mut k = 0;
1168                    while k + 8 <= body.len() {
1169                        let is_prologue = (body[k] == 0x48
1170                            && body[k + 1] == 0x89
1171                            && body[k + 2] == 0x5c
1172                            && body[k + 3] == 0x24)
1173                            || (body[k] == 0x48 && body[k + 1] == 0x83 && body[k + 2] == 0xec)
1174                            || (body[k] == 0x48 && body[k + 1] == 0x81 && body[k + 2] == 0xec)
1175                            || (body[k] == 0x40 && body[k + 1] == 0x53)
1176                            || (body[k] == 0x40 && body[k + 1] == 0x55)
1177                            || (body[k] == 0x40 && body[k + 1] == 0x57);
1178                        if is_prologue {
1179                            call_targets.insert(base_va + k as u64);
1180                        }
1181                        k += 16;
1182                    }
1183                }
1184                let func_vas: Vec<u64> = call_targets.into_iter().collect();
1185                let leaks = rsleigh_decompile::scratch_leak::scan_functions(
1186                    &obj, &data, &iat, &func_vas, 0x800,
1187                );
1188                if !leaks.is_empty() {
1189                    eprintln!(
1190                        "// [scratch-leak] found {} alloc+return-None pattern(s):",
1191                        leaks.len()
1192                    );
1193                    for line in rsleigh_decompile::scratch_leak::render(&leaks)
1194                        .iter()
1195                        .take(16)
1196                    {
1197                        eprintln!("//   - {}", line);
1198                    }
1199                    if leaks.len() > 16 {
1200                        eprintln!("//   ... and {} more", leaks.len() - 16);
1201                    }
1202                }
1203            }
1204        }
1205    }
1206
1207    // Determine which functions to process
1208    // Skip --flag arguments and their values (e.g., --sigs path.json, --fid file.fidb)
1209    let value_flag_positions: std::collections::HashSet<usize> = args
1210        .iter()
1211        .enumerate()
1212        .filter_map(|(i, a)| {
1213            if a == "--sigs" || a == "--fid" {
1214                Some(i + 1)
1215            } else {
1216                None
1217            }
1218        })
1219        .collect();
1220    let func_args: Vec<&str> = args[2..]
1221        .iter()
1222        .enumerate()
1223        .filter(|(i, a)| {
1224            if a.starts_with("--") {
1225                return false;
1226            }
1227            // Index in the full args array is i + 2.
1228            if value_flag_positions.contains(&(*i + 2)) {
1229                return false;
1230            }
1231            true
1232        })
1233        .map(|(_, a)| a.as_str())
1234        .collect();
1235
1236    if func_args.is_empty() && !all_mode && !disasm_mode {
1237        // List functions. Hide CRT-internal / runtime glue whose names start
1238        // with a single `_` (`_init`, `_fini`, `_start`, `_dl_*`, etc.) but
1239        // KEEP demangled-candidate symbols starting with `_Z` / `__Z` (C++
1240        // Itanium mangling) / `_GLOBAL_` (GCC static init) since those are
1241        // the real program surface area.
1242        let funcs: Vec<(&str, u64)> = symbols
1243            .iter()
1244            .filter(|(_, n)| {
1245                if n.is_empty() {
1246                    return false;
1247                }
1248                if n.starts_with("dyld") {
1249                    return false;
1250                }
1251                if HIDDEN.contains(&n.as_str()) {
1252                    return false;
1253                }
1254                // Allow C++ / Itanium / Swift / static-init names.
1255                if n.starts_with("_Z")
1256                    || n.starts_with("__Z")
1257                    || n.starts_with("_GLOBAL_")
1258                    || n.starts_with("$s")
1259                    || n.starts_with("_$s")
1260                {
1261                    return true;
1262                }
1263                // Hide well-known CRT glue by prefix. Python-visible method
1264                // names (e.g. `_ttokwy5gsm`, `__name__`) start with `_` too,
1265                // so a blanket underscore filter is wrong.
1266                if n.starts_with("_dl_")
1267                    || n.starts_with("__do_global")
1268                    || n.starts_with("__libc_")
1269                    || n.starts_with("__pthread_")
1270                    || n.starts_with("_GLOBAL__sub_I_")
1271                    || matches!(
1272                        n.as_str(),
1273                        "_init" | "_fini" | "_start" | "_DYNAMIC" | "_GLOBAL_OFFSET_TABLE_"
1274                    )
1275                {
1276                    return false;
1277                }
1278                true
1279            })
1280            .map(|(a, n)| (n.as_str(), *a))
1281            .collect();
1282
1283        if json_mode {
1284            // Rich JSON: decompile each function and extract metadata
1285            let path = std::path::Path::new(binary_path);
1286            let mut dec = rsleigh_api::Decoder::new(arch);
1287            let entries: Vec<serde_json::Value> = funcs
1288                .iter()
1289                .map(|(name, addr)| {
1290                    let insts = decode_func(*addr, &symbols, &segs, &data, &mut dec);
1291                    if insts.is_empty() {
1292                        return serde_json::json!({
1293                            "name": name, "address": format!("0x{:x}", addr),
1294                            "size": 0, "calls": [], "strings": [], "return_type": "void"
1295                        });
1296                    }
1297                    let output = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1298                        rsleigh_decompile::decompile_with_binary(
1299                            arch,
1300                            &insts,
1301                            Some(&data),
1302                            Some(path),
1303                        )
1304                    }))
1305                    .map(maybe_annotate_crypto)
1306                    .unwrap_or_default();
1307
1308                    // Extract metadata from decompiled output
1309                    let mut calls = Vec::new();
1310                    let mut strings = Vec::new();
1311                    let mut line_count = 0;
1312                    for line in output.lines() {
1313                        let t = line.trim();
1314                        if t.is_empty() || t.starts_with("//") {
1315                            continue;
1316                        }
1317                        line_count += 1;
1318                        // Extract calls
1319                        if t.contains('(') {
1320                            let check = if let Some(eq) = t.find(" = ") {
1321                                &t[eq + 3..]
1322                            } else {
1323                                t
1324                            };
1325                            if let Some(p) = check.find('(') {
1326                                let callee = check[..p].trim().trim_start_matches("return ");
1327                                if !callee.is_empty()
1328                                    && !callee.contains(' ')
1329                                    && !callee.starts_with('*')
1330                                    && !callee.starts_with('(')
1331                                    && !callee.starts_with("if")
1332                                    && !callee.starts_with("while")
1333                                    && !callee.starts_with("switch")
1334                                    && callee.len() < 50
1335                                    && !calls.contains(&callee.to_string())
1336                                {
1337                                    calls.push(callee.to_string());
1338                                }
1339                            }
1340                        }
1341                        // Extract strings
1342                        if let Some(q1) = t.find('"') {
1343                            if let Some(q2) = t[q1 + 1..].find('"') {
1344                                let s = &t[q1 + 1..q1 + 1 + q2];
1345                                if s.len() >= 2
1346                                    && s.len() <= 80
1347                                    && !strings.contains(&s.to_string())
1348                                {
1349                                    strings.push(s.to_string());
1350                                }
1351                            }
1352                        }
1353                    }
1354                    // Extract return type from first line
1355                    let return_type = output
1356                        .lines()
1357                        .next()
1358                        .and_then(|l| l.split_whitespace().next())
1359                        .unwrap_or("void");
1360                    // Extract param count from signature
1361                    let params = output
1362                        .lines()
1363                        .next()
1364                        .map(|l| l.matches("param_").count())
1365                        .unwrap_or(0);
1366                    let size = insts
1367                        .last()
1368                        .map(|(a, i)| (*a + i.len - addr) as u64)
1369                        .unwrap_or(0);
1370
1371                    serde_json::json!({
1372                        "name": name,
1373                        "address": format!("0x{:x}", addr),
1374                        "size": size,
1375                        "params": params,
1376                        "return_type": return_type,
1377                        "calls": calls,
1378                        "strings": strings,
1379                        "complexity": line_count,
1380                        "pseudocode": output,
1381                    })
1382                })
1383                .collect();
1384            println!(
1385                "{}",
1386                serde_json::to_string_pretty(&serde_json::json!({
1387                    "binary": binary_path,
1388                    "arch": format!("{:?}", arch),
1389                    "function_count": entries.len(),
1390                    "functions": entries,
1391                }))
1392                .unwrap()
1393            );
1394        } else {
1395            eprintln!("Architecture: {:?}", arch);
1396            eprintln!("{} functions:", funcs.len());
1397            for (name, addr) in &funcs {
1398                println!("  0x{:08x}  {}", addr, name);
1399            }
1400        }
1401        return;
1402    }
1403
1404    // Determine target functions
1405    let targets: Vec<String> = if all_mode {
1406        symbols
1407            .iter()
1408            .filter(|(_, n)| {
1409                !n.starts_with('_')
1410                    && !n.starts_with("dyld")
1411                    && !HIDDEN.contains(&n.as_str())
1412                    && !n.is_empty()
1413            })
1414            .map(|(_, n)| n.clone())
1415            .collect()
1416    } else {
1417        func_args.iter().map(|s| s.to_string()).collect()
1418    };
1419
1420    let mut dec = rsleigh_api::Decoder::new(arch);
1421
1422    if disasm_mode {
1423        // Disassembly mode
1424        for name in &targets {
1425            let func_addr =
1426                if let Some(hex) = name.strip_prefix("0x").or_else(|| name.strip_prefix("0X")) {
1427                    u64::from_str_radix(hex, 16).ok()
1428                } else {
1429                    symbols.iter().find(|(_, n)| n == name).map(|(a, _)| *a)
1430                };
1431            if let Some(func_addr) = func_addr {
1432                let insts = decode_func(func_addr, &symbols, &segs, &data, &mut dec);
1433                if json_mode {
1434                    let entries: Vec<serde_json::Value> = insts
1435                        .iter()
1436                        .map(|(a, inst)| {
1437                            serde_json::json!({
1438                                "address": format!("0x{:x}", a),
1439                                "disassembly": inst.disassembly,
1440                                "length": inst.len,
1441                                "pcode_ops": inst.ops.len(),
1442                            })
1443                        })
1444                        .collect();
1445                    println!(
1446                        "{}",
1447                        serde_json::to_string_pretty(&serde_json::json!({
1448                            "function": name, "instructions": entries
1449                        }))
1450                        .unwrap()
1451                    );
1452                } else {
1453                    println!("=== {} (0x{:x}) ===", name, func_addr);
1454                    for (a, inst) in &insts {
1455                        println!("  0x{:08x}  {}", a, inst.disassembly);
1456                    }
1457                }
1458            } else {
1459                eprintln!("Function '{}' not found", name);
1460            }
1461        }
1462        return;
1463    }
1464
1465    // --pcode-json and --ssa-json: dump intermediate state for one or
1466    // more functions. Useful for bench debugging — see exactly what
1467    // P-code the lifter produced and what SSA fold did with it.
1468    let pcode_json = args.iter().any(|a| a == "--pcode-json");
1469    let ssa_json = args.iter().any(|a| a == "--ssa-json");
1470    let opaque_scan = args.iter().any(|a| a == "--opaque-scan");
1471    if pcode_json || ssa_json || opaque_scan {
1472        for name in &targets {
1473            let func_addr =
1474                if let Some(hex) = name.strip_prefix("0x").or_else(|| name.strip_prefix("0X")) {
1475                    u64::from_str_radix(hex, 16).ok()
1476                } else {
1477                    symbols.iter().find(|(_, n)| n == name).map(|(a, _)| *a)
1478                };
1479            let Some(func_addr) = func_addr else {
1480                eprintln!("Function '{}' not found", name);
1481                continue;
1482            };
1483            let insts = decode_func(func_addr, &symbols, &segs, &data, &mut dec);
1484            if insts.is_empty() {
1485                eprintln!("// {} — no instructions", name);
1486                continue;
1487            }
1488            let func_name = symbols
1489                .iter()
1490                .find(|(a, _)| *a == func_addr)
1491                .map(|(_, n)| n.clone())
1492                .unwrap_or_else(|| format!("func_{:x}", func_addr));
1493            if pcode_json {
1494                let entries: Vec<serde_json::Value> = insts
1495                    .iter()
1496                    .map(|(a, inst)| {
1497                        serde_json::json!({
1498                            "address":     format!("0x{:x}", a),
1499                            "disassembly": inst.disassembly,
1500                            "length":      inst.len,
1501                            "ops":         inst.ops.iter()
1502                                .map(|op| serde_json::json!({ "op": format!("{:?}", op) }))
1503                                .collect::<Vec<_>>(),
1504                        })
1505                    })
1506                    .collect();
1507                println!(
1508                    "{}",
1509                    serde_json::to_string_pretty(&serde_json::json!({
1510                        "function":     func_name,
1511                        "address":      format!("0x{:x}", func_addr),
1512                        "instructions": entries,
1513                    }))
1514                    .unwrap()
1515                );
1516            }
1517            if ssa_json {
1518                let cfg = rsleigh_decompile::cfg::build_cfg(&insts);
1519                let cc = match arch {
1520                    rsleigh_api::Architecture::X86_64
1521                        if rsleigh_decompile::go_pclntab::parse(&data)
1522                            .keys()
1523                            .next()
1524                            .is_some() =>
1525                    {
1526                        rsleigh_decompile::fold::CallingConv::GoAmd64
1527                    }
1528                    rsleigh_api::Architecture::X86_32 | rsleigh_api::Architecture::MIPS32 => {
1529                        rsleigh_decompile::fold::CallingConv::Cdecl32
1530                    }
1531                    rsleigh_api::Architecture::ARM32 => rsleigh_decompile::fold::CallingConv::Arm32,
1532                    rsleigh_api::Architecture::AArch64 => {
1533                        rsleigh_decompile::fold::CallingConv::AArch64
1534                    }
1535                    _ => rsleigh_decompile::fold::CallingConv::SysV,
1536                };
1537                let mut ssa = rsleigh_decompile::ssa::build_ssa_with_cc(&cfg, cc);
1538                rsleigh_decompile::fold::fold_with_cc(&mut ssa, cc);
1539                let blocks: Vec<serde_json::Value> = ssa
1540                    .blocks
1541                    .iter()
1542                    .enumerate()
1543                    .map(|(bi, blk)| {
1544                        let stmts: Vec<serde_json::Value> = blk
1545                            .stmts
1546                            .iter()
1547                            .map(|s| serde_json::json!({ "stmt": format!("{:?}", s) }))
1548                            .collect();
1549                        serde_json::json!({
1550                            "id":         bi,
1551                            "addr":       format!("0x{:x}", blk.addr),
1552                            "stmts":      stmts,
1553                            "terminator": format!("{:?}", blk.terminator),
1554                        })
1555                    })
1556                    .collect();
1557                let vars: Vec<serde_json::Value> = ssa
1558                    .vars
1559                    .iter()
1560                    .enumerate()
1561                    .map(|(vi, v)| {
1562                        serde_json::json!({
1563                            "id":           vi,
1564                            "varnode":      format!("{:?}", v.varnode),
1565                            "expr":         format!("{:?}", v.expr),
1566                            "size":         v.size,
1567                            "param_name":   v.param_name,
1568                            "inferred":     format!("{:?}", v.inferred_type),
1569                            "call_return":  v.call_return,
1570                        })
1571                    })
1572                    .collect();
1573                println!(
1574                    "{}",
1575                    serde_json::to_string_pretty(&serde_json::json!({
1576                        "function": func_name,
1577                        "address":  format!("0x{:x}", func_addr),
1578                        "blocks":   blocks,
1579                        "vars":     vars,
1580                    }))
1581                    .unwrap()
1582                );
1583            }
1584            if opaque_scan {
1585                let cfg = rsleigh_decompile::cfg::build_cfg(&insts);
1586                let cc = match arch {
1587                    rsleigh_api::Architecture::X86_64
1588                        if rsleigh_decompile::go_pclntab::parse(&data)
1589                            .keys()
1590                            .next()
1591                            .is_some() =>
1592                    {
1593                        rsleigh_decompile::fold::CallingConv::GoAmd64
1594                    }
1595                    rsleigh_api::Architecture::X86_32 | rsleigh_api::Architecture::MIPS32 => {
1596                        rsleigh_decompile::fold::CallingConv::Cdecl32
1597                    }
1598                    rsleigh_api::Architecture::ARM32 => rsleigh_decompile::fold::CallingConv::Arm32,
1599                    rsleigh_api::Architecture::AArch64 => {
1600                        rsleigh_decompile::fold::CallingConv::AArch64
1601                    }
1602                    _ => rsleigh_decompile::fold::CallingConv::SysV,
1603                };
1604                let mut ssa = rsleigh_decompile::ssa::build_ssa_with_cc(&cfg, cc);
1605                rsleigh_decompile::fold::fold_with_cc(&mut ssa, cc);
1606                let findings = rsleigh_decompile::opaque_pred::scan_opaque_branches(&ssa);
1607                if findings.is_empty() {
1608                    println!("// {} 0x{:x} — no opaque branches", func_name, func_addr);
1609                } else {
1610                    println!(
1611                        "// {} 0x{:x} — {} opaque branch(es)",
1612                        func_name,
1613                        func_addr,
1614                        findings.len()
1615                    );
1616                    for f in findings {
1617                        println!(
1618                            "  block 0x{:x}  cond=v{}  free_vars={}  -> {:?}",
1619                            f.block_addr, f.cond.0, f.free_var_count, f.class
1620                        );
1621                    }
1622                }
1623            }
1624        }
1625        return;
1626    }
1627
1628    // Decompile mode — two-pass for interprocedural type propagation
1629    // Pass 1: quick decompile all targets to learn parameter/return types + struct params
1630    if all_mode && targets.len() > 1 {
1631        let mut learned: Vec<rsleigh_decompile::LearnedFuncType> = Vec::new();
1632        let mut callsite_returns: Vec<(u64, &'static str)> = Vec::new();
1633        let mut learned_structs: Vec<rsleigh_decompile::LearnedStructParam> = Vec::new();
1634
1635        for name in &targets {
1636            let func_addr =
1637                if let Some(hex) = name.strip_prefix("0x").or_else(|| name.strip_prefix("0X")) {
1638                    u64::from_str_radix(hex, 16).ok()
1639                } else {
1640                    symbols.iter().find(|(_, n)| n == name).map(|(a, _)| *a)
1641                };
1642            if let Some(func_addr) = func_addr {
1643                let insts = decode_func(func_addr, &symbols, &segs, &data, &mut dec);
1644                if !insts.is_empty() {
1645                    // Extract learned types from this function
1646                    if let Some(lt) =
1647                        rsleigh_decompile::extract_learned_types(arch, &insts, Some(&data))
1648                    {
1649                        learned.push(lt);
1650                    }
1651                    // Infer callee return types from how this function uses call results
1652                    let returns =
1653                        rsleigh_decompile::infer_returns_from_callsites(arch, &insts, Some(&data));
1654                    callsite_returns.extend(returns);
1655
1656                    // Extract struct param identifications from decompiled output
1657                    let output = rsleigh_decompile::decompile_with_binary(
1658                        arch,
1659                        &insts,
1660                        Some(&data),
1661                        Some(path),
1662                    );
1663                    let structs = rsleigh_decompile::extract_learned_structs(func_addr, &output);
1664                    learned_structs.extend(structs);
1665                }
1666            }
1667        }
1668
1669        // Merge call-site inferred returns into learned types
1670        callsite_returns.sort_by_key(|(a, _)| *a);
1671        callsite_returns.dedup_by_key(|(a, _)| *a);
1672        for (addr, ret_type) in &callsite_returns {
1673            // Only add if we don't already have a return type for this function
1674            if !learned
1675                .iter()
1676                .any(|lt| lt.addr == *addr && lt.return_type.is_some())
1677            {
1678                learned.push(rsleigh_decompile::LearnedFuncType {
1679                    addr: *addr,
1680                    param_types: Vec::new(),
1681                    return_type: Some(ret_type),
1682                });
1683            }
1684        }
1685
1686        if !learned.is_empty() {
1687            rsleigh_decompile::signatures::register_learned_types(&learned);
1688        }
1689        if !learned_structs.is_empty() {
1690            rsleigh_decompile::signatures::register_learned_structs(&learned_structs);
1691        }
1692    }
1693
1694    // Pass 2: full decompilation with learned types available
1695    let mut results: Vec<serde_json::Value> = Vec::new();
1696
1697    for name in &targets {
1698        // Support hex addresses like 0x1400013f0
1699        let func_addr =
1700            if let Some(hex) = name.strip_prefix("0x").or_else(|| name.strip_prefix("0X")) {
1701                u64::from_str_radix(hex, 16).ok()
1702            } else {
1703                symbols.iter().find(|(_, n)| n == name).map(|(a, _)| *a)
1704            };
1705        if let Some(func_addr) = func_addr {
1706            let output = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1707                decompile_func(func_addr, &symbols, &segs, &data, &mut dec, arch, path)
1708            })) {
1709                Ok(o) => o,
1710                Err(_) => "// decompilation failed (stack overflow)\n".to_string(),
1711            };
1712
1713            if json_mode {
1714                // Extract rich metadata from pseudocode
1715                let mut calls = Vec::new();
1716                let mut strings = Vec::new();
1717                let mut line_count = 0;
1718                for line in output.lines() {
1719                    let t = line.trim();
1720                    if t.is_empty() || t.starts_with("//") {
1721                        continue;
1722                    }
1723                    line_count += 1;
1724                    if t.contains('(') {
1725                        let check = if let Some(eq) = t.find(" = ") {
1726                            &t[eq + 3..]
1727                        } else {
1728                            t
1729                        };
1730                        if let Some(p) = check.find('(') {
1731                            let callee = check[..p].trim().trim_start_matches("return ");
1732                            if !callee.is_empty()
1733                                && !callee.contains(' ')
1734                                && !callee.starts_with('*')
1735                                && !callee.starts_with('(')
1736                                && !callee.starts_with("if")
1737                                && !callee.starts_with("while")
1738                                && !callee.starts_with("switch")
1739                                && callee.len() < 50
1740                                && !calls.contains(&callee.to_string())
1741                            {
1742                                calls.push(callee.to_string());
1743                            }
1744                        }
1745                    }
1746                    if let Some(q1) = t.find('"') {
1747                        if let Some(q2) = t[q1 + 1..].find('"') {
1748                            let s = &t[q1 + 1..q1 + 1 + q2];
1749                            if s.len() >= 2 && s.len() <= 80 && !strings.contains(&s.to_string()) {
1750                                strings.push(s.to_string());
1751                            }
1752                        }
1753                    }
1754                }
1755                let return_type = output
1756                    .lines()
1757                    .next()
1758                    .and_then(|l| l.split_whitespace().next())
1759                    .unwrap_or("void");
1760                let params = output
1761                    .lines()
1762                    .next()
1763                    .map(|l| l.matches("param_").count())
1764                    .unwrap_or(0);
1765                results.push(serde_json::json!({
1766                    "name": name,
1767                    "address": format!("0x{:x}", func_addr),
1768                    "params": params,
1769                    "return_type": return_type,
1770                    "calls": calls,
1771                    "strings": strings,
1772                    "complexity": line_count,
1773                    "pseudocode": output.trim(),
1774                }));
1775            } else {
1776                // Apply token-efficiency modes
1777                let is_compact = args.iter().any(|a| a == "--compact");
1778                let is_brief = args.iter().any(|a| a == "--brief");
1779                let min_comp: usize = args
1780                    .iter()
1781                    .position(|a| a == "--min-complexity")
1782                    .and_then(|i| args.get(i + 1))
1783                    .and_then(|s| s.parse().ok())
1784                    .unwrap_or(0);
1785
1786                // Skip trivial functions
1787                let line_count = output
1788                    .lines()
1789                    .filter(|l| !l.trim().is_empty() && !l.trim().starts_with("//"))
1790                    .count();
1791                if min_comp > 0 && line_count < min_comp {
1792                    continue;
1793                }
1794
1795                let display = if is_brief {
1796                    brief_output(&output)
1797                } else if is_compact {
1798                    compact_output(&output)
1799                } else {
1800                    output.clone()
1801                };
1802
1803                if !display.trim().is_empty() {
1804                    println!("// {}", name);
1805                    for line in display.lines() {
1806                        if !line.trim().is_empty() {
1807                            println!("{}", line);
1808                        }
1809                    }
1810                    println!();
1811                }
1812            }
1813        } else {
1814            eprintln!("Function '{}' not found", name);
1815        }
1816    }
1817
1818    if json_mode {
1819        println!(
1820            "{}",
1821            serde_json::to_string_pretty(&serde_json::json!({
1822                "binary": binary_path,
1823                "arch": format!("{:?}", arch),
1824                "functions": results,
1825            }))
1826            .unwrap()
1827        );
1828    }
1829}
1830
1831fn decode_func(
1832    fa: u64,
1833    symbols: &[(u64, String)],
1834    segs: &[(u64, u64, u64)],
1835    data: &[u8],
1836    dec: &mut rsleigh_api::Decoder,
1837) -> Vec<(u64, pcode_ir::Instruction)> {
1838    let off = segs.iter().find_map(|(va, sz, fo)| {
1839        if fa >= *va && fa < va + sz {
1840            Some(fo + (fa - va))
1841        } else {
1842            None
1843        }
1844    });
1845    let Some(off) = off else {
1846        return vec![];
1847    };
1848    let max = 4096.min(data.len() - off as usize);
1849    let raw_bytes = &data[off as usize..off as usize + max];
1850    // Function-start padding skip. When a CALL rel32 target lands on
1851    // inter-function zero padding (or .pdata reports a stale entry into
1852    // an unmapped slot), the bogus decode floods the disassembly. Only
1853    // skip when leading 4 bytes are all 0x00 — that pattern is benign
1854    // padding on x86/x86-64 and the AArch64 `udf #0` trap, neither of
1855    // which is a real function start.
1856    let pad_skip: usize = if raw_bytes.len() >= 4 && raw_bytes[..4] == [0u8; 4] {
1857        raw_bytes
1858            .iter()
1859            .take(32)
1860            .position(|&b| b != 0x00)
1861            .unwrap_or(raw_bytes.len().min(32))
1862    } else {
1863        0
1864    };
1865    let fa = fa + pad_skip as u64;
1866    let bytes = &raw_bytes[pad_skip..];
1867    let max = bytes.len();
1868    let next_func = symbols
1869        .iter()
1870        .filter(|(a, name)| *a > fa && !name.starts_with("seh_scope_"))
1871        .map(|(a, _)| *a)
1872        .min()
1873        .unwrap_or(fa + max as u64);
1874    let decode_max = ((next_func - fa) as usize).min(max);
1875
1876    // Go stack-check preamble extension. Three known shapes on amd64:
1877    //   A. Small frame (0-128 bytes):
1878    //        49 3b 66 10        cmp rsp, [r14+0x10]
1879    //        0f 86 rr rr rr rr  jbe morestack
1880    //   B. Medium frame via LEA (uses RSP-N as comparison value):
1881    //        4c 8d 64 24 ii     lea r12, [rsp-ii]
1882    //        4d 3b 66 10        cmp r12, [r14+0x10]
1883    //        0f 86 rr rr rr rr  jbe morestack
1884    //   C. Large frame (>32K) uses 32-bit displacement in LEA:
1885    //        4c 8d a4 24 ii ii ii ii  lea r12, [rsp-iiiiiiii]
1886    //        4d 3b 66 10
1887    //        0f 86 rr rr rr rr
1888    //
1889    // Function-discovery (CALL-target scan) plants a spurious FUN_
1890    // symbol at the byte past the JBE because morestack never returns
1891    // to the JBE; it jumps back to the function entry. Extend decode_max
1892    // past that FUN_ boundary when a preamble is detected.
1893    let extended_max = {
1894        // Go preamble compares RSP against g.stackguard0 (at offset 0x10)
1895        // OR g.preempt (at 0x18, used for cooperative preemption). Both
1896        // bytes are valid for the ModR/M displacement after `[R14+disp8]`.
1897        let is_stackguard_off = |b: u8| b == 0x10 || b == 0x18;
1898        let is_small = bytes.len() >= 10
1899            && bytes[0] == 0x49
1900            && bytes[1] == 0x3b
1901            && bytes[2] == 0x66
1902            && is_stackguard_off(bytes[3])
1903            && bytes[4] == 0x0f
1904            && bytes[5] == 0x86;
1905        let is_lea8 = bytes.len() >= 15
1906            && bytes[0] == 0x4c
1907            && bytes[1] == 0x8d
1908            && bytes[2] == 0x64
1909            && bytes[3] == 0x24
1910            && bytes[5] == 0x4d
1911            && bytes[6] == 0x3b
1912            && bytes[7] == 0x66
1913            && is_stackguard_off(bytes[8])
1914            && bytes[9] == 0x0f
1915            && bytes[10] == 0x86;
1916        let is_lea32 = bytes.len() >= 18
1917            && bytes[0] == 0x4c
1918            && bytes[1] == 0x8d
1919            && bytes[2] == 0xa4
1920            && bytes[3] == 0x24
1921            && bytes[8] == 0x4d
1922            && bytes[9] == 0x3b
1923            && bytes[10] == 0x66
1924            && is_stackguard_off(bytes[11])
1925            && bytes[12] == 0x0f
1926            && bytes[13] == 0x86;
1927        let mut ext = decode_max;
1928        if is_small || is_lea8 || is_lea32 {
1929            let scan_start = if is_small {
1930                10
1931            } else if is_lea8 {
1932                15
1933            } else {
1934                18
1935            };
1936            let scan_max = max.min(8192);
1937            // Walk forward looking for the NEXT Go preamble (= next
1938            // function boundary). Don't stop on RET — Go funcs have
1939            // early returns, panic exits, and morestack tails BEFORE
1940            // the real function end. Use the next-preamble pattern as
1941            // the only firm boundary.
1942            let mut found_boundary = false;
1943            for i in scan_start..scan_max {
1944                let next_small = bytes[i] == 0x49
1945                    && i + 3 < scan_max
1946                    && bytes[i + 1] == 0x3b
1947                    && bytes[i + 2] == 0x66
1948                    && (bytes[i + 3] == 0x10 || bytes[i + 3] == 0x18);
1949                let next_lea8 = bytes[i] == 0x4c
1950                    && i + 8 < scan_max
1951                    && bytes[i + 1] == 0x8d
1952                    && bytes[i + 2] == 0x64
1953                    && bytes[i + 3] == 0x24
1954                    && bytes[i + 5] == 0x4d
1955                    && bytes[i + 6] == 0x3b
1956                    && bytes[i + 7] == 0x66
1957                    && (bytes[i + 8] == 0x10 || bytes[i + 8] == 0x18);
1958                let next_lea32 = bytes[i] == 0x4c
1959                    && i + 11 < scan_max
1960                    && bytes[i + 1] == 0x8d
1961                    && bytes[i + 2] == 0xa4
1962                    && bytes[i + 3] == 0x24
1963                    && bytes[i + 8] == 0x4d
1964                    && bytes[i + 9] == 0x3b
1965                    && bytes[i + 10] == 0x66
1966                    && (bytes[i + 11] == 0x10 || bytes[i + 11] == 0x18);
1967                if next_small || next_lea8 || next_lea32 {
1968                    ext = ext.max(i);
1969                    found_boundary = true;
1970                    break;
1971                }
1972            }
1973            if !found_boundary {
1974                ext = scan_max;
1975            }
1976        }
1977        ext
1978    };
1979    let decode_max = extended_max;
1980    let mut insts = Vec::new();
1981    let mut io = 0;
1982    while io < decode_max {
1983        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1984            dec.decode(&bytes[io..], fa + io as u64)
1985        })) {
1986            Ok(Ok(inst)) => {
1987                let l = inst.len as usize;
1988                if l == 0 {
1989                    io += 1;
1990                    continue;
1991                }
1992                insts.push((fa + io as u64, inst));
1993                io += l;
1994            }
1995            Ok(Err(_)) => break,
1996            Err(_) => {
1997                io += 1;
1998            }
1999        }
2000    }
2001    insts
2002}
2003
2004fn decompile_func(
2005    fa: u64,
2006    symbols: &[(u64, String)],
2007    segs: &[(u64, u64, u64)],
2008    data: &[u8],
2009    dec: &mut rsleigh_api::Decoder,
2010    arch: rsleigh_api::Architecture,
2011    path: &Path,
2012) -> String {
2013    let insts = decode_func(fa, symbols, segs, data, dec);
2014    if insts.is_empty() {
2015        return "// no instructions\n".to_string();
2016    }
2017    maybe_annotate_crypto(rsleigh_decompile::decompile_with_binary(
2018        arch,
2019        &insts,
2020        Some(data),
2021        Some(path),
2022    ))
2023}
2024
2025/// Generate a YARA detection rule from binary analysis.
2026/// Extracts unique strings, imports, hex patterns, and crypto signatures.
2027/// Diff two binaries: decompile both, match functions, show unified diff of changes.
2028fn diff_binaries(old_path: &str, new_path: &str, func_filter: &[String]) {
2029    use std::collections::BTreeMap;
2030
2031    eprintln!("Comparing: {} vs {}", old_path, new_path);
2032
2033    // Helper: decompile all functions in a binary, return map of name → pseudocode
2034    let decompile_all = |path: &str| -> BTreeMap<String, String> {
2035        let data = match std::fs::read(path) {
2036            Ok(d) => d,
2037            Err(e) => {
2038                eprintln!("Error reading {}: {}", path, e);
2039                return BTreeMap::new();
2040            }
2041        };
2042        let obj = match goblin::Object::parse(&data) {
2043            Ok(o) => o,
2044            Err(e) => {
2045                eprintln!("Error parsing {}: {}", path, e);
2046                return BTreeMap::new();
2047            }
2048        };
2049        let (arch, segs, mut symbols) = match parse_binary(&obj, &data) {
2050            Some(r) => r,
2051            None => {
2052                eprintln!("Unsupported format: {}", path);
2053                return BTreeMap::new();
2054            }
2055        };
2056
2057        // Discover functions for stripped binaries
2058        if symbols.is_empty() {
2059            if let goblin::Object::PE(pe) = &obj {
2060                let base = pe.image_base as u64;
2061                let entry = base
2062                    + pe.header
2063                        .optional_header
2064                        .unwrap()
2065                        .standard_fields
2066                        .address_of_entry_point as u64;
2067                symbols = discover_pe_functions(entry, &segs, &data, arch);
2068            }
2069        }
2070        let is_elf_stripped = if let goblin::Object::Elf(elf) = &obj {
2071            elf.syms.len() == 0
2072        } else {
2073            false
2074        };
2075        if is_elf_stripped {
2076            if let goblin::Object::Elf(elf) = &obj {
2077                let discovered = discover_elf_functions(elf, &segs, &data, arch);
2078                let existing: std::collections::BTreeSet<u64> =
2079                    symbols.iter().map(|(a, _)| *a).collect();
2080                for (addr, name) in discovered {
2081                    if !existing.contains(&addr) {
2082                        symbols.push((addr, name));
2083                    }
2084                }
2085            }
2086        }
2087
2088        let p = std::path::Path::new(path);
2089        let import_map = build_import_map(&obj, &data);
2090        let mut dec = rsleigh_api::Decoder::new(arch);
2091        let mut result = BTreeMap::new();
2092
2093        for (func_addr, func_name) in &symbols {
2094            let off = segs.iter().find_map(|(va, sz, fo)| {
2095                if *func_addr >= *va && *func_addr < va + sz {
2096                    Some(fo + (func_addr - va))
2097                } else {
2098                    None
2099                }
2100            });
2101            let Some(off) = off else { continue };
2102            let max = 8192.min(data.len().saturating_sub(off as usize));
2103            if max < 2 {
2104                continue;
2105            }
2106            let bytes = &data[off as usize..off as usize + max];
2107
2108            let next_func = symbols
2109                .iter()
2110                .filter(|(a, _)| *a > *func_addr)
2111                .map(|(a, _)| *a)
2112                .min()
2113                .unwrap_or(func_addr + max as u64);
2114            let decode_max = ((next_func - func_addr) as usize).min(max);
2115
2116            let mut insts = Vec::new();
2117            let mut pos = 0;
2118            while pos < decode_max {
2119                match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2120                    dec.decode(&bytes[pos..], func_addr + pos as u64)
2121                })) {
2122                    Ok(Ok(inst)) => {
2123                        let l = inst.len as usize;
2124                        if l == 0 {
2125                            pos += 1;
2126                            continue;
2127                        }
2128                        insts.push((func_addr + pos as u64, inst));
2129                        pos += l;
2130                    }
2131                    _ => {
2132                        pos += 1;
2133                    }
2134                }
2135            }
2136
2137            if !insts.is_empty() {
2138                let output = maybe_annotate_crypto(rsleigh_decompile::decompile_with_binary(
2139                    arch,
2140                    &insts,
2141                    Some(&data),
2142                    Some(p),
2143                ));
2144                if !output.trim().is_empty() {
2145                    result.insert(func_name.clone(), output);
2146                }
2147            }
2148        }
2149        eprintln!("  {}: {} functions decompiled", path, result.len());
2150        result
2151    };
2152
2153    let old_funcs = decompile_all(old_path);
2154    let new_funcs = decompile_all(new_path);
2155
2156    // Match functions and compute diffs
2157    let mut all_names: std::collections::BTreeSet<&String> = std::collections::BTreeSet::new();
2158    for k in old_funcs.keys() {
2159        all_names.insert(k);
2160    }
2161    for k in new_funcs.keys() {
2162        all_names.insert(k);
2163    }
2164
2165    let mut added = 0usize;
2166    let mut removed = 0usize;
2167    let mut changed = 0usize;
2168    let mut unchanged = 0usize;
2169
2170    for name in &all_names {
2171        // Filter if specific functions requested
2172        if !func_filter.is_empty() && !func_filter.iter().any(|f| f.as_str() == name.as_str()) {
2173            continue;
2174        }
2175
2176        let old_code = old_funcs.get(*name);
2177        let new_code = new_funcs.get(*name);
2178
2179        match (old_code, new_code) {
2180            (None, Some(new)) => {
2181                added += 1;
2182                println!("=== ADDED: {} ===", name);
2183                for line in new.lines() {
2184                    println!("\x1b[32m+ {}\x1b[0m", line); // green
2185                }
2186                println!();
2187            }
2188            (Some(old), None) => {
2189                removed += 1;
2190                println!("=== REMOVED: {} ===", name);
2191                for line in old.lines() {
2192                    println!("\x1b[31m- {}\x1b[0m", line); // red
2193                }
2194                println!();
2195            }
2196            (Some(old), Some(new)) => {
2197                if old == new {
2198                    unchanged += 1;
2199                    continue;
2200                }
2201                changed += 1;
2202                println!("=== CHANGED: {} ===", name);
2203                // Simple line-by-line diff
2204                let old_lines: Vec<&str> = old.lines().collect();
2205                let new_lines: Vec<&str> = new.lines().collect();
2206                // Use longest common subsequence for basic diff
2207                let diff = simple_diff(&old_lines, &new_lines);
2208                for (tag, line) in &diff {
2209                    match tag {
2210                        '-' => println!("\x1b[31m- {}\x1b[0m", line),
2211                        '+' => println!("\x1b[32m+ {}\x1b[0m", line),
2212                        ' ' => println!("  {}", line),
2213                        _ => {}
2214                    }
2215                }
2216                println!();
2217            }
2218            (None, None) => {}
2219        }
2220    }
2221
2222    println!("--- Summary ---");
2223    println!("Unchanged: {}", unchanged);
2224    println!("Changed:   {}", changed);
2225    println!("Added:     {}", added);
2226    println!("Removed:   {}", removed);
2227}
2228
2229/// Simple line diff using LCS (longest common subsequence).
2230fn simple_diff<'a>(old: &[&'a str], new: &[&'a str]) -> Vec<(char, &'a str)> {
2231    // Build LCS table
2232    let m = old.len();
2233    let n = new.len();
2234    let mut dp = vec![vec![0u32; n + 1]; m + 1];
2235    for i in 1..=m {
2236        for j in 1..=n {
2237            if old[i - 1] == new[j - 1] {
2238                dp[i][j] = dp[i - 1][j - 1] + 1;
2239            } else {
2240                dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]);
2241            }
2242        }
2243    }
2244
2245    // Backtrack to produce diff
2246    let mut result = Vec::new();
2247    let mut i = m;
2248    let mut j = n;
2249    while i > 0 || j > 0 {
2250        if i > 0 && j > 0 && old[i - 1] == new[j - 1] {
2251            result.push((' ', old[i - 1]));
2252            i -= 1;
2253            j -= 1;
2254        } else if j > 0 && (i == 0 || dp[i][j - 1] >= dp[i - 1][j]) {
2255            result.push(('+', new[j - 1]));
2256            j -= 1;
2257        } else {
2258            result.push(('-', old[i - 1]));
2259            i -= 1;
2260        }
2261    }
2262    result.reverse();
2263    result
2264}
2265
2266/// Build import map from binary for decompilation.
2267fn build_import_map(obj: &goblin::Object, data: &[u8]) -> std::collections::HashMap<u64, String> {
2268    let mut map = std::collections::HashMap::new();
2269    match obj {
2270        goblin::Object::PE(pe) => {
2271            for imp in &pe.imports {
2272                if imp.rva != 0 {
2273                    map.insert(pe.image_base as u64 + imp.rva as u64, imp.name.to_string());
2274                }
2275            }
2276        }
2277        goblin::Object::Elf(elf) => {
2278            for sym in elf.dynsyms.iter() {
2279                if sym.st_value != 0 {
2280                    if let Some(name) = elf.dynstrtab.get_at(sym.st_name) {
2281                        if !name.is_empty() {
2282                            map.insert(sym.st_value, name.to_string());
2283                        }
2284                    }
2285                }
2286            }
2287        }
2288        _ => {}
2289    }
2290    map
2291}
2292
2293/// Generate a one-line summary per function for AI-assisted triage.
2294/// Shows: function name, calls made, strings referenced, patterns detected.
2295fn run_summary(binary_path: &str, data: &[u8]) {
2296    let obj = match goblin::Object::parse(data) {
2297        Ok(o) => o,
2298        Err(e) => {
2299            eprintln!("Error: {}", e);
2300            return;
2301        }
2302    };
2303    let (arch, segs, mut symbols) = match parse_binary(&obj, data) {
2304        Some(r) => r,
2305        None => {
2306            eprintln!("Unsupported format");
2307            return;
2308        }
2309    };
2310    // Discover functions for stripped binaries
2311    if symbols.is_empty() {
2312        if let goblin::Object::PE(pe) = &obj {
2313            let base = pe.image_base as u64;
2314            let entry = base
2315                + pe.header
2316                    .optional_header
2317                    .unwrap()
2318                    .standard_fields
2319                    .address_of_entry_point as u64;
2320            symbols = discover_pe_functions(entry, &segs, data, arch);
2321        }
2322    }
2323    let is_elf_stripped = if let goblin::Object::Elf(elf) = &obj {
2324        elf.syms.len() == 0
2325    } else {
2326        false
2327    };
2328    if is_elf_stripped {
2329        if let goblin::Object::Elf(elf) = &obj {
2330            let discovered = discover_elf_functions(elf, &segs, data, arch);
2331            let existing: std::collections::BTreeSet<u64> =
2332                symbols.iter().map(|(a, _)| *a).collect();
2333            for (addr, name) in discovered {
2334                if !existing.contains(&addr) {
2335                    symbols.push((addr, name));
2336                }
2337            }
2338        }
2339    }
2340
2341    let path = std::path::Path::new(binary_path);
2342    let import_map = build_import_map(&obj, data);
2343    let mut dec = rsleigh_api::Decoder::new(arch);
2344
2345    eprintln!("{} functions in {}", symbols.len(), binary_path);
2346    println!(
2347        "{:<14} {:<25} {:<40} {}",
2348        "Address", "Name", "Calls", "Strings/Patterns"
2349    );
2350    println!("{}", "-".repeat(100));
2351
2352    for (func_addr, func_name) in &symbols {
2353        let off = segs.iter().find_map(|(va, sz, fo)| {
2354            if *func_addr >= *va && *func_addr < va + sz {
2355                Some(fo + (func_addr - va))
2356            } else {
2357                None
2358            }
2359        });
2360        let Some(off) = off else { continue };
2361        let max = 4096.min(data.len().saturating_sub(off as usize));
2362        if max < 2 {
2363            continue;
2364        }
2365        let bytes = &data[off as usize..off as usize + max];
2366
2367        let next_func = symbols
2368            .iter()
2369            .filter(|(a, _)| *a > *func_addr)
2370            .map(|(a, _)| *a)
2371            .min()
2372            .unwrap_or(func_addr + max as u64);
2373        let decode_max = ((next_func - func_addr) as usize).min(max);
2374
2375        let mut insts = Vec::new();
2376        let mut pos = 0;
2377        while pos < decode_max {
2378            match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2379                dec.decode(&bytes[pos..], func_addr + pos as u64)
2380            })) {
2381                Ok(Ok(inst)) => {
2382                    let l = inst.len as usize;
2383                    if l == 0 {
2384                        pos += 1;
2385                        continue;
2386                    }
2387                    insts.push((func_addr + pos as u64, inst));
2388                    pos += l;
2389                }
2390                _ => {
2391                    pos += 1;
2392                }
2393            }
2394        }
2395
2396        if insts.is_empty() {
2397            continue;
2398        }
2399
2400        // Decompile and extract metadata
2401        let output = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2402            rsleigh_decompile::decompile_with_binary(arch, &insts, Some(data), Some(path))
2403        }));
2404        let output = match output {
2405            Ok(o) => o,
2406            Err(_) => continue,
2407        };
2408
2409        // Extract calls
2410        let mut calls = Vec::new();
2411        for line in output.lines() {
2412            let t = line.trim();
2413            if t.contains('(')
2414                && t.contains(')')
2415                && !t.starts_with("//")
2416                && !t.starts_with("if ")
2417                && !t.starts_with("while ")
2418                && !t.starts_with("for ")
2419                && !t.contains(" = ")
2420            {
2421                // Standalone call: func_name(args);
2422                if let Some(paren) = t.find('(') {
2423                    let callee = t[..paren].trim().trim_start_matches("return ");
2424                    if !callee.is_empty() && !callee.contains(' ') && callee.len() < 40 {
2425                        calls.push(callee.to_string());
2426                    }
2427                }
2428            }
2429            // Also extract from assignments: var = func(args);
2430            if let Some(eq) = t.find(" = ") {
2431                let rhs = &t[eq + 3..];
2432                if let Some(paren) = rhs.find('(') {
2433                    let callee = rhs[..paren].trim();
2434                    if !callee.is_empty()
2435                        && !callee.starts_with('*')
2436                        && !callee.starts_with('(')
2437                        && !callee.contains(' ')
2438                        && callee.len() < 40
2439                    {
2440                        if !calls.contains(&callee.to_string()) {
2441                            calls.push(callee.to_string());
2442                        }
2443                    }
2444                }
2445            }
2446        }
2447
2448        // Extract strings
2449        let mut strings = Vec::new();
2450        for line in output.lines() {
2451            let t = line.trim();
2452            if let Some(q1) = t.find('"') {
2453                if let Some(q2) = t[q1 + 1..].find('"') {
2454                    let s = &t[q1 + 1..q1 + 1 + q2];
2455                    if s.len() >= 3 && s.len() <= 40 && !strings.contains(&s.to_string()) {
2456                        strings.push(s.to_string());
2457                    }
2458                }
2459            }
2460        }
2461
2462        // Detect patterns
2463        let mut patterns = Vec::new();
2464        if output.contains("XOR") || output.contains("^ 0x") {
2465            patterns.push("xor");
2466        }
2467        if output.contains("AES") || output.contains("SHA") || output.contains("CRC32") {
2468            patterns.push("crypto");
2469        }
2470        if output.contains("TAINT") {
2471            patterns.push("taint");
2472        }
2473        if output.contains("stack cookie") {
2474            patterns.push("canary");
2475        }
2476        if output.contains("VirtualAlloc") || output.contains("mmap") {
2477            patterns.push("alloc");
2478        }
2479        if output.contains("recv") || output.contains("send") || output.contains("socket") {
2480            patterns.push("network");
2481        }
2482        if output.contains("RegSetValue") || output.contains("RegCreateKey") {
2483            patterns.push("registry");
2484        }
2485        if output.contains("CreateFile") || output.contains("fopen") {
2486            patterns.push("file");
2487        }
2488        if output.contains("system(") || output.contains("exec(") || output.contains("popen(") {
2489            patterns.push("exec");
2490        }
2491
2492        // Format output
2493        let calls_str = if calls.len() > 3 {
2494            format!("{}, +{} more", calls[..3].join(", "), calls.len() - 3)
2495        } else {
2496            calls.join(", ")
2497        };
2498
2499        let mut info_parts = Vec::new();
2500        if !strings.is_empty() {
2501            let s = if strings.len() > 2 {
2502                format!("\"{}\" +{}", strings[0], strings.len() - 1)
2503            } else {
2504                strings
2505                    .iter()
2506                    .map(|s| format!("\"{}\"", s))
2507                    .collect::<Vec<_>>()
2508                    .join(" ")
2509            };
2510            info_parts.push(s);
2511        }
2512        if !patterns.is_empty() {
2513            info_parts.push(format!("[{}]", patterns.join(",")));
2514        }
2515
2516        println!(
2517            "0x{:012x} {:<25} {:<40} {}",
2518            func_addr,
2519            func_name,
2520            calls_str,
2521            info_parts.join(" ")
2522        );
2523    }
2524}
2525
2526/// Show cross-references for a function: callers, callees, strings, data refs.
2527fn run_xrefs(binary_path: &str, data: &[u8], target_name: &str) {
2528    if target_name.is_empty() {
2529        eprintln!("Usage: rsleigh <binary> --xrefs <func_name>");
2530        return;
2531    }
2532
2533    let obj = match goblin::Object::parse(data) {
2534        Ok(o) => o,
2535        Err(e) => {
2536            eprintln!("Error: {}", e);
2537            return;
2538        }
2539    };
2540    let (arch, segs, mut symbols) = match parse_binary(&obj, data) {
2541        Some(r) => r,
2542        None => {
2543            eprintln!("Unsupported format");
2544            return;
2545        }
2546    };
2547    if symbols.is_empty() {
2548        if let goblin::Object::PE(pe) = &obj {
2549            let base = pe.image_base as u64;
2550            let entry = base
2551                + pe.header
2552                    .optional_header
2553                    .unwrap()
2554                    .standard_fields
2555                    .address_of_entry_point as u64;
2556            symbols = discover_pe_functions(entry, &segs, data, arch);
2557        }
2558    }
2559    let is_elf_stripped = if let goblin::Object::Elf(elf) = &obj {
2560        elf.syms.len() == 0
2561    } else {
2562        false
2563    };
2564    if is_elf_stripped {
2565        if let goblin::Object::Elf(elf) = &obj {
2566            let discovered = discover_elf_functions(elf, &segs, data, arch);
2567            let existing: std::collections::BTreeSet<u64> =
2568                symbols.iter().map(|(a, _)| *a).collect();
2569            for (addr, name) in discovered {
2570                if !existing.contains(&addr) {
2571                    symbols.push((addr, name));
2572                }
2573            }
2574        }
2575    }
2576
2577    // Find the target function
2578    let target_addr = if let Some(hex) = target_name.strip_prefix("0x") {
2579        u64::from_str_radix(hex, 16).ok()
2580    } else {
2581        symbols
2582            .iter()
2583            .find(|(_, n)| n == target_name)
2584            .map(|(a, _)| *a)
2585    };
2586    let Some(target_addr) = target_addr else {
2587        eprintln!("Function '{}' not found", target_name);
2588        return;
2589    };
2590    let target_display = symbols
2591        .iter()
2592        .find(|(a, _)| *a == target_addr)
2593        .map(|(_, n)| n.as_str())
2594        .unwrap_or(target_name);
2595
2596    let path = std::path::Path::new(binary_path);
2597    let mut dec = rsleigh_api::Decoder::new(arch);
2598
2599    // Phase 1: Decompile target function to find its callees and strings
2600    let mut callees = Vec::new();
2601    let mut strings_in_target = Vec::new();
2602    let mut target_output = String::new();
2603    {
2604        let off = segs.iter().find_map(|(va, sz, fo)| {
2605            if target_addr >= *va && target_addr < va + sz {
2606                Some(fo + (target_addr - va))
2607            } else {
2608                None
2609            }
2610        });
2611        if let Some(off) = off {
2612            let max = 8192.min(data.len().saturating_sub(off as usize));
2613            let bytes = &data[off as usize..off as usize + max];
2614            let next_func = symbols
2615                .iter()
2616                .filter(|(a, _)| *a > target_addr)
2617                .map(|(a, _)| *a)
2618                .min()
2619                .unwrap_or(target_addr + max as u64);
2620            let decode_max = ((next_func - target_addr) as usize).min(max);
2621            let mut insts = Vec::new();
2622            let mut pos = 0;
2623            while pos < decode_max {
2624                match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2625                    dec.decode(&bytes[pos..], target_addr + pos as u64)
2626                })) {
2627                    Ok(Ok(inst)) => {
2628                        let l = inst.len as usize;
2629                        if l == 0 {
2630                            pos += 1;
2631                            continue;
2632                        }
2633                        insts.push((target_addr + pos as u64, inst));
2634                        pos += l;
2635                    }
2636                    _ => {
2637                        pos += 1;
2638                    }
2639                }
2640            }
2641            if !insts.is_empty() {
2642                target_output = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2643                    rsleigh_decompile::decompile_with_binary(arch, &insts, Some(data), Some(path))
2644                }))
2645                .unwrap_or_default();
2646            }
2647        }
2648        // Extract callees and strings from decompiled output
2649        for line in target_output.lines() {
2650            let t = line.trim();
2651            // Extract function calls
2652            if t.contains('(') && !t.starts_with("//") {
2653                if let Some(paren) = t.find('(') {
2654                    let before = match t.find(" = ") {
2655                        Some(eq) if eq + 3 <= paren => &t[eq + 3..paren],
2656                        _ => &t[..paren],
2657                    };
2658                    let callee = before.trim().trim_start_matches("return ");
2659                    if !callee.is_empty()
2660                        && !callee.contains(' ')
2661                        && !callee.starts_with('*')
2662                        && !callee.starts_with('(')
2663                        && !callee.starts_with("if")
2664                        && !callee.starts_with("while")
2665                        && callee.len() < 50
2666                        && !callees.contains(&callee.to_string())
2667                    {
2668                        callees.push(callee.to_string());
2669                    }
2670                }
2671            }
2672            // Extract strings
2673            if let Some(q1) = t.find('"') {
2674                if let Some(q2) = t[q1 + 1..].find('"') {
2675                    let s = &t[q1 + 1..q1 + 1 + q2];
2676                    if s.len() >= 2 && s.len() <= 60 {
2677                        strings_in_target.push(s.to_string());
2678                    }
2679                }
2680            }
2681        }
2682    }
2683
2684    // Phase 2: Scan ALL functions to find callers (functions that call target)
2685    let mut callers = Vec::new();
2686    for (func_addr, func_name) in &symbols {
2687        if *func_addr == target_addr {
2688            continue;
2689        }
2690        let off = segs.iter().find_map(|(va, sz, fo)| {
2691            if *func_addr >= *va && *func_addr < va + sz {
2692                Some(fo + (func_addr - va))
2693            } else {
2694                None
2695            }
2696        });
2697        let Some(off) = off else { continue };
2698        let max = 4096.min(data.len().saturating_sub(off as usize));
2699        if max < 2 {
2700            continue;
2701        }
2702        let bytes = &data[off as usize..off as usize + max];
2703        let next_func = symbols
2704            .iter()
2705            .filter(|(a, _)| *a > *func_addr)
2706            .map(|(a, _)| *a)
2707            .min()
2708            .unwrap_or(func_addr + max as u64);
2709        let decode_max = ((next_func - func_addr) as usize).min(max);
2710        let mut insts = Vec::new();
2711        let mut pos = 0;
2712        while pos < decode_max {
2713            match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2714                dec.decode(&bytes[pos..], func_addr + pos as u64)
2715            })) {
2716                Ok(Ok(inst)) => {
2717                    let l = inst.len as usize;
2718                    if l == 0 {
2719                        pos += 1;
2720                        continue;
2721                    }
2722                    // Check if this instruction calls the target
2723                    let dis = &inst.disassembly;
2724                    if dis.starts_with("CALL ") || dis.starts_with("BL ") {
2725                        if let Some(target_str) = dis.split_whitespace().nth(1) {
2726                            if let Some(hex) = target_str.strip_prefix("0x") {
2727                                if let Ok(addr) = u64::from_str_radix(hex, 16) {
2728                                    if addr == target_addr {
2729                                        callers.push((func_addr.clone(), func_name.clone()));
2730                                    }
2731                                }
2732                            }
2733                        }
2734                    }
2735                    insts.push((func_addr + pos as u64, inst));
2736                    pos += l;
2737                }
2738                _ => {
2739                    pos += 1;
2740                }
2741            }
2742        }
2743    }
2744
2745    // Output
2746    println!(
2747        "=== Cross-references for {} (0x{:x}) ===",
2748        target_display, target_addr
2749    );
2750    println!();
2751    let mut caller_counts: std::collections::BTreeMap<(u64, String), usize> =
2752        std::collections::BTreeMap::new();
2753    for (addr, name) in &callers {
2754        *caller_counts.entry((*addr, name.clone())).or_insert(0) += 1;
2755    }
2756    let n_callers = caller_counts.len();
2757    let n_sites = callers.len();
2758    println!(
2759        "Called by ({} {}, {} call site{}):",
2760        n_callers,
2761        if n_callers == 1 { "caller" } else { "callers" },
2762        n_sites,
2763        if n_sites == 1 { "" } else { "s" }
2764    );
2765    if caller_counts.is_empty() {
2766        println!("  (none found — may be called indirectly or is entry point)");
2767    }
2768    for ((addr, name), count) in &caller_counts {
2769        if *count > 1 {
2770            println!("  0x{:012x}  {}  (×{})", addr, name, count);
2771        } else {
2772            println!("  0x{:012x}  {}", addr, name);
2773        }
2774    }
2775    println!();
2776    println!("Calls ({} callees):", callees.len());
2777    for callee in &callees {
2778        println!("  {}", callee);
2779    }
2780    println!();
2781    if !strings_in_target.is_empty() {
2782        println!("Strings ({}):", strings_in_target.len());
2783        for s in &strings_in_target {
2784            println!("  \"{}\"", s);
2785        }
2786        println!();
2787    }
2788    println!("Decompiled output:");
2789    println!("{}", target_output);
2790}
2791
2792/// Search for functions matching a query: string, API call, or hex constant.
2793fn run_search(
2794    binary_path: &str,
2795    data: &[u8],
2796    query: &str,
2797    api_mode: bool,
2798    const_mode: bool,
2799    tag_mode: bool,
2800    decompile_results: bool,
2801    json_output: bool,
2802) {
2803    let obj = match goblin::Object::parse(data) {
2804        Ok(o) => o,
2805        Err(e) => {
2806            eprintln!("Error: {}", e);
2807            return;
2808        }
2809    };
2810    let (arch, segs, mut symbols) = match parse_binary(&obj, data) {
2811        Some(r) => r,
2812        None => {
2813            eprintln!("Unsupported format");
2814            return;
2815        }
2816    };
2817    if symbols.is_empty() {
2818        if let goblin::Object::PE(pe) = &obj {
2819            let base = pe.image_base as u64;
2820            let entry = base
2821                + pe.header
2822                    .optional_header
2823                    .unwrap()
2824                    .standard_fields
2825                    .address_of_entry_point as u64;
2826            symbols = discover_pe_functions(entry, &segs, data, arch);
2827        }
2828    }
2829    let is_elf_stripped = if let goblin::Object::Elf(elf) = &obj {
2830        elf.syms.len() == 0
2831    } else {
2832        false
2833    };
2834    if is_elf_stripped {
2835        if let goblin::Object::Elf(elf) = &obj {
2836            let discovered = discover_elf_functions(elf, &segs, data, arch);
2837            let existing: std::collections::BTreeSet<u64> =
2838                symbols.iter().map(|(a, _)| *a).collect();
2839            for (addr, name) in discovered {
2840                if !existing.contains(&addr) {
2841                    symbols.push((addr, name));
2842                }
2843            }
2844        }
2845    }
2846
2847    let path = std::path::Path::new(binary_path);
2848    let mut dec = rsleigh_api::Decoder::new(arch);
2849    let query_lower = query.to_lowercase();
2850
2851    let mode_str = if api_mode {
2852        " (API)"
2853    } else if const_mode {
2854        " (const)"
2855    } else if tag_mode {
2856        " (tag)"
2857    } else {
2858        ""
2859    };
2860    eprintln!(
2861        "Searching {} functions for '{}'{}...",
2862        symbols.len(),
2863        query,
2864        mode_str
2865    );
2866
2867    // matches: (addr, name, reason, context, pseudocode)
2868    let mut matches: Vec<(u64, String, String, String, String)> = Vec::new();
2869
2870    // Tag-based search: decompile all, extract tags, filter
2871    if tag_mode {
2872        let search_tags: Vec<&str> = query.split(',').map(|s| s.trim()).collect();
2873        for (func_addr, func_name) in &symbols {
2874            let insts = decode_func(*func_addr, &symbols, &segs, data, &mut dec);
2875            if insts.is_empty() {
2876                continue;
2877            }
2878            let output = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2879                rsleigh_decompile::decompile_with_binary(arch, &insts, Some(data), Some(path))
2880            })) {
2881                Ok(o) => o,
2882                Err(_) => continue,
2883            };
2884
2885            let meta =
2886                rsleigh_decompile::analysis::extract_function_meta(func_name, *func_addr, &output);
2887            let has_tag = search_tags
2888                .iter()
2889                .any(|t| meta.tags.iter().any(|mt| mt == t));
2890            if has_tag {
2891                let matched_tags: Vec<&str> = meta
2892                    .tags
2893                    .iter()
2894                    .filter(|t| search_tags.contains(&t.as_str()))
2895                    .map(|t| t.as_str())
2896                    .collect();
2897                let calls_str = if meta.calls.len() > 3 {
2898                    format!("{}, +{}", meta.calls[..3].join(", "), meta.calls.len() - 3)
2899                } else {
2900                    meta.calls.join(", ")
2901                };
2902                matches.push((
2903                    *func_addr,
2904                    func_name.clone(),
2905                    format!("tags: [{}]", matched_tags.join(",")),
2906                    calls_str,
2907                    output,
2908                ));
2909            }
2910        }
2911        // Skip the rest of the function and go to output
2912        return output_search_results(&matches, query, json_output, decompile_results);
2913    }
2914
2915    for (func_addr, func_name) in &symbols {
2916        // Quick pre-filter: check function name first
2917        if !api_mode && !const_mode && func_name.to_lowercase().contains(&query_lower) {
2918            matches.push((
2919                func_addr.clone(),
2920                func_name.clone(),
2921                "name match".to_string(),
2922                String::new(),
2923                String::new(),
2924            ));
2925            continue;
2926        }
2927
2928        let off = segs.iter().find_map(|(va, sz, fo)| {
2929            if *func_addr >= *va && *func_addr < va + sz {
2930                Some(fo + (func_addr - va))
2931            } else {
2932                None
2933            }
2934        });
2935        let Some(off) = off else { continue };
2936        let max = 4096.min(data.len().saturating_sub(off as usize));
2937        if max < 2 {
2938            continue;
2939        }
2940        let bytes = &data[off as usize..off as usize + max];
2941
2942        let next_func = symbols
2943            .iter()
2944            .filter(|(a, _)| *a > *func_addr)
2945            .map(|(a, _)| *a)
2946            .min()
2947            .unwrap_or(func_addr + max as u64);
2948        let decode_max = ((next_func - func_addr) as usize).min(max);
2949
2950        // For API mode: decompile and search for function call pattern "api_name("
2951        if api_mode {
2952            let mut insts = Vec::new();
2953            let mut pos = 0;
2954            while pos < decode_max {
2955                match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2956                    dec.decode(&bytes[pos..], func_addr + pos as u64)
2957                })) {
2958                    Ok(Ok(inst)) => {
2959                        let l = inst.len as usize;
2960                        if l == 0 {
2961                            pos += 1;
2962                            continue;
2963                        }
2964                        insts.push((func_addr + pos as u64, inst));
2965                        pos += l;
2966                    }
2967                    _ => {
2968                        pos += 1;
2969                    }
2970                }
2971            }
2972            if insts.is_empty() {
2973                continue;
2974            }
2975            let output = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2976                rsleigh_decompile::decompile_with_binary(arch, &insts, Some(data), Some(path))
2977            })) {
2978                Ok(o) => o,
2979                Err(_) => continue,
2980            };
2981            // Search for "api_name(" pattern — must be a call, not just a substring
2982            let call_pattern = format!("{}(", query);
2983            if output.contains(&call_pattern) {
2984                let context_line = output
2985                    .lines()
2986                    .find(|l| l.contains(&call_pattern) && !l.trim().starts_with("//"))
2987                    .unwrap_or("")
2988                    .trim()
2989                    .to_string();
2990                let context = if context_line.len() > 80 {
2991                    format!("{}...", &context_line[..80])
2992                } else {
2993                    context_line
2994                };
2995                matches.push((
2996                    *func_addr,
2997                    func_name.clone(),
2998                    format!("calls {}", query),
2999                    context,
3000                    output,
3001                ));
3002            }
3003            continue;
3004        }
3005
3006        // For const mode: search for the hex constant in instruction bytes
3007        if const_mode {
3008            let const_val = if let Some(hex) = query.strip_prefix("0x") {
3009                u64::from_str_radix(hex, 16).ok()
3010            } else {
3011                query.parse::<u64>().ok()
3012            };
3013            if let Some(val) = const_val {
3014                // Search for the constant in instruction immediates
3015                let val_le4 = (val as u32).to_le_bytes();
3016                let val_le8 = val.to_le_bytes();
3017                let val_be4 = (val as u32).to_be_bytes();
3018                let found = if val <= 0xFFFFFFFF {
3019                    bytes[..decode_max]
3020                        .windows(4)
3021                        .any(|w| w == val_le4 || w == val_be4)
3022                } else {
3023                    bytes[..decode_max].windows(8).any(|w| w == val_le8)
3024                };
3025                if found {
3026                    matches.push((
3027                        *func_addr,
3028                        func_name.clone(),
3029                        format!("contains 0x{:x}", val),
3030                        String::new(),
3031                        String::new(),
3032                    ));
3033                }
3034            }
3035            continue;
3036        }
3037
3038        // Default string search: first check raw bytes for the query string
3039        // (much faster than decompiling). If found, decompile for context.
3040        let query_bytes = query.as_bytes();
3041        let has_raw_match = bytes[..decode_max]
3042            .windows(query_bytes.len())
3043            .any(|w| w.eq_ignore_ascii_case(query_bytes));
3044        // Also check for wide string (UTF-16LE)
3045        let wide_query: Vec<u8> = query.bytes().flat_map(|b| [b, 0]).collect();
3046        let has_wide_match = if wide_query.len() <= decode_max {
3047            bytes[..decode_max]
3048                .windows(wide_query.len())
3049                .any(|w| w == wide_query.as_slice())
3050        } else {
3051            false
3052        };
3053
3054        if has_raw_match || has_wide_match {
3055            // Quick match from raw bytes — decompile for context
3056            let mut insts = Vec::new();
3057            let mut pos = 0;
3058            while pos < decode_max {
3059                match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3060                    dec.decode(&bytes[pos..], func_addr + pos as u64)
3061                })) {
3062                    Ok(Ok(inst)) => {
3063                        let l = inst.len as usize;
3064                        if l == 0 {
3065                            pos += 1;
3066                            continue;
3067                        }
3068                        insts.push((func_addr + pos as u64, inst));
3069                        pos += l;
3070                    }
3071                    _ => {
3072                        pos += 1;
3073                    }
3074                }
3075            }
3076            let (context, full_output) = if !insts.is_empty() {
3077                match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3078                    rsleigh_decompile::decompile_with_binary(arch, &insts, Some(data), Some(path))
3079                })) {
3080                    Ok(output) => {
3081                        let ctx = output
3082                            .lines()
3083                            .find(|l| l.to_lowercase().contains(&query_lower))
3084                            .unwrap_or("")
3085                            .trim()
3086                            .to_string();
3087                        (ctx, output)
3088                    }
3089                    Err(_) => (String::new(), String::new()),
3090                }
3091            } else {
3092                (String::new(), String::new())
3093            };
3094            let context = if context.len() > 80 {
3095                format!("{}...", &context[..80])
3096            } else {
3097                context
3098            };
3099            let match_type = if has_wide_match && !has_raw_match {
3100                "wide string"
3101            } else {
3102                "string"
3103            };
3104            matches.push((
3105                *func_addr,
3106                func_name.clone(),
3107                match_type.to_string(),
3108                context,
3109                full_output,
3110            ));
3111            continue;
3112        }
3113
3114        // Fallback: also search by decompiling if no raw match
3115        // (catches computed strings, API names from import resolution, etc.)
3116        // Only do this for short queries that might be API names
3117        if query.len() >= 4 && query.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
3118            let mut insts = Vec::new();
3119            let mut pos = 0;
3120            while pos < decode_max {
3121                match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3122                    dec.decode(&bytes[pos..], func_addr + pos as u64)
3123                })) {
3124                    Ok(Ok(inst)) => {
3125                        let l = inst.len as usize;
3126                        if l == 0 {
3127                            pos += 1;
3128                            continue;
3129                        }
3130                        insts.push((func_addr + pos as u64, inst));
3131                        pos += l;
3132                    }
3133                    _ => {
3134                        pos += 1;
3135                    }
3136                }
3137            }
3138            if !insts.is_empty() {
3139                if let Ok(output) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3140                    rsleigh_decompile::decompile_with_binary(arch, &insts, Some(data), Some(path))
3141                })) {
3142                    if output.to_lowercase().contains(&query_lower) {
3143                        let context = output
3144                            .lines()
3145                            .find(|l| l.to_lowercase().contains(&query_lower))
3146                            .unwrap_or("")
3147                            .trim()
3148                            .to_string();
3149                        let context = if context.len() > 80 {
3150                            format!("{}...", &context[..80])
3151                        } else {
3152                            context
3153                        };
3154                        matches.push((
3155                            *func_addr,
3156                            func_name.clone(),
3157                            "pseudocode match".to_string(),
3158                            context,
3159                            output.clone(),
3160                        ));
3161                    }
3162                }
3163            }
3164        }
3165    }
3166
3167    output_search_results(&matches, query, json_output, decompile_results);
3168}
3169
3170/// Format and display search results.
3171fn output_search_results(
3172    matches: &[(u64, String, String, String, String)],
3173    query: &str,
3174    json_output: bool,
3175    decompile_results: bool,
3176) {
3177    if json_output {
3178        let entries: Vec<serde_json::Value> = matches
3179            .iter()
3180            .map(|(addr, name, reason, context, pseudocode)| {
3181                let mut entry = serde_json::json!({
3182                    "address": format!("0x{:x}", addr),
3183                    "name": name,
3184                    "match_type": reason,
3185                });
3186                if !context.is_empty() {
3187                    entry
3188                        .as_object_mut()
3189                        .unwrap()
3190                        .insert("context".to_string(), serde_json::json!(context));
3191                }
3192                if decompile_results && !pseudocode.is_empty() {
3193                    entry
3194                        .as_object_mut()
3195                        .unwrap()
3196                        .insert("pseudocode".to_string(), serde_json::json!(pseudocode));
3197                }
3198                entry
3199            })
3200            .collect();
3201        println!(
3202            "{}",
3203            serde_json::to_string_pretty(&serde_json::json!({
3204                "query": query,
3205                "match_count": matches.len(),
3206                "results": entries,
3207            }))
3208            .unwrap()
3209        );
3210    } else {
3211        println!("{} matches for '{}':", matches.len(), query);
3212        println!();
3213        for (addr, name, reason, context, pseudocode) in matches {
3214            println!("  0x{:012x}  {:<25} {}", addr, name, reason);
3215            if !context.is_empty() {
3216                println!("                  {}", context);
3217            }
3218            if decompile_results && !pseudocode.is_empty() {
3219                println!();
3220                for line in pseudocode.lines() {
3221                    println!("    {}", line);
3222                }
3223                println!();
3224            }
3225        }
3226    }
3227}
3228
3229/// Scan for common vulnerability patterns in decompiled output.
3230fn run_section_scan(binary_path: &str, data: &[u8]) {
3231    let obj = match goblin::Object::parse(data) {
3232        Ok(o) => o,
3233        Err(e) => {
3234            eprintln!("Error: {}", e);
3235            return;
3236        }
3237    };
3238    let mut sec_list: Vec<(String, &[u8], u64)> = Vec::new();
3239    let mut overlay: Option<&[u8]> = None;
3240    match &obj {
3241        goblin::Object::PE(pe) => {
3242            let mut end_fo: usize = 0;
3243            for sec in &pe.sections {
3244                let name = String::from_utf8_lossy(&sec.name)
3245                    .trim_end_matches('\0')
3246                    .to_string();
3247                let fo = sec.pointer_to_raw_data as usize;
3248                let sz = sec.size_of_raw_data as usize;
3249                if fo == 0 || sz == 0 {
3250                    continue;
3251                }
3252                let end = fo.saturating_add(sz).min(data.len());
3253                if fo < end {
3254                    sec_list.push((
3255                        name,
3256                        &data[fo..end],
3257                        sec.virtual_address as u64 + pe.image_base as u64,
3258                    ));
3259                    if end > end_fo {
3260                        end_fo = end;
3261                    }
3262                }
3263            }
3264            if end_fo < data.len() {
3265                overlay = Some(&data[end_fo..]);
3266            }
3267        }
3268        goblin::Object::Elf(elf) => {
3269            for sh in &elf.section_headers {
3270                if sh.sh_type != goblin::elf::section_header::SHT_PROGBITS {
3271                    continue;
3272                }
3273                let fo = sh.sh_offset as usize;
3274                let sz = sh.sh_size as usize;
3275                if sz == 0 {
3276                    continue;
3277                }
3278                let end = fo.saturating_add(sz).min(data.len());
3279                let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("").to_string();
3280                if fo < end {
3281                    sec_list.push((name, &data[fo..end], sh.sh_addr));
3282                }
3283            }
3284        }
3285        goblin::Object::Mach(goblin::mach::Mach::Binary(m)) => {
3286            for seg in &m.segments {
3287                for sec_result in seg {
3288                    if let Ok((sec, sec_data)) = sec_result {
3289                        let name = sec.name().unwrap_or("").to_string();
3290                        if !sec_data.is_empty() {
3291                            sec_list.push((name, sec_data, sec.addr));
3292                        }
3293                    }
3294                }
3295            }
3296        }
3297        _ => {}
3298    }
3299
3300    println!("=== Section Anomaly Scan: {} ===", binary_path);
3301    println!();
3302    println!("{:<24} {:>10}  entropy", "section", "bytes");
3303    for (name, bytes, _va) in &sec_list {
3304        let h = rsleigh_decompile::analysis::shannon_entropy(bytes);
3305        let flag = if h > 7.9 {
3306            " ** HIGH"
3307        } else if h > 7.5 {
3308            " * elevated"
3309        } else {
3310            ""
3311        };
3312        println!("  {:<22} {:>10}  {:>5.2}{}", name, bytes.len(), h, flag);
3313    }
3314    if let Some(ov) = overlay {
3315        if !ov.is_empty() {
3316            let h = rsleigh_decompile::analysis::shannon_entropy(ov);
3317            println!(
3318                "  {:<22} {:>10}  {:>5.2}  (PE overlay)",
3319                "<overlay>",
3320                ov.len(),
3321                h
3322            );
3323        }
3324    }
3325    println!();
3326    let findings = rsleigh_decompile::analysis::scan_section_anomalies(&sec_list, overlay);
3327    if findings.is_empty() {
3328        println!("No anomalies.");
3329    } else {
3330        println!("Findings:");
3331        for f in &findings {
3332            println!("  [{}] {} — {}", f.severity, f.function, f.description);
3333        }
3334    }
3335}
3336
3337fn run_vulnscan(binary_path: &str, data: &[u8]) {
3338    let obj = match goblin::Object::parse(data) {
3339        Ok(o) => o,
3340        Err(e) => {
3341            eprintln!("Error: {}", e);
3342            return;
3343        }
3344    };
3345    let (arch, segs, mut symbols) = match parse_binary(&obj, data) {
3346        Some(r) => r,
3347        None => {
3348            eprintln!("Unsupported");
3349            return;
3350        }
3351    };
3352    if symbols.is_empty() {
3353        if let goblin::Object::PE(pe) = &obj {
3354            let base = pe.image_base as u64;
3355            let entry = base
3356                + pe.header
3357                    .optional_header
3358                    .unwrap()
3359                    .standard_fields
3360                    .address_of_entry_point as u64;
3361            symbols = discover_pe_functions(entry, &segs, data, arch);
3362        }
3363    }
3364    let is_elf_stripped = if let goblin::Object::Elf(elf) = &obj {
3365        elf.syms.len() == 0
3366    } else {
3367        false
3368    };
3369    if is_elf_stripped {
3370        if let goblin::Object::Elf(elf) = &obj {
3371            let discovered = discover_elf_functions(elf, &segs, data, arch);
3372            let existing: std::collections::BTreeSet<u64> =
3373                symbols.iter().map(|(a, _)| *a).collect();
3374            for (addr, name) in discovered {
3375                if !existing.contains(&addr) {
3376                    symbols.push((addr, name));
3377                }
3378            }
3379        }
3380    }
3381
3382    let path = std::path::Path::new(binary_path);
3383    let mut dec = rsleigh_api::Decoder::new(arch);
3384
3385    // Vulnerability patterns: (pattern_in_pseudocode, severity, description)
3386    let vuln_patterns: &[(&str, &str, &str)] = &[
3387        // Buffer overflows
3388        (
3389            "gets(",
3390            "HIGH",
3391            "buffer overflow: gets() has no bounds check",
3392        ),
3393        (
3394            "strcpy(",
3395            "MED",
3396            "buffer overflow: strcpy() has no bounds check",
3397        ),
3398        (
3399            "strcat(",
3400            "MED",
3401            "buffer overflow: strcat() has no bounds check",
3402        ),
3403        (
3404            "sprintf(",
3405            "MED",
3406            "buffer overflow/format string: sprintf() no bounds check",
3407        ),
3408        (
3409            "vsprintf(",
3410            "MED",
3411            "buffer overflow/format string: vsprintf()",
3412        ),
3413        // Format strings
3414        (
3415            "printf(param_",
3416            "HIGH",
3417            "format string: printf() with user-controlled format",
3418        ),
3419        (
3420            "printf(local_",
3421            "HIGH",
3422            "format string: printf() with stack variable format",
3423        ),
3424        (
3425            "fprintf(param_",
3426            "HIGH",
3427            "format string: fprintf() with user-controlled format",
3428        ),
3429        (
3430            "syslog(param_",
3431            "MED",
3432            "format string: syslog() with user-controlled format",
3433        ),
3434        // Command injection
3435        (
3436            "system(param_",
3437            "CRIT",
3438            "command injection: system() with user-controlled argument",
3439        ),
3440        (
3441            "system(local_",
3442            "HIGH",
3443            "command injection: system() with stack variable",
3444        ),
3445        (
3446            "popen(param_",
3447            "CRIT",
3448            "command injection: popen() with user-controlled argument",
3449        ),
3450        (
3451            "exec(param_",
3452            "CRIT",
3453            "command execution: exec() with user-controlled argument",
3454        ),
3455        ("ShellExecute", "MED", "command execution: ShellExecute()"),
3456        ("WinExec(", "MED", "command execution: WinExec()"),
3457        ("CreateProcess", "MED", "process creation: CreateProcess()"),
3458        // Memory issues
3459        (
3460            "free(",
3461            "LOW",
3462            "potential use-after-free: check if pointer used after free()",
3463        ),
3464        ("VirtualAlloc(", "LOW", "executable memory allocation"),
3465        (
3466            "VirtualProtect(",
3467            "MED",
3468            "memory protection change (DEP bypass)",
3469        ),
3470        ("mmap(", "LOW", "memory mapping"),
3471        // Integer issues
3472        (
3473            "malloc(param_",
3474            "MED",
3475            "unchecked allocation: malloc() with user-controlled size",
3476        ),
3477        (
3478            "realloc(param_",
3479            "MED",
3480            "unchecked reallocation with user-controlled size",
3481        ),
3482        // Crypto issues
3483        (
3484            "rand()",
3485            "LOW",
3486            "weak randomness: rand() is not cryptographically secure",
3487        ),
3488        ("srand(", "LOW", "weak randomness: srand() seed"),
3489        // Info disclosure
3490        (
3491            "GetProcAddress(",
3492            "LOW",
3493            "dynamic API resolution (anti-analysis)",
3494        ),
3495        ("LoadLibrary", "LOW", "dynamic library loading"),
3496        // SQL injection
3497        (
3498            "sqlite3_exec(",
3499            "MED",
3500            "potential SQL injection if query contains user input",
3501        ),
3502        ("mysql_query(", "MED", "potential SQL injection"),
3503    ];
3504
3505    eprintln!(
3506        "Scanning {} functions for vulnerability patterns...",
3507        symbols.len()
3508    );
3509    let mut findings: Vec<(String, u64, String, String, String)> = Vec::new(); // (severity, addr, name, vuln, context)
3510
3511    for (func_addr, func_name) in &symbols {
3512        let off = segs.iter().find_map(|(va, sz, fo)| {
3513            if *func_addr >= *va && *func_addr < va + sz {
3514                Some(fo + (func_addr - va))
3515            } else {
3516                None
3517            }
3518        });
3519        let Some(off) = off else { continue };
3520        let max = 4096.min(data.len().saturating_sub(off as usize));
3521        if max < 2 {
3522            continue;
3523        }
3524
3525        let insts = decode_func(*func_addr, &symbols, &segs, data, &mut dec);
3526        if insts.is_empty() {
3527            continue;
3528        }
3529
3530        let output = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3531            rsleigh_decompile::decompile_with_binary(arch, &insts, Some(data), Some(path))
3532        })) {
3533            Ok(o) => o,
3534            Err(_) => continue,
3535        };
3536
3537        for &(pattern, severity, description) in vuln_patterns {
3538            if output.contains(pattern) {
3539                let context = output
3540                    .lines()
3541                    .find(|l| l.contains(pattern))
3542                    .unwrap_or("")
3543                    .trim()
3544                    .to_string();
3545                let context = if context.len() > 70 {
3546                    format!("{}...", &context[..70])
3547                } else {
3548                    context
3549                };
3550                findings.push((
3551                    severity.to_string(),
3552                    *func_addr,
3553                    func_name.clone(),
3554                    description.to_string(),
3555                    context,
3556                ));
3557            }
3558        }
3559
3560        // Special: check for missing stack cookie in large functions
3561        let has_cookie = output.contains("stack cookie")
3562            || output.contains("__security_check_cookie")
3563            || output.contains("__stack_chk_fail");
3564        let line_count = output.lines().filter(|l| !l.trim().is_empty()).count();
3565        if line_count > 20 && !has_cookie {
3566            findings.push((
3567                "INFO".to_string(),
3568                *func_addr,
3569                func_name.clone(),
3570                "missing stack cookie in large function".to_string(),
3571                String::new(),
3572            ));
3573        }
3574    }
3575
3576    // Section-level anomaly scan: entropy (packed/encrypted) + PE overlay
3577    let mut sec_list: Vec<(String, &[u8], u64)> = Vec::new();
3578    let mut overlay: Option<&[u8]> = None;
3579    match &obj {
3580        goblin::Object::PE(pe) => {
3581            let mut end_fo: usize = 0;
3582            for sec in &pe.sections {
3583                let name = String::from_utf8_lossy(&sec.name)
3584                    .trim_end_matches('\0')
3585                    .to_string();
3586                let fo = sec.pointer_to_raw_data as usize;
3587                let sz = sec.size_of_raw_data as usize;
3588                if fo == 0 || sz == 0 {
3589                    continue;
3590                }
3591                let end = fo.saturating_add(sz).min(data.len());
3592                if fo < end {
3593                    sec_list.push((
3594                        name,
3595                        &data[fo..end],
3596                        sec.virtual_address as u64 + pe.image_base as u64,
3597                    ));
3598                    if end > end_fo {
3599                        end_fo = end;
3600                    }
3601                }
3602            }
3603            if end_fo < data.len() {
3604                overlay = Some(&data[end_fo..]);
3605            }
3606        }
3607        goblin::Object::Elf(elf) => {
3608            for sh in &elf.section_headers {
3609                if sh.sh_type != goblin::elf::section_header::SHT_PROGBITS {
3610                    continue;
3611                }
3612                let fo = sh.sh_offset as usize;
3613                let sz = sh.sh_size as usize;
3614                if sz == 0 {
3615                    continue;
3616                }
3617                let end = fo.saturating_add(sz).min(data.len());
3618                let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("").to_string();
3619                if fo < end {
3620                    sec_list.push((name, &data[fo..end], sh.sh_addr));
3621                }
3622            }
3623        }
3624        goblin::Object::Mach(goblin::mach::Mach::Binary(m)) => {
3625            for seg in &m.segments {
3626                for sec_result in seg {
3627                    if let Ok((sec, sec_data)) = sec_result {
3628                        let name = sec.name().unwrap_or("").to_string();
3629                        if !sec_data.is_empty() {
3630                            sec_list.push((name, sec_data, sec.addr));
3631                        }
3632                    }
3633                }
3634            }
3635        }
3636        _ => {}
3637    }
3638    let sec_findings = rsleigh_decompile::analysis::scan_section_anomalies(&sec_list, overlay);
3639    for f in sec_findings {
3640        findings.push((f.severity, f.address, f.function, f.description, f.context));
3641    }
3642
3643    // Sort by severity
3644    let severity_order = |s: &str| match s {
3645        "CRIT" => 0,
3646        "HIGH" => 1,
3647        "MED" => 2,
3648        "LOW" => 3,
3649        _ => 4,
3650    };
3651    findings.sort_by(|a, b| severity_order(&a.0).cmp(&severity_order(&b.0)));
3652
3653    // Output
3654    println!(
3655        "=== Vulnerability Scan: {} ({} functions) ===",
3656        binary_path,
3657        symbols.len()
3658    );
3659    println!();
3660    let crit = findings.iter().filter(|f| f.0 == "CRIT").count();
3661    let high = findings.iter().filter(|f| f.0 == "HIGH").count();
3662    let med = findings.iter().filter(|f| f.0 == "MED").count();
3663    let low = findings.iter().filter(|f| f.0 == "LOW").count();
3664    println!(
3665        "Summary: {} CRIT, {} HIGH, {} MED, {} LOW ({} total findings)",
3666        crit,
3667        high,
3668        med,
3669        low,
3670        findings.len()
3671    );
3672    println!();
3673    for (severity, addr, name, vuln, context) in &findings {
3674        let color = match severity.as_str() {
3675            "CRIT" => "\x1b[91m",
3676            "HIGH" => "\x1b[31m",
3677            "MED" => "\x1b[33m",
3678            "LOW" => "\x1b[36m",
3679            _ => "\x1b[37m",
3680        };
3681        println!(
3682            "  {}{:<4}\x1b[0m  0x{:012x}  {:<25} {}",
3683            color, severity, addr, name, vuln
3684        );
3685        if !context.is_empty() {
3686            println!("        {}", context);
3687        }
3688    }
3689}
3690
3691/// Extract indicators of compromise (IOCs) from a binary's strings.
3692///
3693/// Pulls ASCII and UTF-16LE strings out of the raw image and bins each
3694/// match into a category that a triage workflow actually wants:
3695///   * URLs (http/https/ftp)
3696///   * IPv4 literals (with octet validation)
3697///   * Domain names (TLD-anchored, length-bounded)
3698///   * Filesystem paths (Win drive letters, %ENVVAR%, /tmp /etc /var /usr)
3699///   * Registry keys (HKEY_*, HKLM\, HKCU\)
3700///   * Windows mutex / kernel object names (Global\, Local\, Session\)
3701///   * Credential/secret-related keywords (token=, password=, api_key=, ...)
3702///
3703/// The point is to give an analyst a one-pager of "where does this binary
3704/// reach out to and what does it touch" without making them grep the strings
3705/// dump by hand. Output is grouped by category, deduped, and sorted.
3706/// Brute single-byte-XOR string recovery. Walks every key 1..=255,
3707/// reports decoded runs of length >= 8 whose source bytes are
3708/// mostly non-printable (so plaintext doesn't dominate output).
3709fn run_xor_strings(binary_path: &str, data: &[u8], json: bool) {
3710    let hits = rsleigh_decompile::xor_strings::brute_decode(data, 8);
3711    if json {
3712        let payload = serde_json::json!({
3713            "binary": binary_path,
3714            "decoded": hits.iter().map(|d| serde_json::json!({
3715                "key":    format!("0x{}", d.key_hex()),
3716                "offset": format!("0x{:x}", d.offset),
3717                "text":   d.text,
3718            })).collect::<Vec<_>>(),
3719        });
3720        println!("{}", serde_json::to_string_pretty(&payload).unwrap());
3721        return;
3722    }
3723    println!("=== XOR-decoded strings from {} ===", binary_path);
3724    if hits.is_empty() {
3725        println!("\n(no decoded strings)");
3726        return;
3727    }
3728    let mut by_key: std::collections::BTreeMap<
3729        Vec<u8>,
3730        Vec<&rsleigh_decompile::xor_strings::Decoded>,
3731    > = std::collections::BTreeMap::new();
3732    for h in &hits {
3733        by_key.entry(h.key.clone()).or_default().push(h);
3734    }
3735
3736    // Brute scan generates many qualifying runs on real binaries.
3737    // For triage, surface the top-5 keys by hit-count (real obfuscated
3738    // tables produce concentrated decode under one key) and cap
3739    // per-key output at 30 lines. Multi-byte keys (Mirai-class) ALWAYS
3740    // print regardless of rank — they're rare hits and very high signal.
3741    let multi_byte: Vec<_> = by_key.iter().filter(|(k, _)| k.len() > 1).collect();
3742    let mut single_byte_ranked: Vec<_> = by_key.iter().filter(|(k, _)| k.len() == 1).collect();
3743    single_byte_ranked.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
3744    let top_single: Vec<_> = single_byte_ranked.into_iter().take(5).collect();
3745
3746    let total_hits = hits.len();
3747    let total_keys = by_key.len();
3748    println!(
3749        "\n{} run(s) across {} key(s); showing all {} multi-byte keys + top 5 single-byte by hit-count.",
3750        total_hits, total_keys, multi_byte.len()
3751    );
3752    let print_group = |key: &[u8], group: &[&rsleigh_decompile::xor_strings::Decoded]| {
3753        let kh: String = key.iter().map(|b| format!("{:02x}", b)).collect();
3754        println!("\nKey 0x{} ({} hit(s)):", kh, group.len());
3755        for d in group.iter().take(30) {
3756            println!("  +{:08x}  {}", d.offset, d.text);
3757        }
3758        if group.len() > 30 {
3759            println!("  ... and {} more", group.len() - 30);
3760        }
3761    };
3762    for (key, group) in &multi_byte {
3763        print_group(key, group);
3764    }
3765    for (key, group) in &top_single {
3766        print_group(key, group);
3767    }
3768}
3769
3770fn run_ioc(binary_path: &str, data: &[u8], json: bool) {
3771    use std::collections::BTreeSet;
3772
3773    // Pull strings: ASCII (>=6 chars) + UTF-16LE (>=4 chars). Window size is
3774    // generous because installer binaries embed long config blobs. Caps
3775    // prevent pathological hits in compressed/encrypted regions.
3776    let mut texts: Vec<String> = Vec::new();
3777    let mut run = Vec::with_capacity(64);
3778    for &b in data.iter() {
3779        if (0x20..0x7f).contains(&b) || b == b'\t' {
3780            run.push(b);
3781        } else {
3782            if run.len() >= 6 {
3783                if let Ok(s) = std::str::from_utf8(&run) {
3784                    texts.push(s.to_string());
3785                }
3786            }
3787            run.clear();
3788        }
3789    }
3790    if run.len() >= 6 {
3791        if let Ok(s) = std::str::from_utf8(&run) {
3792            texts.push(s.to_string());
3793        }
3794    }
3795    // UTF-16LE pass: read pairs (b, 0x00) of printable bytes.
3796    let mut wide_run: Vec<u8> = Vec::with_capacity(64);
3797    let mut i = 0usize;
3798    while i + 1 < data.len() {
3799        let lo = data[i];
3800        let hi = data[i + 1];
3801        if hi == 0 && ((0x20..0x7f).contains(&lo) || lo == b'\t') {
3802            wide_run.push(lo);
3803            i += 2;
3804        } else {
3805            if wide_run.len() >= 4 {
3806                if let Ok(s) = std::str::from_utf8(&wide_run) {
3807                    texts.push(s.to_string());
3808                }
3809            }
3810            wide_run.clear();
3811            i += 1;
3812        }
3813    }
3814    if wide_run.len() >= 4 {
3815        if let Ok(s) = std::str::from_utf8(&wide_run) {
3816            texts.push(s.to_string());
3817        }
3818    }
3819
3820    let mut urls: BTreeSet<String> = BTreeSet::new();
3821    let mut ips: BTreeSet<String> = BTreeSet::new();
3822    let mut domains: BTreeSet<String> = BTreeSet::new();
3823    let mut paths: BTreeSet<String> = BTreeSet::new();
3824    let mut registry: BTreeSet<String> = BTreeSet::new();
3825    let mut mutexes: BTreeSet<String> = BTreeSet::new();
3826    let mut secrets: BTreeSet<String> = BTreeSet::new();
3827
3828    // TLDs that show up in real malware C2 — short list keeps domain
3829    // detection from matching every "foo.exe" / "bar.dll" string. Add to
3830    // taste; staying conservative beats false positives.
3831    const TLDS: &[&str] = &[
3832        "com", "net", "org", "io", "co", "ru", "cn", "tk", "ml", "ga", "cf",
3833        "xyz", "info", "biz", "top", "online", "site", "pro", "dev", "app",
3834        "gov", "edu", "mil", "uk", "de", "fr", "jp", "br", "in", "us",
3835        "ws", "to", "cc", "su", "icu", "club", "host", "live", "fun", "shop",
3836    ];
3837    let known_tld = |label: &str| TLDS.iter().any(|t| label.eq_ignore_ascii_case(t));
3838
3839    let valid_ipv4 = |s: &str| {
3840        let parts: Vec<&str> = s.split('.').collect();
3841        if parts.len() != 4 {
3842            return false;
3843        }
3844        let octets: Vec<u16> = match parts
3845            .iter()
3846            .map(|p| {
3847                if p.is_empty() || p.len() > 3 || !p.bytes().all(|b| b.is_ascii_digit()) {
3848                    return None;
3849                }
3850                p.parse::<u16>().ok().filter(|n| *n <= 255)
3851            })
3852            .collect::<Option<Vec<_>>>()
3853        {
3854            Some(o) => o,
3855            None => return false,
3856        };
3857        // .NET / file-format versions encode as W.X.Y.Z and routinely have
3858        // multiple zero octets ("4.0.0.0", "3.5.0.0", "2.0.0.0"). Real
3859        // routable IPs almost never have 2+ zero octets — public address
3860        // space won't have a trailing .0 host octet, and netblocks don't
3861        // collapse to .0.0 except as network addresses (still not IOCs).
3862        // Reject when 2 or more octets are zero.
3863        let zeros = octets.iter().filter(|&&n| n == 0).count();
3864        if zeros >= 2 {
3865            return false;
3866        }
3867        if octets.iter().all(|&n| n == 255) {
3868            return false;
3869        }
3870        true
3871    };
3872
3873    let push_domain_if_useful = |domains: &mut BTreeSet<String>, host: &str| {
3874        // Reject obvious file refs and intra-binary noise.
3875        let lower = host.to_ascii_lowercase();
3876        if lower.ends_with(".dll") || lower.ends_with(".exe") || lower.ends_with(".sys")
3877            || lower.ends_with(".pdb") || lower.ends_with(".cpp") || lower.ends_with(".c")
3878            || lower.ends_with(".h") || lower.ends_with(".obj") || lower.ends_with(".lib")
3879            || lower.ends_with(".cs") || lower.ends_with(".vb") || lower.ends_with(".rs")
3880            || lower.ends_with(".py") || lower.ends_with(".js") || lower.ends_with(".ts")
3881            || lower.ends_with(".cab") || lower.ends_with(".msi") || lower.ends_with(".log")
3882            || lower.ends_with(".tmp") || lower.ends_with(".dat") || lower.ends_with(".ini")
3883            || lower.ends_with(".xml") || lower.ends_with(".json") || lower.ends_with(".txt")
3884            || lower.ends_with(".crl") || lower.ends_with(".crt") || lower.ends_with(".cer")
3885            || lower.ends_with(".bak")
3886        {
3887            return;
3888        }
3889        // Domain must have at least one fully-lowercase label (real domains
3890        // are case-insensitive but always written lowercase in malware
3891        // configs). PascalCase identifiers like "System.IO" or
3892        // "MyApplication.app" are CLR namespaces / bundle IDs, not hosts.
3893        if !lower.bytes().any(|b| b.is_ascii_lowercase()) {
3894            return;
3895        }
3896        if !host.split('.').all(|label| {
3897            !label.is_empty() && label.bytes().any(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
3898        }) {
3899            return;
3900        }
3901        // Reject tokens that contain `/` or `\` — that's a Go import path
3902        // ("os/exec.in"), a file path, or other non-host noise.
3903        if host.contains('/') || host.contains('\\') || host.contains(':') {
3904            return;
3905        }
3906        // Reject namespace-like tokens with PascalCase labels.
3907        let pascal_labels = host
3908            .split('.')
3909            .filter(|l| {
3910                l.chars().next().map(|c| c.is_ascii_uppercase()).unwrap_or(false)
3911                    && l.bytes().any(|b| b.is_ascii_lowercase())
3912            })
3913            .count();
3914        if pascal_labels > 0 {
3915            return;
3916        }
3917        if let Some(last_dot) = host.rfind('.') {
3918            let tld = &host[last_dot + 1..];
3919            if known_tld(tld) && host.len() >= 5 && host.len() <= 253 {
3920                domains.insert(host.to_string());
3921            }
3922        }
3923    };
3924
3925    for s in &texts {
3926        // URLs
3927        let mut idx = 0;
3928        let bytes = s.as_bytes();
3929        while idx < bytes.len() {
3930            let rest = &s[idx..];
3931            let url_start = ["http://", "https://", "ftp://"]
3932                .iter()
3933                .filter_map(|p| rest.find(p).map(|n| (n, p.len())))
3934                .min_by_key(|(n, _)| *n);
3935            let Some((rel, plen)) = url_start else { break };
3936            let abs = idx + rel;
3937            let scheme_end = abs + plen;
3938            // Read URL chars until whitespace, control, or " ' < > )
3939            let url_end = s[scheme_end..]
3940                .find(|c: char| {
3941                    c.is_whitespace() || c.is_control()
3942                        || matches!(c, '"' | '\'' | '<' | '>' | ')' | '(' | '`')
3943                })
3944                .map(|n| scheme_end + n)
3945                .unwrap_or(s.len());
3946            if url_end > scheme_end && url_end - abs >= 10 && url_end - abs <= 2048 {
3947                let mut u = s[abs..url_end]
3948                    .trim_end_matches(|c: char| matches!(c, '.' | ',' | ';' | ':' | '/' | ']' | '}'))
3949                    .to_string();
3950                // PE security-directory PKCS7 blobs often immediately follow
3951                // an embedded URL with one or two ASN.1 DER tag bytes (e.g.
3952                // 0x30 0x45 = "0E", 0x30 0x53 = "0S"), which our character
3953                // set treats as continuation. Strip a trailing 1-2 alnum
3954                // tail when the URL ends in a known cert file extension
3955                // suffixed with that tail.
3956                for ext in &["crl", "crt", "cer", "axd", "p7s", "p7b"] {
3957                    let needle = format!(".{}", ext);
3958                    if let Some(idx) = u.to_ascii_lowercase().rfind(&needle) {
3959                        let end = idx + needle.len();
3960                        let tail = &u[end..];
3961                        if !tail.is_empty()
3962                            && tail.len() <= 2
3963                            && tail.bytes().all(|b| b.is_ascii_alphanumeric())
3964                        {
3965                            u.truncate(end);
3966                            break;
3967                        }
3968                    }
3969                }
3970                // Bare-host URL with no path (http://host[junk]) — trim a
3971                // trailing 1-2 char tail of alnum or backslash that the
3972                // PE security blob's DER tags routinely append.
3973                if let Some(scheme_idx) = u.find("://") {
3974                    let host_start = scheme_idx + 3;
3975                    if !u[host_start..].contains('/') && !u[host_start..].contains('?') {
3976                        // Only trim if the host body has a digit-or-letter
3977                        // tail of length 1-2 after the last '.'.
3978                        if let Some(last_dot) = u[host_start..].rfind('.') {
3979                            let abs_dot = host_start + last_dot;
3980                            let after_dot = &u[abs_dot + 1..];
3981                            // TLD is the part after last dot; if there's a
3982                            // tail of >=3 chars where the last 1-2 are
3983                            // single-uppercase or backslash and the prefix
3984                            // looks like a TLD, strip the tail.
3985                            if after_dot.len() >= 3 {
3986                                let tail_start = after_dot
3987                                    .bytes()
3988                                    .position(|b| b == b'\\' || b.is_ascii_uppercase() || b.is_ascii_digit())
3989                                    .unwrap_or(after_dot.len());
3990                                let probable_tld_len = tail_start;
3991                                let tail_len = after_dot.len() - tail_start;
3992                                if probable_tld_len >= 2
3993                                    && tail_len >= 1
3994                                    && tail_len <= 2
3995                                    && after_dot[..probable_tld_len]
3996                                        .bytes()
3997                                        .all(|b| b.is_ascii_lowercase())
3998                                    && known_tld(&after_dot[..probable_tld_len])
3999                                {
4000                                    u.truncate(abs_dot + 1 + probable_tld_len);
4001                                }
4002                            }
4003                        }
4004                    }
4005                }
4006                if u.len() >= 10 {
4007                    urls.insert(u);
4008                }
4009            }
4010            idx = url_end.max(abs + 1);
4011        }
4012
4013        // IPv4 + domain extraction over tokens
4014        for tok in s.split(|c: char| {
4015            !(c.is_alphanumeric() || matches!(c, '.' | '-' | '_' | '\\' | '/' | ':' | '%' | '$'))
4016        }) {
4017            if tok.len() < 4 || tok.len() > 256 {
4018                continue;
4019            }
4020            // IPv4 (allow optional :port suffix)
4021            let (host_part, _) = tok.split_once(':').unwrap_or((tok, ""));
4022            if valid_ipv4(host_part) {
4023                // Reject 0.0.0.0 / 127.0.0.1 obvious-noise filters off — they
4024                // can still be IOC-relevant (e.g. localhost C2 in dev malware).
4025                ips.insert(host_part.to_string());
4026            } else if host_part.contains('.') {
4027                push_domain_if_useful(&mut domains, host_part);
4028            }
4029        }
4030
4031        // File paths. Each candidate must consist of path-valid characters
4032        // throughout, otherwise we end up emitting random binary tokens like
4033        // `%GYK%+caWN,\` or `p:/H(k6` that just happen to start with an
4034        // env-var or drive-letter prefix.
4035        let path_valid = |s: &str| {
4036            s.chars().all(|c| {
4037                c.is_ascii_alphanumeric()
4038                    || matches!(c, '\\' | '/' | '.' | '_' | '-' | ' ' | '%' | '(' | ')'
4039                                 | '$' | '~' | '+' | ':' | '@' | '#' | '{' | '}')
4040            })
4041        };
4042        for tok in s.split_whitespace() {
4043            if tok.len() < 5 || tok.len() > 260 {
4044                continue;
4045            }
4046            // Windows drive-letter paths. Drive letter is conventionally
4047            // uppercase; lowercase first char is almost always a token-split
4048            // artifact ("p:/" out of `cpp:/foo` etc.).
4049            if tok.len() >= 4
4050                && tok.as_bytes()[1] == b':'
4051                && (tok.as_bytes()[2] == b'\\' || tok.as_bytes()[2] == b'/')
4052                && tok.as_bytes()[0].is_ascii_uppercase()
4053                && path_valid(tok)
4054            {
4055                paths.insert(tok.to_string());
4056            }
4057            // Env-var paths. Reject printf-style format strings:
4058            // %s, %d, %lu, %ls, %u, %x, %.3f, %02d etc. carry no IOC value.
4059            // The variable name between the two % must be valid env-var
4060            // syntax (uppercase letters/digits/underscore) AND the remainder
4061            // of the token must be path-valid.
4062            else if tok.starts_with('%')
4063                && tok.len() >= 5
4064                && tok.find('%').and_then(|first| {
4065                    tok[first + 1..].find('%').map(|rel| {
4066                        let inner = &tok[first + 1..first + 1 + rel];
4067                        !inner.is_empty()
4068                            && inner.len() >= 2
4069                            && inner.len() <= 32
4070                            && inner.bytes().all(|b| {
4071                                b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'_'
4072                            })
4073                    })
4074                }).unwrap_or(false)
4075                && path_valid(tok)
4076            {
4077                paths.insert(tok.to_string());
4078            }
4079            // Unix-ish absolute paths into common locations
4080            else if (tok.starts_with("/tmp/") || tok.starts_with("/var/")
4081                || tok.starts_with("/etc/") || tok.starts_with("/usr/")
4082                || tok.starts_with("/home/") || tok.starts_with("/root/")
4083                || tok.starts_with("/dev/") || tok.starts_with("/proc/"))
4084                && path_valid(tok)
4085            {
4086                paths.insert(tok.to_string());
4087            }
4088        }
4089
4090        // Registry keys — match "HKEY_*", "HKLM\", "HKCU\" prefix and grab
4091        // the rest of the alphabetic / backslash run.
4092        for prefix in &[
4093            "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER", "HKEY_CLASSES_ROOT",
4094            "HKEY_USERS", "HKEY_CURRENT_CONFIG", "HKLM\\", "HKCU\\", "HKCR\\",
4095        ] {
4096            let mut start = 0;
4097            while let Some(rel) = s[start..].find(prefix) {
4098                let abs = start + rel;
4099                let end = s[abs..]
4100                    .find(|c: char| {
4101                        !(c.is_alphanumeric()
4102                            || matches!(c, '\\' | '_' | '-' | '.' | '{' | '}' | '/' | ' '))
4103                    })
4104                    .map(|n| abs + n)
4105                    .unwrap_or(s.len());
4106                let key = s[abs..end].trim_end();
4107                if key.len() >= prefix.len() + 2 && key.len() <= 256 {
4108                    registry.insert(key.to_string());
4109                }
4110                start = end.max(abs + 1);
4111            }
4112        }
4113
4114        // Mutex / named-object paths.
4115        for prefix in &["Global\\", "Local\\", "Session\\", "BaseNamedObjects\\"] {
4116            let mut start = 0;
4117            while let Some(rel) = s[start..].find(prefix) {
4118                let abs = start + rel;
4119                let end = s[abs..]
4120                    .find(|c: char| c.is_whitespace() || matches!(c, '"' | '\'' | '\0'))
4121                    .map(|n| abs + n)
4122                    .unwrap_or(s.len());
4123                let m = s[abs..end].trim();
4124                if m.len() >= prefix.len() + 2 && m.len() <= 200 {
4125                    mutexes.insert(m.to_string());
4126                }
4127                start = end.max(abs + 1);
4128            }
4129        }
4130
4131        // Credential / secret-ish keywords, embedded in config-like strings.
4132        // Reject .NET assembly identity strings whose `PublicKeyToken=` /
4133        // `Culture=neutral, ...` syntax otherwise produces a flood of
4134        // not-really-secrets in any managed binary.
4135        let lower = s.to_ascii_lowercase();
4136        let is_dotnet_identity = lower.contains("publickeytoken=")
4137            || lower.contains("culture=neutral")
4138            || lower.contains(", version=") && lower.contains(", culture=");
4139        // Skip very long blob-like strings (terminfo databases, embedded
4140        // resource catalogs) that catch unrelated env-var fragments.
4141        if !is_dotnet_identity && s.len() <= 256 {
4142            for needle in &[
4143                "password=", "passwd=",
4144                "api_key=", "apikey=", "api-key=",
4145                "access_token=", "auth_token=", "auth-token=", "bearer_token=",
4146                "bearer ", "client_secret=", "client-secret=",
4147                "private_key=", "ssh-rsa ", "-----BEGIN PRIVATE",
4148            ] {
4149                if lower.contains(needle) {
4150                    secrets.insert(s.trim().to_string());
4151                    break;
4152                }
4153            }
4154        }
4155    }
4156
4157    if json {
4158        let payload = serde_json::json!({
4159            "binary": binary_path,
4160            "urls":     urls.iter().collect::<Vec<_>>(),
4161            "ips":      ips.iter().collect::<Vec<_>>(),
4162            "domains":  domains.iter().collect::<Vec<_>>(),
4163            "paths":    paths.iter().collect::<Vec<_>>(),
4164            "registry": registry.iter().collect::<Vec<_>>(),
4165            "mutexes":  mutexes.iter().collect::<Vec<_>>(),
4166            "secrets":  secrets.iter().collect::<Vec<_>>(),
4167            "family": rsleigh_decompile::iot_family::classify_bytes(data)
4168                .map(|f| serde_json::json!({
4169                    "id": f.id,
4170                    "label": f.label,
4171                    "variant": f.variant,
4172                    "evidence": f.evidence,
4173                })),
4174            "capabilities": rsleigh_decompile::iot_capabilities::classify_bytes(data)
4175                .iter()
4176                .map(|c| serde_json::json!({
4177                    "id": c.id,
4178                    "label": c.label,
4179                    "evidence": c.evidence,
4180                }))
4181                .collect::<Vec<_>>(),
4182        });
4183        println!("{}", serde_json::to_string_pretty(&payload).unwrap());
4184        return;
4185    }
4186
4187    println!("=== IOCs from {} ===", binary_path);
4188    let print_section = |label: &str, set: &BTreeSet<String>| {
4189        if set.is_empty() {
4190            return;
4191        }
4192        println!("\n{} ({})", label, set.len());
4193        for v in set {
4194            println!("  {}", v);
4195        }
4196    };
4197    print_section("URLs", &urls);
4198    print_section("IPv4", &ips);
4199    print_section("Domains", &domains);
4200    print_section("Paths", &paths);
4201    print_section("Registry", &registry);
4202    print_section("Mutexes/Named Objects", &mutexes);
4203    print_section("Secret-like strings", &secrets);
4204
4205    if let Some(fam) = rsleigh_decompile::iot_family::classify_bytes(data) {
4206        let variant = fam
4207            .variant
4208            .as_ref()
4209            .map(|v| format!(" (variant: {})", v))
4210            .unwrap_or_default();
4211        println!("\nFamily: [{}] {}{}", fam.id, fam.label, variant);
4212        for ev in &fam.evidence {
4213            println!("  evidence: {}", ev);
4214        }
4215    }
4216
4217    let caps = rsleigh_decompile::iot_capabilities::classify_bytes(data);
4218    if !caps.is_empty() {
4219        println!("\nCapabilities ({})", caps.len());
4220        for c in &caps {
4221            println!("  [{}] {}", c.id, c.label);
4222            for ev in &c.evidence {
4223                println!("      - {}", ev);
4224            }
4225        }
4226    }
4227
4228    let total = urls.len() + ips.len() + domains.len() + paths.len()
4229        + registry.len() + mutexes.len() + secrets.len();
4230    if total == 0 && caps.is_empty() {
4231        println!("\n(no IOCs found)");
4232    } else {
4233        println!("\nTotal: {} indicators, {} capabilities", total, caps.len());
4234    }
4235}
4236
4237/// Parse the PE Authenticode signature embedded in the Security data
4238/// directory and surface the parts a triage analyst actually wants.
4239///
4240/// Authenticode signatures live in a WIN_CERTIFICATE structure pointed to
4241/// by the optional header's data directory entry #4. The certificate
4242/// itself is a PKCS#7 SignedData blob (BER-encoded). Rather than pull in
4243/// a full ASN.1 / CMS crate, we walk the bytes and pattern-match three
4244/// well-known OID prefixes:
4245///
4246///   * 2.5.4.3 commonName  (06 03 55 04 03 ...) — every Subject/Issuer CN
4247///   * 1.2.840.113549.1.9.5 signingTime (06 09 2A 86 48 86 F7 0D 01 09 05)
4248///   * 1.2.840.113549.1.7.2 signedData (validates the blob shape)
4249///
4250/// The leaf signer is the FIRST commonName encountered after the
4251/// signedData OID — the certificates list is ordered leaf-first in
4252/// practice for code-signing use, with the rest of the chain following.
4253fn run_sigcheck(binary_path: &str, data: &[u8], json: bool) {
4254    let result = sigcheck_parse(data);
4255
4256    if json {
4257        let signed = result.is_some();
4258        let payload = match &result {
4259            Some(r) => serde_json::json!({
4260                "binary": binary_path,
4261                "signed": signed,
4262                "signer_cn": r.signer_cn.clone(),
4263                "issuer_cn": r.issuer_cn.clone(),
4264                "signing_time": r.signing_time.clone(),
4265                "timestamp_signer_cn": r.timestamp_signer_cn.clone(),
4266                "all_cns": r.all_cns.clone(),
4267                "cert_blob_size": r.cert_blob_size,
4268                "win_cert_revision": format!("0x{:04x}", r.revision),
4269                "win_cert_type": format!("0x{:04x}", r.cert_type),
4270            }),
4271            None => serde_json::json!({
4272                "binary": binary_path,
4273                "signed": false,
4274                "reason": "no PE Security directory entry, or directory was empty",
4275            }),
4276        };
4277        println!("{}", serde_json::to_string_pretty(&payload).unwrap());
4278        return;
4279    }
4280
4281    println!("=== Authenticode signature for {} ===", binary_path);
4282    let Some(r) = result else {
4283        println!("\nUNSIGNED — no PE Security directory entry, or directory was empty.");
4284        return;
4285    };
4286    println!(
4287        "\n  Cert blob size:  {} bytes  (revision 0x{:04x}, type 0x{:04x})",
4288        r.cert_blob_size, r.revision, r.cert_type
4289    );
4290    if let Some(cn) = &r.signer_cn {
4291        println!("  Signer CN:       {}", cn);
4292    } else {
4293        println!("  Signer CN:       (not extracted)");
4294    }
4295    if let Some(cn) = &r.issuer_cn {
4296        println!("  Issuer CN:       {}", cn);
4297    }
4298    if let Some(t) = &r.signing_time {
4299        println!("  Signing time:    {}", t);
4300    }
4301    if let Some(cn) = &r.timestamp_signer_cn {
4302        println!("  Timestamp by:    {}", cn);
4303    }
4304    if !r.all_cns.is_empty() {
4305        println!("\n  Cert chain CNs ({}):", r.all_cns.len());
4306        for (i, cn) in r.all_cns.iter().enumerate() {
4307            println!("    [{}] {}", i, cn);
4308        }
4309    }
4310}
4311
4312#[derive(Debug)]
4313struct SigInfo {
4314    cert_blob_size: usize,
4315    revision: u16,
4316    cert_type: u16,
4317    signer_cn: Option<String>,
4318    issuer_cn: Option<String>,
4319    signing_time: Option<String>,
4320    timestamp_signer_cn: Option<String>,
4321    all_cns: Vec<String>,
4322}
4323
4324fn sigcheck_parse(data: &[u8]) -> Option<SigInfo> {
4325    // PE32 / PE32+ both put the Security data directory at the same
4326    // offset relative to the optional header start (data dir entry 4 ×
4327    // 8 bytes per entry past the magic-dependent fixed-size area).
4328    if data.len() < 0x40 || &data[..2] != b"MZ" {
4329        return None;
4330    }
4331    let e_lfanew =
4332        u32::from_le_bytes(data[0x3C..0x40].try_into().ok()?) as usize;
4333    if e_lfanew + 24 > data.len() {
4334        return None;
4335    }
4336    if &data[e_lfanew..e_lfanew + 4] != b"PE\0\0" {
4337        return None;
4338    }
4339    let opt_off = e_lfanew + 24;
4340    let magic = u16::from_le_bytes(data[opt_off..opt_off + 2].try_into().ok()?);
4341    // Security data directory is RVA 4 in the data directories array.
4342    // Offsets are 0x80 (PE32) / 0x90 (PE32+) past opt_off — Microsoft
4343    // PE/COFF spec 6.4.2.
4344    let sec_dir_off = match magic {
4345        0x10b => opt_off + 0x80, // PE32
4346        0x20b => opt_off + 0x90, // PE32+
4347        _ => return None,
4348    };
4349    if sec_dir_off + 8 > data.len() {
4350        return None;
4351    }
4352    // The Security directory's Address is a FILE OFFSET, not an RVA —
4353    // documented quirk of PE format. dwLength is byte length.
4354    let cert_off =
4355        u32::from_le_bytes(data[sec_dir_off..sec_dir_off + 4].try_into().ok()?) as usize;
4356    let cert_size =
4357        u32::from_le_bytes(data[sec_dir_off + 4..sec_dir_off + 8].try_into().ok()?) as usize;
4358    if cert_off == 0 || cert_size == 0 || cert_off + cert_size > data.len() {
4359        return None;
4360    }
4361    let cert_blob = &data[cert_off..cert_off + cert_size];
4362    if cert_blob.len() < 8 {
4363        return None;
4364    }
4365    // WIN_CERTIFICATE = { dwLength, wRevision, wCertificateType, bCertificate[] }
4366    let dw_length = u32::from_le_bytes(cert_blob[0..4].try_into().ok()?) as usize;
4367    let revision = u16::from_le_bytes(cert_blob[4..6].try_into().ok()?);
4368    let cert_type = u16::from_le_bytes(cert_blob[6..8].try_into().ok()?);
4369    if dw_length < 8 || dw_length > cert_blob.len() {
4370        return None;
4371    }
4372    let pkcs7 = &cert_blob[8..dw_length];
4373
4374    // OIDs we care about (as DER-encoded prefixes including the 0x06 tag):
4375    //   2.5.4.3   commonName        06 03 55 04 03
4376    //   1.2.840.113549.1.9.5  signingTime
4377    //                                06 09 2A 86 48 86 F7 0D 01 09 05
4378    //   1.2.840.113549.1.9.6  counterSignature
4379    //                                06 09 2A 86 48 86 F7 0D 01 09 06
4380    //   1.2.840.113549.1.7.2  signedData
4381    //                                06 09 2A 86 48 86 F7 0D 01 07 02
4382    const CN_OID: &[u8] = &[0x06, 0x03, 0x55, 0x04, 0x03];
4383    const SIGNING_TIME_OID: &[u8] = &[
4384        0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x05,
4385    ];
4386    const COUNTER_SIG_OID: &[u8] = &[
4387        0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x06,
4388    ];
4389
4390    // Parse a DER length starting at `i`; returns (length, header_size).
4391    fn der_len(buf: &[u8], i: usize) -> Option<(usize, usize)> {
4392        if i >= buf.len() {
4393            return None;
4394        }
4395        let b0 = buf[i];
4396        if b0 < 0x80 {
4397            return Some((b0 as usize, 1));
4398        }
4399        let n = (b0 & 0x7f) as usize;
4400        if n == 0 || n > 4 || i + 1 + n > buf.len() {
4401            return None;
4402        }
4403        let mut len = 0usize;
4404        for k in 0..n {
4405            len = (len << 8) | buf[i + 1 + k] as usize;
4406        }
4407        Some((len, 1 + n))
4408    }
4409
4410    // Extract a DER string after the OID's value tag. Tag bytes that
4411    // hold human-readable text in this context: 0x13 PrintableString,
4412    // 0x0C UTF8String, 0x16 IA5String, 0x14 T61String, 0x1E BMPString
4413    // (UTF-16BE). Returns the decoded string + bytes consumed.
4414    fn read_der_string(buf: &[u8], i: usize) -> Option<(String, usize)> {
4415        if i >= buf.len() {
4416            return None;
4417        }
4418        let tag = buf[i];
4419        let (len, hdr) = der_len(buf, i + 1)?;
4420        let start = i + 1 + hdr;
4421        if start + len > buf.len() {
4422            return None;
4423        }
4424        let body = &buf[start..start + len];
4425        let s = match tag {
4426            0x13 | 0x0c | 0x16 | 0x14 => {
4427                std::str::from_utf8(body).ok().map(|s| s.to_string())
4428            }
4429            0x1e => {
4430                // BMPString = UTF-16BE
4431                if body.len() % 2 != 0 {
4432                    return None;
4433                }
4434                let mut u16s: Vec<u16> = Vec::with_capacity(body.len() / 2);
4435                for chunk in body.chunks_exact(2) {
4436                    u16s.push(u16::from_be_bytes([chunk[0], chunk[1]]));
4437                }
4438                String::from_utf16(&u16s).ok()
4439            }
4440            _ => None,
4441        }?;
4442        Some((s, 1 + hdr + len))
4443    }
4444
4445    fn read_der_time(buf: &[u8], i: usize) -> Option<String> {
4446        if i >= buf.len() {
4447            return None;
4448        }
4449        let tag = buf[i];
4450        let (len, hdr) = der_len(buf, i + 1)?;
4451        let start = i + 1 + hdr;
4452        if start + len > buf.len() {
4453            return None;
4454        }
4455        let body = std::str::from_utf8(&buf[start..start + len]).ok()?;
4456        match tag {
4457            0x17 => {
4458                // UTCTime: YYMMDDHHMMSSZ — render as 20YY-MM-DD HH:MM:SS UTC
4459                if body.len() < 11 {
4460                    return None;
4461                }
4462                let yy: u32 = body[0..2].parse().ok()?;
4463                let yyyy = if yy < 50 { 2000 + yy } else { 1900 + yy };
4464                Some(format!(
4465                    "{:04}-{}-{} {}:{}:{}{}UTC",
4466                    yyyy,
4467                    &body[2..4],
4468                    &body[4..6],
4469                    &body[6..8],
4470                    &body[8..10],
4471                    &body[10..body.len().saturating_sub(1).min(12)],
4472                    if body.len() >= 13 { " " } else { "" }
4473                ))
4474            }
4475            0x18 => {
4476                // GeneralizedTime: YYYYMMDDHHMMSSZ
4477                if body.len() < 14 {
4478                    return None;
4479                }
4480                Some(format!(
4481                    "{}-{}-{} {}:{}:{} UTC",
4482                    &body[0..4],
4483                    &body[4..6],
4484                    &body[6..8],
4485                    &body[8..10],
4486                    &body[10..12],
4487                    &body[12..14],
4488                ))
4489            }
4490            _ => None,
4491        }
4492    }
4493
4494    let mut all_cns: Vec<String> = Vec::new();
4495    let mut signing_time: Option<String> = None;
4496    let mut after_counter_sig = false;
4497    let mut timestamp_signer_cn: Option<String> = None;
4498
4499    let mut i = 0usize;
4500    while i + 5 <= pkcs7.len() {
4501        if pkcs7[i..].starts_with(CN_OID) {
4502            // CN OID is followed by one of: tag 0x13/0x0c/0x16/0x14/0x1e
4503            // wrapped in either 0x30 SET or directly. The byte right
4504            // after the OID is the value tag for the CN body in the
4505            // RDN SET in practice.
4506            let after_oid = i + CN_OID.len();
4507            if let Some((cn, _)) = read_der_string(pkcs7, after_oid) {
4508                let trimmed = cn.trim().to_string();
4509                if !trimmed.is_empty() && trimmed.len() <= 200 {
4510                    if after_counter_sig && timestamp_signer_cn.is_none() {
4511                        timestamp_signer_cn = Some(trimmed.clone());
4512                    }
4513                    if !all_cns.contains(&trimmed) {
4514                        all_cns.push(trimmed);
4515                    }
4516                }
4517            }
4518            i += CN_OID.len();
4519            continue;
4520        }
4521        if signing_time.is_none() && pkcs7[i..].starts_with(SIGNING_TIME_OID) {
4522            // signingTime OID is followed by SET { UTCTime | GeneralizedTime }.
4523            // Skip the SET tag + length to land on the time tag.
4524            let mut j = i + SIGNING_TIME_OID.len();
4525            // Optional SET (0x31) wrapper.
4526            if j < pkcs7.len() && pkcs7[j] == 0x31 {
4527                if let Some((_, hdr)) = der_len(pkcs7, j + 1) {
4528                    j += 1 + hdr;
4529                }
4530            }
4531            if let Some(t) = read_der_time(pkcs7, j) {
4532                signing_time = Some(t);
4533            }
4534            i += SIGNING_TIME_OID.len();
4535            continue;
4536        }
4537        if pkcs7[i..].starts_with(COUNTER_SIG_OID) {
4538            after_counter_sig = true;
4539            i += COUNTER_SIG_OID.len();
4540            continue;
4541        }
4542        i += 1;
4543    }
4544
4545    // Heuristic: leaf signer = first CN that is NOT a known intermediate
4546    // CA name. Code-signing leaf certs almost always carry the publisher
4547    // name; intermediates carry "... CA" / "... Code Signing" / "... Root".
4548    let is_ca_like = |cn: &str| {
4549        let l = cn.to_ascii_lowercase();
4550        l.contains(" ca ")
4551            || l.ends_with(" ca")
4552            || l.contains("code signing")
4553            || l.contains("root")
4554            || l.contains("timestamping")
4555            || l.contains("time stamping")
4556    };
4557    let signer_cn = all_cns.iter().find(|c| !is_ca_like(c)).cloned();
4558    // Issuer = the CA-like CN immediately preceding the signer in the
4559    // chain. Authenticode certificate sequences typically appear in
4560    // root → intermediate → leaf order, so the entry just before the
4561    // signer is the direct issuer.
4562    let issuer_cn = if let Some(s) = &signer_cn {
4563        let pos = all_cns.iter().position(|c| c == s).unwrap_or(0);
4564        all_cns
4565            .iter()
4566            .take(pos)
4567            .rev()
4568            .find(|c| is_ca_like(c))
4569            .cloned()
4570    } else {
4571        None
4572    };
4573
4574    Some(SigInfo {
4575        cert_blob_size: dw_length,
4576        revision,
4577        cert_type,
4578        signer_cn,
4579        issuer_cn,
4580        signing_time,
4581        timestamp_signer_cn,
4582        all_cns,
4583    })
4584}
4585
4586/// Walk the PE resource directory tree and surface a triage-friendly
4587/// listing of every embedded resource. Optionally dump each resource's
4588/// raw bytes to disk for downstream analysis (icon extraction, MSI
4589/// peeking, embedded-payload recovery).
4590///
4591/// Three-level tree as documented in the PE/COFF spec: TYPE → NAME/ID →
4592/// LANGUAGE → IMAGE_RESOURCE_DATA_ENTRY. This function walks each level
4593/// using only the spec-defined offsets — it does not depend on goblin's
4594/// resource parser, which only exposes the raw section.
4595fn run_resources(binary_path: &str, data: &[u8], json: bool, dump_dir: Option<&str>) {
4596    let entries = match resources_parse(data) {
4597        Some(e) => e,
4598        None => {
4599            if json {
4600                println!(
4601                    "{}",
4602                    serde_json::to_string_pretty(&serde_json::json!({
4603                        "binary": binary_path,
4604                        "has_resources": false,
4605                    })).unwrap()
4606                );
4607            } else {
4608                println!(
4609                    "=== Resources for {} ===\n\n(no resource directory; PE has no .rsrc, or directory was empty)",
4610                    binary_path
4611                );
4612            }
4613            return;
4614        }
4615    };
4616
4617    if let Some(dir) = dump_dir {
4618        if let Err(e) = std::fs::create_dir_all(dir) {
4619            eprintln!("error: cannot create dump dir {}: {}", dir, e);
4620            return;
4621        }
4622        for r in &entries {
4623            let fname = format!(
4624                "{}/{}_{}_{}.bin",
4625                dir.trim_end_matches('/'),
4626                r.type_name(),
4627                r.id_label(),
4628                r.lang
4629            );
4630            if let Some(blob) = data.get(r.file_offset..r.file_offset + r.size) {
4631                if let Err(e) = std::fs::write(&fname, blob) {
4632                    eprintln!("warn: write {}: {}", fname, e);
4633                }
4634            }
4635        }
4636    }
4637
4638    if json {
4639        let arr: Vec<_> = entries
4640            .iter()
4641            .map(|r| {
4642                serde_json::json!({
4643                    "type": r.type_name(),
4644                    "type_id": r.type_id,
4645                    "id": r.id_label(),
4646                    "id_raw": r.id_raw,
4647                    "lang": r.lang,
4648                    "rva": format!("0x{:x}", r.rva),
4649                    "file_offset": format!("0x{:x}", r.file_offset),
4650                    "size": r.size,
4651                    "preview": r.preview(data),
4652                })
4653            })
4654            .collect();
4655        let payload = serde_json::json!({
4656            "binary": binary_path,
4657            "has_resources": true,
4658            "count": entries.len(),
4659            "entries": arr,
4660        });
4661        println!("{}", serde_json::to_string_pretty(&payload).unwrap());
4662        return;
4663    }
4664
4665    println!("=== Resources for {} ===", binary_path);
4666    println!("\n{} entries\n", entries.len());
4667    println!(
4668        "{:<14} {:<24} {:<6} {:>8}  preview",
4669        "type", "id", "lang", "size"
4670    );
4671    println!("{}", "-".repeat(78));
4672    for r in &entries {
4673        println!(
4674            "{:<14} {:<24} {:<6} {:>8}  {}",
4675            r.type_name(),
4676            r.id_label(),
4677            r.lang,
4678            r.size,
4679            r.preview(data),
4680        );
4681    }
4682    if let Some(dir) = dump_dir {
4683        println!("\nResources dumped to {}/", dir);
4684    }
4685}
4686
4687#[derive(Debug)]
4688struct ResEntry {
4689    type_id: u32,
4690    type_name_str: Option<String>,
4691    id_raw: u32,
4692    id_name: Option<String>,
4693    lang: u32,
4694    rva: u32,
4695    file_offset: usize,
4696    size: usize,
4697}
4698
4699impl ResEntry {
4700    fn type_name(&self) -> String {
4701        if let Some(n) = &self.type_name_str {
4702            return n.clone();
4703        }
4704        match self.type_id {
4705            1 => "CURSOR",
4706            2 => "BITMAP",
4707            3 => "ICON",
4708            4 => "MENU",
4709            5 => "DIALOG",
4710            6 => "STRING",
4711            7 => "FONTDIR",
4712            8 => "FONT",
4713            9 => "ACCELERATOR",
4714            10 => "RCDATA",
4715            11 => "MESSAGETABLE",
4716            12 => "GROUP_CURSOR",
4717            14 => "GROUP_ICON",
4718            16 => "VERSION",
4719            17 => "DLGINCLUDE",
4720            19 => "PLUGPLAY",
4721            20 => "VXD",
4722            21 => "ANICURSOR",
4723            22 => "ANIICON",
4724            23 => "HTML",
4725            24 => "MANIFEST",
4726            _ => return format!("TYPE_{}", self.type_id),
4727        }
4728        .to_string()
4729    }
4730    fn id_label(&self) -> String {
4731        match &self.id_name {
4732            Some(n) => n.clone(),
4733            None => format!("#{}", self.id_raw),
4734        }
4735    }
4736    fn preview(&self, data: &[u8]) -> String {
4737        let blob = match data.get(self.file_offset..self.file_offset + self.size.min(96)) {
4738            Some(b) => b,
4739            None => return String::new(),
4740        };
4741        // PE / MZ embedded payload — high signal, surface immediately.
4742        if blob.len() >= 2 && &blob[..2] == b"MZ" {
4743            return format!("[embedded PE/EXE, {} bytes]", self.size);
4744        }
4745        // MSI / OLE compound document.
4746        if blob.len() >= 8 && blob[..8] == [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1] {
4747            return format!("[OLE compound (likely MSI), {} bytes]", self.size);
4748        }
4749        // CAB.
4750        if blob.len() >= 4 && &blob[..4] == b"MSCF" {
4751            return format!("[CAB archive, {} bytes]", self.size);
4752        }
4753        // PNG / JPG / GIF.
4754        if blob.len() >= 8 && &blob[..8] == &[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A] {
4755            return format!("[PNG image, {} bytes]", self.size);
4756        }
4757        if blob.len() >= 3 && &blob[..3] == b"\xff\xd8\xff" {
4758            return format!("[JPEG image, {} bytes]", self.size);
4759        }
4760        // Manifest / XML — UTF-8 text.
4761        if matches!(self.type_id, 24) {
4762            let text = String::from_utf8_lossy(blob);
4763            let cleaned: String = text
4764                .chars()
4765                .filter(|c| !c.is_control() || *c == ' ')
4766                .take(80)
4767                .collect();
4768            return cleaned;
4769        }
4770        // VS_VERSIONINFO — UTF-16LE keyed structure starting with len/value-len.
4771        if self.type_id == 16 && blob.len() >= 6 {
4772            // Decode UTF-16LE printable-ish run as a hint.
4773            let mut out = String::new();
4774            let mut i = 6;
4775            while i + 1 < blob.len() && out.len() < 60 {
4776                let lo = blob[i];
4777                let hi = blob[i + 1];
4778                if hi == 0 && (0x20..0x7f).contains(&lo) {
4779                    out.push(lo as char);
4780                } else if lo == 0 && hi == 0 {
4781                    if !out.is_empty() && !out.ends_with(' ') {
4782                        out.push(' ');
4783                    }
4784                }
4785                i += 2;
4786            }
4787            return out.trim().to_string();
4788        }
4789        // Generic preview: first 32 bytes hex + ascii.
4790        let n = blob.len().min(32);
4791        let hex: String = blob[..n].iter().map(|b| format!("{:02x}", b)).collect::<Vec<_>>().join("");
4792        let ascii: String = blob[..n]
4793            .iter()
4794            .map(|&b| if (0x20..0x7f).contains(&b) { b as char } else { '.' })
4795            .collect();
4796        format!("{}  |{}|", hex, ascii)
4797    }
4798}
4799
4800fn resources_parse(data: &[u8]) -> Option<Vec<ResEntry>> {
4801    if data.len() < 0x40 || &data[..2] != b"MZ" {
4802        return None;
4803    }
4804    let e_lfanew = u32::from_le_bytes(data[0x3C..0x40].try_into().ok()?) as usize;
4805    if e_lfanew + 24 > data.len() || &data[e_lfanew..e_lfanew + 4] != b"PE\0\0" {
4806        return None;
4807    }
4808    let opt_off = e_lfanew + 24;
4809    let magic = u16::from_le_bytes(data[opt_off..opt_off + 2].try_into().ok()?);
4810    let dir_off = match magic {
4811        0x10b => opt_off + 0x70 + 2 * 8, // PE32: data dirs start at +0x60, entry 2 = resources
4812        0x20b => opt_off + 0x70 + 2 * 8 + 16, // PE32+: shifted +16
4813        _ => return None,
4814    };
4815    // Recompute properly: data dirs start at opt_off + (0x60 PE32 / 0x70 PE32+), resources is index 2.
4816    let data_dir_base = match magic {
4817        0x10b => opt_off + 0x60,
4818        0x20b => opt_off + 0x70,
4819        _ => return None,
4820    };
4821    let res_dir_entry = data_dir_base + 2 * 8;
4822    if res_dir_entry + 8 > data.len() {
4823        return None;
4824    }
4825    let res_rva =
4826        u32::from_le_bytes(data[res_dir_entry..res_dir_entry + 4].try_into().ok()?) as u64;
4827    let res_size =
4828        u32::from_le_bytes(data[res_dir_entry + 4..res_dir_entry + 8].try_into().ok()?) as usize;
4829    if res_rva == 0 || res_size == 0 {
4830        return None;
4831    }
4832    let _ = dir_off;
4833
4834    // Walk section table to map RVA → file offset and locate the .rsrc base.
4835    let n_sec = u16::from_le_bytes(data[e_lfanew + 6..e_lfanew + 8].try_into().ok()?) as usize;
4836    let opt_size = u16::from_le_bytes(data[e_lfanew + 20..e_lfanew + 22].try_into().ok()?) as usize;
4837    let sec_off = opt_off + opt_size;
4838    if sec_off + n_sec * 40 > data.len() {
4839        return None;
4840    }
4841    let mut sections: Vec<(u64, u64, u64)> = Vec::new(); // (va, vsz, fo)
4842    for i in 0..n_sec {
4843        let off = sec_off + i * 40;
4844        let vsz = u32::from_le_bytes(data[off + 8..off + 12].try_into().ok()?) as u64;
4845        let va = u32::from_le_bytes(data[off + 12..off + 16].try_into().ok()?) as u64;
4846        let fo = u32::from_le_bytes(data[off + 20..off + 24].try_into().ok()?) as u64;
4847        sections.push((va, vsz, fo));
4848    }
4849    let rva_to_fo = |rva: u64| -> Option<usize> {
4850        for (va, vsz, fo) in &sections {
4851            if rva >= *va && rva < va + vsz {
4852                return Some((fo + (rva - va)) as usize);
4853            }
4854        }
4855        None
4856    };
4857    let res_base_fo = rva_to_fo(res_rva)?;
4858    let res_blob_end = res_base_fo + res_size;
4859    if res_blob_end > data.len() {
4860        return None;
4861    }
4862    let rsrc = &data[res_base_fo..res_blob_end];
4863
4864    // Read a UTF-16LE pascal-style name (length-prefixed) from the
4865    // resource section. Used for named resources.
4866    let read_name = |offset: usize| -> Option<String> {
4867        if offset + 2 > rsrc.len() {
4868            return None;
4869        }
4870        let n = u16::from_le_bytes(rsrc[offset..offset + 2].try_into().ok()?) as usize;
4871        let start = offset + 2;
4872        if start + n * 2 > rsrc.len() {
4873            return None;
4874        }
4875        let mut u16s: Vec<u16> = Vec::with_capacity(n);
4876        for chunk in rsrc[start..start + n * 2].chunks_exact(2) {
4877            u16s.push(u16::from_le_bytes([chunk[0], chunk[1]]));
4878        }
4879        String::from_utf16(&u16s).ok()
4880    };
4881
4882    // Walk a directory at `dir_off` (relative to rsrc base). Returns
4883    // a list of (id_or_name, child_offset, is_dir). Limit recursion to
4884    // depth 3 (TYPE → NAME → LANG → DATA).
4885    fn walk_dir(rsrc: &[u8], dir_off: usize) -> Option<Vec<(u32, Option<String>, u32, bool)>> {
4886        if dir_off + 16 > rsrc.len() {
4887            return None;
4888        }
4889        let n_named = u16::from_le_bytes(rsrc[dir_off + 12..dir_off + 14].try_into().ok()?);
4890        let n_id = u16::from_le_bytes(rsrc[dir_off + 14..dir_off + 16].try_into().ok()?);
4891        let total = n_named as usize + n_id as usize;
4892        let entries_off = dir_off + 16;
4893        if entries_off + total * 8 > rsrc.len() {
4894            return None;
4895        }
4896        let mut out = Vec::with_capacity(total);
4897        for i in 0..total {
4898            let e = entries_off + i * 8;
4899            let name_or_id = u32::from_le_bytes(rsrc[e..e + 4].try_into().ok()?);
4900            let off = u32::from_le_bytes(rsrc[e + 4..e + 8].try_into().ok()?);
4901            let is_dir = (off & 0x8000_0000) != 0;
4902            let child_off = (off & 0x7fff_ffff) as u32;
4903            out.push((name_or_id, None, child_off, is_dir));
4904            // Caller will translate Name pointer (high bit set on
4905            // name_or_id) into a string separately to keep this fn
4906            // borrow-free.
4907            let _ = name_or_id;
4908        }
4909        Some(out)
4910    }
4911
4912    let mut entries: Vec<ResEntry> = Vec::new();
4913
4914    // Level 1: TYPE
4915    let level1 = walk_dir(rsrc, 0)?;
4916    for &(type_raw, _, type_child_off, type_is_dir) in &level1 {
4917        if !type_is_dir {
4918            continue;
4919        }
4920        let type_name_str = if (type_raw & 0x8000_0000) != 0 {
4921            read_name((type_raw & 0x7fff_ffff) as usize)
4922        } else {
4923            None
4924        };
4925        let type_id = type_raw & 0x7fff_ffff;
4926        let level2 = match walk_dir(rsrc, type_child_off as usize) {
4927            Some(v) => v,
4928            None => continue,
4929        };
4930        // Level 2: NAME / ID
4931        for &(id_raw, _, id_child_off, id_is_dir) in &level2 {
4932            if !id_is_dir {
4933                continue;
4934            }
4935            let id_name = if (id_raw & 0x8000_0000) != 0 {
4936                read_name((id_raw & 0x7fff_ffff) as usize)
4937            } else {
4938                None
4939            };
4940            let id_val = id_raw & 0x7fff_ffff;
4941            let level3 = match walk_dir(rsrc, id_child_off as usize) {
4942                Some(v) => v,
4943                None => continue,
4944            };
4945            // Level 3: LANGUAGE → DATA_ENTRY
4946            for &(lang_raw, _, lang_child_off, lang_is_dir) in &level3 {
4947                if lang_is_dir {
4948                    continue;
4949                }
4950                let lang = lang_raw & 0x7fff_ffff;
4951                // IMAGE_RESOURCE_DATA_ENTRY = { OffsetToData(rva), Size, CodePage, Reserved }
4952                let de = lang_child_off as usize;
4953                if de + 16 > rsrc.len() {
4954                    continue;
4955                }
4956                let data_rva = match u32::from_le_bytes(rsrc[de..de + 4].try_into().ok()?) {
4957                    v => v as u64,
4958                };
4959                let data_sz = u32::from_le_bytes(rsrc[de + 4..de + 8].try_into().ok()?) as usize;
4960                let Some(fo) = rva_to_fo(data_rva) else { continue };
4961                if fo + data_sz > data.len() {
4962                    continue;
4963                }
4964                entries.push(ResEntry {
4965                    type_id,
4966                    type_name_str: type_name_str.clone(),
4967                    id_raw: id_val,
4968                    id_name: id_name.clone(),
4969                    lang,
4970                    rva: data_rva as u32,
4971                    file_offset: fo,
4972                    size: data_sz,
4973                });
4974            }
4975        }
4976    }
4977
4978    if entries.is_empty() {
4979        return None;
4980    }
4981    Some(entries)
4982}
4983
4984/// Export full call graph as JSON.
4985fn run_callgraph(binary_path: &str, data: &[u8]) {
4986    let obj = match goblin::Object::parse(data) {
4987        Ok(o) => o,
4988        Err(e) => {
4989            eprintln!("Error: {}", e);
4990            return;
4991        }
4992    };
4993    let (arch, segs, mut symbols) = match parse_binary(&obj, data) {
4994        Some(r) => r,
4995        None => {
4996            eprintln!("Unsupported");
4997            return;
4998        }
4999    };
5000    if symbols.is_empty() {
5001        if let goblin::Object::PE(pe) = &obj {
5002            let base = pe.image_base as u64;
5003            let entry = base
5004                + pe.header
5005                    .optional_header
5006                    .unwrap()
5007                    .standard_fields
5008                    .address_of_entry_point as u64;
5009            symbols = discover_pe_functions(entry, &segs, data, arch);
5010        }
5011    }
5012    let is_elf_stripped = if let goblin::Object::Elf(elf) = &obj {
5013        elf.syms.len() == 0
5014    } else {
5015        false
5016    };
5017    if is_elf_stripped {
5018        if let goblin::Object::Elf(elf) = &obj {
5019            let discovered = discover_elf_functions(elf, &segs, data, arch);
5020            let existing: std::collections::BTreeSet<u64> =
5021                symbols.iter().map(|(a, _)| *a).collect();
5022            for (addr, name) in discovered {
5023                if !existing.contains(&addr) {
5024                    symbols.push((addr, name));
5025                }
5026            }
5027        }
5028    }
5029
5030    let path = std::path::Path::new(binary_path);
5031    let mut dec = rsleigh_api::Decoder::new(arch);
5032    let mut graph: std::collections::BTreeMap<String, serde_json::Value> =
5033        std::collections::BTreeMap::new();
5034
5035    eprintln!("Building call graph for {} functions...", symbols.len());
5036
5037    for (func_addr, func_name) in &symbols {
5038        let insts = decode_func(*func_addr, &symbols, &segs, data, &mut dec);
5039        if insts.is_empty() {
5040            continue;
5041        }
5042
5043        let output = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5044            rsleigh_decompile::decompile_with_binary(arch, &insts, Some(data), Some(path))
5045        })) {
5046            Ok(o) => o,
5047            Err(_) => continue,
5048        };
5049
5050        // Extract callees from pseudocode
5051        let mut calls = Vec::new();
5052        for line in output.lines() {
5053            let t = line.trim();
5054            if t.contains('(') && !t.starts_with("//") {
5055                let check = if let Some(eq) = t.find(" = ") {
5056                    &t[eq + 3..]
5057                } else {
5058                    t
5059                };
5060                if let Some(p) = check.find('(') {
5061                    let callee = check[..p].trim().trim_start_matches("return ");
5062                    if !callee.is_empty()
5063                        && !callee.contains(' ')
5064                        && !callee.starts_with('*')
5065                        && !callee.starts_with('(')
5066                        && !callee.starts_with("if")
5067                        && !callee.starts_with("while")
5068                        && !callee.starts_with("switch")
5069                        && !callee.starts_with("for")
5070                        && callee.len() < 50
5071                        && !calls.contains(&callee.to_string())
5072                    {
5073                        calls.push(callee.to_string());
5074                    }
5075                }
5076            }
5077        }
5078
5079        // Classify function behavior
5080        let mut tags = Vec::new();
5081        if calls.iter().any(|c| {
5082            [
5083                "recv", "send", "socket", "connect", "accept", "bind", "listen",
5084            ]
5085            .contains(&c.as_str())
5086        }) {
5087            tags.push("network");
5088        }
5089        if calls.iter().any(|c| {
5090            [
5091                "CreateFile",
5092                "fopen",
5093                "ReadFile",
5094                "WriteFile",
5095                "fread",
5096                "fwrite",
5097                "open",
5098                "read",
5099                "write",
5100            ]
5101            .contains(&c.as_str())
5102        }) {
5103            tags.push("file_io");
5104        }
5105        if calls
5106            .iter()
5107            .any(|c| c.contains("Reg") || c.contains("Registry"))
5108        {
5109            tags.push("registry");
5110        }
5111        if calls.iter().any(|c| {
5112            [
5113                "system",
5114                "exec",
5115                "execve",
5116                "popen",
5117                "ShellExecute",
5118                "WinExec",
5119                "CreateProcess",
5120            ]
5121            .contains(&c.as_str())
5122        }) {
5123            tags.push("exec");
5124        }
5125        if calls.iter().any(|c| {
5126            [
5127                "malloc",
5128                "free",
5129                "realloc",
5130                "VirtualAlloc",
5131                "mmap",
5132                "HeapAlloc",
5133            ]
5134            .contains(&c.as_str())
5135        }) {
5136            tags.push("memory");
5137        }
5138        if output.contains("AES")
5139            || output.contains("SHA")
5140            || output.contains("CRC")
5141            || output.contains("^ 0x")
5142        {
5143            tags.push("crypto");
5144        }
5145        if calls
5146            .iter()
5147            .any(|c| ["printf", "puts", "fprintf", "sprintf", "snprintf"].contains(&c.as_str()))
5148        {
5149            tags.push("output");
5150        }
5151        if calls
5152            .iter()
5153            .any(|c| ["scanf", "gets", "fgets", "getenv", "getchar"].contains(&c.as_str()))
5154        {
5155            tags.push("input");
5156        }
5157
5158        let return_type = output
5159            .lines()
5160            .next()
5161            .and_then(|l| l.split_whitespace().next())
5162            .unwrap_or("void");
5163
5164        graph.insert(
5165            func_name.clone(),
5166            serde_json::json!({
5167                "address": format!("0x{:x}", func_addr),
5168                "calls": calls,
5169                "return_type": return_type,
5170                "tags": tags,
5171            }),
5172        );
5173    }
5174
5175    // Build called_by reverse map
5176    let mut called_by: std::collections::BTreeMap<String, Vec<String>> =
5177        std::collections::BTreeMap::new();
5178    for (func_name, info) in &graph {
5179        if let Some(calls) = info.get("calls").and_then(|c| c.as_array()) {
5180            for callee in calls {
5181                if let Some(callee_name) = callee.as_str() {
5182                    called_by
5183                        .entry(callee_name.to_string())
5184                        .or_default()
5185                        .push(func_name.clone());
5186                }
5187            }
5188        }
5189    }
5190
5191    // Merge called_by into graph
5192    let mut final_graph = serde_json::Map::new();
5193    for (name, info) in &graph {
5194        let mut entry = info.clone();
5195        if let Some(callers) = called_by.get(name) {
5196            entry
5197                .as_object_mut()
5198                .unwrap()
5199                .insert("called_by".to_string(), serde_json::json!(callers));
5200        }
5201        final_graph.insert(name.clone(), entry);
5202    }
5203
5204    println!(
5205        "{}",
5206        serde_json::to_string_pretty(&serde_json::json!({
5207            "binary": binary_path,
5208            "arch": format!("{:?}", arch),
5209            "function_count": graph.len(),
5210            "callgraph": final_graph,
5211        }))
5212        .unwrap()
5213    );
5214}
5215
5216/// Demangle a C++/Swift symbol name for display, falling back to the raw
5217/// name when demangling fails. Strips the parameter list tail to keep the
5218/// one-line listing compact: `QObject::connect(QObject const*, ...)` →
5219/// `QObject::connect`. `.cold.NN` / `.part.NN` / `.constprop.N` suffixes
5220/// (GCC IPA clones) are preserved so the analyst can distinguish variants.
5221fn demangle_symbol(name: &str) -> String {
5222    // Keep GCC/clang IPA suffixes intact on the original-name fallback.
5223    let (core, suffix) = match name.find('.') {
5224        Some(p)
5225            if name[p..].starts_with(".cold")
5226                || name[p..].starts_with(".part")
5227                || name[p..].starts_with(".constprop")
5228                || name[p..].starts_with(".isra")
5229                || name[p..].starts_with(".lto_priv") =>
5230        {
5231            (&name[..p], &name[p..])
5232        }
5233        _ => (name, ""),
5234    };
5235    if !(core.starts_with("_Z") || core.starts_with("__Z")) {
5236        return name.to_string();
5237    }
5238    let Ok(sym) = cpp_demangle::Symbol::new(core.as_bytes()) else {
5239        return name.to_string();
5240    };
5241    let Ok(demangled) = sym.demangle(&cpp_demangle::DemangleOptions::default()) else {
5242        return name.to_string();
5243    };
5244    // Trim parameter list — matching rsleigh-decompile::imports::demangle_name.
5245    let pretty = if let Some(paren) = demangled.find('(') {
5246        let before = &demangled[..paren];
5247        if !before.is_empty() && !before.ends_with('>') {
5248            before.to_string()
5249        } else {
5250            demangled
5251        }
5252    } else {
5253        demangled
5254    };
5255    if suffix.is_empty() {
5256        pretty
5257    } else {
5258        format!("{}{}", pretty, suffix)
5259    }
5260}
5261
5262/// Load FID databases from `--fid <path>` args and apply fingerprint
5263/// matches to anonymous `func_*` / `sub_*` / `FUN_*` symbols.
5264fn apply_fid_to_symbols(
5265    data: &[u8],
5266    arch: rsleigh_api::Architecture,
5267    segs: &[(u64, u64, u64)],
5268    symbols: &mut [(u64, String)],
5269    args: &[String],
5270) {
5271    let mut dbs: Vec<rsleigh_fid::FidDb> = Vec::new();
5272    // Auto-load bundled glibc/musl/libstdc++ DBs unless --no-fid-auto.
5273    if !args.iter().any(|a| a == "--no-fid-auto") {
5274        for (lib, db) in rsleigh_fid::bundled_dbs(arch) {
5275            eprintln!("[fid] bundled {}: {} entries", lib, db.entries.len());
5276            dbs.push(db);
5277        }
5278    }
5279    let mut i = 0;
5280    while i < args.len() {
5281        if args[i] == "--fid" {
5282            if let Some(p) = args.get(i + 1) {
5283                match std::fs::File::open(p)
5284                    .and_then(|f| rsleigh_fid::FidDb::read(f).map_err(Into::into))
5285                {
5286                    Ok(db) => {
5287                        eprintln!("[fid] loaded {} entries from {}", db.entries.len(), p);
5288                        dbs.push(db);
5289                    }
5290                    Err(e) => eprintln!("[fid] skip {}: {}", p, e),
5291                }
5292                i += 2;
5293                continue;
5294            }
5295        }
5296        i += 1;
5297    }
5298    if dbs.is_empty() {
5299        return;
5300    }
5301    let quiet_banner = args.iter().any(|a| a == "--fid-quiet");
5302    let _ = quiet_banner;
5303    let va_slice = |va: u64| -> Option<&[u8]> {
5304        for (vstart, vend, foff) in segs {
5305            if va >= *vstart && va < *vend {
5306                let rel = (va - vstart) as usize;
5307                let fstart = *foff as usize + rel;
5308                let vsize = (vend - va) as usize;
5309                let end = fstart.saturating_add(vsize).min(data.len());
5310                if fstart < data.len() {
5311                    return Some(&data[fstart..end]);
5312                }
5313            }
5314        }
5315        None
5316    };
5317    let mut hits = 0usize;
5318    for (addr, name) in symbols.iter_mut() {
5319        let anon =
5320            name.starts_with("func_") || name.starts_with("sub_") || name.starts_with("FUN_");
5321        if !anon {
5322            continue;
5323        }
5324        let Some(body) = va_slice(*addr) else {
5325            continue;
5326        };
5327        // Cap body at 4KB — most real funcs are well under this.
5328        let body = &body[..body.len().min(4096)];
5329        for db in &dbs {
5330            if let Some(matched) = rsleigh_fid::identify(arch, body, *addr, db) {
5331                *name = matched.to_string();
5332                hits += 1;
5333                break;
5334            }
5335        }
5336    }
5337    if hits > 0 {
5338        eprintln!("[fid] matched {} anonymous symbols", hits);
5339    }
5340}
5341
5342/// Compute MD5 hash of data, return lowercase hex string.
5343fn compute_md5(data: &[u8]) -> String {
5344    use md5::{Digest, Md5};
5345    let mut h = Md5::new();
5346    h.update(data);
5347    h.finalize().iter().map(|b| format!("{:02x}", b)).collect()
5348}
5349
5350/// Compute SHA-256 hash of data, return lowercase hex string.
5351fn compute_sha256(data: &[u8]) -> String {
5352    use sha2::{Digest, Sha256};
5353    let mut h = Sha256::new();
5354    h.update(data);
5355    h.finalize().iter().map(|b| format!("{:02x}", b)).collect()
5356}
5357
5358/// Mandiant imphash: MD5 of the comma-joined, lowercased `dll.function`
5359/// entries built from the PE import table. DLL extensions are stripped to
5360/// a known short set; ordinal-only imports are encoded as `ord<N>`.
5361///
5362/// Returns None for non-PE binaries or PE files with no imports.
5363///
5364/// Spec: github.com/mandiant/pefile (imphash()).
5365fn compute_imphash(data: &[u8]) -> Option<String> {
5366    use md5::{Digest, Md5};
5367    let obj = goblin::Object::parse(data).ok()?;
5368    let pe = match obj {
5369        goblin::Object::PE(pe) => pe,
5370        _ => return None,
5371    };
5372    if pe.imports.is_empty() {
5373        return None;
5374    }
5375
5376    // Mandiant's normalization:
5377    //  - lowercase DLL name
5378    //  - strip extension if it's one of:
5379    //      .dll, .ocx, .sys, .drv, .cpl, .exe
5380    //  - function name: lowercase as-is; ordinal → "ord<num>"
5381    let strip_exts = [".dll", ".ocx", ".sys", ".drv", ".cpl", ".exe"];
5382    let mut entries: Vec<String> = Vec::new();
5383    for imp in &pe.imports {
5384        let mut dll = imp.dll.to_ascii_lowercase();
5385        for ext in &strip_exts {
5386            if dll.ends_with(ext) {
5387                dll.truncate(dll.len() - ext.len());
5388                break;
5389            }
5390        }
5391        // goblin's pe.imports gives named symbols directly; ordinals come
5392        // through as names like "Ordinal_123" or empty. Use the Import's
5393        // name field: if it looks like an ordinal placeholder, rewrite.
5394        let name = imp.name.to_ascii_lowercase();
5395        let fn_name = if name.starts_with("ordinal_") {
5396            // "ordinal_123" → "ord123"
5397            format!("ord{}", &name[8..])
5398        } else if name.is_empty() {
5399            // Truly unnamed ordinal — fall back to ordinal field
5400            format!("ord{}", imp.ordinal)
5401        } else {
5402            name
5403        };
5404        entries.push(format!("{}.{}", dll, fn_name));
5405    }
5406
5407    // Mandiant preserves import-table order (NOT sorted). Deduplication: no.
5408    let joined = entries.join(",");
5409    let mut h = Md5::new();
5410    h.update(joined.as_bytes());
5411    Some(h.finalize().iter().map(|b| format!("{:02x}", b)).collect())
5412}
5413
5414fn generate_yara_rule(binary_path: &str, data: &[u8]) {
5415    use std::collections::{BTreeMap, BTreeSet};
5416
5417    let filename = std::path::Path::new(binary_path)
5418        .file_stem()
5419        .unwrap_or_default()
5420        .to_string_lossy()
5421        .replace(|c: char| !c.is_ascii_alphanumeric() && c != '_', "_");
5422    let rule_name = format!("rsleigh_{}", filename);
5423
5424    let mut strings: BTreeSet<String> = BTreeSet::new();
5425    let mut wide_strings: BTreeSet<String> = BTreeSet::new();
5426    let mut hex_patterns: Vec<(String, String)> = Vec::new(); // (name, hex)
5427    let mut imports: BTreeSet<String> = BTreeSet::new();
5428    let mut meta: BTreeMap<String, String> = BTreeMap::new();
5429
5430    // Meta information
5431    meta.insert("tool".into(), "rsleigh".into());
5432    meta.insert("date".into(), chrono_date());
5433    let file_size = data.len();
5434    meta.insert("filesize".into(), format!("{}", file_size));
5435
5436    // Detect format
5437    let is_pe = data.len() > 2 && &data[0..2] == b"MZ";
5438    let is_elf = data.len() > 4 && &data[0..4] == b"\x7fELF";
5439    if is_pe {
5440        meta.insert("filetype".into(), "PE".into());
5441    }
5442    if is_elf {
5443        meta.insert("filetype".into(), "ELF".into());
5444    }
5445
5446    // 1. Extract ASCII strings (6+ chars, printable, not too common)
5447    {
5448        let mut pos = 0;
5449        while pos < data.len() {
5450            if data[pos] >= 0x20 && data[pos] < 0x7f {
5451                let start = pos;
5452                while pos < data.len() && data[pos] >= 0x20 && data[pos] < 0x7f {
5453                    pos += 1;
5454                }
5455                let len = pos - start;
5456                if len >= 6 && len <= 200 {
5457                    if let Ok(s) = std::str::from_utf8(&data[start..pos]) {
5458                        let s = s.trim();
5459                        // Filter out common/generic strings
5460                        let is_charset = s.contains("ABCDEFGHIJ") && s.contains("abcdefghij");
5461                        let is_sequential = s
5462                            .bytes()
5463                            .zip(s.bytes().skip(1))
5464                            .filter(|(a, b)| *b == a + 1)
5465                            .count()
5466                            > s.len() / 2;
5467                        if s.len() >= 6
5468                            && !is_charset && !is_sequential
5469                            && !s.chars().all(|c| c == ' ' || c == '.' || c == '-' || c == '0')
5470                            && !s.starts_with("GCC:")
5471                            && !s.starts_with("GNU ")
5472                            && !s.starts_with("!This program")
5473                            && !s.contains("Copyright")
5474                            && !s.contains("GLIBC")
5475                            && !s.starts_with(".debug")
5476                            && !s.starts_with(".note")
5477                            && !s.starts_with(".symtab")
5478                            && !s.starts_with(".strtab")
5479                            && !s.starts_with('`') // MSVC demangled names (generic)
5480                            && !s.contains("Descriptor")
5481                            && !s.contains("constructor")
5482                            && !s.contains("destructor")
5483                            && !s.starts_with("AppPolicy") // CRT internal
5484                            && !s.contains("template-parameter")
5485                            && !s.contains("Hierarchy")
5486                        {
5487                            strings.insert(s.to_string());
5488                        }
5489                    }
5490                }
5491            } else {
5492                pos += 1;
5493            }
5494        }
5495    }
5496
5497    // 2. Extract wide strings (UTF-16LE, for PE binaries)
5498    if is_pe {
5499        let mut pos = 0;
5500        while pos + 1 < data.len() {
5501            if data[pos] >= 0x20 && data[pos] < 0x7f && data[pos + 1] == 0 {
5502                let start = pos;
5503                while pos + 1 < data.len()
5504                    && data[pos] >= 0x20
5505                    && data[pos] < 0x7f
5506                    && data[pos + 1] == 0
5507                {
5508                    pos += 2;
5509                }
5510                let char_count = (pos - start) / 2;
5511                if char_count >= 6 && char_count <= 100 {
5512                    let chars: String = data[start..pos]
5513                        .chunks(2)
5514                        .filter_map(|c| {
5515                            if c.len() == 2 {
5516                                Some(c[0] as char)
5517                            } else {
5518                                None
5519                            }
5520                        })
5521                        .collect();
5522                    if !strings.contains(&chars) {
5523                        // don't duplicate ASCII
5524                        wide_strings.insert(chars);
5525                    }
5526                }
5527            } else {
5528                pos += 1;
5529            }
5530        }
5531    }
5532
5533    // 3. Extract imports (PE + ELF)
5534    if let Ok(obj) = goblin::Object::parse(data) {
5535        match &obj {
5536            goblin::Object::PE(pe) => {
5537                for imp in &pe.imports {
5538                    imports.insert(imp.name.to_string());
5539                }
5540                if let Some(name) = pe.name {
5541                    meta.insert("original_name".into(), name.to_string());
5542                }
5543            }
5544            goblin::Object::Elf(elf) => {
5545                for sym in elf.dynsyms.iter() {
5546                    if let Some(name) = elf.dynstrtab.get_at(sym.st_name) {
5547                        if !name.is_empty() && name.len() > 3 {
5548                            imports.insert(name.to_string());
5549                        }
5550                    }
5551                }
5552            }
5553            _ => {}
5554        }
5555    }
5556
5557    // 4. Detect crypto constants
5558    let crypto_sigs: &[(&str, &[u8])] = &[
5559        (
5560            "aes_sbox",
5561            &[0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5],
5562        ),
5563        (
5564            "sha256_k",
5565            &[0x98, 0x2f, 0x8a, 0x42, 0x91, 0x44, 0x37, 0x71],
5566        ),
5567        (
5568            "sha256_k_be",
5569            &[0x42, 0x8a, 0x2f, 0x98, 0x71, 0x37, 0x44, 0x91],
5570        ),
5571        ("md5_t", &[0x78, 0xa4, 0x6a, 0xd7, 0x56, 0xb7, 0xc7, 0xe8]),
5572        (
5573            "crc32_table",
5574            &[0x00, 0x00, 0x00, 0x00, 0x96, 0x30, 0x07, 0x77],
5575        ),
5576        ("chacha20", b"expand 32-byte k"),
5577        (
5578            "blowfish_p",
5579            &[0x24, 0x3f, 0x6a, 0x88, 0x85, 0xa3, 0x08, 0xd3],
5580        ),
5581    ];
5582    for (name, pattern) in crypto_sigs {
5583        if data.windows(pattern.len()).any(|w| w == *pattern) {
5584            let hex = pattern
5585                .iter()
5586                .map(|b| format!("{:02X}", b))
5587                .collect::<Vec<_>>()
5588                .join(" ");
5589            hex_patterns.push((format!("crypto_{}", name), hex));
5590        }
5591    }
5592
5593    // 5. Extract unique byte patterns from entry point / first function
5594    if let Ok(obj) = goblin::Object::parse(data) {
5595        let entry_bytes = match &obj {
5596            goblin::Object::PE(pe) => {
5597                let entry_rva = pe
5598                    .header
5599                    .optional_header
5600                    .map(|h| h.standard_fields.address_of_entry_point as usize)
5601                    .unwrap_or(0);
5602                pe.sections.iter().find_map(|s| {
5603                    let sr = s.virtual_address as usize;
5604                    if entry_rva >= sr && entry_rva < sr + s.virtual_size as usize {
5605                        let fo = s.pointer_to_raw_data as usize + (entry_rva - sr);
5606                        if fo + 32 <= data.len() {
5607                            Some(&data[fo..fo + 32])
5608                        } else {
5609                            None
5610                        }
5611                    } else {
5612                        None
5613                    }
5614                })
5615            }
5616            goblin::Object::Elf(elf) => {
5617                let entry = elf.header.e_entry as usize;
5618                elf.section_headers.iter().find_map(|sh| {
5619                    if entry >= sh.sh_addr as usize && entry < (sh.sh_addr + sh.sh_size) as usize {
5620                        let fo = sh.sh_offset as usize + (entry - sh.sh_addr as usize);
5621                        if fo + 32 <= data.len() {
5622                            Some(&data[fo..fo + 32])
5623                        } else {
5624                            None
5625                        }
5626                    } else {
5627                        None
5628                    }
5629                })
5630            }
5631            _ => None,
5632        };
5633        if let Some(bytes) = entry_bytes {
5634            let hex = bytes
5635                .iter()
5636                .map(|b| format!("{:02X}", b))
5637                .collect::<Vec<_>>()
5638                .join(" ");
5639            hex_patterns.push(("entry_point".into(), hex));
5640        }
5641    }
5642
5643    // 6. Select best strings for the rule (most unique, not too long)
5644    // Score strings: prefer longer, with special chars, not common words
5645    let mut scored_strings: Vec<(i32, &String)> = strings
5646        .iter()
5647        .map(|s| {
5648            let mut score = s.len() as i32;
5649            if s.contains('/') || s.contains('\\') {
5650                score += 5;
5651            } // paths
5652            if s.contains("http") || s.contains("://") {
5653                score += 10;
5654            } // URLs
5655            if s.contains(".dll") || s.contains(".exe") || s.contains(".sys") {
5656                score += 10;
5657            }
5658            if s.contains("password") || s.contains("secret") || s.contains("key") {
5659                score += 15;
5660            }
5661            if s.contains("cmd") || s.contains("shell") || s.contains("exec") {
5662                score += 10;
5663            }
5664            if s.starts_with("Error") || s.starts_with("Warning") {
5665                score -= 5;
5666            }
5667            // Penalize very common strings
5668            if s.len() > 50 {
5669                score -= 10;
5670            }
5671            (score, s)
5672        })
5673        .collect();
5674    scored_strings.sort_by(|a, b| b.0.cmp(&a.0));
5675
5676    // Select top 20 strings
5677    let selected_strings: Vec<&String> = scored_strings.iter().take(20).map(|(_, s)| *s).collect();
5678
5679    // Select top 5 wide strings
5680    let selected_wide: Vec<&String> = wide_strings.iter().take(5).collect();
5681
5682    // Select suspicious imports
5683    let suspicious_imports: Vec<&String> = imports
5684        .iter()
5685        .filter(|i| {
5686            let il = i.to_lowercase();
5687            il.contains("virtualalloc")
5688                || il.contains("writeprocessmemory")
5689                || il.contains("createremotethread")
5690                || il.contains("ntcreatethreadex")
5691                || il.contains("loadlibrary")
5692                || il.contains("getprocaddress")
5693                || il.contains("cryptencrypt")
5694                || il.contains("internetopen")
5695                || il.contains("urldownload")
5696                || il.contains("shellexecute")
5697                || il.contains("regsetvalue")
5698                || il.contains("createservice")
5699                || il.contains("socket")
5700                || il.contains("connect")
5701                || il.contains("recv")
5702                || il.contains("send")
5703                || il.contains("exec")
5704                || il.contains("system")
5705                || il.contains("popen")
5706                || il.contains("fork")
5707        })
5708        .take(10)
5709        .collect();
5710
5711    // Output YARA rule
5712    println!("rule {} {{", rule_name);
5713    println!("    meta:");
5714    for (k, v) in &meta {
5715        println!("        {} = \"{}\"", k, v);
5716    }
5717    println!("        description = \"Auto-generated by rsleigh decompiler\"");
5718    println!();
5719
5720    println!("    strings:");
5721    let mut str_idx = 0;
5722    for s in &selected_strings {
5723        let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
5724        println!("        $s{} = \"{}\"", str_idx, escaped);
5725        str_idx += 1;
5726    }
5727    for s in &selected_wide {
5728        let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
5729        println!("        $w{} = \"{}\" wide", str_idx, escaped);
5730        str_idx += 1;
5731    }
5732    for (name, hex) in &hex_patterns {
5733        println!("        $h_{} = {{ {} }}", name, hex);
5734    }
5735    for (i, imp) in suspicious_imports.iter().enumerate() {
5736        println!("        $imp{} = \"{}\"", i, imp);
5737    }
5738    println!();
5739
5740    // Condition: require several strings + optional hex patterns
5741    let total_str_count = selected_strings.len() + selected_wide.len();
5742    let min_match = (total_str_count / 3).max(3).min(total_str_count);
5743    println!("    condition:");
5744    let mut conditions = Vec::new();
5745    if is_pe {
5746        conditions.push("uint16(0) == 0x5A4D".to_string()); // MZ header
5747    } else if is_elf {
5748        conditions.push("uint32(0) == 0x464C457F".to_string()); // \x7fELF
5749    }
5750    if total_str_count > 0 {
5751        conditions.push(format!("{} of ($s*, $w*)", min_match));
5752    }
5753    if !hex_patterns.is_empty() {
5754        conditions.push("any of ($h_*)".to_string());
5755    }
5756    if !suspicious_imports.is_empty() {
5757        conditions.push(format!("{} of ($imp*)", suspicious_imports.len().min(3)));
5758    }
5759    if conditions.is_empty() {
5760        conditions.push("true".to_string());
5761    }
5762    println!("        {}", conditions.join(" and\n        "));
5763    println!("}}");
5764}
5765
5766fn chrono_date() -> String {
5767    // Simple date without chrono dependency
5768    "2026-04-13".to_string()
5769}
5770
5771fn run_raw(
5772    data: &[u8],
5773    arch: rsleigh_api::Architecture,
5774    base: u64,
5775    args: &[String],
5776    all_mode: bool,
5777) {
5778    eprintln!(
5779        "Architecture: {:?} (raw binary, base=0x{:x}, size={})",
5780        arch,
5781        base,
5782        data.len()
5783    );
5784
5785    // Treat entire file as one code segment
5786    let segs = vec![(base, data.len() as u64, 0u64)];
5787
5788    // Discover functions via CALL scanning
5789    let mut found = std::collections::BTreeSet::new();
5790    found.insert(base); // entry at base
5791
5792    let mut dec = rsleigh_api::Decoder::new(arch);
5793    let code_end = base + data.len() as u64;
5794
5795    // Architecture-specific CALL scanning
5796    match arch {
5797        rsleigh_api::Architecture::MIPS32 => {
5798            // MIPS JAL: 000011 imm26 → opcode 0x0C000000
5799            for i in (0..data.len().saturating_sub(3)).step_by(4) {
5800                let word = u32::from_be_bytes(data[i..i + 4].try_into().unwrap_or([0; 4]));
5801                if (word >> 26) == 3 {
5802                    // JAL
5803                    let target =
5804                        ((base + i as u64) & 0xF0000000) | ((word & 0x03FFFFFF) as u64) << 2;
5805                    if target >= base && target < code_end {
5806                        found.insert(target);
5807                    }
5808                }
5809            }
5810            // Also try little-endian MIPS
5811            let mut found_le = std::collections::BTreeSet::new();
5812            for i in (0..data.len().saturating_sub(3)).step_by(4) {
5813                let word = u32::from_le_bytes(data[i..i + 4].try_into().unwrap_or([0; 4]));
5814                if (word >> 26) == 3 {
5815                    let target =
5816                        ((base + i as u64) & 0xF0000000) | ((word & 0x03FFFFFF) as u64) << 2;
5817                    if target >= base && target < code_end {
5818                        found_le.insert(target);
5819                    }
5820                }
5821            }
5822            // Use whichever endianness found more targets
5823            if found_le.len() > found.len() * 2 {
5824                found = found_le;
5825                found.insert(base);
5826                eprintln!("Detected: MIPS little-endian ({} JAL targets)", found.len());
5827            } else {
5828                eprintln!("Detected: MIPS big-endian ({} JAL targets)", found.len());
5829            }
5830        }
5831        rsleigh_api::Architecture::ARM32 => {
5832            for i in (0..data.len().saturating_sub(3)).step_by(4) {
5833                let word = u32::from_le_bytes(data[i..i + 4].try_into().unwrap_or([0; 4]));
5834                if (word & 0x0F000000) == 0x0B000000 {
5835                    // BL
5836                    let imm24 = word & 0x00FFFFFF;
5837                    let offset = if imm24 & 0x800000 != 0 {
5838                        ((imm24 | 0xFF000000) as i32) << 2
5839                    } else {
5840                        (imm24 as i32) << 2
5841                    };
5842                    let target = (base as i64 + i as i64 + 8 + offset as i64) as u64;
5843                    if target >= base && target < code_end {
5844                        found.insert(target);
5845                    }
5846                }
5847            }
5848        }
5849        rsleigh_api::Architecture::X86_64 | rsleigh_api::Architecture::X86_32 => {
5850            for i in 0..data.len().saturating_sub(5) {
5851                if data[i] == 0xE8 {
5852                    let rel = i32::from_le_bytes(data[i + 1..i + 5].try_into().unwrap_or([0; 4]));
5853                    let target = (base as i64 + i as i64 + 5 + rel as i64) as u64;
5854                    if target >= base && target < code_end {
5855                        found.insert(target);
5856                    }
5857                }
5858            }
5859        }
5860        _ => {}
5861    }
5862
5863    let symbols: Vec<(u64, String)> = found
5864        .into_iter()
5865        .map(|addr| (addr, format!("FUN_{:08x}", addr)))
5866        .collect();
5867
5868    // Which functions to process? Skip --raw/--base and their values.
5869    let skip_values: std::collections::HashSet<usize> = {
5870        let mut s = std::collections::HashSet::new();
5871        for (i, a) in args.iter().enumerate() {
5872            if a == "--raw" || a == "--base" {
5873                s.insert(i);
5874                s.insert(i + 1);
5875            }
5876            if a == "--all" || a == "--json" || a == "--disasm" || a == "--sigs" {
5877                s.insert(i);
5878            }
5879        }
5880        s
5881    };
5882    let func_args: Vec<&str> = args
5883        .iter()
5884        .enumerate()
5885        .filter(|(i, a)| *i >= 2 && !a.starts_with("--") && !skip_values.contains(i))
5886        .map(|(_, a)| a.as_str())
5887        .collect();
5888
5889    if func_args.is_empty() && !all_mode {
5890        eprintln!("{} functions:", symbols.len());
5891        for (addr, name) in &symbols {
5892            println!("  0x{:08x}  {}", addr, name);
5893        }
5894    } else {
5895        let to_decompile: Vec<&(u64, String)> = if all_mode {
5896            symbols.iter().collect()
5897        } else {
5898            symbols
5899                .iter()
5900                .filter(|(_, n)| func_args.iter().any(|a| n == a))
5901                .collect()
5902        };
5903        let path = std::path::Path::new("raw.bin");
5904        for (addr, name) in to_decompile {
5905            let off = (*addr - base) as usize;
5906            let max = 4096.min(data.len().saturating_sub(off));
5907            if max < 4 {
5908                continue;
5909            }
5910            let bytes = &data[off..off + max];
5911            let mut pos = 0;
5912            let mut insts = Vec::new();
5913            let next_func = symbols
5914                .iter()
5915                .filter(|(a, _)| *a > *addr)
5916                .map(|(a, _)| *a)
5917                .min()
5918                .unwrap_or(*addr + max as u64);
5919            let decode_max = ((next_func - *addr) as usize).min(max);
5920            while pos < decode_max {
5921                match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5922                    dec.decode(&bytes[pos..], *addr + pos as u64)
5923                })) {
5924                    Ok(Ok(inst)) => {
5925                        let l = inst.len as usize;
5926                        if l == 0 {
5927                            pos += 4;
5928                            continue;
5929                        }
5930                        insts.push((*addr + pos as u64, inst));
5931                        pos += l;
5932                    }
5933                    Ok(Err(_)) | Err(_) => {
5934                        pos += 4;
5935                    }
5936                }
5937            }
5938            if !insts.is_empty() {
5939                let output = maybe_annotate_crypto(rsleigh_decompile::decompile_with_binary(
5940                    arch,
5941                    &insts,
5942                    Some(data),
5943                    Some(path),
5944                ));
5945                if !output.trim().is_empty() {
5946                    println!("// {}", name);
5947                    println!("{}", output);
5948                }
5949            }
5950        }
5951    }
5952}
5953
5954fn run_wasm(data: &[u8], args: &[String], all_mode: bool) {
5955    eprintln!("Architecture: WebAssembly");
5956    let funcs = wasm::parse_wasm(data);
5957
5958    // Which functions to decompile?
5959    let func_args: Vec<&str> = args[2..]
5960        .iter()
5961        .filter(|a| !a.starts_with("--"))
5962        .map(|a| a.as_str())
5963        .collect();
5964
5965    if func_args.is_empty() && !all_mode {
5966        // List functions
5967        println!("{} functions:", funcs.len());
5968        for f in &funcs {
5969            let params: Vec<&str> = f
5970                .params
5971                .iter()
5972                .map(|t| match t {
5973                    wasmparser::ValType::I32 => "i32",
5974                    wasmparser::ValType::I64 => "i64",
5975                    wasmparser::ValType::F32 => "f32",
5976                    wasmparser::ValType::F64 => "f64",
5977                    _ => "?",
5978                })
5979                .collect();
5980            let ret = f
5981                .results
5982                .first()
5983                .map(|t| match t {
5984                    wasmparser::ValType::I32 => "i32",
5985                    wasmparser::ValType::I64 => "i64",
5986                    wasmparser::ValType::F32 => "f32",
5987                    wasmparser::ValType::F64 => "f64",
5988                    _ => "?",
5989                })
5990                .unwrap_or("void");
5991            println!(
5992                "  func[{}]  {:20} ({}) -> {}",
5993                f.index,
5994                f.name,
5995                params.join(", "),
5996                ret
5997            );
5998        }
5999    } else {
6000        // Decompile
6001        let to_decompile: Vec<&wasm::WasmFunc> = if all_mode {
6002            funcs.iter().collect()
6003        } else {
6004            funcs
6005                .iter()
6006                .filter(|f| {
6007                    func_args
6008                        .iter()
6009                        .any(|a| f.name == *a || format!("func_{}", f.index) == *a)
6010                })
6011                .collect()
6012        };
6013
6014        for f in &to_decompile {
6015            let code = wasm::decompile_wasm_func(data, f, &funcs);
6016            println!("{}", code);
6017        }
6018    }
6019}
6020
6021fn parse_binary(
6022    obj: &goblin::Object,
6023    _data: &[u8],
6024) -> Option<(
6025    rsleigh_api::Architecture,
6026    Vec<(u64, u64, u64)>,
6027    Vec<(u64, String)>,
6028)> {
6029    match obj {
6030        goblin::Object::Mach(goblin::mach::Mach::Binary(m)) => {
6031            let arch = match m.header.cputype() {
6032                7 | 0x01000007 => rsleigh_api::Architecture::X86_64,
6033                12 | 0x0100000c => rsleigh_api::Architecture::AArch64,
6034                _ => return None,
6035            };
6036            let mut segs = Vec::new();
6037            for seg in &m.segments {
6038                if let Ok(secs) = seg.sections() {
6039                    for sec in secs {
6040                        segs.push((sec.0.addr, sec.0.size, sec.0.offset as u64));
6041                    }
6042                }
6043            }
6044            let mut syms = Vec::new();
6045            // Exported/defined symbols
6046            if let Some(ref st) = m.symbols {
6047                for s in st.iter() {
6048                    if let Ok((name, nlist)) = s {
6049                        if nlist.n_type & 0xe == 0xe && nlist.n_value != 0 {
6050                            let clean = name.strip_prefix('_').unwrap_or(name);
6051                            let display =
6052                                demangle_swift_symbol(clean).unwrap_or_else(|| clean.to_string());
6053                            syms.push((nlist.n_value, display));
6054                        }
6055                    }
6056                }
6057            }
6058            // Parse LC_FUNCTION_STARTS — gives ALL function entry points as ULEB128 deltas.
6059            // This is the Mach-O equivalent of PE .pdata — the most reliable function discovery.
6060            let text_vmaddr = m
6061                .segments
6062                .iter()
6063                .find(|s| s.name().ok() == Some("__TEXT"))
6064                .map(|s| s.vmaddr)
6065                .unwrap_or(0);
6066            if text_vmaddr > 0 {
6067                for lc in &m.load_commands {
6068                    if let goblin::mach::load_command::CommandVariant::FunctionStarts(ref fs) =
6069                        lc.command
6070                    {
6071                        let off = fs.dataoff as usize;
6072                        let size = fs.datasize as usize;
6073                        if off + size <= _data.len() {
6074                            let mut pos = off;
6075                            let end = off + size;
6076                            let mut addr = text_vmaddr;
6077                            while pos < end {
6078                                // ULEB128 decode
6079                                let mut delta: u64 = 0;
6080                                let mut shift = 0;
6081                                loop {
6082                                    if pos >= end {
6083                                        break;
6084                                    }
6085                                    let b = _data[pos] as u64;
6086                                    pos += 1;
6087                                    delta |= (b & 0x7f) << shift;
6088                                    shift += 7;
6089                                    if b & 0x80 == 0 {
6090                                        break;
6091                                    }
6092                                }
6093                                if delta == 0 {
6094                                    break;
6095                                }
6096                                addr += delta;
6097                                // Add if not already in symbol list
6098                                if !syms.iter().any(|(a, _)| *a == addr) {
6099                                    syms.push((addr, format!("FUN_{:x}", addr)));
6100                                }
6101                            }
6102                        }
6103                    }
6104                }
6105            }
6106            // Parse ObjC method lists for implementation addresses.
6107            // __objc_methlist contains relative method lists with IMP pointers.
6108            // __objc_const in __DATA contains class_ro_t with baseMethods pointers.
6109            for seg in &m.segments {
6110                if let Ok(secs) = seg.sections() {
6111                    for (sec, _sec_data) in secs {
6112                        let sname = std::str::from_utf8(&sec.sectname)
6113                            .unwrap_or("")
6114                            .trim_end_matches('\0');
6115                        // __objc_stubs: each entry is a small stub (ADRP+LDR+BR on ARM64,
6116                        // JMP on x86_64). Every stub_size-aligned address is a function.
6117                        if sname == "__objc_stubs" || sname == "__stubs" {
6118                            let _soff = sec.offset as usize;
6119                            let ssize = sec.size as usize;
6120                            let saddr = sec.addr;
6121                            // Determine stub size: ARM64=12 bytes, x86_64=8 bytes
6122                            let stub_size: usize =
6123                                if matches!(arch, rsleigh_api::Architecture::AArch64) {
6124                                    12
6125                                } else {
6126                                    8
6127                                };
6128                            let mut pos = 0usize;
6129                            while pos + stub_size <= ssize {
6130                                let addr = saddr + pos as u64;
6131                                if !syms.iter().any(|(a, _)| *a == addr) {
6132                                    syms.push((addr, format!("objc_stub_{:x}", addr)));
6133                                }
6134                                pos += stub_size;
6135                            }
6136                        }
6137                        if sname == "__objc_methlist" {
6138                            // Relative method lists (modern ObjC, ARM64)
6139                            // Each method_list_t: uint32_t entsize_and_flags, uint32_t count
6140                            // Then count × method_t entries (relative offsets)
6141                            let soff = sec.offset as usize;
6142                            let ssize = sec.size as usize;
6143                            let saddr = sec.addr;
6144                            let mut pos = 0usize;
6145                            while pos + 8 <= ssize && soff + pos + 8 <= _data.len() {
6146                                let entsize_flags = u32::from_le_bytes(
6147                                    _data[soff + pos..soff + pos + 4]
6148                                        .try_into()
6149                                        .unwrap_or([0; 4]),
6150                                );
6151                                let count = u32::from_le_bytes(
6152                                    _data[soff + pos + 4..soff + pos + 8]
6153                                        .try_into()
6154                                        .unwrap_or([0; 4]),
6155                                );
6156                                let entsize = (entsize_flags & 0x3FFFFFFF) as usize;
6157                                let is_relative = entsize_flags & 0x80000000 != 0;
6158
6159                                if count > 1000 || entsize == 0 || entsize > 64 {
6160                                    pos += 8;
6161                                    continue;
6162                                }
6163                                let _list_start = pos;
6164
6165                                for m_idx in 0..count as usize {
6166                                    let m_off = soff + pos + 8 + m_idx * entsize;
6167                                    if m_off + entsize > _data.len() {
6168                                        break;
6169                                    }
6170
6171                                    if is_relative && entsize >= 12 {
6172                                        // Relative method_t: int32_t name, int32_t types, int32_t imp
6173                                        // imp is relative to its own address
6174                                        let imp_field_addr =
6175                                            saddr + (pos + 8 + m_idx * entsize + 8) as u64;
6176                                        let imp_rel = i32::from_le_bytes(
6177                                            _data[m_off + 8..m_off + 12]
6178                                                .try_into()
6179                                                .unwrap_or([0; 4]),
6180                                        );
6181                                        let imp =
6182                                            imp_field_addr.wrapping_add(imp_rel as i64 as u64);
6183                                        if !syms.iter().any(|(a, _)| *a == imp) {
6184                                            syms.push((imp, format!("objc_method_{:x}", imp)));
6185                                        }
6186                                    } else if !is_relative && entsize >= 24 {
6187                                        // Absolute method_t: ptr name, ptr types, ptr imp
6188                                        let imp = u64::from_le_bytes(
6189                                            _data[m_off + 16..m_off + 24]
6190                                                .try_into()
6191                                                .unwrap_or([0; 8]),
6192                                        );
6193                                        if imp > 0 && !syms.iter().any(|(a, _)| *a == imp) {
6194                                            syms.push((imp, format!("objc_method_{:x}", imp)));
6195                                        }
6196                                    }
6197                                }
6198                                pos += 8 + count as usize * entsize;
6199                                // Align to 4 bytes
6200                                if pos % 4 != 0 {
6201                                    pos += 4 - (pos % 4);
6202                                }
6203                            }
6204                        }
6205                    }
6206                }
6207            }
6208
6209            Some((arch, segs, syms))
6210        }
6211        goblin::Object::Elf(elf) => {
6212            let arch = match elf.header.e_machine {
6213                0x3E => rsleigh_api::Architecture::X86_64,
6214                0xB7 => rsleigh_api::Architecture::AArch64,
6215                0x28 => rsleigh_api::Architecture::ARM32,
6216                0x08 => rsleigh_api::Architecture::MIPS32,
6217                0xF3 => rsleigh_api::Architecture::RiscV64,
6218                _ => return None,
6219            };
6220            let segs = elf
6221                .section_headers
6222                .iter()
6223                .filter(|sh| sh.sh_flags & 0x4 != 0)
6224                .map(|sh| (sh.sh_addr, sh.sh_size, sh.sh_offset))
6225                .collect();
6226            let mut syms = Vec::new();
6227            for sym in elf.syms.iter() {
6228                if sym.st_type() == goblin::elf::sym::STT_FUNC && sym.st_value != 0 {
6229                    if let Some(name) = elf.strtab.get_at(sym.st_name) {
6230                        if !name.is_empty() {
6231                            syms.push((sym.st_value, demangle_symbol(name)));
6232                        }
6233                    }
6234                }
6235            }
6236            for sym in elf.dynsyms.iter() {
6237                if sym.st_type() == goblin::elf::sym::STT_FUNC && sym.st_value != 0 {
6238                    if let Some(name) = elf.dynstrtab.get_at(sym.st_name) {
6239                        if !name.is_empty() {
6240                            syms.push((sym.st_value, demangle_symbol(name)));
6241                        }
6242                    }
6243                }
6244            }
6245            Some((arch, segs, syms))
6246        }
6247        goblin::Object::PE(pe) => {
6248            // Detect architecture from PE machine type
6249            let arch = match pe.header.coff_header.machine {
6250                0xAA64 => rsleigh_api::Architecture::AArch64, // ARM64
6251                0x8664 => rsleigh_api::Architecture::X86_64,  // AMD64
6252                0x014C => rsleigh_api::Architecture::X86_32,  // i386
6253                0x01C4 => rsleigh_api::Architecture::ARM32,   // ARMv7
6254                _ => {
6255                    if pe.is_64 {
6256                        rsleigh_api::Architecture::X86_64
6257                    } else {
6258                        rsleigh_api::Architecture::X86_32
6259                    }
6260                }
6261            };
6262            let base = pe.image_base as u64;
6263            let segs = pe
6264                .sections
6265                .iter()
6266                .filter(|s| s.characteristics & 0x20000000 != 0)
6267                .map(|s| {
6268                    (
6269                        base + s.virtual_address as u64,
6270                        s.virtual_size as u64,
6271                        s.pointer_to_raw_data as u64,
6272                    )
6273                })
6274                .collect();
6275            let mut syms = Vec::new();
6276            for exp in pe.exports.iter() {
6277                if let Some(name) = exp.name {
6278                    if exp.rva != 0 {
6279                        syms.push((base + exp.rva as u64, name.to_string()));
6280                    }
6281                }
6282            }
6283            Some((arch, segs, syms))
6284        }
6285        _ => None,
6286    }
6287}
6288
6289/// Discover functions in a stripped PE by recursive descent from entry point.
6290/// Follows direct CALL targets to find function boundaries.
6291fn discover_pe_functions(
6292    entry: u64,
6293    segs: &[(u64, u64, u64)],
6294    data: &[u8],
6295    arch: rsleigh_api::Architecture,
6296) -> Vec<(u64, String)> {
6297    use std::collections::{BTreeSet, VecDeque};
6298
6299    let mut found = BTreeSet::new();
6300    let mut queue = VecDeque::new();
6301    found.insert(entry);
6302    queue.push_back(entry);
6303
6304    let mut dec = rsleigh_api::Decoder::new(arch);
6305
6306    while let Some(func_addr) = queue.pop_front() {
6307        // Translate VA to file offset
6308        let off = segs.iter().find_map(|(va, sz, fo)| {
6309            if func_addr >= *va && func_addr < va + sz {
6310                Some(fo + (func_addr - va))
6311            } else {
6312                None
6313            }
6314        });
6315        let Some(off) = off else { continue };
6316        let max = 4096.min(data.len().saturating_sub(off as usize));
6317        if max == 0 {
6318            continue;
6319        }
6320        let bytes = &data[off as usize..off as usize + max];
6321
6322        let mut io = 0usize;
6323        while io < max {
6324            let ok = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
6325                dec.decode(&bytes[io..], func_addr + io as u64)
6326            }));
6327            match ok {
6328                Ok(Ok(inst)) => {
6329                    let l = inst.len as usize;
6330                    if l == 0 {
6331                        io += 1;
6332                        continue;
6333                    }
6334                    // Look for CALL with direct target
6335                    for op in &inst.ops {
6336                        if let pcode_ir::PcodeOp::Call { dest, .. } = op {
6337                            if dest.space == pcode_ir::AddressSpaceId::Ram {
6338                                let call_target = dest.offset;
6339                                // Only follow targets in executable segments
6340                                let in_seg = segs
6341                                    .iter()
6342                                    .any(|(va, sz, _)| call_target >= *va && call_target < va + sz);
6343                                if in_seg && !found.contains(&call_target) {
6344                                    found.insert(call_target);
6345                                    queue.push_back(call_target);
6346                                }
6347                            }
6348                        }
6349                    }
6350                    // Stop at RET
6351                    if inst
6352                        .ops
6353                        .iter()
6354                        .any(|op| matches!(op, pcode_ir::PcodeOp::Return { .. }))
6355                    {
6356                        break;
6357                    }
6358                    io += l;
6359                }
6360                Ok(Err(_)) => break,
6361                Err(_) => {
6362                    io += 1;
6363                }
6364            }
6365        }
6366    }
6367
6368    // Phase 2a: Parse .pdata exception directory for PE64 (gives exact function boundaries)
6369    if let Ok(obj) = goblin::Object::parse(data) {
6370        if let goblin::Object::PE(pe) = &obj {
6371            if pe.is_64 {
6372                let base = pe.image_base as u64;
6373                for sec in &pe.sections {
6374                    let name = std::str::from_utf8(&sec.name)
6375                        .unwrap_or("")
6376                        .trim_end_matches('\0');
6377                    if name == ".pdata" {
6378                        let fo = sec.pointer_to_raw_data as usize;
6379                        let sz = sec.virtual_size.min(sec.size_of_raw_data) as usize;
6380                        if fo + sz <= data.len() {
6381                            // Entry size depends on architecture:
6382                            // x86-64: 12 bytes (BeginAddress:4, EndAddress:4, UnwindData:4)
6383                            // ARM64:  8 bytes (BeginAddress:4, UnwindData:4)
6384                            let pe_off_local =
6385                                u32::from_le_bytes(data[0x3c..0x40].try_into().unwrap_or([0; 4]))
6386                                    as usize;
6387                            let machine = u16::from_le_bytes([
6388                                data[pe_off_local + 4],
6389                                data[pe_off_local + 5],
6390                            ]);
6391                            let entry_size: usize = if machine == 0xAA64 { 8 } else { 12 };
6392
6393                            let mut off = 0;
6394                            while off + entry_size <= sz {
6395                                let begin_rva = u32::from_le_bytes([
6396                                    data[fo + off],
6397                                    data[fo + off + 1],
6398                                    data[fo + off + 2],
6399                                    data[fo + off + 3],
6400                                ]) as u64;
6401                                if begin_rva == 0 {
6402                                    break;
6403                                }
6404                                let func_va = base + begin_rva;
6405                                if !found.contains(&func_va) {
6406                                    let in_seg = segs
6407                                        .iter()
6408                                        .any(|(va, sz, _)| func_va >= *va && func_va < va + sz);
6409                                    if in_seg {
6410                                        found.insert(func_va);
6411                                    }
6412                                }
6413                                off += entry_size;
6414                            }
6415                        }
6416                    }
6417                }
6418            }
6419        }
6420    }
6421
6422    let is_aarch64 = matches!(
6423        arch,
6424        rsleigh_api::Architecture::AArch64 | rsleigh_api::Architecture::ARM32
6425    );
6426
6427    // Phase 2b: Prologue scanning — find functions not reached by direct CALL.
6428    // Scan executable sections for common function prologues:
6429    //   55 8B EC       push ebp; mov ebp, esp  (x86-32 standard)
6430    //   55 89 E5       push ebp; mov esp, ebp  (GCC variant)
6431    //   48 89 5C 24    mov [rsp+...], rbx      (x86-64 MS ABI)
6432    //   48 83 EC       sub rsp, imm8           (x86-64 leaf)
6433    for (seg_va, seg_sz, seg_fo) in segs {
6434        let fo = *seg_fo as usize;
6435        let sz = (*seg_sz as usize).min(data.len().saturating_sub(fo));
6436        if fo + sz > data.len() {
6437            continue;
6438        }
6439        let bytes = &data[fo..fo + sz];
6440
6441        let mut off = 0usize;
6442        while off + 3 <= sz {
6443            let va = seg_va + off as u64;
6444            if !found.contains(&va) {
6445                let boundary = off == 0 || matches!(bytes[off - 1], 0xC3 | 0xCC | 0x90 | 0x00);
6446                let is_prologue =
6447                    // === x86-32 patterns ===
6448                    // push ebp; mov ebp, esp (55 8B EC / 55 89 E5)
6449                    (bytes[off] == 0x55 && off + 3 <= sz
6450                        && ((bytes[off+1] == 0x8B && bytes[off+2] == 0xEC)
6451                            || (bytes[off+1] == 0x89 && bytes[off+2] == 0xE5)))
6452                    // push esi/edi at boundary — only if followed by another push or sub esp
6453                    || (off + 2 <= sz && (bytes[off] == 0x56 || bytes[off] == 0x57)
6454                        && boundary && off > 0
6455                        && matches!(bytes[off+1], 0x53 | 0x55 | 0x56 | 0x57 | 0x83 | 0x8B))
6456                    // mov reg, [esp+4] at boundary
6457                    || (off + 4 <= sz && bytes[off] == 0x8B
6458                        && (bytes[off+1] == 0x44 || bytes[off+1] == 0x4C)
6459                        && bytes[off+2] == 0x24 && bytes[off+3] == 0x04
6460                        && boundary && off > 0)
6461                    // === x86-64 patterns ===
6462                    // sub rsp, imm8 (48 83 EC xx) — standard x86-64 prologue
6463                    || (off + 4 <= sz && bytes[off] == 0x48
6464                        && bytes[off+1] == 0x83 && bytes[off+2] == 0xEC
6465                        && boundary)
6466                    // sub rsp, imm32 (48 81 EC xx xx xx xx) — large frame
6467                    || (off + 7 <= sz && bytes[off] == 0x48
6468                        && bytes[off+1] == 0x81 && bytes[off+2] == 0xEC
6469                        && boundary)
6470                    // push rbp (55) at boundary in 64-bit context
6471                    || (bytes[off] == 0x55 && off + 2 <= sz
6472                        && bytes[off+1] == 0x48  // followed by REX prefix (mov rbp, rsp)
6473                        && boundary)
6474                    // mov [rsp+N], rbx (48 89 5C 24 xx) — Windows x64 ABI
6475                    || (off + 5 <= sz && bytes[off] == 0x48
6476                        && bytes[off+1] == 0x89 && bytes[off+2] == 0x5C
6477                        && bytes[off+3] == 0x24
6478                        && boundary)
6479                    // mov [rsp+N], rdi (48 89 7C 24 xx) — save first param
6480                    || (off + 5 <= sz && bytes[off] == 0x48
6481                        && bytes[off+1] == 0x89 && bytes[off+2] == 0x7C
6482                        && bytes[off+3] == 0x24
6483                        && boundary)
6484                    // push rbx (53) at boundary with REX following (common Win64 prologue)
6485                    || (off + 2 <= sz && bytes[off] == 0x53
6486                        && boundary && off > 0
6487                        && bytes[off+1] == 0x48)
6488                    // push r-prefixed (41 5x) at boundary — push r12..r15
6489                    || (off + 3 <= sz && bytes[off] == 0x41
6490                        && matches!(bytes[off+1], 0x54 | 0x55 | 0x56 | 0x57)
6491                        && boundary && off > 0);
6492
6493                if is_prologue {
6494                    let valid_boundary =
6495                        off == 0 || matches!(bytes[off - 1], 0xC3 | 0xCC | 0x90 | 0x00);
6496                    if valid_boundary {
6497                        found.insert(va);
6498                    }
6499                }
6500            }
6501            off += 1;
6502        }
6503    }
6504
6505    // AArch64 prologue scanning (4-byte aligned instructions)
6506    if is_aarch64 {
6507        for (seg_va, seg_sz, seg_fo) in segs {
6508            let fo = *seg_fo as usize;
6509            let sz = (*seg_sz as usize).min(data.len().saturating_sub(fo));
6510            if fo + sz > data.len() {
6511                continue;
6512            }
6513            let bytes = &data[fo..fo + sz];
6514
6515            let mut off = 0usize;
6516            while off + 4 <= sz {
6517                let va = seg_va + off as u64;
6518                if !found.contains(&va) {
6519                    let insn = u32::from_le_bytes([
6520                        bytes[off],
6521                        bytes[off + 1],
6522                        bytes[off + 2],
6523                        bytes[off + 3],
6524                    ]);
6525
6526                    // Check for AArch64 function prologues:
6527                    // STP X29, X30, [SP, #off] — save FP+LR (both pre-index and signed offset)
6528                    //   Pre-index: A98xxxxx (STP X29,X30,[SP,#-N]!)
6529                    //   Signed offset: A9BF7BFD etc. (STP X29,X30,[SP,#-16])
6530                    // Check: Rt=29(FP), Rt2=30(LR), Rn=31(SP), opc=10 (64-bit)
6531                    let rt = insn & 0x1F;
6532                    let rt2 = (insn >> 10) & 0x1F;
6533                    let rn = (insn >> 5) & 0x1F;
6534                    let is_stp_fp_lr =
6535                        // STP pre-index: A98xxxxx
6536                        ((insn & 0xFFE00000) == 0xA9800000 && rt == 29 && rt2 == 30)
6537                        // STP signed offset: A9xxxxxx where Rt=29, Rt2=30, Rn=31
6538                        || ((insn & 0xFFC00000) == 0xA9000000 && rt == 29 && rt2 == 30 && rn == 31);
6539
6540                    // SUB SP, SP, #imm — stack frame allocation
6541                    let is_sub_sp = (insn & 0xFF0003E0) == 0xD10003E0 && ((insn >> 5) & 0x1F) == 31;
6542
6543                    // STP with SP base (callee-saved register saves, any register pair)
6544                    let _is_stp_sp = (insn & 0xFFC00000) == 0xA9000000 && rn == 31;
6545
6546                    // ADRP — common leaf function start (loads page address)
6547                    let is_adrp = (insn & 0x9F000000) == 0x90000000;
6548
6549                    // MOV X29, SP (set frame pointer without STP — some leaf functions)
6550                    let is_mov_fp_sp = insn == 0x910003FD; // ADD X29, SP, #0
6551
6552                    // LDR from literal pool or GOT — common in position-independent thunks
6553                    let is_ldr_lit = (insn & 0xFF000000) == 0x58000000; // LDR Xt, label
6554
6555                    // Boundary check: previous instruction should be RET (D65F03C0) or 0/padding
6556                    let prev_ok = if off >= 4 {
6557                        let prev_insn = u32::from_le_bytes([
6558                            bytes[off - 4],
6559                            bytes[off - 3],
6560                            bytes[off - 2],
6561                            bytes[off - 1],
6562                        ]);
6563                        prev_insn == 0xD65F03C0  // RET
6564                            || prev_insn == 0x00000000  // padding
6565                            || prev_insn == 0xD503201F  // NOP
6566                            || (prev_insn >> 26) == 0b000101 // B (unconditional branch)
6567                    } else {
6568                        true // start of section
6569                    };
6570
6571                    if is_stp_fp_lr || is_sub_sp {
6572                        // STP FP/LR and SUB SP are strong prologues — accept with loose boundary
6573                        found.insert(va);
6574                    } else if prev_ok && (is_adrp || is_mov_fp_sp || is_ldr_lit) {
6575                        // Weaker patterns — require boundary check
6576                        found.insert(va);
6577                    }
6578                }
6579                off += 4;
6580            }
6581        }
6582    }
6583
6584    // Phase 2c: Exhaustive CALL target scanning.
6585    // Scan all executable sections for CALL instructions and collect targets.
6586    // x86: E8 rel32 (5 bytes)
6587    // AArch64: BL imm26 (4 bytes, opcode 10010100 + 26-bit signed offset)
6588    for (seg_va, seg_sz, seg_fo) in segs {
6589        let fo = *seg_fo as usize;
6590        let sz = (*seg_sz as usize).min(data.len().saturating_sub(fo));
6591        if fo + sz > data.len() {
6592            continue;
6593        }
6594        let bytes = &data[fo..fo + sz];
6595
6596        if is_aarch64 {
6597            // AArch64: BL imm26 — instruction format: 1001_01xx_xxxx_xxxx_xxxx_xxxx_xxxx_xxxx
6598            // Top 6 bits = 100101, bottom 26 bits = signed offset (in instructions, × 4)
6599            let mut off = 0usize;
6600            while off + 4 <= sz {
6601                let insn = u32::from_le_bytes([
6602                    bytes[off],
6603                    bytes[off + 1],
6604                    bytes[off + 2],
6605                    bytes[off + 3],
6606                ]);
6607                if (insn >> 26) == 0b100101 {
6608                    // BL
6609                    let imm26 = insn & 0x03FF_FFFF;
6610                    // Sign-extend 26-bit to 64-bit, multiply by 4
6611                    let offset = if imm26 & 0x0200_0000 != 0 {
6612                        ((imm26 | 0xFC00_0000) as i32 as i64) * 4
6613                    } else {
6614                        (imm26 as i64) * 4
6615                    };
6616                    let target = (seg_va + off as u64).wrapping_add(offset as u64);
6617                    let in_seg = segs
6618                        .iter()
6619                        .any(|(va, sz, _)| target >= *va && target < va + sz);
6620                    if in_seg && !found.contains(&target) {
6621                        found.insert(target);
6622                    }
6623                }
6624                off += 4; // AArch64 instructions are 4-byte aligned
6625            }
6626        } else {
6627            // x86: E8 rel32 (CALL)
6628            let mut off = 0usize;
6629            while off + 5 <= sz {
6630                if bytes[off] == 0xE8 {
6631                    let disp = i32::from_le_bytes([
6632                        bytes[off + 1],
6633                        bytes[off + 2],
6634                        bytes[off + 3],
6635                        bytes[off + 4],
6636                    ]);
6637                    let target = (seg_va + off as u64 + 5).wrapping_add(disp as i64 as u64);
6638                    let in_seg = segs
6639                        .iter()
6640                        .any(|(va, sz, _)| target >= *va && target < va + sz);
6641                    if in_seg && !found.contains(&target) {
6642                        found.insert(target);
6643                    }
6644                }
6645                off += 1;
6646            }
6647        }
6648    }
6649
6650    // Phase 3: Thunk discovery — find JMP [rip+disp] import thunks at function boundaries.
6651    // Only for PE64 — PE32 thunks are already found by the prologue scanner or import resolution.
6652    let is_pe64 = goblin::Object::parse(data)
6653        .ok()
6654        .and_then(|o| {
6655            if let goblin::Object::PE(pe) = o {
6656                Some(pe.is_64)
6657            } else {
6658                None
6659            }
6660        })
6661        .unwrap_or(false);
6662    if is_pe64 {
6663        for (seg_va, seg_sz, seg_fo) in segs {
6664            let fo = *seg_fo as usize;
6665            let sz = (*seg_sz as usize).min(data.len().saturating_sub(fo));
6666            if fo + sz > data.len() {
6667                continue;
6668            }
6669            let bytes = &data[fo..fo + sz];
6670
6671            let mut off = 0usize;
6672            while off + 2 <= sz {
6673                let va = seg_va + off as u64;
6674                if !found.contains(&va) {
6675                    let boundary = off == 0 || matches!(bytes[off - 1], 0xC3 | 0xCC | 0x90 | 0x00);
6676                    if boundary {
6677                        let is_thunk =
6678                        // JMP [rip+disp32]: FF 25 xx xx xx xx (import thunks)
6679                        (off + 6 <= sz && bytes[off] == 0xFF && bytes[off+1] == 0x25)
6680                        // JMP rel32: E9 xx xx xx xx (C++ virtual thunks, tail calls)
6681                        // At function boundaries — preceded by RET/INT3/NOP.
6682                        || (off + 5 <= sz && bytes[off] == 0xE9
6683                            && off > 0 && matches!(bytes[off - 1], 0xC3 | 0xCC | 0x90));
6684
6685                        if is_thunk {
6686                            found.insert(va);
6687                        }
6688                    }
6689                }
6690                off += 1;
6691            }
6692        }
6693    } // end if is_pe64
6694
6695    // Phase 4: Data reference scanning — find function pointers in .rdata/.data sections.
6696    // Vtable entries, C++ exception handler tables, and callback registrations point to
6697    // code addresses that aren't reached by CALL descent.
6698    // Only for PE64 — PE32 has too many false positives from 32-bit values that look like pointers.
6699    if let Ok(obj) = goblin::Object::parse(data) {
6700        if let goblin::Object::PE(pe) = &obj {
6701            if !pe.is_64 { /* skip PE32 */
6702            } else {
6703                let _base = pe.image_base as u64;
6704                // Identify executable address range
6705                let mut text_start = u64::MAX;
6706                let mut text_end = 0u64;
6707                for seg in segs.iter() {
6708                    text_start = text_start.min(seg.0);
6709                    text_end = text_end.max(seg.0 + seg.1);
6710                }
6711
6712                for sec in &pe.sections {
6713                    let name = std::str::from_utf8(&sec.name)
6714                        .unwrap_or("")
6715                        .trim_end_matches('\0');
6716                    if name == ".rdata" || name == ".data" || name == "_RDATA" {
6717                        let fo = sec.pointer_to_raw_data as usize;
6718                        let sz = sec.virtual_size.min(sec.size_of_raw_data) as usize;
6719                        if fo + sz > data.len() {
6720                            continue;
6721                        }
6722                        let ptr_size: usize = 8; // PE64 only
6723
6724                        // Phase 4a: Vtable detection — consecutive function pointer arrays.
6725                        // A vtable is 2+ consecutive 8-byte pointers into .text.
6726                        // All pointers in a vtable are accepted without prologue check
6727                        // (vtable entries include tiny thunks like "mov al, 1; ret" and
6728                        // C++ adjustment thunks like "sub rcx, N; jmp real_method").
6729                        {
6730                            let mut consecutive = 0usize;
6731                            let mut vtable_ptrs: Vec<u64> = Vec::new();
6732                            let mut off = 0usize;
6733                            while off + ptr_size <= sz {
6734                                let ptr = u64::from_le_bytes(
6735                                    data[fo + off..fo + off + 8].try_into().unwrap_or([0; 8]),
6736                                );
6737                                if ptr >= text_start && ptr < text_end {
6738                                    vtable_ptrs.push(ptr);
6739                                    consecutive += 1;
6740                                } else {
6741                                    if consecutive >= 2 {
6742                                        for &vptr in &vtable_ptrs[vtable_ptrs.len() - consecutive..]
6743                                        {
6744                                            found.insert(vptr);
6745                                        }
6746                                    }
6747                                    consecutive = 0;
6748                                }
6749                                off += ptr_size;
6750                            }
6751                            if consecutive >= 2 {
6752                                for &vptr in &vtable_ptrs[vtable_ptrs.len() - consecutive..] {
6753                                    found.insert(vptr);
6754                                }
6755                            }
6756                        }
6757
6758                        // Phase 4b: Single function pointers with strict prologue verification.
6759                        let mut off = 0usize;
6760                        while off + ptr_size <= sz {
6761                            let ptr = u64::from_le_bytes(
6762                                data[fo + off..fo + off + 8].try_into().unwrap_or([0; 8]),
6763                            );
6764
6765                            if ptr >= text_start && ptr < text_end && !found.contains(&ptr) {
6766                                let target_fo = segs.iter().find_map(|(va, sz, sfo)| {
6767                                    if ptr >= *va && ptr < va + sz {
6768                                        Some(sfo + (ptr - va))
6769                                    } else {
6770                                        None
6771                                    }
6772                                });
6773                                if let Some(target_fo) = target_fo {
6774                                    let tfo = target_fo as usize;
6775                                    if tfo + 3 <= data.len() {
6776                                        let (b0, b1, b2) =
6777                                            (data[tfo], data[tfo + 1], data[tfo + 2]);
6778                                        let looks_like_func = (b0 == 0x48 && b1 == 0x83 && b2 == 0xEC)     // sub rsp, imm8
6779                                        || (b0 == 0x48 && b1 == 0x81 && b2 == 0xEC)   // sub rsp, imm32
6780                                        || (b0 == 0x55 && b1 == 0x48)                 // push rbp; REX
6781                                        || (b0 == 0x48 && b1 == 0x89 && (b2 == 0x5C || b2 == 0x7C)) // mov [rsp+N]
6782                                        || (b0 == 0xFF && b1 == 0x25)                 // JMP [rip+disp]
6783                                        || b0 == 0xE9                                 // JMP rel32
6784                                        || (b0 == 0x55 && b1 == 0x8B && b2 == 0xEC)   // push ebp; mov
6785                                        // MSVC: push <reg>; sub rsp, imm8 (reg = rbx/rbp/rsi/rdi)
6786                                        || ((b0 == 0x53 || b0 == 0x55 || b0 == 0x56 || b0 == 0x57)
6787                                            && b1 == 0x48 && b2 == 0x83)
6788                                        // MSVC: push <reg>; sub rsp, imm32
6789                                        || ((b0 == 0x53 || b0 == 0x55 || b0 == 0x56 || b0 == 0x57)
6790                                            && b1 == 0x48 && b2 == 0x81)
6791                                        // MSVC: mov [rsp+0x10], rdx / [rsp+0x18], r8 (arg home)
6792                                        || (b0 == 0x48 && b1 == 0x89 && b2 == 0x54)
6793                                        || (b0 == 0x4C && b1 == 0x89 && b2 == 0x44);
6794                                        if looks_like_func {
6795                                            found.insert(ptr);
6796                                        }
6797                                    }
6798                                }
6799                            }
6800                            off += ptr_size;
6801                        }
6802                    }
6803                }
6804            }
6805        }
6806    }
6807
6808    // Also run PyMethodDef scan during the stripped-PE path so `--all`
6809    // automatically picks up Python-registered methods.
6810    for (addr, name) in scan_pymethoddef(segs, data) {
6811        found.insert(addr);
6812        // Names attached here lose to existing FUN_xxx in the final map;
6813        // that's OK — the standalone caller above owns name attribution.
6814        let _ = name;
6815    }
6816
6817    let sorted: Vec<u64> = found.into_iter().collect();
6818    // Attach import names to thunk stubs so `CALL <thunk_va>` callers
6819    // render as the resolved import (e.g. `URLDownloadToFileW`) rather than
6820    // an opaque `FUN_004038f2` for a 6-byte `FF 25 disp32` JMP-thunk that
6821    // imports.rs has already resolved.
6822    let import_map = rsleigh_decompile::imports::resolve_imports(data);
6823    sorted
6824        .iter()
6825        .map(|addr| {
6826            let name = import_map
6827                .get(addr)
6828                .cloned()
6829                .unwrap_or_else(|| format!("FUN_{:08x}", addr));
6830            (*addr, name)
6831        })
6832        .collect()
6833}
6834
6835/// Scan PE64 data sections for PyMethodDef arrays.
6836///
6837/// A PyMethodDef entry is a 32-byte struct:
6838///   { const char *ml_name; PyCFunction ml_meth; int ml_flags; const char *ml_doc; }
6839/// Arrays are terminated by a zeroed sentinel. Python C-extensions use this to
6840/// expose methods that would otherwise never be called by any function inside
6841/// the module, so neither CALL-target descent nor vtable scanning finds them.
6842///
6843/// Validation rules per entry, all of which must hold:
6844///   * ml_meth  — within the executable address range
6845///   * ml_name  — points to a short (<=64 byte) ASCII identifier, non-empty
6846///   * ml_flags — fits in a u32 and its value is a plausible METH_* bitmask
6847///   * ml_doc   — NULL, or points to ASCII text
6848///
6849/// Returns a list of (function_va, method_name) for each discovered method.
6850fn scan_pymethoddef(segs: &[(u64, u64, u64)], data: &[u8]) -> Vec<(u64, String)> {
6851    let obj = match goblin::Object::parse(data) {
6852        Ok(o) => o,
6853        Err(_) => return vec![],
6854    };
6855    let pe = match obj {
6856        goblin::Object::PE(pe) => pe,
6857        _ => return vec![],
6858    };
6859    if !pe.is_64 {
6860        return vec![];
6861    }
6862
6863    // segs contains only executable sections — used for the .text range
6864    // check. For string lookups we need all readable sections.
6865    let mut text_start = u64::MAX;
6866    let mut text_end = 0u64;
6867    for seg in segs.iter() {
6868        text_start = text_start.min(seg.0);
6869        text_end = text_end.max(seg.0 + seg.1);
6870    }
6871    let base = pe.image_base as u64;
6872    let all_segs: Vec<(u64, u64, u64)> = pe
6873        .sections
6874        .iter()
6875        .filter(|s| (s.characteristics & 0x40000000) != 0) // readable
6876        .map(|s| {
6877            (
6878                base + s.virtual_address as u64,
6879                s.virtual_size.min(s.size_of_raw_data) as u64,
6880                s.pointer_to_raw_data as u64,
6881            )
6882        })
6883        .collect();
6884    let va_to_fo = |va: u64| -> Option<usize> {
6885        all_segs.iter().find_map(|(v, s, fo)| {
6886            if va >= *v && va < v + s {
6887                Some(*fo as usize + (va - v) as usize)
6888            } else {
6889                None
6890            }
6891        })
6892    };
6893    // Strict C identifier (used for ml_name)
6894    let read_ident = |va: u64| -> Option<String> {
6895        let fo = va_to_fo(va)?;
6896        if fo >= data.len() {
6897            return None;
6898        }
6899        let slice = &data[fo..data.len().min(fo + 128)];
6900        let end = slice.iter().position(|&b| b == 0)?;
6901        if end == 0 || end > 64 {
6902            return None;
6903        }
6904        let s = &slice[..end];
6905        if !s.iter().all(|&b| b == b'_' || b.is_ascii_alphanumeric()) {
6906            return None;
6907        }
6908        Some(String::from_utf8_lossy(s).into_owned())
6909    };
6910    // Loose printable-ASCII check (used for ml_doc — doc strings contain
6911    // spaces, punctuation, newlines). An empty string (first byte is NUL)
6912    // is accepted as equivalent to a NULL doc.
6913    let read_text_ok = |va: u64| -> bool {
6914        let Some(fo) = va_to_fo(va) else {
6915            return false;
6916        };
6917        if fo >= data.len() {
6918            return false;
6919        }
6920        let slice = &data[fo..data.len().min(fo + 512)];
6921        let end = match slice.iter().position(|&b| b == 0) {
6922            Some(e) => e,
6923            None => return false,
6924        };
6925        slice[..end]
6926            .iter()
6927            .all(|&b| b == b'\n' || b == b'\t' || (0x20..=0x7e).contains(&b))
6928    };
6929
6930    let mut out: Vec<(u64, String)> = Vec::new();
6931    for sec in &pe.sections {
6932        let ch = sec.characteristics;
6933        let is_read = (ch & 0x40000000) != 0;
6934        let is_exec = (ch & 0x20000000) != 0;
6935        let is_init = (ch & 0x00000040) != 0;
6936        if !is_read || is_exec || !is_init {
6937            continue;
6938        }
6939        let fo = sec.pointer_to_raw_data as usize;
6940        let sz = sec.virtual_size.min(sec.size_of_raw_data) as usize;
6941        if fo + sz > data.len() || sz < 32 {
6942            continue;
6943        }
6944
6945        let mut off = 0usize;
6946        while off + 32 <= sz {
6947            let rd_q = |o: usize| {
6948                u64::from_le_bytes(data[fo + o..fo + o + 8].try_into().unwrap_or([0; 8]))
6949            };
6950            let ml_name = rd_q(off);
6951            let ml_meth = rd_q(off + 8);
6952            let ml_flags_q = rd_q(off + 16);
6953            let ml_doc = rd_q(off + 24);
6954
6955            let ml_flags_hi = (ml_flags_q >> 32) as u32;
6956            let ml_flags = ml_flags_q as u32;
6957
6958            let meth_ok = ml_meth >= text_start && ml_meth < text_end;
6959            let flags_ok = ml_flags_hi == 0 && ml_flags < 0x1000;
6960            let name_str = if meth_ok && flags_ok {
6961                read_ident(ml_name)
6962            } else {
6963                None
6964            };
6965            let doc_ok = ml_doc == 0 || read_text_ok(ml_doc);
6966
6967            if meth_ok && flags_ok && doc_ok && name_str.is_some() {
6968                out.push((ml_meth, name_str.unwrap()));
6969                off += 32;
6970                continue;
6971            }
6972            off += 8;
6973        }
6974    }
6975    out
6976}
6977
6978/// Discover functions in a stripped ELF binary.
6979/// Uses entry point, CALL scanning, prologue patterns, PLT enumeration, and .init_array.
6980fn discover_elf_functions(
6981    elf: &goblin::elf::Elf,
6982    segs: &[(u64, u64, u64)],
6983    data: &[u8],
6984    arch: rsleigh_api::Architecture,
6985) -> Vec<(u64, String)> {
6986    use std::collections::BTreeSet;
6987
6988    let mut found = BTreeSet::new();
6989
6990    // Detect endianness and pointer size from ELF header
6991    let is_big_endian = elf
6992        .header
6993        .endianness()
6994        .unwrap_or(goblin::container::Endian::Little)
6995        == goblin::container::Endian::Big;
6996    let is_32bit = elf.header.e_machine == 0x08 // MIPS
6997        || elf.header.e_machine == 0x28          // ARM
6998        || (elf.header.e_machine == 0x03 && elf.header.e_ident[4] == 1); // x86 32-bit
6999    let ptr_size: usize = if is_32bit { 4 } else { 8 };
7000
7001    // Endian-aware pointer reading helpers
7002    let read_u32_elf = |bytes: &[u8]| -> u32 {
7003        if is_big_endian {
7004            u32::from_be_bytes(bytes[..4].try_into().unwrap_or([0; 4]))
7005        } else {
7006            u32::from_le_bytes(bytes[..4].try_into().unwrap_or([0; 4]))
7007        }
7008    };
7009    let read_u64_elf = |bytes: &[u8]| -> u64 {
7010        if is_big_endian {
7011            u64::from_be_bytes(bytes[..8].try_into().unwrap_or([0; 8]))
7012        } else {
7013            u64::from_le_bytes(bytes[..8].try_into().unwrap_or([0; 8]))
7014        }
7015    };
7016    let read_i32_elf = |bytes: &[u8]| -> i32 {
7017        if is_big_endian {
7018            i32::from_be_bytes(bytes[..4].try_into().unwrap_or([0; 4]))
7019        } else {
7020            i32::from_le_bytes(bytes[..4].try_into().unwrap_or([0; 4]))
7021        }
7022    };
7023    let read_ptr_elf = |bytes: &[u8]| -> u64 {
7024        if is_32bit {
7025            read_u32_elf(bytes) as u64
7026        } else {
7027            read_u64_elf(bytes)
7028        }
7029    };
7030    let read_i64_elf = |bytes: &[u8]| -> i64 {
7031        if is_big_endian {
7032            i64::from_be_bytes(bytes[..8].try_into().unwrap_or([0; 8]))
7033        } else {
7034            i64::from_le_bytes(bytes[..8].try_into().unwrap_or([0; 8]))
7035        }
7036    };
7037
7038    // 1. Entry point
7039    let entry = elf.header.e_entry;
7040    if entry != 0 {
7041        found.insert(entry);
7042    }
7043
7044    // 2. .init and .fini section addresses
7045    for sh in &elf.section_headers {
7046        let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
7047        if (name == ".init" || name == ".fini") && sh.sh_addr != 0 {
7048            found.insert(sh.sh_addr);
7049        }
7050        // .init_array / .fini_array contain function pointers
7051        if (name == ".init_array" || name == ".fini_array") && sh.sh_size > 0 {
7052            let fo = sh.sh_offset as usize;
7053            let count = (sh.sh_size as usize) / ptr_size;
7054            for i in 0..count {
7055                if fo + i * ptr_size + ptr_size <= data.len() {
7056                    let ptr = read_ptr_elf(&data[fo + i * ptr_size..]);
7057                    if ptr != 0 && ptr != u64::MAX && ptr != 0xFFFFFFFF {
7058                        found.insert(ptr);
7059                    }
7060                }
7061            }
7062        }
7063    }
7064
7065    // 3. PLT entries — each is a small stub that jumps to a GOT entry
7066    for sh in &elf.section_headers {
7067        let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
7068        if name.starts_with(".plt") && sh.sh_addr != 0 && sh.sh_size > 0 {
7069            // PLT entries are typically 16 bytes each (first entry is special)
7070            let entry_size = if sh.sh_entsize > 0 { sh.sh_entsize } else { 16 };
7071            let mut addr = sh.sh_addr + entry_size; // skip PLT[0]
7072            while addr < sh.sh_addr + sh.sh_size {
7073                found.insert(addr);
7074                addr += entry_size;
7075            }
7076        }
7077    }
7078
7079    // 4. Find .text section bounds for CALL scanning
7080    let text_section = elf
7081        .section_headers
7082        .iter()
7083        .find(|sh| elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("") == ".text");
7084
7085    if let Some(text) = text_section {
7086        let text_addr = text.sh_addr;
7087        let text_size = text.sh_size;
7088        let text_fo = text.sh_offset as usize;
7089        let text_end = text_addr + text_size;
7090
7091        // 4b. Architecture-specific raw CALL scanning for initial seeds.
7092        if text_fo + text_size as usize <= data.len() {
7093            let text_bytes = &data[text_fo..text_fo + text_size as usize];
7094
7095            if matches!(arch, rsleigh_api::Architecture::ARM32) {
7096                // ARM32 BL (Branch with Link): condition[31:28] 1011 imm24
7097                // Encoding: cccc 1011 xxxx xxxx xxxx xxxx xxxx xxxx
7098                // Byte pattern: xx xx xx xB (little-endian, top nibble of byte[3] is cond, byte[3]&0x0F == 0x0B)
7099                // Most common: 0xEB (AL condition = always)
7100                for i in (0..text_bytes.len().saturating_sub(3)).step_by(4) {
7101                    let word =
7102                        u32::from_le_bytes(text_bytes[i..i + 4].try_into().unwrap_or([0; 4]));
7103                    let is_bl = (word & 0x0F000000) == 0x0B000000; // BL opcode
7104                    if is_bl {
7105                        let imm24 = word & 0x00FFFFFF;
7106                        // Sign-extend 24-bit immediate
7107                        let offset = if imm24 & 0x800000 != 0 {
7108                            ((imm24 | 0xFF000000) as i32) << 2
7109                        } else {
7110                            (imm24 as i32) << 2
7111                        };
7112                        // PC is at instruction + 8 in ARM mode
7113                        let target = (text_addr as i64 + i as i64 + 8 + offset as i64) as u64;
7114                        if target >= text_addr && target < text_end {
7115                            found.insert(target);
7116                        }
7117                    }
7118                }
7119
7120                // ARM32 PUSH {regs, lr} prologue: E92D xxxx where xxxx has bit 14 set (LR)
7121                for i in (0..text_bytes.len().saturating_sub(3)).step_by(4) {
7122                    let word =
7123                        u32::from_le_bytes(text_bytes[i..i + 4].try_into().unwrap_or([0; 4]));
7124                    // STMDB SP!, {regs} = E92D xxxx (PUSH)
7125                    if (word & 0xFFFF0000) == 0xE92D0000 {
7126                        let reglist = word & 0xFFFF;
7127                        if reglist & (1 << 14) != 0 {
7128                            // LR in register list
7129                            // Verify: preceded by function boundary (previous word is a return)
7130                            if i == 0 || {
7131                                let prev = u32::from_le_bytes(
7132                                    text_bytes[i - 4..i].try_into().unwrap_or([0; 4]),
7133                                );
7134                                // BX LR = E12FFF1E, POP {pc} = E8BD8xxx, MOV PC, LR = E1A0F00E
7135                                (prev & 0x0FFFFFFF) == 0x012FFF1E // BX LR
7136                                || (prev & 0xFFFF0000) == 0xE8BD0000 && (prev & 0x8000) != 0 // POP {.., PC}
7137                                || prev == 0xE1A0F00E // MOV PC, LR
7138                                || prev == 0x00000000 // padding
7139                            } {
7140                                found.insert(text_addr + i as u64);
7141                            }
7142                        }
7143                    }
7144
7145                    // Thumb PUSH {regs, lr}: B5xx (16-bit)
7146                    // Check both halfwords in this 4-byte window
7147                    for off in [0usize, 2] {
7148                        if i + off + 1 < text_bytes.len() {
7149                            let hw = u16::from_le_bytes(
7150                                text_bytes[i + off..i + off + 2]
7151                                    .try_into()
7152                                    .unwrap_or([0; 2]),
7153                            );
7154                            if (hw & 0xFF00) == 0xB500 {
7155                                // PUSH {.., LR}
7156                                let addr = text_addr + (i + off) as u64;
7157                                if !found.contains(&addr) {
7158                                    // Thumb PUSH at aligned boundary
7159                                    if off == 0 || {
7160                                        let prev_hw = u16::from_le_bytes(
7161                                            text_bytes[i + off - 2..i + off]
7162                                                .try_into()
7163                                                .unwrap_or([0; 2]),
7164                                        );
7165                                        // POP {.., PC} = BDxx, BX LR = 4770
7166                                        (prev_hw & 0xFF00) == 0xBD00
7167                                            || prev_hw == 0x4770
7168                                            || prev_hw == 0x0000
7169                                    } {
7170                                        found.insert(addr);
7171                                    }
7172                                }
7173                            }
7174                        }
7175                    }
7176                }
7177
7178                // Thumb BL: F000 F800-FFFF (32-bit Thumb instruction)
7179                for i in 0..text_bytes.len().saturating_sub(3) {
7180                    let hw1 = u16::from_le_bytes(text_bytes[i..i + 2].try_into().unwrap_or([0; 2]));
7181                    let hw2 =
7182                        u16::from_le_bytes(text_bytes[i + 2..i + 4].try_into().unwrap_or([0; 2]));
7183                    // BL: hw1[15:11] = 11110, hw2[15:12] = 1101 (BL) or 1100 (BLX)
7184                    if (hw1 & 0xF800) == 0xF000 && (hw2 & 0xD000) == 0xD000 {
7185                        let s = ((hw1 >> 10) & 1) as i32;
7186                        let imm10 = (hw1 & 0x3FF) as i32;
7187                        let j1 = ((hw2 >> 13) & 1) as i32;
7188                        let j2 = ((hw2 >> 11) & 1) as i32;
7189                        let imm11 = (hw2 & 0x7FF) as i32;
7190                        let i1 = !(j1 ^ s) & 1;
7191                        let i2 = !(j2 ^ s) & 1;
7192                        let offset = if s != 0 {
7193                            (0xFF000000u32 as i32)
7194                                | (s << 24)
7195                                | (i1 << 23)
7196                                | (i2 << 22)
7197                                | (imm10 << 12)
7198                                | (imm11 << 1)
7199                        } else {
7200                            (i1 << 23) | (i2 << 22) | (imm10 << 12) | (imm11 << 1)
7201                        };
7202                        let target = (text_addr as i64 + i as i64 + 4 + offset as i64) as u64;
7203                        if target >= text_addr && target < text_end {
7204                            found.insert(target);
7205                        }
7206                    }
7207                }
7208            }
7209
7210            if matches!(arch, rsleigh_api::Architecture::AArch64) {
7211                // AArch64 BL: 1001 01xx xxxx xxxx xxxx xxxx xxxx xxxx = 0x94000000 mask 0xFC000000
7212                for i in (0..text_bytes.len().saturating_sub(3)).step_by(4) {
7213                    let word =
7214                        u32::from_le_bytes(text_bytes[i..i + 4].try_into().unwrap_or([0; 4]));
7215                    if (word & 0xFC000000) == 0x94000000 {
7216                        let imm26 = word & 0x03FFFFFF;
7217                        let offset = if imm26 & 0x02000000 != 0 {
7218                            ((imm26 | 0xFC000000) as i32) << 2
7219                        } else {
7220                            (imm26 as i32) << 2
7221                        };
7222                        let target = (text_addr as i64 + i as i64 + offset as i64) as u64;
7223                        if target >= text_addr && target < text_end {
7224                            found.insert(target);
7225                        }
7226                    }
7227                }
7228            }
7229
7230            if matches!(arch, rsleigh_api::Architecture::MIPS32) {
7231                // MIPS JAL (Jump And Link): opcode 000011 imm26
7232                // Target: (PC & 0xF0000000) | (imm26 << 2)
7233                // Endianness comes from the ELF header — both BE (mips)
7234                // and LE (mipsel) are common in IoT samples.
7235                for i in (0..text_bytes.len().saturating_sub(3)).step_by(4) {
7236                    let word = read_u32_elf(&text_bytes[i..i + 4]);
7237                    if (word >> 26) == 3 {
7238                        // JAL opcode
7239                        let imm26 = word & 0x03FFFFFF;
7240                        let target = ((text_addr + i as u64) & 0xF0000000) | ((imm26 as u64) << 2);
7241                        if target >= text_addr && target < text_end && target % 4 == 0 {
7242                            found.insert(target);
7243                        }
7244                    }
7245                }
7246
7247                // BAL (Branch And Link) / BGEZAL: opcode=000001 rt=10001 imm16
7248                for i in (0..text_bytes.len().saturating_sub(3)).step_by(4) {
7249                    let word = read_u32_elf(&text_bytes[i..i + 4]);
7250                    let opcode = word >> 26;
7251                    let rt = (word >> 16) & 0x1F;
7252                    if opcode == 1 && rt == 17 {
7253                        // BGEZAL/BAL
7254                        let imm16 = (word & 0xFFFF) as i16;
7255                        let offset = (imm16 as i64) << 2;
7256                        let pc = text_addr + i as u64 + 4; // delay slot: PC+4
7257                        let target = (pc as i64 + offset) as u64;
7258                        if target >= text_addr && target < text_end && target % 4 == 0 {
7259                            found.insert(target);
7260                        }
7261                    }
7262                }
7263            }
7264        }
7265
7266        // 5. Decoder-based CALL target discovery with indirect call resolution.
7267        // Decode from known function starts, track register values via LEA/MOV,
7268        // and resolve both direct CALL 0xNNNN and indirect CALL RAX/CALL [RIP+N].
7269        {
7270            let mut dec = rsleigh_api::Decoder::new(arch);
7271            let mut new_targets = BTreeSet::new();
7272            let max_seeds = 2000;
7273
7274            // Helper: read a pointer from a virtual address in the binary
7275            let read_ptr = |va: u64| -> Option<u64> {
7276                let off = segs.iter().find_map(|(sva, sz, fo)| {
7277                    if va >= *sva && va < sva + sz {
7278                        Some((fo + (va - sva)) as usize)
7279                    } else {
7280                        None
7281                    }
7282                })?;
7283                if off + ptr_size <= data.len() {
7284                    Some(read_ptr_elf(&data[off..]))
7285                } else {
7286                    None
7287                }
7288            };
7289
7290            // Also scan .text raw bytes for CALL [RIP+disp32] (FF 15 XX XX XX XX)
7291            // These are indirect calls through GOT — the GOT entry may contain
7292            // a resolved function address (for statically linked or pre-resolved).
7293            if text_fo + text_size as usize <= data.len() {
7294                let text_bytes = &data[text_fo..text_fo + text_size as usize];
7295                for i in 0..text_bytes.len().saturating_sub(6) {
7296                    if text_bytes[i] == 0xFF && text_bytes[i + 1] == 0x15 {
7297                        // CALL [RIP+disp32]
7298                        let disp = i32::from_le_bytes(
7299                            text_bytes[i + 2..i + 6].try_into().unwrap_or([0; 4]),
7300                        );
7301                        let got_va = (text_addr as i64 + i as i64 + 6 + disp as i64) as u64;
7302                        if let Some(target) = read_ptr(got_va) {
7303                            if target >= text_addr && target < text_end {
7304                                new_targets.insert(target);
7305                            }
7306                        }
7307                    }
7308                    // JMP [RIP+disp32] (FF 25 XX XX XX XX) — PLT-style indirect jump
7309                    if text_bytes[i] == 0xFF && text_bytes[i + 1] == 0x25 {
7310                        let disp = i32::from_le_bytes(
7311                            text_bytes[i + 2..i + 6].try_into().unwrap_or([0; 4]),
7312                        );
7313                        let got_va = (text_addr as i64 + i as i64 + 6 + disp as i64) as u64;
7314                        if let Some(target) = read_ptr(got_va) {
7315                            if target >= text_addr && target < text_end {
7316                                new_targets.insert(target);
7317                            }
7318                        }
7319                    }
7320                }
7321            }
7322            for t in &new_targets {
7323                found.insert(*t);
7324            }
7325            new_targets.clear();
7326
7327            // Decoder-based discovery with register tracking
7328            for _round in 0..2 {
7329                let start_count = found.len();
7330                let seeds: Vec<u64> = found
7331                    .iter()
7332                    .filter(|a| **a >= text_addr && **a < text_end)
7333                    .take(max_seeds)
7334                    .copied()
7335                    .collect();
7336                for func_addr in seeds {
7337                    let off = segs.iter().find_map(|(va, sz, fo)| {
7338                        if func_addr >= *va && func_addr < va + sz {
7339                            Some((fo + (func_addr - va)) as usize)
7340                        } else {
7341                            None
7342                        }
7343                    });
7344                    let Some(off) = off else { continue };
7345                    let max = 4096.min(data.len().saturating_sub(off));
7346                    if max < 2 {
7347                        continue;
7348                    }
7349                    let bytes = &data[off..off + max];
7350
7351                    // Mini register tracker: maps register name → known address value
7352                    let mut reg_vals: std::collections::HashMap<String, u64> =
7353                        std::collections::HashMap::new();
7354                    let mut pos = 0;
7355                    for _ in 0..500 {
7356                        if pos + 1 >= bytes.len() {
7357                            break;
7358                        }
7359                        if let Ok(inst) = dec.decode(&bytes[pos..], func_addr + pos as u64) {
7360                            let sz = inst.len as usize;
7361                            if sz == 0 {
7362                                break;
7363                            }
7364                            let dis = &inst.disassembly;
7365                            let _inst_addr = func_addr + pos as u64;
7366
7367                            // Track LEA reg, [RIP+disp] → reg = computed address
7368                            if dis.starts_with("LEA ") {
7369                                let parts: Vec<&str> =
7370                                    dis.splitn(3, |c: char| c == ',' || c == ' ').collect();
7371                                if parts.len() >= 3 {
7372                                    let dest = parts[1].trim().trim_end_matches(',');
7373                                    let src = parts[2].trim();
7374                                    // LEA with immediate address: "LEA RAX,0xNNNN" or "LEA RAX,[0xNNNN]"
7375                                    let addr_str =
7376                                        src.trim_start_matches('[').trim_end_matches(']');
7377                                    if let Some(hex) = addr_str.strip_prefix("0x") {
7378                                        if let Ok(addr) = u64::from_str_radix(hex, 16) {
7379                                            reg_vals.insert(dest.to_string(), addr);
7380                                        }
7381                                    }
7382                                }
7383                            }
7384
7385                            // Track MOV reg, imm → reg = constant
7386                            if dis.starts_with("MOV ") && !dis.contains('[') {
7387                                let parts: Vec<&str> =
7388                                    dis.splitn(3, |c: char| c == ',' || c == ' ').collect();
7389                                if parts.len() >= 3 {
7390                                    let dest = parts[1].trim().trim_end_matches(',');
7391                                    let src = parts[2].trim();
7392                                    if let Some(hex) = src.strip_prefix("0x") {
7393                                        if let Ok(val) = u64::from_str_radix(hex, 16) {
7394                                            reg_vals.insert(dest.to_string(), val);
7395                                        }
7396                                    }
7397                                }
7398                            }
7399
7400                            // Collect CALL targets — both direct and indirect
7401                            if dis.starts_with("CALL ") {
7402                                let target_part = &dis[5..];
7403                                if let Some(hex) = target_part.trim().strip_prefix("0x") {
7404                                    // Direct CALL 0xNNNN
7405                                    if let Ok(target) = u64::from_str_radix(hex, 16) {
7406                                        if target >= text_addr && target < text_end {
7407                                            new_targets.insert(target);
7408                                        }
7409                                    }
7410                                } else if target_part.contains('[') {
7411                                    // Indirect CALL [addr] — try to resolve via P-code ops
7412                                    // Parse: "CALL dword ptr [0xNNNN]" or "CALL qword ptr [RIP + 0xNN]"
7413                                    let bracket_content = target_part
7414                                        .split('[')
7415                                        .nth(1)
7416                                        .unwrap_or("")
7417                                        .split(']')
7418                                        .next()
7419                                        .unwrap_or("");
7420                                    if let Some(hex) = bracket_content.strip_prefix("0x") {
7421                                        if let Ok(mem_addr) = u64::from_str_radix(hex, 16) {
7422                                            if let Some(target) = read_ptr(mem_addr) {
7423                                                if target >= text_addr && target < text_end {
7424                                                    new_targets.insert(target);
7425                                                }
7426                                            }
7427                                        }
7428                                    }
7429                                } else {
7430                                    // Indirect CALL REG — resolve from tracked register value
7431                                    let reg = target_part.trim();
7432                                    if let Some(&val) = reg_vals.get(reg) {
7433                                        if val >= text_addr && val < text_end {
7434                                            new_targets.insert(val);
7435                                        }
7436                                    }
7437                                }
7438                            }
7439
7440                            // MIPS: collect JAL targets from decoded instructions
7441                            // MIPS JAL disassembles as "jal 0xNNNNNNNN"
7442                            if dis.starts_with("jal ") {
7443                                let target_part = &dis[4..];
7444                                if let Some(hex) = target_part.trim().strip_prefix("0x") {
7445                                    if let Ok(target) = u64::from_str_radix(hex, 16) {
7446                                        if target >= text_addr && target < text_end {
7447                                            new_targets.insert(target);
7448                                        }
7449                                    }
7450                                }
7451                            }
7452                            // MIPS: "bal 0xNNNN" (branch and link)
7453                            if dis.starts_with("bal ")
7454                                || dis.starts_with("bgezal ")
7455                                || dis.starts_with("bltzal ")
7456                            {
7457                                let target_part = dis.split_whitespace().last().unwrap_or("");
7458                                if let Some(hex) = target_part.strip_prefix("0x") {
7459                                    if let Ok(target) = u64::from_str_radix(hex, 16) {
7460                                        if target >= text_addr && target < text_end {
7461                                            new_targets.insert(target);
7462                                        }
7463                                    }
7464                                }
7465                            }
7466
7467                            // Invalidate destination register on any other write
7468                            // (simplistic: CALL clobbers RAX, other writes clobber dest)
7469                            if dis.starts_with("CALL ") {
7470                                reg_vals.remove("RAX");
7471                                reg_vals.remove("RCX");
7472                                reg_vals.remove("RDX");
7473                                reg_vals.remove("RSI");
7474                                reg_vals.remove("RDI");
7475                                reg_vals.remove("R8");
7476                                reg_vals.remove("R9");
7477                                reg_vals.remove("R10");
7478                                reg_vals.remove("R11");
7479                            }
7480
7481                            // Terminators: x86 RET/HLT, MIPS JR RA (jr ra)
7482                            if dis.starts_with("RET") || dis.starts_with("HLT") || dis == "jr ra" {
7483                                break;
7484                            }
7485                            pos += sz;
7486                        } else {
7487                            break;
7488                        }
7489                    }
7490                }
7491                for t in &new_targets {
7492                    found.insert(*t);
7493                }
7494                new_targets.clear();
7495                if found.len() == start_count {
7496                    break;
7497                }
7498            }
7499        }
7500
7501        // 5b. Parse .eh_frame_hdr for function addresses.
7502        // The .eh_frame_hdr contains a sorted table of (PC, FDE) pairs — every function
7503        // with exception handling or unwind info has an entry here. This is authoritative.
7504        for sh in &elf.section_headers {
7505            let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
7506            if name != ".eh_frame_hdr" {
7507                continue;
7508            }
7509            let fo = sh.sh_offset as usize;
7510            let hdr_addr = sh.sh_addr;
7511            if fo + 12 > data.len() {
7512                break;
7513            }
7514            let version = data[fo];
7515            if version != 1 {
7516                break;
7517            }
7518            let fde_count_enc = data[fo + 2];
7519            let table_enc = data[fo + 3];
7520            // Read FDE count (offset 8, encoding determines size)
7521            let fde_count = match fde_count_enc {
7522                0x03 => read_i32_elf(&data[fo + 8..]) as usize,
7523                _ => read_u32_elf(&data[fo + 8..]) as usize,
7524            };
7525            if fde_count == 0 || fde_count > 100_000 {
7526                break;
7527            }
7528            let table_start = fo + 12;
7529            // Table encoding 0x3b = DW_EH_PE_datarel | DW_EH_PE_sdata4 (most common)
7530            if table_enc == 0x3b {
7531                for i in 0..fde_count {
7532                    let entry_off = table_start + i * 8;
7533                    if entry_off + 4 > data.len() {
7534                        break;
7535                    }
7536                    let pc_rel = read_i32_elf(&data[entry_off..]);
7537                    let pc = (hdr_addr as i64 + pc_rel as i64) as u64;
7538                    if pc > 0 && pc < text_end + 0x10000 {
7539                        found.insert(pc);
7540                    }
7541                }
7542            }
7543        }
7544
7545        // 5b2. Parse .eh_frame directly for FDE initial_location addresses.
7546        // Complements .eh_frame_hdr: catches FDEs not in the index and works when
7547        // .eh_frame_hdr is missing. Each FDE has a PC-relative initial_location.
7548        for sh in &elf.section_headers {
7549            let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
7550            if name != ".eh_frame" {
7551                continue;
7552            }
7553            let ef_addr = sh.sh_addr;
7554            let ef_off = sh.sh_offset as usize;
7555            let ef_size = sh.sh_size as usize;
7556            if ef_off + ef_size > data.len() {
7557                break;
7558            }
7559
7560            let mut pos = 0;
7561            while pos + 8 < ef_size {
7562                let fo = ef_off + pos;
7563                let length = read_u32_elf(&data[fo..]) as usize;
7564                if length == 0 {
7565                    break;
7566                } // terminator
7567                if length > ef_size - pos {
7568                    break;
7569                } // corrupt
7570                let record_start = pos + 4;
7571                let cie_id = read_u32_elf(&data[fo + 4..]);
7572
7573                if cie_id != 0 {
7574                    // FDE: initial_location is at offset 8 from record start,
7575                    // encoded as sdata4 PC-relative (most common for gcc/clang)
7576                    let iloc_off = fo + 8;
7577                    if iloc_off + 4 <= data.len() {
7578                        let iloc_rel = read_i32_elf(&data[iloc_off..]);
7579                        let iloc =
7580                            (ef_addr as i64 + (iloc_off - ef_off) as i64 + iloc_rel as i64) as u64;
7581                        if iloc > 0 && iloc < text_end + 0x10000 {
7582                            found.insert(iloc);
7583                        }
7584                    }
7585                }
7586                pos = record_start + length;
7587            }
7588        }
7589
7590        // 5b3. C++ RTTI vtable chain walking.
7591        // Vtable layout: [offset_to_top(8)] [typeinfo_ptr(8)] [vfunc0(8)] [vfunc1(8)] ...
7592        // Identify vtables by: offset_to_top is 0 or small, typeinfo_ptr points to
7593        // .data.rel.ro/.rodata, and next entries point into .text.
7594        for sh in &elf.section_headers {
7595            let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
7596            if name != ".data.rel.ro" {
7597                continue;
7598            }
7599            let _sec_addr = sh.sh_addr;
7600            let sec_off = sh.sh_offset as usize;
7601            let sec_size = sh.sh_size as usize;
7602            if sec_off + sec_size > data.len() || sec_size < 24 {
7603                continue;
7604            }
7605
7606            // Collect all data section address ranges for typeinfo pointer validation
7607            let data_sections: Vec<(u64, u64)> = elf
7608                .section_headers
7609                .iter()
7610                .filter(|s| {
7611                    let n = elf.shdr_strtab.get_at(s.sh_name).unwrap_or("");
7612                    matches!(n, ".data.rel.ro" | ".rodata" | ".data")
7613                })
7614                .map(|s| (s.sh_addr, s.sh_addr + s.sh_size))
7615                .collect();
7616            let in_data = |addr: u64| -> bool {
7617                data_sections
7618                    .iter()
7619                    .any(|(start, end)| addr >= *start && addr < *end)
7620            };
7621
7622            let mut i = 0;
7623            while i + 3 * ptr_size <= sec_size {
7624                let offset_to_top = if is_32bit {
7625                    read_i32_elf(&data[sec_off + i..]) as i64
7626                } else {
7627                    read_i64_elf(&data[sec_off + i..])
7628                };
7629                let typeinfo_ptr = read_ptr_elf(&data[sec_off + i + ptr_size..]);
7630                let first_entry = read_ptr_elf(&data[sec_off + i + 2 * ptr_size..]);
7631
7632                // Vtable heuristic: offset_to_top is 0 or small, typeinfo points to data,
7633                // first entry points to executable code
7634                if offset_to_top.unsigned_abs() <= 1024
7635                    && in_data(typeinfo_ptr)
7636                    && first_entry >= text_addr
7637                    && first_entry < text_end
7638                {
7639                    // Walk virtual function entries
7640                    let mut j = 2 * ptr_size;
7641                    while i + j + ptr_size <= sec_size {
7642                        let vfunc = read_ptr_elf(&data[sec_off + i + j..]);
7643                        if vfunc >= text_addr && vfunc < text_end {
7644                            found.insert(vfunc);
7645                            j += ptr_size;
7646                        } else {
7647                            break;
7648                        }
7649                    }
7650                    i += j; // skip past vtable
7651                } else {
7652                    i += ptr_size;
7653                }
7654            }
7655        }
7656
7657        // 5c. Full data section pointer scan — find ALL 8-byte values pointing into executable code
7658        // Covers vtables, function pointer arrays, switch jump tables, C++ RTTI
7659        {
7660            let _all_exec_start = elf
7661                .section_headers
7662                .iter()
7663                .filter(|sh| sh.sh_flags & 0x4 != 0 && sh.sh_addr > 0)
7664                .map(|sh| sh.sh_addr)
7665                .min()
7666                .unwrap_or(text_addr);
7667            let _all_exec_end = elf
7668                .section_headers
7669                .iter()
7670                .filter(|sh| sh.sh_flags & 0x4 != 0 && sh.sh_addr > 0)
7671                .map(|sh| sh.sh_addr + sh.sh_size)
7672                .max()
7673                .unwrap_or(text_end);
7674
7675            for sh in &elf.section_headers {
7676                let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
7677                if !matches!(name, ".rodata" | ".data.rel.ro") {
7678                    continue;
7679                }
7680                let fo = sh.sh_offset as usize;
7681                let sz = sh.sh_size as usize;
7682                if fo + sz > data.len() || sz < ptr_size {
7683                    continue;
7684                }
7685                let is_vtable_section = name == ".data.rel.ro";
7686                let ps = ptr_size; // local alias
7687                for i in (0..sz.saturating_sub(ps - 1)).step_by(ps) {
7688                    let ptr = read_ptr_elf(&data[fo + i..]);
7689                    if ptr >= text_addr && ptr < text_end {
7690                        if is_vtable_section {
7691                            // .data.rel.ro: vtable entries are always function pointers
7692                            found.insert(ptr);
7693                        } else if matches!(arch, rsleigh_api::Architecture::MIPS32) {
7694                            // MIPS: validate with prologue check
7695                            let target_idx = (ptr - text_addr) as usize;
7696                            if text_fo + target_idx + 4 < data.len() {
7697                                let word = read_u32_elf(
7698                                    &data[text_fo + target_idx..text_fo + target_idx + 4],
7699                                );
7700                                let strong = (word & 0xFFFF0000) == 0x27BD0000  // addiu sp, sp, -N
7701                                    || (word & 0xFFFF0000) == 0x3C1C0000         // lui gp, N
7702                                    || (word & 0xFFFF0000) == 0xAFBF0000; // sw ra, N(sp)
7703                                if strong {
7704                                    found.insert(ptr);
7705                                } else {
7706                                    let mut run = 0;
7707                                    let psi = ps as i64;
7708                                    for k in [-psi, psi, 2 * psi].iter() {
7709                                        let neighbor = i as i64 + k;
7710                                        if neighbor >= 0 && (neighbor as usize) + ps <= sz {
7711                                            let np = read_ptr_elf(&data[fo + neighbor as usize..]);
7712                                            if np >= text_addr && np < text_end {
7713                                                run += 1;
7714                                            }
7715                                        }
7716                                    }
7717                                    if run >= 2 {
7718                                        found.insert(ptr);
7719                                    }
7720                                }
7721                            }
7722                        } else {
7723                            // x86/ARM: existing prologue checks
7724                            let target_idx = (ptr - text_addr) as usize;
7725                            if text_fo + target_idx + 4 < data.len() {
7726                                let b0 = data[text_fo + target_idx];
7727                                let b1 = data[text_fo + target_idx + 1];
7728                                let strong = matches!(
7729                                    (b0, b1),
7730                                    (0x55, 0x48)
7731                                        | (0x55, 0x53)
7732                                        | (0x53, 0x48)
7733                                        | (0x53, 0x55)
7734                                        | (0x41, 0x54)
7735                                        | (0x41, 0x55)
7736                                        | (0x41, 0x56)
7737                                        | (0x41, 0x57)
7738                                        | (0x48, 0x83)
7739                                        | (0x48, 0x81)
7740                                        | (0xF3, 0x0F)
7741                                        | (0x55, 0x41)
7742                                );
7743                                if strong {
7744                                    found.insert(ptr);
7745                                } else {
7746                                    let mut run = 0;
7747                                    let psi = ps as i64;
7748                                    for k in [-psi, psi, 2 * psi].iter() {
7749                                        let neighbor = i as i64 + k;
7750                                        if neighbor >= 0 && (neighbor as usize) + ps <= sz {
7751                                            let np = read_ptr_elf(&data[fo + neighbor as usize..]);
7752                                            if np >= text_addr && np < text_end {
7753                                                run += 1;
7754                                            }
7755                                        }
7756                                    }
7757                                    if run >= 2 {
7758                                        found.insert(ptr);
7759                                    }
7760                                }
7761                            }
7762                        }
7763                    }
7764                }
7765            }
7766        }
7767
7768        // 5d. E9 JMP rel32 pass — add tail call thunks at function boundaries.
7769        // Only add when JMP is preceded by strict terminators (RET/INT3/NOP padding).
7770        // Do NOT include 0xFF — it's the last byte of many multi-byte instructions.
7771        if text_fo + text_size as usize <= data.len() {
7772            let text_bytes = &data[text_fo..text_fo + text_size as usize];
7773            for i in 0..text_bytes.len().saturating_sub(5) {
7774                if text_bytes[i] == 0xE9 {
7775                    let rel =
7776                        i32::from_le_bytes(text_bytes[i + 1..i + 5].try_into().unwrap_or([0; 4]));
7777                    let target = (text_addr as i64 + i as i64 + 5 + rel as i64) as u64;
7778                    if target < text_addr || target >= text_end {
7779                        continue;
7780                    }
7781                    let at_boundary =
7782                        i == 0 || matches!(text_bytes[i - 1], 0xC3 | 0x90 | 0xCC | 0x00);
7783                    if at_boundary {
7784                        found.insert(text_addr + i as u64);
7785                    }
7786                }
7787            }
7788        }
7789
7790        // 6. Prologue pattern scanning in .text
7791        if text_fo + text_size as usize <= data.len() {
7792            let text_bytes = &data[text_fo..text_fo + text_size as usize];
7793            let is_boundary = |i: usize| -> bool {
7794                i == 0
7795                    || matches!(
7796                        text_bytes[i - 1],
7797                        0xC3 | 0x90 | 0xCC | 0x00 | 0xC2 | 0xCB | 0xCA
7798                    )
7799            };
7800            // Also accept NOP padding sequences (66 66 2e 0f 1f etc.)
7801            let is_boundary_or_nop = |i: usize| -> bool {
7802                if is_boundary(i) {
7803                    return true;
7804                }
7805                // Multi-byte NOP: 66 90, 0f 1f XX, 66 2e 0f 1f
7806                if i >= 2 && text_bytes[i - 1] == 0x90 && text_bytes[i - 2] == 0x66 {
7807                    return true;
7808                }
7809                if i >= 1 && text_bytes[i - 1] == 0x90 {
7810                    return true;
7811                }
7812                false
7813            };
7814
7815            for i in 0..text_bytes.len().saturating_sub(4) {
7816                let addr = text_addr + i as u64;
7817                if found.contains(&addr) {
7818                    continue;
7819                }
7820
7821                let b0 = text_bytes[i];
7822                let b1 = if i + 1 < text_bytes.len() {
7823                    text_bytes[i + 1]
7824                } else {
7825                    0
7826                };
7827                let b2 = if i + 2 < text_bytes.len() {
7828                    text_bytes[i + 2]
7829                } else {
7830                    0
7831                };
7832                let b3 = if i + 3 < text_bytes.len() {
7833                    text_bytes[i + 3]
7834                } else {
7835                    0
7836                };
7837
7838                let matched = match (b0, b1, b2, b3) {
7839                    // push rbp; mov rbp, rsp (55 48 89 e5)
7840                    (0x55, 0x48, 0x89, 0xe5) => true,
7841                    // push rbp; mov rbp, rsp (55 48 8b ec)
7842                    (0x55, 0x48, 0x8b, 0xec) => true,
7843                    // push rbx; sub rsp (53 48 83 ec)
7844                    (0x53, 0x48, 0x83, 0xec) => true,
7845                    // push rbx; push rbp (53 55 ..) — C++ common
7846                    (0x53, 0x55, _, _) => true,
7847                    // push r12; push rbp (41 54 55 ..)
7848                    (0x41, 0x54, 0x55, _) => true,
7849                    // push r12; push rbx (41 54 53 ..)
7850                    (0x41, 0x54, 0x53, _) => true,
7851                    // push r13; push r12 (41 55 41 54)
7852                    (0x41, 0x55, 0x41, 0x54) => true,
7853                    // push r14; push r13 (41 56 41 55)
7854                    (0x41, 0x56, 0x41, 0x55) => true,
7855                    // push r15; push r14 (41 57 41 56)
7856                    (0x41, 0x57, 0x41, 0x56) => true,
7857                    // sub rsp, imm8 (48 83 ec NN) — leaf function
7858                    (0x48, 0x83, 0xEC, _) => true,
7859                    // sub rsp, imm32 (48 81 ec NN NN NN NN) — large stack frame
7860                    (0x48, 0x81, 0xEC, _) => true,
7861                    // push rbp; push rbx (55 53 ..)
7862                    (0x55, 0x53, _, _) => true,
7863                    // push rbp; push r12 (55 41 54 ..)
7864                    (0x55, 0x41, 0x54, _) => true,
7865                    // push rbp; sub rsp (55 48 83 ec) — already covered by push rbp patterns
7866                    // mov rdi, rsi or similar arg setup as first instruction (rare standalone)
7867                    _ => false,
7868                };
7869
7870                if matched && is_boundary_or_nop(i) {
7871                    found.insert(addr);
7872                }
7873
7874                // endbr64 (f3 0f 1e fa) — CET indirect branch target
7875                // Only count as function if preceded by a function terminator or NOP padding.
7876                // Switch case targets also have endbr64 but are NOT function entries.
7877                if b0 == 0xF3 && b1 == 0x0F && b2 == 0x1E && b3 == 0xFA {
7878                    if is_boundary_or_nop(i) {
7879                        found.insert(addr);
7880                    }
7881                }
7882            }
7883        }
7884
7885        // 6a. MIPS prologue pattern scanning in .text
7886        if matches!(arch, rsleigh_api::Architecture::MIPS32) {
7887            if text_fo + text_size as usize <= data.len() {
7888                let text_bytes = &data[text_fo..text_fo + text_size as usize];
7889
7890                // MIPS function boundary detection: JR RA (0x03E00008) or
7891                // JR RA in delay slot pair (JR RA + NOP = 03E00008 00000000)
7892                // Word reads use ELF endianness so mipsel works too.
7893                let is_mips_boundary = |i: usize| -> bool {
7894                    if i == 0 {
7895                        return true;
7896                    }
7897                    if i >= 8 {
7898                        let prev2 = read_u32_elf(&text_bytes[i - 8..i - 4]);
7899                        let prev1 = read_u32_elf(&text_bytes[i - 4..i]);
7900                        if prev2 == 0x03E00008 {
7901                            return true;
7902                        }
7903                        if prev1 == 0x03E00008 {
7904                            return true;
7905                        }
7906                    }
7907                    if i >= 4 {
7908                        let prev = read_u32_elf(&text_bytes[i - 4..i]);
7909                        if prev == 0x00000000 {
7910                            return true;
7911                        }
7912                        if prev == 0x03E00008 {
7913                            return true;
7914                        }
7915                    }
7916                    false
7917                };
7918
7919                for i in (0..text_bytes.len().saturating_sub(7)).step_by(4) {
7920                    let addr = text_addr + i as u64;
7921                    if found.contains(&addr) {
7922                        continue;
7923                    }
7924
7925                    let word = read_u32_elf(&text_bytes[i..i + 4]);
7926                    let next_word = read_u32_elf(&text_bytes[i + 4..i + 8]);
7927
7928                    // Pattern 1: addiu sp, sp, -N (0x27BDxxxx where xxxx is negative = high bit set)
7929                    // This is the most common MIPS function prologue
7930                    let is_addiu_sp = (word & 0xFFFF0000) == 0x27BD0000 && (word & 0x8000) != 0;
7931
7932                    if is_addiu_sp {
7933                        // Strong: addiu sp followed by sw ra (save return address)
7934                        let next_is_sw_ra = (next_word & 0xFFFF0000) == 0xAFBF0000;
7935                        // Also strong: addiu sp followed by sw s8/fp
7936                        let next_is_sw_fp = (next_word & 0xFFFF0000) == 0xAFBE0000;
7937                        // Also accept: addiu sp followed by lui gp (PIC prologue)
7938                        let next_is_lui_gp = (next_word & 0xFFFF0000) == 0x3C1C0000;
7939
7940                        if next_is_sw_ra || next_is_sw_fp || next_is_lui_gp {
7941                            // Strong prologue — always add
7942                            found.insert(addr);
7943                        } else if is_mips_boundary(i) {
7944                            // Weaker prologue but at a function boundary
7945                            found.insert(addr);
7946                        }
7947                    }
7948
7949                    // Pattern 2: lui gp, N followed by addiu gp (PIC code, GP setup)
7950                    // Some functions start with GP setup before stack allocation
7951                    let is_lui_gp = (word & 0xFFFF0000) == 0x3C1C0000;
7952                    if is_lui_gp && is_mips_boundary(i) {
7953                        let next_is_addiu_gp = (next_word & 0xFFFF0000) == 0x279C0000;
7954                        if next_is_addiu_gp {
7955                            found.insert(addr);
7956                        }
7957                    }
7958                }
7959            }
7960        }
7961
7962        // 6b. Scan for endbr64 in all executable sections (not just .text)
7963        // This catches .plt.sec and .plt.got entries
7964        for sh in &elf.section_headers {
7965            if sh.sh_flags & 0x4 == 0 {
7966                continue;
7967            } // not executable
7968            let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
7969            if name == ".text" {
7970                continue;
7971            } // already scanned
7972            let fo = sh.sh_offset as usize;
7973            let sz = sh.sh_size as usize;
7974            if fo + sz > data.len() {
7975                continue;
7976            }
7977            let sec_bytes = &data[fo..fo + sz];
7978            for i in (0..sz.saturating_sub(4)).step_by(1) {
7979                if sec_bytes[i] == 0xF3
7980                    && sec_bytes[i + 1] == 0x0F
7981                    && sec_bytes[i + 2] == 0x1E
7982                    && sec_bytes[i + 3] == 0xFA
7983                {
7984                    found.insert(sh.sh_addr + i as u64);
7985                }
7986            }
7987        }
7988
7989        // 7. Gap analysis: scan gaps between known functions for valid prologues.
7990        if text_fo + text_size as usize <= data.len() {
7991            let text_bytes = &data[text_fo..text_fo + text_size as usize];
7992            let mut sorted_addrs: Vec<u64> = found
7993                .iter()
7994                .filter(|a| **a >= text_addr && **a < text_end)
7995                .copied()
7996                .collect();
7997            sorted_addrs.sort();
7998
7999            for window in sorted_addrs.windows(2) {
8000                let gap_start = window[0];
8001                let gap_end = window[1];
8002                let gap_size = gap_end - gap_start;
8003                // Only analyze gaps > 16 bytes (room for a real function)
8004                if gap_size < 32 || gap_size > 2048 {
8005                    continue;
8006                }
8007                // Scan inside the gap for function prologues after terminators
8008                let start_idx = (gap_start - text_addr) as usize;
8009                let end_idx = (gap_end - text_addr) as usize;
8010                if end_idx > text_bytes.len() {
8011                    continue;
8012                }
8013                let mut i = start_idx;
8014                while i + 4 < end_idx {
8015                    let b = text_bytes[i];
8016                    // Look for RET (C3) or unconditional JMP (E9/EB/FF) followed by valid code
8017                    if matches!(b, 0xC3 | 0xCC) {
8018                        // Skip NOP/INT3/alignment padding (require 2+ padding bytes)
8019                        let mut j = i + 1;
8020                        while j < end_idx && matches!(text_bytes[j], 0x90 | 0xCC | 0x00) {
8021                            j += 1;
8022                        }
8023                        // Also skip multi-byte NOPs: 66 90, 0f 1f XX, 66 2e 0f 1f
8024                        while j + 1 < end_idx && text_bytes[j] == 0x66 && text_bytes[j + 1] == 0x90
8025                        {
8026                            j += 2;
8027                        }
8028                        while j + 2 < end_idx && text_bytes[j] == 0x0F && text_bytes[j + 1] == 0x1F
8029                        {
8030                            j += 3;
8031                        }
8032                        if j < end_idx && j >= i + 2 {
8033                            // require 2+ padding bytes
8034                            let candidate = text_addr + j as u64;
8035                            if !found.contains(&candidate) {
8036                                // Verify: must start with a strong prologue pattern
8037                                let fb = text_bytes[j];
8038                                let fb1 = if j + 1 < end_idx {
8039                                    text_bytes[j + 1]
8040                                } else {
8041                                    0
8042                                };
8043                                let valid_start = matches!(
8044                                    (fb, fb1),
8045                                    (0x55, 0x48) | (0x55, 0x53) | (0x55, 0x41) | // push rbp; ...
8046                                    (0x53, 0x48) | (0x53, 0x55) |                 // push rbx; ...
8047                                    (0x41, 0x54) | (0x41, 0x55) | (0x41, 0x56) | (0x41, 0x57) | // push r12-r15
8048                                    (0x48, 0x83) | (0x48, 0x81) |                 // sub rsp
8049                                    (0xF3, 0x0F) // endbr64
8050                                );
8051                                if valid_start {
8052                                    found.insert(candidate);
8053                                }
8054                            }
8055                            i = j;
8056                            continue;
8057                        }
8058                    }
8059                    i += 1;
8060                }
8061            }
8062        }
8063
8064        // 7b. MIPS gap analysis: scan gaps between known functions for prologues after JR RA.
8065        if matches!(arch, rsleigh_api::Architecture::MIPS32) {
8066            if text_fo + text_size as usize <= data.len() {
8067                let text_bytes = &data[text_fo..text_fo + text_size as usize];
8068                let mut sorted_addrs: Vec<u64> = found
8069                    .iter()
8070                    .filter(|a| **a >= text_addr && **a < text_end)
8071                    .copied()
8072                    .collect();
8073                sorted_addrs.sort();
8074
8075                for window in sorted_addrs.windows(2) {
8076                    let gap_start = window[0];
8077                    let gap_end = window[1];
8078                    let gap_size = gap_end - gap_start;
8079                    if gap_size < 16 || gap_size > 4096 {
8080                        continue;
8081                    }
8082                    let start_idx = (gap_start - text_addr) as usize;
8083                    let end_idx = (gap_end - text_addr) as usize;
8084                    if end_idx + 4 > text_bytes.len() {
8085                        continue;
8086                    }
8087
8088                    // Scan for JR RA (0x03E00008) + delay slot, then prologue.
8089                    // ELF endianness drives word reads (BE 'mips' vs LE 'mipsel').
8090                    let mut i = start_idx;
8091                    while i + 12 <= end_idx {
8092                        let word = read_u32_elf(&text_bytes[i..i + 4]);
8093                        if word == 0x03E00008 {
8094                            // JR RA
8095                            // Skip delay slot + any NOP padding
8096                            let mut j = i + 8; // past JR RA + delay slot
8097                            while j + 4 <= end_idx {
8098                                let w = read_u32_elf(&text_bytes[j..j + 4]);
8099                                if w == 0x00000000 {
8100                                    j += 4;
8101                                } else {
8102                                    break;
8103                                }
8104                            }
8105                            if j + 8 <= end_idx && j % 4 == 0 {
8106                                let candidate_word = read_u32_elf(&text_bytes[j..j + 4]);
8107                                let is_prologue = (candidate_word & 0xFFFF0000) == 0x27BD0000
8108                                    && (candidate_word & 0x8000) != 0
8109                                    || (candidate_word & 0xFFFF0000) == 0x3C1C0000;
8110                                if is_prologue {
8111                                    let candidate_addr = text_addr + j as u64;
8112                                    if !found.contains(&candidate_addr) {
8113                                        found.insert(candidate_addr);
8114                                    }
8115                                }
8116                            }
8117                            i = j;
8118                        } else {
8119                            i += 4;
8120                        }
8121                    }
8122                }
8123            }
8124        }
8125
8126        // Step 5 (decoder-based CALL discovery) already covers recursive descent.
8127        // No separate pass needed.
8128    }
8129
8130    // Filter: remove addresses in PLT range that aren't PLT entries
8131    // and sort results
8132    let mut result: Vec<(u64, String)> = found
8133        .into_iter()
8134        .map(|addr| {
8135            // Try to resolve PLT names from dynamic relocations
8136            let plt_name = resolve_plt_name(elf, addr);
8137            let name = plt_name.unwrap_or_else(|| format!("FUN_{:08x}", addr));
8138            (addr, name)
8139        })
8140        .collect();
8141    result.sort_by_key(|(addr, _)| *addr);
8142    result
8143}
8144
8145/// Try to resolve a PLT entry address to its import name via .rela.plt relocations.
8146fn resolve_plt_name(elf: &goblin::elf::Elf, addr: u64) -> Option<String> {
8147    // Check if addr is in a PLT section
8148    let in_plt = elf.section_headers.iter().any(|sh| {
8149        let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
8150        name.starts_with(".plt") && addr >= sh.sh_addr && addr < sh.sh_addr + sh.sh_size
8151    });
8152    if !in_plt {
8153        return None;
8154    }
8155
8156    // Find which PLT slot this is (by index)
8157    let plt_sec = elf.section_headers.iter().find(|sh| {
8158        let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
8159        name == ".plt.sec" || name == ".plt"
8160    })?;
8161    let entry_size = if plt_sec.sh_entsize > 0 {
8162        plt_sec.sh_entsize
8163    } else {
8164        16
8165    };
8166    let plt_name = elf.shdr_strtab.get_at(plt_sec.sh_name).unwrap_or("");
8167    let base = if plt_name == ".plt.sec" {
8168        plt_sec.sh_addr
8169    } else {
8170        plt_sec.sh_addr + entry_size
8171    };
8172    if addr < base {
8173        return None;
8174    }
8175    let idx = ((addr - base) / entry_size) as usize;
8176
8177    // Match against .rela.plt relocations
8178    for rel in &elf.pltrelocs {
8179        // The PLT index corresponds to the relocation index
8180        let sym = &elf.dynsyms.get(rel.r_sym)?;
8181        let name = elf.dynstrtab.get_at(sym.st_name)?;
8182        if !name.is_empty() {
8183            // Count which relocation this is
8184            let rel_idx = elf
8185                .pltrelocs
8186                .iter()
8187                .position(|r| r.r_offset == rel.r_offset)?;
8188            if rel_idx == idx {
8189                return Some(name.to_string());
8190            }
8191        }
8192    }
8193    None
8194}