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/bin/victim1.fault demos/bin/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) with
@RIP - the n-th executed instruction with
#n - function + offset
&main+0x4(requiresSYMMAP_SUPPORT) - source code line
&victim1.c:5(requiresSYMMAP_SUPPORT)
The last two options require compiling the simulator with -DSYMMAP_SUPPORT and pre-processing the binaries with the mksymmap.sh script. The script uses nm and objdump
to produce a .symmap file that contains information about symbol addresses and line numbers that is then parsed by the simulator.
@<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
&<function>+<offset>
Triggers a fault when the RIP reaches the function and offset.
Skip two bytes when the instruction pointer reaches 4 bytes after main: skip 2 &main+2
&<file.c>:<line>
Triggers a fault when the RIP reaches the given source code line. The first instruction on that line is used.
Skip two bytes when the instruction pointer reaches the fifth line of victim.c: skip 2 &victim.c:5
The following commands are supported:
skip <bytes> <trigger>
Skips the given number of bytes <bytes> in the instruction stream when the given trigger <trigger> is reached. This can also be negative to jump backwards. Note that this does not skip <bytes> instructions, as many instructions are larger than one byte.
Skip 1 byte when the instruction pointer is at 0x400b51: skip 1 @0x400b51
zero <address> [<bytes>] <trigger>
Sets the given memory address or register <address> to 0 when the given trigger <trigger> is reached. Without an explicit width, memory destinations zero one byte and registers zero their full native width. If <bytes> is provided, the command zeros that many low-order bytes. The address can also be given relative to a symbol: &symbol+offset, and registers can be written by name (for example rax, rbx, rip, or rflags).
Write a zero word to address 0x6bb330 if the instruction pointer reaches 0x400b6e: zero 0x6bb330 @0x400b6e
havoc <address> [<bytes>] <trigger>
Sets the given memory address or register <address> to random values when the given trigger <trigger> is reached. Without an explicit width, memory destinations randomize one byte and registers randomize their full native width. If <bytes> is provided, the command randomizes that many low-order bytes. The address can also be given relative to a symbol: &symbol+offset, and registers can be written by name.
Write a random word to address 0x6bb330 if the instruction pointer reaches 0x400b6e: havoc 0x6bb330 @0x400b6e
bitflip <bit index> <destination> <trigger>
Flips the <bit index>-th bit at the memory address or register <destination> when the given trigger <trigger> is reached.
Flips bit 0 at the memory location 0x400b59 if the instruction pointer reaches 0x400b4d: bitflip 0 0x400b59 @0x400b4d. The address can also be given relative to a symbol: &symbol+offset.
set <address> <hexstring> <trigger>
Sets the memory at <address> or the low bytes of a register to the bytes encoded in <hexstring> when the given trigger <trigger> is reached. Writes up to 8 bytes in address order (first hex pair to <address>, next to <address+1>, etc.). The address can also be given relative to a symbol: &symbol+offset, and registers can be written by name.
Write bytes 0xde 0xad 0xbe 0xef at address 0x6bb330 if the instruction pointer reaches 0x400b6e: set 0x6bb330 deadbeef @0x400b6e
log <log type> [<trigger>]
Enable log output for certain events. Supported events are inscnt (prints the current instruction counter for every instruction), fault (prints the details of every successfully induced fault), and any register name such as rax or rip.
Register logs can be given a trigger just like faults, for example log rax #100 or log rflags @0x400b51. When a trigger is supplied, the register value is printed only when the trigger fires. If no trigger is supplied, the register value is printed at every instruction.
Register targets and logs currently use the x86-64 register names rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp, r8 through r15, rip, and rflags. These names are case-insensitive.
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_CONFIG_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, NOSET, NOLOG. The features HAVOC, ZERO, BITFLIP, and SET can also be blocked only for the code using NOCODEFAULT.
For the havoc, zero, bitflip, and set commands it is possible to blacklist certain destinations. The following destinations can be blacklisted: NOREGFAULT. It is also possible to blacklist memory addresses with the option: NOMEMORYFAULT.
For the log command it is also possible to blacklist only certain log types: NOLOGFAULT, NOLOGREGISTER, 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.
One can also define a maximum number of faults that can be induced with MAXFAULTS=<faults>. By default, there is no limit.
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. You can also disable the timeout by setting it to zero.
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.
The simulator prints "Successfully exploited " if the binary returns 0, and "Failed to exploit " if it returns non-zero. This can be suppressed with NOSUCCESS.