From 085fa676b1d06faf2a6dc1b3d23420dacd24a95e Mon Sep 17 00:00:00 2001 From: localcc Date: Sat, 11 Nov 2023 23:03:55 +0100 Subject: [PATCH] initial commit --- .github/workflows/tests.yml | 32 ++++++ .gitignore | 1 + Cargo.toml | 22 +++++ LICENSE | 21 ++++ README.md | 28 ++++++ benches/scan_1gb.rs | 53 ++++++++++ src/aligned_bytes.rs | 88 +++++++++++++++++ src/backends/avx2.rs | 66 +++++++++++++ src/backends/mod.rs | 44 +++++++++ src/backends/scalar.rs | 51 ++++++++++ src/backends/sse42.rs | 63 ++++++++++++ src/lib.rs | 192 ++++++++++++++++++++++++++++++++++++ src/pattern.rs | 96 ++++++++++++++++++ tests/big_pattern.rs | 49 +++++++++ tests/similar_pattern.rs | 46 +++++++++ tests/small_pattern.rs | 49 +++++++++ 16 files changed, 901 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 benches/scan_1gb.rs create mode 100644 src/aligned_bytes.rs create mode 100644 src/backends/avx2.rs create mode 100644 src/backends/mod.rs create mode 100644 src/backends/scalar.rs create mode 100644 src/backends/sse42.rs create mode 100644 src/lib.rs create mode 100644 src/pattern.rs create mode 100644 tests/big_pattern.rs create mode 100644 tests/similar_pattern.rs create mode 100644 tests/small_pattern.rs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..1c7ef36 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,32 @@ +name: Unit Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +env: + RUSTFLAGS: "-C target-cpu=native" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + components: clippy + + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.3 + + - name: Run clippy + run: cargo clippy -- -D warnings + + - name: Run tests + run: cargo test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3c34dc0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "lightningscanner" +description = "A lightning-fast memory pattern scanner, capable of scanning gigabytes of data per second." +edition = "2021" +version = "1.0.0" +authors = ["localcc"] +license = "MIT" +keywords = ["memory", "pattern", "reverse-engineering", "game-hacking"] +categories = ["algorithms"] +repository = "https://github.com/localcc/lightningscanner-rs" + +[dependencies] +elain = "0.3.0" +parking_lot = "0.12.1" + +[dev-dependencies] +criterion = "0.5.1" +tinyrand = "0.5.0" + +[[bench]] +name = "scan_1gb" +harness = false \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..715aca9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 localcc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..64a0165 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# LightningScanner + +A lightning-fast memory pattern scanner, capable of scanning gigabytes of data per second. + +## Installation + +``` +cargo add lightningscanner +``` + +## Examples + +Here's an example of how to find an IDA-style memory pattern inside of a binary. + +```rust + +use lightningscanner::Scanner; + +fn main() { + let binary = [0xab, 0xec, 0x48, 0x89, 0x5c, 0x24, 0xee, 0x48, 0x89, 0x6c]; + + let scanner = Scanner::new("48 89 5c 24 ?? 48 89 6c"); + let result = scanner.find(None, &binary); + + println!("{:?}", result); +} + +``` diff --git a/benches/scan_1gb.rs b/benches/scan_1gb.rs new file mode 100644 index 0000000..bf81ba6 --- /dev/null +++ b/benches/scan_1gb.rs @@ -0,0 +1,53 @@ +use criterion::{criterion_group, criterion_main, Criterion, Throughput}; +use lightningscanner::{ScanMode, Scanner}; +use tinyrand::{Rand, Wyrand}; + +fn benchmark(c: &mut Criterion) { + const GB: usize = 1073741824; + + let mut data = Vec::with_capacity(GB); + let mut rand = Wyrand::default(); + for _ in 0..(GB / 2) { + let value = rand.next_u16(); + data.push((value & 0xff) as u8); + data.push(((value >> 8) & 0xff) as u8); + } + + const PATTERN_DATA: [u8; 32] = [ + 0x48, 0x89, 0x5C, 0x24, 0x00, 0x48, 0x89, 0x6C, 0x24, 0x00, 0x48, 0x89, 0x74, 0x24, 0x00, + 0x48, 0x89, 0x7C, 0x24, 0x00, 0x41, 0x56, 0x41, 0x57, 0x4c, 0x8b, 0x79, 0x38, 0xaa, 0xbf, + 0xcd, 0x00, + ]; + + let len = data.len(); + data[len - 32..].copy_from_slice(&PATTERN_DATA); + + let mut group = c.benchmark_group("1gb scan"); + group.throughput(Throughput::Bytes(GB as u64)); + + group.bench_function("scalar", |b| { + let scanner = Scanner::new("48 89 5c 24 ?? 48 89 6c 24 ?? 48 89 74 24 ?? 48 89 7c 24 ?? 41 56 41 57 4c 8b 79 38 aa bf cd"); + b.iter(|| { + scanner.find(Some(ScanMode::Scalar), &data) + }); + }); + + group.bench_function("sse4.2", |b| { + let scanner = Scanner::new("48 89 5c 24 ?? 48 89 6c 24 ?? 48 89 74 24 ?? 48 89 7c 24 ?? 41 56 41 57 4c 8b 79 38 aa bf cd"); + b.iter(|| { + scanner.find(Some(ScanMode::Sse42), &data) + }); + }); + + group.bench_function("avx2", |b| { + let scanner = Scanner::new("48 89 5c 24 ?? 48 89 6c 24 ?? 48 89 74 24 ?? 48 89 7c 24 ?? 41 56 41 57 4c 8b 79 38 aa bf cd"); + b.iter(|| { + scanner.find(Some(ScanMode::Avx2), &data) + }); + }); + + group.finish(); +} + +criterion_group!(benches, benchmark); +criterion_main!(benches); diff --git a/src/aligned_bytes.rs b/src/aligned_bytes.rs new file mode 100644 index 0000000..0d13dae --- /dev/null +++ b/src/aligned_bytes.rs @@ -0,0 +1,88 @@ +//! Aligned byte storage implementation + +use elain::{Align, Alignment}; +use std::ops::Deref; +use std::{alloc, ptr}; + +#[repr(C)] +pub struct AlignedBytes(Align, [u8]) +where + Align: Alignment; + +#[repr(C)] +struct AlignedByte(Align, u8) +where + Align: Alignment; + +impl AlignedBytes +where + Align: Alignment, +{ + /// Create a new `AlignedBytes` instance from a slice + pub fn new(data: &[u8]) -> Box> { + if data.is_empty() { + // SAFETY: The pointer isn't null and is aligned because it was returned from + // `NonNull::dangling`. The length is zero, so no other requirements apply. + unsafe { + Self::from_byte_ptr( + ptr::NonNull::>::dangling() + .as_ptr() + .cast::(), + 0, + ) + } + } else { + if data.len().checked_next_multiple_of(N).unwrap_or(usize::MAX) > isize::MAX as usize { + panic!("unable to allocate {} bytes (overflows isize)", data.len()); + } + + // SAFETY: The alignment `N` is not zero and is a power of two. `data.len()`'s next + // multiple of N does not overflow an `isize`. + let layout = unsafe { alloc::Layout::from_size_align_unchecked(data.len(), N) }; + + // SAFETY: `layout`'s size is not zero. + let ptr = unsafe { alloc::alloc(layout) }; + + if ptr.is_null() { + alloc::handle_alloc_error(layout) + } else { + // SAFETY: `data.as_ptr()` is valid for reads because it comes from a slice. `ptr` is + // valid for writes because it was returned from `alloc::alloc` and is not null. They + // can't overlap because `data` has a lifetime longer than this function and `ptr` was + // just allocated in this function. + unsafe { + ptr::copy_nonoverlapping(data.as_ptr(), ptr, data.len()); + } + + // SAFETY: `ptr` is non-null, and is aligned because it was returned from `alloc::alloc`. + // The data pointed to is valid for reads and writes because it was initialized by + // `ptr::copy_nonoverlapping`. `ptr` is currently allocated by the global allocator. + unsafe { Self::from_byte_ptr(ptr, data.len()) } + } + } + } + + /// # Safety + /// + /// `ptr` must be non-null, aligned to N bytes, and valid for reads and writes for `len` bytes. + /// The data pointed to must be initialized. If `len` is non-zero, `ptr` must be currently + /// allocated by the global allocator and valid to deallocate. + unsafe fn from_byte_ptr(ptr: *mut u8, len: usize) -> Box> { + let slice_ptr = ptr::slice_from_raw_parts_mut(ptr, len); + + // SAFETY: The invariants of `Box::from_raw` are enforced by this function's safety + // contract. + unsafe { Box::from_raw(slice_ptr as *mut AlignedBytes) } + } +} + +impl Deref for AlignedBytes +where + Align: Alignment, +{ + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.1 + } +} diff --git a/src/backends/avx2.rs b/src/backends/avx2.rs new file mode 100644 index 0000000..108c8e5 --- /dev/null +++ b/src/backends/avx2.rs @@ -0,0 +1,66 @@ +//! AVX2 pattern scanning backend + +use crate::pattern::Pattern; +use crate::ScanResult; +use std::arch::x86_64::{ + _mm256_blendv_epi8, _mm256_cmpeq_epi8, _mm256_load_si256, _mm256_loadu_si256, + _mm256_movemask_epi8, _mm256_set1_epi8, +}; +use std::ptr; + +/// Find the first occurrence of a pattern in the binary +/// using AVX2 instructions +/// +/// # Safety +/// +/// * `binary` - is a valid pointer +/// +/// * `binary_size` - corresponds to a valid size of `binary` +/// +/// * Currently running CPU supports AVX2 +#[target_feature(enable = "avx2")] +pub unsafe fn find(pattern_data: &Pattern, binary: *const u8, binary_size: usize) -> ScanResult { + const UNIT_SIZE: usize = 32; + + let mut processed_size = 0; + + // SAFETY: this function is only called if the CPU supports AVX2 + unsafe { + let mut pattern = _mm256_load_si256(pattern_data.data.as_ptr() as *const _); + let mut mask = _mm256_load_si256(pattern_data.mask.as_ptr() as *const _); + let all_zeros = _mm256_set1_epi8(0x00); + + let mut chunk = 0; + while chunk < binary_size { + let chunk_data = _mm256_loadu_si256(binary.add(chunk) as *const _); + + let blend = _mm256_blendv_epi8(all_zeros, chunk_data, mask); + let eq = _mm256_cmpeq_epi8(pattern, blend); + + if _mm256_movemask_epi8(eq) as u32 == 0xffffffff { + processed_size += UNIT_SIZE; + + if processed_size < pattern_data.unpadded_size { + chunk += UNIT_SIZE - 1; + + pattern = _mm256_load_si256( + pattern_data.data.as_ptr().add(processed_size) as *const _ + ); + mask = _mm256_load_si256( + pattern_data.mask.as_ptr().add(processed_size) as *const _ + ); + } else { + let addr = binary.add(chunk).sub(processed_size).add(UNIT_SIZE); + return ScanResult { addr }; + } + } else { + pattern = _mm256_load_si256(pattern_data.data.as_ptr() as *const _); + mask = _mm256_load_si256(pattern_data.mask.as_ptr() as *const _); + processed_size = 0; + } + chunk += 1; + } + } + + ScanResult { addr: ptr::null() } +} diff --git a/src/backends/mod.rs b/src/backends/mod.rs new file mode 100644 index 0000000..83641b9 --- /dev/null +++ b/src/backends/mod.rs @@ -0,0 +1,44 @@ +//! Pattern scanning backends + +use crate::pattern::Pattern; +use crate::{ScanMode, ScanResult}; + +#[cfg(target_arch = "x86_64")] +mod avx2; +mod scalar; +#[cfg(target_arch = "x86_64")] +mod sse42; + +/// Find the first occurrence of a pattern in the binary +/// +/// # Safety +/// +/// * `binary` - is a valid pointer +/// * `binary_size` - corresponds to a valid size of `binary` +pub unsafe fn find( + pattern: &Pattern, + preferred_scan_mode: Option, + binary: *const u8, + binary_size: usize, +) -> ScanResult { + #[cfg(target_arch = "x86_64")] + { + let avx2 = is_x86_feature_detected!("avx2"); + let sse42 = is_x86_feature_detected!("sse4.2"); + + match (preferred_scan_mode, avx2, sse42) { + (Some(ScanMode::Avx2) | None, true, _) => { + // SAFETY: safe to call as long as the safety conditions were met for this function + return unsafe { avx2::find(pattern, binary, binary_size) }; + } + (Some(ScanMode::Sse42), _, true) | (None, false, true) => { + // SAFETY: safe to call as long as the safety conditions were met for this function + return unsafe { sse42::find(pattern, binary, binary_size) }; + } + _ => {} + } + } + + // SAFETY: safe to call as long as the safety conditions were met for this function + unsafe { scalar::find(pattern, binary, binary_size) } +} diff --git a/src/backends/scalar.rs b/src/backends/scalar.rs new file mode 100644 index 0000000..4af80b6 --- /dev/null +++ b/src/backends/scalar.rs @@ -0,0 +1,51 @@ +//! Scalar pattern scanning backend + +use crate::pattern::Pattern; +use crate::ScanResult; +use std::ptr; + +/// Find the first occurrence of a pattern in the binary +/// using scalar instructions +/// +/// # Safety +/// +/// * `binary` - is a valid pointer +/// +/// * `binary_size` - corresponds to a valid size of `binary` +pub unsafe fn find(pattern: &Pattern, binary: *const u8, binary_size: usize) -> ScanResult { + // SAFETY: safe to call as the pointer will be exactly one byte past the end of the binary + let binary_end = unsafe { binary.add(binary_size) }; + + for binary_offset in 0..binary_size { + let mut found = true; + + for pattern_offset in 0..pattern.data.len() { + if pattern.mask[pattern_offset] == 0x00 { + continue; + } + + // SAFETY: safe to call as further behavior doesn't rely on overflows + // further reads from this address are safe because of the min call + // ensuring the pointer is always in bounds + let checked_addr = unsafe { + binary + .add(binary_offset) + .add(pattern_offset) + .min(binary_end) + }; + + // SAFETY: checked addr is always in binary bounds + if unsafe { *checked_addr } != pattern.data[pattern_offset] { + found = false; + break; + } + } + + if found { + // SAFETY: safe to call because binary offset never gets out of binary+binary_size space + let addr = unsafe { binary.add(binary_offset) }; + return ScanResult { addr }; + } + } + ScanResult { addr: ptr::null() } +} diff --git a/src/backends/sse42.rs b/src/backends/sse42.rs new file mode 100644 index 0000000..ff5ceec --- /dev/null +++ b/src/backends/sse42.rs @@ -0,0 +1,63 @@ +//! SSE4.2 pattern scanning backend +//! +use crate::pattern::Pattern; +use crate::ScanResult; +use std::arch::x86_64::{ + _mm_blendv_epi8, _mm_cmpeq_epi8, _mm_load_si128, _mm_loadu_si128, _mm_movemask_epi8, + _mm_set1_epi8, +}; +use std::ptr; + +/// Find the first occurrence of a pattern in the binary +/// using SSE4.2 instructions +/// +/// # Safety +/// +/// * `binary` - is a valid pointer +/// +/// * `binary_size` - corresponds to a valid size of `binary` +/// +/// * Currently running CPU supports SSE4.2 +#[target_feature(enable = "sse4.2")] +pub unsafe fn find(pattern_data: &Pattern, binary: *const u8, binary_size: usize) -> ScanResult { + const UNIT_SIZE: usize = 16; + + let mut processed_size = 0; + + // SAFETY: this function is only called if the CPU supports SSE4.2 + unsafe { + let mut pattern = _mm_load_si128(pattern_data.data.as_ptr() as *const _); + let mut mask = _mm_load_si128(pattern_data.mask.as_ptr() as *const _); + let all_zeros = _mm_set1_epi8(0x00); + + let mut chunk = 0; + + while chunk < binary_size { + let chunk_data = _mm_loadu_si128(binary.add(chunk) as *const _); + let blend = _mm_blendv_epi8(all_zeros, chunk_data, mask); + let eq = _mm_cmpeq_epi8(pattern, blend); + + if _mm_movemask_epi8(eq) == 0xffff { + processed_size += UNIT_SIZE; + + if processed_size < pattern_data.unpadded_size { + chunk += UNIT_SIZE - 1; + pattern = + _mm_load_si128(pattern_data.data.as_ptr().add(processed_size) as *const _); + mask = + _mm_load_si128(pattern_data.mask.as_ptr().add(processed_size) as *const _); + } else { + let addr = binary.add(chunk).sub(processed_size).add(UNIT_SIZE); + return ScanResult { addr }; + } + } else { + pattern = _mm_load_si128(pattern_data.data.as_ptr() as *const _); + mask = _mm_load_si128(pattern_data.mask.as_ptr() as *const _); + processed_size = 0; + } + + chunk += 1; + } + } + ScanResult { addr: ptr::null() } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..84373c3 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,192 @@ +//! LightningScanner +//! +//! A lightning-fast memory pattern scanner, capable of scanning gigabytes of data per second. +//! +//! # Example +//! +//! ``` +//! use lightningscanner::Scanner; +//! +//! let binary = [0xab, 0xec, 0x48, 0x89, 0x5c, 0x24, 0xee, 0x48, 0x89, 0x6c]; +//! +//! let scanner = Scanner::new("48 89 5c 24 ?? 48 89 6c"); +//! let result = scanner.find(None, &binary); +//! +//! println!("{:?}", result); +//! ``` +#![deny(unsafe_op_in_unsafe_fn, clippy::undocumented_unsafe_blocks)] + +use crate::pattern::Pattern; + +mod aligned_bytes; +mod backends; +pub mod pattern; + +/// Single result IDA-style pattern scanner +/// +/// A pattern scanner that searches for an IDA-style pattern +/// and returns the pointer to the first occurrence in the binary. +pub struct Scanner(Pattern); + +impl Scanner { + /// Create a new [`Scanner`] instance + /// + /// # Example + /// + /// ``` + /// use lightningscanner::Scanner; + /// + /// let scanner = Scanner::new("48 89 5c 24 ?? 48 89 6c"); + /// ``` + pub fn new(pattern: &str) -> Self { + Scanner(Pattern::new(pattern)) + } + + /// Find the first occurence of the pattern in the binary + /// + /// # Params + /// + /// * `preferred_scan_mode` - preferred scan mode to use (Avx2, Sse42, Scalar) + /// if the preferred mode is not available, will choose the fastest out of the availble ones + /// + /// * `binary` - binary to search for the pattern in + /// + /// # Example + /// + /// ``` + /// use lightningscanner::Scanner; + /// + /// let binary = [0xab, 0xec, 0x48, 0x89, 0x5c, 0x24, 0xee, 0x48, 0x89, 0x6c]; + /// + /// let scanner = Scanner::new("48 89 5c 24 ?? 48 89 6c"); + /// let result = scanner.find(None, &binary); + /// + /// println!("{:?}", result); + /// ``` + pub fn find(&self, preferred_scan_mode: Option, binary: &[u8]) -> ScanResult { + // SAFETY: always safe to call because binary is a valid slice + unsafe { self.find_ptr(preferred_scan_mode, binary.as_ptr(), binary.len()) } + } + + /// Find the first occurence of the pattern in the binary + /// + /// # Params + /// + /// * `preferred_scan_mode` - preferred scan mode to use (Avx2, Sse42, Scalar) + /// if the preferred mode is not available, will choose the fastest out of the availble ones + /// + /// * `binary_ptr` - pointer to the first element of the binary to search the pattern in + /// + /// * `binary_size` - binary size + /// + /// # Safety + /// + /// * `binary_ptr` - is a valid pointer + /// + /// * `binary_size` - corresponds to a valid size of `binary` + /// + /// # Example + /// + /// ``` + /// use lightningscanner::Scanner; + /// + /// let binary = [0xab, 0xec, 0x48, 0x89, 0x5c, 0x24, 0xee, 0x48, 0x89, 0x6c]; + /// + /// let scanner = Scanner::new("48 89 5c 24 ?? 48 89 6c"); + /// let result = unsafe { scanner.find_ptr(None, binary.as_ptr(), binary.len()) }; + /// + /// println!("{:?}", result); + /// ``` + pub unsafe fn find_ptr( + &self, + preferred_scan_mode: Option, + binary_ptr: *const u8, + binary_size: usize, + ) -> ScanResult { + // SAFETY: safe to call as long as the safety conditions were met for this function + unsafe { backends::find(&self.0, preferred_scan_mode, binary_ptr, binary_size) } + } +} + +impl From for Scanner { + fn from(value: Pattern) -> Self { + Scanner(value) + } +} + +/// Scan mode +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum ScanMode { + /// Scalar scan mode + Scalar, + /// Scan mode that uses SSE4.2 SIMD instructions + Sse42, + /// Scan mode that uses AVX2 SIMD instructions + Avx2, +} + +/// Scan result +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct ScanResult { + addr: *const u8, +} + +impl ScanResult { + /// Check if the result is a valid pointer + pub fn is_valid(&self) -> bool { + !self.addr.is_null() + } + + /// Get address of this scan result + pub fn get_addr(&self) -> *const u8 { + self.addr + } + + /// Get a pointer to the value + /// + /// Gets the result address, shifts by `offset` bytes and casts to *const T + /// + /// # Safety + /// + /// If any of the following conditions are violated, the result is Undefined + /// Behavior: + /// + /// * Both the starting and resulting pointer must be either in bounds or one + /// byte past the end of the same [allocated object]. + /// + /// * The computed offset, **in bytes**, cannot overflow an `isize`. + /// + /// * The offset being in bounds cannot rely on "wrapping around" the address + /// space. That is, the infinite-precision sum, **in bytes** must fit in a usize. + /// + /// [allocated object]: crate::ptr#allocated-object + pub unsafe fn get_ptr(&self, offset: isize) -> *const T { + // SAFETY: the caller must uphold the safety contract for `get_ptr`. + unsafe { self.addr.offset(offset) as *const _ } + } + + /// Get a mutable pointer to the value + /// + /// Gets the result address, shifts by `offset` bytes and casts to *mut T + /// + /// # Safety + /// + /// If any of the following conditions are violated, the result is Undefined + /// Behavior: + /// + /// * Both the starting and resulting pointer must be either in bounds or one + /// byte past the end of the same [allocated object]. + /// + /// * The computed offset, **in bytes**, cannot overflow an `isize`. + /// + /// * The offset being in bounds cannot rely on "wrapping around" the address + /// space. That is, the infinite-precision sum, **in bytes** must fit in a usize. + /// + /// * The scan result was produced from an immutable reference + /// + /// [allocated object]: crate::ptr#allocated-object + pub unsafe fn get_mut_ptr(&self, offset: isize) -> *mut T { + // SAFETY: the caller must uphold the safety contract for `get_mut_ptr`. + unsafe { self.addr.offset(offset) as *mut _ } + } +} diff --git a/src/pattern.rs b/src/pattern.rs new file mode 100644 index 0000000..ec9b127 --- /dev/null +++ b/src/pattern.rs @@ -0,0 +1,96 @@ +//! IDA-style pattern + +use crate::aligned_bytes::AlignedBytes; + +/// An IDA-style binary pattern +pub struct Pattern { + pub(crate) data: Box>, + pub(crate) mask: Box>, + pub(crate) unpadded_size: usize, +} + +impl Pattern { + const ALIGNMENT: usize = 32; + + /// Create a new IDA-style [`Pattern`] instance + /// + /// # Example + /// + /// ``` + /// use lightningscanner::pattern::Pattern; + /// + /// Pattern::new("48 89 5c 24 ?? 48 89 6c"); + /// ``` + pub fn new(pattern: &str) -> Self { + let pattern = pattern.chars().collect::>(); + + let mut data = Vec::new(); + let mut mask = Vec::new(); + + let mut i = 0; + while i < pattern.len() { + let symbol = pattern[i]; + let next_symbol = pattern.get(i + 1).copied().unwrap_or('\0'); + + i += 1; + + match symbol { + ' ' => continue, + '?' => { + data.push(0x00); + mask.push(0x00); + + if next_symbol == '?' { + i += 1; + } + + continue; + } + _ => { + let byte = Self::char_to_byte(symbol) << 4 | Self::char_to_byte(next_symbol); + + data.push(byte); + mask.push(0xff); + + i += 1; + } + } + + if symbol == ' ' { + continue; + } + } + + let unpadded_size = data.len(); + + let count = f32::ceil(unpadded_size as f32 / Self::ALIGNMENT as f32) as usize; + let padding_size = count * Self::ALIGNMENT - unpadded_size; + + data.resize(unpadded_size + padding_size, 0); + mask.resize(unpadded_size + padding_size, 0); + + Pattern { + data: AlignedBytes::new(&data), + mask: AlignedBytes::new(&mask), + unpadded_size, + } + } + + const fn char_to_byte(c: char) -> u8 { + if c >= 'a' && c <= 'z' { + c as u8 - b'a' + 0xA + } else if c >= 'A' && c <= 'Z' { + c as u8 - b'A' + 0xA + } else if c >= '0' && c <= '9' { + c as u8 - b'0' + } else { + 0 + } + } +} + +impl From<&str> for Pattern { + fn from(value: &str) -> Self { + Pattern::new(value) + } +} diff --git a/tests/big_pattern.rs b/tests/big_pattern.rs new file mode 100644 index 0000000..b55d71c --- /dev/null +++ b/tests/big_pattern.rs @@ -0,0 +1,49 @@ +use lightningscanner::{ScanMode, Scanner}; + +const PATTERN: &str = "42 cd e7 f8 21 5b d6 b8 d1 be 12 0e 85 34 c4 ?? 03 7e bc 7b b9 29 b6 07 31 7e ?? dd 3e 0a e7 71 f3 b7 76 3f 36 e1 f3 3b c6 e5 ?? f8 97 67 86 60"; + +const DATA_SET: [u8; 128] = [ + 0xdb, 0x2f, 0x16, 0x37, 0xd5, 0xff, 0x12, 0x74, 0x7c, 0xf2, 0x27, 0xed, 0x7b, 0x2e, 0x54, 0x9a, + 0xe2, 0xec, 0x73, 0x9e, 0xbb, 0xd1, 0x42, 0xc2, 0x0c, 0x9e, 0xa3, 0xa1, 0x10, 0xb3, 0x97, 0xf2, + 0xaf, 0x47, 0x43, 0x9f, 0xa0, 0x9e, 0x87, 0x00, 0x76, 0x5c, 0x3a, 0xae, 0x40, 0x30, 0x7f, 0xc0, + 0x53, 0xf4, 0xeb, 0xcc, 0xf2, 0x04, 0x6d, 0x35, 0x5c, 0x88, 0xc3, 0x83, 0xdf, 0x9b, 0xc9, 0x44, + 0x42, 0xcd, 0xe7, 0xf8, 0x21, 0x5b, 0xd6, 0xb8, 0xd1, 0xbe, 0x12, 0x0e, 0x85, 0x34, 0xc4, 0xf9, + 0x03, 0x7e, 0xbc, 0x7b, 0xb9, 0x29, 0xb6, 0x07, 0x31, 0x7e, 0x69, 0xdd, 0x3e, 0x0a, 0xe7, 0x71, + 0xf3, 0xb7, 0x76, 0x3f, 0x36, 0xe1, 0xf3, 0x3b, 0xc6, 0xe5, 0x69, 0xf8, 0x97, 0x67, 0x86, 0x60, + 0x4d, 0x2b, 0xf6, 0x2f, 0x9e, 0x03, 0x5f, 0x56, 0x02, 0x2e, 0x5f, 0x58, 0x9c, 0x6d, 0xa3, 0xf5, +]; + +#[test] +#[cfg(target_feature = "avx2")] +fn avx2() { + let scanner = Scanner::new(PATTERN); + let result = scanner.find(Some(ScanMode::Avx2), &DATA_SET); + + let data_set_addr = DATA_SET.as_ptr() as usize; + let ptr = result.get_addr() as usize; + + assert_eq!(ptr - data_set_addr, 0x40); +} + +#[test] +#[cfg(target_feature = "sse4.2")] +fn sse42() { + let scanner = Scanner::new(PATTERN); + let result = scanner.find(Some(ScanMode::Sse42), &DATA_SET); + + let data_set_addr = DATA_SET.as_ptr() as usize; + let ptr = result.get_addr() as usize; + + assert_eq!(ptr - data_set_addr, 0x40); +} + +#[test] +fn scalar() { + let scanner = Scanner::new(PATTERN); + let result = scanner.find(Some(ScanMode::Scalar), &DATA_SET); + + let data_set_addr = DATA_SET.as_ptr() as usize; + let ptr = result.get_addr() as usize; + + assert_eq!(ptr - data_set_addr, 0x40); +} diff --git a/tests/similar_pattern.rs b/tests/similar_pattern.rs new file mode 100644 index 0000000..6655ba0 --- /dev/null +++ b/tests/similar_pattern.rs @@ -0,0 +1,46 @@ +use lightningscanner::{ScanMode, Scanner}; + +const PATTERN: &str = "40 57 48 83 EC ? 48 C7 44 24 ? ? ? ? ? 48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 49 8B E9 48 8B F2"; + +const DATA_SET: [u8; 72] = [ + 0x40, 0x57, 0x48, 0x83, 0xEC, 0x30, 0x48, 0xC7, 0x44, 0x24, 0x28, 0xFE, 0xFF, 0xFF, 0xFF, 0x48, + 0x89, 0x5C, 0x24, 0x40, 0x48, 0x89, 0x6C, 0x24, 0x48, 0x48, 0x89, 0x74, 0x24, 0x50, 0x49, 0x8B, + 0xE9, 0x49, 0x8B, 0xF0, 0x40, 0x57, 0x48, 0x83, 0xEC, 0x30, 0x48, 0xC7, 0x44, 0x24, 0x28, 0xFE, + 0xFF, 0xFF, 0xFF, 0x48, 0x89, 0x5C, 0x24, 0x40, 0x48, 0x89, 0x6C, 0x24, 0x48, 0x48, 0x89, 0x74, + 0x24, 0x50, 0x49, 0x8B, 0xE9, 0x48, 0x8B, 0xF2, +]; + +#[test] +#[cfg(target_feature = "avx2")] +fn avx2() { + let scanner = Scanner::new(PATTERN); + let result = scanner.find(Some(ScanMode::Avx2), &DATA_SET); + + let data_set_addr = DATA_SET.as_ptr() as usize; + let ptr = result.get_addr() as usize; + + assert_eq!(ptr - data_set_addr, 0x24); +} + +#[test] +#[cfg(target_feature = "sse4.2")] +fn sse42() { + let scanner = Scanner::new(PATTERN); + let result = scanner.find(Some(ScanMode::Sse42), &DATA_SET); + + let data_set_addr = DATA_SET.as_ptr() as usize; + let ptr = result.get_addr() as usize; + + assert_eq!(ptr - data_set_addr, 0x24); +} + +#[test] +fn scalar() { + let scanner = Scanner::new(PATTERN); + let result = scanner.find(Some(ScanMode::Scalar), &DATA_SET); + + let data_set_addr = DATA_SET.as_ptr() as usize; + let ptr = result.get_addr() as usize; + + assert_eq!(ptr - data_set_addr, 0x24); +} diff --git a/tests/small_pattern.rs b/tests/small_pattern.rs new file mode 100644 index 0000000..6fb75df --- /dev/null +++ b/tests/small_pattern.rs @@ -0,0 +1,49 @@ +use lightningscanner::{ScanMode, Scanner}; + +const PATTERN: &str = "a0 9e 87 00 ?? 5c"; + +const DATA_SET: [u8; 128] = [ + 0xdb, 0x2f, 0x16, 0x37, 0xd5, 0xff, 0x12, 0x74, 0x7c, 0xf2, 0x27, 0xed, 0x7b, 0x2e, 0x54, 0x9a, + 0xe2, 0xec, 0x73, 0x9e, 0xbb, 0xd1, 0x42, 0xc2, 0x0c, 0x9e, 0xa3, 0xa1, 0x10, 0xb3, 0x97, 0xf2, + 0xaf, 0x47, 0x43, 0x9f, 0xa0, 0x9e, 0x87, 0x00, 0x76, 0x5c, 0x3a, 0xae, 0x40, 0x30, 0x7f, 0xc0, + 0x53, 0xf4, 0xeb, 0xcc, 0xf2, 0x04, 0x6d, 0x35, 0x5c, 0x88, 0xc3, 0x83, 0xdf, 0x9b, 0xc9, 0x44, + 0x42, 0xcd, 0xe7, 0xf8, 0x21, 0x5b, 0xd6, 0xb8, 0xd1, 0xbe, 0x12, 0x0e, 0x85, 0x34, 0xc4, 0xf9, + 0x03, 0x7e, 0xbc, 0x7b, 0xb9, 0x29, 0xb6, 0x07, 0x31, 0x7e, 0x69, 0xdd, 0x3e, 0x0a, 0xe7, 0x71, + 0xf3, 0xb7, 0x76, 0x3f, 0x36, 0xe1, 0xf3, 0x3b, 0xc6, 0xe5, 0x69, 0xf8, 0x97, 0x67, 0x86, 0x60, + 0x4d, 0x2b, 0xf6, 0x2f, 0x9e, 0x03, 0x5f, 0x56, 0x02, 0x2e, 0x5f, 0x58, 0x9c, 0x6d, 0xa3, 0xf5, +]; + +#[test] +#[cfg(target_feature = "avx2")] +fn avx2() { + let scanner = Scanner::new(PATTERN); + let result = scanner.find(Some(ScanMode::Avx2), &DATA_SET); + + let data_set_addr = DATA_SET.as_ptr() as usize; + let ptr = result.get_addr() as usize; + + assert_eq!(ptr - data_set_addr, 0x24); +} + +#[test] +#[cfg(target_feature = "sse4.2")] +fn sse42() { + let scanner = Scanner::new(PATTERN); + let result = scanner.find(Some(ScanMode::Sse42), &DATA_SET); + + let data_set_addr = DATA_SET.as_ptr() as usize; + let ptr = result.get_addr() as usize; + + assert_eq!(ptr - data_set_addr, 0x24); +} + +#[test] +fn scalar() { + let scanner = Scanner::new(PATTERN); + let result = scanner.find(Some(ScanMode::Scalar), &DATA_SET); + + let data_set_addr = DATA_SET.as_ptr() as usize; + let ptr = result.get_addr() as usize; + + assert_eq!(ptr - data_set_addr, 0x24); +}