diff --git a/plugins/warp/fixtures/bin/test.exe b/plugins/warp/fixtures/bin/test.exe new file mode 100644 index 0000000000..8ee42be209 Binary files /dev/null and b/plugins/warp/fixtures/bin/test.exe differ diff --git a/plugins/warp/fixtures/bin/test_load.warp b/plugins/warp/fixtures/bin/test_load.warp new file mode 100644 index 0000000000..8e9cbfc3fe Binary files /dev/null and b/plugins/warp/fixtures/bin/test_load.warp differ diff --git a/plugins/warp/src/convert/types.rs b/plugins/warp/src/convert/types.rs index 8421eb5eff..21a86fb30c 100644 --- a/plugins/warp/src/convert/types.rs +++ b/plugins/warp/src/convert/types.rs @@ -6,7 +6,6 @@ use binaryninja::calling_convention::CoreCallingConvention as BNCallingConventio use binaryninja::confidence::Conf as BNConf; use binaryninja::confidence::MAX_CONFIDENCE; use binaryninja::rc::Ref as BNRef; -use binaryninja::types::BaseStructure as BNBaseStructure; use binaryninja::types::EnumerationBuilder as BNEnumerationBuilder; use binaryninja::types::FunctionParameter as BNFunctionParameter; use binaryninja::types::MemberAccess as BNMemberAccess; @@ -17,6 +16,7 @@ use binaryninja::types::StructureMember as BNStructureMember; use binaryninja::types::StructureType as BNStructureType; use binaryninja::types::Type as BNType; use binaryninja::types::TypeClass as BNTypeClass; +use binaryninja::types::{BaseStructure as BNBaseStructure, StructureType}; use binaryninja::types::{NamedTypeReference, NamedTypeReferenceClass}; use std::collections::HashSet; use warp::r#type::class::array::ArrayModifiers; @@ -25,7 +25,7 @@ use warp::r#type::class::structure::StructureMemberModifiers; use warp::r#type::class::{ ArrayClass, BooleanClass, CallingConvention, CharacterClass, EnumerationClass, EnumerationMember, FloatClass, FunctionClass, FunctionMember, IntegerClass, PointerClass, - ReferrerClass, StructureClass, StructureMember, TypeClass, + ReferrerClass, StructureClass, StructureMember, TypeClass, UnionClass, UnionMember, }; use warp::r#type::{Type, TypeModifiers}; @@ -65,58 +65,117 @@ pub fn from_bn_type_internal( }; TypeClass::Float(float_class) } - // TODO: Union????? BNTypeClass::StructureTypeClass => { let raw_struct = raw_ty.get_structure().unwrap(); + match raw_struct.structure_type() { + StructureType::ClassStructureType | StructureType::StructStructureType => { + let mut members = raw_struct + .members() + .into_iter() + .map(|raw_member| { + let bit_offset = bytes_to_bits(raw_member.offset) + + raw_member.bit_position.unwrap_or(0) as u64; + let mut modifiers = StructureMemberModifiers::empty(); + // If this member is not public, mark it as internal. + modifiers.set( + StructureMemberModifiers::Internal, + !matches!(raw_member.access, BNMemberAccess::PublicAccess), + ); + let mut member_ty = from_bn_type_internal( + view, + visited_refs, + &raw_member.ty.contents, + raw_member.ty.confidence, + ); - let mut members = raw_struct - .members() - .into_iter() - .map(|raw_member| { - let bit_offset = bytes_to_bits(raw_member.offset); - let mut modifiers = StructureMemberModifiers::empty(); - // If this member is not public mark it as internal. - modifiers.set( - StructureMemberModifiers::Internal, - !matches!(raw_member.access, BNMemberAccess::PublicAccess), - ); - StructureMember { - name: Some(raw_member.name), - offset: bit_offset, - ty: Box::new(from_bn_type_internal( - view, - visited_refs, - &raw_member.ty.contents, - raw_member.ty.confidence, - )), - modifiers, + // TODO: Handle this directly in from_bn_type_internal? bit_width: Option param + // Adjust type class widths when dealing with bitfields. + if let Some(member_bit_width) = raw_member.bit_width { + match &mut member_ty.class { + TypeClass::Boolean(c) => { + c.width = Some(member_bit_width as u16); + } + TypeClass::Integer(c) => { + c.width = Some(member_bit_width as u16); + } + TypeClass::Character(c) => { + c.width = Some(member_bit_width as u16); + } + TypeClass::Float(c) => { + c.width = Some(member_bit_width as u16); + } + TypeClass::Pointer(c) => { + c.width = Some(member_bit_width as u16); + } + _ => {} + } + } + + StructureMember { + name: Some(raw_member.name), + offset: bit_offset, + ty: Box::new(member_ty), + modifiers, + } + }) + .collect::>(); + + // Add base structures as flattened members + let base_to_member_iter = + raw_struct.base_structures().into_iter().map(|base_struct| { + let bit_offset = bytes_to_bits(base_struct.offset); + let mut modifiers = StructureMemberModifiers::empty(); + modifiers.set(StructureMemberModifiers::Flattened, true); + let base_struct_ty = from_bn_type_internal( + view, + visited_refs, + &BNType::named_type(&base_struct.ty), + MAX_CONFIDENCE, + ); + StructureMember { + name: base_struct_ty.name.to_owned(), + offset: bit_offset, + ty: Box::new(base_struct_ty), + modifiers, + } + }); + members.extend(base_to_member_iter); + + // Add padding to the end of the struct if needed. + let mut struct_class = StructureClass::new(members); + let tail_padding_size = + raw_ty_bit_width.saturating_sub(struct_class.size().unwrap_or(0)); + if tail_padding_size > 0 { + let padding_member = StructureMember { + name: None, + offset: raw_ty_bit_width, + ty: Box::new(padding_ty(tail_padding_size)), + modifiers: StructureMemberModifiers::Internal, + }; + struct_class.members.push(padding_member); } - }) - .collect::>(); - // Add base structures as flattened members - let base_to_member_iter = raw_struct.base_structures().into_iter().map(|base_struct| { - let bit_offset = bytes_to_bits(base_struct.offset); - let mut modifiers = StructureMemberModifiers::empty(); - modifiers.set(StructureMemberModifiers::Flattened, true); - let base_struct_ty = from_bn_type_internal( - view, - visited_refs, - &BNType::named_type(&base_struct.ty), - MAX_CONFIDENCE, - ); - StructureMember { - name: base_struct_ty.name.to_owned(), - offset: bit_offset, - ty: Box::new(base_struct_ty), - modifiers, + TypeClass::Structure(struct_class) } - }); - members.extend(base_to_member_iter); - - // TODO: Check if union - let struct_class = StructureClass::new(members); - TypeClass::Structure(struct_class) + StructureType::UnionStructureType => { + let members = raw_struct + .members() + .into_iter() + .map(|raw_member| { + let member_name = raw_member.name.to_owned(); + let member_ty = from_bn_type_internal( + view, + visited_refs, + &raw_member.ty.contents, + raw_member.ty.confidence, + ); + UnionMember::new(member_name, member_ty) + }) + .collect(); + let union_class = UnionClass::new(members); + TypeClass::Union(union_class) + } + } } BNTypeClass::EnumerationTypeClass => { let raw_enum = raw_ty.get_enumeration().unwrap(); @@ -321,16 +380,26 @@ pub fn to_bn_type(arch: Option, ty: &Type) -> BNRef let mut base_structs: Vec = Vec::new(); for member in &c.members { let member_type = BNConf::new(to_bn_type(arch, &member.ty), u8::MAX); - let member_name = member.name.to_owned().unwrap_or("field_OFFSET".into()); - let member_offset = bits_to_bytes(member.offset); - let member_access = if member + let member_offset_in_bytes = bits_to_bytes(member.offset); + let member_name = member + .name + .to_owned() + .unwrap_or_else(|| format!("field_{}", member_offset_in_bytes)); + let is_member_internal = member .modifiers - .contains(StructureMemberModifiers::Internal) - { + .contains(StructureMemberModifiers::Internal); + let member_access = if is_member_internal { BNMemberAccess::PrivateAccess } else { BNMemberAccess::PublicAccess }; + + // Member has no name, is private; we consider this padding, don't create a member for it. + if is_member_internal && member.name.is_none() { + builder.width(member_offset_in_bytes + member_type.contents.width()); + continue; + } + // TODO: Member scope let member_scope = BNMemberScope::NoScope; if member @@ -356,7 +425,7 @@ pub fn to_bn_type(arch: Option, ty: &Type) -> BNRef }; base_structs.push(BNBaseStructure::new( base_struct_ntr, - member_offset, + member_offset_in_bytes, member.ty.size().unwrap_or(0), )) } @@ -369,11 +438,16 @@ pub fn to_bn_type(arch: Option, ty: &Type) -> BNRef } } } else { + let mut member_bit_width = member.ty.size().unwrap_or(8); + if member_bit_width % 8 == 0 { + member_bit_width = 0; + } builder.insert_member( - BNStructureMember::new( + BNStructureMember::new_bitfield( member_type, member_name, - member_offset, + member.offset, + member_bit_width as u8, member_access, member_scope, ), @@ -483,6 +557,22 @@ pub fn to_bn_type(arch: Option, ty: &Type) -> BNRef } } +/// Return a new type to represent padding. +fn padding_ty(size: u64) -> Type { + let size_in_bytes = (size + 7) / 8; + let byte_class = IntegerClass::builder().width(8).signed(false).build(); + let byte_ty = Type::builder::() + .class(TypeClass::Integer(byte_class)) + .build(); + let array_class = ArrayClass::builder() + .length(size_in_bytes / 8) + .member_type(byte_ty) + .build(); + Type::builder::() + .class(TypeClass::Array(array_class)) + .build() +} + #[cfg(test)] mod tests { use super::*; diff --git a/plugins/warp/src/matcher.rs b/plugins/warp/src/matcher.rs index f96128e5a5..a7e419bf90 100644 --- a/plugins/warp/src/matcher.rs +++ b/plugins/warp/src/matcher.rs @@ -10,6 +10,7 @@ use std::cmp::Ordering; use std::collections::HashSet; use std::hash::Hash; use warp::r#type::class::TypeClass; +use warp::r#type::guid::TypeGUID; use warp::r#type::Type; use warp::signature::function::Function; @@ -98,6 +99,37 @@ impl Matcher { } } + pub fn add_function_types_to_view( + &self, + container: &dyn Container, + possible_sources: &[SourceId], + view: &BinaryView, + arch: A, + function: &Function, + ) where + Self: Sized, + { + if let Some(matched_func_ty) = &function.ty { + // NOTE: We only need one source with the guid, types with the same guid + // should be identical across sources, and types should be referable across + // sources, otherwise we will end up with a lot of duplicate types. + // TODO: Add a special type reference system for referring to types in a different source. + // TODO: If a child type is in a source other than the function type this will + // TODO: Fail to work, so we are enumerating all sources here, this is just + // TODO: so annoying and a bunch of wasted effort. + for source in possible_sources { + self.add_type_to_view(container, source, view, arch, matched_func_ty); + } + } + for variable in &function.variables { + if let Some(var_ty) = &variable.ty { + for source in possible_sources { + self.add_type_to_view(container, source, view, arch, var_ty) + } + } + } + } + // TODO: I would really like for WARP types to be added in a seperate type container, so that we don't // TODO: just add them as system or user types. pub fn add_type_to_view( @@ -248,6 +280,14 @@ impl Matcher { // Adds the ref'd type to the view. match (c.guid, &c.name, resolved_ty) { (Some(guid), Some(name), Some(ref_ty)) => { + inner_add_type_to_view( + container, + source, + view, + arch, + visited_refs, + &ref_ty, + ); view.define_auto_type_with_id( name, &guid.to_string(), @@ -275,10 +315,10 @@ impl Matcher { | TypeClass::Float(_) => {} } - // TODO: Some refs likely need to ommitted because they are just that, refs to another type. - // let guid = TypeGUID::from(ty); - // let name = ty.name.clone().unwrap_or(guid.to_string()); - // view.define_auto_type_with_id(name, &guid.to_string(), &to_bn_type(arch, ty)); + if let Some(name) = ty.name.clone() { + let guid = TypeGUID::from(ty); + view.define_auto_type_with_id(name, &guid.to_string(), &to_bn_type(Some(arch), ty)); + } } inner_add_type_to_view(container, source, view, arch, &mut HashSet::new(), ty) } diff --git a/plugins/warp/src/plugin/workflow.rs b/plugins/warp/src/plugin/workflow.rs index 1f4ef1016f..105a9d133f 100644 --- a/plugins/warp/src/plugin/workflow.rs +++ b/plugins/warp/src/plugin/workflow.rs @@ -196,6 +196,18 @@ pub fn run_matcher(view: &BinaryView) { // match if we have not already done so in a previous round. // TODO: What if the new round changes the matched function metadata? Unlikely but possible. if matcher_results.insert(function.start()) { + // Add the referenced types to the view. + // TODO: This should ideally be within insert_cached_function_match, right now + // TODO: we have this code here that needs to be duplicated when applying a function + // TODO: manually from the C++ UI code. + matcher.add_function_types_to_view( + container, + &sources, + view, + function.arch(), + matched_function, + ); + // 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)); @@ -301,71 +313,83 @@ pub fn run_fetcher(view: &BinaryView) { background_task.finish(); } -pub fn insert_workflow() -> Result<(), ()> { - // TODO: Note: because of symbol persistence function symbol is applied in `insert_cached_function_match`. - // TODO: Comments are also applied there, they are "user" like, persisted and make undo actions. - // "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) { - // core.function.propagateAnalysis will assign user type info to auto, so we must not apply - // otherwise we will wipe over user type info. - if !function.has_user_type() { - if let Some(func_ty) = &matched_function.ty { - function.set_auto_type(&to_bn_type(Some(function.arch()), func_ty)); - } else if !function.has_explicitly_defined_type() { - // Attempt to retrieve the type information from the named platform functions. - // NOTE: We check `has_explicitly_defined_type` because after applying imported type - // information, that flag will be set, allowing us to avoid applying it again. - let bn_symbol = - to_bn_symbol_at_address(&view, &matched_function.symbol, function.start()); - function.apply_imported_types(&bn_symbol, None); - } +// TODO: Note: because of symbol persistence function symbol is applied in `insert_cached_function_match`. +// TODO: Comments are also applied there, they are "user" like, persisted and make undo actions. +pub fn run_applier(ctx: &AnalysisContext) { + let view = ctx.view(); + let function = ctx.function(); + let func_start = function.start(); + if let Some(matched_function) = try_cached_function_match(&function) { + // core.function.propagateAnalysis will assign user type info to auto, so we must not apply + // otherwise we will wipe over user type info. + if !function.has_user_type() { + if let Some(func_ty) = &matched_function.ty { + function.set_auto_type(&to_bn_type(Some(function.arch()), func_ty)); + } else if !function.has_explicitly_defined_type() { + // Attempt to retrieve the type information from the named platform functions. + // NOTE: We check `has_explicitly_defined_type` because after applying imported type + // information, that flag will be set, allowing us to avoid applying it again. + let bn_symbol = + to_bn_symbol_at_address(&view, &matched_function.symbol, func_start); + function.apply_imported_types(&bn_symbol, None); } - if let Some(mlil) = ctx.mlil_function() { - for variable in matched_function.variables { - let decl_addr = ((function.start() as i64) + variable.offset) as u64; - if let Some(decl_instr) = mlil.instruction_at(decl_addr) { - let decl_var = match variable.location { - Location::Register(RegisterLocation { id, .. }) => { - decl_instr.variable_for_register_after(RegisterId(id as u32)) - } - Location::Stack(StackLocation { offset, .. }) => { - decl_instr.variable_for_stack_location_after(offset) - } - }; - if function.is_var_user_defined(&decl_var) { - // Internally, analysis will just assign user vars to auto vars and consult only that. - // So we must skip if there is a user-defined var at the decl. + } + + if let Some(mlil) = ctx.mlil_function() { + // Apply variable information to the function. + for var in matched_function.variables { + let Some(decl_addr) = func_start.checked_add_signed(var.offset) else { + tracing::warn!( + "Variable offset {} overflowed for function {:#x}", + var.offset, + func_start + ); + continue; + }; + let Some(decl_instr) = mlil.instruction_at(decl_addr) else { + tracing::warn!("Failed to find instruction at {:#x}", decl_addr); + continue; + }; + let decl_var = match var.location { + Location::Register(RegisterLocation { id, .. }) => { + decl_instr.variable_for_register_after(RegisterId(id as u32)) + } + Location::Stack(StackLocation { offset, .. }) => { + decl_instr.variable_for_stack_location_after(offset) + } + }; + if function.is_var_user_defined(&decl_var) { + // Internally, analysis will just assign user vars to auto vars and consult only that. + // So we must skip if there is a user-defined var at the decl. + continue; + } + let decl_ty = match var.ty { + Some(decl_ty) => to_bn_type(Some(function.arch()), &decl_ty), + None => { + let Some(existing_var) = function.variable_type(&decl_var) else { continue; - } - let decl_ty = match variable.ty { - Some(decl_ty) => to_bn_type(Some(function.arch()), &decl_ty), - None => { - let Some(existing_var) = function.variable_type(&decl_var) else { - continue; - }; - existing_var.contents - } }; - let decl_name = variable - .name - .unwrap_or_else(|| function.variable_name(&decl_var)); - function.create_auto_var(&decl_var, &decl_ty, &decl_name, false) + existing_var.contents } - } + }; + let decl_name = var + .name + .unwrap_or_else(|| function.variable_name(&decl_var)); + function.create_auto_var(&decl_var, &decl_ty, &decl_name, false) } - function.add_tag( - &get_warp_tag_type(&view), - &matched_function.guid.to_string(), - None, - false, - None, - ); } - }; + function.add_tag( + &get_warp_tag_type(&view), + &matched_function.guid.to_string(), + None, + false, + None, + ); + } +} + +pub fn insert_workflow() -> Result<(), ()> { let matcher_activity = |ctx: &AnalysisContext| { let view = ctx.view(); run_matcher(&view); @@ -395,7 +419,7 @@ pub fn insert_workflow() -> Result<(), ()> { "This analysis step applies WARP info to matched functions...", ) .eligibility(activity::Eligibility::auto().run_once(false)); - let apply_activity = Activity::new_with_action(&apply_config, apply_activity); + let apply_activity = Activity::new_with_action(&apply_config, run_applier); let add_function_activities = |workflow: Option| -> Result<(), ()> { let Some(workflow) = workflow else { diff --git a/plugins/warp/tests/matcher.rs b/plugins/warp/tests/matcher.rs index 880112d031..eb209a2cb0 100644 --- a/plugins/warp/tests/matcher.rs +++ b/plugins/warp/tests/matcher.rs @@ -7,6 +7,7 @@ use binaryninja::platform::Platform; use binaryninja::rc::Ref; use binaryninja::symbol::{Symbol as BNSymbol, SymbolType}; use binaryninja::types::TypeClass as BNTypeClass; +use std::path::PathBuf; use std::str::FromStr; use warp::mock::{mock_constraint, mock_function}; use warp::r#type::class::{IntegerClass, ReferrerClass, StructureClass, StructureMember}; @@ -14,8 +15,10 @@ use warp::r#type::guid::TypeGUID; use warp::r#type::Type; use warp::signature::function::FunctionGUID; use warp::target::Target; +use warp_ninja::container::disk::DiskContainer; use warp_ninja::container::memory::MemoryContainer; use warp_ninja::container::{Container, SourceId}; +use warp_ninja::convert::platform_to_target; use warp_ninja::function_guid; use warp_ninja::matcher::{Matcher, MatcherSettings}; @@ -180,3 +183,49 @@ fn test_add_type_to_view() { // Make sure we actually added it as a structure type. assert_eq!(found_type.type_class(), BNTypeClass::StructureTypeClass); } + +#[test] +fn test_load_type_from_file() { + let matcher_settings = MatcherSettings::default(); + let matcher = Matcher::new(matcher_settings); + + let session = Session::new().expect("Failed to create session"); + let out_dir = env!("OUT_DIR").parse::().unwrap(); + let file = out_dir.join("test.exe"); + let view = session.load(file).expect("Failed to load view"); + + let platform = view.default_platform().expect("No default platform"); + let target = platform_to_target(&platform); + let container = DiskContainer::new_from_dir(out_dir); + let bn_function = view + .function_at(&platform, 0x43e83b) + .expect("Failed to get function"); + let func_guid = function_guid(&bn_function, &bn_function.lifted_il().unwrap()); + let possible_funcs: Vec<_> = container + .sources() + .unwrap() + .iter() + .flat_map(|source| { + container + .functions_with_guid(&target, source, &func_guid) + .unwrap() + }) + .collect(); + let matched_func = matcher + .match_function_from_constraints(&bn_function, &possible_funcs) + .expect("Failed to match function"); + assert_eq!(matched_func.symbol.name, "my_signature".to_string()); + + // Actually add the type information to the view. + matcher.add_function_types_to_view( + &container, + &container.sources().unwrap(), + &view, + platform.arch(), + matched_func, + ); + + // Verify that the type information is present in the view. + view.type_by_name("VOID_PTR").expect("Failed to find type"); + view.type_by_name("test_type").expect("Failed to find type"); +}