diff --git a/Cargo.lock b/Cargo.lock index 93b6283d1d..4a843fe306 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -749,6 +749,16 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" +[[package]] +name = "example_data_renderer" +version = "0.1.0" +dependencies = [ + "binaryninja", + "binaryninjacore-sys", + "log", + "uuid", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -3015,9 +3025,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index 2811850a56..5bd540ad8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "rust", "arch/riscv", "arch/msp430", + "rust/plugin_examples/*", "view/minidump", "plugins/dwarf/dwarf_import", "plugins/dwarf/dwarf_export", diff --git a/rust/plugin_examples/data_renderer/Cargo.toml b/rust/plugin_examples/data_renderer/Cargo.toml new file mode 100644 index 0000000000..4ea72d3878 --- /dev/null +++ b/rust/plugin_examples/data_renderer/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "example_data_renderer" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +uuid = "1.18.1" +binaryninjacore-sys = { path = "../../binaryninjacore-sys", default-features = false } +binaryninja = { path = "../.." } +log = "0.4.27" diff --git a/rust/plugin_examples/data_renderer/README.md b/rust/plugin_examples/data_renderer/README.md new file mode 100644 index 0000000000..794b93b950 --- /dev/null +++ b/rust/plugin_examples/data_renderer/README.md @@ -0,0 +1,63 @@ +# Data Renderer Example + +This example implements a simple data renderer for the Mach-O load command LC_UUID. +You can try the renderer by loading the `/bin/cat` binary from macOS. + +We're implementing a functionality similar to the one described in the Python data renderer blog post: +https://binary.ninja/2024/04/08/customizing-data-display.html. + +## Building + +```sh +# Build from the root directory (binaryninja-api) +cargo build --manifest-path rust/plugin_examples/data_renderer/Cargo.toml +# Link binary on macOS +ln -sf $PWD/target/debug/libexample_data_renderer.dylib ~/Library/Application\ Support/Binary\ Ninja/plugins +``` + +## Result + +The following Mach-O load command be will be transformed from + +```c +struct uuid __macho_load_command_[10] = +{ + enum load_command_type_t cmd = LC_UUID + uint32_t cmdsize = 0x18 + uint8_t uuid[0x10] = + { + [0x0] = 0x74 + [0x1] = 0xa0 + [0x2] = 0x3a + [0x3] = 0xbd + [0x4] = 0x1e + [0x5] = 0x19 + [0x6] = 0x32 + [0x7] = 0x67 + [0x8] = 0x9a + [0x9] = 0xdc + [0xa] = 0x42 + [0xb] = 0x99 + [0xc] = 0x4e + [0xd] = 0x26 + [0xe] = 0xa2 + [0xf] = 0xb7 + } +} +``` + +into the following representation + +```c +struct uuid __macho_load_command_[10] = +{ + enum load_command_type_t cmd = LC_UUID + uint32_t cmdsize = 0x18 + uint8_t uuid[0x10] = UUID("74a03abd-1e19-3267-9adc-42994e26a2b7") +} +``` + +You can compare the shown UUID with the output of otool: +```sh +otool -arch all -l /bin/cat +``` \ No newline at end of file diff --git a/rust/plugin_examples/data_renderer/build.rs b/rust/plugin_examples/data_renderer/build.rs new file mode 100644 index 0000000000..9006f16a69 --- /dev/null +++ b/rust/plugin_examples/data_renderer/build.rs @@ -0,0 +1,25 @@ +fn main() { + let link_path = std::env::var_os("DEP_BINARYNINJACORE_PATH") + .expect("DEP_BINARYNINJACORE_PATH not specified"); + + println!("cargo::rustc-link-lib=dylib=binaryninjacore"); + println!("cargo::rustc-link-search={}", link_path.to_str().unwrap()); + + #[cfg(target_os = "linux")] + { + println!( + "cargo::rustc-link-arg=-Wl,-rpath,{0},-L{0}", + link_path.to_string_lossy() + ); + } + + #[cfg(target_os = "macos")] + { + let crate_name = std::env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME not set"); + let lib_name = crate_name.replace('-', "_"); + println!( + "cargo::rustc-link-arg=-Wl,-install_name,@rpath/lib{}.dylib", + lib_name + ); + } +} diff --git a/rust/plugin_examples/data_renderer/src/lib.rs b/rust/plugin_examples/data_renderer/src/lib.rs new file mode 100644 index 0000000000..8e16bb1f2c --- /dev/null +++ b/rust/plugin_examples/data_renderer/src/lib.rs @@ -0,0 +1,109 @@ +use binaryninja::binary_view::{BinaryView, BinaryViewBase}; +use binaryninja::data_renderer::{ + register_specific_data_renderer, CustomDataRenderer, TypeContext, +}; +use binaryninja::disassembly::{ + DisassemblyTextLine, InstructionTextToken, InstructionTextTokenKind, +}; +use binaryninja::types::{Type, TypeClass}; +use uuid::Uuid; + +struct UuidDataRenderer {} + +impl CustomDataRenderer for UuidDataRenderer { + fn is_valid_for_data( + &self, + _view: &BinaryView, + _addr: u64, + type_: &Type, + types: &[TypeContext], + ) -> bool { + // We only want to render arrays with a size of 16 elements + if type_.type_class() != TypeClass::ArrayTypeClass { + return false; + } + if type_.count() != 0x10 { + return false; + } + + // The array elements must be of the type uint8_t + let Some(element_type_conf) = type_.element_type() else { + return false; + }; + let element_type = element_type_conf.contents; + if element_type.type_class() != TypeClass::IntegerTypeClass { + return false; + } + if element_type.width() != 1 { + return false; + } + + // The array should be embedded in a named type reference with the id macho:["uuid"] + for type_ctx in types { + if type_ctx.type_().type_class() != TypeClass::NamedTypeReferenceClass { + continue; + } + + let Some(name_ref) = type_ctx.type_().get_named_type_reference() else { + continue; + }; + + if name_ref.id() == "macho:[\"uuid\"]" { + return true; + } + } + + false + } + + fn lines_for_data( + &self, + view: &BinaryView, + addr: u64, + _type_: &Type, + prefix: Vec, + _width: usize, + _types_ctx: &[TypeContext], + _language: &str, + ) -> Vec { + let mut tokens = prefix.clone(); + + let mut buf = [0u8; 0x10]; + let bytes_read = view.read(&mut buf, addr); + + // Make sure that we've read all UUID bytes and convert them to token + if bytes_read == 0x10 { + tokens.extend([ + InstructionTextToken::new("UUID(\"", InstructionTextTokenKind::Text), + InstructionTextToken::new( + Uuid::from_bytes(buf).to_string(), + InstructionTextTokenKind::String { value: 0 }, + ), + InstructionTextToken::new("\")", InstructionTextTokenKind::Text), + ]); + } else { + tokens.push(InstructionTextToken::new( + "error: cannot read 0x10 bytes", + InstructionTextTokenKind::Annotation, + )); + } + + vec![DisassemblyTextLine::new_with_addr(tokens, addr)] + } +} + +/// # Safety +/// This function is called from Binary Ninja once to initialize the plugin. +#[allow(non_snake_case)] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn CorePluginInit() -> bool { + // Initialize logging + binaryninja::logger::Logger::new("UUID Data Renderer") + .with_level(log::LevelFilter::Debug) + .init(); + + // Register data renderer + register_specific_data_renderer(UuidDataRenderer {}); + + true +} diff --git a/rust/src/data_renderer.rs b/rust/src/data_renderer.rs new file mode 100644 index 0000000000..75e43f286a --- /dev/null +++ b/rust/src/data_renderer.rs @@ -0,0 +1,190 @@ +use binaryninjacore_sys::*; +use core::ffi; +use ffi::c_void; +use std::ptr::NonNull; + +use crate::binary_view::BinaryView; +use crate::disassembly::{DisassemblyTextLine, InstructionTextToken}; +use crate::types::Type; + +// NOTE the type_ inside the context can be both owned or borrowed, because +// this type only exist as a reference and is never created by itself (AKA +// don't have a *from_raw function, it don't need to worry about drop it. +#[repr(transparent)] +pub struct TypeContext { + handle: BNTypeContext, +} + +impl TypeContext { + pub fn type_(&self) -> &Type { + // debug!("TypeContext type_"); + // SAFETY Type and `*mut BNType` are transparent + unsafe { core::mem::transmute::<&*mut BNType, &Type>(&self.handle.type_) } + } + + pub fn offset(&self) -> usize { + // debug!("TypeContext offset"); + self.handle.offset + } +} + +pub trait CustomDataRenderer: Sized + Sync + Send + 'static { + fn is_valid_for_data( + &self, + view: &BinaryView, + addr: u64, + type_: &Type, + types: &[TypeContext], + ) -> bool; + fn lines_for_data( + &self, + view: &BinaryView, + addr: u64, + type_: &Type, + prefix: Vec, + width: usize, + types_ctx: &[TypeContext], + language: &str, + ) -> Vec; +} + +trait CustomDataRendererFFI: CustomDataRenderer { + unsafe extern "C" fn free_object_ffi(ctxt: *mut c_void) { + // debug!("free_object_ffi"); + drop(Box::from_raw(ctxt as *mut Self)) + } + + unsafe extern "C" fn is_valid_for_data_ffi( + ctxt: *mut c_void, + view: *mut BNBinaryView, + addr: u64, + type_: *mut BNType, + type_ctx: *mut BNTypeContext, + ctx_count: usize, + ) -> bool { + // debug!("is_valid_for_data_ffi"); + let ctxt = ctxt as *mut Self; + // SAFETY BNTypeContext and TypeContext are transparent + let types = core::slice::from_raw_parts(type_ctx as *mut TypeContext, ctx_count); + (*ctxt).is_valid_for_data( + &BinaryView::from_raw(view), + addr, + &Type::from_raw(type_), + types, + ) + } + + unsafe extern "C" fn get_lines_for_data_ffi( + ctxt: *mut c_void, + view: *mut BNBinaryView, + addr: u64, + type_: *mut BNType, + prefix: *const BNInstructionTextToken, + prefix_count: usize, + width: usize, + count: *mut usize, + type_ctx: *mut BNTypeContext, + ctx_count: usize, + language: *const ffi::c_char, + ) -> *mut BNDisassemblyTextLine { + // debug!("get_lines_for_data_ffi"); + let ctxt = ctxt as *mut Self; + // SAFETY BNTypeContext and TypeContext are transparent + let types = core::slice::from_raw_parts(type_ctx as *mut TypeContext, ctx_count); + let prefix = core::slice::from_raw_parts(prefix, prefix_count) + .iter() + .map(InstructionTextToken::from_raw) + .collect::>(); + let result = (*ctxt).lines_for_data( + &BinaryView::from_raw(view), + addr, + &Type::from_raw(type_), + prefix, + width, + types, + ffi::CStr::from_ptr(language).to_str().unwrap(), + ); + let result: Box<[BNDisassemblyTextLine]> = result + .into_iter() + .map(DisassemblyTextLine::into_raw) + .collect(); + *count = result.len(); + Box::leak(result).as_mut_ptr() + } + + unsafe extern "C" fn free_lines_ffi( + _ctx: *mut c_void, + lines: *mut BNDisassemblyTextLine, + count: usize, + ) { + // debug!("free_lines_ffi"); + let lines = Box::from_raw(core::slice::from_raw_parts_mut(lines, count)); + drop( + lines + .iter() + .map(DisassemblyTextLine::from_raw) + .collect::>(), + ); + } +} + +impl CustomDataRendererFFI for C {} + +pub struct CoreDataRenderer { + pub(crate) handle: NonNull, +} + +impl CoreDataRenderer { + pub(crate) unsafe fn from_raw(handle: NonNull) -> CoreDataRenderer { + Self { handle } + } +} + +fn create_custom_data_renderer( + renderer: T, +) -> (&'static mut T, CoreDataRenderer) { + let renderer = Box::leak(Box::new(renderer)); + let mut callbacks = BNCustomDataRenderer { + context: renderer as *mut _ as *mut c_void, + freeObject: Some(::free_object_ffi), + isValidForData: Some(::is_valid_for_data_ffi), + getLinesForData: Some(::get_lines_for_data_ffi), + freeLines: Some(::free_lines_ffi), + }; + let result = unsafe { BNCreateDataRenderer(&mut callbacks) }; + let core = unsafe { CoreDataRenderer::from_raw(NonNull::new(result).unwrap()) }; + (renderer, core) +} + +pub fn register_generic_data_renderer( + custom: T, +) -> (&'static mut T, CoreDataRenderer) { + let (renderer, core) = create_custom_data_renderer(custom); + // debug!("register_generic_data_renderer: core={:?}", core.handle); + let container = DataRendererContainer::get(); + unsafe { BNRegisterGenericDataRenderer(container.handle, core.handle.as_ptr()) } + (renderer, core) +} + +pub fn register_specific_data_renderer( + custom: C, +) -> (&'static mut C, CoreDataRenderer) { + let (renderer, core) = create_custom_data_renderer(custom); + // debug!("register_specific_data_renderer: core={:?}", core.handle); + let container = DataRendererContainer::get(); + unsafe { BNRegisterTypeSpecificDataRenderer(container.handle, core.handle.as_ptr()) } + (renderer, core) +} + +#[derive(Clone, Copy)] +struct DataRendererContainer { + pub(crate) handle: *mut BNDataRendererContainer, +} + +impl DataRendererContainer { + pub fn get() -> Self { + Self { + handle: unsafe { BNGetDataRendererContainer() }, + } + } +} diff --git a/rust/src/disassembly.rs b/rust/src/disassembly.rs index c1021197d0..76a45c373c 100644 --- a/rust/src/disassembly.rs +++ b/rust/src/disassembly.rs @@ -142,6 +142,14 @@ impl DisassemblyTextLine { ..Default::default() } } + + pub fn new_with_addr(tokens: Vec, addr: u64) -> Self { + Self { + address: addr, + tokens, + ..Default::default() + } + } } impl From<&str> for DisassemblyTextLine { @@ -493,13 +501,13 @@ pub enum InstructionTextTokenKind { hash: Option, }, CodeSymbol { - // TODO: Value of what? + // Target address of the symbol value: u64, // TODO: Size of what? size: usize, // TODO: Operand? }, DataSymbol { - // TODO: Value of what? + // Target address of the symbol value: u64, // TODO: Size of what? size: usize, // TODO: Operand? diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 3194ae14ca..e1d33e019a 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -39,6 +39,7 @@ pub mod component; pub mod confidence; pub mod custom_binary_view; pub mod data_buffer; +pub mod data_renderer; pub mod database; pub mod debuginfo; pub mod demangle; diff --git a/rust/tests/data_renderer.rs b/rust/tests/data_renderer.rs new file mode 100644 index 0000000000..a941f3693b --- /dev/null +++ b/rust/tests/data_renderer.rs @@ -0,0 +1,89 @@ +use binaryninja::binary_view::BinaryView; +use binaryninja::data_renderer::{ + register_specific_data_renderer, CustomDataRenderer, TypeContext, +}; +use binaryninja::disassembly::{ + DisassemblyTextLine, InstructionTextToken, InstructionTextTokenKind, +}; +use binaryninja::types::Type; + +#[test] +fn test_data_renderer_basic() { + struct StructRenderer {} + impl CustomDataRenderer for StructRenderer { + fn is_valid_for_data( + &self, + _view: &BinaryView, + _addr: u64, + type_: &Type, + _types: &[TypeContext], + ) -> bool { + type_.get_structure().is_some() + } + + fn lines_for_data( + &self, + _view: &BinaryView, + addr: u64, + type_: &Type, + _prefix: Vec, + width: usize, + _types_ctx: &[TypeContext], + _language: &str, + ) -> Vec { + let name = type_.registered_name().map(|name| name.name().to_string()); + let Some(type_) = type_.get_structure() else { + unreachable!(); + }; + + let mut output = vec![ + DisassemblyTextLine::new(vec![InstructionTextToken::new( + format!( + "Struct{}{} width {} or {width} {addr}", + name.as_ref().map(|_| " ").unwrap_or(""), + name.as_ref().map(String::as_str).unwrap_or(""), + type_.width() + ), + InstructionTextTokenKind::Comment { target: addr }, + )]), + DisassemblyTextLine::new(vec![InstructionTextToken::new( + "{", + InstructionTextTokenKind::Text, + )]), + ]; + let members = type_.members(); + let offset_size = + usize::try_from(members.last().map(|last| last.offset.ilog(16)).unwrap_or(0) + 3) + .unwrap(); + for member in members { + let line = [ + InstructionTextToken::new( + format!("{:#0width$x}", member.offset, width = offset_size), + InstructionTextTokenKind::StructOffset { + offset: member.offset, + type_names: vec![member.name.clone()], + }, + ), + InstructionTextToken::new("|", InstructionTextTokenKind::Text), + InstructionTextToken::new( + member.name.clone(), + InstructionTextTokenKind::FieldName { + offset: member.offset, + type_names: vec![member.name.clone()], + }, + ), + InstructionTextToken::new(",", InstructionTextTokenKind::Text), + ]; + output.push(DisassemblyTextLine::new(line.to_vec())); + } + output.push(DisassemblyTextLine::new(vec![InstructionTextToken::new( + "}", + InstructionTextTokenKind::Text, + )])); + output + } + } + + let _renderer = register_specific_data_renderer(StructRenderer {}); + // TODO render a Type +}