This transformation inserts checkers into the program
in order to ensure the integrity of the codebytes.
A checker computes a hash over a code region and, if the hash is
different than expected, issues a response, such as
crashing the program. A hash function can be simple (such as
computing the xor over the code region) or a highly complex
randomly generated function. Responders can be simple
(such as exiting the program with an abort()
or
modifying a global variable) or can be user-specified
plugins of arbitrary complexity. Checkers can, of course, be
obfuscated with Tigress' other transformations (such as
flattening or virtualization), making them harder to
detect.
Here is a LigerLabs video that gives a good introduction to the checksum transformation:
And here's another LigerLabs video that describes attacks against checksums:
Checkers can be invoked over a function prior to it being called,
ensuring that the function hasn't been tampered with before it is
executed. This is illustrated in this figure, where foo
is checked by the pink checker, prior to being called.
The e1
value is the expected hash value computed
over foo
.
Checkers can also cover arbitrary levels of overlapping,
randomly chosen, code regions. This is the case for the
orange and blue checkers. Here, f2
and t2
represent the range of addresses
covered by the orange checker.
Unlike other Tigress transformations, the Checksum
transform requires a post-processing step. This is performed
by the tigress_post
program:
The Checksum
transformation generates two
files: the Tigress-obfuscated output file (here P1.c
) into
which checkers have been inserted and a file (here hashes.c
)
that contains information about the generated hash functions.
This file needs to be compiled into a dynamically linked .so
file.
The tigress_post
post-processor
reads the compiled obfuscated file (P1.exe
) and
hashes.so
and generates a patched
executable file (here P2.exe
).
Here's an example of how to call Tigress and tigress_post
:
# Run Tigress, generating 2 files: hashes.c and obf.c.
> tigress --Seed=0 --Statistics=0 --Verbosity=0 --Environment=x86_64:Darwin:Clang:5.1 \
--Verbosity=0 \
--Transform=InitEntropy --Functions=main --InitEntropyKinds=vars \
--Transform=InitOpaque --Functions=main --InitOpaqueStructs=list,array,env \
--Transform=Checksum \
--ChecksumAddressing=relative \
--ChecksumInsertSegmentCheckersInTheseFunctions=main \
--ChecksumDoNotInsertSegmentCheckersInTheseFunctions=fib \
--ChecksumSegmentCheckersToBeInsertedWhere=randomlyNoLoops \
--ChecksumRequiredNumberOfSegmentCheckers=10 \
--ChecksumFunctionsToBeCheckedAtCallSites=fac,fib \
--ChecksumCallCheckersToBeInsertedWhere='((fac beforeCall) (fib beforeCall))' \
--ChecksumFunctionPriority="((fac 1.0) (fib 0.5))" \
--ChecksumHashFunctionsFile=hashes.c \
--ChecksumHashValueTypes=int64 \
--ChecksumHashFunctionKinds=add \
test1.c --out=obf.c
# Compile the obfuscated program, making sure to use position-independent
# code. Link statically, if available:
> gcc -static -fPIC -fno-plt -fcf-protection=none -o obf.exe obf.c # for Linux
> gcc -fPIC -fno-plt -o obf.exe obf.c # for Darwin
# Compile hashes.c into a shared object file hashes.so which will be
# read by tigress_post:
> gcc -shared -o hashes.so -fPIC hashes.c
# Patch the executable obf.exe. Note that tigress_post will modify
# obf.exe (i.e. the input file and output file are the same):
> tigress_post \
--Action=checksum \
--HashFunctionsFile=hashes.so \
obf.exe
A randomly generated checker looks like this:
/*******************************************************************************/
/* Inline template to be patched by tigress_post. */
if (opaque-false) {
__asm__ volatile (".align 8,0xf5;\n":);
L4: __asm__ volatile (".long 0xAAAAA001":); /* Start address */
L5: __asm__ volatile (".long 0xBBBBB001":); /* End address */
L6: __asm__ volatile (".quad 0xCCCCCCCCCCCCC001":); /* Expected hash value */
}
/*******************************************************************************/
/* Load values from the patched template */
codePtrVar_offset = (int )*((unsigned long *)(&&L4));
codePtrVar_templateAddress = (unsigned long *)(&&L4);
codePtrVar = (unsigned long *)((char *)codePtrVar_templateAddress + codePtrVar_offset);
endPtrVar_offset = (int )*((unsigned long *)(&&L5));
endPtrVar_templateAddress = (unsigned long *)(&&L5);
endPtrVar = (unsigned long *)((char *)endPtrVar_templateAddress + endPtrVar_offset);
/*******************************************************************************/
/* Compute the hash value. */
hashVar = 0;
while (codePtrVar < endPtrVar) {
hashVar = ((hashVar + *codePtrVar) + (760749724UL ^ (1 ^ hashVar))) &
(*codePtrVar * 5027005UL + 4949079U);
codePtrVar ++;
}
/*******************************************************************************/
/* Check if the hash value is correct, if not, respond. */
expectedHashVar = *((unsigned long *)(&&L6));
if (hashVar != expectedHashVar) {
abort();
}
/*******************************************************************************/
The word at L4
will (after patching by tigress_post
) hold
the address of the region that the hash function will check, L5
will hold the address of the end of that region, and L6
will hold
the expected hash value.
Option | Arguments | Description |
---|---|---|
--Transform | Checksum | Insert checkers that compute hashes over the code bytes and take action if the hashes return the wrong value |
--ChecksumTemplateKinds | local, function, string, data | Where to insert checker templates. Default=local.
|
--ChecksumHashFunctionKinds | add, mul, xor, linear, quadratic, random | Kinds of expressions to use in the hash function. Default=xor.
|
--ChecksumHashValueTypes | int32, int64 | Comma-separated list of the kinds of expressions to use in the hash functions. Default=int32,int64.
|
--ChecksumRandomHashFunctionSize | INTSPEC | Size of the randomly generated hash function. Default=1. |
--ChecksumObfuscateBody | BOOLSPEC | Obfuscate the body of the hash function. Default=false. |
The purpose of tigress_post --Action=checksum
is to patch the checker
``templates'' that were inserted by Tigress:
There are three main steps: first the executable is parsed and the
templates are located, then each checker is assigned to a region of the text
segment, and finally the hash value is computed and the templates are patched.
An option tigress_post ... --OverlapRate=int ...
can be set to adjust the number checkers will
check each region of the text segment.
tigress_post
has the following options:
--Action=checksum
Patch the checkers after the 'tigress --Transform=Checksum' transformation.
--Action=parse
Parse and print the information collected from the executable input file.
--HashFunctionsFile=file.so
The .so-file for the generated hash functions.
The file 'hashes'c' is generated by Tigress and
should be compiled using the command:
'gcc -shared -o hashes.so -fPIC hashes.c'
--OverlapRate=int
Amount of overlap in hash function assignment.
--ShowResults
Show a table of final results, i.e. which hashfunctions check which regions.
--Dump=templates
Print the checker templates.
--Dump=regions
Print the regions that the executable was broken up into.
--Dump=textSegment
Print the text segment from the executable.
--Dump=sections
Print the sections of the executable.
--Dump=symbols
Print the symbol table extracted from the executable.
--Dump=codeBytes
Print the code bytes for each function in the executable.
--Trace|-e
Trace the execution of tigress_post.
--Help
To check that the checkers inserted by Tigress actually trigger when
the executable is modified, you can use the tigress_post --Action=kill
command. It will randomly modify one or more bits in the executable which
should cause the responses to trigger.
These are the relevant tigress_post
options:
--Action=kill
Destroy a function to test that the checkers trigger a response.
--KillFunctions=f1,f2,...
The functions to be killed by changing random bits.
--KillSize=1|8|16|32
Number of consecutive bits to be changed when killing a function.
--KillCount=
Number of times to kill a function.
Tigress has a few simple built-in responders. You can easily add your own by adding a plugin function to your input program:
void responder_1 () {
for(int i=0; i<10; i++) {
printf("Hacked %i!\n", i);
}
exit(-1);
}
The body of these functions will be inserted in the checkers. These are the relevant options:
tigress ... \
--Transform=InitPlugins \
--InitPluginsResponderPrefix=responder \
--Transform=Checksum \
--ChecksumResponseKinds=plugin \
...
You can read more about plugins here.
Option | Arguments | Description |
---|---|---|
--ChecksumResponseKinds | abort, random, global, plugin | Kinds of response to use in in case a checksum computes the wrong hash value Default=abort.
|
Tigress has a few options that control where checkers are inserted, and which functions are being checked.
Unlike all other Tigress transformations, --Transform=Checksum
does not make use of the --Functions=...
option. Instead, for call checkers
you should use --ChecksumFunctionsToBeCheckedAtCallSitess=...
to indicate which functions
should be checked before they are called, and --ChecksumCallCheckersToBeInsertedWhere=..
to
specify a more exact location (such as after the call, randomly, etc.):
> tigress ... \
--ChecksumFunctionsToBeCheckedAtCallSites=f1,f2 \
--ChecksumCallCheckersToBeInsertedWhere='((f1 beforeCall) (f2 beforeCall))' \
...
The option --ChecksumCallCheckersToBeInsertedWhere=...
describes where in the function the
checkers can be inserted. Here, function f6
, for example, will be checked by a function that calls it,
and the checker will be inserted anywhere in the caller, except within loops (this helps with performance):
tigress ...
--ChecksumCallCheckersToBeInsertedWhere='((f1 beforeCall) (f2 afterCall) (f3 randomly) (f4 first) (f5 topLevel) (f6 randomlyNoLoops))'
The option --ChecksumFunctionPriority=...
can give a priority value for each function, where 0.0 means "never check this function",
and 1.0 means "always check this function." This allows you to control how many of the available checkers are assigned to check
a particular function:
tigress ...
--ChecksumFunctionPriority='((fac 1.0) (fib 0.7) (zap 0.2))' \
Option | Arguments | Description |
---|---|---|
--ChecksumFunctionPriority | S-Expression | S-Expression describing which functions are most important to checksum. The syntax is ((function priority) (function priority) ...) where priority is a floating point number in the range [0.0,1.0]. (foo 0.0) means that foo should not be checked. Default=NONE. |
--ChecksumFunctionsToBeCheckedAtCallSites | String,String,... | Comma-separated list of functions that should be checked where they are called. Use --ChecksumCallCheckersToBeInsertedWhere to specify where withing the caller the checker should be inserted. Default=NONE. |
--ChecksumCallCheckersToBeInsertedWhere | randomly, randomlyNoLoops, topLevel, first, beforeCall, afterCall, annotations | S-Expression describing which functions should be checksummed before/after they are called. The syntax is ((function where) (function where) ...) where where is one of "randomly", "first", "beforeCall", "afterCall", "randomlyNoLoops", "topLevel", "annotations". If function bar calls function foo and (foo,randomly) is specified, a checker of foo will be inserted at some random place in bar. If (foo,beforeCall) is specified, the checker will be inserted immediately before the call to foo. If (foo,afterCall) is specified, the checker will be inserted immediately after the call to foo. If (foo,first) is specified, the checker will be inserted at the very beginning of bar. If (foo,randomlyNoLoops) is specified, the checker will be inserted anywhere within bar, but loops will be avoided. If (foo,annotations) is specified, the checker will be inserted where the annotation CHECKSUM_INSERT is specified within bar. Default=first.
|
For segment checkers, --ChecksumRequiredNumberOfSegmentCheckers=...
specifies how many checkers to insert, and
--ChecksumInsertCheckersWhere=...
describe where in the function to insert them. Here, checkers will be inserted anywhere
in the function, except within control structures, or anywhere where the user has added a CHECKSUM_INSERT
annotation:
tigress ...
--ChecksumInsertSegmentCheckersInTheseFunctions='/foo*/' \
--ChecksumProtectSegments=text \
--ChecksumRequiredNumberOfSegmentCheckers=10 \
--ChecksumSegmentCheckersToBeInsertedWhere=topLevel,annotations \
Here's an example of a function with annotations (don't forget to include tigress.h
!):
#include "tigress.h"
void foo(void) {
int x = 10;
int y = 2;
if (x < 5) {
y++;
CHECKSUM_INSERT;
} else {
x++;
CHECKSUM_INSERT;
}
y++;
}
Option | Arguments | Description |
---|---|---|
--ChecksumRequiredNumberOfSegmentCheckers | INTSPEC | Number of segment checkers to insert. Default=0. |
--ChecksumProtectSegments | text, cstring, const, data | Comma-separated list of which segments to protect. Default=text.
|
--ChecksumSegmentCheckersToBeInsertedWhere | randomly, randomlyNoLoops, topLevel, first, annotations | Insert checkers randomly, at top-level, first, or where 'CHECKER'-annotations have been given in the code. Default=first.
|
--ChecksumInsertSegmentCheckersInTheseFunctions | IDENTSPEC | Names of functions into which checkers may be inserted. Default=NONE. |
--ChecksumDoNotInsertSegmentCheckersInTheseFunctions | String,String,... | Comma-separated list of functions in which not to insert checkers. Default=NONE. |
Calling tigress_post --ShowResults
will generate a table like this:
+========+===========================================+===============+=====================================+========+========+========+========+========+========+
| region | section, priority, address range, size | function | template, checker, size | A | B | C | D | E | F |
+========+===========================================+===============+=====================================+========+========+========+========+========+========+
| 0 | text,0.5,[0x100002d40, 0x100002d4f],16 | _megaInit | | | | | | | |
+--------+-------------------------------------------+---------------+-------------------------------------+--------+--------+--------+--------+--------+--------+
| 1 | text,1.0,[0x100002d50, 0x100002d8f],64 | _loop_switch | | hash_8 | | | | | |
+--------+-------------------------------------------+---------------+-------------------------------------+--------+--------+--------+--------+--------+--------+
| 2 | text,0.5,[0x100002d90, 0x100002e8f],256 | ___sputc | | | | | | | |
+--------+-------------------------------------------+---------------+-------------------------------------+--------+--------+--------+--------+--------+--------+
| 3 | text,0.5,[0x100002e90, 0x100003007],376 | _main | | | | | | | |
| 4 | text,1.0,[0x100003008, 0x10000300b],4 | | start=100002d50,hash_8,64 | | | | | | |
| 5 | text,1.0,[0x10000300c, 0x10000300f],4 | | end=100002d8f,hash_8,64 | | | | | | |
| 6 | text,1.0,[0x100003010, 0x100003013],4 | | expected=5a11fe33,hash_8,64 | | | | | | |
| 7 | text,0.5,[0x100003014, 0x10000311f],268 | | | | | | | | |
| 8 | text,1.0,[0x100003120, 0x100003123],4 | | start=100003dc0,hash_0,96 | | | | | | |
| 9 | text,1.0,[0x100003124, 0x100003127],4 | | end=100003e1f,hash_0,96 | | | | | | |
| 10 | text,1.0,[0x100003128, 0x10000312b],4 | | expected=37f9a377,hash_0,96 | | | | | | |
| 11 | text,0.5,[0x10000312c, 0x1000031ff],212 | | | | | | | | |
| 12 | text,1.0,[0x100003200, 0x100003203],4 | | start=100003dc0,hash_1,96 | | | | | | |
| 13 | text,1.0,[0x100003204, 0x100003207],4 | | end=100003e1f,hash_1,96 | | | | | | |
| 14 | text,1.0,[0x100003208, 0x10000320b],4 | | expected=37f9a377,hash_1,96 | | | | | | |
| 15 | text,0.5,[0x10000320c, 0x1000033d7],460 | | | | | | | | |
| 16 | text,1.0,[0x1000033d8, 0x1000033db],4 | | start=100003dc0,hash_2,96 | | | | | | |
| 17 | text,1.0,[0x1000033dc, 0x1000033df],4 | | end=100003e1f,hash_2,96 | | | | | | |
| 18 | text,1.0,[0x1000033e0, 0x1000033e3],4 | | expected=37f9a377,hash_2,96 | | | | | | |
| 19 | text,0.5,[0x1000033e4, 0x1000034f7],276 | | | | | | | | |
| 20 | text,1.0,[0x1000034f8, 0x1000034fb],4 | | start=100003dc0,hash_3,96 | | | | | | |
| 21 | text,1.0,[0x1000034fc, 0x1000034ff],4 | | end=100003e1f,hash_3,96 | | | | | | |
| 22 | text,1.0,[0x100003500, 0x100003503],4 | | expected=37f9a377,hash_3,96 | | | | | | |
| 23 | text,0.5,[0x100003504, 0x100003617],276 | | | | | | | | |
| 24 | text,1.0,[0x100003618, 0x10000361b],4 | | start=100003dc0,hash_4,96 | | | | | | |
| 25 | text,1.0,[0x10000361c, 0x10000361f],4 | | end=100003e1f,hash_4,96 | | | | | | |
| 26 | text,1.0,[0x100003620, 0x100003627],8 | | expected=a8b3c8039f4a6b74,hash_4,96 | | | | | | |
| 27 | text,0.5,[0x100003628, 0x100003987],864 | | | | | | | | |
| 28 | text,1.0,[0x100003988, 0x10000398b],4 | | start=100003dc0,hash_5,96 | | | | | | |
| 29 | text,1.0,[0x10000398c, 0x10000398f],4 | | end=100003e1f,hash_5,96 | | | | | | |
| 30 | text,1.0,[0x100003990, 0x100003993],4 | | expected=37f9a377,hash_5,96 | | | | | | |
| 31 | text,0.5,[0x100003994, 0x100003aa7],276 | | | | | | | | |
| 32 | text,1.0,[0x100003aa8, 0x100003aab],4 | | start=100003e20,hash_6,128 | | | | | | |
| 33 | text,1.0,[0x100003aac, 0x100003aaf],4 | | end=100003e9f,hash_6,128 | | | | | | |
| 34 | text,1.0,[0x100003ab0, 0x100003ab3],4 | | expected=2a71a278,hash_6,128 | | | | | | |
| 35 | text,0.5,[0x100003ab4, 0x100003c1f],364 | | | | | | | | |
| 36 | text,1.0,[0x100003c20, 0x100003c23],4 | | start=100003ea0,hash_7,96 | | | | | | |
| 37 | text,1.0,[0x100003c24, 0x100003c27],4 | | end=100003eff,hash_7,96 | | | | | | |
| 38 | text,1.0,[0x100003c28, 0x100003c2f],8 | | expected=14d8ac0977e8efd8,hash_7,96 | | | | | | |
| 39 | text,0.5,[0x100003c30, 0x100003d3f],272 | | | | | | | | |
+--------+-------------------------------------------+---------------+-------------------------------------+--------+--------+--------+--------+--------+--------+
| 40 | text,0.5,[0x100003d40, 0x100003d9f],96 | _switch_sw... | | | | | | | |
+--------+-------------------------------------------+---------------+-------------------------------------+--------+--------+--------+--------+--------+--------+
| 41 | text,0.5,[0x100003da0, 0x100003dbf],32 | _tigress_exit | | | | | | | |
+--------+-------------------------------------------+---------------+-------------------------------------+--------+--------+--------+--------+--------+--------+
| 42 | text,1.0,[0x100003dc0, 0x100003e1f],96 | _fac | | hash_0 | hash_1 | hash_2 | hash_3 | hash_4 | hash_5 |
+--------+-------------------------------------------+---------------+-------------------------------------+--------+--------+--------+--------+--------+--------+
| 43 | text,1.0,[0x100003e20, 0x100003e9f],128 | _fib | | hash_6 | | | | | |
+--------+-------------------------------------------+---------------+-------------------------------------+--------+--------+--------+--------+--------+--------+
| 44 | text,1.0,[0x100003ea0, 0x100003eff],96 | _sum | | hash_7 | | | | | |
+--------+-------------------------------------------+---------------+-------------------------------------+--------+--------+--------+--------+--------+--------+
| 45 | text,0.5,[0x100003f00, 0x100003f4f],80 | _tigress_r... | | | | | | | |
+--------+-------------------------------------------+---------------+-------------------------------------+--------+--------+--------+--------+--------+--------+
| 46 | text,0.5,[0x100003f50, 0x100003f55],6 | _dbg | | | | | | | |
+--------+-------------------------------------------+---------------+-------------------------------------+--------+--------+--------+--------+--------+--------+
| 47 | cstring,1.0,[0x100003f56, 0x100003fb7],98 | | | | | | | | |
+--------+-------------------------------------------+---------------+-------------------------------------+--------+--------+--------+--------+--------+--------+
The first three columns show that the .text
-segment has been broken up into
47 regions. The main
function, for example, has been broken up into
37 regions. The fourth column shows that there are 9 checkers, named hash_0
through
hash_8
. You can learn more about these checkers by inspecting the
file generated by Tigress (the --ChecksumHashFunctionsFile=hashes.c
options provide the names of
the files). The size
field shows how many bytes each hash function is
checking. The function hash_7
, for example, computes a hash over
the sum
function. Columns A-F, finally, show which regions each hash
function is covering. The function hash_7
, again, computes a hash
over region 44.
Calling tigress_post --ShowResults
will also generate tables that provide some statistical information:
Number of bytes checked by each hash function
=============================================
Hash function 'hash_0' checks 96 bytes
Hash function 'hash_1' checks 96 bytes
Hash function 'hash_2' checks 96 bytes
Hash function 'hash_3' checks 96 bytes
Hash function 'hash_4' checks 96 bytes
Hash function 'hash_5' checks 96 bytes
Hash function 'hash_6' checks 128 bytes
Hash function 'hash_7' checks 96 bytes
Hash function 'hash_8' checks 64 bytes
On average, a hash function checks 96.0 bytes
Number of hash function checking each byte
==========================================
4344 bytes are checked by 0 checkers
288 bytes are checked by 1 checkers
96 bytes are checked by 6 checkers
The average byte is checked by 0.2 hash functions
Note that the amount of overlap needs to be controlled with options to
both Tigress and tigress_post
: for more overlap you need to
introduce more checkers (using
tigress --ChecksumRequiredNumberOfSegmentCheckers=...
)
and increase the overlap rate in tigress_post
(tigress_post --OverlapRate=...
).
Option | Arguments | Description |
---|---|---|
--Transform | Checksum | Insert checkers that compute hashes over the code bytes and take action if the hashes return the wrong value |
--ChecksumTraceKinds | start, stop, step, check | List of the information that should be printed at runtime when a checker is executed. Default=NONE.
|
--ChecksumHashFunctionsFile | String | The name of the generated file containing hash functions. It should be compiled into a .so file and becomes input into tigress_post. Default=hashFunctions.c. |
--ChecksumAddressing | relative, absolute | How to load the address of the code region to check. Default=relative.
|
endbr64
after branches. In what appears to be a gcc
bug, in the code below the compiler inserts the endbr64
instruction after the label, when, presumably, it should go between the test and the label. As a result, we can't correctly calculate the address of the asm
instruction:
if (_2_main_1_opaque_ptr_1 == _2_main_1_opaque_ptr_2) {
Lab_2000002:
__asm__ volatile (".balign 8,0xf9;\n"
".long 0xAAAAA001;\n"
".long 0xBBBBB001;\n"
".quad 0xCCCCCCCCCCCCC001;\n":);
}
-fcf-protection=none
to your gcc
command line.