Unverified Commit 11ca1728 by Enkelmann Committed by GitHub

Remove old ocaml code (#148)

* Remove old Ocaml files

* adjust makefile

* Remove BAP build instructions from Readme

* minor documentation fix

* Remove old Ocaml documentation

* Remove unused Travis CI files

* Move Rust and Java code into src directory

* adjust build scripts to new folder structure
parent 02864348
#!/bin/bash
pat=".*\.ml(i|l|y)?$"
if [[ $1 =~ $pat ]];
then
s1=$(cat $1)
s2=$(ocp-indent $1)
if [ "$s1" == "$s2" ]
then
exit 0
else
echo "$1: ocp-indent"
exit 1
fi
fi
exit 0
JaneStreet
match_clause = 4
\ No newline at end of file
default_stages: [commit, push]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.1.0
hooks:
- id: check-added-large-files
args: [--maxkb=10000]
- id: check-json
- id: check-merge-conflict
- id: check-yaml
- id: end-of-file-fixer
types: [python]
- id: fix-encoding-pragma
args: [--remove]
- id: flake8
args: [--ignore=E501]
- id: forbid-new-submodules
- id: no-commit-to-branch
- id: pretty-format-json
args: [--autofix]
- id: trailing-whitespace
types: [python]
- repo: https://github.com/pryorda/dockerfilelint-precommit-hooks
rev: v0.1.0
hooks:
- id: dockerfilelint
- repo: local
hooks:
- id: ocp-indent
name: ocp-indent
language: system
verbose: true
entry: .hooks/ocp-indent.sh
dist: xenial
language: python
python: 3.6
services:
- docker
install:
- sudo apt-get update
- pip install scons
before_script:
- ./.travis_prepare.sh
script:
- ./.travis_run_tests.sh
notifications:
email:
- nils-edvin.enkelmann@fkie.fraunhofer.de
#!/bin/bash
cd test/artificial_samples/
./install_cross_compilers.sh
scons
cd ../unit/
./specify_test_files_for_compilation.sh
cd ../..
docker build -t cwe-checker .
docker build -t cwe-checker-ghidra -f ghidra.Dockerfile .
\ No newline at end of file
#!/bin/bash
docker run --rm -t cwe-checker make codestyle-check \
&& docker run --rm -t cwe-checker cargo test \
&& docker run --rm -t cwe-checker dune runtest \
&& pytest \
&& docker run --rm -t cwe-checker-ghidra cargo test --no-fail-fast -p acceptance_tests_ghidra -- --show-output --ignored --test-threads 1
[workspace]
members = ["cwe_checker_rs", "caller", "test"]
members = ["src/cwe_checker_lib", "src/caller", "test"]
......@@ -18,7 +18,7 @@ USER cwe
# Install all necessary files from the builder stage
COPY --from=builder /cwe_checker/target/release/cwe_checker /home/cwe/cwe_checker
COPY --from=builder /cwe_checker/src/config.json /home/cwe/.config/cwe_checker/config.json
COPY --from=builder /cwe_checker/ghidra/p_code_extractor /home/cwe/.local/share/cwe_checker/ghidra/p_code_extractor
COPY --from=builder /cwe_checker/src/ghidra/p_code_extractor /home/cwe/.local/share/cwe_checker/ghidra/p_code_extractor
RUN echo "{ \"ghidra_path\": \"/opt/ghidra\" }" | sudo tee /home/cwe/.config/cwe_checker/ghidra.json
WORKDIR /
......
......@@ -6,40 +6,19 @@ all:
ifdef GHIDRA_PATH
mkdir -p ${HOME}/.config/cwe_checker
cp src/config.json ${HOME}/.config/cwe_checker/config.json
cargo install --path caller --locked
cargo install --path src/caller --locked
echo "{ \"ghidra_path\": \"${GHIDRA_PATH}\" }" > ${HOME}/.config/cwe_checker/ghidra.json
mkdir -p ${HOME}/.local/share/cwe_checker
cp -r ghidra ${HOME}/.local/share/cwe_checker/ghidra
cp -r src/ghidra ${HOME}/.local/share/cwe_checker/ghidra
else
echo "GHIDRA_PATH not specified. Please set it to the path to your local Ghidra installation."
false
endif
with_bap_backend:
cargo build --release
mkdir -p ${HOME}/.config/cwe_checker
cp src/config.json ${HOME}/.config/cwe_checker/config.json
cp target/release/libcwe_checker_rs.a src/libcwe_checker_rs.a
cp target/release/libcwe_checker_rs.so src/dllcwe_checker_rs.so
dune build
dune install
cd plugins/cwe_checker && make all
cd plugins/cwe_checker_emulation && make all
cd plugins/cwe_checker_type_inference && make all
cd plugins/cwe_checker_type_inference_print && make all
cd plugins/cwe_checker_pointer_inference_debug && make all
test:
cargo test
ifeq (,$(wildcard ${HOME}/.config/cwe_checker/ghidra.json))
cd test/unit/ && ./specify_test_files_for_compilation.sh
dune runtest
cd test/artificial_samples; scons; cd ../..
pytest -v --ignore=_build
else
cd test/artificial_samples; scons; cd ../..
cargo test --no-fail-fast -p acceptance_tests_ghidra -- --show-output --ignored
endif
compile_test_files:
cd test/artificial_samples \
......@@ -52,28 +31,12 @@ codestyle-check:
clean:
cargo clean
rm -f src/libcwe_checker_rs.a
rm -f src/dllcwe_checker_rs.so
dune clean
bapbuild -clean
rm -f -r doc/html
cd test/unit; make clean; cd ../..
cd plugins/cwe_checker; make clean; cd ../..
cd plugins/cwe_checker_emulation; make clean; cd ../..
cd plugins/cwe_checker_type_inference; make clean; cd ../..
cd plugins/cwe_checker_type_inference_print; make clean; cd ../..
cd plugins/cwe_checker_pointer_inference_debug; make clean; cd ../..
uninstall:
rm -f -r ${HOME}/.config/cwe_checker
rm -f -r ${HOME}/.local/share/cwe_checker
cargo uninstall cwe_checker; echo ""
dune uninstall
cd plugins/cwe_checker; make uninstall; cd ../..
cd plugins/cwe_checker_emulation; make uninstall; cd ../..
cd plugins/cwe_checker_type_inference; make uninstall; cd ../..
cd plugins/cwe_checker_type_inference_print; make uninstall; cd ../..
cd plugins/cwe_checker_pointer_inference_debug; make uninstall; cd ../..
cargo uninstall cwe_checker
documentation:
cargo doc --open
......
......@@ -54,24 +54,6 @@ The following dependencies must be installed in order to build and install the *
Run `make all GHIDRA_PATH=path/to/ghidra_folder` (with the correct path to the local Ghidra installation inserted) to compile and install the *cwe_checker*.
### Local installation with BAP as backend ###
If you want to use the older [BAP](https://github.com/BinaryAnalysisPlatform/bap) backend instead of Ghidra, you must ensure that the following dependencies are fulfilled:
- Ocaml 4.08.0
- Opam 2.0.2
- dune >= 2.0
- BAP 2.2.0 (and its dependencies).
- yojson >= 1.6.0
- ppx_deriving_yojson >= 3.5.1
- alcotest >= 0.8.3 (for tests)
- Sark (latest) for IDA Pro annotations
- pytest >= 3.5.1 (for tests)
- SCons >= 3.0.5 (for tests)
- odoc >= 1.4 (for documentation)
- [Rust](https://www.rust-lang.org) >= 1.49
Just run `make with_bap_backend` to compile and register the plugin with BAP.
## Usage ##
The *cwe_checker* takes a binary as input,
......
FROM fkiecad/cwe_checker_travis_docker_image:bap
COPY . /home/cwe/cwe_checker/
RUN sudo chown -R cwe:cwe /home/cwe/cwe_checker \
&& cd /home/cwe/cwe_checker \
&& make with_bap_backend \
&& cargo clean \
&& dune clean
WORKDIR /home/cwe/cwe_checker
ENTRYPOINT ["opam", "config", "exec", "--"]
CMD cwe_checker /tmp/input
opam-version: "2.0"
name: "cwe_checker"
version: "0.4"
synopsis: "BAP plugin collection to detect common bug classes"
description: """
cwe_checker is a suite of tools to detect common bug classes such as use of dangerous functions and simple integer overflows. These bug classes are formally known as Common Weakness Enumerations (CWEs).
"""
maintainer: "CWE_checker Team <nils-edvin.enkelmann@fkie.fraunhofer.de>"
authors: [ "Thomas Barabosch <thomas.barabosch@fkie.fraunhofer.de>" "Nils-Edvin Enkelmann <nils-edvin.enkelmann@fkie.fraunhofer.de>" ]
license: "LGPL-3.0"
homepage: "https://github.com/fkie-cad/cwe_checker"
bug-reports: "https://github.com/fkie-cad/cwe_checker/issues"
dev-repo: "git+https://github.com/fkie-cad/cwe_checker"
depends: [
"ocaml" {>= "4.08.0"}
"dune" {>= "2.0"}
"yojson" {>= "1.6.0"}
"bap" {>= "2.2.0"}
"alcotest" {>= "0.8.3"}
"core_kernel" {>= "v0.14"}
"ppx_jane" {>= "v0.14"}
"ppx_deriving_yojson" {>= "3.5.1"}
"odoc" {>= "1.4"}
]
depexts: [
"binutils"
]
conflicts: [
"fkie-cad-cwe-checker" {!= "0.2"}
]
build: [
[ "dune" "build" "--profile" "release" ]
]
install: [
[ make "uninstall" ]
[ make "clean" ]
[ make "all" ]
]
(install
(section etc)
(files
(src/config.json as config.json)
(src/utils/registers.json as registers.json)
)
)
(documentation)
(lang dune 2.0)
(name cwe_checker)
{0 cwe_checker}
{1 Contents}
- {!section:Basics}
- {!section:CmdLineOptions}
- {!section:ToolIntegration}
- {!section:HackingHowto}
{1:Basics Basic usage}
{i cwe_checker} is implemented as a plugin for the {{: https://github.com/BinaryAnalysisPlatform/bap} Binary Analysis Platform} (BAP).
To use it, just run BAP with {i cwe_checker} as a pass:
{[bap [BINARY_FILE] --pass=cwe-checker]}
This runs all static analysis based checks.
You can find more documentation on these checks at their {{: ../cwe_checker_core/Cwe_checker_core/index.html} module documentation pages}.
The behaviour of these checks can be modified through an optional configuration file.
Just edit the [src/config.json] file and then add [--cwe-checker-config=src/config.json] as a command line option.
You can find more on the available command line options {{!section:CmdLineOptions} here}.
Alternatively, you can run the {i cwe_checker} through the command
{[cwe_checker [BINARY_FILE] ]}
Internally, this also calls BAP as above, but enables shorter {{!section:CmdLineOptions} command line options}.
The symbolic execution based checks can be run with the emulation recipe in the recipes folder.
{[bap PATH_TO_BINARY --recipe=recipes/emulation]}
Note that these checks are rather slow and should only be applied to small binaries.
{e Deprecation warning:} We recently decided to deprecate support for the symbolic execution based checks and they will be removed in a future version.
Users of these checks should take a look at the {{: https://github.com/BinaryAnalysisPlatform/bap-toolkit} BAP toolkit},
which provides better-maintained versions of these checks.
{2 How to use the docker images}
There are two docker images containing preinstalled versions of the {i cwe_checker}:
- [docker pull fkiecad/cwe_checker:latest] pulls the image based on the current master branch.
- [docker pull fkiecad/cwe_checker:stable] pulls the image based on the current stable release version.
To use them, mount the target binary inside the docker container and call {i bap} with {i cwe_checker} as a pass as usual:
{[docker run --rm -v [BINARY]:/tmp/input fkiecad/cwe_checker bap /tmp/input --pass=cwe-checker]}
If you are using a customized [config.json] file, don't forget to mount it inside your container as well!
If you want to print the output to a file with [--cwe-checker-out], you also need to mount the output file to the docker container,
or else the file will be lost once the container gets destroyed.
{1:CmdLineOptions Command line options}
If you run the {i cwe_checker} as a BAP plugin, all command line options have to be prefixed with [--cwe-checker] (so that BAP knows to forward them to the {i cwe_checker} plugin).
If you run the {i cwe_checker} directly, do not prefix the command line options.
The available command line options are:
- [-version] Print the version number of the {i cwe_checker} and exit.
- [-check-path] Find paths between input functions (configurable in the configuration file) and CWE hits.
Should be used together with the [-partial] command line option if you are only interested in paths to specific CWEs.
- [-config=[FILE]] Use [[FILE]] as the configuration file.
If you omit this option, {i cwe_checker} uses a standard configuration file located at [src/config.json].
- [-module-versions] Prints the version numbers of each check.
- [-json] Format the CWE-warnings as JSON.
If you print to {i stdout}, note that debug, info and error messages are not formatted as JSON and may pollute the output.
Use [-no-logging] to suppress these messages.
- [-no-logging] Suppress printing of debug, info and error messages.
This is useful if you want to print to {i stdout} with the [-json] flag to prevent these messages polluting the JSON output.
- [-out=[FILE]] Print the CWE-warnings to the file located at [[FILE]].
Note that debug, info and error messages are still printed to stdout and not to the file.
- [-partial=[MODULE_LIST]] Only run the checks given in [[MODULE_LIST]], where [[MODULE_LIST]] is a comma separated list of module names.
E.g. [-partial=CWE190,CWE476] would only run the checks for CWE-190 and CWE-476.
The names of all available modules can be printed with the [-module-versions] command line option.
{1:ToolIntegration Integration with IDA Pro and Ghidra}
To annotate CWE-hits in IDA Pro or Ghidra, first run {i cwe_checker} and save the JSON-formatted output to a file.
{[bap [BINARY] --pass=cwe-checker --cwe-checker-json --cwe-checker-out=cwe_hits.json]}
After that execute the tool-specific script to import the results:
- For IDA Pro run the [cwe_checker_to_ida.py] script located in the [cwe_checker_to_ida] folder.
{[python3 cwe_checker_to_ida.py -i cwe_hits.json -o cwe_hits.py]}
Now open the binary file in IDA Pro and execute the generated [cwe_hits.py] script from within IDA Pro (Alt+F7).
- For Ghidra copy the [cwe_checker_ghidra_plugin.py] script located in the [ghidra_plugin] folder into the script folder of Ghidra.
Now open the binary in Ghidra and run the [cwe_checker_ghidra_plugin.py] script through the script manager and select the generated [cwe_hits.json] file when prompted.
{1:HackingHowto How to write your own check}
{2 Step 1: Get acquainted with BAP}
{i cwe_checker} interfaces via OCaml with the {{: https://github.com/BinaryAnalysisPlatform/bap} Binary Analysis Platform} as a backend and you should read its documentation.
All information about a binary file is gathered through BAP.
{2 Step 2: Write the check}
You need a [your_check.ml] and a [your_check.mli] file that should be located in the [src/checkers] folder.
The [your_check.mli] file should look like this:
{[
(** This module implements your_check.
Some more documentation about your_check.
*)
val name : string (* The name of your check *)
val version : string (* The version of your check (e.g. "0.1"). *)
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
]}
A corresponding example [your_check.ml] file, which just prints a "hello world" message, would look like this:
{[
open Core_kernel
open Bap.Std (* To interface with BAP *)
let name = "your_check"
let version = "0.1"
let check_cwe program project tid_map symbol_pairs parameters =
Log_utils.info "Hello world!"
]}
In practice you would use the parameters of {i check_cwe}-function to gather the necessary information for the computation of your check.
These parameters are:
- [program: Bap.Std.program Bap.Std.term] The program term of the binary
- [project: Bap.Std.project] The BAP-project term of the binary
- [Tid_map: Bap.Std.word Bap.Std.Tid.Map.t] A map from the Tids of basic blocks to concrete addresses in the binary file
- [symbols_pairs: string list list] Symbols read from the {i config.json} file
- [parameters: string list] Parameters read from the config.json file
The results of your check should be reported back to the user via the functions in the {{: ../cwe_checker_core/Cwe_checker_core/Log_utils/index.html} Log_utils}-module.
See its module-level documentation for more details.
{2 Step 3: Add your check to the {i cwe_checker} plugin}
The main file of the {i cwe_checker} plugin is located at [plugins/cwe_checker/cwe_checker.ml].
In there you have to add your check to the list of {i known_modules}.
Here the {i requires_pairs} flag controls whether the symbols in the [config.json] file are a string list or a list of string lists.
The {i has_parameters} flag controls whether the [config.json] file contains parameters to control the behaviour of the check.
Now just recompile {i cwe_checker} via [make all] and your check will be available for use.
all:
bapbuild -pkgs yojson,unix,ppx_jane,cwe_checker_core cwe_checker.plugin
bapbundle install cwe_checker.plugin
clean:
bapbuild -clean
uninstall:
bapbundle remove cwe_checker.plugin
open Cwe_checker_core.Main
open Bap.Std
open Core_kernel
include Self()
let generate_bap_flags flags =
List.map flags (fun (name, docstring) -> (name, Config.flag name ~doc:docstring))
let generate_bap_params params =
List.map params (fun (name, docstring) -> (name, Config.param Config.string name ~doc:docstring))
let () =
let cmdline_flags = generate_bap_flags cmdline_flags in
let cmdline_params = generate_bap_params cmdline_params in
let () = Config.when_ready (fun ({get=(!!)}) ->
let flags: Bool.t String.Map.t = List.fold cmdline_flags ~init:String.Map.empty ~f:(fun flag_map (name, bap_flag) ->
String.Map.set flag_map ~key:name ~data:(!!bap_flag)
) in
let params: String.t String.Map.t = List.fold cmdline_params ~init:String.Map.empty ~f:(fun param_map (name, bap_param) ->
String.Map.set param_map ~key:name ~data:(!!bap_param)
) in
Project.register_pass' ~deps:["callsites"] (main flags params)
) in
let () = Config.manpage [
`S "DESCRIPTION";
`P "This plugin checks various CWEs such as Insufficient Entropy in PRNG (CWE-332) or Use of Potentially Dangerous Function (CWE-676)"
] in
()
all:
bapbuild -pkgs yojson,unix,ppx_jane,bap-primus,monads,graphlib,str,cwe_checker_core cwe_checker_emulation.plugin
bapbundle install cwe_checker_emulation.plugin
clean:
bapbuild -clean
uninstall:
bapbundle remove cwe_checker_emulation.plugin
open Core_kernel
open Bap.Std
open Bap_primus.Std
open Bap_future.Std
open Graphlib.Std
open Monads.Std
open Format
open Ppx_jane
open Cwe_checker_core
include Self()
let pp_id = Monad.State.Multi.Id.pp
module Machine = struct
type 'a m = 'a
include Primus.Machine.Make(Monad.Ident)
end
open Machine.Syntax
module Main = Primus.Machine.Main(Machine)
module Interpreter = Primus.Interpreter.Make(Machine)
module Linker = Primus.Linker.Make(Machine)
module Env = Primus.Env.Make(Machine)
module Lisp = Primus.Lisp.Make(Machine)
module Eval = Primus.Interpreter.Make(Machine)
(** this array collects the observed primus events*)
let collected_events = ref ([||])
(** Converts a hexadecimal string representation of
an address to an integer. *)
let convert_location loc =
match (Str.split (Str.regexp ":") loc) with
| fst::snd::[] -> Int.of_string ("0x" ^ snd)
| _ -> failwith "Could not parse location"
(** Converts a list of hexadecimal strings to a
list of integers. *)
let convert_location_list loc_list =
let locs = ref [] in
Sexplib__Sexp_with_layout.List.iter loc_list ~f:(fun x -> locs := (convert_location @@ Sexp.to_string x)::(!locs));
!locs
(** Analyze events and report to the user. *)
let analyze_events _ =
let location_tbl = Hashtbl.create (module String) in
Array.iter ~f:(fun (p, ev) ->
begin
match ev with
| Sexp.Atom _ -> failwith "Sexp.Atom not expected in report_events."
| Sexp.List [Sexp.Atom location_id; Sexp.List location_list] -> Hashtbl.add_exn location_tbl location_id (convert_location_list location_list)
| Sexp.List incident -> Incident_reporter.report incident location_tbl
end) !collected_events
(** Just adds the observed Primus events to the collected_events array. *)
let collect_events p ev =
collected_events := Array.append !collected_events [|(p, ev)|]
(* Most functions beyond here have been taken and adjusted from BAP's Primus plugins*)
let string_of_name = function
| `symbol s -> s
| `tid t -> Tid.to_string t
| `addr x -> Addr.string_of_value x
(** Executes/forks another Primus machine. *)
let exec x =
Machine.current () >>= fun cid ->
info "Fork %a: starting from the %s entry point"
pp_id cid (string_of_name x);
Machine.catch (Linker.exec x)
(fun exn ->
info "execution from %s terminated with: %s "
(string_of_name x)
(Primus.Exn.to_string exn);
Machine.return ())
let rec run = function
| [] ->
info "all toplevel machines done, halting";
Eval.halt >>=
never_returns
| x :: xs ->
Machine.current () >>= fun pid ->
Machine.fork () >>= fun () ->
Machine.current () >>= fun cid ->
if Poly.(=) pid cid
then run xs
else
exec x >>= fun () ->
Eval.halt >>=
never_returns
(** Checks if a certain Primus.Observation.Provider is equal
to a string like 'incident'. *)
let has_name name p =
Poly.(=) (Primus.Observation.Provider.name p) name
(** Register a monitor. *)
let monitor_provider name ps =
Primus.Observation.list_providers () |>
List.find ~f:(has_name name) |> function
| None -> invalid_argf "An unknown observation provider `%s'" name ()
| Some p -> p :: ps
let parse_monitors =
List.fold ~init:[] ~f:(fun ps name -> monitor_provider name ps)
(** Register monitors for 'incident' related events. *)
module Monitor(Machine : Primus.Machine.S) = struct
open Machine.Syntax
let init () =
parse_monitors ["incident"; "incident-location"] |>
List.iter ~f:(fun m ->
info "monitoring %s" (Primus.Observation.Provider.name m);
Stream.observe (Primus.Observation.Provider.data m) (collect_events m));
Machine.return ()
end
(** Main logic of program:
- we monitor all 'incident' related events
- for all subroutines we fork a Primus machine
- all monitored events are collected globally
- after the last Primus machine has terminated we report all observed incidents *)
let main json_output file_output proj =
print_endline "INFO: The emulation based checks in this plugin have been deprecated. Please look at https://github.com/BinaryAnalysisPlatform/bap-toolkit for an alternative." ;
Primus.Machine.add_component (module Monitor);
begin
let prog = (Project.program proj) in
let targets = Seq.to_list @@ Seq.map (Term.enum sub_t prog) ~f:(fun x -> `tid (Term.tid x)) in
Main.run ~envp:[||] ~args:[||] proj (run targets) |> function
| (Primus.Normal,proj)
| (Primus.Exn Primus.Interpreter.Halt,proj) ->
info "Ok, we've terminated normally";
| (Primus.Exn exn,proj) ->
info "program terminated by a signal: %s" (Primus.Exn.to_string exn);
end;
analyze_events ();
Incident_reporter.parse_reports ();
Incident_reporter.report_cwe ();
Incident_reporter.report_unknown_incidents ();
if json_output then
begin
match Project.get proj filename with
| Some fname -> Log_utils.emit_json fname file_output
| None -> Log_utils.emit_json "" file_output
end
else
Log_utils.emit_native file_output
module Cmdline = struct
open Config
let json_output = flag "json" ~doc:"Outputs the result as JSON."
let file_output = param string "out" ~doc:"Path to output file."
let () = when_ready (fun ({get=(!!)}) -> Project.register_pass' ~deps:["trivial-condition-form"] (main !!json_output !!file_output))
let () = manpage [
`S "DESCRIPTION";
`P "This plugin utilizes symbolic execution to find CWEs like Double Free (CWE415) or Use After Free (CWE416)."]
end
open Core_kernel
open Cwe_checker_core
open Log_utils
let version = "0.1"
(** Keeps track of reported events so that events are not reported multiple times. *)
let reported_events = ref (String.Set.empty)
(** We may want to get the number of emulated CWEs from a central point for scalability *)
let collected_locations = Hashtbl.create (module Int) ~size:4
let known_incidents = Hashtbl.of_alist_exn (module Int) [(125, "(Out-of-bounds Read)"); (787, "(Out-of-bounds Write)"); (415, "(Double Free)"); (416, "(Use After Free)")]
let cwe_incidents = ref [||]
let unknown_cwe_incidents = ref [||]
(** Builds a string of a path of addresses. *)
let build_location_path locations =
let rec internal locations path_str =
match locations with
| [] -> path_str
| hd::[] -> internal [] (path_str ^ (Printf.sprintf "0x%x" hd))
| hd::tl -> internal tl (path_str ^ (Printf.sprintf "0x%x -> " hd)) in
internal locations ""
(** Looks up a concrete address for an id in the location table loc_tbl. *)
let map_id_to_location id loc_tbl =
match Hashtbl.find loc_tbl id with
| Some loc -> loc
| _ -> failwith "Expected location in hashtbl but failed"
(** Translates a list of incident ids to a list of concrete addresses. *)
let get_incident_locations_from_ids ids location_tbl =
let incident_locations = ref [] in
Sexplib__Sexp_with_layout.List.iter ids ~f:(fun id -> incident_locations := (map_id_to_location (Sexp.to_string id) location_tbl)::(!incident_locations)); !incident_locations
let build_description (incident_str : string) (end_point : string) (paths : string list) : string =
let pretty_paths = ref "" in
pretty_paths := !pretty_paths^end_point^"\n";
List.iter ~f:(fun path ->
let clean_path = String.drop_suffix path 3 in
pretty_paths := !pretty_paths^"\n "^clean_path
) paths;
sprintf "%s %s \n" incident_str !pretty_paths
let report_cwe _ =
Array.iter ~f:(fun (id, loc_hash) ->
let incident_str = (Hashtbl.find_exn known_incidents id) in
Hashtbl.iter_keys loc_hash ~f:(fun end_point ->
let paths = (Hashtbl.find_multi loc_hash end_point) in
let description = build_description incident_str end_point paths in
let other = List.map ~f:(fun path ->
let clean_path = String.drop_suffix path 3 in
["path"; clean_path]
) paths in
let cwe = sprintf "CWE%d" id in
collect_cwe_warning (cwe_warning_factory cwe version ~addresses:[end_point] ~other:other description)
)
) !cwe_incidents
let report_unknown_incidents _ =
Array.iter ~f:(fun (path, inc) ->
let description = inc ^ " " ^ path in
collect_cwe_warning (cwe_warning_factory inc version ~other:[["path"; path]] description)
) !unknown_cwe_incidents
let collect_known_incidents (cwe : int) (execution_path : string) =
Hashtbl.add_multi collected_locations ~key:cwe ~data:(String.rsplit2_exn execution_path ~on:' ')
let collect_unknown_incidents (path_inc : string * string) =
unknown_cwe_incidents := Array.append !unknown_cwe_incidents [|path_inc|]
let parse_reports _ =
Hashtbl.iter_keys collected_locations ~f:(fun id ->
let loc_hashtbl = Hashtbl.create (module String) ~size:3 in
List.iter ~f:(fun (path, end_point) ->
Hashtbl.add_multi loc_hashtbl ~key:end_point ~data:path
) (Hashtbl.find_multi collected_locations id);
cwe_incidents := Array.append !cwe_incidents [|(id, loc_hashtbl)|]
)
(** Reports an incident. *)
let report incident location_tbl =
match incident with
| name::ids ->
begin
let incident_locations = get_incident_locations_from_ids ids location_tbl in
let filtered_locs = Int.Set.to_list (Int.Set.of_list (List.concat incident_locations)) in
let incident_str = Sexp.to_string name in
let location_path = build_location_path filtered_locs in
if Set.mem !reported_events location_path
then
()
else
begin
reported_events := Set.add !reported_events location_path;
match incident_str with
| "memcheck-out-of-bound" -> collect_known_incidents 125 location_path
| "memcheck-double-release" -> collect_known_incidents 415 location_path
| "memcheck-use-after-release" -> collect_known_incidents 416 location_path
| _ -> collect_unknown_incidents (location_path, incident_str)
end
end
| __ -> failwith "Strange incident sexp encountered"
all:
bapbuild -pkgs yojson,unix,ppx_jane,cwe_checker_core cwe_checker_pointer_inference_debug.plugin
bapbundle install cwe_checker_pointer_inference_debug.plugin
clean:
bapbuild -clean
uninstall:
bapbundle remove cwe_checker_pointer_inference_debug.plugin
open Bap.Std
open Core_kernel
open Cwe_checker_core
include Self()
let main project =
let program = Project.program project in
let tid_map = Address_translation.generate_tid_map program in
Pointer_inference.run_and_print_debug project tid_map
module Cmdline = struct
open Config
let () = when_ready (fun ({get=(!!)}) -> Project.register_pass' main)
let () = manpage [`S "DESCRIPTION";
`P "This plugin prints verbose debug information from the pointer inference analysis of the cwe_checker to stdout."]
end
all:
bapbuild -pkgs yojson,unix,ppx_jane,cwe_checker_core cwe_checker_type_inference.plugin
bapbundle install cwe_checker_type_inference.plugin
clean:
bapbuild -clean
uninstall:
bapbundle remove cwe_checker_type_inference.plugin
open Bap.Std
open Core_kernel
open Cwe_checker_core
let () = Project.register_pass Type_inference.compute_pointer_register
all:
bapbuild -pkgs yojson,unix,ppx_jane,cwe_checker_core cwe_checker_type_inference_print.plugin
bapbundle install cwe_checker_type_inference_print.plugin
clean:
bapbuild -clean
uninstall:
bapbundle remove cwe_checker_type_inference_print.plugin
open Bap.Std
open Core_kernel
open Cwe_checker_core
include Self()
let main json_output file_output project =
let program = Project.program project in
let tid_map = Address_translation.generate_tid_map program in
Type_inference.print_type_info_tags project tid_map;
if json_output then
begin
match Project.get project filename with
| Some fname -> Log_utils.emit_json fname file_output
| None -> Log_utils.emit_json "" file_output
end
else
Log_utils.emit_native file_output
module Cmdline = struct
open Config
let json_output = flag "json" ~doc:"Outputs the result as JSON."
let file_output = param string "out" ~doc:"Path to output file."
let () = when_ready (fun ({get=(!!)}) -> Project.register_pass' ~deps:["cwe-checker-type-inference"] (main !!json_output !!file_output))
let () = manpage [`S "DESCRIPTION";
`P "This plugin prints the results of the type inference plugin."]
end
Runs cwe_checker's checks that utilize bap primus emulation.
\ No newline at end of file
;; taken from
;;https://github.com/BinaryAnalysisPlatform/bap-recipes/blob/master/primus-checks/limit-malloc.lisp
;; up to 4 Mb each chunk, up to 128 Mbytes total
(defmethod init ()
(set *malloc-max-chunk-size* (* 4 1024 1024))
(set *malloc-guard-edges* 0)
(set *malloc-max-arena-size* (* 32 *malloc-max-chunk-size*))
(set *malloc-arena-start* brk)
(set *malloc-zero-sentinel* 0))
(option primus-lisp-add $prefix)
(option primus-lisp-load
posix
memcheck-malloc
limit-malloc
taint-sources
sensitive-sinks)
(option primus-promiscuous-mode)
(option primus-greedy-scheduler)
(option primus-limit-max-length 4096)
(option pass cwe-checker-emulation)
Runs those checks of the cwe-checker plugin that rely on static analysis.
(option pass cwe-checker)
(option rooter internal)
(option cwe-checker-config $prefix/../../src/config.json)
Prints the results of the type inference pass of each block.
(option pass cwe-type-inference-print)
open Core_kernel
open Bap.Std
open Graphlib.Std
open Format
include Self()
module CG = Graphs.Callgraph
module CFG = Graphs.Tid
type proof =
| Calls of CG.edge path
| Sites of CFG.edge path
(** Taken from https://stackoverflow.com/questions/8373460/substring-check-in-ocaml *)
let contains_substring search target =
Option.is_some (String.substr_index ~pattern:search target)
let format_path get_source get_destination path tid_map =
let e_count = List.length (Seq.to_list (Path.edges path)) in
if e_count = 0 then "()" else
begin
let format_node n = sprintf "%s" (Address_translation.translate_tid_to_assembler_address_string n tid_map) in
let formated_start_node = format_node (get_source (Path.start path)) in
let formated_rest_nodes = List.map (Seq.to_list @@ Path.edges path) ~f:(fun e -> format_node (get_destination e)) in
let formated_full_path = "(" ^ formated_start_node ^ ", " ^ (String.concat ~sep:", " formated_rest_nodes) ^ ")" in
formated_full_path
end
let find_subfunction_name program name =
Term.enum sub_t program
|> Seq.find_map ~f:(fun s -> Option.some_if (contains_substring name (Sub.name s)) (Term.tid s))
let get_tids_from_cwe_hit (cwe_hit: Log_utils.CweWarning.t) =
cwe_hit.tids
let reaches cg callee target =
Graphlib.is_reachable (module CG) cg callee target
(* ignores indirect calls and jumps as well as return statements and interupts *)
let callsites cg target sub =
Term.enum blk_t sub |>
Seq.concat_map ~f:(fun blk ->
Term.enum jmp_t blk |> Seq.filter_map ~f:(fun j ->
match Jmp.kind j with
| Goto _ | Ret _ | Int (_,_) -> None
| Call destination -> begin match Call.target destination with
| Direct tid when reaches cg tid target -> Some (Term.tid blk)
| _ -> None
end))
let verify source destination program : proof option =
let cg = Program.to_graph program in
match Graphlib.shortest_path (module CG) cg source destination with
| Some path -> Some (Calls path)
| None ->
Term.enum sub_t program |> Seq.find_map ~f:(fun sub ->
let g = Sub.to_graph sub in
Seq.find_map (callsites cg source sub) ~f:(fun sc ->
Seq.find_map (callsites cg destination sub) ~f:(fun dc ->
if Tid.equal sc dc then None
else Graphlib.shortest_path (module CFG) g sc dc))) |>
Option.map ~f:(fun p -> Sites p)
let get_fst_tid_from_cwe_hit (cwe_hit: Log_utils.CweWarning.t) =
match cwe_hit.tids with
| [] -> None
| hd :: _ -> Some (Bap.Std.Tid.from_string_exn hd)
let cwe_hit_fst_addr cwe_hit =
match get_tids_from_cwe_hit cwe_hit with
| [] -> Bap.Std.Tid.from_string_exn "0x00"
| hd :: _ -> Bap.Std.Tid.from_string_exn hd
let block_has_callsite blk t =
Term.enum jmp_t blk |>
Seq.exists ~f:(fun j ->
match Jmp.kind j with
| Goto _ | Ret _ | Int (_,_) -> false
| Call destination -> begin match Call.target destination with
| Direct tid -> Tid.(=) tid t
| _ -> false
end)
let collect_callsites program t =
Term.enum sub_t program
|> Seq.filter_map ~f:(fun s -> if Term.enum blk_t s |>
Seq.exists ~f:(fun b -> block_has_callsite b t) then Some s else None)
|> Seq.map ~f:(fun s -> Term.tid s)
let sub_has_tid sub tid =
Term.enum blk_t sub
|> Seq.exists ~f:(fun blk -> Tid.(=) (Term.tid blk) tid || Blk.elts blk
|> Seq.exists ~f:(fun e -> match e with
| `Def d -> Tid.(=) (Term.tid d) tid
| `Jmp j -> Tid.(=) (Term.tid j) tid
| `Phi p -> Tid.(=) (Term.tid p) tid ))
let find_sub_tid_of_term_tid program tid =
match tid with
| Some t -> let s = Term.enum sub_t program
|> Seq.find ~f:(fun s -> sub_has_tid s t) in
begin
match s with
| Some f -> Some (Term.tid f)
| None -> None
end
| None -> None
let log_path p source source_tid destination tid_map =
let source_addr = Address_translation.translate_tid_to_assembler_address_string source_tid tid_map in
let destination_addr = Address_translation.translate_tid_to_assembler_address_string
(cwe_hit_fst_addr destination) tid_map in
begin match p with
| Calls p ->
let path_str = format_path CG.Edge.src CG.Edge.dst p tid_map in
let current_path = Log_utils.check_path_factory source source_addr destination_addr destination_addr ~path:[] ~path_str:path_str in
Log_utils.collect_check_path current_path
| Sites p -> let path_str = format_path CFG.Edge.src CFG.Edge.dst p tid_map in
let current_path = Log_utils.check_path_factory source source_addr destination_addr destination_addr ~path:[] ~path_str:path_str in
Log_utils.collect_check_path current_path
end
let verify_one program tid_map source destination source_tid destination_tid =
match verify source_tid destination_tid program with
| None -> ()
| Some p -> log_path p source source_tid destination tid_map
let find_source_sink_pathes source destination program tid_map =
match Option.both (find_subfunction_name program source) (find_sub_tid_of_term_tid program (get_fst_tid_from_cwe_hit destination)) with
| None -> () (*one or both functions are not utilized.*)
| Some (callsite_tid, destination_tid) ->
begin
collect_callsites program callsite_tid
|> Seq.iter ~f:(fun source_tid -> verify_one program tid_map source destination source_tid destination_tid )
end
let check_path prog tid_map input_functions cwe_hits =
List.iter input_functions ~f:(fun f ->
List.iter cwe_hits ~f:(fun h -> find_source_sink_pathes f h prog tid_map))
(**
This analyzer module checks if there exists a path from an input function (e.g. recv or scanf) to a location of a CWE hit. The existence of such a path is great news: we might trigger the CWE hit via user provided input. This helps analysts to further priotize the CWE hits.
The analysis prooves that there is a path that fulfills the following constraints:
- it starts at the location of an input function
- it ends at a CWE hit
This module is loosely based on the BAP tutorial (https://github.com/BinaryAnalysisPlatform/bap-tutorial/blob/master/src/path_check/path_check.ml).
*)
val name : string
val version : string
val check_path : Bap.Std.program Bap.Std.term -> Bap.Std.word Bap.Std.Tid.Map.t -> string list -> Log_utils.CweWarning.t list -> unit
open Bap.Std
open Core_kernel
let (+), (-) = Bitvector.(+), Bitvector.(-)
let (>) x y = Bitvector.(>) (Bitvector.signed x) (Bitvector.signed y)
let (<) x y = Bitvector.(<) (Bitvector.signed x) (Bitvector.signed y)
(* let (>=) x y = Bitvector.(>=) (Bitvector.signed x) (Bitvector.signed y) *)
let (<=) x y = Bitvector.(<=) (Bitvector.signed x) (Bitvector.signed y)
let (=) x y = Bitvector.(=) x y
type 'a mem_node = {
pos: Bitvector.t; (* address of the element *)
size: Bitvector.t; (* size (in bytes) of the element *)
data: ('a, unit) Result.t;
} [@@deriving bin_io, compare, sexp]
type 'a t = 'a mem_node list [@@deriving bin_io, compare, sexp]
let empty () : 'a t =
[]
(** Return an error mem_node at the given position with the given size. *)
let error_elem ~pos ~size =
{ pos = pos;
size = size;
data = Error ();}
let rec add mem_region elem ~pos ~size =
let () = if pos + size < pos then failwith "[CWE-checker] element out of bounds for mem_region" in
let new_node = {
pos=pos;
size=size;
data=Ok(elem);
} in
match mem_region with
| [] -> new_node :: []
| head :: tail ->
if head.pos + head.size <= pos then
head :: (add tail elem ~pos ~size)
else if pos + size <= head.pos then
new_node :: mem_region
else begin (* head and new node intersect => at the intersection, head gets overwritten and the rest of head gets marked as error. *)
let tail = if head.pos + head.size > pos + size then (* mark the right end of head as error *)
let err = error_elem ~pos:(pos + size) ~size:(head.pos + head.size - (pos + size)) in
err :: tail
else
tail in
let tail = add tail elem ~pos ~size in (* add the new element*)
let tail = if head.pos < pos then (* mark the left end of head as error *)
let err = error_elem ~pos:(head.pos) ~size:(pos - head.pos) in
err :: tail
else
tail in
tail
end
let rec get mem_region pos =
match mem_region with
| [] -> None
| head :: tail ->
if head.pos > pos then
None
else if head.pos = pos then
match head.data with
| Ok(x) -> Some(Ok(x, head.size))
| Error(_) -> Some(Error(()))
else if head.pos + head.size <= pos then
get tail pos
else
Some(Error(())) (* pos intersects some data, but does not equal its starting address*)
let rec remove mem_region ~pos ~size =
let () = if pos + size < pos then failwith "[CWE-checker] element out of bounds for mem_region" in
match mem_region with
| [] -> []
| hd :: tl ->
if hd.pos + hd.size <= pos then
hd :: remove tl ~pos ~size
else if pos + size <= hd.pos then
mem_region
else
let mem_region = remove tl ~pos ~size in
let mem_region =
if hd.pos + hd.size > pos + size then
error_elem ~pos:(pos + size) ~size:(hd.pos + hd.size - (pos + size)) :: mem_region
else
mem_region in
let mem_region =
if hd.pos < pos then
error_elem ~pos:hd.pos ~size:(pos - hd.pos) :: mem_region
else
mem_region in
mem_region
let rec mark_error mem_region ~pos ~size =
let () = if pos + size < pos then failwith "[CWE-checker] element out of bounds for mem_region" in
match mem_region with
| [] -> (error_elem ~pos ~size) :: []
| hd :: tl ->
if hd.pos + hd.size <= pos then
hd :: (mark_error tl ~pos ~size)
else if pos + size <= hd.pos then
(error_elem ~pos ~size) :: mem_region
else
let start_pos = Word.min pos hd.pos in
let end_pos_plus_one = Word.max (pos + size) (hd.pos + hd.size) in
mark_error tl ~pos:start_pos ~size:(end_pos_plus_one - start_pos)
(* TODO: This is probably a very inefficient implementation in some cases. Write a faster implementation if necessary. *)
let rec merge mem_region1 mem_region2 ~data_merge =
match (mem_region1, mem_region2) with
| (value, [])
| ([], value) -> value
| (hd1 :: tl1, hd2 :: tl2) ->
if hd1.pos + hd1.size <= hd2.pos then
hd1 :: merge tl1 mem_region2 ~data_merge
else if hd2.pos + hd2.size <= hd1.pos then
hd2 :: merge mem_region1 tl2 ~data_merge
else if hd1.pos = hd2.pos && hd1.size = hd2.size then
match (hd1.data, hd2.data) with
| (Ok(data1), Ok(data2)) -> begin
match data_merge data1 data2 with
| Some(Ok(value)) -> { hd1 with data = Ok(value) } :: merge tl1 tl2 ~data_merge
| Some(Error(_)) -> {hd1 with data = Error(())} :: merge tl1 tl2 ~data_merge
| None -> merge tl1 tl2 ~data_merge
end
| _ -> { hd1 with data = Error(()) } :: merge tl1 tl2 ~data_merge
else
let start_pos = Word.min hd1.pos hd2.pos in
let end_pos_plus_one = Word.max (hd1.pos + hd1.size) (hd2.pos + hd2.size) in
let mem_region = merge tl1 tl2 ~data_merge in
mark_error mem_region ~pos:start_pos ~size:(end_pos_plus_one - start_pos)
let rec equal (mem_region1:'a t) (mem_region2:'a t) ~data_equal : bool =
match (mem_region1, mem_region2) with
| ([], []) -> true
| (hd1 :: tl1, hd2 :: tl2) ->
if hd1.pos = hd2.pos && hd1.size = hd2.size then
match (hd1.data, hd2.data) with
| (Ok(data1), Ok(data2)) when data_equal data1 data2 ->
equal tl1 tl2 ~data_equal
| (Error(()), Error(())) -> equal tl1 tl2 ~data_equal
| _ -> false
else
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
)
(** contains an abstract memory region data type where you can assign arbitrary data to locations
inside the memory regions. A memory region has no fixed size, so it can be used
for memory regions of variable size like arrays or stacks.
TODO: Right now this data structure is unsuited for elements that get only partially loaded. *)
open Bap.Std
open Core_kernel
type 'a t [@@deriving bin_io, compare, sexp]
(** Get an empty memory region- *)
val empty: unit -> 'a t
(** Add an element to the memory region. If the element intersects existing elements,
the non-overwritten part gets marked as Error *)
val add: 'a t -> 'a -> pos:Bitvector.t -> size:Bitvector.t -> 'a t
(** Mark the memory region between pos (included) and pos+size (excluded) as empty.
If elements get partially removed, mark the non-removed parts as Error *)
val remove: 'a t -> pos:Bitvector.t -> size:Bitvector.t -> 'a t
(** Returns the element and its size at position pos or None, when there is no element at that position.
If pos intersects an element but does not match its starting position, it returns Some(Error(())). *)
val get: 'a t -> Bitvector.t -> (('a * Bitvector.t), unit) Result.t Option.t
(** Merge two memory regions. Elements with the same position and size get merged using
data_merge, other intersecting elements get marked as Error. Note that data_merge
may return None (to remove the elements from the memory region) or Some(Error(_)) to
mark the merged element as error. *)
val merge: 'a t -> 'a t -> data_merge:('a -> 'a -> ('a, 'b) result option) -> 'a t
(** Check whether two memory regions are equal. *)
val equal: 'a t -> 'a t -> data_equal:('a -> 'a -> bool) -> bool
(** Mark an area in the mem_region as containing errors. *)
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
open Bap.Std
open Core_kernel
external rs_run_pointer_inference: Serde_json.t -> string = "rs_run_pointer_inference"
external rs_run_pointer_inference_and_print_debug: Serde_json.t -> unit = "rs_run_pointer_inference_and_print_debug"
type cwelist = Log_utils.CweWarning.t array [@@deriving yojson]
let run (project: Project.t) (tid_map: Bap.Std.word Bap.Std.Tid.Map.t) : unit =
let program = Project.program project in
let entry_points = Symbol_utils.get_program_entry_points program in
let entry_points = List.map entry_points ~f:(fun sub -> Term.tid sub) in
let extern_symbols = Symbol_utils.build_and_return_extern_symbols project program tid_map in
let project_serde = Serde_json.of_project project extern_symbols entry_points tid_map in
let cwe_warnings_json = Yojson.Safe.from_string @@ rs_run_pointer_inference project_serde in
match cwe_warnings_json with
| `List ((`List cwe_warnings) :: (`List log_messages) :: []) ->
List.iter cwe_warnings ~f:(fun warning -> Log_utils.collect_cwe_warning @@ Result.ok_or_failwith @@ Log_utils.CweWarning.of_yojson warning);
List.iter log_messages ~f:(fun message ->
match message with
| `String message_string ->
begin match String.lsplit2 message_string ~on:':' with
| Some("ERROR", msg) -> Log_utils.error @@ String.strip msg
| Some("DEBUG", msg) -> Log_utils.debug @@ String.strip msg
| Some("INFO", msg) -> Log_utils.info @@ String.strip msg
| _ -> failwith "Malformed log-message."
end
| _ -> failwith "Log-message is not a string."
)
| _ -> failwith "Log-message-json not as expected"
let run_and_print_debug (project: Project.t) (tid_map: Bap.Std.word Bap.Std.Tid.Map.t) : unit =
let program = Project.program project in
let entry_points = Symbol_utils.get_program_entry_points program in
let entry_points = List.map entry_points ~f:(fun sub -> Term.tid sub) in
let extern_symbols = Symbol_utils.build_and_return_extern_symbols project program tid_map in
let project_serde = Serde_json.of_project project extern_symbols entry_points tid_map in
rs_run_pointer_inference_and_print_debug project_serde
(** This module manages the communication with the actual pointer inference analysis
through the foreign function interface to Rust.
*)
open Bap.Std
(** Run the pointer inference analysis and log the returned CWE warnings and log messages. *)
val run: Project.t -> Bap.Std.word Bap.Std.Tid.Map.t -> unit
(** Run the pointer inference analysis and print the computed state of each basic block
(at the start and at the end of the basic block respectively)
as json to stdout.
Does not print log messages or CWE warnings.
The output is meant for debugging purposes.
*)
val run_and_print_debug: Project.t -> Bap.Std.word Bap.Std.Tid.Map.t -> unit
(* This file contains analysis passes for type recognition.
It can annotate whether registers or values on the stack hold data or pointers
to memory. For the latter the target memory location is also tracked if known.
Pointers to the heap are tracked by tracking calls to malloc, calloc and realloc.
This analysis does not check whether the return values of these calls are checked
for NULL values (see cwe_476 for that). *)
open Bap.Std
open Core_kernel
(** The PointerTargetInfo contains knowledge about the offset and the alignment of
a pointer into a memory area. Here the alignment is always considered relative
offset zero of the target memory area. *)
module PointerTargetInfo : sig
type t = {
offset: (Bitvector.t, unit) Result.t Option.t;
alignment: (int, unit) Result.t Option.t;
} [@@deriving bin_io, compare, sexp]
end
(** The Register.t type. A register holds either arbitrary data or a pointer to some
memory region. We do track possible targets of the pointer as follows:
- heap objects: tid of corresponding call instruction to malloc, calloc, etc.
- current stack frame: sub_tid of current function
- some other stack frame: tid of corresponding call that left the stack frame.
This way we can distinguish between current stack pointers and pointers to the
stack frame of the same function coming from recursive calls. *)
module Register : sig
type t =
| Pointer of PointerTargetInfo.t Tid.Map.t
| Data
[@@deriving bin_io, compare, sexp]
end
(** The TypeInfo module. A TypeInfo.t structure holds a list of registers with known
type information (see Register.t type) and known type information for values
on the stack. *)
module TypeInfo : sig
type reg_state = (Register.t, unit) Result.t Var.Map.t [@@deriving bin_io, compare, sexp]
type t = {
stack: Register.t Mem_region.t;
reg: reg_state;
} [@@deriving bin_io, compare, sexp]
(* Pretty Printer. At the moment, the output is not pretty at all. *)
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
(** A tag for TypeInfo.t, so that we can annotate basic blocks with known type information
using bap tags. *)
val type_info_tag: TypeInfo.t Value.tag
(** Computes TypeInfo for the given project. Adds tags to each block containing the
TypeInfo at the start of the block. *)
val compute_pointer_register: Project.t -> Project.t
(** Print type info tags. TODO: If this should be used for more than debug purposes,
then the output format should be refactored accordingly. *)
val print_type_info_tags: project:Project.t -> tid_map:word Tid.Map.t -> unit
(** Updates the type info for a single element (Phi/Def/Jmp) of a block. Input
is the type info before execution of the element, output is the type info
after execution of the element. sub_tid is the Tid of the current function
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
(** 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: *)
module Private : sig
val update_block_analysis: Blk.t -> TypeInfo.t -> sub_tid:Tid.t -> project:Project.t -> TypeInfo.t
val function_start_state: Tid.t -> Project.t -> TypeInfo.t
val compute_stack_offset: TypeInfo.t -> Exp.t -> sub_tid:Tid.t -> project:Project.t -> Bitvector.t Option.t
val only_stack_pointer_and_flags: Tid.t -> Project.t -> TypeInfo.t
val merge_type_infos: TypeInfo.t -> TypeInfo.t -> TypeInfo.t
val type_info_equal: TypeInfo.t -> TypeInfo.t -> bool
end
......@@ -6,7 +6,7 @@ edition = "2018"
[dependencies]
structopt = "0.3"
cwe_checker_rs = { path = "../cwe_checker_rs" }
cwe_checker_lib = { path = "../cwe_checker_lib" }
serde_json = "1.0"
directories = "3.0"
nix = "0.19.1"
\ No newline at end of file
use cwe_checker_rs::analysis::graph;
use cwe_checker_rs::utils::binary::RuntimeMemoryImage;
use cwe_checker_rs::utils::log::print_all_messages;
use cwe_checker_rs::utils::{get_ghidra_plugin_path, read_config_file};
use cwe_checker_rs::AnalysisResults;
use cwe_checker_rs::{intermediate_representation::Project, utils::log::LogMessage};
use cwe_checker_lib::analysis::graph;
use cwe_checker_lib::utils::binary::RuntimeMemoryImage;
use cwe_checker_lib::utils::log::print_all_messages;
use cwe_checker_lib::utils::{get_ghidra_plugin_path, read_config_file};
use cwe_checker_lib::AnalysisResults;
use cwe_checker_lib::{intermediate_representation::Project, utils::log::LogMessage};
use nix::{sys::stat, unistd};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
......@@ -27,6 +27,8 @@ struct CmdlineArgs {
out: Option<String>,
/// Specify a specific set of checks to be run as a comma separated list, e.g. 'CWE332,CWE476,CWE782'.
///
/// Use the "--module-names" command line option to get a list of all valid check names.
#[structopt(long, short)]
partial: Option<String>,
......@@ -106,7 +108,7 @@ fn check_file_existence(file_path: String) -> Result<(), String> {
/// Run the cwe_checker with Ghidra as its backend.
fn run_with_ghidra(args: CmdlineArgs) {
let mut modules = cwe_checker_rs::get_modules();
let mut modules = cwe_checker_lib::get_modules();
if args.module_versions {
// Only print the module versions and then quit.
println!("[cwe_checker] module_versions:");
......@@ -189,7 +191,7 @@ fn run_with_ghidra(args: CmdlineArgs) {
// Right now there is only one debug printing function.
// When more debug printing modes exist, this behaviour will change!
if args.debug {
cwe_checker_rs::analysis::pointer_inference::run(
cwe_checker_lib::analysis::pointer_inference::run(
&project,
&runtime_memory_image,
&control_flow_graph,
......@@ -217,7 +219,7 @@ fn run_with_ghidra(args: CmdlineArgs) {
/// Only keep the modules specified by the `--partial` parameter in the `modules` list.
/// The parameter is a comma-separated list of module names, e.g. 'CWE332,CWE476,CWE782'.
fn filter_modules_for_partial_run(
modules: &mut Vec<&cwe_checker_rs::CweModule>,
modules: &mut Vec<&cwe_checker_lib::CweModule>,
partial_param: &str,
) {
let module_names: HashSet<&str> = partial_param.split(',').collect();
......@@ -326,10 +328,10 @@ fn get_project_from_ghidra(file_path: &Path, binary: &[u8], quiet_flag: bool) ->
// Open the FIFO
let file = std::fs::File::open(fifo_path.clone()).expect("Could not open FIFO.");
let mut project_pcode: cwe_checker_rs::pcode::Project =
let mut project_pcode: cwe_checker_lib::pcode::Project =
serde_json::from_reader(std::io::BufReader::new(file)).unwrap();
project_pcode.normalize();
let project: Project = match cwe_checker_rs::utils::get_binary_base_address(binary) {
let project: Project = match cwe_checker_lib::utils::get_binary_base_address(binary) {
Ok(binary_base_address) => project_pcode.into_ir_project(binary_base_address),
Err(_err) => {
if !quiet_flag {
......
open Core_kernel
open Bap.Std
open Symbol_utils
open Log_utils
let name = "CWE190"
let version = "0.1"
let collect_muliplications = Exp.fold ~init:0 (object
inherit [Int.t] Exp.visitor
method! enter_binop op _o1 _o2 binops = match op with
| Bil.TIMES | Bil.LSHIFT -> binops + 1
| _ -> binops
end)
let contains_multiplication d =
let rhs = Def.rhs d in
let binops = collect_muliplications rhs in
binops > 0
let check_multiplication_before_symbol _proj _prog _sub blk jmp tid_map symbols =
Seq.iter (Term.enum def_t blk)
~f:(fun d -> if contains_multiplication d then
let address = (Address_translation.translate_tid_to_assembler_address_string (Term.tid blk) tid_map) in
let description = sprintf "(Integer Overflow or Wraparound) Potential overflow due to multiplication at %s" address in
let tids = [Address_translation.tid_to_string @@ Term.tid blk] in
let symbols = [(Symbol_utils.get_symbol_name_from_jmp jmp symbols)] in
let cwe_warning = cwe_warning_factory name version description ~addresses:[address] ~tids ~symbols in
collect_cwe_warning cwe_warning)
let check_cwe prog proj tid_map symbol_names _ =
match symbol_names with
| hd::[] ->
let symbols = Symbol_utils.build_symbols hd prog in
let calls = get_calls prog in
let relevant_calls = filter_calls_to_symbols calls symbols in
check_calls relevant_calls prog proj tid_map symbols check_multiplication_before_symbol
| _ -> failwith "[CWE190] symbol_names not as expected"
(** This module implements a check for CWE-190: Integer overflow or wraparound.
An integer overflow can lead to undefined behaviour and is especially dangerous
in conjunction with memory management functions.
See {: https://cwe.mitre.org/data/definitions/190.html} for a detailed description.
{1 How the check works}
For each call to a function from the CWE190 symbol list we check whether the
basic block directly before the call contains a multiplication instruction.
If one is found, the call gets flagged as a CWE hit, as there is no overflow
check corresponding to the multiplication befor the call. The default CWE190
symbol list contains the memory allocation functions {i malloc}, {i xmalloc},
{i calloc} and {i realloc}. The list is configurable in config.json.
{1 False Positives}
- There is no check whether the result of the multiplication is actually used
as input to the function call. However, this does not seem to generate a lot
of false positives in practice.
- There is no value set analysis in place to determine whether an overflow is
possible or not at the specific instruction.
{1 False Negatives}
- All integer overflows not in a basic block right before a call to a function
from the CWE190 symbol list.
- All integer overflows caused by addition or subtraction.
*)
val name : 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
open Core_kernel
open Bap.Std
open Log_utils
(* TODO: IVG via gitter:
I see, so you need the CU information, and yes BAP doesn't provide this.
The right way to do this thing (a little bit complicated, but it
will preserve the abstractions), would be the following:
- Define the abstract interface for the CU providers (you can use Source module
or just define it manually with the interface you like)
- Write plugins that will provide implementations
(i.e., using readelf, objdump, IDA, LLVM, or whatever). The implementation shall
subscribe to Project.Info.file information stream and generate CU information every time a new file is open.
Of course, for the prototype your approach will work,
but in general case it is better to use the approach described above. *)
let name = "CWE215"
let version = "0.1"
(* TODO: check if program contains strings like "DEBUG"*)
let check_cwe _ project _ _ _ =
match Project.get project filename with
| Some fname -> begin
let cmd = Format.sprintf "objdump --dwarf=decodedline %s | grep CU" fname in
try
let in_chan = Caml_unix.open_process_in cmd in
In_channel.input_lines in_chan |> List.iter ~f:(fun l ->
let description = sprintf "(Information Exposure Through Debug Information) %s" l in
let cwe_warning = cwe_warning_factory name version description ~symbols:[l] in
collect_cwe_warning cwe_warning)
with
Caml_unix.Unix_error (e,fm,argm) ->
Log_utils.error (sprintf "[%s] {%s} %s %s %s" name version (Caml_unix.error_message e) fm argm)
end
| _ -> failwith "[CWE215] symbol_names not as expected"
(** This module implements a check for CWE-215: Information Exposure Through Debug Information.
Sensitive debugging information can be leveraged to get a better understanding
of a binary in less time.
See {: https://cwe.mitre.org/data/definitions/215.html} for a detailed description.
{1 How the check works}
The binary is checked for debug strings using readelf.
{1 False Positives}
None known.
{1 False Negatives}
- There may be other debug information not found by readelf.
*)
val name : 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
open Core_kernel
open Bap.Std
open Symbol_utils
open Log_utils
include Self()
let name = "CWE243"
let version = "0.1"
let get_call_dests_of_blk blk_tid sub =
match Term.find blk_t sub blk_tid with
| Some blk -> begin
Term.enum jmp_t blk
|> Seq.filter_map ~f:(fun jmp -> match Jmp.kind jmp with
| Goto _ | Ret _ | Int (_,_) -> None
| Call destination -> begin
match Call.target destination with
| Direct addr -> Some addr
| _ -> None
end)
end |> Seq.to_list
| _ -> []
let get_call_dests_of_sub sub =
let entry_blk =(Term.first blk_t sub) in
match entry_blk with
| Some blk -> begin
let blks = Graphlib.Std.Graphlib.postorder_traverse (module Graphs.Tid) (Sub.to_graph sub) ~start:(Term.tid blk) ~rev:true in
List.concat_map (Seq.to_list blks) ~f:(fun blk -> get_call_dests_of_blk blk sub)
end
| _ -> []
let rec check dests (symbols : symbol list) =
match dests with
| [] -> (List.length symbols) = 0
| hd :: tl ->
begin
match symbols with
| [] -> true
| first_symbol :: symbol_rest -> begin
match first_symbol.address with
| Some address -> if Tid.(=) address hd then check tl symbol_rest else check tl symbols
| _ -> false
end
end
let check_route sub symbols =
let call_dests = get_call_dests_of_sub sub in
let res = check call_dests symbols in
if res then res else res
(** Checks one possible valid path (combination of APIs) of chroot. *)
let check_path prog _tid_map sub path =
let symbols = build_symbols path prog in
if List.length symbols = List.length path then
begin
if List.length symbols = List.length path then
check_route sub symbols
else
false
end
else
false
(** Checks a subfunction for CWE-243. Only functions that actually call "chroot" are considered.
It checks each of the configured VALID pathes found in config.json, e.g.
"chroot_pathes": [["chroot", "chdir"], ["chdir", "chroot", "setresuid"], ["chdir", "chroot", "seteuid"],
["chdir", "chroot", "setreuid"], ["chdir", "chroot", "setuid"]].
If all of them fail then we supose that the program handles chroot on
*)
let check_subfunction prog tid_map sub pathes =
if sub_calls_symbol prog sub "chroot" then
begin
let path_checks: Bool.t List.t = List.map pathes ~f:(fun path -> check_path prog tid_map sub path) in
if not (List.exists path_checks ~f:(fun x -> x)) then
let address = (Address_translation.translate_tid_to_assembler_address_string (Term.tid sub) tid_map) in
let tid = Address_translation.tid_to_string @@ Term.tid sub in
let symbol = Term.name sub in
let description = sprintf
"(The program utilizes chroot without dropping privileges and/or changing the directory) at %s (%s)"
address
symbol in
let cwe_warning = cwe_warning_factory name version description ~addresses:[address] ~tids:[tid] ~symbols:[symbol] in
collect_cwe_warning cwe_warning
end
let check_cwe prog _proj tid_map pathes _ =
let chroot_symbol = find_symbol prog "chroot" in
match chroot_symbol with
| Some _ ->
Seq.iter (Term.enum sub_t prog) ~f:(fun sub -> check_subfunction prog tid_map sub pathes)
| _ -> ()
(** This module implements a check for CWE-243: Creation of chroot Jail Without Changing Working Directory.
Creating a chroot Jail without changing the working directory afterwards does
not prevent access to files outside of the jail.
See {: https://cwe.mitre.org/data/definitions/243.html} for detailed a description.
{1 How the check works}
According to {: http://www.unixwiz.net/techtips/chroot-practices.html}, there are
several ways to achieve the safe creation of a chroot jail, e.g. chdir -> chroot -> setuid.
They are configurable in config.json. We check whether each function that calls
chroot is using one of these safe call sequences to do so. If not, a warning is emitted.
{1 False Positives}
None known.
{1 False Negatives}
None known.
*)
val name : 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
open Core_kernel
open Bap.Std
open Log_utils
let name = "CWE248"
let version = "0.1"
(* Print the findings to the log *)
let print_uncatched_exception block_tid ~tid_map =
let address = (Address_translation.translate_tid_to_assembler_address_string block_tid tid_map) in
let description = sprintf "(Possibly Uncaught Exception) (Exception thrown at %s)." address in
let cwe_warning = cwe_warning_factory name version description ~addresses:[address] in
collect_cwe_warning cwe_warning
(* Extract the name of a direct call, if the block contains a direct call. *)
let extract_direct_call_symbol block =
match Symbol_utils.extract_direct_call_tid_from_block block with
| Some(tid) -> Some(Tid.name tid)
| None -> None
(* check whether block contains a direct call to a symbol with name symbol_name *)
let contains_symbol block symbol_name =
match extract_direct_call_symbol block with
| Some(symb) -> String.(=) symb symbol_name
| None -> false
(* Checks whether a subfunction contains a catch block. *)
let contains_catch subfunction =
let blocks = Term.enum blk_t subfunction in
Seq.exists blocks ~f:(fun block -> contains_symbol block "@__cxa_begin_catch")
(* Find all calls to subfunctions that are reachable from this subfunction. The calls are returned
as a list, except for calls to "@__cxa_throw", which are logged as possibly uncaught exceptions. *)
let find_calls_and_throws (subfunction: Sub.t) ~tid_map : Tid.t List.t =
let blocks = Term.enum blk_t subfunction in
Seq.fold blocks ~init:[] ~f:(fun call_list block ->
if contains_symbol block "@__cxa_throw" then
let () = print_uncatched_exception (Term.tid block) ~tid_map:tid_map in
call_list
else
match Symbol_utils.extract_direct_call_tid_from_block block with
| Some(tid) -> tid :: call_list
| None -> call_list
)
(* find exception throws with for which an exception handler was not necessarily allocated beforehand.
The return value is a list of all already checked functions.*)
let rec find_uncaught_exceptions subfunction already_checked_functions program ~tid_map =
if contains_catch subfunction then
(* This function contains a catch so we assume every throw reachable from here is catched. *)
already_checked_functions
else
let subfunction_calls = find_calls_and_throws subfunction ~tid_map:tid_map in
List.fold subfunction_calls ~init:already_checked_functions ~f:(fun already_checked subfunc ->
match List.exists ~f:(fun a -> Tid.(=) a subfunc) already_checked with
| true -> already_checked
| false -> find_uncaught_exceptions ~tid_map:tid_map (Core_kernel.Option.value_exn (Term.find sub_t program subfunc)) (subfunc :: already_checked) program)
(* Search for uncatched exceptions for each entry point into the binary.
TODO: Exceptions, that are catched when starting from one entry point, but not from another, are masked this
way. We should check whether this produces a lot of false negatives. *)
let check_cwe program _project tid_map _symbol_pairs _ =
let entry_points = Symbol_utils.get_program_entry_points program in
let _: Tid.t List.t = List.fold entry_points ~init:[] ~f:(fun already_checked_functions sub -> find_uncaught_exceptions ~tid_map:tid_map sub already_checked_functions program) in
()
(** This module implements a check for CWE-248: Uncaught Exception.
An uncaught exception may lead to a crash.
See {: https://cwe.mitre.org/data/definitions/248.html} for a detailed description.
{1 How the check works}
The tool searches for exception throws that are reachable in the callgraph without
touching a function that contains a catch block. We do not check whether a catch block
can actually catch the thrown exceptions, thus we generate some false negatives.
{1 False Positives}
- There is no check whether a specific exception throw can be triggered or not
{1 False Negatives}
- An exception that gets catched through one execution path but would not get
catched through a different execution path will not get flagged.
- It is not checked whether the catch block can actually catch a thrown exception
or not. A catch block may only be able to catch exceptions of a specific type.
*)
val name : 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
open Core_kernel
open Symbol_utils
open Log_utils
let name = "CWE332"
let version = "0.1"
let check_cwe program _proj _tid_map _symbol_pairs _ =
match Option.both (find_symbol program "srand") (find_symbol program "rand") with
| None -> begin
match (find_symbol program "rand") with
| None -> ()
| Some _ -> begin
let description = "(Insufficient Entropy in PRNG) program uses rand without calling srand before" in
let cwe_warning = cwe_warning_factory name version description in
collect_cwe_warning cwe_warning
end
end
| Some (_srand_tid, _rand_tid) -> ()
(** This module implements a check for CWE332: Insufficient Entropy in PRNG.
This can happen, for instance, if the PRNG is not seeded. A classical example
would be calling rand without srand. This could lead to predictable random
numbers and could, for example, weaken crypto functionality.
See {: https://cwe.mitre.org/data/definitions/332.html} for a detailed description.
{1 How the check works}
We check whether the program calls rand but not srand.
{1 False Positives}
None known
{1 False Negatives}
- It is not checked whether srand gets called before rand
*)
val name : string
val version : string
val check_cwe : Bap.Std.program Bap.Std.term -> 'a -> 'b -> string list list -> 'c -> unit
open Core_kernel
open Bap.Std
open Log_utils
let name = "CWE367"
let version = "0.1"
let get_calls_to_symbol symbol_name callsites program =
match Symbol_utils.find_symbol program symbol_name with
| Some symbol ->
begin
Seq.filter callsites ~f:(fun callsite -> match Jmp.kind callsite with
| Goto _ | Ret _ | Int (_,_) -> false
| Call destination -> match Call.target destination with
| Direct addr -> Tid.(=) addr symbol
| _ -> false)
end
| None -> Seq.empty
let get_blk_tid_of_tid sub tid =
let blk = Seq.find (Term.enum blk_t sub) ~f:(
fun b ->
match Term.last jmp_t b with
| Some last_term -> Tid.(=) tid (Term.tid last_term)
| None -> false) in
match blk with
| Some b -> Term.tid b
| _ -> assert(false)
let is_reachable sub source sink =
let cfg = Sub.to_graph sub in
let source_tid = Term.tid source in
let sink_tid = Term.tid sink in
let source_blk = get_blk_tid_of_tid sub source_tid in
let sink_blk = get_blk_tid_of_tid sub sink_tid in
Graphlib.Std.Graphlib.is_reachable (module Graphs.Tid) cfg source_blk sink_blk
let handle_sub sub program tid_map _symbols source_sink_pair =
match source_sink_pair with
| [source;sink;] -> begin
if (Symbol_utils.sub_calls_symbol program sub source) && (Symbol_utils.sub_calls_symbol program sub sink) then
begin
let calls = Symbol_utils.get_direct_callsites_of_sub sub in
let source_calls = get_calls_to_symbol source calls program in
let sink_calls = get_calls_to_symbol sink calls program in
Seq.iter source_calls ~f:(fun source_call ->
Seq.iter sink_calls ~f:(fun sink_call ->
if is_reachable sub source_call sink_call then
let address = (Address_translation.translate_tid_to_assembler_address_string (Term.tid sub) tid_map) in
let tid = Address_translation.tid_to_string @@ Term.tid sub in
let symbol = (Term.name sub) in
let other = [["source"; source]; ["sink"; sink]] in
let description = sprintf
"(Time-of-check Time-of-use Race Condition) %s is reachable from %s at %s (%s). This could lead to a TOCTOU."
sink
source
address
symbol in
let cwe_warning = cwe_warning_factory
name
version
description
~other:other
~addresses:[address]
~tids:[tid]
~symbols:[symbol] in
collect_cwe_warning cwe_warning
else
()))
end
else
()
end
| _ -> ()
let check_cwe program _proj tid_map symbol_pairs _ =
List.iter symbol_pairs ~f:(fun current_pair ->
let symbols = Symbol_utils.build_symbols current_pair in
Seq.iter (Term.enum sub_t program) ~f:(fun s -> handle_sub s program tid_map symbols current_pair))
(** This module implements a check for CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition.
Time-of-check Time-of-use race conditions happen when a property of a resource
(e.g. access rights of a file) get checked before the resource is accessed, leaving
a short time window for an attacker to change the entity and thus invalidating
the check before the access.
See {: https://cwe.mitre.org/data/definitions/367.html} for a detailed description.
{1 How the check works}
For pairs of (check-call, use-call), configurable in config.json, we check whether
a function may call the check-call before the use-call.
{1 False Positives}
- The check-call and the use-call may access different, unrelated resources
(e. g. different files).
{1 False Negatives}
- If the check-call and the use-call happen in different functions it will not
be found by the check.
*)
val name : 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
open Core_kernel
open Bap.Std
open Log_utils
let name = "CWE426"
let version = "0.1"
let calls_privilege_changing_sub sub program symbols =
List.exists symbols ~f:(fun s -> Symbol_utils.sub_calls_symbol program sub s)
let handle_sub sub program tid_map symbols =
if calls_privilege_changing_sub sub program symbols then
begin
if Symbol_utils.sub_calls_symbol program sub "system" then
let symbol = Term.name sub in
let address = Address_translation.translate_tid_to_assembler_address_string (Term.tid sub) tid_map in
let tid = Address_translation.tid_to_string @@ Term.tid sub in
let description = sprintf "(Untrusted Search Path) sub %s at %s may be vulnerable to PATH manipulation."
symbol
address in
let cwe_warning = cwe_warning_factory name version ~addresses:[address] ~tids:[tid] ~symbols:[symbol] description in
collect_cwe_warning cwe_warning
else
()
end
else ()
let check_cwe program _proj tid_map symbols _ =
match symbols with
| hd::[] ->
Seq.iter (Term.enum sub_t program) ~f:(fun s -> handle_sub s program tid_map hd)
| _ -> failwith "[CWE426] symbol_names not as expected"
(** This module implements a check for CWE-426: Untrusted Search Path.
Basically, the program searches for critical resources on an untrusted search
path that can be adjusted by an adversary. For example, see Nebula Level 1
({: https://exploit-exercises.com/nebula/level01/}).
According to the manual page of system() the following problems can arise:
"Do not use system() from a program with set-user-ID or set-group-ID privileges,
because strange values for some environment variables might be used to subvert
system integrity. Use the exec(3) family of functions instead, but not execlp(3)
or execvp(3). system() will not, in fact, work properly from programs with set-user-ID
or set-group-ID privileges on systems on which /bin/sh is bash version 2, since bash 2
drops privileges on startup. (Debian uses a modified bash which does not do this when invoked as sh.)"
See {: https://cwe.mitre.org/data/definitions/426.html} for a detailed description.
{1 How the check works}
We check whether a function that calls a privilege-changing function (configurable
in config.json) also calls system().
{1 False Positives}
- If the call to system() happens before the privilege-changing function, the call
may not be used for privilege escalation
{1 False Negatives}
- If the calls to the privilege-changing function and system() happen in different
functions, the calls will not be flagged as a CWE-hit.
- This check only finds potential privilege escalation bugs, but other types of
bugs can also be triggered by untrusted search paths.
*)
val name : 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
open Core_kernel
open Bap.Std
open Log_utils
let name = "CWE457"
let version = "0.1"
let get_defs sub_ssa =
Term.enum blk_t sub_ssa
|> Seq.concat_map ~f:(fun blk -> Term.enum def_t blk)
let collect_stores_of_exp = Exp.fold ~init:0 (object
inherit [int] Exp.visitor
method! enter_store ~mem:_ ~addr:_ ~exp:_ _ _ stores =
stores + 1
end)
let exp_has_store e =
collect_stores_of_exp e > 0
let ints_of_exp = Exp.fold ~init:Word.Set.empty (object
inherit [Word.Set.t] Exp.visitor
method! enter_int i ints = Set.add ints i
end)
let vars_of_exp = Exp.fold ~init:Var.Set.empty (object
inherit [Var.Set.t] Exp.visitor
method! enter_var var vars = Set.add vars var
end)
let vars_contain_mem vars =
let mems = Set.filter vars ~f:(fun var -> match Var.to_string var with
| "mem" -> true
| _ -> false) in
Set.length mems > 0
(*FIXME: this is architecture dependent and ugly*)
let get_min_fp_offset arch =
match arch with
| `x86 | `x86_64 -> 0x10000
| _ -> 0x0
(*FIXME: this is architecture dependent and ugly*)
let get_fp_of_arch arch =
match arch with
| `x86 -> "EBP"
| `x86_64 -> "RBP"
| `armv4 | `armv5 | `armv6 | `armv7 | `armv4eb | `armv5eb | `armv6eb | `armv7eb -> "R11"
| `mips | `mips64 | `mips64el | `mipsel -> "FP"
| `ppc | `ppc64 | `ppc64le -> "R31"
| _ -> failwith "Unknown architecture."
let vars_contain_fp vars fp_pointer =
let regs = Set.filter vars ~f:(fun var -> String.(=) (Var.to_string var) fp_pointer) in
Set.length regs > 0
let is_interesting_load_store def fp_pointer =
let vars = vars_of_exp (Def.rhs def) in
let contains_fp = vars_contain_fp vars fp_pointer in
let contains_mem = vars_contain_mem vars in
contains_mem && contains_fp
(*TODO: implement real filtering*)
let filter_mem_address i min_fp_offset = Set.filter i ~f:(fun elem -> Word.(<) (Word.of_int ~width:32 min_fp_offset) elem)
let log_cwe_warning sub i d tid_map =
let word = Word.to_string i in
let other =[["word"; word]] in
let symbol = Sub.name sub in
let address = Address_translation.translate_tid_to_assembler_address_string (Term.tid d) tid_map in
let tid = Address_translation.tid_to_string @@ Term.tid d in
let description = sprintf
"(Use of Uninitialized Variable) Found potentially unitialized stack variable (FP + %s) in function %s at %s"
word
symbol
address in
let cwe_warning = cwe_warning_factory name version ~other:other ~addresses:[address] ~tids:[tid] ~symbols:[symbol] description in
collect_cwe_warning cwe_warning
let check_subfunction _prog proj tid_map sub =
let fp_pointer = get_fp_of_arch (Project.arch proj) in
let min_fp_offset = get_min_fp_offset (Project.arch proj) in
let stores = ref [||] in
let defs = get_defs sub in
Seq.iter defs ~f:(fun d ->
if is_interesting_load_store d fp_pointer then
let rhs = Def.rhs d in
let ints = ints_of_exp rhs in
begin
if exp_has_store rhs then
begin
let filter_mem_addresses = filter_mem_address ints min_fp_offset in
Set.iter filter_mem_addresses ~f:(fun addr -> stores := Array.append !stores [|addr|])
end
else
begin
let filter_mem_addresses = filter_mem_address ints min_fp_offset in
Set.iter filter_mem_addresses ~f:(fun i -> if not (Array.exists !stores ~f:(fun elem -> Word.(=) elem i)) then
log_cwe_warning sub i d tid_map)
end
end)
let check_cwe prog proj tid_map _symbol_names _ =
Seq.iter (Term.enum sub_t prog) ~f:(fun sub -> check_subfunction prog proj tid_map sub)
(** This module implements a check for CWE-457: Use of Uninitialized Variable.
Accessing variables on the stack or heap before their initialization can lead
to unintended or undefined behaviour, which could be exploited by an attacker.
See {: https://cwe.mitre.org/data/definitions/457.html} for a detailed description.
{1 How the check works}
The check uses the frame pointer to look for loads to addresses which do not
have an associated store instruction.
{1 False Positives}
- The check is still very basic and can be easily get confused by loads/stores
through different registers than the frame pointer.
- Modern compilers often use only the stack pointer for stack access, freeing
up the frame pointer as a general purpose register. This is not recognized by
the check.
{1 False Negatives}
- Heap accesses are not examined by the check.
- Memory accesses through different registers than the frame pointer are not
examined by the check.
*)
val name: 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
open Core_kernel
open Bap.Std
open Symbol_utils
open Log_utils
let name = "CWE467"
let version = "0.1"
let get_pointer_size arch =
Size.in_bytes @@ Arch.addr_size arch
let check_input_is_pointer_size proj _prog _sub blk jmp tid_map symbols =
Seq.iter (Term.enum def_t blk) ~f:(fun d -> match Exp.eval @@ Def.rhs d with
| Imm w ->
begin
try
if get_pointer_size (Project.arch proj) = (Word.to_int_exn w) then
begin
let address = Address_translation.translate_tid_to_assembler_address_string (Term.tid blk) tid_map in
let tid = Address_translation.tid_to_string @@ Term.tid blk in
let symbol = Symbol_utils.get_symbol_name_from_jmp jmp symbols in
let description = sprintf
"(Use of sizeof on a Pointer Type) sizeof on pointer at %s (%s)."
address
symbol in
let cwe_warning = cwe_warning_factory name version ~addresses:[address] ~tids:[tid] ~symbols:[symbol] description in
collect_cwe_warning cwe_warning
end
with _ -> Log_utils.error "Caught exception in module [CWE467]."
end
| _ -> ())
let check_cwe prog proj tid_map symbol_names _ =
match symbol_names with
| hd::[] ->
let symbols = Symbol_utils.build_symbols hd prog in
let calls = get_calls prog in
let relevant_calls = filter_calls_to_symbols calls symbols in
check_calls relevant_calls prog proj tid_map symbols check_input_is_pointer_size
| _ -> failwith "[CWE467] symbol_names not as expected"
(** This module implements a check for CWE-467: Use of sizeof() on a Pointer Type.
Functions like malloc and memmove take a size parameter of some data size as
input. If accidentially the size of a pointer to the data instead of the size of
the data itself gets passed to the function, this can have severe consequences.
See {: https://cwe.mitre.org/data/definitions/467.html} for a detailed description.
{1 How the check works}
The check is quite basic: We check whether in the basic block before a call
to a function listed in the symbols for CWE467 (configurable in in config.json)
an immediate value that equals the size of a pointer (e.g. 4 bytes on x86) is
referenced.
{1 False Positives}
- It is not checked whether the immediate value is actually an input to the call
or not. However, this does not seem to produce false positives in practice.
- The size value might be correct and not a bug.
{1 False Negatives}
- If the incorrect size value is generated before the basic block that contains
the call, the check will not be able to find it.
*)
val name : 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
(** This module implements a check for CWE-476: NULL Pointer Dereference.
Functions like malloc() may return NULL values instead of pointers to indicate
failed calls. If one tries to access memory through this return value without
checking it for being NULL first, this can crash the program.
See {: https://cwe.mitre.org/data/definitions/476.html} for a detailed description.
{1 How the check works}
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 jump instruction.
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
these stubs.
{2 Parameters configurable in config.json}
- 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,
checks all unchecked values still contained in parameter registers. If true, every
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.
{2 Symbols configurable in config.json}
The symbols are the functions whose return values are assumed to be potential
NULL pointers.
{1 False Positives}
- If strict_mem_policy is set to true, writing a return value to memory other than the stack
gets reported even if a NULL pointer check happens right afterwards.
- 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}
- We do not check whether an access to a potential NULL pointer happens regardless
of a prior check.
- We do not check whether the conditional jump instruction checks specifically
for the return value being NULL or something else
- For functions with more than one return value we do not distinguish between
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 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
(**/**)
(* 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
open Core_kernel
open Bap.Std
open Log_utils
let name = "CWE560"
let version = "0.1"
let upper_bound_of_correct_umask_arg_value = 100
let upper_bound_of_correct_chmod_arg_value = 1000
let collect_int_values = Exp.fold ~init:[] (object
inherit [word list] Exp.visitor
method! enter_int x addrs = x :: addrs
end)
let is_chmod_style_arg umask_arg =
umask_arg > upper_bound_of_correct_umask_arg_value && umask_arg < upper_bound_of_correct_chmod_arg_value
let check_umask_arg tid_map blk w =
try
let umask_arg = Word.to_int_exn w in
if is_chmod_style_arg umask_arg then
let address = Address_translation.translate_tid_to_assembler_address_string (Term.tid blk) tid_map in
let tid = Address_translation.tid_to_string @@ Term.tid blk in
let umask_arg_str = sprintf "%d" umask_arg in
let description = sprintf
"(Use of umask() with chmod-style Argument) Function %s calls umask with argument %s"
address
umask_arg_str in
let other = [["umask_arg"; umask_arg_str]] in
let cwe_warning = cwe_warning_factory name version ~addresses:[address] ~tids:[tid] ~other:other description in
collect_cwe_warning cwe_warning
with _ -> Log_utils.error "Caught exception in module [CWE560]."
let check_umask_callsite tid_map blk =
Seq.iter (Term.enum def_t blk) ~f:(fun d ->
let rhs = Def.rhs d in
let int_values = collect_int_values rhs in
List.iter int_values ~f:(fun x -> check_umask_arg tid_map blk x)
)
let blk_calls_umask sym_umask blk =
Term.enum jmp_t blk
|> Seq.exists ~f:(fun callsite -> Symbol_utils.calls_callsite_symbol callsite sym_umask)
let check_subfunction program tid_map sym_umask sub =
if Symbol_utils.sub_calls_symbol program sub "umask" then
Term.enum blk_t sub
|> Seq.filter ~f:(fun blk -> blk_calls_umask sym_umask blk)
|> Seq.iter ~f:(fun blk -> check_umask_callsite tid_map blk)
else
()
let check_subfunctions program tid_map sym_umask =
Seq.iter (Term.enum sub_t program) ~f:(fun sub -> check_subfunction program tid_map sym_umask sub)
let check_cwe program _ tid_map _ _ =
let sym = Symbol_utils.get_symbol_of_string program "umask" in
match sym with
| None -> ()
| Some sym_umask -> check_subfunctions program tid_map sym_umask
(* Functions made available for unit tests *)
module Private = struct
let is_chmod_style_arg = is_chmod_style_arg
end
(** This module implements a check for CWE-560: Use of umask() with chmod-style Argument.
The program uses the system call umask(2) with arguements for chmod(2). For instance,
instead of a reasonable value like 0022 a value like 0666 is passed. This may result wrong
read and/or write access to files and directories, which could be utilized to bypass
protection mechanisms.
See {: https://cwe.mitre.org/data/definitions/560.html} for a detailed description.
{1 How the check works}
This check looks for umask calls and checks if they have a reasonable value, i.e. smaller than
a certain value, currently set to 1000 and greater than a reasonable value for umask, currently set to 100.
{1 False Positives}
- The current version considers all immediate values of an umask callsite's basic
block. It does not check whether the value is an input to the call or not.
{1 False Negatives}
- If the input to umask is not defined in the basic block before the call, the
check will not see it.
- Calls where the input is not an immediate value but a variable are not examined.
*)
val name : 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
(* functions made available for unit tests: *)
module Private : sig
val is_chmod_style_arg : int -> bool
end
open Core_kernel
open Bap.Std
open Log_utils
let name = "CWE676"
let version = "0.1"
let get_call_to_target _cg callee target =
Term.enum blk_t callee |>
Seq.concat_map ~f:(fun blk ->
Term.enum jmp_t blk |> Seq.filter_map ~f:(fun j ->
match Jmp.kind j with
| Goto _ | Ret _ | Int (_,_) -> None
| Call dst -> match Call.target dst with
| Direct tid when Tid.(=) tid (Term.tid target) ->
Some (Term.name callee, Term.tid j, Term.name target)
| _ -> None))
let get_calls_to_symbols cg subfunctions symbols =
(Seq.concat_map subfunctions ~f:(fun subfunction ->
Seq.concat_map symbols ~f:(fun symbol -> get_call_to_target cg subfunction symbol)))
(* FIXME: refactor variable names *)
let print_calls calls ~tid_map =
Seq.iter calls ~f:(fun call -> match call with
| (a, b, c) ->
begin
let address = Address_translation.translate_tid_to_assembler_address_string b tid_map in
let tid = Address_translation.tid_to_string b in
let other = [["dangerous_function"; c]] in
let description = sprintf
"(Use of Potentially Dangerous Function) %s (%s) -> %s."
a
address
c in
let cwe_warning = cwe_warning_factory
name
version
~other:other
~addresses:[address]
~tids:[tid]
~symbols:[a]
description in
collect_cwe_warning cwe_warning
end
)
let resolve_symbols prog symbols =
Term.enum sub_t prog |>
Seq.filter ~f:(fun s -> List.exists ~f:(fun x -> String.(=) x (Sub.name s)) symbols)
let check_cwe prog _proj tid_map symbol_names _ =
match symbol_names with
| hd::[] ->
let subfunctions = Term.enum sub_t prog in
let cg = Program.to_graph prog in
get_calls_to_symbols cg subfunctions (resolve_symbols prog hd)
|> print_calls ~tid_map:tid_map
| _ -> failwith "[CWE676] symbol_names not as expected"
(** This module implements a check for CWE-676: Use of Potentially Dangerous Function.
Potentially dangerous functions like memcpy can lead to security issues like buffer
overflows.
See {: https://cwe.mitre.org/data/definitions/676.html} for a detailed description.
{1 How the check works}
Calls to dangerous functions are flagged. The list of functions that are considered
dangerous can be configured in config.json. The default list is taken from
{: https://github.com/01org/safestringlib/wiki/SDL-List-of-Banned-Functions}.
{1 False Positives}
None known
{1 False Negatives}
None known
*)
val name : 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
open Core_kernel
open Bap.Std
open Log_utils
let name = "CWE782"
let version = "0.1"
(*TODO: check if binary is setuid*)
let handle_sub sub program tid_map _symbols =
if Symbol_utils.sub_calls_symbol program sub "ioctl" then
begin
let address = Address_translation.translate_tid_to_assembler_address_string (Term.tid sub) tid_map in
let tid = Address_translation.tid_to_string @@ Term.tid sub in
let symbol = Term.name sub in
let description = sprintf
"(Exposed IOCTL with Insufficient Access Control) Program uses ioctl at %s (%s). Be sure to double check the program and the corresponding driver."
symbol
address in
let cwe_warning = cwe_warning_factory name version ~addresses:[address] ~tids:[tid] ~symbols:[symbol] description in
collect_cwe_warning cwe_warning
end
else
()
let check_cwe program _proj tid_map symbols _ =
Seq.iter (Term.enum sub_t program) ~f:(fun s -> handle_sub s program tid_map symbols)
(** This module implements a check for CWE-782: Exposed IOCTL with Insufficient Access Control.
See {: https://cwe.mitre.org/data/definitions/782.html} for a detailed description.
{1 How the check works}
Calls to ioctl() get flagged as CWE hits.
{1 False Positives}
- We cannot check whether the call contains sufficient access control.
{1 False Negatives}
- There are other ways to expose I/O control without access control.
*)
val name : 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
open Bap.Std
let name = "Memory"
let version = "0.1"
let check_cwe (_program: Program.t) (project: Project.t) (tid_map: word Tid.Map.t) (_: string list list) (_: string list) =
Pointer_inference.run project tid_map
(** This module implements memory-related CWE checks.
Right now the check detects cases of
- {{: https://cwe.mitre.org/data/definitions/415.html} CWE 415: Double Free}
- {{: https://cwe.mitre.org/data/definitions/416.html} CWE 416: Use After Free}
{1 How the check works}
Via Dataflow Analysis, the check tries to keep track of all memory objects and pointers
known at specific points in the program.
It also keeps track of the status of memory object, i.e. if they have been already freed.
Access to an already freed object generates a CWE warning.
In cases where the analysis cannot reliably determine whether accessed memory has been freed or not,
a CWE warning may (or may not) be issued to the user based on the likelihood of it being a false positive.
Note that the check is still experimental.
Bugs may occur and the rate of false positive and false negative warnings is not yet known.
*)
val name: 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
opam-version: "2.0"
name: "cwe_checker_core"
version: "0.4"
synopsis: "Core library for the cwe_checker package"
description: """
Core library for the cwe_checker suite of tools.
"""
maintainer: "CWE_checker Team <nils-edvin.enkelmann@fkie.fraunhofer.de>"
authors: [ "Thomas Barabosch <thomas.barabosch@fkie.fraunhofer.de>" "Nils-Edvin Enkelmann <nils-edvin.enkelmann@fkie.fraunhofer.de>" ]
license: "LGPL-3.0"
homepage: "https://github.com/fkie-cad/cwe_checker"
bug-reports: "https://github.com/fkie-cad/cwe_checker/issues"
dev-repo: "git+https://github.com/fkie-cad/cwe_checker"
depends: [
"ocaml" {>= "4.08.0"}
"dune" {>= "2.0"}
"yojson" {>= "1.6.0"}
"bap" {>= "2.2.0"}
"alcotest" {>= "0.8.3"}
"core_kernel" {>= "v0.14"}
"ppx_jane" {>= "v0.14"}
"ppx_deriving_yojson" {>= "3.5.1"}
"odoc" {>= "1.4"}
]
depexts: [
"binutils"
]
conflicts: [
"fkie-cad-cwe-checker" {!= "0.2"}
]
build: [
[ "dune" "build" "--profile" "release" ]
]
install: [
[ "dune" "install" ]
]
[package]
name = "cwe_checker_rs"
name = "cwe_checker_lib"
version = "0.5.0-dev"
authors = ["Nils-Edvin Enkelmann <nils-edvin.enkelmann@fkie.fraunhofer.de>"]
edition = "2018"
......@@ -19,5 +19,4 @@ directories = "3.0"
goblin = "0.2"
[lib]
name = "cwe_checker_rs"
crate-type = ["staticlib", "cdylib", "lib"]
name = "cwe_checker_lib"
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