Unverified Commit 275c13aa by Enkelmann Committed by GitHub

Buffer overflow CWE checks (#174)

parent b9b6b6dd
......@@ -113,39 +113,109 @@ impl<'a> Context<'a> {
}
}
/// If the given `extern_symbol` is a call to a known allocation function
/// return the size of the memory object allocated by it.
///
/// The function returns a `Top` element if the size could not be determined.
/// Known allocation functions: `malloc`, `realloc`, `calloc`.
fn get_allocation_size_of_alloc_call(
&self,
state: &State,
extern_symbol: &ExternSymbol,
) -> ValueDomain {
let address_bytesize = self.project.get_pointer_bytesize();
let object_size = match extern_symbol.name.as_str() {
"malloc" => {
let size_parameter = extern_symbol.parameters.get(0).unwrap();
state
.eval_parameter_arg(
size_parameter,
&self.project.stack_pointer_register,
self.runtime_memory_image,
)
.unwrap_or_else(|_| Data::new_top(address_bytesize))
}
"realloc" => {
let size_parameter = extern_symbol.parameters.get(1).unwrap();
state
.eval_parameter_arg(
size_parameter,
&self.project.stack_pointer_register,
self.runtime_memory_image,
)
.unwrap_or_else(|_| Data::new_top(address_bytesize))
}
"calloc" => {
let size_param1 = extern_symbol.parameters.get(0).unwrap();
let size_param2 = extern_symbol.parameters.get(1).unwrap();
let param1_value = state
.eval_parameter_arg(
size_param1,
&self.project.stack_pointer_register,
self.runtime_memory_image,
)
.unwrap_or_else(|_| Data::new_top(address_bytesize));
let param2_value = state
.eval_parameter_arg(
size_param2,
&self.project.stack_pointer_register,
self.runtime_memory_image,
)
.unwrap_or_else(|_| Data::new_top(address_bytesize));
param1_value.bin_op(BinOpType::IntMult, &param2_value)
}
_ => DataDomain::new_top(address_bytesize),
};
match object_size {
Data::Value(val) => val,
_ => ValueDomain::new_top(address_bytesize),
}
}
/// Add a new abstract object and a pointer to it in the return register of an extern call.
/// This models the behaviour of `malloc`-like functions,
/// except that we cannot represent possible `NULL` pointers as return values yet.
fn add_new_object_in_call_return_register(
&self,
mut state: State,
state: &State,
mut new_state: State,
call: &Term<Jmp>,
extern_symbol: &ExternSymbol,
) -> State {
let address_bytesize = self.project.get_pointer_bytesize();
let object_size = self.get_allocation_size_of_alloc_call(&state, extern_symbol);
match extern_symbol.get_unique_return_register() {
Ok(return_register) => {
let object_id = AbstractIdentifier::new(
call.tid.clone(),
AbstractLocation::from_var(return_register).unwrap(),
);
let address_bytesize = self.project.get_pointer_bytesize();
state.memory.add_abstract_object(
new_state.memory.add_abstract_object(
object_id.clone(),
Bitvector::zero(apint::BitWidth::from(address_bytesize)).into(),
super::object::ObjectType::Heap,
address_bytesize,
);
new_state.memory.set_lower_index_bound(
&object_id,
&Bitvector::zero(address_bytesize.into()).into(),
);
new_state.memory.set_upper_index_bound(
&object_id,
&(object_size - Bitvector::one(address_bytesize.into()).into()),
);
let pointer = PointerDomain::new(
object_id,
Bitvector::zero(apint::BitWidth::from(address_bytesize)).into(),
);
state.set_register(return_register, pointer.into());
state
new_state.set_register(return_register, pointer.into());
new_state
}
Err(err) => {
// We cannot track the new object, since we do not know where to store the pointer to it.
self.log_debug(Err(err), Some(&call.tid));
state
new_state
}
}
}
......@@ -254,6 +324,51 @@ impl<'a> Context<'a> {
}
}
/// Check whether a parameter of a call to an extern symbol may point outside of the bounds of a memory object.
/// If yes, generate a CWE-warning,
/// since the pointer may be used for an out-of-bounds memory access by the function.
fn check_parameter_register_for_out_of_bounds_pointer(
&self,
state: &State,
call: &Term<Jmp>,
extern_symbol: &ExternSymbol,
) {
for parameter in extern_symbol.parameters.iter() {
match state.eval_parameter_arg(
parameter,
&self.project.stack_pointer_register,
&self.runtime_memory_image,
) {
Ok(data) => {
if state.pointer_contains_out_of_bounds_target(&data, self.runtime_memory_image)
{
let warning = CweWarning {
name: "CWE119".to_string(),
version: VERSION.to_string(),
addresses: vec![call.tid.address.clone()],
tids: vec![format!("{}", call.tid)],
symbols: Vec::new(),
other: Vec::new(),
description: format!(
"(Buffer Overflow) Call to {} at {} may access out-of-bounds memory",
extern_symbol.name,
call.tid.address
),
};
let _ = self.log_collector.send(LogThreadMsg::Cwe(warning));
}
}
Err(err) => self.log_debug(
Err(err.context(format!(
"Function parameter {:?} could not be evaluated",
parameter
))),
Some(&call.tid),
),
}
}
}
/// Check whether the jump is an indirect call whose target evaluates to a *Top* value in the given state.
fn is_indirect_call_with_top_target(&self, state: &State, call: &Term<Jmp>) -> bool {
match &call.term {
......@@ -277,7 +392,7 @@ impl<'a> Context<'a> {
let stack_register = &self.project.stack_pointer_register;
let stack_pointer = state_before_call.get_register(stack_register);
match self.project.cpu_architecture.as_str() {
"x86" | "x86_64" => {
"x86" | "x86_32" | "x86_64" => {
let offset = Bitvector::from_u64(stack_register.size.into())
.into_truncate(apint::BitWidth::from(stack_register.size))
.unwrap();
......
......@@ -32,6 +32,36 @@ impl<'a> crate::analysis::forward_interprocedural_fixpoint::Context<'a> for Cont
};
let _ = self.log_collector.send(LogThreadMsg::Cwe(warning));
}
// check for out-of-bounds memory access
if state.contains_out_of_bounds_mem_access(&def.term, self.runtime_memory_image) {
let (warning_name, warning_description) = match def.term {
Def::Load { .. } => (
"CWE125",
format!(
"(Out-of-bounds Read) Memory load at {} may be out of bounds",
def.tid.address
),
),
Def::Store { .. } => (
"CWE787",
format!(
"(Out-of-bounds Write) Memory write at {} may be out of bounds",
def.tid.address
),
),
Def::Assign { .. } => panic!(),
};
let warning = CweWarning {
name: warning_name.to_string(),
version: VERSION.to_string(),
addresses: vec![def.tid.address.clone()],
tids: vec![format!("{}", def.tid)],
symbols: Vec::new(),
other: Vec::new(),
description: warning_description,
};
let _ = self.log_collector.send(LogThreadMsg::Cwe(warning));
}
match &def.term {
Def::Store { address, value } => {
......@@ -59,8 +89,7 @@ impl<'a> crate::analysis::forward_interprocedural_fixpoint::Context<'a> for Cont
}
/// Update the state according to the effects of the given `Jmp` term.
/// Right now the state is not changed,
/// as specialization for conditional jumps is not implemented yet.
/// Right now the state is not changed.
fn update_jump(
&self,
value: &State,
......@@ -106,8 +135,14 @@ impl<'a> crate::analysis::forward_interprocedural_fixpoint::Context<'a> for Cont
// Note that this may lead to analysis errors if the function uses another calling convention.
callee_state.remove_callee_saved_register(cconv);
}
// Set the lower index bound for the caller stack frame.
callee_state
.memory
.set_lower_index_bound(&state.stack_id, &stack_offset_adjustment);
// Replace the caller stack ID with one determined by the call instruction.
// This has to be done *before* adding the new callee stack id to avoid confusing caller and callee stack ids in case of recursive calls.
// This has to be done *before* adding the new callee stack id
// to avoid confusing caller and callee stack ids in case of recursive calls.
callee_state.replace_abstract_id(
&state.stack_id,
&new_caller_stack_id,
......@@ -245,6 +280,12 @@ impl<'a> crate::analysis::forward_interprocedural_fixpoint::Context<'a> for Cont
// remove non-referenced objects from the state
state_after_return.remove_unreferenced_objects();
// remove the lower index bound of the stack frame
state_after_return.memory.set_lower_index_bound(
original_caller_stack_id,
&IntervalDomain::new_top(self.project.stack_pointer_register.size),
);
Some(state_after_return)
}
......@@ -265,6 +306,8 @@ impl<'a> crate::analysis::forward_interprocedural_fixpoint::Context<'a> for Cont
};
let mut new_state = state.clone();
if let Some(extern_symbol) = self.extern_symbol_map.get(call_target) {
// Generate a CWE-message if some argument is an out-of-bounds pointer.
self.check_parameter_register_for_out_of_bounds_pointer(state, call, extern_symbol);
// Clear non-callee-saved registers from the state.
let cconv = extern_symbol.get_calling_convention(&self.project);
new_state.clear_non_callee_saved_register(&cconv.callee_saved_register[..]);
......@@ -276,6 +319,7 @@ impl<'a> crate::analysis::forward_interprocedural_fixpoint::Context<'a> for Cont
match extern_symbol.name.as_str() {
malloc_like_fn if self.allocation_symbols.iter().any(|x| x == malloc_like_fn) => {
Some(self.add_new_object_in_call_return_register(
state,
new_state,
call,
extern_symbol,
......
......@@ -118,7 +118,17 @@ impl<'a> PointerInference<'a> {
if let Some(start_node_index) =
tid_to_graph_indices_map.get(&(block_tid, sub_tid.clone()))
{
Some((sub_tid.clone(), *start_node_index))
// We only add entry points that are also control flow graph roots
if context
.graph
.neighbors_directed(*start_node_index, Direction::Incoming)
.next()
.is_none()
{
Some((sub_tid.clone(), *start_node_index))
} else {
None
}
} else {
None
}
......
......@@ -49,6 +49,14 @@ pub struct AbstractObjectInfo {
type_: Option<ObjectType>,
/// The actual content of the memory object
memory: MemRegion<Data>,
/// The smallest index still contained in the memory region.
/// A `Top` value represents an unknown bound.
/// The bound is not enforced, i.e. reading and writing to indices violating the bound is still allowed.
lower_index_bound: BitvectorDomain,
/// The largest index still contained in the memory region.
/// A `Top` value represents an unknown bound.
/// The bound is not enforced, i.e. reading and writing to indices violating the bound is still allowed.
upper_index_bound: BitvectorDomain,
}
impl AbstractObjectInfo {
......@@ -60,6 +68,56 @@ impl AbstractObjectInfo {
state: Some(ObjectState::Alive),
type_: Some(type_),
memory: MemRegion::new(address_bytesize),
lower_index_bound: BitvectorDomain::Top(address_bytesize),
upper_index_bound: BitvectorDomain::Top(address_bytesize),
}
}
/// Set the lower index bound that is still considered to be contained in the abstract object.
pub fn set_lower_index_bound(&mut self, lower_bound: BitvectorDomain) {
self.lower_index_bound = lower_bound;
}
/// Set the upper index bound that is still considered to be contained in the abstract object.
pub fn set_upper_index_bound(&mut self, upper_bound: BitvectorDomain) {
self.upper_index_bound = upper_bound;
}
/// Check whether a memory access to the abstract object at the given offset
/// and with the given size of the accessed value is contained in the bounds of the memory object.
/// If `offset` contains more than one possible index value,
/// then only return `true` if the access is contained in the abstract object for all possible offset values.
pub fn access_contained_in_bounds(&self, offset: &ValueDomain, size: ByteSize) -> bool {
if let Ok(offset_interval) = offset.try_to_interval() {
if let Ok(lower_bound) = self.lower_index_bound.try_to_bitvec() {
if lower_bound.checked_sgt(&offset_interval.start).unwrap() {
return false;
}
}
if let Ok(upper_bound) = self.upper_index_bound.try_to_bitvec() {
let mut size_as_bitvec = Bitvector::from_u64(u64::from(size));
match offset.bytesize().cmp(&size_as_bitvec.bytesize()) {
std::cmp::Ordering::Less => size_as_bitvec.truncate(offset.bytesize()).unwrap(),
std::cmp::Ordering::Greater => {
size_as_bitvec.sign_extend(offset.bytesize()).unwrap()
}
std::cmp::Ordering::Equal => (),
}
let max_index = if let Some(val) = offset_interval
.end
.signed_add_overflow_checked(&size_as_bitvec)
{
val - &Bitvector::one(offset.bytesize().into())
} else {
return false; // The max index already causes an integer overflow
};
if upper_bound.checked_slt(&max_index).unwrap() {
return false;
}
}
true
} else {
false
}
}
......@@ -245,6 +303,8 @@ impl AbstractDomain for AbstractObjectInfo {
state: same_or_none(&self.state, &other.state),
type_: same_or_none(&self.type_, &other.type_),
memory: self.memory.merge(&other.memory),
lower_index_bound: self.lower_index_bound.merge(&other.lower_index_bound),
upper_index_bound: self.upper_index_bound.merge(&other.upper_index_bound),
}
}
......@@ -271,6 +331,14 @@ impl AbstractObjectInfo {
"type".to_string(),
serde_json::Value::String(format!("{:?}", self.type_)),
),
(
"lower_index_bound".to_string(),
serde_json::Value::String(format!("{}", self.lower_index_bound)),
),
(
"upper_index_bound".to_string(),
serde_json::Value::String(format!("{}", self.upper_index_bound)),
),
];
let memory = self
.memory
......@@ -322,6 +390,8 @@ mod tests {
state: Some(ObjectState::Alive),
type_: Some(ObjectType::Heap),
memory: MemRegion::new(ByteSize::new(8)),
lower_index_bound: Bitvector::from_u64(0).into(),
upper_index_bound: Bitvector::from_u64(99).into(),
};
AbstractObject(Arc::new(obj_info))
}
......@@ -442,4 +512,13 @@ mod tests {
.collect()
);
}
#[test]
fn access_contained_in_bounds() {
let object = new_abstract_object();
assert!(object.access_contained_in_bounds(&IntervalDomain::mock(0, 99), ByteSize::new(1)));
assert!(!object.access_contained_in_bounds(&IntervalDomain::mock(-1, -1), ByteSize::new(8)));
assert!(object.access_contained_in_bounds(&IntervalDomain::mock(92, 92), ByteSize::new(8)));
assert!(!object.access_contained_in_bounds(&IntervalDomain::mock(93, 93), ByteSize::new(8)));
}
}
use super::object::*;
use super::{Data, ValueDomain};
use crate::abstract_domain::*;
use crate::prelude::*;
use crate::{abstract_domain::*, utils::binary::RuntimeMemoryImage};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
......@@ -23,13 +23,16 @@ pub struct AbstractObjectList {
impl AbstractObjectList {
/// Create a new abstract object list with just one abstract object corresponding to the stack.
/// The offset into the stack object will be set to zero.
///
/// The offset into the stack object and the `upper_index_bound` of the stack object will be both set to zero.
/// This corresponds to the generic stack state at the start of a function.
pub fn from_stack_id(
stack_id: AbstractIdentifier,
address_bytesize: ByteSize,
) -> AbstractObjectList {
let mut objects = BTreeMap::new();
let stack_object = AbstractObject::new(ObjectType::Stack, address_bytesize);
let mut stack_object = AbstractObject::new(ObjectType::Stack, address_bytesize);
stack_object.set_upper_index_bound(Bitvector::zero(address_bytesize.into()).into());
objects.insert(
stack_id,
(
......@@ -68,6 +71,70 @@ impl AbstractObjectList {
false
}
/// Check whether a memory access at the given address (and accessing `size` many bytes)
/// may be an out-of-bounds memory access.
///
/// Note that `Top` values as addresses are not marked as out-of-bounds,
/// since they are more likely due to analysis imprecision than to actual out-of-bounds access.
pub fn is_out_of_bounds_mem_access(
&self,
address: &Data,
size: ByteSize,
global_data: &RuntimeMemoryImage,
) -> bool {
match address {
Data::Value(value) => {
if let Ok((start, end)) = value.try_to_offset_interval() {
if start < 0 || end < start {
return true;
}
return global_data
.is_interval_readable(start as u64, end as u64 + u64::from(size) - 1)
.is_err();
}
}
Data::Top(_) => (),
Data::Pointer(pointer) => {
for (id, offset) in pointer.targets() {
let (object, base_offset) = self.objects.get(id).unwrap();
let adjusted_offset = offset.clone() + base_offset.clone();
if !adjusted_offset.is_top()
&& !object.access_contained_in_bounds(&adjusted_offset, size)
{
return true;
}
}
}
}
false
}
/// Set the lower index bound for indices to be considered inside the memory object.
/// The bound is inclusive, i.e. the bound index itself is also considered to be inside the memory object.
///
/// Any `bound` value other than a constant bitvector is interpreted as the memory object not having a lower bound.
pub fn set_lower_index_bound(&mut self, object_id: &AbstractIdentifier, bound: &ValueDomain) {
let (object, base_offset) = self.objects.get_mut(object_id).unwrap();
let bound = (bound.clone() + base_offset.clone())
.try_to_bitvec()
.map(|bitvec| bitvec.into())
.unwrap_or_else(|_| BitvectorDomain::new_top(bound.bytesize()));
object.set_lower_index_bound(bound);
}
/// Set the upper index bound for indices to be considered inside the memory object.
/// The bound is inclusive, i.e. the bound index itself is also considered to be inside the memory object.
///
/// Any `bound` value other than a constant bitvector is interpreted as the memory object not having an upper bound.
pub fn set_upper_index_bound(&mut self, object_id: &AbstractIdentifier, bound: &ValueDomain) {
let (object, base_offset) = self.objects.get_mut(object_id).unwrap();
let bound = (bound.clone() + base_offset.clone())
.try_to_bitvec()
.map(|bitvec| bitvec.into())
.unwrap_or_else(|_| BitvectorDomain::new_top(bound.bytesize()));
object.set_upper_index_bound(bound);
}
/// Get the value at a given address.
/// If the address is not unique, merge the value of all possible addresses.
///
......
......@@ -257,6 +257,60 @@ impl State {
}
}
/// Returns `true` if the given `Def` is a load or store instruction
/// which may access a memory object outside its bounds.
pub fn contains_out_of_bounds_mem_access(
&self,
def: &Def,
global_data: &RuntimeMemoryImage,
) -> bool {
let (raw_address, size) = match def {
Def::Load { address, var } => (self.eval(address), var.size),
Def::Store { address, value } => (self.eval(address), value.bytesize()),
_ => return false,
};
if self.is_stack_pointer_with_nonnegative_offset(&raw_address) {
// Access to a parameter or the return address of the function
return false;
}
let address = self.adjust_pointer_for_read(&raw_address);
self.memory
.is_out_of_bounds_mem_access(&address, size, global_data)
}
/// Returns `true` if `data` is a pointer pointing outside of the bounds of a memory buffer.
/// Does not check whether `data` may represent an out-of-bounds access to global memory,
/// since this function assumes that all absolute values are not pointers.
pub fn pointer_contains_out_of_bounds_target(
&self,
data: &Data,
global_data: &RuntimeMemoryImage,
) -> bool {
let data = self.adjust_pointer_for_read(data);
matches!(data, Data::Pointer(_))
&& self
.memory
.is_out_of_bounds_mem_access(&data, ByteSize::new(1), global_data)
}
/// Return `true` if `data` is a pointer to the current stack frame with a constant positive address,
/// i.e. if it accesses a stack parameter (or the return-to address for x86) of the current function.
pub fn is_stack_pointer_with_nonnegative_offset(&self, data: &Data) -> bool {
if let Data::Pointer(pointer) = data {
if pointer.targets().len() == 1 {
let (target, offset) = pointer.targets().iter().next().unwrap();
if *target == self.stack_id {
if let Ok(offset_val) = offset.try_to_offset() {
if offset_val >= 0 {
return true;
}
}
}
}
}
false
}
/// If the given address is a positive stack offset and `self.caller_stack_ids` is non-empty,
/// i.e. it is an access to the caller stack, return the offset.
///
......
......@@ -345,6 +345,24 @@ impl State {
(Data::Value(old_value), Data::Value(result_value)) => {
self.set_register(var, old_value.intersect(&result_value)?.into())
}
(Data::Pointer(old_pointer), Data::Pointer(result_pointer)) => {
let mut specialized_targets = BTreeMap::new();
for (id, offset) in result_pointer.targets() {
if let Some(old_offset) = old_pointer.targets().get(id) {
if let Ok(specialized_offset) = old_offset.intersect(offset) {
specialized_targets.insert(id.clone(), specialized_offset);
}
}
}
if !specialized_targets.is_empty() {
self.set_register(
var,
PointerDomain::with_targets(specialized_targets).into(),
);
} else {
return Err(anyhow!("Pointer with no targets is unsatisfiable"));
}
}
(Data::Top(_), result) => self.set_register(var, result),
_ => (),
}
......@@ -421,25 +439,21 @@ impl State {
) -> Result<(), Error> {
match op {
BinOpType::IntAdd => {
if let Ok(bitvec) = self.eval(lhs).try_to_bitvec() {
let intermediate_result = result.clone() - bitvec.into();
self.specialize_by_expression_result(rhs, intermediate_result)?;
}
if let Ok(bitvec) = self.eval(rhs).try_to_bitvec() {
let intermediate_result = result - bitvec.into();
self.specialize_by_expression_result(lhs, intermediate_result)?;
}
let intermediate_result = result.clone() - self.eval(lhs);
self.specialize_by_expression_result(rhs, intermediate_result)?;
let intermediate_result = result - self.eval(rhs);
self.specialize_by_expression_result(lhs, intermediate_result)?;
return Ok(());
}
BinOpType::IntSub => {
if let Ok(bitvec) = self.eval(lhs).try_to_bitvec() {
let intermediate_result: Data = Data::from(bitvec) - result.clone();
self.specialize_by_expression_result(rhs, intermediate_result)?;
}
if let Ok(bitvec) = self.eval(rhs).try_to_bitvec() {
let intermediate_result = result + bitvec.into();
self.specialize_by_expression_result(lhs, intermediate_result)?;
}
let intermediate_result: Data = self.eval(lhs) - result.clone();
self.specialize_by_expression_result(rhs, intermediate_result)?;
let intermediate_result = result + self.eval(rhs);
self.specialize_by_expression_result(lhs, intermediate_result)?;
return Ok(());
}
_ => (),
......
......@@ -497,6 +497,25 @@ fn specialize_by_expression_results() {
.specialize_by_expression_result(&Expression::var("RAX"), Bitvector::from_i64(-20).into());
assert!(x.is_err());
let mut state = base_state.clone();
let abstract_id = AbstractIdentifier::new(
Tid::new("heap_obj"),
AbstractLocation::from_var(&register("RAX")).unwrap(),
);
state.set_register(
&register("RAX"),
PointerDomain::new(abstract_id.clone(), IntervalDomain::mock(0, 50)).into(),
);
let x = state.specialize_by_expression_result(
&Expression::var("RAX"),
PointerDomain::new(abstract_id.clone(), IntervalDomain::mock(20, 70)).into(),
);
assert!(x.is_ok());
assert_eq!(
state.get_register(&register("RAX")),
PointerDomain::new(abstract_id, IntervalDomain::mock(20, 50)).into()
);
// Expr = Const
let mut state = base_state.clone();
let x = state.specialize_by_expression_result(
......@@ -989,3 +1008,61 @@ fn specialize_by_unsigned_comparison_op() {
IntervalDomain::mock_with_bounds(Some(-19), -5, -1, None).into()
);
}
#[test]
fn stack_pointer_with_nonnegative_offset() {
let state = State::new(&register("RSP"), Tid::new("func_tid"));
let pointer = PointerDomain::new(state.stack_id.clone(), Bitvector::from_i64(-1).into()).into();
assert!(!state.is_stack_pointer_with_nonnegative_offset(&pointer));
let pointer = PointerDomain::new(state.stack_id.clone(), Bitvector::from_i64(5).into()).into();
assert!(state.is_stack_pointer_with_nonnegative_offset(&pointer));
let pointer = PointerDomain::new(state.stack_id.clone(), IntervalDomain::mock(2, 3)).into();
assert!(!state.is_stack_pointer_with_nonnegative_offset(&pointer)); // The offset is not a constant
}
#[test]
fn out_of_bounds_access_recognition() {
let mut state = State::new(&register("RSP"), Tid::new("func_tid"));
let global_data = RuntimeMemoryImage::mock();
let heap_obj_id = new_id("heap_malloc", "RAX");
state.memory.add_abstract_object(
heap_obj_id.clone(),
Bitvector::from_u64(0).into(),
crate::analysis::pointer_inference::object::ObjectType::Heap,
ByteSize::new(8),
);
state
.memory
.set_lower_index_bound(&heap_obj_id, &Bitvector::from_u64(0).into());
state
.memory
.set_upper_index_bound(&heap_obj_id, &Bitvector::from_u64(7).into());
let pointer = PointerDomain::new(heap_obj_id.clone(), Bitvector::from_i64(-1).into()).into();
assert!(state.pointer_contains_out_of_bounds_target(&pointer, &global_data));
let pointer = PointerDomain::new(heap_obj_id.clone(), Bitvector::from_u64(0).into()).into();
assert!(!state.pointer_contains_out_of_bounds_target(&pointer, &global_data));
let pointer = PointerDomain::new(heap_obj_id.clone(), Bitvector::from_u64(7).into()).into();
assert!(!state.pointer_contains_out_of_bounds_target(&pointer, &global_data));
let pointer = PointerDomain::new(heap_obj_id.clone(), Bitvector::from_u64(8).into()).into();
assert!(state.pointer_contains_out_of_bounds_target(&pointer, &global_data));
let address = PointerDomain::new(heap_obj_id.clone(), Bitvector::from_u64(0).into()).into();
state.set_register(&Variable::mock("RAX", 8), address);
let load_def = Def::load(
"tid",
Variable::mock("RBX", 8),
Expression::Var(Variable::mock("RAX", 8)),
);
assert!(!state.contains_out_of_bounds_mem_access(&load_def.term, &global_data));
let address = PointerDomain::new(heap_obj_id.clone(), Bitvector::from_u64(0).into()).into();
state.set_register(&Variable::mock("RAX", 8), address);
assert!(!state.contains_out_of_bounds_mem_access(&load_def.term, &global_data));
let address = PointerDomain::new(heap_obj_id.clone(), Bitvector::from_u64(1).into()).into();
state.set_register(&Variable::mock("RAX", 8), address);
assert!(state.contains_out_of_bounds_mem_access(&load_def.term, &global_data));
let address = PointerDomain::new(state.stack_id.clone(), Bitvector::from_u64(8).into()).into();
state.set_register(&Variable::mock("RAX", 8), address);
assert!(!state.contains_out_of_bounds_mem_access(&load_def.term, &global_data));
}
......@@ -37,6 +37,9 @@ pub trait BitvectorExtended: Sized {
/// Returns an error for bitvectors larger than 8 bytes,
/// since multiplication for them is not yet implemented in the [`apint`] crate.
fn signed_mult_with_overflow_flag(&self, rhs: &Self) -> Result<(Self, bool), Error>;
/// Return the size in bytes of the bitvector.
fn bytesize(&self) -> ByteSize;
}
impl BitvectorExtended for Bitvector {
......@@ -261,6 +264,11 @@ impl BitvectorExtended for Bitvector {
}
}
}
/// Return the size in bytes of the bitvector.
fn bytesize(&self) -> ByteSize {
self.width().into()
}
}
#[cfg(test)]
......
......@@ -460,7 +460,7 @@ impl Project {
pub fn get_standard_calling_convention(&self) -> Option<&CallingConvention> {
self.calling_conventions
.iter()
.find(|cconv| cconv.name == "__stdcall")
.find(|cconv| cconv.name == "__stdcall" || cconv.name == "__cdecl")
}
}
......
......@@ -66,7 +66,7 @@ mod prelude {
pub use apint::Width;
pub use serde::{Deserialize, Serialize};
pub use crate::intermediate_representation::{Bitvector, ByteSize};
pub use crate::intermediate_representation::{Bitvector, BitvectorExtended, ByteSize};
pub use crate::intermediate_representation::{Term, Tid};
pub use crate::AnalysisResults;
pub use anyhow::{anyhow, Error};
......
......@@ -78,9 +78,9 @@ def which(pgm):
def optimize(filename):
optimize_me = []
optimize_me = ["cwe_119.c"]
if filename in optimize_me:
return ' -O3'
return ' -O1'
else:
return ' -O0'
......
#include <stdlib.h>
#include <stdio.h>
void set_array_elements(int* array) {
for(int i = 0; i<= 10; i++) {
array[i] = i*i; // Out-of-bounds write for arrays that are too small.
}
}
void print_array_sum(int* array) {
int sum = 0;
for(int i = 0; i<= 10; i++) {
sum += array[i]; // Out-of-bounds read for arrays that are too small.
}
printf("%d\n", sum);
}
int main() {
int* array = calloc(5, sizeof(int));
set_array_elements(array);
free(array);
array = malloc(5 * sizeof(int));
print_array_sum(array);
puts((void*) array - 1); // Parameter is an out-of-bounds pointer.
free(array);
}
\ No newline at end of file
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
int id_sequence[3];
id_sequence[0] = 123;
id_sequence[1] = 234;
id_sequence[2] = 345;
id_sequence[0] = id_sequence[3];
}
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
int id_sequence[3];
id_sequence[0] = 123;
id_sequence[1] = 234;
id_sequence[2] = 345;
id_sequence[3] = 678;
}
......@@ -202,6 +202,61 @@ mod tests {
#[test]
#[ignore]
fn cwe_119() {
let mut error_log = Vec::new();
let mut tests = all_test_cases("cwe_119", "Memory");
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.
mark_skipped(&mut tests, "x86", "gcc"); // Loss of stack register value since we do not track pointer alignment yet.
for test_case in tests {
let num_expected_occurences = 1;
if let Err(error) = test_case.run_test("[CWE119]", num_expected_occurences) {
error_log.push((test_case.get_filepath(), error));
}
}
if !error_log.is_empty() {
print_errors(error_log);
panic!();
}
}
#[test]
#[ignore]
fn cwe_125() {
let mut error_log = Vec::new();
let mut tests = all_test_cases("cwe_119", "Memory");
mark_skipped(&mut tests, "aarch64", "clang"); // TODO: Check reason for failure!
mark_skipped(&mut tests, "mips64", "gcc"); // TODO: Check reason for failure!
mark_skipped(&mut tests, "mips64el", "gcc"); // TODO: Check reason for failure!
mark_skipped(&mut tests, "mips", "clang"); // TODO: Check reason for failure!
mark_skipped(&mut tests, "mipsel", "clang"); // TODO: Check reason for failure!
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.
mark_skipped(&mut tests, "x86", "gcc"); // Loss of stack register value since we do not track pointer alignment yet.
mark_skipped(&mut tests, "x86", "clang"); // TODO: Check reason for failure!
mark_compiler_skipped(&mut tests, "mingw32-gcc"); // TODO: Check reason for failure!
for test_case in tests {
let num_expected_occurences = 1;
if let Err(error) = test_case.run_test("[CWE125]", num_expected_occurences) {
error_log.push((test_case.get_filepath(), error));
}
}
if !error_log.is_empty() {
print_errors(error_log);
panic!();
}
}
#[test]
#[ignore]
fn cwe_190() {
let mut error_log = Vec::new();
let mut tests = all_test_cases("cwe_190", "CWE190");
......@@ -509,4 +564,38 @@ mod tests {
panic!();
}
}
#[test]
#[ignore]
fn cwe_787() {
let mut error_log = Vec::new();
let mut tests = all_test_cases("cwe_119", "Memory");
mark_skipped(&mut tests, "arm", "gcc"); // TODO: Check reason for failure!
mark_skipped(&mut tests, "mips64", "gcc"); // TODO: Check reason for failure!
mark_skipped(&mut tests, "mips64el", "gcc"); // TODO: Check reason for failure!
mark_architecture_skipped(&mut tests, "mips"); // TODO: Check reason for failure!
mark_architecture_skipped(&mut tests, "mipsel"); // TODO: Check reason for failure!
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.
mark_skipped(&mut tests, "ppc", "gcc"); // TODO: Check reason for failure!
mark_skipped(&mut tests, "x86", "gcc"); // Loss of stack register value since we do not track pointer alignment yet.
mark_compiler_skipped(&mut tests, "mingw32-gcc"); // TODO: Check reason for failure!
for test_case in tests {
let num_expected_occurences = 1;
if let Err(error) = test_case.run_test("[CWE787]", num_expected_occurences) {
error_log.push((test_case.get_filepath(), error));
}
}
if !error_log.is_empty() {
print_errors(error_log);
panic!();
}
}
}
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