Unverified Commit 242c5325 by Enkelmann Committed by GitHub

Implement new check for CWE-415 and CWE-416 (#318)

parent 26f9844d
......@@ -178,7 +178,7 @@ fn run_with_ghidra(args: &CmdlineArgs) {
let modules_depending_on_string_abstraction = BTreeSet::from_iter(["CWE78"]);
let modules_depending_on_pointer_inference =
BTreeSet::from_iter(["CWE119", "CWE134", "CWE476", "Memory"]);
BTreeSet::from_iter(["CWE119", "CWE134", "CWE416", "CWE476", "Memory"]);
let string_abstraction_needed = modules
.iter()
......
......@@ -29,7 +29,7 @@
use super::fixpoint::Computation;
use super::forward_interprocedural_fixpoint::GeneralizedContext;
use super::interprocedural_fixpoint_generic::NodeValue;
use crate::abstract_domain::{DataDomain, IntervalDomain, SizedDomain};
use crate::abstract_domain::{AbstractIdentifier, DataDomain, IntervalDomain, SizedDomain};
use crate::analysis::forward_interprocedural_fixpoint::Context as _;
use crate::analysis::graph::{Graph, Node};
use crate::intermediate_representation::*;
......@@ -37,7 +37,7 @@ use crate::prelude::*;
use crate::utils::log::*;
use petgraph::graph::NodeIndex;
use petgraph::visit::IntoNodeReferences;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
mod context;
pub mod object;
......@@ -96,6 +96,9 @@ pub struct PointerInference<'a> {
/// Maps certain TIDs like the TIDs of [`Jmp`] instructions to the pointer inference state at that TID.
/// The map will be filled after the fixpoint computation finished.
states_at_tids: HashMap<Tid, State>,
/// Maps the TIDs of call instructions to a map mapping callee IDs to the corresponding value in the caller.
/// The map will be filled after the fixpoint computation finished.
id_renaming_maps_at_calls: HashMap<Tid, BTreeMap<AbstractIdentifier, Data>>,
}
impl<'a> PointerInference<'a> {
......@@ -145,6 +148,7 @@ impl<'a> PointerInference<'a> {
values_at_defs: HashMap::new(),
addresses_at_defs: HashMap::new(),
states_at_tids: HashMap::new(),
id_renaming_maps_at_calls: HashMap::new(),
}
}
......@@ -256,12 +260,12 @@ impl<'a> PointerInference<'a> {
let context = self.computation.get_context().get_context();
let graph = self.computation.get_graph();
for node in graph.node_indices() {
let node_state = match self.computation.get_node_value(node) {
Some(NodeValue::Value(value)) => value,
_ => continue,
};
match graph[node] {
Node::BlkStart(blk, _sub) => {
let node_state = match self.computation.get_node_value(node) {
Some(NodeValue::Value(value)) => value,
_ => continue,
};
let mut state = node_state.clone();
for def in &blk.term.defs {
match &def.term {
......@@ -291,12 +295,40 @@ impl<'a> PointerInference<'a> {
}
}
Node::BlkEnd(blk, _sub) => {
let node_state = match self.computation.get_node_value(node) {
Some(NodeValue::Value(value)) => value,
_ => continue,
};
for jmp in &blk.term.jmps {
self.states_at_tids
.insert(jmp.tid.clone(), node_state.clone());
}
}
Node::CallSource { .. } | Node::CallReturn { .. } => (),
Node::CallSource { .. } => (),
Node::CallReturn {
call: (caller_blk, _caller_sub),
return_: _,
} => {
let call_tid = match caller_blk.term.jmps.get(0) {
Some(call) => &call.tid,
_ => continue,
};
let (state_before_call, state_before_return) =
match self.computation.get_node_value(node) {
Some(NodeValue::CallFlowCombinator {
call_stub: Some(state_before_call),
interprocedural_flow: Some(state_before_return),
}) => (state_before_call, state_before_return),
_ => continue,
};
let id_to_data_map = context.create_callee_id_to_caller_data_map(
state_before_call,
state_before_return,
call_tid,
);
self.id_renaming_maps_at_calls
.insert(call_tid.clone(), id_to_data_map);
}
}
}
}
......@@ -307,6 +339,18 @@ impl<'a> PointerInference<'a> {
self.states_at_tids.get(jmp_tid)
}
/// Get the mapping from callee IDs to caller values for the given call.
/// This function only yields results after the fixpoint has been computed.
///
/// Note that the maps may contain mappings from callee IDs to temporary caller IDs that get instantly removed from the caller
/// since they are not referenced in any caller object.
pub fn get_id_renaming_map_at_call_tid(
&self,
call_tid: &Tid,
) -> Option<&BTreeMap<AbstractIdentifier, Data>> {
self.id_renaming_maps_at_calls.get(call_tid)
}
/// Print information on dead ends in the control flow graph for debugging purposes.
/// Ignore returns where there is no known caller stack id.
#[allow(dead_code)]
......
......@@ -12,6 +12,7 @@ pub mod cwe_215;
pub mod cwe_243;
pub mod cwe_332;
pub mod cwe_367;
pub mod cwe_416;
pub mod cwe_426;
pub mod cwe_467;
pub mod cwe_476;
......
......@@ -39,19 +39,19 @@ pub struct Context<'a> {
impl<'a> Context<'a> {
/// Create a new context object.
pub fn new(
project: &'a Project,
graph: &'a Graph<'a>,
pointer_inference: &'a PointerInference<'a>,
function_signatures: &'a BTreeMap<Tid, FunctionSignature>,
analysis_results: &AnalysisResults,
pub fn new<'b>(
analysis_results: &'b AnalysisResults<'a>,
log_collector: crossbeam_channel::Sender<LogThreadMsg>,
) -> Self {
) -> Context<'a>
where
'a: 'b,
{
let project = analysis_results.project;
Context {
project,
graph,
pointer_inference,
function_signatures,
graph: analysis_results.control_flow_graph,
pointer_inference: analysis_results.pointer_inference.unwrap(),
function_signatures: analysis_results.function_signatures.unwrap(),
callee_to_callsites_map: compute_callee_to_call_sites_map(project),
param_replacement_map: compute_param_replacement_map(analysis_results),
malloc_tid_to_object_size_map: compute_size_values_of_malloc_calls(analysis_results),
......
......@@ -18,14 +18,7 @@ impl<'a> Context<'a> {
let analysis_results = Box::leak(analysis_results);
let (log_collector, _) = crossbeam_channel::unbounded();
Context::new(
analysis_results.project,
analysis_results.control_flow_graph,
analysis_results.pointer_inference.unwrap(),
analysis_results.function_signatures.unwrap(),
analysis_results,
log_collector,
)
Context::new(analysis_results, log_collector)
}
}
......
......@@ -65,14 +65,7 @@ pub fn check_cwe(
) -> (Vec<LogMessage>, Vec<CweWarning>) {
let log_thread = LogThread::spawn(LogThread::collect_and_deduplicate);
let context = Context::new(
analysis_results.project,
analysis_results.control_flow_graph,
analysis_results.pointer_inference.unwrap(),
analysis_results.function_signatures.unwrap(),
analysis_results,
log_thread.get_msg_sender(),
);
let context = Context::new(analysis_results, log_thread.get_msg_sender());
let mut fixpoint_computation =
crate::analysis::forward_interprocedural_fixpoint::create_computation(context, None);
......@@ -91,5 +84,7 @@ pub fn check_cwe(
fixpoint_computation.compute_with_max_steps(100);
log_thread.collect()
let (logs, mut cwe_warnings) = log_thread.collect();
cwe_warnings.sort();
(logs, cwe_warnings)
}
use super::State;
use super::CWE_MODULE;
use crate::abstract_domain::AbstractDomain;
use crate::analysis::function_signature::FunctionSignature;
use crate::analysis::graph::Graph;
use crate::analysis::pointer_inference::PointerInference;
use crate::analysis::vsa_results::VsaResult;
use crate::intermediate_representation::*;
use crate::prelude::*;
use crate::utils::log::CweWarning;
use crate::utils::log::LogMessage;
use crate::utils::log::LogThreadMsg;
use std::collections::BTreeMap;
/// The context struct for the fixpoint algorithm that contains references to the analysis results
/// of other analyses used in this analysis.
pub struct Context<'a> {
/// A pointer to the project struct.
pub project: &'a Project,
/// A pointer to the control flow graph.
pub graph: &'a Graph<'a>,
/// A pointer to the results of the pointer inference analysis.
pub pointer_inference: &'a PointerInference<'a>,
/// A pointer to the computed function signatures for all internal functions.
pub function_signatures: &'a BTreeMap<Tid, FunctionSignature>,
/// A sender channel that can be used to collect logs in the corresponding logging thread.
pub log_collector: crossbeam_channel::Sender<LogThreadMsg>,
/// Generic function arguments assumed for calls to functions where the real number of parameters are unknown.
generic_function_parameter: Vec<Arg>,
}
impl<'a> Context<'a> {
/// Generate a new context struct from the given analysis results and a channel for gathering log messages and CWE warnings.
pub fn new<'b>(
analysis_results: &'b AnalysisResults<'a>,
log_collector: crossbeam_channel::Sender<LogThreadMsg>,
) -> Context<'a>
where
'a: 'b,
{
let generic_function_parameter: Vec<_> =
if let Some(cconv) = analysis_results.project.get_standard_calling_convention() {
cconv
.integer_parameter_register
.iter()
.map(|reg| Arg::from_var(reg.clone(), None))
.collect()
} else {
Vec::new()
};
Context {
project: analysis_results.project,
graph: analysis_results.control_flow_graph,
pointer_inference: analysis_results.pointer_inference.unwrap(),
function_signatures: analysis_results.function_signatures.unwrap(),
log_collector,
generic_function_parameter,
}
}
/// For the given call parameters of the given call check for possible Use-After-Free bugs
/// and return the possible causes for such bugs.
fn collect_cwe_warnings_of_call_params<'b>(
&self,
state: &mut State,
call_tid: &Tid,
call_params: impl IntoIterator<Item = &'b Arg>,
) -> Option<Vec<String>> {
let mut warnings = Vec::new();
for arg in call_params {
if let Some(arg_value) = self
.pointer_inference
.eval_parameter_arg_at_call(call_tid, arg)
{
if let Some(mut warning_causes) = state.check_address_for_use_after_free(&arg_value)
{
warnings.append(&mut warning_causes);
}
}
}
if !warnings.is_empty() {
Some(warnings)
} else {
None
}
}
/// Check the parameters of an internal function call for dangling pointers and report CWE warnings accordingly.
fn check_internal_call_params_for_use_after_free(
&self,
state: &mut State,
callee_sub_tid: &Tid,
call_tid: &Tid,
) {
let function_signature = match self.function_signatures.get(callee_sub_tid) {
Some(fn_sig) => fn_sig,
None => return,
};
let mut warnings = Vec::new();
for (arg, access_pattern) in &function_signature.parameters {
if access_pattern.is_dereferenced() {
if let Some(arg_value) = self
.pointer_inference
.eval_parameter_arg_at_call(call_tid, arg)
{
if let Some(mut warning_causes) =
state.check_address_for_use_after_free(&arg_value)
{
warnings.append(&mut warning_causes);
}
}
}
}
let callee_sub_name = &self.project.program.term.subs[callee_sub_tid].term.name;
if !warnings.is_empty() {
let cwe_warning = CweWarning {
name: "CWE416".to_string(),
version: CWE_MODULE.version.to_string(),
addresses: vec![call_tid.address.clone()],
tids: vec![format!("{}", call_tid)],
symbols: Vec::new(),
other: vec![warnings],
description: format!(
"(Use After Free) Call to {} at {} may access dangling pointers through its parameters",
callee_sub_name,
call_tid.address
),
};
self.log_collector.send(cwe_warning.into()).unwrap();
}
}
/// Handle a call to `free` by marking the corresponding memory object IDs as dangling and detecting possible double frees.
fn handle_call_to_free(&self, state: &mut State, call_tid: &Tid, free_symbol: &ExternSymbol) {
if free_symbol.parameters.is_empty() {
let error_msg = LogMessage::new_error("free symbol without parameter encountered.")
.location(call_tid.clone())
.source(CWE_MODULE.name);
self.log_collector.send(error_msg.into()).unwrap();
return;
}
if let Some(param) = self
.pointer_inference
.eval_parameter_arg_at_call(call_tid, &free_symbol.parameters[0])
{
if let Some(pi_state) = self.pointer_inference.get_state_at_jmp_tid(call_tid) {
if let Some(warning_causes) =
state.handle_param_of_free_call(call_tid, &param, pi_state)
{
let cwe_warning = CweWarning {
name: "CWE415".to_string(),
version: CWE_MODULE.version.to_string(),
addresses: vec![call_tid.address.clone()],
tids: vec![format!("{}", call_tid)],
symbols: Vec::new(),
other: vec![warning_causes],
description: format!(
"(Double Free) Object may have been freed before at {}",
call_tid.address
),
};
self.log_collector.send(cwe_warning.into()).unwrap();
}
}
}
}
}
impl<'a> crate::analysis::forward_interprocedural_fixpoint::Context<'a> for Context<'a> {
type Value = State;
/// Get a reference to the control flow graph.
fn get_graph(&self) -> &Graph<'a> {
self.graph
}
/// Merge two node states.
fn merge(&self, state1: &State, state2: &State) -> State {
state1.merge(state2)
}
/// Check whether the `def` may access already freed memory.
/// If yes, generate a CWE warning and mark the corresponding object IDs as already flagged.
fn update_def(&self, state: &State, def: &Term<Def>) -> Option<State> {
let mut state = state.clone();
if let Some(address) = self.pointer_inference.eval_address_at_def(&def.tid) {
if let Some(warning_causes) = state.check_address_for_use_after_free(&address) {
let cwe_warning = CweWarning {
name: "CWE416".to_string(),
version: CWE_MODULE.version.to_string(),
addresses: vec![def.tid.address.clone()],
tids: vec![format!("{}", def.tid)],
symbols: Vec::new(),
other: vec![warning_causes],
description: format!(
"(Use After Free) Access through a dangling pointer at {}",
def.tid.address
),
};
self.log_collector.send(cwe_warning.into()).unwrap();
}
}
Some(state)
}
/// Just returns the unmodified state.
fn update_jump(
&self,
state: &State,
_jump: &Term<Jmp>,
_untaken_conditional: Option<&Term<Jmp>>,
_target: &Term<Blk>,
) -> Option<State> {
Some(state.clone())
}
/// Check whether any call parameters are dangling pointers and generate CWE warnings accordingly.
/// Always returns `None` since the analysis is a bottom-up analysis (i.e. no information flows from caller to callee).
fn update_call(
&self,
state: &State,
call: &Term<Jmp>,
target: &crate::analysis::graph::Node,
_calling_convention: &Option<String>,
) -> Option<State> {
use crate::analysis::graph::Node;
let sub = match *target {
Node::BlkStart(_, sub) => sub,
_ => return None,
};
let mut state = state.clone();
self.check_internal_call_params_for_use_after_free(&mut state, &sub.tid, &call.tid);
// No information flows from caller to callee, so we return `None` regardless.
None
}
/// Collect the IDs of objects freed in the callee and mark the corresponding objects in the caller as freed.
/// Also check the call parameters for Use-After-Frees.
fn update_return(
&self,
state: Option<&State>,
state_before_call: Option<&State>,
call: &Term<Jmp>,
_return_term: &Term<Jmp>,
_calling_convention: &Option<String>,
) -> Option<State> {
let (state_before_return, state_before_call) = match (state, state_before_call) {
(Some(state_before_return), Some(state_before_call)) => {
(state_before_return, state_before_call)
}
_ => return None,
};
let id_replacement_map = match self
.pointer_inference
.get_id_renaming_map_at_call_tid(&call.tid)
{
Some(map) => map,
None => return None,
};
let pi_state_before_call = match self.pointer_inference.get_state_at_jmp_tid(&call.tid) {
Some(pi_state) => pi_state,
None => return None,
};
let mut state_after_return = state_before_call.clone();
// Check for Use-After-Frees through function parameters.
// FIXME: This is actually done twice, since the `update_call` method uses the same check.
// But to remove the check there we would have to know the callee function TID here
// even in the case when the call does not actually return at all.
self.check_internal_call_params_for_use_after_free(
&mut state_after_return,
&state_before_return.current_fn_tid,
&call.tid,
);
// Add object IDs of objects that may have been freed in the callee.
state_after_return.collect_freed_objects_from_called_function(
state_before_return,
id_replacement_map,
&call.tid,
pi_state_before_call,
);
Some(state_after_return)
}
/// Handle extern symbols by checking for Use-After-Frees in the call parameters.
/// Also handle calls to `free` by marking the corresponding object ID as dangling.
fn update_call_stub(&self, state: &State, call: &Term<Jmp>) -> Option<State> {
let mut state = state.clone();
if let Some(extern_symbol) = match &call.term {
Jmp::Call { target, .. } => self.project.program.term.extern_symbols.get(target),
_ => None,
} {
match extern_symbol.name.as_str() {
"free" => self.handle_call_to_free(&mut state, &call.tid, extern_symbol),
extern_symbol_name => {
if let Some(warnings) = self.collect_cwe_warnings_of_call_params(
&mut state,
&call.tid,
&extern_symbol.parameters,
) {
let cwe_warning = CweWarning {
name: "CWE416".to_string(),
version: CWE_MODULE.version.to_string(),
addresses: vec![call.tid.address.clone()],
tids: vec![format!("{}", call.tid)],
symbols: Vec::new(),
other: vec![warnings],
description: format!(
"(Use After Free) Call to {} at {} may access dangling pointers through its parameters",
extern_symbol_name,
call.tid.address
),
};
self.log_collector.send(cwe_warning.into()).unwrap();
}
}
}
} else if let Some(warnings) = self.collect_cwe_warnings_of_call_params(
&mut state,
&call.tid,
&self.generic_function_parameter,
) {
let cwe_warning = CweWarning {
name: "CWE416".to_string(),
version: CWE_MODULE.version.to_string(),
addresses: vec![call.tid.address.clone()],
tids: vec![format!("{}", call.tid)],
symbols: Vec::new(),
other: vec![warnings],
description: format!(
"(Use After Free) Call at {} may access dangling pointers through its parameters",
call.tid.address
),
};
self.log_collector.send(cwe_warning.into()).unwrap();
}
Some(state)
}
/// Just returns the unmodified state
fn specialize_conditional(
&self,
state: &State,
_condition: &Expression,
_block_before_condition: &Term<Blk>,
_is_true: bool,
) -> Option<State> {
Some(state.clone())
}
}
//! This module implements a check for CWE-415: Double Free and CWE-416: Use After Free.
//!
//! If a program tries to reference memory objects or other resources after they have been freed
//! it can lead to crashes, unexpected behaviour or even arbitrary code execution.
//! The same is true if the program tries to free the same resource more than once
//! as this can lead to another unrelated resource being freed instead.
//!
//! See <https://cwe.mitre.org/data/definitions/415.html> and <https://cwe.mitre.org/data/definitions/416.html> for detailed descriptions.
//!
//! ## How the check works
//!
//! Using an interprocedural, bottom-up dataflow analysis
//! based on the results of the [Pointer Inference analysis](`crate::analysis::pointer_inference`)
//! the check keeps track of memory objects that have already been freed.
//! If a pointer to an already freed object is used to access memory or provided as a parameter to another function
//! then a CWE warning is generated.
//! To prevent duplicate CWE warnings with the same root cause
//! the check also keeps track of objects for which a CWE warning was already generated.
//!
//! ## False Positives
//!
//! - Since the analysis is not path-sensitive, infeasible paths may lead to false positives.
//! - Any analysis imprecision of the pointer inference analysis
//! that leads to assuming that a pointer can target more memory objects that it actually can target
//! may lead to false positive CWE warnings in this check.
//!
//! ## False Negatives
//!
//! - Arrays of memory objects are not tracked by this analysis as we currently cannot distinguish different array elements in the analysis.
//! Subsequently, CWEs corresponding to arrays of memory objects are not detected.
//! - Memory objects not tracked by the Pointer Inference analysis or pointer targets missed by the Pointer Inference
//! may lead to missed CWEs in this check.
//! - The analysis currently only tracks pointers to objects that were freed by a call to `free`.
//! If a memory object is freed by another external function then this may lead to false negatives in this check.
use crate::prelude::*;
use crate::utils::log::CweWarning;
use crate::utils::log::LogMessage;
use crate::utils::log::LogThread;
use crate::CweModule;
/// The module name and version
pub static CWE_MODULE: CweModule = CweModule {
name: "CWE416",
version: "0.3",
run: check_cwe,
};
mod context;
use context::Context;
mod state;
use state::State;
/// Run the check for CWE-416: Use After Free.
///
/// This function prepares the bottom-up fixpoint computation
/// by initializing the state at the start of each function with the empty state (i.e. no dangling objects known)
/// and then executing the fixpoint algorithm.
/// Returns collected log messages and CWE warnings.
pub fn check_cwe(
analysis_results: &AnalysisResults,
_config: &serde_json::Value,
) -> (Vec<LogMessage>, Vec<CweWarning>) {
let log_thread = LogThread::spawn(LogThread::collect_and_deduplicate);
let context = Context::new(analysis_results, log_thread.get_msg_sender());
let mut fixpoint_computation =
crate::analysis::forward_interprocedural_fixpoint::create_computation(context, None);
for (sub_tid, entry_node_of_sub) in
crate::analysis::graph::get_entry_nodes_of_subs(analysis_results.control_flow_graph)
{
let fn_start_state = State::new(sub_tid);
fixpoint_computation.set_node_value(
entry_node_of_sub,
crate::analysis::interprocedural_fixpoint_generic::NodeValue::Value(fn_start_state),
);
}
fixpoint_computation.compute_with_max_steps(100);
let (logs, mut cwe_warnings) = log_thread.collect();
cwe_warnings.sort();
(logs, cwe_warnings)
}
use crate::analysis::pointer_inference::State as PiState;
use crate::{
abstract_domain::{AbstractDomain, AbstractIdentifier, DomainMap, UnionMergeStrategy},
analysis::pointer_inference::Data,
prelude::*,
};
use std::collections::BTreeMap;
/// The state of a memory object for which at least one possible call to a `free`-like function was detected.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
enum ObjectState {
/// The object is already freed, i.e. pointers to it are dangling.
/// The associated TID denotes the point in time when the object was freed.
Dangling(Tid),
/// The object is already freed and a use-after-free CWE message for it was already generated.
/// This object state is used to prevent duplicate CWE warnings with the same root cause.
AlreadyFlagged,
}
impl AbstractDomain for ObjectState {
/// Merge two object states.
/// If both object states are dangling then use the source TID of `self` in the result.
fn merge(&self, other: &Self) -> Self {
match (self, other) {
(ObjectState::AlreadyFlagged, _) | (_, ObjectState::AlreadyFlagged) => {
ObjectState::AlreadyFlagged
}
(ObjectState::Dangling(tid), ObjectState::Dangling(_)) => {
ObjectState::Dangling(tid.clone())
}
}
}
/// The `Top` element for object states is a dangling pointer.
fn is_top(&self) -> bool {
matches!(self, ObjectState::Dangling(_))
}
}
/// The `State` currently only keeps track of the list of TIDs of memory object that may have been freed already
/// together with the corresponding object states.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct State {
pub current_fn_tid: Tid,
dangling_objects: DomainMap<AbstractIdentifier, ObjectState, UnionMergeStrategy>,
}
impl State {
/// Create a new, empty state, i.e. a state without any object marked as already freed.
pub fn new(current_fn_tid: Tid) -> State {
State {
current_fn_tid,
dangling_objects: BTreeMap::new().into(),
}
}
/// Check the given address on whether it may point to already freed memory.
/// For each possible dangling pointer target a string describing the root cause is returnen.
/// The object states of corresponding memory objects are set to [`ObjectState::AlreadyFlagged`]
/// to prevent reporting duplicate CWE messages with the same root cause.
pub fn check_address_for_use_after_free(&mut self, address: &Data) -> Option<Vec<String>> {
let mut free_ids_of_dangling_pointers = Vec::new();
for id in address.get_relative_values().keys() {
if let Some(ObjectState::Dangling(free_id)) = self.dangling_objects.get(id) {
free_ids_of_dangling_pointers.push(format!(
"Accessed ID {} may have been already freed at {}",
id, free_id
));
self.dangling_objects
.insert(id.clone(), ObjectState::AlreadyFlagged);
}
}
if free_ids_of_dangling_pointers.is_empty() {
None
} else {
Some(free_ids_of_dangling_pointers)
}
}
/// All TIDs that the given `param` may point to are marked as freed, i.e. pointers to them are dangling.
/// For each ID that was already marked as dangling return a string describing the root cause of a possible double free bug.
pub fn handle_param_of_free_call(
&mut self,
call_tid: &Tid,
param: &Data,
pi_state: &PiState,
) -> Option<Vec<String>> {
// FIXME: This function could also generate debug log messages whenever nonsensical information is detected.
// E.g. stack frame IDs or non-zero ID offsets can be indicators of other bugs.
let mut warnings = Vec::new();
for id in param.get_relative_values().keys() {
if pi_state.memory.is_unique_object(id).ok() == Some(false) {
// FIXME: We cannot distinguish different objects represented by the same ID.
// So to avoid producing lots of false positive warnings
// we ignore these cases by not marking these IDs as freed.
continue;
}
if let Some(ObjectState::Dangling(old_free_id)) = self
.dangling_objects
.insert(id.clone(), ObjectState::Dangling(call_tid.clone()))
{
warnings.push(format!(
"Object {} may have been freed before at {}.",
id, old_free_id
));
}
}
if !warnings.is_empty() {
Some(warnings)
} else {
None
}
}
/// Add objects that were freed in the callee of a function call to the list of dangling pointers of `self`.
/// May return a list of warnings if cases of possible double frees are detected,
/// i.e. if an already freed object may also have been freed in the callee.
pub fn collect_freed_objects_from_called_function(
&mut self,
state_before_return: &State,
id_replacement_map: &BTreeMap<AbstractIdentifier, Data>,
call_tid: &Tid,
pi_state: &PiState,
) {
for (callee_id, callee_object_state) in state_before_return.dangling_objects.iter() {
if let Some(caller_value) = id_replacement_map.get(callee_id) {
for caller_id in caller_value.get_relative_values().keys() {
if pi_state.memory.is_unique_object(caller_id).ok() != Some(false) {
// FIXME: We cannot distinguish different objects represented by the same ID.
// So to avoid producing lots of false positive warnings we ignore these cases.
match (callee_object_state, self.dangling_objects.get(caller_id)) {
// Case 1: The dangling object is unknown to the caller, so we add it.
(ObjectState::Dangling(_), None)
| (ObjectState::AlreadyFlagged, None) => {
self.dangling_objects.insert(
caller_id.clone(),
ObjectState::Dangling(call_tid.clone()),
);
}
// Case 2: The dangling object is already known to the caller.
// If this were a case of Use-After-Free, then this should have been flagged when checking the call parameters.
// Thus we can simply leave the object state as it is.
(_, Some(ObjectState::Dangling(_)))
| (_, Some(&ObjectState::AlreadyFlagged)) => (),
}
}
}
}
}
}
}
impl AbstractDomain for State {
/// Merge two states.
fn merge(&self, other: &Self) -> Self {
State {
current_fn_tid: self.current_fn_tid.clone(),
dangling_objects: self.dangling_objects.merge(&other.dangling_objects),
}
}
/// Always returns false. The state has no logical `Top` element.
fn is_top(&self) -> bool {
false
}
}
#[cfg(test)]
pub mod tests {
use crate::intermediate_representation::Variable;
use super::*;
#[test]
fn test_check_address_for_use_after_free() {
let mut state = State::new(Tid::new("current_fn"));
state.dangling_objects.insert(
AbstractIdentifier::mock("obj_id", "RAX", 8),
ObjectState::Dangling(Tid::new("free_call")),
);
state.dangling_objects.insert(
AbstractIdentifier::mock("flagged_obj_id", "RAX", 8),
ObjectState::AlreadyFlagged,
);
let address = Data::mock_from_target_map(BTreeMap::from([
(
AbstractIdentifier::mock("obj_id", "RAX", 8),
Bitvector::from_i64(0).into(),
),
(
AbstractIdentifier::mock("flagged_obj_id", "RAX", 8),
Bitvector::from_i64(0).into(),
),
]));
// Check that one warning is generated for the dangling pointer
// and that afterwards all corresponding IDs are marked as already flagged.
assert_eq!(
state
.check_address_for_use_after_free(&address)
.unwrap()
.len(),
1
);
assert_eq!(
*state
.dangling_objects
.get(&AbstractIdentifier::mock("obj_id", "RAX", 8))
.unwrap(),
ObjectState::AlreadyFlagged
);
assert_eq!(
*state
.dangling_objects
.get(&AbstractIdentifier::mock("flagged_obj_id", "RAX", 8))
.unwrap(),
ObjectState::AlreadyFlagged
);
}
#[test]
fn test_handle_param_of_free_call() {
let mut state = State::new(Tid::new("current_fn"));
let param = Data::from_target(
AbstractIdentifier::mock("obj_id", "RAX", 8),
Bitvector::from_i64(0).into(),
);
let pi_state = PiState::new(&Variable::mock("RSP", 8), Tid::new("call"));
// Check that the parameter is correctly marked as freed in the state.
assert!(state
.handle_param_of_free_call(&Tid::new("free_call"), &param, &pi_state)
.is_none());
assert_eq!(
*state
.dangling_objects
.get(&AbstractIdentifier::mock("obj_id", "RAX", 8))
.unwrap(),
ObjectState::Dangling(Tid::new("free_call"))
);
// Check that a second free operation yields a double free warning.
assert!(state
.handle_param_of_free_call(&Tid::new("free_call"), &param, &pi_state)
.is_some());
}
#[test]
fn test_collect_freed_objects_from_called_function() {
let mut state = State::new(Tid::new("current_fn"));
let mut state_before_return = State::new(Tid::new("callee_fn_tid"));
state_before_return.dangling_objects.insert(
AbstractIdentifier::mock("callee_obj_tid", "RAX", 8),
ObjectState::Dangling(Tid::new("free_tid")),
);
let pi_state = PiState::new(&Variable::mock("RSP", 8), Tid::new("call"));
let id_replacement_map = BTreeMap::from([(
AbstractIdentifier::mock("callee_obj_tid", "RAX", 8),
Data::from_target(
AbstractIdentifier::mock("caller_tid", "RBX", 8),
Bitvector::from_i64(42).into(),
),
)]);
// Check that the callee object ID is correctly translated to a caller object ID
state.collect_freed_objects_from_called_function(
&state_before_return,
&id_replacement_map,
&Tid::new("call_tid"),
&pi_state,
);
assert_eq!(state.dangling_objects.len(), 1);
assert_eq!(
state
.dangling_objects
.get(&AbstractIdentifier::mock("caller_tid", "RBX", 8))
.unwrap(),
&ObjectState::Dangling(Tid::new("call_tid"))
);
}
}
......@@ -123,6 +123,7 @@ pub fn get_modules() -> Vec<&'static CweModule> {
&crate::checkers::cwe_243::CWE_MODULE,
&crate::checkers::cwe_332::CWE_MODULE,
&crate::checkers::cwe_367::CWE_MODULE,
&crate::checkers::cwe_416::CWE_MODULE,
&crate::checkers::cwe_426::CWE_MODULE,
&crate::checkers::cwe_467::CWE_MODULE,
&crate::checkers::cwe_476::CWE_MODULE,
......
......@@ -423,7 +423,7 @@ mod tests {
#[ignore]
fn cwe_415() {
let mut error_log = Vec::new();
let mut tests = all_test_cases("cwe_415", "Memory");
let mut tests = all_test_cases("cwe_415", "CWE416");
mark_architecture_skipped(&mut tests, "ppc64"); // Ghidra generates mangled function names here for some reason.
mark_architecture_skipped(&mut tests, "ppc64le"); // Ghidra generates mangled function names here for some reason.
......@@ -451,7 +451,7 @@ mod tests {
#[ignore]
fn cwe_416() {
let mut error_log = Vec::new();
let mut tests = all_test_cases("cwe_416", "Memory");
let mut tests = all_test_cases("cwe_416", "CWE416");
mark_architecture_skipped(&mut tests, "ppc64"); // Ghidra generates mangled function names here for some reason.
mark_architecture_skipped(&mut tests, "ppc64le"); // Ghidra generates mangled function names here for some reason.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment