Recipes

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:

  • What assets are you trying to protect?
  • What is the value of those assets?
  • For how long do you need to protect those assets?
  • What is your performance budget?
  • How does obfuscation fit into your overall security architecture?
  • What are you adversary's goals and motivation?
  • What are your adversary's tools and skill sets?

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.


Recipe #1: Opaque Predicates, Branch Functions, and Encoded Arithmetic

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:

  • Most Tigress scripts require you to start off with a sequence of 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.
  • It is convenient, but not necessary, to create a special 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.
  • If we instead set --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.
  • The generated code starts off with a lot of seemingly random declarations. This is because Tigress is a whole program obfuscator: it starts by including all the .h files (including all system files) into one .c file that then becomes the input to the obfuscation process. If your program consists of multiple C-files you first need to merge them together using tigress-merge.

Recipe #2: Virtualization and Self-Modification

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:

  • The --Skip-false option is handy to turn transformations on-and-off to try out the impact of different transformations.
  • For more diversity, try different values for --VirtualizeDispatch.
  • To compile this program you need to use gcc -segprot __TEXT rwx rwx output2.c -o output2.exe (on Darwin). See the SelfModify page for more information.

Recipe #3: Virtualization and Dynamic Obfuscation

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:

  • We use the 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!
  • Setting --JitDynamicBlockFraction=1 (which means only one basic block will be encoded) reduces the time to 1.800674 seconds.

Recipe #4: Merge, Virtualization, and Encode Literals

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:

  • There is no need to flatten the code in the merging process since we are going to virtualize the resulting code anyway. So we set --MergeFlatten=false to save some on performance.
  • Have a look at main: it now consists of 3 calls to MERGED rather than calls to the individual functions.
  • Merging adds an extra argument so that we can know which function we're calling. This is a dead giveaway so we use the EncodeLiterals transformation to add some opaque expressions to the calls to MERGED in main
  • Another possibility is add extra bogus arguments, using --Transform=RndArgs --Functions=MERGED --RndArgsBogusNo=10, which results in this code.

Recipe #5: Packing (Encrypting Functions at Rest)

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:

  • Packing hides a function's code until the moment it runs, which defeats a purely static reading of the binary. It composes with the other transformations — you can virtualize or flatten a function and pack it.
  • --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.
  • Add --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.
  • On Darwin the packed body is copied before it runs, so it must be relocatable; a function that makes a variadic call such as 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.