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