summaryrefslogtreecommitdiff
path: root/plugins/warp/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/warp/src/lib.rs')
-rw-r--r--plugins/warp/src/lib.rs266
1 files changed, 183 insertions, 83 deletions
diff --git a/plugins/warp/src/lib.rs b/plugins/warp/src/lib.rs
index 6d147bdc..aab26932 100644
--- a/plugins/warp/src/lib.rs
+++ b/plugins/warp/src/lib.rs
@@ -1,7 +1,5 @@
-use crate::cache::{
- cached_adjacency_constraints, cached_call_site_constraints, cached_function_guid,
-};
-use crate::convert::{from_bn_symbol, from_bn_type};
+use crate::cache::{cached_constraints, cached_function_guid};
+use crate::convert::{bn_comment_to_comment, from_bn_symbol, from_bn_type};
use binaryninja::architecture::{
Architecture, ImplicitRegisterExtend, Register as BNRegister, RegisterInfo,
};
@@ -9,25 +7,55 @@ use binaryninja::basic_block::BasicBlock as BNBasicBlock;
use binaryninja::binary_view::{BinaryView, BinaryViewExt};
use binaryninja::confidence::MAX_CONFIDENCE;
use binaryninja::function::{Function as BNFunction, NativeBlock};
-use binaryninja::low_level_il::expression::{ExpressionHandler, LowLevelILExpressionKind};
+use binaryninja::low_level_il::expression::{
+ ExpressionHandler, LowLevelILExpression, LowLevelILExpressionKind, ValueExpr,
+};
use binaryninja::low_level_il::function::{FunctionMutability, LowLevelILFunction, NonSSA};
use binaryninja::low_level_il::instruction::{
InstructionHandler, LowLevelILInstruction, LowLevelILInstructionKind,
};
use binaryninja::low_level_il::{LowLevelILRegisterKind, VisitorAction};
-use binaryninja::rc::Ref as BNRef;
+use binaryninja::rc::{Ref as BNRef, Ref};
use std::ops::Range;
use std::path::PathBuf;
use warp::signature::basic_block::BasicBlockGUID;
-use warp::signature::function::constraints::FunctionConstraints;
use warp::signature::function::{Function, FunctionGUID};
+use binaryninja::tags::TagType;
+use binaryninja::variable::RegisterValueType;
+/// Re-export the warp crate that is used, this is useful for consumers of this crate.
+pub use warp;
+
pub mod cache;
+pub mod container;
pub mod convert;
-mod matcher;
+pub mod matcher;
+pub mod processor;
+pub mod report;
+
/// Only used when compiled for cdylib target.
mod plugin;
+// TODO: Make this 4kb
+/// If the address is within this range before or after a relocatable region, we will assume the address to be relocatable.
+const ADDRESS_RELOCATION_THRESHOLD: u64 = 0x10000;
+
+const TAG_ICON: &str = "🌐";
+const TAG_NAME: &str = "WARP";
+
+fn get_warp_tag_type(view: &BinaryView) -> Ref<TagType> {
+ view.tag_type_by_name(TAG_NAME)
+ .unwrap_or_else(|| view.create_tag_type(TAG_NAME, TAG_ICON))
+}
+
+const INCLUDE_TAG_ICON: &str = "🚀";
+const INCLUDE_TAG_NAME: &str = "WARP: Selected Function";
+
+fn get_warp_include_tag_type(view: &BinaryView) -> Ref<TagType> {
+ view.tag_type_by_name(INCLUDE_TAG_NAME)
+ .unwrap_or_else(|| view.create_tag_type(INCLUDE_TAG_NAME, INCLUDE_TAG_ICON))
+}
+
pub fn core_signature_dir() -> PathBuf {
// Get core signatures for the given platform
let install_dir = binaryninja::install_directory();
@@ -45,22 +73,34 @@ pub fn user_signature_dir() -> PathBuf {
pub fn build_function<M: FunctionMutability>(
func: &BNFunction,
- llil: &LowLevelILFunction<M, NonSSA>,
+ lifted_il: &LowLevelILFunction<M, NonSSA>,
) -> Function {
- let bn_fn_ty = func.function_type();
+ let comments = func
+ .comments()
+ .iter()
+ .map(|c| bn_comment_to_comment(func, c))
+ .collect();
Function {
- guid: cached_function_guid(func, llil),
+ guid: cached_function_guid(func, lifted_il),
symbol: from_bn_symbol(&func.symbol()),
- ty: from_bn_type(&func.view(), &bn_fn_ty, MAX_CONFIDENCE),
- constraints: FunctionConstraints {
- // NOTE: Adding adjacent only works if analysis is complete.
- // NOTE: We do not filter out adjacent functions here.
- adjacent: cached_adjacency_constraints(func, |_| true),
- call_sites: cached_call_site_constraints(func),
- // TODO: Add caller sites (when adjacent and call sites are minimal)
- // NOTE: Adding caller sites only works if analysis is complete.
- caller_sites: Default::default(),
+ // Currently we only store the type if its a user type.
+ // TODO: In the future we might want to make this configurable.
+ ty: match func.has_user_type() || func.has_explicitly_defined_type() {
+ true => Some(from_bn_type(
+ &func.view(),
+ &func.function_type(),
+ MAX_CONFIDENCE,
+ )),
+ false => None,
},
+ // NOTE: Adding adjacent only works if analysis is complete.
+ // NOTE: We do not filter out adjacent functions here.
+ constraints: cached_constraints(func, |_| true),
+ comments,
+ // TODO: Gather relevant variables (only user?).
+ // TODO: Will need MLIL SSA for this to locate def sites.
+ // TODO: Add this info in a second pass?
+ variables: vec![],
}
}
@@ -71,20 +111,21 @@ pub fn sorted_basic_blocks(func: &BNFunction) -> Vec<BNRef<BNBasicBlock<NativeBl
.iter()
.map(|bb| bb.clone())
.collect::<Vec<_>>();
+ // NOTE: start_index is actually the address with [`NativeBlock`].
basic_blocks.sort_by_key(|f| f.start_index());
basic_blocks
}
pub fn function_guid<M: FunctionMutability>(
func: &BNFunction,
- llil: &LowLevelILFunction<M, NonSSA>,
+ lifted_il: &LowLevelILFunction<M, NonSSA>,
) -> FunctionGUID {
// TODO: We might want to make this configurable, or otherwise _not_ retrieve from the view here.
let relocatable_regions = relocatable_regions(&func.view());
let basic_blocks = sorted_basic_blocks(func);
let basic_block_guids = basic_blocks
.iter()
- .map(|bb| basic_block_guid(&relocatable_regions, bb, llil))
+ .map(|bb| basic_block_guid(&relocatable_regions, bb, lifted_il))
.collect::<Vec<_>>();
FunctionGUID::from_basic_blocks(&basic_block_guids)
}
@@ -92,30 +133,60 @@ pub fn function_guid<M: FunctionMutability>(
pub fn basic_block_guid<M: FunctionMutability>(
relocatable_regions: &[Range<u64>],
basic_block: &BNBasicBlock<NativeBlock>,
- llil: &LowLevelILFunction<M, NonSSA>,
+ lifted_il: &LowLevelILFunction<M, NonSSA>,
) -> BasicBlockGUID {
let func = basic_block.function();
+ // TODO: We really should never consult another IL, no guarantee that it exists.
+ let low_level_il = func.low_level_il();
let view = func.view();
let arch = func.arch();
let max_instr_len = arch.max_instr_len();
+ // NOTE: Whenever you make a change here, prefer being "additive", that is, make a smaller change that
+ // only increases the masked contents, instead of making a larger change that could *remove* masked
+ // contents. The reason is that we assume any change that is purely additive to increase the ability
+ // to match previously "unmatchable" functions, whereas the latter would take away. This is not always
+ // the case, but it is generally a good rule to follow.
let basic_block_range = basic_block.start_index()..basic_block.end_index();
let mut basic_block_bytes = Vec::with_capacity(basic_block_range.count());
for instr_addr in basic_block.into_iter() {
let mut instr_bytes = view.read_vec(instr_addr, max_instr_len);
if let Some(instr_info) = arch.instruction_info(&instr_bytes, instr_addr) {
instr_bytes.truncate(instr_info.length);
- if let Some(instr_llil) = llil.instruction_at(instr_addr) {
- // If instruction is blacklisted don't include the bytes.
- if !is_blacklisted_instruction(&instr_llil) {
- if is_variant_instruction(relocatable_regions, &instr_llil) {
- // Found a variant instruction, mask off entire instruction.
+
+ // Find variant and blacklisted instructions using lifted il.
+ if let Some(lifted_il_instr) = lifted_il.instruction_at(instr_addr) {
+ // If instruction is blacklisted, don't include the bytes.
+ if is_blacklisted_instruction(&lifted_il_instr) {
+ continue;
+ }
+
+ if is_variant_instruction(relocatable_regions, &lifted_il_instr) {
+ // Found a variant instruction, mask off the entire instruction.
+ instr_bytes.fill(0);
+ }
+ }
+
+ // TODO: We cannot access the values of expression in lifted IL, we have to go and consult low level IL.
+ // TODO: But because of some extremely annoying simplifications that are happening at LLIL, namely
+ // TODO: Folding of expressions into other instructions, we cannot use only LLIL. Therefor
+ // TODO: We only put the checks that require the expr value here.
+ // TODO: This still has the issue of, some (if (rax + 44) => 28) expression being masked,
+ // TODO: But the only way to remove that is to not consult LLIL at all and have the values
+ // TODO: Available at lifted IL, I have not found a good way to do this without making
+ // TODO: A "mapped llil" or having some simple data flow, the simple data flow is the most attractive
+ // TODO: "solution", but it would require
+ if let Ok(llil) = &low_level_il {
+ if let Some(low_level_instr) = llil.instruction_at(instr_addr) {
+ if is_computed_variant_instruction(relocatable_regions, &low_level_instr) {
+ // Found a computed variant instruction, mask off the entire instruction.
instr_bytes.fill(0);
}
- // Add the instructions bytes to the basic blocks bytes
- basic_block_bytes.extend(instr_bytes);
}
}
+
+ // Add the instruction bytes to the basic blocks bytes
+ basic_block_bytes.extend(instr_bytes);
}
}
@@ -127,8 +198,8 @@ pub fn basic_block_guid<M: FunctionMutability>(
/// Blacklisted instructions will make an otherwise identical function GUID fail to match.
///
/// Example: NOPs and useless moves are blacklisted to allow for hot-patchable functions.
-pub fn is_blacklisted_instruction<A: Architecture, M: FunctionMutability>(
- instr: &LowLevelILInstruction<A, M, NonSSA<RegularNonSSA>>,
+pub fn is_blacklisted_instruction<M: FunctionMutability>(
+ instr: &LowLevelILInstruction<M, NonSSA>,
) -> bool {
match instr.kind() {
LowLevelILInstructionKind::Nop(_) => true,
@@ -138,13 +209,13 @@ pub fn is_blacklisted_instruction<A: Architecture, M: FunctionMutability>(
if op.dest_reg() == source_op.source_reg() =>
{
match op.dest_reg() {
- LowLevelILRegister::ArchReg(r) => {
- // If this register has no implicit extend then we can safely assume it's a NOP.
+ LowLevelILRegisterKind::Arch(r) => {
+ // If this register has no implicit extend, we can safely assume it's a NOP.
// Ex. on x86_64 we don't want to remove `mov edi, edi` as it will zero the upper 32 bits.
// Ex. on x86 we do want to remove `mov edi, edi` as it will not have a side effect like above.
matches!(r.info().implicit_extend(), ImplicitRegisterExtend::NoExtend)
}
- LowLevelILRegister::Temp(_) => false,
+ LowLevelILRegisterKind::Temp(_) => false,
}
}
_ => false,
@@ -154,24 +225,22 @@ pub fn is_blacklisted_instruction<A: Architecture, M: FunctionMutability>(
}
}
-pub fn is_variant_instruction<A: Architecture, M: FunctionMutability>(
+pub fn is_variant_instruction<M: FunctionMutability>(
relocatable_regions: &[Range<u64>],
- instr: &LowLevelILInstruction<A, M, NonSSA<RegularNonSSA>>,
+ instr: &LowLevelILInstruction<M, NonSSA>,
) -> bool {
- let is_variant_expr = |expr: &LowLevelILExpressionKind<A, M, NonSSA<RegularNonSSA>>| {
- match expr {
+ let is_variant_expr = |expr: &LowLevelILExpression<M, NonSSA, ValueExpr>| {
+ match expr.kind() {
LowLevelILExpressionKind::ConstPtr(op)
if is_address_relocatable(relocatable_regions, op.value()) =>
{
// Constant Pointer must be in a section for it to be relocatable.
- // NOTE: We cannot utilize segments here as there will be a zero based segment.
true
}
LowLevelILExpressionKind::Const(op)
if is_address_relocatable(relocatable_regions, op.value()) =>
{
// Constant value must be in a section for it to be relocatable.
- // NOTE: We cannot utilize segments here as there will be a zero based segment.
true
}
LowLevelILExpressionKind::ExternPtr(_) => true,
@@ -181,7 +250,7 @@ pub fn is_variant_instruction<A: Architecture, M: FunctionMutability>(
// Visit instruction expressions looking for variant expression, [VisitorAction::Halt] means variant.
instr.visit_tree(&mut |expr| {
- if is_variant_expr(&expr.kind()) {
+ if is_variant_expr(expr) {
// Found a variant expression.
VisitorAction::Halt
} else {
@@ -191,18 +260,87 @@ pub fn is_variant_instruction<A: Architecture, M: FunctionMutability>(
}) == VisitorAction::Halt
}
-/// If the address is inside any of the given ranges we will assume the address to be relocatable.
+/// NOTE: This will only work at LLIL, **NOT** lifted IL. You must do this in a second pass.
+///
+/// This was previously done inside `is_variant_instruction` but had to be moved to access expr value.
+pub fn is_computed_variant_instruction<M: FunctionMutability>(
+ relocatable_regions: &[Range<u64>],
+ instr: &LowLevelILInstruction<M, NonSSA>,
+) -> bool {
+ let is_expr_constant = |expr: &LowLevelILExpression<M, NonSSA, ValueExpr>| match expr.kind() {
+ LowLevelILExpressionKind::Const(_) => true,
+ _ => false,
+ };
+
+ let is_variant_observed_expr = |expr: &LowLevelILExpression<M, NonSSA, ValueExpr>| {
+ match expr.kind() {
+ // TODO: Skip problematic expressions like IF?
+ LowLevelILExpressionKind::Add(op) | LowLevelILExpressionKind::Sub(op) => {
+ // For now, we limit to only expressions that contain some constant; this keeps add expressions
+ // with two registers with known values from being marked variant.
+ let constant_expressed =
+ is_expr_constant(&op.left()) || is_expr_constant(&op.right());
+ // NOTE: Lifted IL does not have the value ever, we must consult Low Level IL.
+ // If the expression value is known, we check to see if it's a relocatable address.
+ let expr_value = expr.value();
+ match expr_value.state {
+ RegisterValueType::EntryValue
+ | RegisterValueType::ConstantValue
+ | RegisterValueType::ConstantPointerValue
+ | RegisterValueType::ExternalPointerValue
+ | RegisterValueType::StackFrameOffset
+ | RegisterValueType::ReturnAddressValue
+ | RegisterValueType::ImportedAddressValue
+ if constant_expressed
+ && is_address_relocatable(
+ relocatable_regions,
+ expr_value.value as u64,
+ ) =>
+ {
+ // Concrete arithmetic operation with a relocatable result.
+ true
+ }
+ _ => false,
+ }
+ }
+ _ => false,
+ }
+ };
+
+ // Visit instruction expressions looking for an observed variant expression, [VisitorAction::Halt] means variant.
+ instr.visit_tree(&mut |expr| {
+ if is_variant_observed_expr(expr) {
+ // Found a variant expression.
+ VisitorAction::Halt
+ } else {
+ // Keep looking for an observed variant expression.
+ VisitorAction::Descend
+ }
+ }) == VisitorAction::Halt
+}
+
+/// If the address is inside any of the given ranges, we will assume the address to be relocatable.
pub fn is_address_relocatable(relocatable_regions: &[Range<u64>], address: u64) -> bool {
relocatable_regions
.iter()
- .any(|range| range.contains(&address))
+ .any(|range| {
+ // Check if the address is within the range itself
+ (range.contains(&address))
+ // Check if the address is within the threshold **AFTER** the range
+ // NOTE: The address must at least be larger than the threshold itself, for lower image-based binaries.
+ || (address > range.end && address > ADDRESS_RELOCATION_THRESHOLD && address <= range.end + ADDRESS_RELOCATION_THRESHOLD)
+ // Check if the address is within the threshold **BEFORE** the range
+ // NOTE: The address must at least be larger than the threshold itself, for lower image-based binaries.
+ || (address < range.start && address > ADDRESS_RELOCATION_THRESHOLD && address >= range.start.saturating_sub(ADDRESS_RELOCATION_THRESHOLD))
+ })
}
// TODO: This might need to be configurable, in that case we better remove this function.
/// Get the relocatable regions of the view.
///
-/// Currently, this is all the sections, however this might be refined later.
+/// Currently, this is all the sections, however, this might be refined later.
pub fn relocatable_regions(view: &BinaryView) -> Vec<Range<u64>> {
+ // NOTE: We cannot use segments here as there will be a zero-based segment.
view.sections()
.iter()
.map(|s| Range {
@@ -211,41 +349,3 @@ pub fn relocatable_regions(view: &BinaryView) -> Vec<Range<u64>> {
})
.collect()
}
-
-#[cfg(test)]
-mod tests {
- use crate::cache::cached_function_guid;
- use binaryninja::binary_view::BinaryViewExt;
- use binaryninja::headless::Session;
- use std::path::PathBuf;
- use std::sync::OnceLock;
-
- static INIT: OnceLock<Session> = OnceLock::new();
-
- fn get_session<'a>() -> &'a Session {
- // TODO: This is not shared between other test modules, should still be fine (mutex in core now).
- INIT.get_or_init(|| Session::new().expect("Failed to initialize session"))
- }
-
- #[test]
- fn insta_signatures() {
- let session = get_session();
- let out_dir = env!("OUT_DIR").parse::<PathBuf>().unwrap();
- for entry in std::fs::read_dir(out_dir).expect("Failed to read OUT_DIR") {
- let entry = entry.expect("Failed to read directory entry");
- let path = entry.path();
- if path.is_file() {
- let view = session.load(&path).expect("Failed to load view");
- let mut functions = view
- .functions()
- .iter()
- .map(|f| cached_function_guid(&f, &f.low_level_il().unwrap()))
- .collect::<Vec<_>>();
- functions.sort_by_key(|guid| guid.guid);
- let snapshot_name =
- format!("snapshot_{}", path.file_stem().unwrap().to_string_lossy());
- insta::assert_debug_snapshot!(snapshot_name, functions);
- }
- }
- }
-}