summaryrefslogtreecommitdiff
path: root/plugins/warp/src/plugin
diff options
context:
space:
mode:
authorMason Reed <mason@vector35.com>2025-01-31 12:59:42 -0500
committerMason Reed <mason@vector35.com>2025-07-02 01:58:31 -0400
commit110c06851bbbd09f78a3e87979d529d6e09df851 (patch)
tree7849015b26a14cd2b7be2d87fc1e0d5c101ef457 /plugins/warp/src/plugin
parent7b1e8bbdb971aed21b6d889aa4a46f9ef54829c1 (diff)
WARP 1.0
- Added FFI - Added a sidebar to the UI - Added project, directory and archive processing - Added generic `Container` interface for extensible stores of WARP data - Fixed type references being constructed and pulled incorrectly - Added HTML, Markdown and JSON report generation - Made the WARP information added as an analysis activity - Flattened the signatures directory, the target information is stored in the file now - Matched function information is stored as function metadata in the database to reliably persist, alongside the function GUID - Split the matching out from the application, allowing you to match on a given function without applying it - Added more/better tests - Added support for binaries with multiple architectures, the functions are now also queried based off the Target, see WARP spec for more details - Greatly improved support for RISC architectures, see WARP spec for more details - Greatly improved UX when loading files after the fact, will now sanely rerun the matcher - Omitted the function type if not a user type, this greatly reduces file size - Improved support for functions that reference a page aligned base pointer, see WARP spec for more details - Removed some extra cache structures that were causing erroneous behavior - Fixed edge-case in LLIL traversal missing some constant pointers, this was a bug in the Rust bindings - Added support for function comments - Made long running tasks, such as generating, matching and loading signatures, cancellable where possible - Made function constraints more versatile, allowing for easy extensions in the future, see WARP spec for details - Added options to signature generation, such as what data to store, and whether to compress the data or not - Made all long running tasks prompt the user for required information before the task starts, allowing users to "set it and forget it" and not have to baby sit the finalization of the task - Myriad of other changes to the actual WARP format that impact performance, file size and general feature set, see https://github.com/Vector35/warp for more details
Diffstat (limited to 'plugins/warp/src/plugin')
-rw-r--r--plugins/warp/src/plugin/add.rs72
-rw-r--r--plugins/warp/src/plugin/copy.rs29
-rw-r--r--plugins/warp/src/plugin/create.rs284
-rw-r--r--plugins/warp/src/plugin/debug.rs48
-rw-r--r--plugins/warp/src/plugin/ffi.rs205
-rw-r--r--plugins/warp/src/plugin/ffi/container.rs412
-rw-r--r--plugins/warp/src/plugin/ffi/function.rs228
-rw-r--r--plugins/warp/src/plugin/file.rs41
-rw-r--r--plugins/warp/src/plugin/find.rs57
-rw-r--r--plugins/warp/src/plugin/function.rs133
-rw-r--r--plugins/warp/src/plugin/load.rs182
-rw-r--r--plugins/warp/src/plugin/project.rs132
-rw-r--r--plugins/warp/src/plugin/render_layer.rs100
-rw-r--r--plugins/warp/src/plugin/settings.rs129
-rw-r--r--plugins/warp/src/plugin/types.rs57
-rw-r--r--plugins/warp/src/plugin/workflow.rs188
16 files changed, 1910 insertions, 387 deletions
diff --git a/plugins/warp/src/plugin/add.rs b/plugins/warp/src/plugin/add.rs
deleted file mode 100644
index 2f76c8b5..00000000
--- a/plugins/warp/src/plugin/add.rs
+++ /dev/null
@@ -1,72 +0,0 @@
-use crate::cache::{cached_function, cached_type_references};
-use crate::matcher::invalidate_function_matcher_cache;
-use crate::user_signature_dir;
-use binaryninja::binary_view::BinaryView;
-use binaryninja::command::FunctionCommand;
-use binaryninja::function::Function;
-use std::thread;
-
-pub struct AddFunctionSignature;
-
-impl FunctionCommand for AddFunctionSignature {
- fn action(&self, view: &BinaryView, func: &Function) {
- let func_plat_name = func.platform().name().to_string();
- let signature_dir = user_signature_dir().join(func_plat_name);
- let view = view.to_owned();
- let func = func.to_owned();
- thread::spawn(move || {
- let Ok(llil) = func.low_level_il() else {
- log::error!("Could not get low level IL for function.");
- return;
- };
-
- // NOTE: Because we only can consume signatures from a specific directory, we don't need to use the interaction API.
- // If we did need to save signature files to a project than this would need to change.
- let Some(save_file) = rfd::FileDialog::new()
- .add_filter("Signature Files", &["sbin"])
- .set_file_name("user.sbin")
- .set_directory(signature_dir)
- .save_file()
- else {
- return;
- };
-
- let mut data = warp::signature::Data::default();
- if let Ok(file_bytes) = std::fs::read(&save_file) {
- // If the file we are adding the function to already has data we should preserve it!
- log::info!("Signature file already exists, preserving data...");
- let Some(file_data) = warp::signature::Data::from_bytes(&file_bytes) else {
- log::error!("Could not get data from signature file: {:?}", save_file);
- return;
- };
- data = file_data;
- };
-
- // Now add our function to the data.
- data.functions.push(cached_function(&func, &llil));
-
- if let Some(ref_ty_cache) = cached_type_references(&view) {
- let referenced_types = ref_ty_cache
- .cache
- .iter()
- .filter_map(|t| t.to_owned())
- .collect::<Vec<_>>();
-
- data.types.extend(referenced_types);
- }
-
- match std::fs::write(&save_file, data.to_bytes()) {
- Ok(_) => {
- log::info!("Signature file saved successfully.");
- // Force rebuild platform matcher.
- invalidate_function_matcher_cache();
- }
- Err(e) => log::error!("Failed to write data to signature file: {:?}", e),
- }
- });
- }
-
- fn valid(&self, _view: &BinaryView, _func: &Function) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/plugin/copy.rs b/plugins/warp/src/plugin/copy.rs
deleted file mode 100644
index b9b985fc..00000000
--- a/plugins/warp/src/plugin/copy.rs
+++ /dev/null
@@ -1,29 +0,0 @@
-use binaryninja::binary_view::BinaryView;
-use binaryninja::command::FunctionCommand;
-use binaryninja::function::Function;
-
-use crate::cache::cached_function_guid;
-
-pub struct CopyFunctionGUID;
-
-impl FunctionCommand for CopyFunctionGUID {
- fn action(&self, _view: &BinaryView, func: &Function) {
- let Ok(llil) = func.low_level_il() else {
- log::error!("Could not get low level il for copied function");
- return;
- };
- let guid = cached_function_guid(func, &llil);
- log::info!(
- "Function GUID for {:?}... {}",
- func.symbol().short_name(),
- guid
- );
- if let Ok(mut clipboard) = arboard::Clipboard::new() {
- let _ = clipboard.set_text(guid.to_string());
- }
- }
-
- fn valid(&self, _view: &BinaryView, _func: &Function) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/plugin/create.rs b/plugins/warp/src/plugin/create.rs
index 1dd83e6e..9ebcba2d 100644
--- a/plugins/warp/src/plugin/create.rs
+++ b/plugins/warp/src/plugin/create.rs
@@ -1,94 +1,232 @@
-use crate::cache::{cached_function, cached_type_references};
-use crate::matcher::invalidate_function_matcher_cache;
-use crate::user_signature_dir;
+use crate::processor::{
+ new_processing_state_background_thread, CompressionTypeField, FileDataKindField,
+ IncludedFunctionsField, SaveReportToDiskField, WarpFileProcessor,
+};
+use crate::report::{ReportGenerator, ReportKindField};
+use crate::{user_signature_dir, INCLUDE_TAG_NAME};
+use binaryninja::background_task::BackgroundTask;
use binaryninja::binary_view::{BinaryView, BinaryViewExt};
use binaryninja::command::Command;
-use binaryninja::function::Function;
-use binaryninja::rc::Guard;
-use rayon::prelude::*;
-use std::sync::atomic::AtomicUsize;
-use std::sync::atomic::Ordering::Relaxed;
+use binaryninja::interaction::form::{Form, FormInputField};
+use binaryninja::interaction::{MessageBoxButtonResult, MessageBoxButtonSet, MessageBoxIcon};
+use binaryninja::rc::Ref;
+use std::path::PathBuf;
use std::thread;
-use std::time::Instant;
+use warp::chunk::Chunk;
+use warp::WarpFile;
-pub struct CreateSignatureFile;
+pub struct SaveFileField;
-// TODO: Prompt the user to add the newly created signature file to the signature blacklist (so that it doesn't keep getting applied)
+impl SaveFileField {
+ pub fn field(view: &BinaryView) -> FormInputField {
+ let default_name = view
+ .file()
+ .filename()
+ .split('/')
+ .last()
+ .unwrap_or("file")
+ .to_string();
+ let signature_dir = user_signature_dir();
+ let default_file_path = signature_dir.join(&default_name).with_extension("warp");
+ FormInputField::SaveFileName {
+ prompt: "File Path".to_string(),
+ // TODO: This is called extension but is really a filter.
+ extension: Some("*.warp".to_string()),
+ default_name: Some(default_name),
+ default: Some(default_file_path.to_string_lossy().to_string()),
+ value: None,
+ }
+ }
-impl Command for CreateSignatureFile {
- fn action(&self, view: &BinaryView) {
- let is_function_named = |f: &Guard<Function>| {
- !f.symbol().short_name().to_string_lossy().contains("sub_") || f.has_user_annotations()
- };
- let mut signature_dir = user_signature_dir();
- if let Some(default_plat) = view.default_platform() {
- // If there is a default platform, put the signature in there.
- // TODO: We should instead use the platform of the function.
- signature_dir.push(default_plat.name().to_string());
+ pub fn from_form(form: &Form) -> Option<PathBuf> {
+ let field = form.get_field_with_name("File Path")?;
+ let field_value = field.try_value_string()?;
+ Some(PathBuf::from(field_value))
+ }
+}
+
+pub struct OpenFileField;
+
+impl OpenFileField {
+ pub fn field() -> FormInputField {
+ FormInputField::OpenFileName {
+ prompt: "Input File Path".to_string(),
+ extension: None,
+ default: None,
+ value: None,
}
- let view = view.to_owned();
- thread::spawn(move || {
- let total_functions = view.functions().len();
- let done_functions = AtomicUsize::default();
- let background_task = binaryninja::background_task::BackgroundTask::new(
- &format!("Generating signatures... ({}/{})", 0, total_functions),
- true,
- );
+ }
+
+ pub fn from_form(form: &Form) -> Option<PathBuf> {
+ let field = form.get_field_with_name("Input File Path")?;
+ let field_value = field.try_value_string()?;
+ Some(PathBuf::from(field_value))
+ }
+}
+
+pub struct CreateFromCurrentView;
- let start = Instant::now();
+impl CreateFromCurrentView {
+ pub fn execute(view: Ref<BinaryView>, external_file: bool) -> Option<()> {
+ // Prompt the user first so that they can go do other things and not worry about a popup.
+ let mut form = Form::new("Create From View");
- let mut data = warp::signature::Data::default();
- data.functions.par_extend(
- view.functions()
- .par_iter()
- .inspect(|_| {
- done_functions.fetch_add(1, Relaxed);
- background_task.set_progress_text(&format!(
- "Generating signatures... ({}/{})",
- done_functions.load(Relaxed),
- total_functions
- ))
- })
- .filter(is_function_named)
- .filter(|f| !f.analysis_skipped())
- .filter_map(|func| {
- let llil = func.low_level_il().ok()?;
- Some(cached_function(&func, &llil))
- }),
+ if external_file {
+ form.add_field(OpenFileField::field());
+ }
+
+ form.add_field(SaveFileField::field(&view));
+
+ let fd_field = FileDataKindField::default();
+ form.add_field(fd_field.to_field());
+
+ let compression_field = CompressionTypeField::default();
+ form.add_field(compression_field.to_field());
+
+ let mut included_field = IncludedFunctionsField::default();
+ // If the view has the include tag, we better set the default to the selected functions.
+ if view.tag_type_by_name(INCLUDE_TAG_NAME).is_some() {
+ included_field = IncludedFunctionsField::Selected;
+ }
+ form.add_field(included_field.to_field());
+
+ let report_field = ReportKindField::default();
+ form.add_field(report_field.to_field());
+ let report_to_disk_field = SaveReportToDiskField::default();
+ form.add_field(report_to_disk_field.to_field());
+
+ if !form.prompt() {
+ return None;
+ }
+ let compression_type = CompressionTypeField::from_form(&form).unwrap_or_default();
+ let file_path = SaveFileField::from_form(&form)?;
+ let file_data_kind = FileDataKindField::from_form(&form).unwrap_or_default();
+ let file_included_functions = IncludedFunctionsField::from_form(&form).unwrap_or_default();
+ let report_kind = ReportKindField::from_form(&form).unwrap_or_default();
+ let save_report_to_disk = SaveReportToDiskField::from_form(&form).unwrap_or_default();
+ let open_file_path = OpenFileField::from_form(&form);
+
+ // If we already have a file, prompt the user if they want to add the data.
+ let mut existing_chunks = Vec::new();
+ if file_path.exists() {
+ let prompt_result = binaryninja::interaction::show_message_box(
+ "Keep existing file data?",
+ "The file already exists. Do you want to keep the existing data?",
+ MessageBoxButtonSet::YesNoCancelButtonSet,
+ MessageBoxIcon::QuestionIcon,
);
- if let Some(ref_ty_cache) = cached_type_references(&view) {
- let referenced_types = ref_ty_cache
- .cache
- .iter()
- .filter_map(|t| t.to_owned())
- .collect::<Vec<_>>();
+ match prompt_result {
+ MessageBoxButtonResult::NoButton => {
+ // User wants to overwrite the file.
+ }
+ MessageBoxButtonResult::YesButton | MessageBoxButtonResult::OKButton => {
+ // User wants to keep the existing data.
+ let data = std::fs::read(&file_path).ok()?;
+ let existing_file = WarpFile::from_owned_bytes(data)?;
+ existing_chunks.extend(existing_file.chunks);
+ }
+ MessageBoxButtonResult::CancelButton => {
+ log::info!(
+ "User cancelled signature file creation, no operations were performed."
+ );
+ return None;
+ }
+ }
+ }
+
+ let processor = WarpFileProcessor::new()
+ .with_compression_type(compression_type)
+ .with_file_data(file_data_kind)
+ .with_included_functions(file_included_functions);
- data.types.extend(referenced_types);
+ let file = match open_file_path {
+ None => {
+ // We are processing the current view. NOT an external file.
+ // Reference path is just used for the state tracking. Does not need to be readable.
+ let reference_path = file_path.clone();
+ processor.process_view(reference_path, &view)
+ }
+ Some(open_file_path) => {
+ // This thread will show the state in a background task.
+ let background_task = BackgroundTask::new("Processing started...", true);
+ new_processing_state_background_thread(background_task.clone(), processor.state());
+ let file = processor.process(open_file_path);
+ background_task.finish();
+ file
}
+ };
+
+ if let Err(err) = file {
+ binaryninja::interaction::show_message_box(
+ "Error",
+ &format!("Failed to create signature file: {}", err),
+ MessageBoxButtonSet::OKButtonSet,
+ MessageBoxIcon::ErrorIcon,
+ );
+ log::error!("Failed to create signature file: {}", err);
+ return None;
+ }
- log::info!("Signature generation took {:?}", start.elapsed());
- background_task.finish();
+ let mut file = file.unwrap();
+ // Add back the existing chunks if the user selected to keep them.
+ file.chunks.extend(existing_chunks);
+ // TODO: Make merging optional?
+ file.chunks = Chunk::merge(&file.chunks, compression_type.into());
+
+ if std::fs::write(&file_path, file.to_bytes()).is_err() {
+ log::error!("Failed to write data to signature file!");
+ }
- // NOTE: Because we only can consume signatures from a specific directory, we don't need to use the interaction API.
- // If we did need to save signature files to a project than this would need to change.
- let Some(save_file) = rfd::FileDialog::new()
- .add_filter("Signature Files", &["sbin"])
- .set_file_name(format!("{}.sbin", view.file().filename()))
- .set_directory(signature_dir)
- .save_file()
- else {
- return;
- };
+ // Show a report of the generate signatures, if desired.
+ let report_generator = ReportGenerator::new();
+ if let Some(report_string) = report_generator.report(&report_kind, &file) {
+ if save_report_to_disk == SaveReportToDiskField::Yes {
+ let report_ext = report_generator
+ .report_extension(&report_kind)
+ .unwrap_or_default();
+ let report_path = file_path.with_extension(report_ext);
+ let _ = std::fs::write(report_path, &report_string);
+ }
- match std::fs::write(&save_file, data.to_bytes()) {
- Ok(_) => {
- log::info!("Signature file saved successfully.");
- // Force rebuild platform matcher.
- invalidate_function_matcher_cache();
+ match report_kind {
+ ReportKindField::None => {}
+ ReportKindField::Html => {
+ view.show_html_report("Generated WARP File", report_string.as_str(), "");
+ }
+ ReportKindField::Markdown => {
+ view.show_markdown_report("Generated WARP File", report_string.as_str(), "");
+ }
+ ReportKindField::Json => {
+ view.show_plaintext_report("Generated WARP File", report_string.as_str());
}
- Err(e) => log::error!("Failed to write data to signature file: {:?}", e),
}
+ }
+
+ Some(())
+ }
+}
+
+impl Command for CreateFromCurrentView {
+ fn action(&self, view: &BinaryView) {
+ let view = view.to_owned();
+ thread::spawn(move || {
+ CreateFromCurrentView::execute(view, false);
+ });
+ }
+
+ fn valid(&self, _view: &BinaryView) -> bool {
+ true
+ }
+}
+
+pub struct CreateFromFiles;
+
+impl Command for CreateFromFiles {
+ fn action(&self, view: &BinaryView) {
+ let view = view.to_owned();
+ thread::spawn(move || {
+ CreateFromCurrentView::execute(view, true);
});
}
diff --git a/plugins/warp/src/plugin/debug.rs b/plugins/warp/src/plugin/debug.rs
new file mode 100644
index 00000000..fc4e5817
--- /dev/null
+++ b/plugins/warp/src/plugin/debug.rs
@@ -0,0 +1,48 @@
+use crate::cache::container::for_cached_containers;
+use crate::{build_function, cache};
+use binaryninja::binary_view::BinaryView;
+use binaryninja::command::{Command, FunctionCommand};
+use binaryninja::function::Function;
+use binaryninja::ObjectDestructor;
+
+pub struct DebugFunction;
+
+impl FunctionCommand for DebugFunction {
+ fn action(&self, _view: &BinaryView, func: &Function) {
+ if let Ok(lifted_il) = func.lifted_il() {
+ log::info!("{:#?}", build_function(func, &lifted_il));
+ }
+ }
+
+ fn valid(&self, _view: &BinaryView, _func: &Function) -> bool {
+ true
+ }
+}
+
+pub struct DebugCache;
+
+impl Command for DebugCache {
+ fn action(&self, _view: &BinaryView) {
+ for_cached_containers(|c| {
+ log::info!("Container: {:#?}", c);
+ });
+ }
+
+ fn valid(&self, _view: &BinaryView) -> bool {
+ true
+ }
+}
+
+pub struct DebugInvalidateCache;
+
+impl Command for DebugInvalidateCache {
+ fn action(&self, view: &BinaryView) {
+ let destructor = cache::CacheDestructor {};
+ destructor.destruct_view(view);
+ log::info!("Invalidated all WARP caches...");
+ }
+
+ fn valid(&self, _view: &BinaryView) -> bool {
+ true
+ }
+}
diff --git a/plugins/warp/src/plugin/ffi.rs b/plugins/warp/src/plugin/ffi.rs
new file mode 100644
index 00000000..d78ca3c5
--- /dev/null
+++ b/plugins/warp/src/plugin/ffi.rs
@@ -0,0 +1,205 @@
+mod container;
+mod function;
+
+use binaryninjacore_sys::{
+ BNBasicBlock, BNBinaryView, BNFunction, BNLowLevelILFunction, BNPlatform,
+};
+use std::ffi::c_char;
+use std::sync::{Arc, RwLock};
+use uuid::Uuid;
+
+use binaryninja::basic_block::{BasicBlock, BasicBlockType};
+use binaryninja::function::{Function, NativeBlock};
+
+use crate::cache::cached_function_guid;
+use crate::container::{Container, SourceId};
+use crate::convert::platform_to_target;
+use crate::plugin::workflow::run_matcher;
+use crate::{
+ basic_block_guid, is_blacklisted_instruction, is_computed_variant_instruction,
+ is_variant_instruction, relocatable_regions,
+};
+use binaryninja::binary_view::BinaryView;
+use binaryninja::low_level_il::function::{LowLevelILFunction, Mutable, NonSSA};
+use binaryninja::low_level_il::instruction::LowLevelInstructionIndex;
+use binaryninja::platform::Platform;
+use binaryninja::string::BnString;
+use warp::r#type::guid::TypeGUID;
+use warp::signature::basic_block::BasicBlockGUID;
+use warp::signature::constraint::{Constraint, ConstraintGUID, UNRELATED_OFFSET};
+use warp::signature::function::FunctionGUID;
+
+/// [`SourceId`] is marked transparent to the underlying `[u8; 16]`, safe to use directly in FFI.
+pub type BNWARPSource = SourceId;
+
+/// [`BasicBlockGUID`] is marked transparent to the underlying `[u8; 16]`, safe to use directly in FFI.
+pub type BNWARPBasicBlockGUID = BasicBlockGUID;
+
+/// [`ConstraintGUID`] is marked transparent to the underlying `[u8; 16]`, safe to use directly in FFI.
+pub type BNWARPConstraintGUID = ConstraintGUID;
+
+/// [`FunctionGUID`] is marked transparent to the underlying `[u8; 16]`, safe to use directly in FFI.
+pub type BNWARPFunctionGUID = FunctionGUID;
+
+/// [`TypeGUID`] is marked transparent to the underlying `[u8; 16]`, safe to use directly in FFI.
+pub type BNWARPTypeGUID = TypeGUID;
+
+pub type BNWARPTarget = warp::target::Target;
+pub type BNWARPFunction = warp::signature::function::Function;
+pub type BNWARPContainer = RwLock<Box<dyn Container>>;
+
+// TODO: Some sort of callback for loading functions
+// TODO: Be able to run matcher for a specific file
+// TODO: Generate signatures for a file, return what?
+
+#[repr(C)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+pub struct BNWARPConstraint {
+ guid: BNWARPConstraintGUID,
+ offset: i64,
+}
+
+impl From<BNWARPConstraint> for Constraint {
+ fn from(constraint: BNWARPConstraint) -> Self {
+ Constraint {
+ guid: constraint.guid,
+ offset: match constraint.offset {
+ UNRELATED_OFFSET => None,
+ _ => Some(constraint.offset),
+ },
+ }
+ }
+}
+
+impl From<Constraint> for BNWARPConstraint {
+ fn from(constraint: Constraint) -> Self {
+ BNWARPConstraint {
+ guid: constraint.guid,
+ offset: constraint.offset.unwrap_or(UNRELATED_OFFSET),
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPUUIDGetString(uuid: *const Uuid) -> *mut c_char {
+ let uuid_str = (*uuid).to_string();
+ // NOTE: Leak the uuid string to be freed by BNFreeString
+ BnString::into_raw(uuid_str.into())
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPUUIDEqual(a: *const Uuid, b: *const Uuid) -> bool {
+ (*a) == (*b)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPRunMatcher(view: *mut BNBinaryView) {
+ let view = BinaryView::from_raw(view);
+ run_matcher(&view)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPGetBasicBlockGUID(
+ basic_block: *mut BNBasicBlock,
+ result: *mut BNWARPBasicBlockGUID,
+) -> bool {
+ let basic_block = unsafe { BasicBlock::from_raw(basic_block, NativeBlock::new()) };
+ if basic_block.block_type() != BasicBlockType::Native {
+ return false;
+ }
+ let function = basic_block.function();
+ match function.lifted_il() {
+ Ok(lifted_il) => {
+ let relocatable_regions = relocatable_regions(&function.view());
+ *result = basic_block_guid(&relocatable_regions, &basic_block, &lifted_il);
+ true
+ }
+ Err(_) => false,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPGetAnalysisFunctionGUID(
+ analysis_function: *mut BNFunction,
+ result: *mut BNWARPFunctionGUID,
+) -> bool {
+ let function = unsafe { Function::from_raw(analysis_function) };
+ match function.lifted_il() {
+ Ok(lifted_il) => {
+ *result = cached_function_guid(&function, &lifted_il);
+ true
+ }
+ Err(_) => false,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPIsLiftedInstructionVariant(
+ analysis_function: *mut BNLowLevelILFunction,
+ index: LowLevelInstructionIndex,
+) -> bool {
+ let lifted_il: LowLevelILFunction<Mutable, NonSSA> =
+ unsafe { LowLevelILFunction::from_raw(analysis_function) };
+ match lifted_il.instruction_from_index(index) {
+ Some(instr) => {
+ let relocatable_regions = relocatable_regions(&lifted_il.function().view());
+ is_variant_instruction(&relocatable_regions, &instr)
+ }
+ None => false,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPIsLowLevelInstructionComputedVariant(
+ analysis_function: *mut BNLowLevelILFunction,
+ index: LowLevelInstructionIndex,
+) -> bool {
+ let llil: LowLevelILFunction<Mutable, NonSSA> =
+ unsafe { LowLevelILFunction::from_raw(analysis_function) };
+ match llil.instruction_from_index(index) {
+ Some(instr) => {
+ let relocatable_regions = relocatable_regions(&llil.function().view());
+ is_computed_variant_instruction(&relocatable_regions, &instr)
+ }
+ None => false,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPIsLiftedInstructionBlacklisted(
+ analysis_function: *mut BNLowLevelILFunction,
+ index: LowLevelInstructionIndex,
+) -> bool {
+ let lifted_il: LowLevelILFunction<Mutable, NonSSA> =
+ unsafe { LowLevelILFunction::from_raw(analysis_function) };
+ match lifted_il.instruction_from_index(index) {
+ Some(instr) => is_blacklisted_instruction(&instr),
+ None => false,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeUUIDList(uuids: *mut Uuid, count: usize) {
+ let sources_ptr = std::ptr::slice_from_raw_parts_mut(uuids, count);
+ let _ = unsafe { Box::from_raw(sources_ptr) };
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPGetTarget(platform: *mut BNPlatform) -> *mut BNWARPTarget {
+ let platform = Platform::from_raw(platform);
+ Arc::into_raw(Arc::new(platform_to_target(&platform))) as *mut BNWARPTarget
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewTargetReference(target: *mut BNWARPTarget) -> *mut BNWARPTarget {
+ Arc::increment_strong_count(target);
+ target
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeTargetReference(target: *mut BNWARPTarget) {
+ if target.is_null() {
+ return;
+ }
+ Arc::decrement_strong_count(target);
+}
diff --git a/plugins/warp/src/plugin/ffi/container.rs b/plugins/warp/src/plugin/ffi/container.rs
new file mode 100644
index 00000000..21ad4dc0
--- /dev/null
+++ b/plugins/warp/src/plugin/ffi/container.rs
@@ -0,0 +1,412 @@
+use crate::cache::container::cached_containers;
+use crate::container::SourcePath;
+use crate::convert::{from_bn_type, to_bn_type};
+use crate::plugin::ffi::{
+ BNWARPContainer, BNWARPFunction, BNWARPFunctionGUID, BNWARPSource, BNWARPTarget, BNWARPTypeGUID,
+};
+use binaryninja::architecture::CoreArchitecture;
+use binaryninja::binary_view::BinaryView;
+use binaryninja::rc::Ref;
+use binaryninja::string::BnString;
+use binaryninja::types::Type;
+use binaryninjacore_sys::{BNArchitecture, BNBinaryView, BNType};
+use std::ffi::{c_char, CStr};
+use std::mem::ManuallyDrop;
+use std::sync::Arc;
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPGetContainers(count: *mut usize) -> *mut *mut BNWARPContainer {
+ // NOTE: Leak the arc pointers to be freed by BNWARPFreeContainerList
+ let boxed_raw_containers: Box<[_]> =
+ cached_containers().into_iter().map(Arc::into_raw).collect();
+ *count = boxed_raw_containers.len();
+ let leaked_raw_containers = Box::into_raw(boxed_raw_containers);
+ leaked_raw_containers as *mut *mut BNWARPContainer
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerGetName(container: *mut BNWARPContainer) -> *const c_char {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return std::ptr::null();
+ };
+ let name = container.to_string();
+ // NOTE: Leak the container name to be freed by BNFreeString
+ BnString::into_raw(name.into())
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerGetSources(
+ container: *mut BNWARPContainer,
+ count: *mut usize,
+) -> *mut BNWARPSource {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.write() else {
+ return std::ptr::null_mut();
+ };
+
+ // NOTE: Leak the sources to be freed by BNWARPFreeSourceList
+ let boxed_sources: Box<[_]> = container.sources().unwrap_or_default().into_boxed_slice();
+ *count = boxed_sources.len();
+ Box::into_raw(boxed_sources) as *mut BNWARPSource
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerAddSource(
+ container: *mut BNWARPContainer,
+ source_path: *const c_char,
+ result: *mut BNWARPSource,
+) -> bool {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(mut container) = arc_container.write() else {
+ return false;
+ };
+
+ let source_path_cstr = unsafe { CStr::from_ptr(source_path) };
+ let source_path_str = source_path_cstr.to_str().unwrap();
+ let source_path = SourcePath::new_with_str(source_path_str);
+
+ match container.add_source(source_path) {
+ Ok(source) => {
+ // NOTE: Leak the source to be freed by BNFreeString
+ *result = source;
+ true
+ }
+ Err(_) => false,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerCommitSource(
+ container: *mut BNWARPContainer,
+ source: *const BNWARPSource,
+) -> bool {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(mut container) = arc_container.write() else {
+ return false;
+ };
+
+ let source = unsafe { *source };
+
+ container
+ .commit_source(&source)
+ .is_ok_and(|committed| committed)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerIsSourceUncommitted(
+ container: *mut BNWARPContainer,
+ source: *const BNWARPSource,
+) -> bool {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return false;
+ };
+
+ let source = unsafe { *source };
+
+ container
+ .is_source_uncommitted(&source)
+ .is_ok_and(|uncommitted| uncommitted)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerIsSourceWritable(
+ container: *mut BNWARPContainer,
+ source: *const BNWARPSource,
+) -> bool {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return false;
+ };
+
+ let source = unsafe { *source };
+
+ container
+ .is_source_writable(&source)
+ .is_ok_and(|writable| writable)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerGetSourcePath(
+ container: *mut BNWARPContainer,
+ source: *const BNWARPSource,
+) -> *const c_char {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return std::ptr::null();
+ };
+
+ let source = unsafe { *source };
+
+ match container.source_path(&source) {
+ Ok(path) => {
+ let path = path.to_string();
+ // NOTE: Leak the source path to be freed by BNFreeString
+ BnString::into_raw(path.into())
+ }
+ Err(_) => std::ptr::null(),
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerAddFunctions(
+ container: *mut BNWARPContainer,
+ target: *mut BNWARPTarget,
+ source: *const BNWARPSource,
+ functions: *mut *mut BNWARPFunction,
+ count: usize,
+) -> bool {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(mut container) = arc_container.write() else {
+ return false;
+ };
+
+ let target = unsafe { ManuallyDrop::new(Arc::from_raw(target)) };
+
+ let source = unsafe { *source };
+
+ let functions_ptr = std::slice::from_raw_parts(functions, count);
+ // TODO: We have to clone the objects here to make the type checker happy.
+ // TODO: See about avoiding this later.
+ let functions: Vec<_> = functions_ptr
+ .iter()
+ .map(|&f| unsafe { ManuallyDrop::new(Arc::from_raw(f)).as_ref().clone() })
+ .collect();
+ container
+ .add_functions(&target, &source, &functions)
+ .is_ok()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerAddTypes(
+ view: *mut BNBinaryView,
+ container: *mut BNWARPContainer,
+ source: *const BNWARPSource,
+ types: *mut *mut BNType,
+ count: usize,
+) -> bool {
+ let view = unsafe { BinaryView::from_raw(view) };
+
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(mut container) = arc_container.write() else {
+ return false;
+ };
+
+ let source = unsafe { *source };
+
+ let types_ptr = std::slice::from_raw_parts(types, count);
+ let types: Vec<_> = types_ptr
+ .iter()
+ .map(|&t| Type::from_raw(t))
+ .map(|ty| from_bn_type(&view, &ty, 255))
+ .collect();
+ container.add_types(&source, &types).is_ok()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerRemoveFunctions(
+ container: *mut BNWARPContainer,
+ target: *mut BNWARPTarget,
+ source: *const BNWARPSource,
+ functions: *mut *mut BNWARPFunction,
+ count: usize,
+) -> bool {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(mut container) = arc_container.write() else {
+ return false;
+ };
+
+ let target = unsafe { ManuallyDrop::new(Arc::from_raw(target)) };
+
+ let source = unsafe { *source };
+
+ let functions_ptr = std::slice::from_raw_parts(functions, count);
+ // TODO: We have to clone the objects here to make the type checker happy.
+ // TODO: See about avoiding this later.
+ let functions: Vec<_> = functions_ptr
+ .iter()
+ .map(|&f| unsafe { ManuallyDrop::new(Arc::from_raw(f)).as_ref().clone() })
+ .collect();
+ container
+ .remove_functions(&target, &source, &functions)
+ .is_ok()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerRemoveTypes(
+ container: *mut BNWARPContainer,
+ source: *const BNWARPSource,
+ guids: *mut BNWARPTypeGUID,
+ count: usize,
+) -> bool {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(mut container) = arc_container.write() else {
+ return false;
+ };
+
+ let source = unsafe { *source };
+
+ let guids = std::slice::from_raw_parts(guids, count);
+ container.remove_types(&source, &guids).is_ok()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerGetSourcesWithFunctionGUID(
+ container: *mut BNWARPContainer,
+ target: *mut BNWARPTarget,
+ guid: *const BNWARPFunctionGUID,
+ count: *mut usize,
+) -> *mut BNWARPSource {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return std::ptr::null_mut();
+ };
+
+ let target = unsafe { ManuallyDrop::new(Arc::from_raw(target)) };
+
+ let guid = unsafe { *guid };
+
+ // NOTE: Leak the sources to be freed by BNWARPFreeSourceList
+ let boxed_sources: Box<[_]> = container
+ .sources_with_function_guid(&target, &guid)
+ .unwrap_or_default()
+ .into_boxed_slice();
+ *count = boxed_sources.len();
+ Box::into_raw(boxed_sources) as *mut BNWARPSource
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerGetSourcesWithTypeGUID(
+ container: *mut BNWARPContainer,
+ guid: *const BNWARPTypeGUID,
+ count: *mut usize,
+) -> *mut BNWARPSource {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return std::ptr::null_mut();
+ };
+
+ let guid = unsafe { *guid };
+
+ // NOTE: Leak the sources to be freed by BNWARPFreeSourceList
+ let boxed_sources: Box<[_]> = container
+ .sources_with_type_guid(&guid)
+ .unwrap_or_default()
+ .into_boxed_slice();
+ *count = boxed_sources.len();
+ Box::into_raw(boxed_sources) as *mut BNWARPSource
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerGetFunctionsWithGUID(
+ container: *mut BNWARPContainer,
+ target: *mut BNWARPTarget,
+ source: *const BNWARPSource,
+ guid: *const BNWARPFunctionGUID,
+ count: *mut usize,
+) -> *mut *mut BNWARPFunction {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return std::ptr::null_mut();
+ };
+
+ let source = unsafe { *source };
+
+ let target = unsafe { ManuallyDrop::new(Arc::from_raw(target)) };
+
+ let guid = unsafe { *guid };
+
+ // NOTE: Leak the functions to be freed by BNWARPFreeFunctionList
+ let raw_boxed_functions: Box<[_]> = container
+ .functions_with_guid(&target, &source, &guid)
+ .unwrap_or_default()
+ .into_iter()
+ .map(Arc::new)
+ .map(Arc::into_raw)
+ .collect();
+ *count = raw_boxed_functions.len();
+ Box::into_raw(raw_boxed_functions) as *mut *mut BNWARPFunction
+}
+
+// TODO: Swap arch to Target?
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerGetTypeWithGUID(
+ arch: *mut BNArchitecture,
+ container: *mut BNWARPContainer,
+ source: *const BNWARPSource,
+ guid: *const BNWARPTypeGUID,
+) -> *mut BNType {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return std::ptr::null_mut();
+ };
+
+ // NOTE: to convert the type, we must have an architecture.
+ let arch = CoreArchitecture::from_raw(arch);
+
+ let source = unsafe { *source };
+
+ let guid = unsafe { *guid };
+
+ let Some(ty) = container.type_with_guid(&source, &guid).unwrap_or_default() else {
+ return std::ptr::null_mut();
+ };
+ let function_type = to_bn_type(&arch, &ty);
+ // NOTE: The type ref has been pre-incremented for the caller.
+ unsafe { Ref::into_raw(function_type) }.handle
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPContainerGetTypeGUIDsWithName(
+ container: *mut BNWARPContainer,
+ source: *const BNWARPSource,
+ name: *const c_char,
+ count: *mut usize,
+) -> *mut BNWARPTypeGUID {
+ let arc_container = ManuallyDrop::new(Arc::from_raw(container));
+ let Ok(container) = arc_container.read() else {
+ return std::ptr::null_mut();
+ };
+
+ let source = unsafe { *source };
+
+ let name_cstr = unsafe { CStr::from_ptr(name) };
+ let name = name_cstr.to_str().unwrap();
+
+ // NOTE: Leak the guids to be freed by BNWARPFreeTypeGUIDList
+ let boxed_guids = container
+ .type_guids_with_name(&source, name)
+ .unwrap_or_default()
+ .into_boxed_slice();
+ *count = boxed_guids.len();
+ Box::into_raw(boxed_guids) as *mut BNWARPTypeGUID
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewContainerReference(
+ container: *mut BNWARPContainer,
+) -> *mut BNWARPContainer {
+ Arc::increment_strong_count(container);
+ container
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeContainerReference(container: *mut BNWARPContainer) {
+ if container.is_null() {
+ return;
+ }
+ Arc::decrement_strong_count(container);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeContainerList(
+ containers: *mut *mut BNWARPContainer,
+ count: usize,
+) {
+ let containers_ptr = std::ptr::slice_from_raw_parts_mut(containers, count);
+ let containers = unsafe { Box::from_raw(containers_ptr) };
+ for container in containers {
+ BNWARPFreeContainerReference(container);
+ }
+}
diff --git a/plugins/warp/src/plugin/ffi/function.rs b/plugins/warp/src/plugin/ffi/function.rs
new file mode 100644
index 00000000..3db8f307
--- /dev/null
+++ b/plugins/warp/src/plugin/ffi/function.rs
@@ -0,0 +1,228 @@
+use crate::build_function;
+use crate::cache::{insert_cached_function_match, try_cached_function_match};
+use crate::convert::{to_bn_symbol_at_address, to_bn_type};
+use crate::plugin::ffi::{BNWARPConstraint, BNWARPFunction, BNWARPFunctionGUID};
+use binaryninja::function::Function;
+use binaryninja::rc::Ref;
+use binaryninja::string::BnString;
+use binaryninjacore_sys::{BNFunction, BNSymbol, BNType};
+use std::ffi::c_char;
+use std::mem::ManuallyDrop;
+use std::sync::Arc;
+use warp::signature::comment::FunctionComment;
+
+#[repr(C)]
+pub struct BNWarpFunctionComment {
+ pub text: *mut c_char,
+ pub offset: i64,
+}
+
+impl BNWarpFunctionComment {
+ /// Leaks the text string to be freed with BNWARPFreeFunctionComment
+ pub fn from_owned(value: &FunctionComment) -> Self {
+ let text = BnString::into_raw(BnString::new(&value.text));
+ Self {
+ text,
+ offset: value.offset,
+ }
+ }
+
+ pub unsafe fn free_raw(value: &Self) {
+ unsafe { BnString::free_raw(value.text) }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPGetFunction(
+ analysis_function: *mut BNFunction,
+) -> *mut BNWARPFunction {
+ let function = Function::from_raw(analysis_function);
+ let Ok(lifted_il) = function.lifted_il() else {
+ return std::ptr::null_mut();
+ };
+ let function = build_function(&function, &lifted_il);
+ Arc::into_raw(Arc::new(function)) as *mut BNWARPFunction
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPGetMatchedFunction(
+ analysis_function: *mut BNFunction,
+) -> *mut BNWARPFunction {
+ let function = Function::from_raw(analysis_function);
+ match try_cached_function_match(&function) {
+ Some(matched_function) => {
+ let arc_matched_function = Arc::new(matched_function);
+ // NOTE: Freed by BNWARPFreeFunctionReference
+ Arc::into_raw(arc_matched_function) as *mut BNWARPFunction
+ }
+ None => std::ptr::null_mut(),
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFunctionApply(
+ function: *mut BNWARPFunction,
+ analysis_function: *mut BNFunction,
+) {
+ let analysis_function = Function::from_raw(analysis_function);
+ match function.is_null() {
+ false => {
+ // Set the matched function to `function` and return previous.
+ let matched_function = ManuallyDrop::new(Arc::from_raw(function));
+ insert_cached_function_match(
+ &analysis_function,
+ Some(matched_function.as_ref().clone()),
+ )
+ }
+ true => {
+ // We are removing the previous match and returning it.
+ insert_cached_function_match(&analysis_function, None)
+ }
+ };
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFunctionGetGUID(
+ function: *mut BNWARPFunction,
+) -> BNWARPFunctionGUID {
+ // We do not own function so we should not drop.
+ let function = ManuallyDrop::new(Arc::from_raw(function));
+ function.guid
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFunctionGetSymbol(
+ function: *mut BNWARPFunction,
+ analysis_function: *mut BNFunction,
+) -> *mut BNSymbol {
+ let analysis_function = Function::from_raw(analysis_function);
+ // We do not own function so we should not drop.
+ let function = ManuallyDrop::new(Arc::from_raw(function));
+ let view = analysis_function.view();
+ let address = analysis_function.symbol().address();
+ let function_symbol = to_bn_symbol_at_address(&view, &function.symbol, address);
+ // NOTE: The symbol ref has been pre-incremented for the caller.
+ Ref::into_raw(function_symbol).handle
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFunctionGetSymbolName(function: *mut BNWARPFunction) -> *mut c_char {
+ // We do not own function so we should not drop.
+ let function = ManuallyDrop::new(Arc::from_raw(function));
+ let bn_name = BnString::new(&function.symbol.name);
+ // NOTE: The symbol name string to be freed by BNFreeString
+ BnString::into_raw(bn_name)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFunctionGetType(
+ function: *mut BNWARPFunction,
+ analysis_function: *mut BNFunction,
+) -> *mut BNType {
+ let analysis_function = Function::from_raw(analysis_function);
+ // We do not own function so we should not drop.
+ let function = ManuallyDrop::new(Arc::from_raw(function));
+ match &function.ty {
+ Some(func_ty) => {
+ let arch = analysis_function.arch();
+ let function_type = to_bn_type(&arch, func_ty);
+ // NOTE: The type ref has been pre-incremented for the caller.
+ unsafe { Ref::into_raw(function_type) }.handle
+ }
+ None => std::ptr::null_mut(),
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFunctionGetConstraints(
+ function: *mut BNWARPFunction,
+ count: *mut usize,
+) -> *mut BNWARPConstraint {
+ // We do not own function so we should not drop.
+ let function = ManuallyDrop::new(Arc::from_raw(function));
+ let raw_constraints: Box<[BNWARPConstraint]> = function
+ .constraints
+ .clone()
+ .into_iter()
+ .map(Into::into)
+ .collect();
+ *count = raw_constraints.len();
+ let raw_constraints_ptr = Box::into_raw(raw_constraints);
+ raw_constraints_ptr as *mut BNWARPConstraint
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFunctionGetComments(
+ function: *mut BNWARPFunction,
+ count: *mut usize,
+) -> *mut BNWarpFunctionComment {
+ // We do not own function so we should not drop.
+ let function = ManuallyDrop::new(Arc::from_raw(function));
+ let raw_comments: Box<[_]> = function
+ .comments
+ .iter()
+ .map(BNWarpFunctionComment::from_owned)
+ .collect();
+ *count = raw_comments.len();
+ let raw_comments_ptr = Box::into_raw(raw_comments);
+ raw_comments_ptr as *mut BNWarpFunctionComment
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFunctionsEqual(
+ function_a: *mut BNWARPFunction,
+ function_b: *mut BNWARPFunction,
+) -> bool {
+ // We do not own function so we should not drop.
+ let function_a = ManuallyDrop::new(Arc::from_raw(function_a));
+ // We do not own function so we should not drop.
+ let function_b = ManuallyDrop::new(Arc::from_raw(function_b));
+ function_a.eq(&function_b)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPNewFunctionReference(
+ function: *mut BNWARPFunction,
+) -> *mut BNWARPFunction {
+ Arc::increment_strong_count(function);
+ function
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeFunctionReference(function: *mut BNWARPFunction) {
+ if function.is_null() {
+ return;
+ }
+ Arc::decrement_strong_count(function);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeFunctionList(functions: *mut *mut BNWARPFunction, count: usize) {
+ let functions_ptr = std::ptr::slice_from_raw_parts_mut(functions, count);
+ let functions = Box::from_raw(functions_ptr);
+ for function in functions {
+ // NOTE: The functions themselves should also be arc.
+ BNWARPFreeFunctionReference(function);
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeFunctionCommentList(
+ comments: *mut BNWarpFunctionComment,
+ count: usize,
+) {
+ let comments_ptr = std::ptr::slice_from_raw_parts_mut(comments, count);
+ let comments = Box::from_raw(comments_ptr);
+ for comment in &comments {
+ BNWarpFunctionComment::free_raw(comment)
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn BNWARPFreeConstraintList(
+ constraints: *mut BNWARPConstraint,
+ count: usize,
+) {
+ let constraints_ptr = std::ptr::slice_from_raw_parts_mut(constraints, count);
+ let _constraints = unsafe { Box::from_raw(constraints_ptr) };
+}
diff --git a/plugins/warp/src/plugin/file.rs b/plugins/warp/src/plugin/file.rs
new file mode 100644
index 00000000..ec61bd78
--- /dev/null
+++ b/plugins/warp/src/plugin/file.rs
@@ -0,0 +1,41 @@
+use crate::report::ReportGenerator;
+use binaryninja::binary_view::{BinaryView, BinaryViewExt};
+use binaryninja::command::Command;
+
+pub struct ShowFileReport;
+
+impl Command for ShowFileReport {
+ fn action(&self, view: &BinaryView) {
+ let view = view.to_owned();
+ std::thread::spawn(move || {
+ let Some(path) =
+ binaryninja::interaction::get_open_filename_input("Select file to show", "*.warp")
+ else {
+ return;
+ };
+
+ let Ok(bytes) = std::fs::read(&path) else {
+ log::error!("Failed to read file: {:?}", path);
+ return;
+ };
+
+ let Some(file) = warp::WarpFile::from_bytes(&bytes) else {
+ log::error!("Failed to parse file: {:?}", path);
+ return;
+ };
+
+ let report_generator = ReportGenerator::new();
+ if let Some(html_string) = report_generator.html_report(&file) {
+ view.show_html_report(
+ &format!("WARP File: {}", path.to_string_lossy()),
+ html_string.as_str(),
+ "",
+ );
+ }
+ });
+ }
+
+ fn valid(&self, _view: &BinaryView) -> bool {
+ true
+ }
+}
diff --git a/plugins/warp/src/plugin/find.rs b/plugins/warp/src/plugin/find.rs
deleted file mode 100644
index accc015d..00000000
--- a/plugins/warp/src/plugin/find.rs
+++ /dev/null
@@ -1,57 +0,0 @@
-use crate::cache::try_cached_function_guid;
-use binaryninja::binary_view::{BinaryView, BinaryViewExt};
-use binaryninja::command::Command;
-use binaryninja::function::Function as BNFunction;
-use binaryninja::rc::Guard as BNGuard;
-use rayon::prelude::*;
-use std::thread;
-use warp::signature::function::FunctionGUID;
-
-pub struct FindFunctionFromGUID;
-
-impl Command for FindFunctionFromGUID {
- fn action(&self, view: &BinaryView) {
- let Some(guid_str) = binaryninja::interaction::get_text_line_input(
- "Function GUID",
- "Find Function from GUID",
- ) else {
- return;
- };
-
- let Ok(searched_guid) = guid_str.parse::<FunctionGUID>() else {
- log::error!("Failed to parse function guid... {}", guid_str);
- return;
- };
-
- log::info!("Searching functions for GUID... {}", searched_guid);
- let funcs = view.functions();
- thread::spawn(move || {
- let background_task = binaryninja::background_task::BackgroundTask::new(
- &format!("Searching functions for GUID... {}", searched_guid),
- false,
- );
-
- // Only run this for functions which have already generated a GUID.
- let matched = funcs
- .par_iter()
- .filter(|func| {
- try_cached_function_guid(func).is_some_and(|guid| guid == searched_guid)
- })
- .collect::<Vec<BNGuard<BNFunction>>>();
-
- if matched.is_empty() {
- log::info!("No matches found for GUID... {}", searched_guid);
- } else {
- for func in matched {
- log::info!("Match found at function... 0x{:0x}", func.start());
- }
- }
-
- background_task.finish();
- });
- }
-
- fn valid(&self, _view: &BinaryView) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/plugin/function.rs b/plugins/warp/src/plugin/function.rs
new file mode 100644
index 00000000..130e8582
--- /dev/null
+++ b/plugins/warp/src/plugin/function.rs
@@ -0,0 +1,133 @@
+use crate::cache::{cached_function_guid, try_cached_function_guid};
+use crate::{get_warp_include_tag_type, INCLUDE_TAG_NAME};
+use binaryninja::background_task::BackgroundTask;
+use binaryninja::binary_view::{BinaryView, BinaryViewExt};
+use binaryninja::command::{Command, FunctionCommand};
+use binaryninja::function::Function;
+use binaryninja::rc::Guard;
+use rayon::iter::ParallelIterator;
+use std::thread;
+use warp::signature::function::FunctionGUID;
+
+pub struct IncludeFunction;
+
+impl FunctionCommand for IncludeFunction {
+ fn action(&self, view: &BinaryView, func: &Function) {
+ let sym_name = func.symbol().short_name();
+ let sym_name_str = sym_name.to_string_lossy();
+ let should_add_tag = func.function_tags(None, Some(INCLUDE_TAG_NAME)).is_empty();
+ let insert_tag_type = get_warp_include_tag_type(view);
+ match should_add_tag {
+ true => {
+ log::info!(
+ "Including selected function '{}' at 0x{:x}",
+ sym_name_str,
+ func.start()
+ );
+ func.add_tag(&insert_tag_type, "", None, false, None);
+ }
+ false => {
+ log::info!(
+ "Removing included function '{}' at 0x{:x}",
+ sym_name_str,
+ func.start()
+ );
+ func.remove_tags_of_type(&insert_tag_type, None, false, None);
+ }
+ }
+ }
+
+ fn valid(&self, _view: &BinaryView, _func: &Function) -> bool {
+ // TODO: Only allow if the function is named?
+ true
+ }
+}
+
+pub struct CopyFunctionGUID;
+
+impl FunctionCommand for CopyFunctionGUID {
+ fn action(&self, _view: &BinaryView, func: &Function) {
+ let Ok(lifted_il) = func.lifted_il() else {
+ log::error!("Could not get lifted il for copied function");
+ return;
+ };
+ let guid = cached_function_guid(func, &lifted_il);
+ log::info!(
+ "Function GUID for {:?}... {}",
+ func.symbol().short_name(),
+ guid
+ );
+ if let Ok(mut clipboard) = arboard::Clipboard::new() {
+ let _ = clipboard.set_text(guid.to_string());
+ }
+ }
+
+ fn valid(&self, _view: &BinaryView, _func: &Function) -> bool {
+ true
+ }
+}
+
+pub struct FindFunctionFromGUID;
+
+impl Command for FindFunctionFromGUID {
+ fn action(&self, view: &BinaryView) {
+ let Some(guid_str) = binaryninja::interaction::get_text_line_input(
+ "Function GUID",
+ "Find Function from GUID",
+ ) else {
+ return;
+ };
+
+ let Ok(searched_guid) = guid_str.parse::<FunctionGUID>() else {
+ log::error!("Failed to parse function guid... {}", guid_str);
+ return;
+ };
+
+ log::info!("Searching functions for GUID... {}", searched_guid);
+ let funcs = view.functions();
+ let view = view.to_owned();
+ thread::spawn(move || {
+ let background_task = BackgroundTask::new(
+ &format!("Searching functions for GUID... {}", searched_guid),
+ false,
+ );
+
+ // Only run this for functions which have already generated a GUID.
+ let matched: Vec<Guard<Function>> = funcs
+ .par_iter()
+ .filter(|func| {
+ try_cached_function_guid(func).is_some_and(|guid| guid == searched_guid)
+ })
+ .collect();
+
+ if matched.is_empty() {
+ log::info!("No matches found for GUID... {}", searched_guid);
+ } else {
+ for func in &matched {
+ // Also navigate the user, as that is probably what they want.
+ if matched.len() == 1 {
+ let current_view = view.file().current_view();
+ if view
+ .file()
+ .navigate_to(&current_view, func.start())
+ .is_err()
+ {
+ log::error!(
+ "Failed to navigate to found function 0x{:0x} in view {}",
+ func.start(),
+ current_view
+ );
+ }
+ }
+ log::info!("Match found at function... 0x{:0x}", func.start());
+ }
+ }
+
+ background_task.finish();
+ });
+ }
+
+ fn valid(&self, _view: &BinaryView) -> bool {
+ true
+ }
+}
diff --git a/plugins/warp/src/plugin/load.rs b/plugins/warp/src/plugin/load.rs
index c86a16cb..c0319ea5 100644
--- a/plugins/warp/src/plugin/load.rs
+++ b/plugins/warp/src/plugin/load.rs
@@ -1,54 +1,162 @@
-use crate::matcher::{Matcher, PlatformID, PLAT_MATCHER_CACHE};
+use crate::cache::container::add_cached_container;
+use crate::container::disk::{DiskContainer, DiskContainerSource};
+use crate::container::{ContainerError, SourcePath};
+use crate::convert::platform_to_target;
+use crate::plugin::workflow::run_matcher;
use binaryninja::binary_view::{BinaryView, BinaryViewExt};
use binaryninja::command::Command;
+use binaryninja::interaction::{
+ show_message_box, Form, FormInputField, MessageBoxButtonResult, MessageBoxButtonSet,
+ MessageBoxIcon,
+};
+use binaryninja::rc::Ref;
+use std::collections::HashMap;
+use std::path::PathBuf;
+use std::thread;
+use warp::WarpFile;
+
+pub struct LoadFileField;
+
+impl LoadFileField {
+ pub fn field() -> FormInputField {
+ FormInputField::OpenFileName {
+ prompt: "File Path".to_string(),
+ // TODO: This is called extension but is really a filter.
+ extension: Some("*.warp".to_string()),
+ default: None,
+ value: None,
+ }
+ }
+
+ pub fn from_form(form: &Form) -> Option<PathBuf> {
+ let field = form.get_field_with_name("File Path")?;
+ let field_value = field.try_value_string()?;
+ Some(PathBuf::from(field_value))
+ }
+}
+
+pub struct RunMatcherField;
+
+impl RunMatcherField {
+ pub fn field() -> FormInputField {
+ FormInputField::Choice {
+ prompt: "Rerun Initial Matcher".to_string(),
+ choices: vec!["No".to_string(), "Yes".to_string()],
+ default: Some(1),
+ value: 0,
+ }
+ }
+
+ pub fn from_form(form: &Form) -> Option<bool> {
+ let field = form.get_field_with_name("Rerun Initial Matcher")?;
+ let field_value = field.try_value_index()?;
+ match field_value {
+ 1 => Some(true),
+ _ => Some(false),
+ }
+ }
+}
+
pub struct LoadSignatureFile;
-impl Command for LoadSignatureFile {
- fn action(&self, view: &BinaryView) {
- let Some(platform) = view.default_platform() else {
- log::error!("Default platform must be set to load signature!");
- return;
- };
+impl LoadSignatureFile {
+ pub fn read_file(
+ view: &BinaryView,
+ path: SourcePath,
+ ) -> Result<WarpFile<'static>, ContainerError> {
+ let contents = std::fs::read(&path).map_err(|e| ContainerError::FailedIO(e.kind()))?;
+ let mut file = WarpFile::from_owned_bytes(contents).ok_or(
+ ContainerError::CorruptedData("file data failed to validate"),
+ )?;
- // NOTE: Because we only can consume signatures from a specific directory, we don't need to use the interaction API.
- // If we did need to load signature files from a project than this would need to change.
- let Some(file) = rfd::FileDialog::new()
- .add_filter("Signature Files", &["sbin"])
- .set_file_name(format!("{}.sbin", view.file().filename()))
- .pick_file()
- else {
- return;
- };
+ let view_target = view
+ .default_platform()
+ .map(|p| platform_to_target(&p))
+ .unwrap_or_default();
+ let file_has_target = file
+ .chunks
+ .iter()
+ .find(|c| c.header.target == view_target)
+ .is_some();
+
+ if !file_has_target {
+ // File does not contain a view target, alert user if they would like to override the file chunks to the view target.
+ let text = format!(
+ "Attempting to load WARP file with no target `{:?}`, continue loading anyways?",
+ &view_target
+ );
+ let res = show_message_box(
+ "Override file target?",
+ &text,
+ MessageBoxButtonSet::YesNoButtonSet,
+ MessageBoxIcon::WarningIcon,
+ );
+ if res != MessageBoxButtonResult::YesButton {
+ return Err(ContainerError::CorruptedData(
+ "User does not want to load file",
+ ));
+ }
+
+ // Take all the chunks and convert them to the target, so we load them.
+ // If we do not do this, the user will be surprised when they get no new matches.
+ for chunk in &mut file.chunks {
+ chunk.header.target = view_target.clone();
+ }
+ }
- let Ok(data) = std::fs::read(&file) else {
- log::error!("Could not read signature file: {:?}", file);
+ Ok(file)
+ }
+
+ pub fn execute(view: Ref<BinaryView>) {
+ let mut form = Form::new("Load Signature File");
+ form.add_field(LoadFileField::field());
+ // let fd_field = FileDataKindField::default();
+ // form.add_field(fd_field.to_field());
+ form.add_field(RunMatcherField::field());
+ if !form.prompt() {
return;
- };
+ }
- let Some(data) = warp::signature::Data::from_bytes(&data) else {
- log::error!("Could not get data from signature file: {:?}", file);
+ let Some(file_path) = LoadFileField::from_form(&form) else {
return;
};
+ // TODO: Decide what to pull using the file data kind.
+ // let _file_data_kind = FileDataKindField::from_form(&form).unwrap_or_default();
+ let rerun_matcher = RunMatcherField::from_form(&form).unwrap_or(false);
+
+ let source_file_path = SourcePath::new(file_path.clone());
- let new_matcher = Matcher::from_data(data);
- log::info!(
- "Loading signature file with {} functions and {} types...",
- new_matcher.functions.len(),
- new_matcher.types.len()
- );
- let platform_id = PlatformID::from(platform.as_ref());
- let matcher_cache = PLAT_MATCHER_CACHE.get_or_init(Default::default);
- match matcher_cache.get_mut(&platform_id) {
- Some(mut matcher) => matcher.extend_with_matcher(new_matcher),
- None => {
- // We still must uphold `from_platform` in case we are running this before the matcher workflow
- // is kicked off. Other-wise we only will have the `new_matcher` data.
- let mut matcher = Matcher::from_platform(platform);
- matcher.extend_with_matcher(new_matcher);
- matcher_cache.insert(platform_id, matcher);
+ let file = match LoadSignatureFile::read_file(&view, source_file_path.clone()) {
+ Ok(file) => file,
+ Err(e) => {
+ log::error!("Failed to read signature file: {}", e);
+ return;
}
+ };
+
+ let container_source = DiskContainerSource::new(source_file_path.clone(), file);
+ log::info!("Loading container source: '{}'", container_source.path);
+ let mut map = HashMap::new();
+ map.insert(source_file_path.to_source_id(), container_source);
+ let container = DiskContainer::new("Loaded signatures".to_string(), map);
+ // TODO: See notes in the matcher about doing this, we really need to load it into an existing container.
+ add_cached_container(container);
+
+ if rerun_matcher {
+ thread::spawn(move || {
+ run_matcher(&view);
+ });
}
}
+}
+
+impl Command for LoadSignatureFile {
+ fn action(&self, view: &BinaryView) {
+ let view = view.to_owned();
+ thread::spawn(move || {
+ LoadSignatureFile::execute(view);
+ });
+ }
fn valid(&self, _view: &BinaryView) -> bool {
true
diff --git a/plugins/warp/src/plugin/project.rs b/plugins/warp/src/plugin/project.rs
new file mode 100644
index 00000000..ea5a5600
--- /dev/null
+++ b/plugins/warp/src/plugin/project.rs
@@ -0,0 +1,132 @@
+use binaryninja::background_task::BackgroundTask;
+use binaryninja::command::ProjectCommand;
+use binaryninja::interaction::{Form, FormInputField};
+use binaryninja::project::Project;
+use binaryninja::rc::Ref;
+use regex::Regex;
+use std::thread;
+use std::time::Instant;
+
+use crate::processor::{
+ new_processing_state_background_thread, FileDataKindField, FileFilterField, WarpFileProcessor,
+};
+use crate::report::{ReportGenerator, ReportKindField};
+
+pub struct CreateSignaturesForm {
+ form: Form,
+}
+
+impl CreateSignaturesForm {
+ pub fn new(_project: &Project) -> CreateSignaturesForm {
+ let mut form = Form::new("Create Signature File");
+ form.add_field(Self::file_data_field());
+ form.add_field(Self::file_filter_field());
+ form.add_field(Self::generated_report_field());
+ // TODO: Threads (we run the analysis in the background)
+ Self { form }
+ }
+
+ pub fn file_data_field() -> FormInputField {
+ FileDataKindField::default().to_field()
+ }
+
+ pub fn file_data_kind(&self) -> FileDataKindField {
+ FileDataKindField::from_form(&self.form).unwrap_or_default()
+ }
+
+ pub fn file_filter_field() -> FormInputField {
+ FileFilterField::to_field()
+ }
+
+ pub fn file_filter(&self) -> Option<Regex> {
+ FileFilterField::from_form(&self.form)
+ }
+
+ pub fn generated_report_field() -> FormInputField {
+ ReportKindField::default().to_field()
+ }
+
+ pub fn generated_report_kind(&self) -> ReportKindField {
+ ReportKindField::from_form(&self.form).unwrap_or_default()
+ }
+
+ pub fn prompt(&mut self) -> bool {
+ self.form.prompt()
+ }
+}
+
+pub struct CreateSignatures;
+
+impl CreateSignatures {
+ pub fn execute(project: Ref<Project>) {
+ let mut form = CreateSignaturesForm::new(&project);
+ if !form.prompt() {
+ return;
+ }
+ let file_data_kind = form.file_data_kind();
+ let report_kind = form.generated_report_kind();
+
+ let mut processor = WarpFileProcessor::new().with_file_data(file_data_kind);
+
+ // This thread will show the state in a background task.
+ let background_task = BackgroundTask::new("Processing started...", true);
+ new_processing_state_background_thread(background_task.clone(), processor.state());
+
+ if let Some(filter) = form.file_filter() {
+ processor = processor.with_file_filter(filter);
+ }
+
+ let start = Instant::now();
+ match processor.process_project(&project) {
+ Ok(warp_file) => {
+ // Print the processor string into the description of the file, so we know how it was generated.
+ let processor_str = format!("{:#?}", &processor);
+
+ // TODO: File name needs to be configurable.
+ if project
+ .create_file(
+ &warp_file.to_bytes(),
+ None,
+ "generated.warp",
+ &processor_str,
+ )
+ .is_err()
+ {
+ log::error!("Failed to create project file!");
+ }
+
+ let report = ReportGenerator::new();
+ if let Some(generated) = report.report(&report_kind, &warp_file) {
+ let ext = report.report_extension(&report_kind).unwrap_or_default();
+ let file_name = format!("report.{}", ext);
+ if project
+ .create_file(&generated.into_bytes(), None, &file_name, "Warp file")
+ .is_err()
+ {
+ log::error!("Failed to create project file!");
+ }
+ }
+ }
+ Err(e) => {
+ log::error!("Failed to process project: {}", e);
+ }
+ }
+ log::info!("Processing project files took: {:?}", start.elapsed());
+
+ // Tells the processing state thread to finish.
+ background_task.finish();
+ }
+}
+
+impl ProjectCommand for CreateSignatures {
+ fn action(&self, project: &Project) {
+ let project = project.to_owned();
+ thread::spawn(move || {
+ CreateSignatures::execute(project);
+ });
+ }
+
+ fn valid(&self, _view: &Project) -> bool {
+ true
+ }
+}
diff --git a/plugins/warp/src/plugin/render_layer.rs b/plugins/warp/src/plugin/render_layer.rs
index cdac71f2..d7984303 100644
--- a/plugins/warp/src/plugin/render_layer.rs
+++ b/plugins/warp/src/plugin/render_layer.rs
@@ -1,56 +1,94 @@
-use crate::{is_blacklisted_instruction, is_variant_instruction, relocatable_regions};
+use crate::{
+ is_blacklisted_instruction, is_computed_variant_instruction, is_variant_instruction,
+ relocatable_regions,
+};
use binaryninja::basic_block::BasicBlock;
use binaryninja::disassembly::DisassemblyTextLine;
+use binaryninja::flowgraph::FlowGraph;
use binaryninja::function::{HighlightColor, HighlightStandardColor, NativeBlock};
-use binaryninja::low_level_il::instruction::LowLevelInstructionIndex;
+use binaryninja::low_level_il::LowLevelILRegularFunction;
use binaryninja::render_layer::{register_render_layer, RenderLayer};
-pub struct HighlightRenderLayer {}
+// TODO: Add a render layer to show basic block GUID's?
+// TODO: Add a render layer to show constraints for current function?
+
+pub struct HighlightRenderLayer {
+ blacklist: HighlightColor,
+ variant: HighlightColor,
+ computed_variant: HighlightColor,
+}
impl HighlightRenderLayer {
pub fn register() {
register_render_layer(
"WARP Highlight Layer",
- HighlightRenderLayer {},
+ // TODO: Make the highlight colors configurable.
+ HighlightRenderLayer {
+ blacklist: HighlightColor::StandardHighlightColor {
+ color: HighlightStandardColor::BlackHighlightColor,
+ alpha: 155,
+ },
+ variant: HighlightColor::StandardHighlightColor {
+ color: HighlightStandardColor::RedHighlightColor,
+ alpha: 155,
+ },
+ computed_variant: HighlightColor::StandardHighlightColor {
+ color: HighlightStandardColor::OrangeHighlightColor,
+ alpha: 155,
+ },
+ },
Default::default(),
);
}
+
+ /// Highlights the lines that are variant or blacklisted.
+ pub fn highlight_lines(
+ &self,
+ lifted_il: &LowLevelILRegularFunction,
+ llil: &LowLevelILRegularFunction,
+ lines: &mut [DisassemblyTextLine],
+ ) {
+ let relocatable_regions = relocatable_regions(&lifted_il.function().view());
+ for line in lines {
+ // We use address here instead of index since it's more reliable for other IL's.
+ if let Some(lifted_il_instr) = lifted_il.instruction_at(line.address) {
+ if is_blacklisted_instruction(&lifted_il_instr) {
+ line.highlight = self.blacklist;
+ } else if is_variant_instruction(&relocatable_regions, &lifted_il_instr) {
+ line.highlight = self.variant;
+ }
+ }
+
+ if let Some(llil_instr) = llil.instruction_at(line.address) {
+ if is_computed_variant_instruction(&relocatable_regions, &llil_instr) {
+ line.highlight = self.computed_variant;
+ }
+ }
+ }
+ }
}
impl RenderLayer for HighlightRenderLayer {
- fn apply_to_llil_block(
+ fn apply_to_flow_graph(&self, graph: &mut FlowGraph) {
+ if let (Some(lifted_il), Some(llil)) = (graph.lifted_il(), graph.low_level_il()) {
+ for node in &graph.nodes() {
+ let mut new_lines = node.lines().to_vec();
+ self.highlight_lines(&lifted_il, &llil, &mut new_lines);
+ node.set_lines(new_lines);
+ }
+ }
+ }
+
+ fn apply_to_block(
&self,
block: &BasicBlock<NativeBlock>,
mut lines: Vec<DisassemblyTextLine>,
) -> Vec<DisassemblyTextLine> {
- // Highlight any LLIL instruction that will be masked by WARP.
+ // Highlight any instruction that WARP will mask.
let function = block.function();
- // TODO: We might need to make relocatable regions configurable.
- let relocatable_regions = relocatable_regions(&function.view());
- let Ok(llil) = function.low_level_il() else {
- // Don't even think this is possible but _shrug_.
- return lines;
- };
-
- for line in &mut lines {
- let llil_instr_idx = LowLevelInstructionIndex(line.instruction_index);
- if let Some(llil_instr) = llil.instruction_from_index(llil_instr_idx) {
- if is_blacklisted_instruction(&llil_instr) {
- // We have a blacklisted instruction, highlight it as orange!
- line.highlight = HighlightColor::StandardHighlightColor {
- color: HighlightStandardColor::OrangeHighlightColor,
- alpha: 155,
- };
- } else if is_variant_instruction(&relocatable_regions, &llil_instr) {
- // We have a variant instruction, highlight it as red!
- line.highlight = HighlightColor::StandardHighlightColor {
- color: HighlightStandardColor::RedHighlightColor,
- alpha: 155,
- };
- }
- }
+ if let (Ok(lifted_il), Ok(llil)) = (function.lifted_il(), function.low_level_il()) {
+ self.highlight_lines(&lifted_il, &llil, &mut lines);
}
-
lines
}
}
diff --git a/plugins/warp/src/plugin/settings.rs b/plugins/warp/src/plugin/settings.rs
new file mode 100644
index 00000000..6cd2e0cc
--- /dev/null
+++ b/plugins/warp/src/plugin/settings.rs
@@ -0,0 +1,129 @@
+use binaryninja::settings::Settings as BNSettings;
+use serde_json::json;
+use std::string::ToString;
+
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct PluginSettings {
+ /// Whether to load bundled WARP files on startup. Turn this off if you want to manually load them.
+ ///
+ /// This is set to [PluginSettings::LOAD_BUNDLED_FILES_DEFAULT] by default.
+ pub load_bundled_files: bool,
+ /// Whether to load user WARP files on startup. Turn this off if you want to manually load them.
+ ///
+ /// This is set to [PluginSettings::LOAD_USER_FILES_DEFAULT] by default.
+ pub load_user_files: bool,
+ /// The WARP server to use.
+ ///
+ /// This is set to [PluginSettings::SERVER_URL_DEFAULT] by default.
+ pub server_url: String,
+ /// The API key to use for the selected WARP server, if not specified, you will be unable to push data and may be rate-limited.
+ ///
+ /// This is set to [PluginSettings::SERVER_API_KEY_DEFAULT] by default.
+ pub server_api_key: String,
+ /// Whether to allow networked WARP requests. Turning this off will not disable local WARP functionality.
+ ///
+ /// This is set to [PluginSettings::ENABLE_SERVER_DEFAULT] by default.
+ pub enable_server: bool,
+}
+
+impl PluginSettings {
+ pub const LOAD_BUNDLED_FILES_DEFAULT: bool = true;
+ pub const LOAD_BUNDLED_FILES_SETTING: &'static str = "analysis.warp.loadBundledFiles";
+ pub const LOAD_USER_FILES_DEFAULT: bool = true;
+ pub const LOAD_USER_FILES_SETTING: &'static str = "analysis.warp.loadUserFiles";
+ pub const SERVER_URL_DEFAULT: &'static str = "https://warp.binary.ninja";
+ pub const SERVER_URL_SETTING: &'static str = "analysis.warp.serverUrl";
+ pub const SERVER_API_KEY_DEFAULT: &'static str = "";
+ pub const SERVER_API_KEY_SETTING: &'static str = "analysis.warp.serverApiKey";
+ pub const ENABLE_SERVER_DEFAULT: bool = true;
+ pub const ENABLE_SERVER_SETTING: &'static str = "network.enableWARP";
+
+ pub fn register(bn_settings: &mut BNSettings) {
+ let load_bundled_files_prop = json!({
+ "title" : "Load Bundled Files",
+ "type" : "boolean",
+ "default" : Self::LOAD_BUNDLED_FILES_DEFAULT,
+ "description" : "Whether to load bundled WARP files on startup. Turn this off if you want to manually load them.",
+ "ignore" : ["SettingsProjectScope", "SettingsResourceScope"]
+ });
+ bn_settings.register_setting_json(
+ Self::LOAD_BUNDLED_FILES_SETTING,
+ &load_bundled_files_prop.to_string(),
+ );
+ let load_user_files_prop = json!({
+ "title" : "Load User Files",
+ "type" : "boolean",
+ "default" : Self::LOAD_USER_FILES_DEFAULT,
+ "description" : "Whether to load user WARP files on startup. Turn this off if you want to manually load them.",
+ "ignore" : ["SettingsProjectScope", "SettingsResourceScope"]
+ });
+ bn_settings.register_setting_json(
+ Self::LOAD_USER_FILES_SETTING,
+ &load_user_files_prop.to_string(),
+ );
+ let server_url_prop = json!({
+ "title" : "Server URL",
+ "type" : "string",
+ "default" : Self::SERVER_URL_DEFAULT,
+ "description" : "The WARP server to use.",
+ "ignore" : ["SettingsProjectScope", "SettingsResourceScope"]
+ });
+ bn_settings.register_setting_json(Self::SERVER_URL_SETTING, &server_url_prop.to_string());
+ let server_api_key_prop = json!({
+ "title" : "Server API Key",
+ "type" : "string",
+ "default" : Self::SERVER_API_KEY_DEFAULT,
+ "description" : "The API key to use for the selected WARP server, if not specified you will be unable to push data, and may be rate limited.",
+ "ignore" : ["SettingsProjectScope", "SettingsResourceScope"],
+ "hidden": true
+ });
+ bn_settings.register_setting_json(
+ Self::SERVER_API_KEY_SETTING,
+ &server_api_key_prop.to_string(),
+ );
+ let server_enabled_prop = json!({
+ "title" : "Enable WARP",
+ "type" : "boolean",
+ "default" : Self::ENABLE_SERVER_DEFAULT,
+ "description" : "Whether or not to allow networked WARP requests. Turning this off will not disable local WARP functionality.",
+ "ignore" : ["SettingsProjectScope", "SettingsResourceScope"]
+ });
+ bn_settings.register_setting_json(
+ Self::ENABLE_SERVER_SETTING,
+ &server_enabled_prop.to_string(),
+ );
+ }
+
+ /// Retrieve plugin settings from [`BNSettings`].
+ pub fn from_settings(bn_settings: &BNSettings) -> Self {
+ let mut settings = PluginSettings::default();
+ if bn_settings.contains(Self::LOAD_BUNDLED_FILES_SETTING) {
+ settings.load_bundled_files = bn_settings.get_bool(Self::LOAD_BUNDLED_FILES_SETTING);
+ }
+ if bn_settings.contains(Self::LOAD_USER_FILES_SETTING) {
+ settings.load_user_files = bn_settings.get_bool(Self::LOAD_USER_FILES_SETTING);
+ }
+ if bn_settings.contains(Self::SERVER_URL_SETTING) {
+ settings.server_url = bn_settings.get_string(Self::SERVER_URL_SETTING);
+ }
+ if bn_settings.contains(Self::SERVER_API_KEY_SETTING) {
+ settings.server_url = bn_settings.get_string(Self::SERVER_API_KEY_SETTING);
+ }
+ if bn_settings.contains(Self::ENABLE_SERVER_SETTING) {
+ settings.enable_server = bn_settings.get_bool(Self::ENABLE_SERVER_SETTING);
+ }
+ settings
+ }
+}
+
+impl Default for PluginSettings {
+ fn default() -> Self {
+ Self {
+ load_bundled_files: PluginSettings::LOAD_BUNDLED_FILES_DEFAULT,
+ load_user_files: PluginSettings::LOAD_USER_FILES_DEFAULT,
+ server_url: PluginSettings::SERVER_URL_DEFAULT.to_string(),
+ server_api_key: PluginSettings::SERVER_API_KEY_DEFAULT.to_string(),
+ enable_server: PluginSettings::ENABLE_SERVER_DEFAULT,
+ }
+ }
+}
diff --git a/plugins/warp/src/plugin/types.rs b/plugins/warp/src/plugin/types.rs
deleted file mode 100644
index 41e03cd3..00000000
--- a/plugins/warp/src/plugin/types.rs
+++ /dev/null
@@ -1,57 +0,0 @@
-use crate::convert::to_bn_type;
-use binaryninja::binary_view::{BinaryView, BinaryViewExt};
-use binaryninja::command::Command;
-use std::time::Instant;
-
-pub struct LoadTypes;
-
-impl Command for LoadTypes {
- fn action(&self, view: &BinaryView) {
- // NOTE: Because we only can consume signatures from a specific directory, we don't need to use the interaction API.
- // If we did need to load signature files from a project than this would need to change.
- let Some(file) = rfd::FileDialog::new()
- .add_filter("Signature Files", &["sbin"])
- .set_file_name(format!("{}.sbin", view.file().filename()))
- .pick_file()
- else {
- return;
- };
-
- let Ok(data) = std::fs::read(&file) else {
- log::error!("Could not read signature file: {:?}", file);
- return;
- };
-
- let Some(data) = warp::signature::Data::from_bytes(&data) else {
- log::error!("Could not get data from signature file: {:?}", file);
- return;
- };
-
- let Some(arch) = view.default_arch() else {
- log::error!("Could not get default architecture");
- return;
- };
-
- let view = view.to_owned();
- std::thread::spawn(move || {
- let background_task = binaryninja::background_task::BackgroundTask::new(
- &format!("Applying {} types...", data.types.len()),
- true,
- );
-
- let start = Instant::now();
- for comp_ty in data.types {
- let ty_id = comp_ty.guid.to_string();
- let ty_name = comp_ty.ty.name.to_owned().unwrap_or_else(|| ty_id.clone());
- view.define_auto_type_with_id(ty_name, &ty_id, &to_bn_type(&arch, &comp_ty.ty));
- }
-
- log::info!("Type application took {:?}", start.elapsed());
- background_task.finish();
- });
- }
-
- fn valid(&self, _view: &BinaryView) -> bool {
- true
- }
-}
diff --git a/plugins/warp/src/plugin/workflow.rs b/plugins/warp/src/plugin/workflow.rs
index f5763116..487fccfe 100644
--- a/plugins/warp/src/plugin/workflow.rs
+++ b/plugins/warp/src/plugin/workflow.rs
@@ -1,16 +1,40 @@
-use crate::cache::cached_function_guid;
-use crate::matcher::cached_function_matcher;
+use crate::cache::container::for_cached_containers;
+use crate::cache::{
+ cached_function_guid, insert_cached_function_match, try_cached_function_guid,
+ try_cached_function_match,
+};
+use crate::convert::{
+ comment_to_bn_comment, platform_to_target, to_bn_symbol_at_address, to_bn_type,
+};
+use crate::matcher::{Matcher, MatcherSettings};
+use crate::{get_warp_tag_type, relocatable_regions};
use binaryninja::background_task::BackgroundTask;
use binaryninja::binary_view::{BinaryView, BinaryViewExt};
use binaryninja::command::Command;
+use binaryninja::settings::{QueryOptions, Settings};
use binaryninja::workflow::{Activity, AnalysisContext, Workflow};
+use itertools::Itertools;
+use std::collections::HashMap;
use std::time::Instant;
+use warp::signature::function::{Function, FunctionGUID};
+use warp::target::Target;
+
+pub const APPLY_ACTIVITY_NAME: &str = "analysis.warp.apply";
+const APPLY_ACTIVITY_CONFIG: &str = r#"{
+ "name": "analysis.warp.apply",
+ "title" : "WARP Apply Matched",
+ "description": "This analysis step applies WARP info to matched functions...",
+ "eligibility": {
+ "auto": {},
+ "runOnce": false
+ }
+}"#;
pub const MATCHER_ACTIVITY_NAME: &str = "analysis.warp.matcher";
const MATCHER_ACTIVITY_CONFIG: &str = r#"{
"name": "analysis.warp.matcher",
"title" : "WARP Matcher",
- "description": "This analysis step applies WARP info to matched functions...",
+ "description": "This analysis step attempts to find matching WARP functions after the initial analysis is complete...",
"eligibility": {
"auto": {},
"runOnce": true
@@ -24,7 +48,7 @@ const GUID_ACTIVITY_CONFIG: &str = r#"{
"description": "This analysis step generates the GUID for all analyzed functions...",
"eligibility": {
"auto": {},
- "runOnce": true
+ "runOnce": false
}
}"#;
@@ -33,19 +57,8 @@ pub struct RunMatcher;
impl Command for RunMatcher {
fn action(&self, view: &BinaryView) {
let view = view.to_owned();
- // TODO: Check to see if the GUID cache is empty and ask the user if they want to regenerate the guids.
std::thread::spawn(move || {
- let undo_id = view.file().begin_undo_actions(true);
- let background_task = BackgroundTask::new("Matching on functions...", false);
- let start = Instant::now();
- view.functions()
- .iter()
- .for_each(|function| cached_function_matcher(&function));
- log::info!("Function matching took {:?}", start.elapsed());
- background_task.finish();
- view.file().commit_undo_actions(&undo_id);
- // Now we want to trigger re-analysis.
- view.update_analysis();
+ run_matcher(&view);
});
}
@@ -54,44 +67,157 @@ impl Command for RunMatcher {
}
}
+pub fn run_matcher(view: &BinaryView) {
+ // Alert the user if we have no actual regions (one comes from the synthetic section).
+ let regions = relocatable_regions(view);
+ if regions.len() <= 1 {
+ log::warn!(
+ "No relocatable regions found, for best results please define sections for the binary!"
+ );
+ }
+
+ // Then we want to actually find matching functions.
+ let background_task = BackgroundTask::new("Matching on WARP functions...", true);
+ let start = Instant::now();
+
+ // Build matcher
+ let view_settings = Settings::new();
+ let mut query_opts = QueryOptions::new_with_view(view);
+ let matcher_settings = MatcherSettings::from_settings(&view_settings, &mut query_opts);
+ let matcher = Matcher::new(matcher_settings);
+
+ // TODO: Par iter this? Using dashmap
+ let functions_by_target_and_guid: HashMap<(FunctionGUID, Target), Vec<_>> = view
+ .functions()
+ .iter()
+ .filter_map(|f| {
+ let guid = try_cached_function_guid(&f)?;
+ let target = platform_to_target(&f.platform());
+ Some(((guid, target), f.to_owned()))
+ })
+ .into_group_map();
+
+ // TODO: Par iter this? Using dashmap
+ let guids_by_target: HashMap<Target, Vec<FunctionGUID>> = functions_by_target_and_guid
+ .keys()
+ .map(|(guid, target)| (target.clone(), *guid))
+ .into_group_map();
+
+ // TODO: Target gets cloned a lot.
+ // TODO: Containers might both match on the same function. What should we do?
+ for_cached_containers(|container| {
+ if background_task.is_cancelled() {
+ return;
+ }
+
+ for (target, guids) in &guids_by_target {
+ let function_guid_with_sources = container
+ .sources_with_function_guids(target, guids)
+ .unwrap_or_default();
+
+ for (guid, sources) in &function_guid_with_sources {
+ let matched_functions: Vec<Function> = sources
+ .iter()
+ .flat_map(|source| {
+ container
+ .functions_with_guid(target, source, guid)
+ .unwrap_or_default()
+ })
+ .collect();
+
+ let functions = functions_by_target_and_guid
+ .get(&(*guid, target.clone()))
+ .expect("Function guid not found");
+
+ for function in functions {
+ // Match on all the possible functions
+ if let Some(matched_function) =
+ matcher.match_function_from_constraints(function, &matched_functions)
+ {
+ // We were able to find a match, add it to the match cache and then mark the function
+ // as requiring updates; this is so that we know about it in the applier activity.
+ insert_cached_function_match(function, Some(matched_function.clone()));
+ }
+ }
+ }
+ }
+ });
+
+ if background_task.is_cancelled() {
+ log::info!("Matcher was cancelled by user, you may run it again by running the 'Run Matcher' command.");
+ }
+
+ log::info!("Function matching took {:?}", start.elapsed());
+ background_task.finish();
+
+ // Now we want to trigger re-analysis.
+ view.update_analysis();
+}
+
pub fn insert_workflow() {
+ // "Hey look, it's a plier" ~ Josh 2025
+ let apply_activity = |ctx: &AnalysisContext| {
+ let view = ctx.view();
+ let function = ctx.function();
+ if let Some(matched_function) = try_cached_function_match(&function) {
+ view.define_auto_symbol(&to_bn_symbol_at_address(
+ &view,
+ &matched_function.symbol,
+ function.symbol().address(),
+ ));
+ if let Some(func_ty) = &matched_function.ty {
+ function.set_auto_type(&to_bn_type(&function.arch(), func_ty));
+ }
+ // TODO: How to clear the comments? They are just persisted.
+ // TODO: Also they generate an undo action, i hate implicit undo actions so much.
+ for comment in matched_function.comments {
+ let bn_comment = comment_to_bn_comment(&function, comment);
+ function.set_comment_at(bn_comment.addr, &bn_comment.comment);
+ }
+ function.add_tag(
+ &get_warp_tag_type(&view),
+ &matched_function.guid.to_string(),
+ None,
+ false,
+ None,
+ );
+ }
+ };
+
let matcher_activity = |ctx: &AnalysisContext| {
let view = ctx.view();
- let undo_id = view.file().begin_undo_actions(true);
- let background_task = BackgroundTask::new("Matching on functions...", false);
- let start = Instant::now();
- view.functions()
- .iter()
- .for_each(|function| cached_function_matcher(&function));
- log::info!("Function matching took {:?}", start.elapsed());
- background_task.finish();
- view.file().commit_undo_actions(&undo_id);
- // Now we want to trigger re-analysis.
- view.update_analysis();
+ run_matcher(&view);
};
let guid_activity = |ctx: &AnalysisContext| {
let function = ctx.function();
- // TODO: Returning RegularNonSSA means we cant modify the il (the lifting code was written just for lifted il, that needs to be fixed)
- if let Some(llil) = unsafe { ctx.llil_function() } {
- cached_function_guid(&function, &llil);
+ if let Some(lifted_il) = unsafe { ctx.lifted_il_function() } {
+ cached_function_guid(&function, &lifted_il);
}
};
let old_function_meta_workflow = Workflow::instance("core.function.metaAnalysis");
let function_meta_workflow = old_function_meta_workflow.clone_to("core.function.metaAnalysis");
let guid_activity = Activity::new_with_action(GUID_ACTIVITY_CONFIG, guid_activity);
+ let apply_activity = Activity::new_with_action(APPLY_ACTIVITY_CONFIG, apply_activity);
function_meta_workflow
.register_activity(&guid_activity)
.unwrap();
+ // Because we are going to impact analysis with application we must make sure the function update is triggered to continue to update analysis.
+ // TODO: need to ask why i cant do core.function.update like in the rtti plugin.
+ function_meta_workflow
+ .register_activity_with_subactivities::<Vec<String>>(&apply_activity, vec![])
+ .unwrap();
function_meta_workflow.insert("core.function.runFunctionRecognizers", [GUID_ACTIVITY_NAME]);
+ function_meta_workflow.insert("core.function.generateMediumLevelIL", [APPLY_ACTIVITY_NAME]);
function_meta_workflow.register().unwrap();
let old_module_meta_workflow = Workflow::instance("core.module.metaAnalysis");
let module_meta_workflow = old_module_meta_workflow.clone_to("core.module.metaAnalysis");
let matcher_activity = Activity::new_with_action(MATCHER_ACTIVITY_CONFIG, matcher_activity);
+ // Matcher activity must have core.module.update as subactivity otherwise analysis will sometimes never retrigger.
module_meta_workflow
- .register_activity(&matcher_activity)
+ .register_activity_with_subactivities(&matcher_activity, vec!["core.module.update"])
.unwrap();
module_meta_workflow.insert(
"core.module.deleteUnusedAutoFunctions",