Unverified Commit 041b795b by Enkelmann Committed by GitHub

Cwe476 (#47)

refactored cwe476 to add stack tracking
parent f3cb4d8e
dev
====
- Added a lot more test cases to acceptance tests (PR #46)
- Reworked CWE-476 check to track stack variables (PR #47)
0.3 (2019-12) 0.3 (2019-12)
==== ====
......
...@@ -152,3 +152,25 @@ let rec equal (mem_region1:'a t) (mem_region2:'a t) ~data_equal : bool = ...@@ -152,3 +152,25 @@ let rec equal (mem_region1:'a t) (mem_region2:'a t) ~data_equal : bool =
else else
false false
| _ -> false | _ -> false
let map_data (mem_region: 'a t) ~(f: 'a -> 'b) : 'b t =
List.map mem_region ~f:(fun mem_node ->
{ pos = mem_node.pos;
size = mem_node.size;
data = Result.map mem_node.data ~f
}
)
let list_data (mem_region: 'a t) : 'a List.t =
List.filter_map mem_region ~f:(fun mem_node ->
match mem_node.data with
| Ok(value) -> Some(value)
| Error(_) -> None
)
let list_data_pos (mem_region: 'a t) : (Bitvector.t * 'a) List.t =
List.filter_map mem_region ~f:(fun mem_node ->
match mem_node.data with
| Ok(value) -> Some( mem_node.pos, value )
| Error(_) -> None
)
...@@ -37,3 +37,12 @@ val equal: 'a t -> 'a t -> data_equal:('a -> 'a -> bool) -> bool ...@@ -37,3 +37,12 @@ val equal: 'a t -> 'a t -> data_equal:('a -> 'a -> bool) -> bool
(** Mark an area in the mem_region as containing errors. *) (** Mark an area in the mem_region as containing errors. *)
val mark_error: 'a t -> pos:Bitvector.t -> size:Bitvector.t -> 'a t val mark_error: 'a t -> pos:Bitvector.t -> size:Bitvector.t -> 'a t
(** Map the contained data to new values. *)
val map_data: 'a t -> f:('a -> 'b) -> 'b t
(** List the contained data (ignoring error values). *)
val list_data: 'a t -> 'a List.t
(** List the contained data (ignoring error values) together with their positions. *)
val list_data_pos: 'a t -> (Bitvector.t * 'a) List.t
...@@ -196,10 +196,7 @@ module TypeInfo = struct ...@@ -196,10 +196,7 @@ module TypeInfo = struct
let remove_virtual_registers state = let remove_virtual_registers state =
{ state with reg = Map.filter_keys state.reg ~f:(fun var -> Var.is_physical var) } { state with reg = Map.filter_keys state.reg ~f:(fun var -> Var.is_physical var) }
(** if the addr_exp is a (computable) stack offset, return the offset. In cases where addr_expr let compute_stack_offset (state: t) (addr_exp: Exp.t) ~(sub_tid: Tid.t) ~(project: Project.t) : Bitvector.t Option.t =
may or may not be a stack offset (i.e. offset of a register which may point to the stack or
to some other memory region), it still returns an offset. *)
let compute_stack_offset state addr_exp ~sub_tid ~project : Bitvector.t Option.t =
let (register, offset) = match addr_exp with let (register, offset) = match addr_exp with
| Bil.Var(var) -> (Some(var), Bitvector.of_int 0 ~width:(Symbol_utils.arch_pointer_size_in_bytes project * 8)) | Bil.Var(var) -> (Some(var), Bitvector.of_int 0 ~width:(Symbol_utils.arch_pointer_size_in_bytes project * 8))
| Bil.BinOp(Bil.PLUS, Bil.Var(var), Bil.Int(num)) -> (Some(var), num) | Bil.BinOp(Bil.PLUS, Bil.Var(var), Bil.Int(num)) -> (Some(var), num)
...@@ -280,7 +277,7 @@ let value_of_exp exp = ...@@ -280,7 +277,7 @@ let value_of_exp exp =
| _ -> None | _ -> None
let rec type_of_exp exp (state: TypeInfo.t) ~sub_tid ~project = let rec type_of_exp (exp: Exp.t) (state: TypeInfo.t) ~(sub_tid: Tid.t) ~(project: Project.t) : (Register.t, unit) Result.t Option.t =
let open Register in let open Register in
match exp with match exp with
| Bil.Load(_) -> (* TODO: Right now only the stack is tracked for type infos. *) | Bil.Load(_) -> (* TODO: Right now only the stack is tracked for type infos. *)
...@@ -321,7 +318,8 @@ let rec type_of_exp exp (state: TypeInfo.t) ~sub_tid ~project = ...@@ -321,7 +318,8 @@ let rec type_of_exp exp (state: TypeInfo.t) ~sub_tid ~project =
| Bil.Unknown(_) -> None | Bil.Unknown(_) -> None
| Bil.Ite(_if_, then_, else_) -> begin | Bil.Ite(_if_, then_, else_) -> begin
match (type_of_exp then_ state ~sub_tid ~project, type_of_exp else_ state ~sub_tid ~project) with match (type_of_exp then_ state ~sub_tid ~project, type_of_exp else_ state ~sub_tid ~project) with
| (Some(value1), Some(value2)) -> if value1 = value2 then Some(value1) else None | (Some(Ok(value1)), Some(Ok(value2))) -> if Register.equal value1 value2 then Some(Ok(value1)) else None
| (Some(Error ()), Some(Error ())) -> Some(Error ())
| _ -> None | _ -> None
end end
| Bil.Extract(_) -> Some(Ok(Data)) (* TODO: Similar to cast: Are there cases of 32bit-64bit-address-conversions here? *) | Bil.Extract(_) -> Some(Ok(Data)) (* TODO: Similar to cast: Are there cases of 32bit-64bit-address-conversions here? *)
...@@ -643,7 +641,26 @@ let print_type_info_tags ~project ~tid_map = ...@@ -643,7 +641,26 @@ let print_type_info_tags ~project ~tid_map =
(Tid.name (Term.tid block)) in (Tid.name (Term.tid block)) in
Log_utils.error error_str Log_utils.error error_str
) )
) )
let get_type_info_of_block ~(project: Project.t) (block: Blk.t) ~(sub_tid: Tid.t) : TypeInfo.t Tid.Map.t =
match Term.get_attr block type_info_tag with
| Some(start_state) ->
let elements = Blk.elts block in
let (type_info_map, _) = Seq.fold elements ~init:(Tid.Map.empty, start_state) ~f:(fun (type_info_map, state) element ->
match element with
| `Phi _ -> (type_info_map, state)
| `Def term ->
let new_type_info_map = Tid.Map.set type_info_map ~key:(Term.tid term) ~data:state in
let new_state = update_type_info element state ~sub_tid ~project in
(new_type_info_map, new_state)
| `Jmp term ->
let new_type_info_map = Tid.Map.set type_info_map ~key:(Term.tid term) ~data:state in
let new_state = update_type_info element state ~sub_tid ~project in
(new_type_info_map, new_state)
) in
type_info_map
| None -> failwith "[cwe_checker] Error: Tag not found"
(* Functions made available for unit tests *) (* Functions made available for unit tests *)
module Private = struct module Private = struct
......
...@@ -45,6 +45,12 @@ module TypeInfo : sig ...@@ -45,6 +45,12 @@ module TypeInfo : sig
(* Pretty Printer. At the moment, the output is not pretty at all. *) (* Pretty Printer. At the moment, the output is not pretty at all. *)
val pp: Format.formatter -> t -> unit val pp: Format.formatter -> t -> unit
(** if the addr_exp is a (computable) stack offset, return the offset. In cases where addr_expr
may or may not be a stack offset (i.e. offset of a register which may point to the stack or
to some other memory region), it still returns an offset. *)
val compute_stack_offset: t -> Exp.t -> sub_tid:Tid.t -> project:Project.t -> Bitvector.t Option.t
end end
(** A tag for TypeInfo.t, so that we can annotate basic blocks with known type information (** A tag for TypeInfo.t, so that we can annotate basic blocks with known type information
...@@ -65,6 +71,12 @@ val print_type_info_tags: project:Project.t -> tid_map:word Tid.Map.t -> unit ...@@ -65,6 +71,12 @@ val print_type_info_tags: project:Project.t -> tid_map:word Tid.Map.t -> unit
which is internally used to mark which pointers point to the current stack frame.*) which is internally used to mark which pointers point to the current stack frame.*)
val update_type_info: Blk.elt -> TypeInfo.t -> sub_tid:Tid.t -> project:Project.t -> TypeInfo.t val update_type_info: Blk.elt -> TypeInfo.t -> sub_tid:Tid.t -> project:Project.t -> TypeInfo.t
(** Get the type info for each def term and jump term in a block.
The returned type info for the tid of a (def/jmp) term is the tid before that term.
The sub_tid is the Tid of the current function which is internally used to mark
which pointers point to the current stack frame. *)
val get_type_info_of_block: project:Project.t -> Blk.t -> sub_tid:Tid.t -> TypeInfo.t Tid.Map.t
(* functions made available for unit tests: *) (* functions made available for unit tests: *)
module Private : sig module Private : sig
val update_block_analysis: Blk.t -> TypeInfo.t -> sub_tid:Tid.t -> project:Project.t -> TypeInfo.t val update_block_analysis: Blk.t -> TypeInfo.t -> sub_tid:Tid.t -> project:Project.t -> TypeInfo.t
......
...@@ -3,166 +3,306 @@ open Bap.Std ...@@ -3,166 +3,306 @@ open Bap.Std
open Log_utils open Log_utils
let name = "CWE476" let name = "CWE476"
let version = "0.2" let version = "0.3"
(* Access type denotes whether a variable var gets accessed or the memory stored at (* TODO: This check is based on Mem_region, which does not support partial access yet.
address var gets accessed *) Thus partially written tainted values may be marked as error and thus the taint is falsely forgotten. *)
type access_type = | Access of Bil.var | MemAccess of Bil.var | NoAccess
(* The union of two accesses is the higher access with MemAcces > Access > NoAccess *)
let union_access access1 access2 : access_type =
match (access1, access2) with
| (MemAccess(_), _) -> access1
| (_, MemAccess(_))
| (_, Access(_)) -> access2
| _ -> access1
(** Each taint is denoted by the Tid of the basic block where it originated from.
Each value can be tainted by different sources at the same time. *)
module Taint = Tid.Set
(* union of three accesses for convenience *) (** The state contains taint information for all registers and stack variables. *)
let union_access_triple access1 access2 access3 =
union_access access1 access2
|> union_access access3
(* the state contains a list of pairs of register names containing an unchecked
return value and the term identifiers of the block where the unchecked
return value was generated. *)
module State = struct module State = struct
type t = (Var.t * Tid.t) list type t = {
register: Taint.t Var.Map.t;
stack: Taint.t Mem_region.t;
} [@@deriving bin_io, compare, sexp]
(** Get an empty state without tainted values. *)
let empty : t =
{ register = Var.Map.empty;
stack = Mem_region.empty () }
(** adds var as a tainted register (with the taint source given by tid) *) (** equality function for states *)
let add state var tid = let equal (state1: t) (state2: t) : Bool.t =
let state = List.Assoc.remove state ~equal:Var.(=) var in let reg_equal = Var.Map.equal Taint.equal state1.register state2.register in
List.Assoc.add state ~equal:Var.(=) var tid let stack_equal = Mem_region.equal state1.stack state2.stack ~data_equal:Taint.equal in
reg_equal && stack_equal
(** returns Some(tid) if var is a tainted register, None otherwise *) (** set the taint of a register *)
let find state var = let set_register (state: t) (register: Var.t) (taint: Taint.t) : t =
List.Assoc.find state ~equal:Var.(=) var { state with register = Var.Map.set state.register ~key:register ~data: taint}
(** returns the tid associated with a tainted register *) (** return the taint of a register *)
let find_exn state var = let find_register (state: t) (register: Var.t) : Taint.t Option.t =
Option.value_exn (find state var) Var.Map.find state.register register
(** only remove the register var from the list of tainted registers *) (** only remove the register var from the list of tainted registers *)
let remove_var state var = let remove_register (state: t) (register: Var.t) : t =
List.Assoc.remove state ~equal:Var.(=) var { state with register = Var.Map.remove state.register register }
(** filters out all registers from the state with the same tid *) (** set the taint of a stack element *)
let remove_tid state var = let set_stack (state: t) ~(pos: Bitvector.t) ~(size: Bitvector.t) (taint: Taint.t) : t =
let tid = find_exn state var in { state with stack = Mem_region.add state.stack taint ~pos ~size }
List.filter state ~f:(fun (_, state_elem_tid) -> not (state_elem_tid = tid))
(** get the taint from the stack
(** two states are equal if they contain the same set of tainted registers*) TODO: Mem_region is currently unsound for only partially loaded values, which might lead to errors here. *)
let equal state1 state2 = let find_stack (state: t) ~(pos: Bitvector.t) : Taint.t Option.t =
(List.length state1) = (List.length state2) && match Mem_region.get state.stack pos with
not (List.exists state1 ~f:(fun (var, _tid) -> Option.is_none (find state2 var) )) | Some(Ok(taint, _size)) -> Some(taint)
| _ -> None
(** The union of two states is the union of the tainted registers*)
let union state1 state2 = (** remove a stack element *)
List.fold state2 ~init:state1 ~f:(fun state (var, tid) -> let remove_stack (state: t) ~(pos: Bitvector.t) ~(size: Bitvector.t) : t =
if Option.is_some (find state var) then { state with stack = Mem_region.remove state.stack ~pos ~size}
state
else (** remove all Tids contained in the taint from all taints in the state *)
(var, tid) :: state let remove_taint (state: t) (taint_to_remove: Taint.t) : t =
) let register_list = Var.Map.to_alist state.register in
let cleaned_register = List.fold register_list ~init:Var.Map.empty ~f:(fun cleaned_register (register, taint) ->
(** remove virtual registers from the state (useful at the end of a block) *) let cleaned_taint = Tid.Set.diff taint taint_to_remove in
let remove_virtual_registers state = if Tid.Set.is_empty cleaned_taint then
List.filter state ~f:(fun (var, _tid) -> Var.is_physical var) cleaned_register
else
Var.Map.set cleaned_register ~key:register ~data:cleaned_taint
) in
let cleaned_stack = Mem_region.map_data state.stack ~f:(fun taint ->
Tid.Set.diff taint taint_to_remove
) in
{ register = cleaned_register;
stack = cleaned_stack; }
(** The union of two states is the union of all taints *)
let union (state1: t) (state2: t) : t =
let register = Var.Map.merge state1.register state2.register ~f:(fun ~key:_ values->
match values with
| `Both (taint1, taint2) -> Some (Taint.union taint1 taint2)
| `Left taint | `Right taint -> Some taint
) in
let stack = Mem_region.merge state1.stack state2.stack ~data_merge:(fun taint1 taint2 ->
Some( Ok(Taint.union taint1 taint2) )
) in
{ register = register;
stack = stack; }
(** remove virtual register from the state (useful at the end of a block) *)
let remove_virtual_register (state: t) : t =
{ state with register = Var.Map.filter_keys state.register ~f:(fun var -> Var.is_physical var) }
end
(** The stack info contains all necessary information to access stack variables. *)
module StackInfo = struct
type t = {
type_info: Type_inference.TypeInfo.t;
sub_tid: Tid.t;
project: Project.t;
strict_mem_policy: Bool.t;
}
(** If the expression denotes an address on the stack, return the address. *)
let get_address (stack_info: t) (expression: Exp.t) : Bitvector.t Option.t =
Type_inference.TypeInfo.compute_stack_offset stack_info.type_info expression ~sub_tid:stack_info.sub_tid ~project:stack_info.project
(** Assemble a StackInfo.t object. *)
let assemble (pointer_info_map: Type_inference.TypeInfo.t Tid.Map.t) (term_tid: Tid.t) ~(sub_tid: Tid.t) ~(project: Project.t) ~(strict_mem_policy: Bool.t) : t =
{ type_info = Tid.Map.find_exn pointer_info_map term_tid;
sub_tid = sub_tid;
project = project;
strict_mem_policy = strict_mem_policy; }
(**/**)
(* assemble a mock StackInfo for unit tests *)
let assemble_mock_info (mock_tid: Tid.t) (project: Project.t) : t =
{ type_info = { Type_inference.TypeInfo.stack = Mem_region.empty (); Type_inference.TypeInfo.reg = Var.Map.empty};
sub_tid = mock_tid;
project = project;
strict_mem_policy = false; }
(**/**)
end end
(* check whether an expression contains an unchecked value. *)
let rec contains_unchecked exp state : access_type = (** append taint to the list of already found cwe_hits *)
let append_to_hits (cwe_hits:Taint.t ref) (taint: Taint.t) : unit =
cwe_hits := Taint.union !cwe_hits taint
(** Check whether an expression contains a tainted value.
Memory accesses through tainted values are added to cwe_hits, but the Tids are not removed from the state. *)
let rec contains_taint (exp: Exp.t) (state: State.t) ~(cwe_hits: Taint.t ref) ~(stack: StackInfo.t) : Taint.t =
match exp with match exp with
| Bil.Load(_mem, addr, _, _)-> | Bil.Load(_mem, addr, _endian, _size)->
begin begin
let acc = contains_unchecked addr state in let access_taint = contains_taint addr state ~cwe_hits ~stack in
match acc with let () = if Taint.is_empty access_taint = false then append_to_hits cwe_hits access_taint in
| MemAccess(_) -> acc match StackInfo.get_address stack addr with
| Access(var) -> MemAccess(var) | Some(stack_offset) -> Option.value (State.find_stack state ~pos:stack_offset) ~default:Taint.empty
| NoAccess -> NoAccess | None -> Taint.empty
end end
| Bil.Store(_mem, addr, val_expression, _,_) -> | Bil.Store(_mem, addr, val_expression, _,_) ->
begin begin
let acc = union_access (contains_unchecked addr state) (contains_unchecked val_expression state) in let access_taint = contains_taint addr state ~cwe_hits ~stack in
match acc with let value_taint = contains_taint val_expression state ~cwe_hits ~stack in
| MemAccess(_) -> acc let () = if Taint.is_empty access_taint = false then append_to_hits cwe_hits access_taint in
| Access(var) -> MemAccess(var) match StackInfo.get_address stack addr with
| NoAccess -> NoAccess | Some(_) -> Taint.empty
end | None ->
| Bil.BinOp(_, exp1, exp2) -> union_access (contains_unchecked exp1 state) (contains_unchecked exp2 state) let () = if stack.strict_mem_policy && (Taint.is_empty value_taint = false) then append_to_hits cwe_hits value_taint in
| Bil.UnOp(_, exp) -> contains_unchecked exp state Taint.empty
| Bil.Var(var) ->
begin
match State.find state var with
| Some(_) -> Access(var)
| None -> NoAccess
end end
| Bil.Int(_) -> NoAccess | Bil.BinOp(Bil.XOR, Bil.Var(var1), Bil.Var(var2)) when var1 = var2 -> Taint.empty (* standard assembly shortcut for setting a register to NULL *)
| Bil.Cast(_, _, exp) -> contains_unchecked exp state | Bil.BinOp(_, exp1, exp2) -> Taint.union (contains_taint exp1 state ~cwe_hits ~stack) (contains_taint exp2 state ~cwe_hits ~stack)
| Bil.UnOp(_, exp) -> contains_taint exp state ~cwe_hits ~stack
| Bil.Var(var) -> Option.value (State.find_register state var) ~default:Taint.empty
| Bil.Int(_) -> Taint.empty
| Bil.Cast(_, _, exp) -> contains_taint exp state ~cwe_hits ~stack
| Bil.Let(var, exp1, exp2) -> | Bil.Let(var, exp1, exp2) ->
union_access_triple (contains_unchecked exp1 state) (contains_unchecked exp2 state) (contains_unchecked (Bil.var var) state) Taint.union_list (
| Bil.Unknown(_) -> NoAccess (contains_taint exp1 state ~cwe_hits ~stack)
:: (contains_taint exp2 state ~cwe_hits ~stack)
:: (contains_taint (Bil.var var) state ~cwe_hits ~stack) :: [])
| Bil.Unknown(_) -> Taint.empty
| Bil.Ite(if_, then_, else_) -> | Bil.Ite(if_, then_, else_) ->
union_access_triple (contains_unchecked if_ state) (contains_unchecked then_ state) (contains_unchecked else_ state) Taint.union_list (
| Bil.Extract(_,_, exp) -> contains_unchecked exp state (contains_taint if_ state ~cwe_hits ~stack)
| Bil.Concat(exp1, exp2) -> union_access (contains_unchecked exp1 state) (contains_unchecked exp2 state) :: (contains_taint then_ state ~cwe_hits ~stack)
:: (contains_taint else_ state ~cwe_hits ~stack) :: [])
| Bil.Extract(_,_, exp) -> contains_taint exp state ~cwe_hits ~stack
| Bil.Concat(exp1, exp2) -> Taint.union (contains_taint exp1 state ~cwe_hits ~stack) (contains_taint exp2 state ~cwe_hits ~stack)
(* If an formerly unchecked return value was checked then remove all registers pointing
to the source of this return value from state. *) (** Parse an expression for memory accesses through tainted values and taint contained in the value itself.
let checks_value exp state : State.t = All memory accesses except for loading/storing values from/to the stack get flagged as cwe_hits.
Returns the taint of the expression and the new state, with the Tids of new cwe_hits removed from both. *)
let parse_taint_of_exp (exp: Exp.t) (state: State.t) ~(cwe_hits: Taint.t ref) ~(stack: StackInfo.t) : Taint.t * State.t =
let hits_to_clean : Taint.t ref = ref Taint.empty in
let unchecked_taint = contains_taint exp state ~cwe_hits:hits_to_clean ~stack in
let () = append_to_hits cwe_hits !hits_to_clean in
let state = State.remove_taint state !hits_to_clean in
let unchecked_taint = Taint.diff unchecked_taint !hits_to_clean in
(unchecked_taint, state)
(** If an formerly unchecked return value was checked then remove all registers pointing
to the source of this return value from state. *)
let checks_value (exp: Exp.t) (state: State.t) ~(cwe_hits: Taint.t ref) ~(stack: StackInfo.t) : State.t =
match exp with match exp with
| Bil.Ite(if_, _then_, _else_) -> begin | Bil.Ite(if_, _then_, _else_) -> begin
match contains_unchecked if_ state with let (taint_to_remove, state) = parse_taint_of_exp if_ state ~cwe_hits ~stack in
| Access(var) -> if Taint.is_empty taint_to_remove = false then
(* We filter out all registers with the same generating tid, since we have checked State.remove_taint state taint_to_remove
the return value of this source *) else
State.remove_tid state var state
| MemAccess(_) (* This is a memory access before checking the return value, so do nothing here. *)
| NoAccess -> state
end end
| _ -> state | _ -> state
let append_to_hits (cwe_hits:Tid.t list ref) (tid:Tid.t) =
match List.find cwe_hits.contents ~f:(fun elem -> elem = tid) with
| Some(_) -> ()
| None -> (cwe_hits := (tid :: cwe_hits.contents))
(** flags any access (not just memory access) from an unchecked source as a cwe_hit. *) (** flags any access (not just memory access) from an unchecked source as a cwe_hit. *)
let flag_any_access exp state ~cwe_hits = let flag_any_access (exp: Exp.t) (state: State.t) ~(cwe_hits: Taint.t ref) ~(stack: StackInfo.t) : State.t=
match contains_unchecked exp state with let (taint_to_flag, state) = parse_taint_of_exp exp state ~cwe_hits ~stack in
| MemAccess(var) | Access(var) -> let () = append_to_hits cwe_hits taint_to_flag in
let tid = State.find_exn state var in State.remove_taint state taint_to_flag
append_to_hits cwe_hits tid;
State.remove_tid state var
| NoAccess -> state (** flag all unchecked registers and stack variables that may be used as return values.
That means stack variables above the return pointer get flagged,
(** flag all unchecked registers as cwe_hits, return empty state *) but variables below the return pointer are treated as local variables and do not get flagged.
let flag_all_unchecked_registers state ~cwe_hits = Return empty state *)
let () = List.iter state ~f:(fun (_var, tid) -> let flag_unchecked_return_values (state: State.t) ~(cwe_hits: Taint.t ref) ~(project: Project.t) : State.t =
append_to_hits cwe_hits tid) in let taint_to_flag = Var.Map.fold state.register ~init:Taint.empty ~f:(fun ~key ~data taint_accum ->
[] if Cconv.is_return_register key project then
Taint.union taint_accum data
else
taint_accum
) in
let taint_to_flag = List.fold (Mem_region.list_data_pos state.stack) ~init:taint_to_flag ~f:(fun taint_accum (position_unsigned, taint_value) ->
let position = Bitvector.to_int_exn (Bitvector.signed position_unsigned) in
if position >= 0 then
Taint.union taint_accum taint_value
else
taint_accum
) in
let () = append_to_hits cwe_hits taint_to_flag in
State.empty
(** flag all register taints as cwe_hits, but not taints that are only contained in stack variables *)
let flag_register_taints (state: State.t) ~(cwe_hits: Taint.t ref) : State.t =
let taint_to_flag = List.fold (Var.Map.data state.register) ~init: Taint.empty ~f:(fun taint_accum register_taint ->
Taint.union taint_accum register_taint
) in
let () = append_to_hits cwe_hits taint_to_flag in
State.remove_taint state taint_to_flag
(** Flag all possible parameter register as cwe_hits. These registers may be input values to an extern function call.
This can lead to false positives if a function does not use all of these registers for argument passing. *)
let flag_parameter_register (state: State.t) ~(cwe_hits: Taint.t ref) ~(project: Project.t) : State.t =
let taint_to_flag = Var.Map.fold state.register ~init:Taint.empty ~f:(fun ~key ~data taint_accum ->
if Cconv.is_parameter_register key project then
Taint.union taint_accum data
else
taint_accum
) in
let () = append_to_hits cwe_hits taint_to_flag in
State.remove_taint state taint_to_flag
(** Remove the taint of non-callee-saved register (without flagging them).
For taints in parameter register we assume that they are checked by the callee, thus we also remove the corresponding Tids from the state. *)
let untaint_non_callee_saved_register (state: State.t) ~(project: Project.t) : State.t =
let taint_to_remove = Var.Map.fold state.register ~init:Taint.empty ~f:(fun ~key ~data taint_accum ->
if Cconv.is_callee_saved key project then
taint_accum
else
Taint.union taint_accum data
) in
let state = State.remove_taint state taint_to_remove in
Var.Map.fold state.register ~init:state ~f:(fun ~key ~data:_ state ->
if Cconv.is_callee_saved key project then
state
else
State.remove_register state key
)
(** If the expression is a store onto a stack variable, write the corresponding taint to the stack. *)
let update_stack_on_stores (exp: Exp.t) (state: State.t) ~(stack: StackInfo.t) : State.t =
let pointer_size = Symbol_utils.arch_pointer_size_in_bytes stack.project in
match exp with
| Bil.Store(_mem, address_exp, value, _endian, size) -> begin
let value_taint = contains_taint value state ~cwe_hits:(ref Taint.empty) ~stack in
match StackInfo.get_address stack address_exp with
| Some(address) ->
if Taint.is_empty value_taint then
State.remove_stack state ~pos:address ~size:(Bitvector.of_int (Size.in_bytes size) ~width:pointer_size)
else
State.set_stack state value_taint ~pos:address ~size:(Bitvector.of_int (Size.in_bytes size) ~width:pointer_size)
| None -> state
end
| _ -> state
(** Updates the state depending on the def. If memory is accessed using an unchecked return value, (** Updates the state depending on the def. If memory is accessed using an unchecked return value,
then the access is added to the list of cwe_hits. *) then the access is added to the list of cwe_hits. *)
let update_state_def def state ~cwe_hits = let update_state_def (def: Def.t) (state: State.t) ~(cwe_hits: Taint.t ref) ~(stack: StackInfo.t) : State.t =
let (lhs, rhs) = (Def.lhs def, Def.rhs def) in let (lhs, rhs) = (Def.lhs def, Def.rhs def) in
let state = checks_value rhs state in let state = checks_value rhs state ~cwe_hits ~stack in
match contains_unchecked rhs state with let (rhs_taint, state) = parse_taint_of_exp rhs state ~cwe_hits ~stack in
| MemAccess(var) -> begin (* we found a case of unchecked return value *) let state =
let tid = State.find_exn state var in if Taint.is_empty rhs_taint then
append_to_hits cwe_hits tid; State.remove_register state lhs
State.remove_tid state var else
end State.set_register state lhs rhs_taint in
| Access(var) -> (* taint the lhs as an unchecked return value *) update_stack_on_stores rhs state ~stack
let tid = State.find_exn state var in
State.add state lhs tid
| NoAccess -> (* no access to an unchecked return value in rhs. Since lhs is overwritten, it cannot be an unchecked return value anymore. *)
State.remove_var state lhs
(** Taint the return registers of a function as unchecked return values. *) (** Taint the return registers of a function as unchecked return values. *)
let taint_return_registers func_tid state ~program ~block = let taint_return_registers (func_tid: Tid.t) (state: State.t) ~(project: Project.t) ~(block: Blk.t) : State.t =
let func = Term.find_exn sub_t program func_tid in let func = Term.find_exn sub_t (Project.program project) func_tid in
let arguments = Term.enum arg_t func in let arguments = Term.enum arg_t func in
(* Every return register is tainted as unchecked return value. *) (* Every return register is tainted as unchecked return value. *)
Seq.fold arguments ~init:state ~f:(fun state arg -> Seq.fold arguments ~init:state ~f:(fun state arg ->
...@@ -172,126 +312,186 @@ let taint_return_registers func_tid state ~program ~block = ...@@ -172,126 +312,186 @@ let taint_return_registers func_tid state ~program ~block =
let variable = match Bap.Std.Arg.rhs arg with let variable = match Bap.Std.Arg.rhs arg with
| Bil.Var(var) -> var | Bil.Var(var) -> var
| _ -> failwith "[CWE476] Return register wasn't a register." in | _ -> failwith "[CWE476] Return register wasn't a register." in
State.add state variable (Term.tid block) State.set_register state variable (Taint.add Taint.empty (Term.tid block))
) )
(** Updates the state depending on the jump. On a jump to a function from the function list (** Updates the state depending on the jump. On a jump to a function from the function list
taint all return registers as unchecked return values. *) taint all return registers as unchecked return values. *)
let update_state_jmp jmp state ~cwe_hits ~function_names ~program ~block ~strict_call_policy = let update_state_jmp
(jmp: Jmp.t)
(state: State.t)
~(cwe_hits: Taint.t ref)
~(malloc_like_functions: String.t List.t)
~(extern_functions: String.Set.t)
~(stack: StackInfo.t)
~(block: Blk.t)
~(strict_call_policy: Bool.t) : State.t =
(* first check the guard condition for unchecked access. Any normal access clears the access from being unchecked *) (* first check the guard condition for unchecked access. Any normal access clears the access from being unchecked *)
let condition_exp = Jmp.cond jmp in let condition_exp = Jmp.cond jmp in
let state = begin let state = begin
match contains_unchecked condition_exp state with let (condition_taint, state) = parse_taint_of_exp condition_exp state ~cwe_hits ~stack in
| Access(var) -> if Taint.is_empty condition_taint then
State.remove_tid state var state
| MemAccess(var) -> (* a memory access using an unchecked value is still an error *) else
let tid = State.find_exn state var in State.remove_taint state condition_taint
let () = append_to_hits cwe_hits tid in
State.remove_tid state var
| NoAccess -> state
end in end in
match Jmp.kind jmp with match Jmp.kind jmp with
| Goto(Indirect(exp)) -> flag_any_access exp state ~cwe_hits | Goto(Indirect(exp)) -> flag_any_access exp state ~cwe_hits ~stack
| Goto(Direct(_)) -> state | Goto(Direct(_)) -> state
| Ret(_) -> if strict_call_policy then | Ret(_) -> if strict_call_policy then
flag_all_unchecked_registers state ~cwe_hits flag_unchecked_return_values state ~cwe_hits ~project:stack.project
else else
state state
| Int(_, _) -> flag_all_unchecked_registers state ~cwe_hits | Int(_, _) -> flag_register_taints state ~cwe_hits
| Call(call) -> | Call(call) ->
let state = match Call.return call with (* flag tainted values in the call and return expressions of indirect calls *)
| Some(Indirect(exp)) -> flag_any_access exp state ~cwe_hits let state = match Call.return call with
| _ -> state in | Some(Indirect(exp)) -> flag_any_access exp state ~cwe_hits ~stack
let state = match Call.target call with | _ -> state in
| Indirect(exp) -> flag_any_access exp state ~cwe_hits let state = begin match Call.target call with
| _ -> state in | Indirect(exp) -> flag_any_access exp state ~cwe_hits ~stack
let state = match strict_call_policy with | _ -> state end in
| true -> (* all unchecked registers get flagged as hits *) (* flag tainted values in the parameter registers (if strict_call_policy is set to true)*)
flag_all_unchecked_registers state ~cwe_hits let state = match (Call.target call, strict_call_policy) with
| false -> (* we assume that the callee will check all remaining unchecked values *) | (Indirect(_), false)
[] in | (Direct(_), false) -> state
match Call.target call with | (Indirect(_), true) -> flag_parameter_register state ~cwe_hits ~project:stack.project (* TODO: indirect calls are handled as extern calls right now. Change that *)
| Indirect(_) -> state (* already handled above *) | (Direct(tid), true) ->
| Direct(tid) -> let sub = Term.find_exn sub_t (Project.program stack.project) tid in
if List.exists function_names ~f:(fun elem -> String.(=) elem (Tid.name tid)) then if Set.mem extern_functions (Sub.name sub) then
taint_return_registers tid state ~program ~block flag_parameter_register state ~cwe_hits ~project:stack.project
else else (* flag all registers for intern calls, as these do not necessarily adhere to any calling convention *)
state flag_register_taints state ~cwe_hits
in
(* remove the taint of non-callee-saved registers *)
let state = match Call.target call with
| Direct(tid) ->
let sub = Term.find_exn sub_t (Project.program stack.project) tid in
if Set.mem extern_functions (Sub.name sub) then
untaint_non_callee_saved_register state ~project:stack.project
else (* we untaint all registers for internal function calls, as these do not necessarily adhere to any calling convention *)
{ state with register = Var.Map.empty }
| Indirect(_) -> (* we treat all indirect calls as extern function calls, since we cannot handle indirect calls properly yet *)
untaint_non_callee_saved_register state ~project:stack.project
in
(* introduce new taint for the return values of malloc_like_functions *)
match Call.target call with
| Indirect(_) -> state
| Direct(tid) ->
if List.exists malloc_like_functions ~f:(fun elem -> String.(=) elem (Tid.name tid)) then
taint_return_registers tid state ~project:stack.project ~block
else
state
(** updates a block analysis. (** updates a block analysis.
The strict call policy decides the behaviour on call and return instructions: The strict call policy decides the behaviour on call and return instructions:
strict: all unchecked values get flagged as cwe-hits strict: unchecked values in registers get flagged as cwe_hits
non-strict: the state gets cleared, it is assumed that the target of the call/return non-strict: unchecked values in registers get marked as checked. It is assumed that the callee checks these values. *)
instruction checks all remaining unchecked values. *) let update_block_analysis
let update_block_analysis block register_state ~cwe_hits ~function_names ~program ~strict_call_policy = (block: Blk.t)
(state: State.t)
~(cwe_hits: Taint.t ref)
~(malloc_like_functions: String.t List.t)
~(extern_functions: String.Set.t)
~(sub_tid: Tid.t)
~(project: Project.t)
~(strict_call_policy: Bool.t)
~(strict_mem_policy: Bool.t) : State.t =
let elements = Blk.elts block in let elements = Blk.elts block in
let register_state = Seq.fold elements ~init:register_state ~f:(fun state element -> let type_info_map = Type_inference.get_type_info_of_block ~project block ~sub_tid in
let state = Seq.fold elements ~init:state ~f:(fun state element ->
match element with match element with
| `Def def -> update_state_def def state ~cwe_hits | `Def def ->
let stack = StackInfo.assemble type_info_map (Term.tid def) ~sub_tid ~project ~strict_mem_policy in
update_state_def def state ~cwe_hits ~stack
| `Phi _phi -> state (* We ignore phi terms for this analysis. *) | `Phi _phi -> state (* We ignore phi terms for this analysis. *)
| `Jmp jmp -> update_state_jmp jmp state ~cwe_hits ~function_names ~program ~block ~strict_call_policy | `Jmp jmp ->
let stack = StackInfo.assemble type_info_map (Term.tid jmp) ~sub_tid ~project ~strict_mem_policy in
update_state_jmp jmp state ~cwe_hits ~malloc_like_functions ~extern_functions ~stack ~block ~strict_call_policy
) in ) in
State.remove_virtual_registers register_state (* virtual registers should not be accessed outside of the block where they are defined. *) State.remove_virtual_register state (* virtual registers should not be accessed outside of the block where they are defined. *)
let print_hit tid ~sub ~function_names ~tid_map = (** print a cwe_hit to the log *)
let print_hit (tid: Tid.t) ~(sub: Sub.t) ~(malloc_like_functions: String.t List.t) ~(tid_map: Word.t Tid.Map.t) : unit =
let block = Option.value_exn (Term.find blk_t sub tid) in let block = Option.value_exn (Term.find blk_t sub tid) in
let jmps = Term.enum jmp_t block in let jmps = Term.enum jmp_t block in
let _ = Seq.find_exn jmps ~f:(fun jmp -> let _ = Seq.find_exn jmps ~f:(fun jmp ->
match Jmp.kind jmp with match Jmp.kind jmp with
| Call(call) -> begin | Call(call) -> begin
match Call.target call with match Call.target call with
| Direct(call_tid) -> Option.is_some (List.find function_names ~f:(fun fn_name -> | Direct(call_tid) -> Option.is_some (List.find malloc_like_functions ~f:(fun fn_name ->
if fn_name = (Tid.name call_tid) then if fn_name = (Tid.name call_tid) then
begin begin
let address = Address_translation.translate_tid_to_assembler_address_string tid tid_map in let address = Address_translation.translate_tid_to_assembler_address_string tid tid_map in
let tids = [Address_translation.tid_to_string tid] in let tids = [Address_translation.tid_to_string tid] in
let description = sprintf let description = sprintf
"(NULL Pointer Dereference) There is no check if the return value is NULL at %s (%s)." "(NULL Pointer Dereference) There is no check if the return value is NULL at %s (%s)."
address address
fn_name in fn_name in
let cwe_warning = cwe_warning_factory let cwe_warning = cwe_warning_factory
name name
version version
~addresses:[address] ~addresses:[address]
~tids:tids ~tids:tids
~symbols:[fn_name] ~symbols:[fn_name]
description in description in
collect_cwe_warning cwe_warning; collect_cwe_warning cwe_warning;
true true
end else end else
false false
)) ))
| _ -> false | _ -> false
end end
| _ -> false | _ -> false
) in () ) in ()
let check_cwe prog _proj tid_map symbol_names parameters =
let check_cwe (_prog: Program.t) (project: Project.t) (tid_map: Word.t Tid.Map.t) (symbol_names: String.t List.t List.t) (parameters: String.t List.t) =
let symbols = match symbol_names with let symbols = match symbol_names with
| hd :: _ -> hd | hd :: _ -> hd
| _ -> failwith "[CWE476] symbol_names not as expected" in | _ -> failwith "[CWE476] symbol_names not as expected" in
let (strict_call_policy_string, max_steps_string) = match parameters with let (strict_call_policy_string, strict_mem_policy_string, max_steps_string) = match parameters with
| par1 :: par2 :: _ -> (par1, par2) | par1 :: par2 :: par3 :: _ -> (par1, par2, par3)
| _ -> failwith "[CWE476] parameters not as expected" in | _ -> failwith "[CWE476] parameters not as expected" in
let strict_call_policy = match String.split strict_call_policy_string ~on:'=' with let strict_call_policy = match String.split strict_call_policy_string ~on:'=' with
| "strict_call_policy" :: policy :: [] -> bool_of_string policy | "strict_call_policy" :: policy :: [] -> bool_of_string policy
| _ -> failwith "[CWE476] parameters not as expected" in | _ -> failwith "[CWE476] parameters not as expected" in
let strict_mem_policy = match String.split strict_mem_policy_string ~on:'=' with
| "strict_memory_policy" :: policy :: [] -> bool_of_string policy
| _ -> failwith "[CWE476] parameters not as expected" in
let max_steps = match String.split max_steps_string ~on:'=' with let max_steps = match String.split max_steps_string ~on:'=' with
| "max_steps" :: num :: [] -> int_of_string num | "max_steps" :: num :: [] -> int_of_string num
| _ -> failwith "[CWE476] parameters not as expected" in | _ -> failwith "[CWE476] parameters not as expected" in
let function_names = List.map symbols ~f:(fun symb -> "@" ^ symb) in let malloc_like_functions = List.map symbols ~f:(fun symb -> "@" ^ symb) in
let subfunctions = Term.enum sub_t prog in let extern_functions = Cconv.parse_dyn_syms project in
(* run the pointer inference analysis. TODO: This should be done somewhere else as this analysis will be needed in more than one check! *)
let project = Type_inference.compute_pointer_register project in
let subfunctions = Term.enum sub_t (Project.program project) in
Seq.iter subfunctions ~f:(fun subfn -> Seq.iter subfunctions ~f:(fun subfn ->
let cfg = Sub.to_cfg subfn in let cfg = Sub.to_cfg subfn in
let cwe_hits = ref [] in let cwe_hits = ref Taint.empty in
let empty = Map.empty (module Graphs.Ir.Node) in let empty = Map.empty (module Graphs.Ir.Node) in
let init = Graphlib.Std.Solution.create empty [] in let init = Graphlib.Std.Solution.create empty State.empty in
let equal = State.equal in let equal = State.equal in
let merge = State.union in let merge = State.union in
let f = (fun node state -> let f = (fun node state ->
let block = Graphs.Ir.Node.label node in let block = Graphs.Ir.Node.label node in
update_block_analysis block state ~cwe_hits ~function_names ~program:prog ~strict_call_policy update_block_analysis block state ~cwe_hits ~malloc_like_functions ~extern_functions ~sub_tid:(Term.tid subfn) ~project ~strict_call_policy ~strict_mem_policy
) in ) in
let _ = Graphlib.Std.Graphlib.fixpoint (module Graphs.Ir) cfg ~steps:max_steps ~rev:false ~init:init ~equal:equal ~merge:merge ~f:f in let _ = Graphlib.Std.Graphlib.fixpoint (module Graphs.Ir) cfg ~steps:max_steps ~rev:false ~init:init ~equal:equal ~merge:merge ~f:f in
List.iter (!cwe_hits) ~f:(fun hit -> print_hit hit ~sub:subfn ~function_names ~tid_map) Tid.Set.iter (!cwe_hits) ~f:(fun hit -> print_hit hit ~sub:subfn ~malloc_like_functions ~tid_map)
) )
(**/**)
(* Functions made public for unit tests *)
module Private = struct
module StackInfo = StackInfo
module Taint = Taint
module State = State
let flag_unchecked_return_values = flag_unchecked_return_values
let flag_register_taints = flag_register_taints
let flag_parameter_register = flag_parameter_register
let untaint_non_callee_saved_register = untaint_non_callee_saved_register
end
...@@ -8,9 +8,8 @@ ...@@ -8,9 +8,8 @@
{1 How the check works} {1 How the check works}
We search for an execution path where a memory access using the return value of Using dataflow analysis we search for an execution path where a memory access using the return value of
a symbol happens before the return value is checked through a conditional a symbol happens before the return value is checked through a conditional jump instruction.
jump instruction.
Note that the check relies on Bap-generated stubs to identify return registers of the Note that the check relies on Bap-generated stubs to identify return registers of the
checked functions. Therefore it only works for functions for which Bap generates checked functions. Therefore it only works for functions for which Bap generates
...@@ -20,8 +19,14 @@ ...@@ -20,8 +19,14 @@
- strict_call_policy=\{true, false\}: Determines behaviour on call and return instructions. - strict_call_policy=\{true, false\}: Determines behaviour on call and return instructions.
If false, we assume that the callee, resp. the caller on a return instruction, If false, we assume that the callee, resp. the caller on a return instruction,
checks all unchecked values still contained in the registers. If true, every checks all unchecked values still contained in parameter registers. If true, every
unchecked value on a call or return instruction gets reported. unchecked value on a call or return instruction gets reported.
- strict_mem_policy=|{true, false|}:
Determines behaviour on writing an unchecked return value to a memory region other than the stack.
If true, these instances get reported.
Depending on the coding style, this can lead to a lot false positives if return values are
only checked after writing them to their target destination.
If false, these instances do not get reported, which in turn can lead to false negatives.
- max_steps=<num>: Max number of steps for the dataflow fixpoint algorithm. - max_steps=<num>: Max number of steps for the dataflow fixpoint algorithm.
{2 Symbols configurable in config.json} {2 Symbols configurable in config.json}
...@@ -31,9 +36,10 @@ ...@@ -31,9 +36,10 @@
{1 False Positives} {1 False Positives}
- The check does not yet track values on the stack. Thus instances, where the - If strict_mem_policy is set to true, writing a return value to memory other than the stack
return value gets written onto the stack before the check happens get incorrectly gets reported even if a NULL pointer check happens right afterwards.
flagged. This happens a lot on unoptimized binaries but rarely on optimized ones. - The check has no knowledge about the actual number of parameters that an extern function call takes.
This can lead to false positives if strict_call_policy is set to true.
{1 False Negatives} {1 False Negatives}
...@@ -43,9 +49,41 @@ ...@@ -43,9 +49,41 @@
for the return value being NULL or something else for the return value being NULL or something else
- For functions with more than one return value we do not distinguish between - For functions with more than one return value we do not distinguish between
the return values. the return values.
- If strict_mem_policy is set to false, unchecked return values that are
saved somewhere other than the stack may be missed.
- The check has no knowledge about the actual number of parameters that an extern function call takes.
This can lead to false negatives, especially if function parameters are passed on the stack.
*) *)
open Bap.Std
open Core_kernel
val name : string val name : string
val version : string val version : string
val check_cwe : Bap.Std.program Bap.Std.term -> Bap.Std.project -> Bap.Std.word Bap.Std.Tid.Map.t -> string list list -> string list -> unit val check_cwe : Bap.Std.program Bap.Std.term -> Bap.Std.project -> Bap.Std.word Bap.Std.Tid.Map.t -> string list list -> string list -> unit
(**/**)
(* Functions made public for unit tests *)
module Private : sig
module Taint : module type of Tid.Set
module State : sig
type t
val empty: t
val set_register: t -> Var.t -> Taint.t -> t
val find_register: t -> Var.t -> Taint.t Option.t
val union: t -> t -> t
end
module StackInfo : sig
type t
val assemble_mock_info: Tid.t -> Project.t -> t
end
val flag_unchecked_return_values: State.t -> cwe_hits: Taint.t ref -> project: Project.t -> State.t
val flag_register_taints: State.t -> cwe_hits: Taint.t ref -> State.t
val flag_parameter_register: State.t -> cwe_hits: Taint.t ref -> project: Project.t -> State.t
val untaint_non_callee_saved_register: State.t -> project: Project.t -> State.t
end
...@@ -98,6 +98,7 @@ ...@@ -98,6 +98,7 @@
"_comment1": "included functions of the following libs: stdlib.h, locale.h, stdio.h, cstring.h, wchar.h", "_comment1": "included functions of the following libs: stdlib.h, locale.h, stdio.h, cstring.h, wchar.h",
"parameters": [ "parameters": [
"strict_call_policy=true", "strict_call_policy=true",
"strict_memory_policy=false",
"max_steps=100" "max_steps=100"
], ],
"symbols": [ "symbols": [
......
...@@ -41,6 +41,58 @@ let is_callee_saved var project = ...@@ -41,6 +41,58 @@ let is_callee_saved var project =
callee_saved_registers := Some(String.Set.of_list (callee_saved_register_list project)); callee_saved_registers := Some(String.Set.of_list (callee_saved_register_list project));
String.Set.mem (Option.value_exn !callee_saved_registers) (Var.name var) String.Set.mem (Option.value_exn !callee_saved_registers) (Var.name var)
(** Return a list of all registers that may hold function arguments. *)
let get_parameter_register_list (project: Project.t) : String.t List.t =
let architecture = Project.arch project in
match architecture with
| `x86 ->
[] (* TODO: This is the value for the standard C calling convention. But it is incorrect for some of the other calling conventions for x86! *)
| `x86_64 -> (* System V ABI. TODO: Floationg Point registers are mising! TODO: Microsoft calling convention uses different register. *)
"RDI" :: "RSI" :: "RDX" :: "RCX" :: "R8" :: "R9" :: []
| `armv4 | `armv5 | `armv6 | `armv7
| `armv4eb | `armv5eb | `armv6eb | `armv7eb
| `thumbv4 | `thumbv5 | `thumbv6 | `thumbv7
| `thumbv4eb | `thumbv5eb | `thumbv6eb | `thumbv7eb ->
"R0" :: "R1" :: "R2" :: "R3" :: []
| `aarch64 | `aarch64_be -> (* ARM 64bit *)
"X0" :: "X1" :: "X2" :: "X3" :: "X4" :: "X5" :: "X6" :: "X7" :: []
| `ppc (* 32bit PowerPC *) (* TODO: add floating point register! *)
| `ppc64 | `ppc64le -> (* 64bit PowerPC *)
"R3" :: "R4" :: "R5" :: "R6" :: "R7" :: "R8" :: "R9" :: "R10" :: []
| `mips | `mips64 | `mips64el | `mipsel -> (* TODO: MIPS has also a calling convention with less arguments. TODO: check whether BAP actually uses A4-A7 as register names or gives them different names *)
"A0" :: "A1" :: "A2" :: "A3" :: "A4" :: "A5" :: "A6" :: "A7" :: []
| _ -> failwith "No calling convention implemented for the given architecture"
let is_parameter_register (var: Var.t) (project: Project.t) : Bool.t =
let param_register = get_parameter_register_list project in
Option.is_some (List.find param_register ~f:(String.equal (Var.name var)))
(** Return all registers that may contain return values of function calls *) (* TODO: Add Floating Point register! *)
let get_return_register_list (project: Project.t) : String.t List.t =
let architecture = Project.arch project in
match architecture with
| `x86 ->
"EAX" :: []
| `x86_64 -> (* System V ABI *)
"RAX" :: "RDX" :: []
| `armv4 | `armv5 | `armv6 | `armv7
| `armv4eb | `armv5eb | `armv6eb | `armv7eb
| `thumbv4 | `thumbv5 | `thumbv6 | `thumbv7
| `thumbv4eb | `thumbv5eb | `thumbv6eb | `thumbv7eb ->
"R0" :: "R1" :: "R2" :: "R3" :: []
| `aarch64 | `aarch64_be -> (* ARM 64bit *)
"X0" :: "X1" :: "X2" :: "X3" :: "X4" :: "X5" :: "X6" :: "X7" :: []
| `ppc (* 32bit PowerPC *) (* TODO: add floating point register! *)
| `ppc64 | `ppc64le -> (* 64bit PowerPC *)
"R3" :: "R4" :: []
| `mips | `mips64 | `mips64el | `mipsel ->
"V0" :: "V1" :: []
| _ -> failwith "No calling convention implemented for the given architecture"
let is_return_register (var: Var.t) (project: Project.t) : Bool.t =
let ret_register = get_return_register_list project in
Option.is_some (List.find ret_register ~f:(String.equal (Var.name var)))
(** Parse a line from the dyn-syms output table of readelf. Return the name of a symbol if the symbol is an extern function name. *) (** Parse a line from the dyn-syms output table of readelf. Return the name of a symbol if the symbol is an extern function name. *)
let parse_dyn_sym_line line = let parse_dyn_sym_line line =
let line = ref (String.strip line) in let line = ref (String.strip line) in
......
open Bap.Std open Bap.Std
open Core_kernel open Core_kernel
(** Returns whether a variable is callee saved according to the calling convention (** Returns whether a variable is callee saved according to the calling convention
of the target architecture. Should only used for calls to functions outside of the target architecture. Should only used for calls to functions outside
of the program, not for calls between functions inside the program. *) of the program, not for calls between functions inside the program. *)
val is_callee_saved: Var.t -> Project.t -> bool val is_callee_saved: Var.t -> Project.t -> Bool.t
(** Returns whether a variable may be used to pass parameters to a function.
This depends on the calling convention of the target architecture and should only be used for extern function calls. *)
val is_parameter_register: Var.t -> Project.t -> Bool.t
(** Returns whether a variable may be used for return values of function calls.
This depends on the calling convention of the target architecture and should only be used for extern function calls. *)
val is_return_register: Var.t -> Project.t -> Bool.t
(** Returns a list of those function names that are extern symbols. (** Returns a list of those function names that are extern symbols.
......
...@@ -16,4 +16,4 @@ class TestCheckPath(unittest.TestCase): ...@@ -16,4 +16,4 @@ class TestCheckPath(unittest.TestCase):
output = subprocess.check_output(self.cmd.split()) output = subprocess.check_output(self.cmd.split())
j = json.loads(output) j = json.loads(output)
self.assertTrue('check_path' in j) self.assertTrue('check_path' in j)
self.assertEqual(len(j['check_path']), 7) self.assertEqual(len(j['check_path']), 5)
...@@ -14,34 +14,31 @@ class TestCwe476(unittest.TestCase): ...@@ -14,34 +14,31 @@ class TestCwe476(unittest.TestCase):
self.target, self.target, 'x64', 'gcc', self.string) self.target, self.target, 'x64', 'gcc', self.string)
self.assertEqual(res, expect_res) self.assertEqual(res, expect_res)
@unittest.skip('FIXME!')
def test_cwe476_01_x64_clang(self): def test_cwe476_01_x64_clang(self):
expect_res = 1 expect_res = 1
res = cwe_checker_testlib.execute_and_check_occurence( res = cwe_checker_testlib.execute_and_check_occurence(
self.target, self.target, 'x64', 'clang', self.string) self.target, self.target, 'x64', 'clang', self.string)
self.assertEqual(res, expect_res) self.assertEqual(res, expect_res)
@unittest.skip('FIXME!')
def test_cwe476_01_x86_gcc(self): def test_cwe476_01_x86_gcc(self):
expect_res = 1 expect_res = 1
res = cwe_checker_testlib.execute_and_check_occurence( res = cwe_checker_testlib.execute_and_check_occurence(
self.target, self.target, 'x86', 'gcc', self.string) self.target, self.target, 'x86', 'gcc', self.string)
self.assertEqual(res, expect_res) self.assertEqual(res, expect_res)
@unittest.skip('FIXME!')
def test_cwe476_01_x86_clang(self): def test_cwe476_01_x86_clang(self):
expect_res = 1 expect_res = 1
res = cwe_checker_testlib.execute_and_check_occurence( res = cwe_checker_testlib.execute_and_check_occurence(
self.target, self.target, 'x86', 'clang', self.string) self.target, self.target, 'x86', 'clang', self.string)
self.assertEqual(res, expect_res) self.assertEqual(res, expect_res)
@unittest.skip('FIXME!')
def test_cwe476_01_arm_gcc(self): def test_cwe476_01_arm_gcc(self):
expect_res = 1 expect_res = 1
res = cwe_checker_testlib.execute_and_check_occurence( res = cwe_checker_testlib.execute_and_check_occurence(
self.target, self.target, 'arm', 'gcc', self.string) self.target, self.target, 'arm', 'gcc', self.string)
self.assertEqual(res, expect_res) self.assertEqual(res, expect_res)
@unittest.skip('FIXME!')
def test_cwe476_01_arm_clang(self): def test_cwe476_01_arm_clang(self):
expect_res = 1 expect_res = 1
res = cwe_checker_testlib.execute_and_check_occurence( res = cwe_checker_testlib.execute_and_check_occurence(
...@@ -118,7 +115,6 @@ class TestCwe476(unittest.TestCase): ...@@ -118,7 +115,6 @@ class TestCwe476(unittest.TestCase):
self.target, self.target, 'mips64el', 'clang', self.string) self.target, self.target, 'mips64el', 'clang', self.string)
self.assertEqual(res, expect_res) self.assertEqual(res, expect_res)
@unittest.skip("Fix issue in CWE476 implementation to support PPC")
def test_cwe476_01_ppc_gcc(self): def test_cwe476_01_ppc_gcc(self):
expect_res = 1 expect_res = 1
res = cwe_checker_testlib.execute_and_check_occurence( res = cwe_checker_testlib.execute_and_check_occurence(
......
...@@ -78,7 +78,7 @@ def which(pgm): ...@@ -78,7 +78,7 @@ def which(pgm):
def optimize(filename): def optimize(filename):
optimize_me = ['cwe_476.c'] optimize_me = []
if filename in optimize_me: if filename in optimize_me:
return ' -O3' return ' -O3'
else: else:
...@@ -140,7 +140,7 @@ def build_cpp(arch, compiler): ...@@ -140,7 +140,7 @@ def build_cpp(arch, compiler):
env['CXXFLAGS'] = cpp_flags[arch] + optimize(str(prog)) env['CXXFLAGS'] = cpp_flags[arch] + optimize(str(prog))
if arch == 'x86': if arch == 'x86':
env['LINKFLAGS'] = '-m32' env['LINKFLAGS'] = '-m32'
compiler_abrev = get_compiler_abrev(compiler) compiler_abrev = get_compiler_abrev(compiler)
if compiler_abrev == 'mingw32-g++' and str(prog) in skip_for_pe: if compiler_abrev == 'mingw32-g++' and str(prog) in skip_for_pe:
continue continue
......
...@@ -87,6 +87,22 @@ let test_around_zero () = ...@@ -87,6 +87,22 @@ let test_around_zero () =
let x = Mem_region.add x "Two" ~pos:(bv (-5)) ~size:(bv 20) in let x = Mem_region.add x "Two" ~pos:(bv (-5)) ~size:(bv 20) in
check "around_zero2" (Some(Error()) = Mem_region.get x (bv 0)) check "around_zero2" (Some(Error()) = Mem_region.get x (bv 0))
let test_list_data () =
let bv num = Bitvector.of_int num ~width:32 in
let x = Mem_region.empty () in
let x = Mem_region.add x "One" ~pos:(bv (15)) ~size:(bv 10) in
let x = Mem_region.add x "Two" ~pos:(bv 0) ~size:(bv 10) in
let x = Mem_region.add x "Three" ~pos:(bv (-5)) ~size:(bv 10) in
let x = Mem_region.add x "Four" ~pos:(bv (-15)) ~size:(bv 10) in
let data_list = "Four" :: "Three" :: "One" :: [] in
check "list_data" (Mem_region.list_data x = data_list);
let data_pos_list = (bv (-15), "Four") :: (bv (-5), "Three") :: (bv 15, "One") ::[] in
check "list_data_pos" (Mem_region.list_data_pos x = data_pos_list);
let pos_minus = Bitvector.to_int_exn (Bitvector.signed (bv (-5))) in
let pos_plus = Bitvector.to_int_exn (Bitvector.signed (bv 5)) in
check "expected_sign_minus" (pos_minus < 0);
check "expected_sign_plus" (pos_plus > 0)
let tests = [ let tests = [
"Add", `Quick, test_add; "Add", `Quick, test_add;
"Negative Indices", `Quick, test_minus; "Negative Indices", `Quick, test_minus;
...@@ -95,4 +111,5 @@ let tests = [ ...@@ -95,4 +111,5 @@ let tests = [
"Merge", `Quick, test_merge; "Merge", `Quick, test_merge;
"Equal", `Quick, test_equal; "Equal", `Quick, test_equal;
"Around Zero", `Quick, test_around_zero; "Around Zero", `Quick, test_around_zero;
"List data", `Quick, test_list_data;
] ]
open Bap.Std
open Core_kernel
open Cwe_checker_core
open Cwe_checker_core.Cwe_476.Private
let check msg x = Alcotest.(check bool) msg true x
let example_project : Project.t Option.t ref = ref None
let call_handling_test () =
let project = Option.value_exn !example_project in
let state = State.empty in
let mock_tid = Tid.create () in
let mock_taint = Taint.add Taint.empty mock_tid in
let mock_hits = ref Taint.empty in
let rax_register = Var.create "RAX" (Bil.Imm (Symbol_utils.arch_pointer_size_in_bytes project * 8)) in
let rbx_register = Var.create "RBX" (Bil.Imm (Symbol_utils.arch_pointer_size_in_bytes project * 8)) in
let rdx_register = Var.create "RDX" (Bil.Imm (Symbol_utils.arch_pointer_size_in_bytes project * 8)) in
let state = State.set_register state rax_register mock_taint in
let _state = flag_unchecked_return_values state ~cwe_hits:mock_hits ~project in
check "flag_RAX_return" (false = Taint.is_empty !mock_hits);
let state = State.empty in
let state = State.set_register state rbx_register mock_taint in
mock_hits := Taint.empty;
let _state = flag_unchecked_return_values state ~cwe_hits:mock_hits ~project in
check "dont_flag_RBX_return" (Taint.is_empty !mock_hits);
let state = State.empty in
mock_hits := Taint.empty;
let state = State.set_register state rbx_register mock_taint in
let _state = flag_register_taints state ~cwe_hits:mock_hits in
check "flag_all_registers" (false = Taint.is_empty !mock_hits);
let state = State.empty in
mock_hits := Taint.empty;
let other_mock_taint = Taint.add Taint.empty (Tid.create ()) in
let state = State.set_register state rdx_register mock_taint in
let state = State.set_register state rbx_register other_mock_taint in
let state = flag_parameter_register state ~cwe_hits:mock_hits ~project in
check "flag_RDX_parameter" (false = Taint.is_empty !mock_hits && Option.is_none (State.find_register state rdx_register));
check "dont_flag_RBX_parameter" (Option.is_some (State.find_register state rbx_register));
let state = State.empty in
mock_hits := Taint.empty;
let state = State.set_register state rax_register mock_taint in
let state = State.set_register state rbx_register other_mock_taint in
let state = untaint_non_callee_saved_register state ~project in
check "RAX_non_callee_saved" (Option.is_none (State.find_register state rax_register));
check "RBX_callee_saved" (Option.is_some (State.find_register state rbx_register));
()
let state_test () =
let project = Option.value_exn !example_project in
let state = State.empty in
let mock_tid = Tid.create () in
let mock_taint = Taint.add Taint.empty mock_tid in
let _mock_hits = ref Taint.empty in
let rax_register = Var.create "RAX" (Bil.Imm (Symbol_utils.arch_pointer_size_in_bytes project * 8)) in
let rbx_register = Var.create "RBX" (Bil.Imm (Symbol_utils.arch_pointer_size_in_bytes project * 8)) in
let rdx_register = Var.create "RDX" (Bil.Imm (Symbol_utils.arch_pointer_size_in_bytes project * 8)) in
let state1 = State.set_register state rax_register mock_taint in
let state2 = State.set_register state rbx_register mock_taint in
let union_state = State.union state1 state2 in
check "state_union_RAX" (Option.is_some (State.find_register union_state rax_register));
check "state_union_RBX" (Option.is_some (State.find_register union_state rbx_register));
check "state_union_not_RDX" (Option.is_none (State.find_register union_state rdx_register));
()
(* TODO: write checks for expression handling!! *)
let tests = [
"Call Handling", `Quick, call_handling_test;
"State Operations", `Quick, state_test;
]
open Bap.Std
open Core_kernel
val example_project: Project.t option ref
val tests: unit Alcotest.test_case list
...@@ -5,10 +5,12 @@ open Core_kernel ...@@ -5,10 +5,12 @@ open Core_kernel
let run_tests project = let run_tests project =
Type_inference_test.example_project := Some(project); Type_inference_test.example_project := Some(project);
Cconv_test.example_project := Some(project); Cconv_test.example_project := Some(project);
Cwe_476_test.example_project := Some(project);
Alcotest.run "Unit tests" ~argv:[|"DoNotComplainWhenRunAsABapPlugin";"--color=always";|] [ Alcotest.run "Unit tests" ~argv:[|"DoNotComplainWhenRunAsABapPlugin";"--color=always";|] [
"Mem_region_tests", Mem_region_test.tests; "Mem_region_tests", Mem_region_test.tests;
"Type_inference_tests", Type_inference_test.tests; "Type_inference_tests", Type_inference_test.tests;
"Cconv_tests", Cconv_test.tests; "Cconv_tests", Cconv_test.tests;
"CWE_476_tests", Cwe_476_test.tests;
"CWE_560_tests", Cwe_560_test.tests; "CWE_560_tests", Cwe_560_test.tests;
] ]
......
...@@ -17,6 +17,24 @@ let test_callee_saved () = ...@@ -17,6 +17,24 @@ let test_callee_saved () =
let () = check "caller_saved_register" (is_callee_saved register project = false) in let () = check "caller_saved_register" (is_callee_saved register project = false) in
() ()
let test_parameter_register () =
(* this test assumes, that the example project is a x64 binary *)
let project = Option.value_exn !example_project in
let register = Var.create "RDX" (Bil.Imm (Symbol_utils.arch_pointer_size_in_bytes project * 8)) in
let () = check "return_register" (is_parameter_register register project) in
let register = Var.create "RAX" (Bil.Imm (Symbol_utils.arch_pointer_size_in_bytes project * 8)) in
let () = check "no_return_register" (is_parameter_register register project = false) in
()
let test_return_register () =
(* this test assumes, that the example project is a x64 binary *)
let project = Option.value_exn !example_project in
let register = Var.create "RAX" (Bil.Imm (Symbol_utils.arch_pointer_size_in_bytes project * 8)) in
let () = check "return_register" (is_return_register register project) in
let register = Var.create "R12" (Bil.Imm (Symbol_utils.arch_pointer_size_in_bytes project * 8)) in
let () = check "no_return_register" (is_return_register register project = false) in
()
let test_parse_dyn_syms () = let test_parse_dyn_syms () =
(* this test assumes, that the example project is the arrays_x64.out binary from the artificial samples. *) (* this test assumes, that the example project is the arrays_x64.out binary from the artificial samples. *)
let project = Option.value_exn !example_project in let project = Option.value_exn !example_project in
...@@ -29,5 +47,7 @@ let test_parse_dyn_syms () = ...@@ -29,5 +47,7 @@ let test_parse_dyn_syms () =
let tests = [ let tests = [
"Callee saved register", `Quick, test_callee_saved; "Callee saved register", `Quick, test_callee_saved;
"Parameter register", `Quick, test_parameter_register;
"Return register", `Quick, test_return_register;
"Parse dynamic symbols", `Quick, test_parse_dyn_syms; "Parse dynamic symbols", `Quick, test_parse_dyn_syms;
] ]
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