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