//! Chunked sweeping of a remote address space, plus the two things we sweep //! for: byte patterns and printable strings. //! //! # Why chunking, and the off-by-one that ruins scanners //! //! The target has roughly 3 GB resident. Reading a region in one allocation is //! wasteful and can fail outright, so regions are walked in 4 MiB chunks. //! //! The classic bug in every hand-rolled scanner is that a pattern straddling a //! chunk boundary is never found: the tail of chunk N holds the first few bytes //! and the head of chunk N+1 holds the rest, and neither buffer contains the //! whole thing. The fix is to overlap consecutive chunks by `pattern_len - 1` //! bytes. //! //! That specific overlap is exactly right, and it is worth showing why it is //! neither too small nor too large. Let a chunk cover `[0, n)` and the pattern //! have length `P`. A match starting at index `s` occupies `s ..= s + P - 1`, so //! the last match fully inside the chunk starts at `s = n - P`. Any match //! starting at `s > n - P` runs off the end and must be caught by the next //! chunk, so the next chunk has to begin at or before `n - P + 1`. Advancing by //! `n - (P - 1)` starts it at precisely `n - P + 1`: //! //! * Nothing is missed: every straddling match starts at `s >= n - P + 1`, //! which is inside the next chunk. //! * Nothing is double-reported: the first index of the overlap is //! `n - P + 1`, which is strictly greater than `n - P`, the last index that //! can host a complete match in this chunk. The two windows of *reportable* //! match starts are disjoint even though the byte windows overlap. //! //! Overlapping by `P` instead would report every boundary-straddling match //! twice; overlapping by `P - 2` would miss one alignment. Hence `P - 1`. //! //! # Holes //! //! A region marked readable in `/proc//maps` is frequently not readable in //! practice: guard pages, Wine's special mappings, and pages Denuvo has not //! faulted in all return `EIO`. These are counted and stepped over a page at a //! time, never propagated as errors, because in a sweep this size they are //! routine. The counts are reported so the user knows the sweep was partial and //! does not read a zero-hit result as proof of absence. use crate::image::Module; use crate::maps::Region; use crate::mem::{ChunkRead, ProcMem, PAGE}; pub const CHUNK: usize = 4 * 1024 * 1024; #[derive(Default, Debug)] pub struct SweepStats { pub regions_scanned: usize, /// Regions from which not a single byte could be read. pub regions_skipped: usize, /// Individual chunk reads that hit an unreadable hole. pub holes: usize, pub bytes_read: u64, } impl SweepStats { pub fn summary(&self) -> String { format!( "scanned {} regions ({}), skipped {} unreadable regions, {} holes stepped over", self.regions_scanned, crate::maps::human(self.bytes_read), self.regions_skipped, self.holes ) } } fn align_up(va: u64, align: u64) -> u64 { va.div_ceil(align) * align } /// Walk one region in chunks, invoking `f(chunk_va, bytes, contiguous)`. /// /// `contiguous` is true when this chunk's data continues directly from the /// previous callback with no gap, which string extraction needs in order to /// join a run that spans a boundary. `overlap` is `pattern_len - 1` for pattern /// search and 0 for stateful scanners that track continuity themselves. /// /// Returns early (`false`) if `f` signals it has seen enough. fn sweep_region( mem: &ProcMem, region: &Region, overlap: usize, buf: &mut [u8], stats: &mut SweepStats, f: &mut F, ) -> bool where F: FnMut(u64, &[u8], bool) -> bool, { let mut pos = region.start; let mut contiguous = false; let mut read_anything = false; while pos < region.end { let want = (buf.len() as u64).min(region.end - pos) as usize; match mem.read_chunk(pos, &mut buf[..want]) { ChunkRead::Hole => { stats.holes += 1; contiguous = false; // Step to the next page; the current one is unreadable. pos = align_up(pos + 1, PAGE); } ChunkRead::Got(n) => { read_anything = true; stats.bytes_read += n as u64; if !f(pos, &buf[..n], contiguous) { return false; } if pos + n as u64 >= region.end { break; } if n < want { // Short read: an unmapped hole begins at pos + n. No pattern // can span a hole, so no overlap is needed here; resume on // the next page boundary. contiguous = false; pos = align_up(pos + n as u64 + 1, PAGE); } else { if n <= overlap { break; // cannot make forward progress } contiguous = true; pos += (n - overlap) as u64; } } } } if read_anything { stats.regions_scanned += 1; } else { stats.regions_skipped += 1; } true } /// Which regions a sweep should touch. pub fn scan_targets(regions: &[Region], module: Option<&Module>, anon_only: bool) -> Vec { regions .iter() .filter(|r| r.readable() && !r.pseudo()) .filter(|r| !anon_only || r.anonymous()) .filter_map(|r| match module { None => Some(r.clone()), // Clip the region to the module's image span rather than dropping // it: under Wine a module's sections live in large anonymous // regions that may extend past the image. Some(m) => { let start = r.start.max(m.base); let end = r.end.min(m.end()); if start < end { let mut clipped = (*r).clone(); clipped.start = start; clipped.end = end; Some(clipped) } else { None } } }) .collect::>() } /// Search every target region for `pattern`. Calls `hit(va)` per match. pub fn find_pattern( mem: &ProcMem, targets: &[Region], pattern: &[u8], max: Option, mut hit: F, ) -> SweepStats where F: FnMut(u64), { let mut stats = SweepStats::default(); if pattern.is_empty() { return stats; } let finder = memchr::memmem::Finder::new(pattern); let overlap = pattern.len() - 1; // The buffer must comfortably exceed the overlap or progress stalls. let mut buf = vec![0u8; CHUNK.max(pattern.len() * 4)]; let mut found = 0usize; for region in targets { let keep_going = sweep_region( mem, region, overlap, &mut buf, &mut stats, &mut |base, data, _contiguous| { for off in finder.find_iter(data) { hit(base + off as u64); found += 1; if max.is_some_and(|m| found >= m) { return false; } } true }, ); if !keep_going { break; } } stats } fn printable(b: u8) -> bool { (0x20..=0x7e).contains(&b) } /// Extracts printable runs, carrying an unfinished run across contiguous chunks /// so a string straddling a boundary is still emitted whole. struct StringScanner { utf16: bool, min: usize, run: Vec, run_start: u64, open: bool, /// UTF-16 only: a low byte at the very end of a chunk whose high byte will /// arrive in the next one. carry: Option<(u64, u8)>, } impl StringScanner { fn new(utf16: bool, min: usize) -> Self { Self { utf16, min, run: Vec::with_capacity(256), run_start: 0, open: false, carry: None, } } fn flush(&mut self, emit: &mut F) { if self.open && self.run.len() >= self.min { // Runs are printable ASCII by construction, so this cannot fail. if let Ok(s) = std::str::from_utf8(&self.run) { emit(self.run_start, s); } } self.run.clear(); self.open = false; } fn push(&mut self, va: u64, b: u8, emit: &mut F) { if !self.open { self.open = true; self.run_start = va; } self.run.push(b); // Guard against a pathological all-printable megabyte eating memory. if self.run.len() >= 4096 { self.flush(emit); } } fn feed( &mut self, base: u64, data: &[u8], contiguous: bool, emit: &mut F, ) { if !contiguous { self.flush(emit); self.carry = None; } if self.utf16 { self.feed_utf16(base, data, emit); } else { for (i, &b) in data.iter().enumerate() { if printable(b) { self.push(base + i as u64, b, emit); } else { self.flush(emit); } } } } fn feed_utf16(&mut self, base: u64, data: &[u8], emit: &mut F) { let mut i = 0usize; // A pair split across the chunk boundary: complete it if the high byte // is the expected 0x00, otherwise the run ends here. if let Some((addr, lo)) = self.carry.take() { if data.first() == Some(&0) && printable(lo) { self.push(addr, lo, emit); i = 1; } else { self.flush(emit); } } while i + 1 < data.len() { let (lo, hi) = (data[i], data[i + 1]); if hi == 0 && printable(lo) { self.push(base + i as u64, lo, emit); i += 2; } else { self.flush(emit); i += 1; } } if i < data.len() { self.carry = Some((base + i as u64, data[i])); } } } /// Extract strings from every target region. Calls `emit(va, text)`. pub fn find_strings( mem: &ProcMem, targets: &[Region], utf16: bool, min: usize, grep: Option<&str>, max: Option, mut emit: F, ) -> SweepStats where F: FnMut(u64, &str), { let mut stats = SweepStats::default(); let mut buf = vec![0u8; CHUNK]; let grep_lower = grep.map(|g| g.to_ascii_lowercase()); let mut count = 0usize; for region in targets { let mut scanner = StringScanner::new(utf16, min); let mut stop = false; // overlap 0: the scanner tracks continuity itself via `contiguous`. let keep_going = sweep_region( mem, region, 0, &mut buf, &mut stats, &mut |base, data, contiguous| { scanner.feed(base, data, contiguous, &mut |va, s| { let matches = match &grep_lower { Some(g) => s.to_ascii_lowercase().contains(g.as_str()), None => true, }; if matches { emit(va, s); count += 1; if max.is_some_and(|m| count >= m) { stop = true; } } }); !stop }, ); scanner.flush(&mut |va, s| { let matches = match &grep_lower { Some(g) => s.to_ascii_lowercase().contains(g.as_str()), None => true, }; if matches { emit(va, s); } }); if !keep_going || stop { break; } } stats }