diff options
| author | LukBukkit <luk.bukkit@gmail.com> | 2025-04-24 16:20:32 +0000 |
|---|---|---|
| committer | Mason Reed <mason@vector35.com> | 2025-10-07 16:47:33 -0400 |
| commit | 64633f61f7b9e03be9437b5f4896bbe122f7a7a2 (patch) | |
| tree | 8fafed1395a8d2e29e5a6b8b7f7d7b8a2af2731a /rust | |
| parent | e6e4ebea5d0ea7842766c7e686e0dfd45cb54bb8 (diff) | |
[Rust] Implement custom data renderer API
Also adds an example plugin and misc rust fixes / documentation.
This is a continuation of https://github.com/Vector35/binaryninja-api/pull/6721
Co-authored-by: rbran <lgit@rubens.io>
Diffstat (limited to 'rust')
| -rw-r--r-- | rust/plugin_examples/data_renderer/Cargo.toml | 13 | ||||
| -rw-r--r-- | rust/plugin_examples/data_renderer/README.md | 63 | ||||
| -rw-r--r-- | rust/plugin_examples/data_renderer/build.rs | 25 | ||||
| -rw-r--r-- | rust/plugin_examples/data_renderer/src/lib.rs | 111 | ||||
| -rw-r--r-- | rust/src/data_renderer.rs | 242 | ||||
| -rw-r--r-- | rust/src/disassembly.rs | 16 | ||||
| -rw-r--r-- | rust/src/lib.rs | 1 | ||||
| -rw-r--r-- | rust/tests/data_renderer.rs | 122 |
8 files changed, 591 insertions, 2 deletions
diff --git a/rust/plugin_examples/data_renderer/Cargo.toml b/rust/plugin_examples/data_renderer/Cargo.toml new file mode 100644 index 00000000..7ac32dfd --- /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] +binaryninjacore-sys = { path = "../../binaryninjacore-sys" } +binaryninja = { path = "../.." } +uuid = "1.18.1" +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 00000000..794b93b9 --- /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 00000000..9006f16a --- /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 00000000..178402af --- /dev/null +++ b/rust/plugin_examples/data_renderer/src/lib.rs @@ -0,0 +1,111 @@ +use binaryninja::binary_view::{BinaryView, BinaryViewBase}; +use binaryninja::data_renderer::{ + register_data_renderer, CustomDataRenderer, RegistrationType, TypeContext, +}; +use binaryninja::disassembly::{ + DisassemblyTextLine, InstructionTextToken, InstructionTextTokenKind, +}; +use binaryninja::types::{Type, TypeClass}; +use uuid::Uuid; + +struct UuidDataRenderer {} + +impl CustomDataRenderer for UuidDataRenderer { + const REGISTRATION_TYPE: RegistrationType = RegistrationType::Specific; + + 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.ty().type_class() != TypeClass::NamedTypeReferenceClass { + continue; + } + + let Some(name_ref) = type_ctx.ty().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<InstructionTextToken>, + _width: usize, + _types_ctx: &[TypeContext], + _language: &str, + ) -> Vec<DisassemblyTextLine> { + 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_data_renderer(UuidDataRenderer {}); + + true +} diff --git a/rust/src/data_renderer.rs b/rust/src/data_renderer.rs new file mode 100644 index 00000000..6fa19b59 --- /dev/null +++ b/rust/src/data_renderer.rs @@ -0,0 +1,242 @@ +use binaryninjacore_sys::*; +use core::ffi; +use ffi::c_void; +use std::fmt::Debug; +use std::ptr::NonNull; + +use crate::binary_view::BinaryView; +use crate::disassembly::{DisassemblyTextLine, InstructionTextToken}; +use crate::rc::Array; +use crate::string::BnString; +use crate::types::Type; + +/// Registers a custom data renderer, this allows you to customize the representation of data variables. +pub fn register_data_renderer<C: CustomDataRenderer>( + custom: C, +) -> (&'static mut C, CoreDataRenderer) { + let renderer = Box::leak(Box::new(custom)); + let mut callbacks = BNCustomDataRenderer { + context: renderer as *mut _ as *mut c_void, + freeObject: Some(cb_free_object::<C>), + isValidForData: Some(cb_is_valid_for_data::<C>), + getLinesForData: Some(cb_get_lines_for_data::<C>), + freeLines: Some(cb_free_lines), + }; + let result = unsafe { BNCreateDataRenderer(&mut callbacks) }; + let core = unsafe { CoreDataRenderer::from_raw(NonNull::new(result).unwrap()) }; + let container = DataRendererContainer::get(); + match C::REGISTRATION_TYPE { + RegistrationType::Generic => container.register_data_renderer(&core), + RegistrationType::Specific => container.register_specific_data_renderer(&core), + } + (renderer, core) +} + +/// Renders the data at the given address using the registered data renderers, returning associated lines. +pub fn render_lines_for_data( + view: &BinaryView, + addr: u64, + type_: &Type, + prefix: Vec<InstructionTextToken>, + width: usize, + types_ctx: &[TypeContext], + language: Option<&str>, +) -> Vec<DisassemblyTextLine> { + let bn_prefix: Vec<BNInstructionTextToken> = prefix + .into_iter() + .map(InstructionTextToken::into_raw) + .collect(); + let bn_language = BnString::from(language.unwrap_or("")); + + let mut count: usize = 0; + let lines_ptr = unsafe { + BNRenderLinesForData( + view.handle, + addr, + type_.handle, + bn_prefix.as_ptr(), + bn_prefix.len(), + width, + &mut count as *mut usize, + types_ctx.as_ptr() as *mut BNTypeContext, + types_ctx.len(), + bn_language.as_ptr(), + ) + }; + + for token in bn_prefix { + InstructionTextToken::free_raw(token); + } + + let lines_arr: Array<DisassemblyTextLine> = unsafe { Array::new(lines_ptr, count, ()) }; + lines_arr.to_vec() +} + +#[derive(Clone, Copy)] +struct DataRendererContainer { + pub(crate) handle: *mut BNDataRendererContainer, +} + +impl DataRendererContainer { + pub fn get() -> Self { + Self { + handle: unsafe { BNGetDataRendererContainer() }, + } + } + + pub fn register_data_renderer(&self, renderer: &CoreDataRenderer) { + unsafe { BNRegisterGenericDataRenderer(self.handle, renderer.handle.as_ptr()) }; + } + + pub fn register_specific_data_renderer(&self, renderer: &CoreDataRenderer) { + unsafe { BNRegisterTypeSpecificDataRenderer(self.handle, renderer.handle.as_ptr()) }; + } +} + +/// Used by [`CustomDataRenderer`] to determine the priority of the renderer relative to other registered renderers. +pub enum RegistrationType { + Generic, + /// This data renderer wants to run before any generic data renderers. + /// + /// Use this if you want to take priority over rendering of specific types. + Specific, +} + +pub trait CustomDataRenderer: Sized + Sync + Send + 'static { + /// The registration type for the renderer really only determines the priority for the renderer. + /// + /// If you are overriding the behavior of a specific type, you should use [`RegistrationType::Specific`]. + const REGISTRATION_TYPE: RegistrationType; + + 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<InstructionTextToken>, + width: usize, + types_ctx: &[TypeContext], + language: &str, + ) -> Vec<DisassemblyTextLine>; +} + +pub struct CoreDataRenderer { + pub(crate) handle: NonNull<BNDataRenderer>, +} + +impl CoreDataRenderer { + pub(crate) unsafe fn from_raw(handle: NonNull<BNDataRenderer>) -> CoreDataRenderer { + Self { handle } + } +} + +/// Data renderers are recursive, so we keep track of observed types. +/// +/// This can be used to influence the rendering of structure fields and related nested types. +#[repr(transparent)] +pub struct TypeContext { + handle: BNTypeContext, +} + +impl TypeContext { + /// The [`Type`] in the context. + pub fn ty(&self) -> &Type { + // SAFETY Type and `*mut BNType` are transparent, and the type is expected to be valid for the lifetime of the context. + unsafe { core::mem::transmute::<&*mut BNType, &Type>(&self.handle.type_) } + } + + /// The offset with which the type is associated. + /// + /// The offset in many cases refers to a structure byte offset. + pub fn offset(&self) -> usize { + self.handle.offset + } +} + +impl Debug for TypeContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TypeContext") + .field("ty", &self.ty()) + .field("offset", &self.offset()) + .finish() + } +} + +unsafe extern "C" fn cb_free_object<C: CustomDataRenderer>(ctxt: *mut c_void) { + let _ = Box::from_raw(ctxt as *mut C); +} + +unsafe extern "C" fn cb_is_valid_for_data<C: CustomDataRenderer>( + ctxt: *mut c_void, + view: *mut BNBinaryView, + addr: u64, + type_: *mut BNType, + type_ctx: *mut BNTypeContext, + ctx_count: usize, +) -> bool { + let ctxt = ctxt as *mut C; + // 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 cb_get_lines_for_data<C: CustomDataRenderer>( + 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 { + let ctxt = ctxt as *mut C; + // 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::<Vec<_>>(); + 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 cb_free_lines( + _ctx: *mut c_void, + lines: *mut BNDisassemblyTextLine, + count: usize, +) { + let lines = Box::from_raw(core::slice::from_raw_parts_mut(lines, count)); + for line in lines { + let _ = DisassemblyTextLine::from_raw(&line); + } +} diff --git a/rust/src/disassembly.rs b/rust/src/disassembly.rs index ed9b0854..cd5a860d 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<InstructionTextToken>, addr: u64) -> Self { + Self { + address: addr, + tokens, + ..Default::default() + } + } } impl From<&str> for DisassemblyTextLine { @@ -308,6 +316,10 @@ impl InstructionTextToken { } } + /// Construct a new token **without** an associated address. + /// + /// You most likely want to call [`InstructionTextToken::new_with_address`], while also adjusting + /// the [`InstructionTextToken::expr_index`] field where applicable. pub fn new(text: impl Into<String>, kind: InstructionTextTokenKind) -> Self { Self { address: 0, @@ -493,13 +505,13 @@ pub enum InstructionTextTokenKind { hash: Option<u64>, }, 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 9d34f6bc..c9b0bb0a 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 00000000..fdf39697 --- /dev/null +++ b/rust/tests/data_renderer.rs @@ -0,0 +1,122 @@ +use binaryninja::binary_view::{BinaryView, BinaryViewExt}; +use binaryninja::data_renderer::{ + register_data_renderer, render_lines_for_data, CustomDataRenderer, RegistrationType, + TypeContext, +}; +use binaryninja::disassembly::{ + DisassemblyTextLine, InstructionTextToken, InstructionTextTokenKind, +}; +use binaryninja::headless::Session; +use binaryninja::types::Type; +use std::path::PathBuf; + +struct StructRenderer {} +impl CustomDataRenderer for StructRenderer { + const REGISTRATION_TYPE: RegistrationType = RegistrationType::Specific; + + 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<InstructionTextToken>, + width: usize, + _types_ctx: &[TypeContext], + _language: &str, + ) -> Vec<DisassemblyTextLine> { + 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 + } +} + +#[test] +fn test_data_renderer_basic() { + let _session = Session::new().expect("Failed to initialize session"); + let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap(); + let view = binaryninja::load(out_dir.join("atox.obj")).expect("Failed to create view"); + let _ = register_data_renderer(StructRenderer {}); + + // This will use all available data renderers, so we are also verifying that our custom renderer is being used. + let lines = render_lines_for_data( + &view, + 0x362e9, + &view.type_by_name("_ABC").unwrap(), + vec![], + 100, + &[], + None, + ); + + // TODO: This is not really checking all possible issues that could occur with round-tripping. + // TODO: But it is a good start to just make sure it visually is what we expect. + let lines_str = lines.iter().map(ToString::to_string).collect::<Vec<_>>(); + assert_eq!( + lines_str, + vec![ + "Struct _ABC width 12 or 100 221929", + "{", + "0x0|abcA,", + "0x4|abcB,", + "0x8|abcC,", + "}" + ] + ) +} |
