diff --git a/patternsleuth/src/lib.rs b/patternsleuth/src/lib.rs index cc84d03..061e4e7 100644 --- a/patternsleuth/src/lib.rs +++ b/patternsleuth/src/lib.rs @@ -213,6 +213,7 @@ impl PatternConfig { pub struct Executable<'data> { pub data: &'data [u8], + pub exception_data: &'data [u8], pub object: object::File<'data>, pub memory: MountedPE<'data>, pub functions: Option>, @@ -237,6 +238,17 @@ impl<'data> Executable<'data> { .then(|| symbols::dump_pdb_symbols(pdb_path, base_address)) .transpose()?; + let exception_data = match object { + object::File::Pe64(ref inner) => { + let exception_directory = inner + .data_directory(object::pe::IMAGE_DIRECTORY_ENTRY_EXCEPTION) + .context("no exception directory")?; + + exception_directory.data(data, &inner.section_table())? + } + _ => &[], + }; + let functions = load_functions .then(|| -> Result<_> { Ok(match object { @@ -268,6 +280,7 @@ impl<'data> Executable<'data> { let section = memory .get_section_containing(unwind) .context("out of bounds reading unwind info")?; + let mut offset = unwind - section.address; let has_chain_info = section.data[offset] >> 3 == 0x4; @@ -280,10 +293,15 @@ impl<'data> Executable<'data> { offset += 2; } - let mut tmp = [0; 4]; - tmp.copy_from_slice(§ion.data[offset..offset + 4]); - let parent = base_address + u32::from_le_bytes(tmp) as usize; - children.push((parent, function.clone())); + if section.data.len() > offset { + let parent = base_address + + u32::from_le_bytes( + section.data[offset..offset + 4].try_into().unwrap(), + ) as usize; + children.push((parent, function.clone())); + } else { + dbg!("not adding chain info {offset}"); + } } functions.insert(function.range.start, function); } @@ -300,43 +318,98 @@ impl<'data> Executable<'data> { }) }) .transpose()?; - /* - for (i, window) in functions.windows(2).enumerate() { - match window { - [a, b] => { - if a.range.end > b.range.start { - println!("overlapping exception table @ {} len = {}: {a:x?} {b:x?}", i, functions.len()); - } - if a.range.start >= b.range.start { - println!("out of order exception table @ {} len = {}: {a:x?} {b:x?}", i, functions.len()); - } - } - _ => unreachable!() - } - } - */ Ok(Executable { data, + exception_data, object, memory, functions, symbols, }) } - pub fn get_function(&self, address: usize) -> Option<&RuntimeFunction> { - if let Some(functions) = &self.functions { - // TODO figure out what to do in the rare case of overlapping entries - match functions.binary_search_by_key(&address, |f| f.range.start) { - Ok(i) => Some(&functions[i]), - Err(i) => { - let f = &functions.get(i - 1); - f.filter(|f| f.range.contains(&address)) + pub fn get_function(&self, address: usize) -> Option { + let base_address = self.object.relative_address_base() as usize; + + let count = self.exception_data.len() / 12; + + for i in 0..count { + let addr_begin = base_address + + u32::from_le_bytes(self.exception_data[i * 12..i * 12 + 4].try_into().unwrap()) + as usize; + if addr_begin <= address { + let addr_end = base_address + + u32::from_le_bytes( + self.exception_data[i * 12 + 4..i * 12 + 8] + .try_into() + .unwrap(), + ) as usize; + if addr_end > address { + let unwind = base_address + + u32::from_le_bytes( + self.exception_data[i * 12 + 8..i * 12 + 12] + .try_into() + .unwrap(), + ) as usize; + + let mut f = RuntimeFunction { + range: addr_begin..addr_end, + unwind, + children: vec![], + }; + + loop { + let Some(section) = self.memory.get_section_containing(f.unwind) else { + dbg!("out of bounds reading unwind info"); + return None; + }; + + let mut offset = f.unwind - section.address; + + let has_chain_info = section.data[offset] >> 3 == 0x4; + if has_chain_info { + let unwind_code_count = section.data[offset + 2]; + + offset += 4 + 2 * unwind_code_count as usize; + if offset % 4 != 0 { + // align + offset += 2; + } + + if section.data.len() > offset { + let addr_begin = base_address + + u32::from_le_bytes( + section.data[offset..offset + 4].try_into().unwrap(), + ) as usize; + let addr_end = base_address + + u32::from_le_bytes( + section.data[offset + 4..offset + 8].try_into().unwrap(), + ) as usize; + let unwind = base_address + + u32::from_le_bytes( + section.data[offset + 8..offset + 12].try_into().unwrap(), + ) as usize; + + let mut children = std::mem::take(&mut f.children); + children.push(f.clone()); + + f = RuntimeFunction { + range: addr_begin..addr_end, + unwind, + children, + }; + } else { + todo!("not adding chain info {offset}"); + } + } else { + return Some(f); + } + //functions.insert(function.range.start, function); + } } } - } else { - None } + None } } diff --git a/patternsleuth/src/patterns.rs b/patternsleuth/src/patterns.rs index 51b7270..5db3311 100644 --- a/patternsleuth/src/patterns.rs +++ b/patternsleuth/src/patterns.rs @@ -76,7 +76,8 @@ pub enum Sig { AES, Lock, FParseParam, - FParseParamCalls, + + UEVRConsoleManager, } pub fn get_patterns() -> Result> { @@ -1492,14 +1493,15 @@ pub fn get_patterns() -> Result> { "FParse::Param".to_string(), Some(object::SectionKind::Text), Pattern::new("48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 41 54 41 55 41 56 41 57 48 83 EC 20 66 83 39 00 4C 8B ?? 4C")?, - resolve_self, + fparseparam::resolve_stage1, ), + PatternConfig::new( - Sig::FParseParamCalls, - "FParse::Param calls".to_string(), - Some(object::SectionKind::Text), - Pattern::new("48 8d 15 [ ?? ?? ?? ?? ] e8 X0x141aac5a0")?, // TODO get this xref address from pattern dependency - fparseparam::resolve, + Sig::UEVRConsoleManager, + "UEVRConsoleManager".to_string(), + None, + Pattern::from_bytes("r.DumpingMovie".encode_utf16().flat_map(u16::to_le_bytes).collect())?, + uevr::resolve_console_manager, ), ]) } @@ -2003,6 +2005,21 @@ mod signing_key { mod fparseparam { use super::*; + pub fn resolve_stage1(ctx: ResolveContext, stages: &mut ResolveStages) -> ResolutionAction { + stages.0.push(ctx.match_address); + + ResolutionAction::Continue(Scan { + section: Some(object::SectionKind::Text), + scan_type: Pattern::new(&format!( + "48 8d 15 [ ?? ?? ?? ?? ] e8 X0x{:x}", + ctx.match_address + )) + .unwrap() + .into(), + resolve, + }) + } + pub fn resolve(ctx: ResolveContext, stages: &mut ResolveStages) -> ResolutionAction { stages.0.push(ctx.match_address); @@ -2014,3 +2031,35 @@ mod fparseparam { read_wstring(&ctx, addr.into()).into() } } + +mod uevr { + use super::*; + + pub fn resolve_console_manager( + ctx: ResolveContext, + stages: &mut ResolveStages, + ) -> ResolutionAction { + stages.0.push(ctx.match_address); + + ResolutionAction::Continue(Scan { + section: Some(object::SectionKind::Text), + scan_type: Pattern::new(&format!("48 8d 15 X0x{:x}", ctx.match_address)) + .unwrap() + .into(), + resolve: resolve_console_manager_stage2, + }) + } + + pub fn resolve_console_manager_stage2( + ctx: ResolveContext, + stages: &mut ResolveStages, + ) -> ResolutionAction { + stages.0.push(ctx.match_address); + + if let Some(f) = ctx.exe.get_function(ctx.match_address) { + f.range.start.into() + } else { + ResolutionType::Failed.into() + } + } +} diff --git a/patternsleuth_cli/src/main.rs b/patternsleuth_cli/src/main.rs index 616e16f..cf728a7 100644 --- a/patternsleuth_cli/src/main.rs +++ b/patternsleuth_cli/src/main.rs @@ -4,6 +4,7 @@ use std::path::{Path, PathBuf}; use anyhow::Result; use clap::Parser; +use indicatif::ProgressBar; use itertools::Itertools; use object::{Object, ObjectSection}; use patricia_tree::StringPatriciaMap; @@ -54,6 +55,10 @@ struct CommandScan { #[arg(short, long)] disassemble: bool, + /// Show disassembly context for each matched address + #[arg(short, long)] + disassemble_merged: bool, + /// A pattern to scan for (can be specified multiple times) #[arg(short, long, value_parser(|s: &_| Pattern::new(s)))] patterns: Vec, @@ -73,6 +78,10 @@ struct CommandScan { /// Show scan summary #[arg(long)] summary: bool, + + /// Show scan progress + #[arg(long)] + progress: bool, } #[derive(Parser)] @@ -161,21 +170,30 @@ mod disassemble { )); let (is_fn, data, start_address) = if let Some(f) = exe.get_function(address) { + let range = f.full_range(); output.buffer.push_str(&format!( "{:016x} - {:016x} = function\n", - f.range.start, f.range.end + range.start, range.end )); if let Some(symbols) = &exe.symbols { - if let Some(symbol) = symbols.get(&f.range.start) { + if let Some(symbol) = symbols.get(&range.start) { #[allow(clippy::unnecessary_to_owned)] output .buffer - .push_str(&format!("{}\n", symbol).bright_yellow().to_string()); + .push_str(&format!("{}", symbol).bright_yellow().to_string()); + output.buffer.push_str(&"".normal().to_string()); + output.buffer.push('\n'); } } + println!( + "{:x} {:x} {:x} {f:x?}", + address, + section.address, + section.data.len() + ); let data = - §ion.data[f.range.start - section.address..f.range.end - section.address]; - let start_address = f.range.start as u64; + §ion.data[range.start - section.address..range.end - section.address]; + let start_address = range.start as u64; (true, data, start_address) } else { output.buffer.push_str("no function"); @@ -593,21 +611,50 @@ fn scan(command: CommandScan) -> Result<()> { let mut all: HashMap<(String, (&Sig, &String)), Vec> = HashMap::new(); use colored::Colorize; + use indicatif::ProgressIterator; use itertools::join; use prettytable::{format, row, Cell, Row, Table}; - for GameEntry { name, exe_path } in get_games(command.game)? { - println!("{:?} {:?}", name, exe_path.display()); - let bin_data = fs::read(&exe_path)?; + enum Output { + None, + Stdout, + Progress(ProgressBar), + } + + impl Output { + fn println>(&self, msg: M) { + match self { + Output::None => {} + Output::Stdout => println!("{}", msg.as_ref()), + Output::Progress(progress) => progress.println(msg), + } + } + } + + let games_vec = get_games(command.game)?; + + let (output, iter): (_, Box>) = if command.progress { + let progress = ProgressBar::new(games_vec.len() as u64); + ( + Output::Progress(progress.clone()), + Box::new(games_vec.iter().progress_with(progress)), + ) + } else { + (Output::Stdout, Box::new(games_vec.iter())) + }; + + for GameEntry { name, exe_path } in iter { + output.println(format!("{:?} {:?}", name, exe_path.display())); + let bin_data = fs::read(exe_path)?; let exe = match Executable::read( &bin_data, - &exe_path, + exe_path, command.symbols, !command.skip_exceptions, ) { Ok(exe) => exe, Err(err) => { - println!("err reading {}: {}", exe_path.display(), err); + output.println(format!("err reading {}: {}", exe_path.display(), err)); continue; } }; @@ -682,6 +729,54 @@ fn scan(command: CommandScan) -> Result<()> { table.add_row(Row::new(cells)); } cells.push(Cell::new(&table.to_string())); + } else if command.disassemble_merged { + cells.push(Cell::new({ + let cells = sig_scans + .iter() + .fold( + HashMap::<&ResolutionType, HashMap<&str, usize>>::new(), + |mut map, m| { + *map.entry(&m.1.res) + .or_default() + .entry(&m.0.name) + .or_default() += 1; + map + }, + ) + .iter() + // sort by pattern name, then match address + .sorted_by_key(|&data| data.0) + .map(|(m, counts)| match &m { + ResolutionType::Address(address) => { + let dis = disassemble::disassemble(&exe, *address, None); + + let mut lines = vec![]; + for (name, count) in counts.iter().sorted_by_key(|e| e.0) { + let count = if *count > 1 { + format!(" (x{count})") + } else { + "".to_string() + }; + + lines.push( + format!("{:?}{}", name, count).normal().to_string(), + ); + } + lines.push(dis); + + Cell::new(&join(lines, "\n")) + } + _ => todo!(), + }) + .collect::>(); + + let mut table = Table::new(); + table.set_format(*format::consts::FORMAT_NO_BORDER); + + table.add_row(Row::new(cells)); + + &table.to_string() + })); } else { cells.push(Cell::new({ let mut lines = sig_scans @@ -752,7 +847,7 @@ fn scan(command: CommandScan) -> Result<()> { table.add_row(Row::new(cells)); } - table.printstd(); + output.println(table.to_string()); // fold current game scans into summary scans scan.results.into_iter().fold(&mut all, |map, m| { @@ -761,10 +856,11 @@ fn scan(command: CommandScan) -> Result<()> { .push(m.1); map }); - - println!(); } + // force any progress output to be dropped + let output = Output::Stdout; + if command.summary { #[derive(Debug, Default)] struct Summary { @@ -783,7 +879,7 @@ fn scan(command: CommandScan) -> Result<()> { } let mut summary = Table::new(); - let title_strs: Vec = ["".to_owned()] + let title_strs: Vec = ["".into(), "unqiue addresses".into()] .into_iter() .chain( patterns @@ -794,14 +890,25 @@ fn scan(command: CommandScan) -> Result<()> { summary.set_titles(Row::new(title_strs.iter().map(|s| Cell::new(s)).collect())); let mut totals = patterns.iter().map(|_| Summary::default()).collect_vec(); + let mut no_matches = 0; + let mut one_match = 0; + let mut gt_one_match = 0; + for game in games.iter().sorted() { let mut row = vec![Cell::new(game)]; + let mut matched_addresses = HashSet::new(); + let summaries: Vec = patterns .iter() .map(|conf| { let res = all.get(&(game.to_string(), (&conf.sig, &conf.name))); if let Some(res) = res { + for res in res { + if let ResolutionType::Address(addr) = res.res { + matched_addresses.insert(addr); + } + } Summary { matches: res.len(), resolved: res @@ -835,15 +942,32 @@ fn scan(command: CommandScan) -> Result<()> { } } + match matched_addresses.len() { + 0 => { + no_matches += 1; + } + 1 => { + one_match += 1; + } + _ => { + gt_one_match += 1; + } + } + + row.push(Cell::new(&format!("unique={}", matched_addresses.len()))); + let cell_strs: Vec = summaries.iter().map(Summary::format).collect(); row.extend(cell_strs.iter().map(|s| Cell::new(s))); summary.add_row(Row::new(row)); } - let total_strs = [format!("{}", games.len())] - .into_iter() - .chain(totals.iter().map(Summary::format)) - .collect_vec(); + let total_strs = [ + format!("total={}", games.len()), + format!("0={} 1={} >1={}", no_matches, one_match, gt_one_match), + ] + .into_iter() + .chain(totals.iter().map(Summary::format)) + .collect_vec(); summary.add_row(Row::new( total_strs.iter().map(|s| Cell::new(s)).collect_vec(), )); @@ -851,7 +975,7 @@ fn scan(command: CommandScan) -> Result<()> { //let games: HashSet = all.keys().map(|(game, _)| game).cloned().collect(); //println!("{:#?}", all); - summary.printstd(); + output.println(summary.to_string()); } Ok(()) @@ -883,14 +1007,7 @@ fn symbols(command: CommandSymbols) -> Result<()> { for (address, name) in exe.symbols.as_ref().unwrap() { if filter(name) { if let Some(exception) = exe.get_function(*address) { - let full_range = exception.full_range(); - println!( - "{:016x} {:016x} {:08x} {}", - address, - full_range.end, - full_range.len(), - name - ); + let full_range = exception.full_range(); // TODO this now shows only the first exception block and misses any chained exceptions that may be covering the function if exception.range.start != *address { println!("MISALIGNED EXCEPTION ENTRY FOR {}", name); } else { @@ -1043,6 +1160,7 @@ mod index { } pub(crate) fn view(command: CommandViewSymbol) -> Result<()> { + println!("{:?}", command.symbol); let conn = Connection::open("data.db")?; struct SqlFunction { @@ -1079,11 +1197,7 @@ mod index { } if !functions.is_empty() { - if let Some(pattern) = - build_common_pattern(functions.iter().map(|f| &f.sql.data).collect::>()) - { - println!("{}", pattern); - } + println!("{} total functions", functions.len()); for function in &functions { println!( @@ -1183,7 +1297,7 @@ mod index { ); } - println!("./run.sh scan --skip-exceptions --summary\\"); + println!("./run.sh scan --skip-exceptions --summary \\"); for pattern in patterns { println!(" -p '{}' \\", pattern); } diff --git a/patternsleuth_scanner/src/lib.rs b/patternsleuth_scanner/src/lib.rs index 86c1ee2..b4e5bdf 100644 --- a/patternsleuth_scanner/src/lib.rs +++ b/patternsleuth_scanner/src/lib.rs @@ -147,7 +147,7 @@ impl Pattern { xrefs, }) } - /// Create a pattern from a literal Vec with `mask` filled with 0xff and `custom_offset = 0`. + /// Create a pattern from a literal `Vec` with `mask` filled with 0xff and `custom_offset = 0`. pub fn from_bytes(sig: Vec) -> Result { Ok(Self { mask: vec![0xff; sig.len()],