diff options
| author | Mason Reed <mason@vector35.com> | 2026-02-11 18:04:07 -0800 |
|---|---|---|
| committer | Mason Reed <35282038+emesare@users.noreply.github.com> | 2026-02-23 00:09:44 -0800 |
| commit | 37008b7fa16837d04c1658868646cad681cbe035 (patch) | |
| tree | 577c2b62ee47c78a5d31d11f2aa610e441f5c808 /plugins/bntl_utils/src/winmd.rs | |
| parent | 837f8590be80b7c98162e70e4f0c1814b83e9d7b (diff) | |
Add BNTL utility plugin
Allow users to easily create, diff, dump and validate type libraries
Supports the following formats:
- C header files (via core type parsers)
- Binary files (collects exported and imported functions)
- WinMD files (via `windows-metadata` crate)
- Existing type library files (for easy fixups)
- Apiset files (to resolve through forwarded windows dlls)
Can be invoked as a regular plugin via UI commands or via CLI.
Processing of type libraries inherently requires external linking, processing will automatically merge and deduplicate colliding type libraries so prefer to use inside a project or a directory and process all information (for a given platform) at once, rather than smaller invocations.
Diffstat (limited to 'plugins/bntl_utils/src/winmd.rs')
| -rw-r--r-- | plugins/bntl_utils/src/winmd.rs | 592 |
1 files changed, 592 insertions, 0 deletions
diff --git a/plugins/bntl_utils/src/winmd.rs b/plugins/bntl_utils/src/winmd.rs new file mode 100644 index 00000000..1cf99ab2 --- /dev/null +++ b/plugins/bntl_utils/src/winmd.rs @@ -0,0 +1,592 @@ +//! Import windows metadata types into a Binary Ninja type library. + +use std::collections::HashMap; +use std::num::NonZeroUsize; +use std::path::PathBuf; +use thiserror::Error; + +use binaryninja::architecture::Architecture; +use binaryninja::platform::Platform; +use binaryninja::qualified_name::QualifiedName; +use binaryninja::rc::Ref; +use binaryninja::types::{ + EnumerationBuilder, FunctionParameter, MemberAccess, MemberScope, NamedTypeReference, + NamedTypeReferenceClass, StructureBuilder, StructureType, Type, TypeBuilder, TypeLibrary, +}; + +use info::{LibraryName, MetadataFunctionInfo, MetadataInfo, MetadataTypeInfo, MetadataTypeKind}; + +pub mod info; +pub mod translate; + +#[derive(Error, Debug)] +pub enum ImportError { + #[error("no files were provided")] + NoFiles, + #[error("the type name '{0}' is not handled")] + UnhandledType(String), + #[error("failed to translate windows metadata")] + TransactionError(#[from] translate::TranslationError), + #[error("the type '{0}' has an unhandled size")] + UnhandledTypeSize(&'static str), +} + +#[derive(Debug)] +pub struct WindowsMetadataImporter { + info: MetadataInfo, + // TODO: If we can replace / add this with type libraries we can make multi-pass importer. + type_lookup: HashMap<(String, String), MetadataTypeInfo>, + address_size: usize, + integer_size: usize, +} + +impl WindowsMetadataImporter { + pub fn new() -> Self { + Self { + info: MetadataInfo::default(), + type_lookup: HashMap::new(), + address_size: 8, + integer_size: 8, + } + } + + #[allow(dead_code)] + pub fn new_with_info(info: MetadataInfo) -> Self { + let mut res = Self::new(); + res.info = info; + res.build_type_lookup(); + res + } + + pub fn with_files(mut self, paths: &[PathBuf]) -> Result<Self, ImportError> { + let mut files = Vec::new(); + for path in paths { + let file = windows_metadata::reader::File::read(path).expect("Failed to read file"); + files.push(file); + } + self.info = translate::WindowsMetadataTranslator::new().translate(files)?; + // We updated info, so we must rebuild the lookup table. + self.build_type_lookup(); + Ok(self) + } + + pub fn with_platform(mut self, platform: &Platform) -> Self { + // TODO: platform.address_size() + self.address_size = platform.arch().address_size(); + self.integer_size = platform.arch().default_integer_size(); + self + } + + /// Build the lookup table for us to use when referencing types. + /// + /// Should be called anytime we update `self.info`. + fn build_type_lookup(&mut self) { + for ty in &self.info.types { + if let Some(_existing) = self + .type_lookup + .insert((ty.namespace.clone(), ty.name.clone()), ty.clone()) + { + tracing::warn!( + "Duplicate type name '{}' found when building type lookup", + ty.name + ); + } + } + } + + pub fn import(&self, platform: &Platform) -> Result<Vec<Ref<TypeLibrary>>, ImportError> { + // TODO: We need to take all of these enums and figure out where to put them. + let mut test = self.info.clone(); + let constant_enums = test.create_constant_enums(); + // TODO: Creating zero width enums + test.types.extend(constant_enums); + let partitioned_info = test.partitioned(); + + let mut type_libs = Vec::new(); + for (name, info) in partitioned_info.libraries { + let type_lib_name = match name { + LibraryName::Module(module_name) => module_name.clone(), + LibraryName::Namespace(ns_name) => { + // TODO: We might need to do something different for namespaced type libraries in the future. + ns_name.clone() + } + }; + let til = TypeLibrary::new(platform.arch(), &type_lib_name); + til.add_platform(platform); + til.set_dependency_name(&type_lib_name); + for ty in &info.metadata.types { + self.import_type(&til, &ty)?; + } + for func in &info.metadata.functions { + self.import_function(&til, &func)?; + } + for (name, library_name) in &info.external_references { + let qualified_name = QualifiedName::from(name.clone()); + match library_name { + LibraryName::Namespace(source) => { + // TODO: We might need to do something different for namespaced type libraries in the future. + til.add_type_source(qualified_name, source); + } + LibraryName::Module(source) => { + til.add_type_source(qualified_name, source); + } + } + } + + type_libs.push(til); + } + + Ok(type_libs) + } + + pub fn import_function( + &self, + til: &TypeLibrary, + func: &MetadataFunctionInfo, + ) -> Result<(), ImportError> { + // TODO: Handle ordinals? Ordinals exist in binaries that need to be parsed, maybe we + // TODO: make another handler for that + let qualified_name = QualifiedName::from(func.name.clone()); + let ty = self.convert_type_kind(&func.ty)?; + til.add_named_object(qualified_name, &ty); + Ok(()) + } + + pub fn import_type( + &self, + til: &TypeLibrary, + type_info: &MetadataTypeInfo, + ) -> Result<(), ImportError> { + let qualified_name = QualifiedName::from(type_info.name.clone()); + let ty = self.convert_type_kind(&type_info.kind)?; + til.add_named_type(qualified_name, &ty); + Ok(()) + } + + pub fn convert_type_kind(&self, kind: &MetadataTypeKind) -> Result<Ref<Type>, ImportError> { + match kind { + MetadataTypeKind::Void => Ok(Type::void()), + MetadataTypeKind::Bool { size: None } => Ok(Type::bool()), + MetadataTypeKind::Bool { size: Some(size) } => { + Ok(TypeBuilder::bool().set_width(*size).finalize()) + } + MetadataTypeKind::Integer { size, is_signed } => { + Ok(Type::int(size.unwrap_or(self.integer_size), *is_signed)) + } + MetadataTypeKind::Character { size: 1 } => Ok(Type::int(1, true)), + MetadataTypeKind::Character { size } => Ok(Type::wide_char(*size)), + MetadataTypeKind::Float { size } => Ok(Type::float(*size)), + MetadataTypeKind::Pointer { + is_const, + is_pointee_const: _is_pointee_const, + target, + } => { + let target_ty = self.convert_type_kind(target)?; + Ok(Type::pointer_of_width( + &target_ty, + self.address_size, + *is_const, + false, + None, + )) + } + MetadataTypeKind::Array { element, count } => { + let element_ty = self.convert_type_kind(element)?; + Ok(Type::array(&element_ty, *count as u64)) + } + MetadataTypeKind::Struct { fields, is_packed } => { + let mut structure = StructureBuilder::new(); + // Current offset in bytes + let mut current_byte_offset = 0usize; + + // TODO: Change how this operates now that we have an is_packed flag. + // Used to add tail padding to satisfy alignment requirements. + let mut max_alignment = 0usize; + // We need to look ahead to figure out when bitfields end and adjust current_byte_offset accordingly. + let mut field_iter = fields.iter().peekable(); + while let Some(field) = field_iter.next() { + let field_ty = self.convert_type_kind(&field.ty)?; + let field_size = self.type_kind_size(&field.ty)?; + let field_alignment = self.type_kind_alignment(&field.ty)?; + max_alignment = max_alignment.max(field_alignment); + if let Some((bit_pos, bit_width)) = field.bitfield { + let current_bit_offset = current_byte_offset * 8; + let field_bit_offset = current_bit_offset + bit_pos as usize; + // TODO: member access and member scope have definitions inside winmd we can use. + structure.insert_bitwise( + &field_ty, + &field.name, + field_bit_offset as u64, + Some(bit_width), + false, + MemberAccess::PublicAccess, + MemberScope::NoScope, + ); + + if let Some(next_field) = field_iter.peek() { + if next_field.bitfield.is_some() { + // Continue as if we are in the same storage unit (no alignment) + current_byte_offset = (current_bit_offset + bit_width as usize) / 8; + } else { + // Find the start of the storage unit. + // if we are at byte 1 of u32 (align 4), storage starts at 0. + // if we are at byte 5 of u32 (align 4), storage starts at 4. + let storage_start = + (current_byte_offset / field_alignment) * field_alignment; + // Jump to the end of that storage unit. + current_byte_offset = storage_start + field_size; + } + } + } else { + // Align the field placement based on the current field alignment. + let aligned_current_offset = + align_up(current_byte_offset as u64, field_alignment as u64); + structure.insert( + &field_ty, + &field.name, + aligned_current_offset, + false, + MemberAccess::PublicAccess, + MemberScope::NoScope, + ); + current_byte_offset = aligned_current_offset as usize + field_size; + } + } + structure.alignment(max_alignment); + + // TODO: Only add tail padding if we are not packed? I think we still need to do more. + if *is_packed { + structure.packed(true); + } else { + let total_size = align_up(current_byte_offset as u64, max_alignment as u64); + structure.width(total_size); + } + + Ok(Type::structure(&structure.finalize())) + } + MetadataTypeKind::Enum { ty, variants } => { + // NOTE: A void type may be returned by synthetic constant enums, which is why we + // do not error when there is a zero width enum. + let enum_ty = self.convert_type_kind(ty)?; + let mut builder = EnumerationBuilder::new(); + for (name, value) in variants { + builder.insert(name, *value); + } + Ok(Type::enumeration( + &builder.finalize(), + NonZeroUsize::new(enum_ty.width() as usize) + .unwrap_or(NonZeroUsize::new(self.integer_size).unwrap()), + enum_ty.is_signed().contents, + )) + } + MetadataTypeKind::Function { + params, + return_type, + is_vararg, + } => { + let return_ty = self.convert_type_kind(return_type)?; + let mut bn_params = Vec::new(); + for param in params { + let param_ty = self.convert_type_kind(¶m.ty)?; + bn_params.push(FunctionParameter::new(param_ty, param.name.clone(), None)); + } + Ok(Type::function(&return_ty, bn_params, *is_vararg)) + } + MetadataTypeKind::Reference { name, namespace } => { + // We are required to set the ID here since type libraries seem to only look up through + // the ID, and never fall back to name lookup. This is strange considering you must also + // set the types source to the given library, which seems counterintuitive. + // TODO: Add kind to ntr. + let ntr = NamedTypeReference::new_with_id( + NamedTypeReferenceClass::TypedefNamedTypeClass, + &format!("{}::{}", namespace, name), + name, + ); + // TODO: Type alignment? + let type_size = self.type_kind_size(kind)?; + Ok(TypeBuilder::named_type(&ntr) + .set_width(type_size) + .set_alignment(type_size) + .finalize()) + } + MetadataTypeKind::Union { fields } => { + let mut union = StructureBuilder::new(); + union.structure_type(StructureType::UnionStructureType); + + let mut max_alignment = 0usize; + // We need to look ahead to figure out when bitfields end and adjust current_byte_offset accordingly. + let mut field_iter = fields.iter().peekable(); + while let Some(field) = field_iter.next() { + let field_ty = self.convert_type_kind(&field.ty)?; + let field_alignment = self.type_kind_alignment(&field.ty)?; + max_alignment = max_alignment.max(field_alignment); + union.insert( + &field_ty, + &field.name, + 0, + false, + MemberAccess::PublicAccess, + MemberScope::NoScope, + ); + } + + union.alignment(max_alignment); + Ok(Type::structure(&union.finalize())) + } + } + } + + /// Retrieve the size of a type kind in bytes, references to types will be looked up + /// such that we can determine the size of structures with references as fields. + pub fn type_kind_size(&self, kind: &MetadataTypeKind) -> Result<usize, ImportError> { + match kind { + MetadataTypeKind::Void => Ok(0), + MetadataTypeKind::Bool { size } => Ok(size.unwrap_or(self.integer_size)), + MetadataTypeKind::Integer { size, .. } => Ok(size.unwrap_or(self.integer_size)), + MetadataTypeKind::Character { size } => Ok(*size), + MetadataTypeKind::Float { size } => Ok(*size), + MetadataTypeKind::Pointer { .. } => Ok(self.address_size), + MetadataTypeKind::Array { element, count } => { + let elem_size = self.type_kind_size(element)?; + Ok(elem_size * *count) + } + MetadataTypeKind::Struct { fields, is_packed } => { + let mut current_offset = 0usize; + let mut max_struct_alignment = 1usize; + for field in fields { + let field_size = self.type_kind_size(&field.ty)?; + let field_alignment = if *is_packed { + 1 + } else { + self.type_kind_alignment(&field.ty)? + }; + max_struct_alignment = max_struct_alignment.max(field_alignment); + current_offset = + align_up(current_offset as u64, field_alignment as u64) as usize; + current_offset += field_size; + } + // Tail padding is only needed if not packed. + let final_alignment = if *is_packed { 1 } else { max_struct_alignment }; + let total_size = align_up(current_offset as u64, final_alignment as u64) as usize; + Ok(total_size) + } + MetadataTypeKind::Union { fields } => { + let mut largest_field_size = 0usize; + for field in fields { + let field_size = self.type_kind_size(&field.ty)?; + largest_field_size = largest_field_size.max(field_size); + } + Ok(largest_field_size) + } + MetadataTypeKind::Enum { ty, .. } => self.type_kind_size(ty), + MetadataTypeKind::Function { .. } => Err(ImportError::UnhandledTypeSize( + "Function types are not sized", + )), + MetadataTypeKind::Reference { name, namespace } => { + // Look up the type and return its size. + let Some(ty_info) = self.type_lookup.get(&(namespace.clone(), name.clone())) else { + // This should really only happen if we did not specify all the required winmd files. + // tracing::error!( + // "Failed to find type '{}' when looking up type size for reference", + // name + // ); + return Ok(1); + }; + self.type_kind_size(&ty_info.kind) + } + } + } + + pub fn type_kind_alignment(&self, kind: &MetadataTypeKind) -> Result<usize, ImportError> { + match kind { + MetadataTypeKind::Bool { size: None } => Ok(1), + MetadataTypeKind::Bool { size } => Ok(size.unwrap_or(self.integer_size)), + // TODO: Clean this stuff up. + MetadataTypeKind::Character { size } => Ok(*size), + MetadataTypeKind::Integer { size: Some(1), .. } => Ok(1), + MetadataTypeKind::Integer { size: Some(2), .. } => Ok(2), + MetadataTypeKind::Integer { size: Some(4), .. } => Ok(4), + MetadataTypeKind::Integer { size: Some(8), .. } + | MetadataTypeKind::Float { size: 8 } + | MetadataTypeKind::Pointer { .. } => Ok(self.address_size), // 8 on x64 + MetadataTypeKind::Array { element, .. } => self.type_kind_alignment(element), + MetadataTypeKind::Struct { fields, is_packed } => { + if *is_packed { + return Ok(1); + } + let mut max_align = 1usize; + for field in fields { + max_align = max_align.max(self.type_kind_alignment(&field.ty)?); + } + Ok(max_align) + } + MetadataTypeKind::Union { fields } => { + let mut max_align = 1usize; + for field in fields { + max_align = max_align.max(self.type_kind_alignment(&field.ty)?); + } + Ok(max_align) + } + MetadataTypeKind::Reference { name, namespace } => { + let Some(ty_info) = self.type_lookup.get(&(namespace.clone(), name.clone())) else { + // TODO: Failed to find it in local type lookup, try type libraries? + // tracing::error!( + // "Failed to find type '{}' when looking up type alignment for reference", + // name + // ); + return Ok(4); + }; + self.type_kind_alignment(&ty_info.kind) + } + _ => Ok(4), + } + } +} + +// Aligns an offset up to the nearest multiple of `align`. +fn align_up(offset: u64, align: u64) -> u64 { + if align == 0 { + return offset; + } + let mask = align - 1; + (offset + mask) & !mask +} + +#[cfg(test)] +mod tests { + use super::info::{ + MetadataFieldInfo, MetadataImportInfo, MetadataImportMethod, MetadataModuleInfo, + }; + use super::*; + use binaryninja::architecture::CoreArchitecture; + use binaryninja::types::TypeClass; + + #[test] + fn test_import_type() { + // We must initialize binary ninja to access architectures. + let _session = binaryninja::headless::Session::new().expect("Failed to create session"); + + let mut info = MetadataInfo::default(); + info.functions = vec![MetadataFunctionInfo { + name: "MyFunction".to_string(), + ty: MetadataTypeKind::Function { + params: vec![], + return_type: Box::new(MetadataTypeKind::Void), + is_vararg: false, + }, + namespace: "Win32.Test".to_string(), + import_info: Some(MetadataImportInfo { + method: MetadataImportMethod::ByName("MyFunction".to_string()), + module: MetadataModuleInfo { + name: "TestModule.dll".to_string(), + }, + }), + }]; + info.types = vec![ + MetadataTypeInfo { + name: "Bar".to_string(), + kind: MetadataTypeKind::Integer { + size: Some(4), + is_signed: true, + }, + namespace: "Win32.Test".to_string(), + }, + MetadataTypeInfo { + name: "TestType".to_string(), + kind: MetadataTypeKind::Struct { + fields: vec![ + MetadataFieldInfo { + name: "field1".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(4), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + // TODO: Add more fields to verify bitfields, and const fields. + MetadataFieldInfo { + name: "field2_0".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(4), + is_signed: true, + }, + is_const: true, + bitfield: Some((0, 1)), + }, + MetadataFieldInfo { + name: "field2_1".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(4), + is_signed: true, + }, + is_const: true, + bitfield: Some((1, 1)), + }, + MetadataFieldInfo { + name: "field3".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(2), + is_signed: true, + }, + is_const: true, + bitfield: None, + }, + MetadataFieldInfo { + name: "field4".to_string(), + ty: MetadataTypeKind::Pointer { + is_pointee_const: false, + is_const: false, + target: Box::new(MetadataTypeKind::Reference { + namespace: "Win32.Test".to_string(), + name: "Bar".to_string(), + }), + }, + is_const: false, + bitfield: None, + }, + ], + is_packed: false, + }, + namespace: "Foo".to_string(), + }, + ]; + let importer = WindowsMetadataImporter::new_with_info(info); + let x86 = CoreArchitecture::by_name("x86").expect("No x86 architecture"); + let platform = Platform::by_name("windows-x86").expect("No windows-x86 platform"); + let type_libraries = importer.import(&platform).expect("Failed to import types"); + assert_eq!(type_libraries.len(), 1); + let til = type_libraries.first().expect("No type libraries"); + assert_eq!(til.named_types().len(), 1); + let first_ty = til + .named_types() + .iter() + .next() + .expect("No types in library"); + assert_eq!(first_ty.name.to_string(), "TestType"); + assert_eq!(first_ty.ty.type_class(), TypeClass::StructureTypeClass); + let first_ty_struct = first_ty + .ty + .get_structure() + .expect("Type is not a structure"); + assert_eq!(first_ty_struct.members().len(), 5); + let mut structure_fields = first_ty_struct.members().iter(); + + for member in first_ty_struct.members() { + println!(" +{}: {}", member.offset, member.name.to_string()) + } + + // TODO: Finish this! + assert!(false); + // let first_member = structure_fields.next().expect("No fields in structure"); + // assert_eq!(first_member.name.to_string(), "field1"); + // assert_eq!(first_member.ty, TypeClass::IntegerTypeClass); + // let second_member = structure_fields.next().expect("No fields in structure"); + // assert_eq!(second_member.name.to_string(), "field2_0"); + // assert_eq!(second_member.type.type_class(), TypeClass::IntegerTypeClass); + // let third_member = structure_fields.next().expect("No fields in structure"); + // assert_eq!(third_member.name.to_string(), "field2_1"); + // assert_eq!(third_member.type.type_class(), TypeClass::IntegerTypeClass); + // let fourth_member = structure_fields.next().expect("No fields in structure"); + } +} |
