A simple framework to simulate fault attacks on x86 binaries.
The simulator loads a statically-compiled x86 ELF binary and a "fault" script. The fault script contains commands for the simulator to induce certain kinds of faults (see Fault Script). The simulator simply runs the binary, and induces the faults as specified in the fault script.
The behavior of the simulator can be configured directly via the victim binaries. A victim binary can contain configuration options for the simulator (see Configuration). For example, to run a demo victim:
./simulator demos/victim1.fault demos/victim1
Faults are induced using fault scripts which are loaded with the binary. Commands typically modify parts of the memory, which can be data or code, when they are triggered. Lines starting with # are ignored and can be used for comments.
Currently, a trigger can be an absolute RIP value (i.e., when the instruction pointer reaches a certain position in the binary) or the n-th executed instruction.
@<RIP>
Triggers a fault when the RIP reaches a certain position in the executable. The RIP can be specified as decimal or hexadecimal value.
Skip two bytes when the instruction pointer is at 0x400b51: skip 2 @0x400b51
#<Instruction Number>
Triggers a fault after that many instructions were executed.
Skip two bytes in the instruction stream after executing 10000 instructions: skip 2 #10000
The following commands are supported:
skip <bytes> <trigger>
Skips the given number of bytes in the instruction stream when the given trigger is reached. This can also be negative to jump backwards. Note that this does not skip instructions, as many instructions are larger than one byte.
Skip 1 byte when the instruction pointer is at 0x400b51: skip 1 @0x400b51
zero <address> <trigger>
Sets the given memory address
to 0 when the given trigger is reached. The function zeroes out one byte (i.e., 8 bit).Write a zero word to address 0x6bb330 if the instruction pointer reaches 0x400b6e: zero 0x6bb330 @0x400b6e
havoc <address> <trigger>
Sets the given memory address
to a random value when the given trigger is reached. The function affects one byte (i.e., 8 bit).Write a random word to address 0x6bb330 if the instruction pointer reaches 0x400b6e: havoc 0x6bb330 @0x400b6e
bitflip <bit index> <destination> <trigger>
Flips the -th bit at the memory address when the given trigger is reached.
Flips bit 0 at the memory location 0x400b59 if the instruction pointer reaches 0x400b4d: bitflip 0 0x400b59 @0x400b4d
log <log type>
Enables log output for certain events. Supported events are rip
(prints the current instruction pointer for every instruction), inscnt
(prints the current instruction counter for every instruction), and fault
(prints the details of every successfully induced fault).
The simulator can be configured via the loaded binaries. A binary can include configurations which blacklist certain functions and modify other behavior of the simulator.
To include a configuration, simply include the faultconfig.h
header and insert as many FAULT_CONFIG
statements as required.
An example binary with embedded configuration can look like this:
#include <stdio.h>
#include <faultconfig.h>
volatile int is_admin = 0;
int main() {
if(!is_admin) {
puts("Nope, you are not admin\n");
return 1;
} else {
puts("Success!\n");
return 0;
}
}
FAULT_CONFIG("NOHAVOC");
FAULT_CONFIG("NOSKIP");
FAULT_CONFIG("TIMEOUT=3");
FAULT_CONFIG("FAILEVERY=2");
FAULT_CONFIG("COOLDOWN=100");
FAULT_ENTRY(main);
The following configuration options are supported:
Any of the supported commands for fault scripts can be disabled for a binary. Blacklisting a feature is done with the options NOSKIP
, NOHAVOC
, NOZERO
, NOBITFLIP
, NOLOG
. The features HAVOC
, ZERO
, and BITFLIP
can also be blocked only for the code using NOCODEFAULT
.
For the log
command it is also possible to blacklist only certain log types: NOLOGFAULT
, NOLOGRIP
, NOLOGINSTRUCTION
.
It is also possible to blacklist triggers: NORIPTRIGGER
, NOINSTRUCTIONTRIGGER
.
For the skip
command it is possible to restrict the range of skippable bytes. The default is [-15,15], but it can be configured with MINSKIP=<skip>
and MAXSKIP=<skip>
, e.g. FAULT_CONFIG("MINSKIP=0"); FAULT_CONFIG("MAXSKIP=2");
to only allow skipping 1 or 2 bytes.
It is possible to define a probability with with a fault will not be induced. FAILEVERY=<faults>
defines that on average 1 out of faults fails. For example, with FAULT_CONFIG("FAILEVERY=2");
, 50% of all faults are not induced. By default, all faults are successful.
Additionally, COOLDOWN=<instruction>
defines that after inducing a fault, it takes instructions until another fault can be induced. By default, there is no cooldown.
A binary can define a timeout in seconds after which it is terminated: TIMEOUT=<seconds>
. If the binary runs into the timeout, the exploit is considered to be unsuccessful. The default timeout is 30 seconds.
To ensure reproducibility, the seed for the random function (used e.g. in havoc
) can be fixed with SEED=<seed>
. The default seed is time(NULL)
.
To disable ASLR for a binary, NOASLR
can be used.
To ensure that the instruction counter is deterministic, FAULT_ENTRY(function)
allows providing a function at which the simulator start.
Typically, this will be FAULT_ENTRY(main)
to start at main
.