Below are some simple recipes that will make it easy for you to get started using Tigress. See them as a starting point for you to explore all of Tigress' features, not as recommendations for particular situations. Before using any kind of software protection technique you must start with a detailed security analysis:
Only when you have answered these questions, and have determined that obfuscation needs to be an integral part of your architecture, can you start putting together Tigress scripts. Over time we will add more recipes here. If you have found a particular combination of Tigress transformations useful, please let us know, and we will add your recipe to the collection!
In the examples below we will be using these programs as input: test.c and test-jit.c.
Here is a simple recipe that should have a relatively low performance impact:
tigress --Seed=42 --Statistics=0 --Verbosity=0 \
--Transform=InitEntropy \
--Functions=init_tigress \
--InitEntropyKinds=vars \
--Transform=InitOpaque \
--Functions=init_tigress \
--InitOpaqueStructs=list,array,env \
--Transform=InitBranchFuns \
--InitBranchFunsCount=1 \
--Transform=AddOpaque \
--Functions=fac,fib \
--AddOpaqueStructs=list \
--AddOpaqueKinds=true \
--Transform=AntiBranchAnalysis \
--Functions=fac,fib \
--AntiBranchAnalysisKinds=branchFuns \
--AntiBranchAnalysisObfuscateBranchFunCall=false \
--AntiBranchAnalysisBranchFunFlatten=true \
--Transform=EncodeArithmetic \
--Functions=fac,fib \
test.c --out=output1.c
Here is the resulting code. There are a few things to note here:
Init-transformations.
In this case we have InitEntropy, InitOpaque, and InitBranchFuns.
This is admittedly annoying, but necessary, so that Tigress can create the types, variables, and functions
necessary to transform the program. For example, InitBranchFuns creates one or more
branch functions that the AntiBranchAnalysis transformation can use later on.
init_tigress function into
which the Init-transformations can put initialization code. This function can then
also be obfuscated to hide the initializations!
--AntiBranchAnalysisBranchFunFlatten=true will flatten the code prior to inserting
calls to branch functions. This is because branch functions operate on direct jumps (gotos), and
most codes don't have many of those. After the flattening transformation, however, there are
many jumps to encode.
--Seed=0 (which means the internal random number generator
will be seeded with a new value every time Tigress is run), every time you run this script you
will get a differently obfuscated program.
tigress-merge.
This transformation shows off a combination of two more powerful transformation, virtualization and self-modifying code, that together is likely to have serious performance impact:
tigress --Seed=42 --Statistics=0 --Verbosity=0 --Environment=x86_64:Darwin:Clang:9.0 \
--Transform=InitEntropy \
--Functions=init_tigress \
--InitEntropyKinds=vars \
--Transform=InitOpaque \
--Functions=init_tigress \
--InitOpaqueStructs=list,array,env \
--Transform=Virtualize \
--Skip=false \
--VirtualizeDispatch=ifnest \
--Functions=fac,fib \
--Transform=SelfModify \
--Skip=false \
--Functions=fac,fib \
--SelfModifySubExpressions=false \
--SelfModifyBogusInstructions=10 \
test.c --out=output2.c
Here is the resulting code. Here are a few things to note:
--Skip-false option is handy to turn transformations on-and-off to try out
the impact of different transformations.
--VirtualizeDispatch.
gcc -segprot __TEXT rwx rwx output2.c -o output2.exe
(on Darwin). See the SelfModify page for more information.
In this recipe we first virtualize and then add dynamic obfusction; i.e. the program will decode and re-encode itself as it is running. In this particular case we decode/encode using the XTEA cipher:
tigress --Seed=42 --Statistics=0 --Verbosity=0 \
--Transform=InitEntropy \
--Functions=init_tigress \
--InitEntropyKinds=vars \
--Transform=InitOpaque \
--Functions=init_tigress \
--InitOpaqueStructs=list,array,env \
--Transform=Virtualize \
--Skip=false \
--VirtualizeDispatch=direct \
--Functions=fib \
--Transform=JitDynamic \
--Skip=false \
--Functions=fib \
--JitDynamicCodecs=xtea \
--JitDynamicBlockFraction=%100 \
--Transform=Measure \
--Functions=fib \
--MeasureTimes=100 \
test-jit.c --out=output3.c
Here is the resulting code. Here are a few things to note:
Measure transformation along with differen values of Skip to
measure the performance impact. On my machine virtualization alone takes 0.000192 seconds,
dynamic obfuscation alone takes 0.077977 seconds, and together, the program slows down to
2.125580 seconds!
--JitDynamicBlockFraction=1 (which means only one basic block will be
encoded) reduces the time to 1.800674 seconds.
In this recipe we first merge all functions together. This includes init_tigress which has important
secrets in it, in particular the initalization of opaque structures:
tigress --Seed=42 --Statistics=0 --Verbosity=1 --Environment=x86_64:Darwin:Clang:9.0 \
--Transform=InitEntropy \
--Functions=init_tigress \
--InitEntropyKinds=vars \
--Transform=InitOpaque \
--Functions=init_tigress \
--InitOpaqueStructs=list,array,env \
--Transform=Merge \
--MergeFlatten=false \
--MergeName=MERGED \
--Functions=fib,fac,init_tigress \
--Transform=Virtualize \
--VirtualizeDispatch=direct \
--Functions=MERGED \
--Transform=EncodeLiterals \
--Functions=main \
test.c --out=output4.c
Here is the resulting code. Here are a few things to note:
--MergeFlatten=false to save some on performance.
main: it now consists of 3 calls to MERGED rather than calls to
the individual functions.
EncodeLiterals transformation to add some opaque expressions
to the calls to MERGED in main
--Transform=RndArgs --Functions=MERGED --RndArgsBogusNo=10,
which results in this code.
In this recipe we pack a function: its compiled machine code is stored encrypted in the
binary and decrypted, at run time, only when it is needed. We use a small input,
test-pack.c, whose work function is the one we protect.
Unlike the other recipes, packing is a two-stage process. First Tigress rewrites the
source, inserting a run-time decryptor and writing a spec file (here pack.json) that
describes each region to encrypt:
tigress --Seed=42 --Statistics=0 --Verbosity=0 \
--Transform=Pack \
--Functions=work \
--PackCiphers=xtea,des \
--PackLevels=2 \
--PackKeySource=folded \
--PackSpecFile=pack.json \
test-pack.c --out=output5.c
Then you compile the result and run the tigress_post post-processor, which encrypts the
compiled bytes of each packed function in place and patches the binary. On Linux the code is
decrypted in place, so the code segment must be made writable; on Darwin the body is copied into
JIT memory and decrypted there, so the binary only needs re-signing:
gcc -no-pie -o output5.exe output5.c
tigress_post --Action=pack --PackSpecFile=pack.json output5.exe
tigress_post --Action=writable output5.exe
./output5.exe
gcc -o output5.exe output5.c
tigress_post --Action=pack --PackSpecFile=pack.json output5.exe
codesign -s - output5.exe
./output5.exe
Both print work(10)=135 — the same as the unobfuscated program — but in the
packed binary the bytes of work are ciphertext until it first runs. Here are a few things
to note:
--PackCiphers chooses the cipher(s) and --PackLevels the number of nested
encryption layers; with more than one cipher the layers cycle through them (here layer 1 is
xtea and layer 2 is des), so the region is des(xtea(code)) at rest.
--PackKeySource=folded whiteboxes the decryptor: the key is folded into the cipher's code, so
no key array is left in the binary. It applies to xtea and des; the
feistel cipher is always baked.
--PackWhen=call --PackRepack=true (Linux) to decrypt a function on entry and
re-encrypt it on return, so it is plaintext only while it is actually running.
printf is not yet move-safe there. That is why the
protected work here is a computation-only leaf and the I/O stays in main.