diff options
| author | Mason Reed <mason@vector35.com> | 2025-07-13 17:34:02 -0400 |
|---|---|---|
| committer | Mason Reed <mason@vector35.com> | 2025-07-15 12:34:43 -0400 |
| commit | bd1b6b8a1a75df296da5c7afa8107ba301c9cd8e (patch) | |
| tree | b0155db15147c9d91da10519ee72eb87560ba2d4 | |
| parent | fa3c81fd9b4287792c3e3d534a7d237d6005cb5f (diff) | |
[Rust] Add `BinaryView::search` and friends
| -rw-r--r-- | Cargo.lock | 2 | ||||
| -rw-r--r-- | plugins/svd/src/mapper.rs | 2 | ||||
| -rw-r--r-- | rust/Cargo.toml | 2 | ||||
| -rw-r--r-- | rust/src/architecture.rs | 5 | ||||
| -rw-r--r-- | rust/src/binary_view.rs | 197 | ||||
| -rw-r--r-- | rust/src/binary_view/search.rs | 109 | ||||
| -rw-r--r-- | rust/src/data_buffer.rs | 21 | ||||
| -rw-r--r-- | rust/tests/binary_view.rs | 48 |
8 files changed, 365 insertions, 21 deletions
@@ -179,6 +179,8 @@ dependencies = [ "log", "rayon", "rstest", + "serde", + "serde_derive", "serde_json", "serial_test", "tempfile", diff --git a/plugins/svd/src/mapper.rs b/plugins/svd/src/mapper.rs index 314fba76..8846c858 100644 --- a/plugins/svd/src/mapper.rs +++ b/plugins/svd/src/mapper.rs @@ -167,7 +167,7 @@ impl DeviceMapper { if self.settings.add_backing_regions { // Because adding a memory region will add a possibly large memory buffer in the BNDB, this // is optional. if a user disables this, they cannot write to the segment until they add a backing memory region. - let data_memory = DataBuffer::new(&vec![0; address_block.size as usize]).unwrap(); + let data_memory = DataBuffer::new(&vec![0; address_block.size as usize]); let added_memory = view.memory_map().add_data_memory_region( &block_name, block_addr, diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 9b29b283..0b9ac4f3 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -18,6 +18,8 @@ log = { version = "0.4", features = ["std"] } rayon = { version = "1.10", optional = true } binaryninjacore-sys = { path = "binaryninjacore-sys" } thiserror = "2.0" +serde = "1.0" +serde_derive = "1.0" # Parts of the collaboration and workflow APIs consume and produce JSON. serde_json = "1.0" diff --git a/rust/src/architecture.rs b/rust/src/architecture.rs index d226230c..6b33b6bb 100644 --- a/rust/src/architecture.rs +++ b/rust/src/architecture.rs @@ -1828,10 +1828,7 @@ impl Architecture for CoreArchitecture { fn assemble(&self, code: &str, addr: u64) -> Result<Vec<u8>, String> { let code = CString::new(code).map_err(|_| "Invalid encoding in code string".to_string())?; - let result = match DataBuffer::new(&[]) { - Ok(result) => result, - Err(_) => return Err("Result buffer allocation failed".to_string()), - }; + let result = DataBuffer::new(&[]); // TODO: This is actually a list of errors. let mut error_raw: *mut c_char = std::ptr::null_mut(); let res = unsafe { diff --git a/rust/src/binary_view.rs b/rust/src/binary_view.rs index 73c3a2ab..2db833c4 100644 --- a/rust/src/binary_view.rs +++ b/rust/src/binary_view.rs @@ -34,7 +34,7 @@ use crate::external_library::{ExternalLibrary, ExternalLocation}; use crate::file_accessor::{Accessor, FileAccessor}; use crate::file_metadata::FileMetadata; use crate::flowgraph::FlowGraph; -use crate::function::{Function, NativeBlock}; +use crate::function::{Function, FunctionViewType, NativeBlock}; use crate::linear_view::{LinearDisassemblyLine, LinearViewCursor}; use crate::metadata::Metadata; use crate::platform::Platform; @@ -66,8 +66,11 @@ use std::{result, slice}; pub mod memory_map; pub mod reader; +pub mod search; pub mod writer; +use crate::binary_view::search::SearchQuery; +use crate::disassembly::DisassemblySettings; use crate::workflow::Workflow; pub use memory_map::MemoryMap; pub use reader::BinaryReader; @@ -78,6 +81,7 @@ pub type BinaryViewEventType = BNBinaryViewEventType; pub type AnalysisState = BNAnalysisState; pub type ModificationStatus = BNModificationStatus; pub type StringType = BNStringType; +pub type FindFlag = BNFindFlag; #[allow(clippy::len_without_is_empty)] pub trait BinaryViewBase: AsRef<BinaryView> { @@ -230,6 +234,197 @@ pub trait BinaryViewExt: BinaryViewBase { read_size } + /// Search the view using the query options. + fn search<C: FnMut(u64, &DataBuffer) -> bool>(&self, query: &SearchQuery, on_match: C) -> bool { + self.search_with_progress(query, on_match, NoProgressCallback) + } + + /// Search the view using the query options. + fn search_with_progress<P: ProgressCallback, C: FnMut(u64, &DataBuffer) -> bool>( + &self, + query: &SearchQuery, + mut on_match: C, + mut progress: P, + ) -> bool { + unsafe extern "C" fn cb_on_match<C: FnMut(u64, &DataBuffer) -> bool>( + ctx: *mut c_void, + offset: u64, + data: *mut BNDataBuffer, + ) -> bool { + let f = ctx as *mut C; + let buffer = DataBuffer::from_raw(data); + (*f)(offset, &buffer) + } + + let query = query.to_json().to_cstr(); + unsafe { + BNSearch( + self.as_ref().handle, + query.as_ptr(), + &mut progress as *mut P as *mut c_void, + Some(P::cb_progress_callback), + &mut on_match as *const C as *mut c_void, + Some(cb_on_match::<C>), + ) + } + } + + fn find_next_data(&self, start: u64, end: u64, data: &DataBuffer) -> Option<u64> { + self.find_next_data_with_opts( + start, + end, + data, + FindFlag::FindCaseInsensitive, + NoProgressCallback, + ) + } + + /// # Warning + /// + /// This function is likely to be changed to take in a "query" structure. Or deprecated entirely. + fn find_next_data_with_opts<P: ProgressCallback>( + &self, + start: u64, + end: u64, + data: &DataBuffer, + flag: FindFlag, + mut progress: P, + ) -> Option<u64> { + let mut result: u64 = 0; + let found = unsafe { + BNFindNextDataWithProgress( + self.as_ref().handle, + start, + end, + data.as_raw(), + &mut result, + flag, + &mut progress as *mut P as *mut c_void, + Some(P::cb_progress_callback), + ) + }; + + if found { + Some(result) + } else { + None + } + } + + fn find_next_constant( + &self, + start: u64, + end: u64, + constant: u64, + view_type: FunctionViewType, + ) -> Option<u64> { + // TODO: What are the best "default" settings? + let settings = DisassemblySettings::new(); + self.find_next_constant_with_opts( + start, + end, + constant, + &settings, + view_type, + NoProgressCallback, + ) + } + + /// # Warning + /// + /// This function is likely to be changed to take in a "query" structure. + fn find_next_constant_with_opts<P: ProgressCallback>( + &self, + start: u64, + end: u64, + constant: u64, + disasm_settings: &DisassemblySettings, + view_type: FunctionViewType, + mut progress: P, + ) -> Option<u64> { + let mut result: u64 = 0; + let raw_view_type = FunctionViewType::into_raw(view_type); + let found = unsafe { + BNFindNextConstantWithProgress( + self.as_ref().handle, + start, + end, + constant, + &mut result, + disasm_settings.handle, + raw_view_type, + &mut progress as *mut P as *mut c_void, + Some(P::cb_progress_callback), + ) + }; + FunctionViewType::free_raw(raw_view_type); + + if found { + Some(result) + } else { + None + } + } + + fn find_next_text( + &self, + start: u64, + end: u64, + text: &str, + view_type: FunctionViewType, + ) -> Option<u64> { + // TODO: What are the best "default" settings? + let settings = DisassemblySettings::new(); + self.find_next_text_with_opts( + start, + end, + text, + &settings, + FindFlag::FindCaseInsensitive, + view_type, + NoProgressCallback, + ) + } + + /// # Warning + /// + /// This function is likely to be changed to take in a "query" structure. + fn find_next_text_with_opts<P: ProgressCallback>( + &self, + start: u64, + end: u64, + text: &str, + disasm_settings: &DisassemblySettings, + flag: FindFlag, + view_type: FunctionViewType, + mut progress: P, + ) -> Option<u64> { + let text = text.to_cstr(); + let raw_view_type = FunctionViewType::into_raw(view_type); + let mut result: u64 = 0; + let found = unsafe { + BNFindNextTextWithProgress( + self.as_ref().handle, + start, + end, + text.as_ptr(), + &mut result, + disasm_settings.handle, + flag, + raw_view_type, + &mut progress as *mut P as *mut c_void, + Some(P::cb_progress_callback), + ) + }; + FunctionViewType::free_raw(raw_view_type); + + if found { + Some(result) + } else { + None + } + } + fn notify_data_written(&self, offset: u64, len: usize) { unsafe { BNNotifyDataWritten(self.as_ref().handle, offset, len); diff --git a/rust/src/binary_view/search.rs b/rust/src/binary_view/search.rs new file mode 100644 index 00000000..cd86603a --- /dev/null +++ b/rust/src/binary_view/search.rs @@ -0,0 +1,109 @@ +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct SearchQuery { + /// ex. "42 2e 64 65 ?? 75 67 24" + pattern: String, + #[serde(skip_serializing_if = "Option::is_none")] + start: Option<u64>, + #[serde(skip_serializing_if = "Option::is_none")] + end: Option<u64>, + #[serde(rename = "ignoreCase")] + ignore_case: bool, + raw: bool, + overlap: bool, + #[serde(skip_serializing_if = "Option::is_none")] + align: Option<u64>, +} + +impl SearchQuery { + pub fn new(pattern: impl Into<String>) -> Self { + Self { + pattern: pattern.into(), + ..Default::default() + } + } + + /// Set the starting address for the search + pub fn start(mut self, addr: u64) -> Self { + self.start = Some(addr); + self + } + + /// Set the ending address for the search (inclusive) + pub fn end(mut self, addr: u64) -> Self { + self.end = Some(addr); + self + } + + /// Set whether to interpret the pattern as a raw string + pub fn raw(mut self, raw: bool) -> Self { + self.raw = raw; + self + } + + /// Set whether to perform case-insensitive matching + pub fn ignore_case(mut self, ignore_case: bool) -> Self { + self.ignore_case = ignore_case; + self + } + + /// Set whether to allow matches to overlap + pub fn overlap(mut self, overlap: bool) -> Self { + self.overlap = overlap; + self + } + + /// Set the alignment of matches (must be a power of 2) + pub fn align(mut self, align: u64) -> Self { + // Validate that align is a power of 2 + if align != 0 && (align & (align - 1)) == 0 { + self.align = Some(align); + } + self + } +} + +impl SearchQuery { + /// Serialize the query to a JSON string + pub fn to_json(&self) -> String { + serde_json::to_string(self).expect("failed to serialize search query") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_search_query_builder() { + let query = SearchQuery::new("test pattern") + .start(0x1000) + .end(0x2000) + .raw(true) + .ignore_case(true) + .overlap(false) + .align(16); + + assert_eq!(query.pattern, "test pattern"); + assert_eq!(query.start, Some(0x1000)); + assert_eq!(query.end, Some(0x2000)); + assert!(query.raw); + assert!(query.ignore_case); + assert!(!query.overlap); + assert_eq!(query.align, Some(16)); + } + + #[test] + fn test_search_query_json() { + let query = SearchQuery::new("test").start(0x1000).align(8); + + let json = query.to_json().unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); + + assert_eq!(parsed["pattern"], "test"); + assert_eq!(parsed["start"], 4096); + assert_eq!(parsed["align"], 8); + assert!(!parsed.as_object().unwrap().contains_key("end")); + } +} diff --git a/rust/src/data_buffer.rs b/rust/src/data_buffer.rs index a3f51d54..5f59393e 100644 --- a/rust/src/data_buffer.rs +++ b/rust/src/data_buffer.rs @@ -32,6 +32,12 @@ impl DataBuffer { self.0 } + pub fn new(data: &[u8]) -> Self { + let buffer = unsafe { BNCreateDataBuffer(data.as_ptr() as *const c_void, data.len()) }; + assert!(!buffer.is_null()); + DataBuffer::from_raw(buffer) + } + pub fn get_data(&self) -> &[u8] { let buffer = unsafe { BNGetDataBufferContents(self.0) }; if buffer.is_null() { @@ -161,15 +167,6 @@ impl DataBuffer { pub fn is_empty(&self) -> bool { self.len() == 0 } - - pub fn new(data: &[u8]) -> Result<Self, ()> { - let buffer = unsafe { BNCreateDataBuffer(data.as_ptr() as *const c_void, data.len()) }; - if buffer.is_null() { - Err(()) - } else { - Ok(DataBuffer::from_raw(buffer)) - } - } } impl Default for DataBuffer { @@ -192,10 +189,8 @@ impl Clone for DataBuffer { } } -impl TryFrom<&[u8]> for DataBuffer { - type Error = (); - - fn try_from(value: &[u8]) -> Result<Self, Self::Error> { +impl From<&[u8]> for DataBuffer { + fn from(value: &[u8]) -> Self { DataBuffer::new(value) } } diff --git a/rust/tests/binary_view.rs b/rust/tests/binary_view.rs index 5e8206e0..c3dfc1b8 100644 --- a/rust/tests/binary_view.rs +++ b/rust/tests/binary_view.rs @@ -1,11 +1,13 @@ +use binaryninja::binary_view::search::SearchQuery; use binaryninja::binary_view::{AnalysisState, BinaryViewBase, BinaryViewExt}; -use binaryninja::function::Function; +use binaryninja::data_buffer::DataBuffer; +use binaryninja::function::{Function, FunctionViewType}; use binaryninja::headless::Session; use binaryninja::main_thread::execute_on_main_thread_and_wait; use binaryninja::platform::Platform; use binaryninja::rc::Ref; use binaryninja::symbol::{Symbol, SymbolBuilder, SymbolType}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashSet}; use std::path::PathBuf; #[test] @@ -100,6 +102,48 @@ fn test_binary_view_strings() { } #[test] +fn test_binary_view_search() { + let _session = Session::new().expect("Failed to initialize session"); + let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap(); + let view = binaryninja::load(out_dir.join("atox.obj")).expect("Failed to create view"); + let image_base = view.original_image_base(); + + // Test text search. + let txt_1580 = view + .find_next_text(0, view.end(), "minkernel", FunctionViewType::MediumLevelIL) + .expect("Failed to find text 'minkernel'"); + assert_eq!(txt_1580, image_base + 0x1580); + + // Test data search. + // 65 5c 6d 69 6e 6b 65 72 6e 65 6c (prepend bytes + minkernel) + let data = DataBuffer::new(&[ + 0x65, 0x5c, 0x6d, 0x69, 0x6e, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, + ]); + let data_1580 = view + .find_next_data(0, view.end(), &data) + .expect("Failed to find data"); + assert_eq!(data_1580, image_base + 0x1580); + + // Test constant search. + let constant = 0x80000000; + let const_2607b = view + .find_next_constant(0, view.end(), constant, FunctionViewType::MediumLevelIL) + .expect("Failed to find constant"); + assert_eq!(const_2607b, image_base + 0x2607b); + + // Test binary search. + let query = SearchQuery::new("42 2e 64 65 ?? 75 67 24"); + let mut found: HashSet<u64> = HashSet::new(); + let found_any = view.search(&query, |offset, _data| { + found.insert(offset); + true + }); + assert!(found_any); + assert_eq!(found.len(), 1); + assert_eq!(found.contains(&(&image_base + 0x63)), true); +} + +#[test] fn test_binary_tags() { let _session = Session::new().expect("Failed to initialize session"); let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap(); |
