Backward Edge Control Flow Integrity for Real-Time Embedded Systems
A (non-exhaustive) list of software necessary to build the project
make
To compile to assembly (for example the BOF4.c file) and then to binary:
make asm/[name of the file].s
To compile to assembly (for example the BOF4.c file) and then to binary:
make asm_to_bin/[THE-File-Name]
Override the CC variable on the command line with your cross compiler:
make CC=arm-linux-gnueabihf-gcc bin/BOF4
To compile and test the ringbuffer library:
make runtests
To test the record write/verify functionality:
make -B bin/cfi-checker bin/injection-test
LD_PRELOAD=bin/lib/libringbuffer.so bin/cfi-checker bin/injection-test
To compile and test the BOF4 executable:
make asm_to_bin/[filename]
LD_PRELOAD=bin/lib/libringbuffer.so bin/cfi-checker bin/BOF4
To load the cfi-checker injection test in gdb, with debug symbols:
./gdb.sh asm_to_bin/Filename
To end up in the right process, you may have to set follow-fork-mode child
at an appropriate point in gdb.
make -B all
LD_PRELOAD=bin/lib/libringbuffer.so bin/cfi-checker <path to executable>
To generate CFG using angr, both angr and angrutils are required. It is recommended to install these in a virutalenv. The example below assumes that they are installed in a virtualenv named angr. The angr install through pip may return an error due to a bug in libcapstone. To fix this problem, search for libcapstone in the file tree indicated by the installer, and move it to the location where the installer says it is missing. After the installation of both angr and angrutils, change the visualize.py code of angrutils with the modified angrutils code that generates CFG dot files instead of PNG files. To generate the dot file:
workon angr
ipython
Then in iPython:
import angr
from angrutils import *
proj = angr.Project("BOFM", load_options={'auto_load_libs':False})
main = proj.loader.main_bin.get_symbol("main")
start_state = proj.factory.blank_state(addr=main.addr)
cfg = proj.analyses.CFGAccurate(fail_fast=True, enable_symbolic_back_traversal=True, starts=[main.addr], initial_state=start_state)
plot_cfg(cfg, "ais3_cfg", asminst=True, remove_imports=True, remove_path_terminator=True)
The plot CFG will generate a file named test.dot which is the dot file of the CFG for the binary.
Memory layout for ring buffer:
Activity diagram of the CFI-Checker.
The injected set-up code from injectioncode/Call-C-Function-Setup.s calls the function cfg_setup() from include/ringbuffer.h and passes two arguments. These arguments are:
The hotsite ID is a number given to a location where code is injected into the assembly. The hotsite ID is used to determine where in the CFG the execution is.
The hotsite ID is a multiple of 8, plus a suffix of 0-7. The hotsite ID is stored in the binary CFG, but the last three bits are all zeroes in there.
The lowest three bits of the hotsite are used to determine the type of the control flow transfer. With three bits, currenlty eight different transfers can be
distinguished, but only two are used so far:
0 - Indirect branch (BLX
The binary CFG used at runtime is defined in include/cfg.h and shown in diagrams/binary-cfg.svg
The function checker in cfi-checker.c continuously reads data from the ringbuffer when available. It passes this data to a number of functions. Currently these are:
cfi_validate_forward_edge calls the cfg_validate_jump function from src/cfg.c which does this:
(NOTE: Part of this is broken at the moment)
Iterate over all cfgblock structs in the cfg struct:
If the hotsite ID (minus the lowest three bits (transfer type identification)) of the current cfgnode matches the hotsite argument that was passed:
Iterate over the post_data in the cfgnode struct, and declare the transfer valid if a match is found. Otherwise declare the transfer invalid.
If after reaching the end of the binary CFG, no matching cfgnode is found, the transfer is declared invalid too.
The current problem is to get the correct target (aka. post_data) values in the binary CFG: Ali has suggested that after the RBWriteInjector.run() call in injector.py:
set(injector.funcs.keys()
import subprocess
gdb = subprocess.Popen(['gdb', '<name-of-binary>'])
gdb.communicate()
to query the addresses for each function in the binary.cfg_offset
(note, step a few times, it's set in the first function call in main()
.rm /dev/shm/rb_cfi_*
C function for injection:
ANGR: