Virtualize

 

This transformation turns a function into an interpreter, whose bytecode language is specialized for this function. The transformation has been designed to induce as much diversity as possible, i.e. every decision made is dependent on the randomization seed. The diversity is both static and dynamic, i.e. each interpreter variant differs in the structure of its code as well as in its execution pattern.

A generated interpreter consists of a virtual instruction set, specialized for the input function, a bytecode array, a virtual program counter (VPC), a virtual stack pointer (VSP), a dispatch unit, and a list of instruction handlers, one for each virtual instruction.

Here is a LigerLabs video that gives a good introduction to the virtualization obfuscation:

Here's an example of an interpreter constructed by Tigress for the program main(){int x; x++;}:

enum ops {Locals = 116, Plus  = 135, Load   = 60, Goto = 231,
          Const  = 3,   Store = 122, Return = 72};

unsigned char bytecode[41]  = {
 Locals,24,0,0,0,Const,1,0,0,0,Locals,24,0,0,0,Load,Plus,Store,Locals,
 28,0,0,0,Const,0,0,0,0,Store,Goto,4,0,0,0,Locals,28,0,0,0,Load,Return};

int main() { 
  while (1) {
    switch (*pc) {
    case Const: pc++; (sp+1)->_int = *((int *)pc); sp++; pc+=4; break;
    case Load: pc++; sp->_int = *((int *)sp->_vs); break;
    case Goto: pc++; pc+= *((int *)pc); break;
    case Plus: pc++; (sp+-1)->_int=sp->_int+(sp+-1)->_int; sp--; break;
    case Return: pc++; return (sp->_int); break;
    case Store: pc++; *((int *)(sp+-1)->_vs)=sp->_int; sp+=-2; break;
    case Locals: pc++; (sp+1)->_vs=(void *)(vars+*((int *)pc)); 
                 sp++; pc+=4; break;
}}

For this transformation, Tigress first constructs type-annotated abstract syntax tree (AST) from the C source, from which it generates control-flow graphs of instruction trees. Tigress then selects a random instruction set architecture (ISA) and, using this ISA, generates a bytecode program specialized for the input function. Finally, Tigress selects a random dispatch method and produces an output program.

 

Diversity and Stealth

Static diversity. Tigress supports two mechanisms for generating ISAs with a high degree of static diversity:

  • instruction opcodes can be randomized,
  • the ISA can have duplicate instructions with the same semantics,
  • instructions can pass arguments in arbitrary combinations of stack locations and registers,
  • instructions can be made arbitrarily long (with highly complex semantics) through the use of superoperators.

Dynamic diversity. We ensure that dynamic execution patterns are diversified by merging randomized bogus functions with the ``real'' function. We can furthermore impede dynamic analysis by making instruction traces artificially long.

Static stealth. Not only diversity but also stealth is important for interpreters. For static stealth, the split transformation can break up the interpreter loop into smaller pieces, and the AddOpaque transformation can make instruction handlers less conspicuous.

Dynamic stealth. For dynamic stealth, Tigress interpreters can be made reentrant, meaning only a few iterations of the dispatch loop are executed at a time, effectively mixing instructions executed from the interpreter with instructions executed by the rest of the program. This is of particular interest when wanting to hide the execution pattern from analysts, and when the exact time that the function executes is not important, as long as it completes eventually.

Here is a LigerLabs video that presents some of protections against attack that can be added to an obfuscated interpreter:

Usage

A typical call to Tigress to virtualize a function foo looks like this:

tigress \
   --Environment=x86_64:Linux:Gcc:4.6 \
   --Transform=Virtualize \
      --Functions=foo \
      --VirtualizeDispatch=direct \
   --out=out.c in.c

And here are the options for debugging and optimization:

OptionArgumentsDescription
--Transform Virtualize Turn a function into an interpreter.
--VirtualizeShortIdents bool Generate shorter identifiers to produce interpreters suitable for publication. Default=false.
--VirtualizePerformance IndexedStack, PointerStack, AddressSizeShort, AddressSizeInt, AddressSizeLong, CacheTop Tweak performance. A comma-separated list of the options below. MODIFIES Virtualize DEFAULT PointerStack
  • IndexedStack = Use array indexing to access stack elements.
  • PointerStack = Use pointer operations to access stack elements.
  • AddressSizeShort = Assume addresses for accessing instruction handlers fit in a short (only available with direct dispatch).
  • AddressSizeInt = Assume addresses for accessing instruction handlers fit in an int (only available with direct dispatch).
  • AddressSizeLong = Assume addresses for accessing instruction handlers fit in a long (only available with direct dispatch).
  • CacheTop = Store the top of stack in a register.
--VirtualizeOptimizeBody BOOLSPEC Clean up after superoperator generation by optimizing the body of the generated function. Default=false.
--VirtualizeOptimizeTreeCode BOOLSPEC Do constant folding etc. prior to interpreter generation. Default=false.
--VirtualizeTrace instr, args, stack, checkTags, actions, regs, locals, checkLocals, threads, * Insert tracing code to show the stack and the virtual instructions executing. Default=print nothing.
  • instr = print instruction names
  • args = print arguments to instructions
  • stack = print stack contents. Currently only works if you set --VirtualizePerformance=IndexedStack. You get more readable output if you --VirtualizeTaggedStore=true
  • checkTags = check that the tags indicate the correct type. Requires --VirtualizeTaggedStore=true to be set.
  • actions = print high level actions as they occur
  • regs = print register contents (not implemented)
  • locals = print current values of local variables
  • checkLocals = print local variables that have changed after a store. Useful to find stores that affect more than one variable, ie. write overruns.
  • threads = trace the thread execution for --VirtualDispatch=concurrent.
  • * = select all options
--VirtualizeTaggedStore BOOLSPEC Make stack and registers tagged with their type. Useful for debugging. Default=false.
--VirtualizeStackSize INTSPEC Number of elements in the evaluation stack. Default=32.
--VirtualizeComment bool Insert comments in the generated interpreter. Default=false.
--VirtualizeDump input, tree, ISA, instrs, types, vars, strings, SuperOps, calls, bytes, array, stack, * Dump internal data structures used by the virtualizer. Comma-separated list. Default=dump nothing.
  • input = dump the function that is to be virtualized
  • tree = dump the expression trees generated from the CIL representation
  • ISA = dump the Instruction Set Architecture
  • instrs = dump the generated virtual instructions
  • types = dump the types found
  • vars = dump the local variables found
  • strings = dump the strings found
  • SuperOps = dump the super operator instructions
  • calls = dump the function calls found
  • bytes = dump the bytecode array
  • array = dump the instruction array
  • stack = dump the evaluation stack
  • * = select all options
--VirtualizeConditionalKinds branch, compute, flag Ways to transform the one conditional branch that occurs in instruction handlers. Default=branch.
  • branch = Use normal branches, such as if (a>b) VPC=L1 else VPC=L2
  • compute = Compute the branch, such as x=(a>b); VPC=*(expression over x). Not yet implemented.
  • flag = Compute the branch from the values of the flag register, such as asm("cmp a b;pushf;pop"); VPC=*(expression over flag register)
 

Dispatch Method Selection

For both static and dynamic diversity, Tigress supports nine different dispatch methods. The following code is generated for the different methods, where Ξop1; is the instruction handler for operator op1:

DispatchGenerated code
switch
switch(prog[pc]) {
   op1: Ξop1; break;
   op2: Ξop2; break;
}
direct
goto *prog[pc];
op1hdl: Ξop1; goto *prog[pc];
op2hdl: Ξop2; goto *prog[pc];
indirect
goto *jtab[prog[pc]];
op1hdl: Ξop1; goto *jtab[prog[pc]];
op2hdl: Ξop2; goto *jtab[prog[pc]];
call
void op1fun(){Ξop1}
void op2fun(){Ξop2}
…
call *prog[pc]();
ifnest
if (prog[pc]==op1) Ξop1
else if (prog[pc]==op2) Ξop2
else if …
linear, binary, interpolation
alg = linear|binary|interpolation|…
top: 
   goto *(searchalg(map,prog[pc]));
op1hdl: Ξop1; goto top;
op2hdl: Ξop2; goto top;

Tigress also supports concurrent dispatch, which is a variant of switch dispatch, where every iteration around the interpreter loop is split between a number of concurrently executing threads. It's specified like this: --VirtualizeDispatch=concurrent --VirtualizeNumberOfThreads=2.

To further confuse static analysis, in an direct and indirect threaded dispatch loop you can make every indirect jump go through a branch function. It is specified like this:

--Transform=InitBranchFuns \
   --InitBranchFunsCount=1 \
    ...  \
--Transform-Virtualize \
   --VirtualizeDispatch=direct \
   --VirtualizeBranchFunctions=true
OptionArgumentsDescription
--VirtualizeDispatch switch, direct, indirect, call, ifnest, linear, binary, interpolation, concurrent, ?, * Select the interpreter's dispatch method. The argument should be a comma-separated list of disparch kinds. One of these will be picked at random. Default=switch.
  • switch = dispatch by while(){switch(next){...}}
  • direct = dispatch by direct threading
  • indirect = dispatch by indirect threading
  • call = dispatch by call threading
  • ifnest = dispatch by nested if-statements
  • linear = dispatch by searching a table using linear search
  • binary = dispatch by searching a table using binary search
  • interpolation = dispatch by searching a table using interpolation search
  • concurrent = dispatch by spawning threads
  • ? = pick a random dispatch method
  • * = select all dispatch method
--VirtualizeNumberOfThreads INTSPEC Number of threads to spawn when using concurrent dispatch. Default=2.
--VirtualizeBranchFunctions BOOLSPEC Make every dispatch jump go through a branch function. This only applies to direct and indirect threaded dispatch. Requires --Transform=InitBranchFuns --InitBranchFunsCount=1. Default=false.
 

Instruction Set Architecture Generation

Instruction sets can use stacks, registers, or both to pass values between instructions. By default, the following, very simple, instruction set is used:

  labels:         l ∈ Labels 
  functions:      f ∈ Funs 
  variables:      x ∈ Vars 
  strings:        s ∈ Strings 
  temporaries:     t ::= regint | stackint  
  binary operators: binop ::= add | sub | …
  unary operators:  unop ::= uminus | neg | …
  types:           τ ::= int | float | … | void *
  literals:        λ ::= intlit | floatlit | …
  instructions: e ::=  
       t ← constant τ λ
     | t ← local  x
     | t ← global  x
     | t ← formal  x
     | t ← string  s
     | t ← binary  τ  binop t t
     | t ← unary  τ  unop t
     | t ← convert  τ τ t
     | t ← ternary  τ t t t
     | t ← load  τ t
     | store τ t t
     | t ← memcpy  t t int
     | call  f
     | x, x, ← asm  s  t, t, …
     | indirectCall  t
     | return  τ t
     | goto  l
     | t ← addrOfLabel  l
     | indirectGoto  t
     | branchIfTrue  t  l 
     | switch  τ t  λ  λ  l ⟨l, l, …⟩ 
     | merged  ⟨ e, e, \ldots⟩ 

However, a high degree of diversity can be achieved from the way instructions communicate with each other, through values stored on the stack or passed in virtual registers. Tigress can generate instructions that use any combination of registers and stack storage for the inputs they read or the output they produce.

Tigress can induce further diversity by merging instructions into superoperators. New, merged, instructions can have an almost abritrary complex semantics, involving multiple arithmetic operations and operations both on the stack and virtual registers. For more information on superoperators, see Optimizing an ANSI C interpreter with superoperators by Todd Proebsting. The complex semantics of instructions generated by superoperators make manual analysis of generated interpreters, such as discussed by Rolles in Unpacking virtualization obfuscators, difficult.

Consider setting --VirtualizeMaxDuplicateOps=2 and --VirtualizeOperands=mixed resulting in two store-int instructions, one that takes both arguments in registers, and one that takes one argument on the stack and the other in a register. Tigress will chose between them randomly. Here are the corresponding instruction handlers:

case _0__store_int$left_REG_0$right_REG_1: 
   (_0__pc[0]) ++;
   *((int *)_0__regs[0][*((int *)_0__pc[0])]._void_star) = _0__regs[0][*((int *)(_0__pc[0] + 4))]._int;
   _0__pc[0] += 8;
   break;

case _0__store_int$right_STA_0$left_REG_0: 
   (_0__pc[0]) ++;
   *((int *)_0__regs[0][*((int *)_0__pc[0])]._void_star) = _0__stack[0][_0__sp[0] + 0]._int;
   (_0__sp[0]) --;
   _0__pc[0] += 4;
   break;

Consider next setting --VirtualizeSuperOpsRatio=2.0 and --VirtualizeMaxMergeLength=10, resulting in virtual instructions with highly complex semantics: Here is the instruction handler for one such instruction, made up by merging 10 primitive instructions:

case _0__local$result_STA_0$value_LIT_0__\
   convert_void_star2void_star$left_STA_0$result_REG_0__\
   load_int$result_REG_0$left_REG_1__\
   local$result_STA_0$value_LIT_0__\
   convert_void_star2void_star$left_STA_0$result_REG_0__\
   store_int$left_REG_0$right_REG_1__\
   local$result_REG_0$value_LIT_1__\
   local$result_STA_0$value_LIT_0__\
   convert_void_star2void_star$left_STA_0$result_REG_0__\
   load_int$result_STA_0$left_REG_0: 
    (_0__pc[0]) ++;
    _0__regs[0][*((int *)(_0__pc[0] + 4))]._void_star = (void *)(_0__locals + *((int *)_0__pc[0]));
    _0__regs[0][*((int *)(_0__pc[0] + 8))]._int = *((int *)_0__regs[0][*((int *)(_0__pc[0] + 12))]._void_star);
    _0__regs[0][*((int *)(_0__pc[0] + 20))]._void_star = (void *)(_0__locals + *((int *)(_0__pc[0] + 16)));
    *((int *)_0__regs[0][*((int *)(_0__pc[0] + 24))]._void_star) = _0__regs[0][*((int *)(_0__pc[0] + 28))]._int;
    _0__regs[0][*((int *)(_0__pc[0] + 32))]._void_star = (void *)(_0__locals + *((int *)(_0__pc[0] + 36)));
    _0__regs[0][*((int *)(_0__pc[0] + 44))]._void_star = (void *)(_0__locals + *((int *)(_0__pc[0] + 40)));
    _0__stack[0][_0__sp[0] + 1]._int = *((int *)_0__regs[0][*((int *)(_0__pc[0] + 48))]._void_star);
    (_0__sp[0]) ++;
    _0__pc[0] += 52;
    break;

Note that the instruction name really is almost 400 characters long; the backslashes are here only for display purposes! Also note that the instruction itself is 53 bytes long, almost as long as the longest VAX instruction (EMODH, 54 bytes) and much longer than the longest x86 instruction (15 bytes)

OptionArgumentsDescription
--VirtualizeOperands stack, registers, mixed, ? Comma-separated list of the types of operands allowed in the ISA. Default=stack.
  • stack = use stack arguments to instructions
  • registers = use register arguments to instructions
  • mixed = same as stack,registers
--VirtualizeMaxDuplicateOps INTSPEC Number of ADD instructions, for example, with different signatures. Default=0.
--VirtualizeRandomOps bool Should opcodes be randomized, or go from 0..n? Default=true.
--VirtualizeSuperOpsRatio Float>0.0 Desired number of super operators. Default=0.0.
--VirtualizeMaxMergeLength INTSPEC Longest sequence of instructions to be merged into one. Default=0.
 

Instruction Handler Obfuscation

You can split up the generated interpreter by inserting opaque predicates. This is useful to make the instruction handlers and the dispatch logic less conspicuous. Here's one example; the handler can obviously be split in multiple ways:

OptionArgumentsDescription
--VirtualizeInstructionHandlerSplitCount INTSPEC Number of opaques to add to each instruction handler. Default=0.
 

VPC Obfuscation

You can add opaque expressions to the virtual PC to make it more difficult to find.

One possible attack on interpreters is to perform a taint analysis on input-dependent variables and discard any instructions which are not tainted. To frustrate such analyses we can add implicit flow to the VPC.

OptionArgumentsDescription
--VirtualizeAddOpaqueToVPC BOOLSPEC Whether to add opaques to the virtual program counter. Default=false.
--VirtualizeImplicitFlowPC PCInit, PCUpdate, * Insert implicit flow between the virtual program counter and instruction dispatcher. Default=none.
  • PCInit = insert implcit flow between the computation of the VPC address and the first load
  • PCUpdate = insert implcit flow for each VPC load (potentially very slow)
  • * = select all options
--VirtualizeImplicitFlow S-Expression The type of implicit flow to insert. See --AntiTaintAnalysisImplicitFlow for a description. Default=none.
 

Bogus Functions

Generate bogus functions that are virtualized along with the "real" function. Instructions from the bogus and real function are executed cyclically and in sequence, i.e. first an instruction from the real function, then one from bogus function number 1, then one from bogus function number 2, etc., and then the process repeats with an instruction from the real function. The purpose is to frustrate dynamic analyses that try to locate the virtual program counter, by providing multiple VPCs, one "real", and one or more that behave as if they were real, but which interpret unused functions:

OptionArgumentsDescription
--VirtualizeNumberOfBogusFuns INTSPEC Weave the execution of random functions into the execution of the original program. This makes certain kinds of pattern-based dynamic analysis more difficult. Default=0.
--VirtualizeAddOpaqueToBogusFuns BOOLSPEC Whether to add opaque expressions to the generated bogus function. Default=false.
--VirtualizeBogusFunsGenerateOutput BOOLSPEC Make the bogus function produce output (typically be writing to /dev/null), to prevent it from appearing to have no effect. Default=true.
--VirtualizeBogusFunKinds trivial, arithSeq, collatz, * The kind of bogus function to generate. Comma-separated list. Default=arithSeq,collatz.
  • trivial = insert a trivial computation
  • arithSeq = insert a simple arithmetic loop
  • collatz = insert a computation of the Collatz sequence
  • * = select all options
 

Bogus Loops

Add random computations to every iteration of the dispatch loop. Use this to frustrate dynamic analysis by

1

inserting bogus instructions between consecutive iterations of the dispatch loop, thereby making the dispatch harder to recognize;

2

making traces longer and thereby harder to store and analyze.


OptionArgumentsDescription
--VirtualizeBogusLoopKinds trivial, arithSeq, collatz, * Insert a bogus loop for each instruction list. This will extend the length of the trace, making dynamic analysis more difficult. Default=collatz.
  • trivial = insert a trivial computation
  • arithSeq = insert a simple arithmetic loop
  • collatz = insert a computation of the Collatz sequence
  • * = select all options
--VirtualizeBogusLoopIterations INTSPEC Adjust this value to balance performance and trace length. Default=0.
 

Reentrant Interpreters

Make interpreters that can execute a few instructions, return, and later resume to execute a few more instructions, until, eventually, they terminate. This is particularly useful when it is not important exactly when the a piece of code executes, as long as it executes eventually, and where the stealthiness of the computations is paramount.

You must prepare your code in the following ways:

1

The function you want to virtualize must have an argument int* operation. It can occur anywhere among the formal parameters:

void foo(int* operation, int n, int* result) {…}

2

The first time foo gets called, operation must be <0, and you must pass actual arguments to foo that it will use throughout the computation:

int operation = -10; 
foo(&operation,n,&result);

"-10" here means to initialize foo and execute 10 instructions.

3

Sprinkle calls to foo throughout your program, making sure that operation>0:

operation = 10;
foo(&operation,bogus1,&bogus2);  

Here you can pass whatever arguments you want to foo, they won't be used. Rather, the ones that were passed in the first call will be used throughout. "10" here means to resume foo and execute 10 instructions.

4

You can check if foo has terminated by testing the value of operation after the call:

operation = 10;
foo(&operation,bogus1,&bogus2);  
if (operation > 0)
   /* we're done! */
else if (operation < 0)
   /* more work to do! */

5

If you want to make sure that foo has terminated --- because you really want its result at a particular point --- set operation to a large enough value:

operation = 1000;
foo(&operation,bogus1,&bogus2);  

6

Additional calls to foo once termination has been reached is safe; no additional instructions will be executed.

7

If you want to call foo to compute a new value, call it again with operation<0:

   int operation = -10; 
   foo(&operation,n,&result);

To ensure termination you can

1

experiment yourself with how many iterations are necessary to finish the computation;

2

make sure that the last call to foo is passed a huge value to 'operation';

3

put the last call to foo in a loop

   foo(&operation);   
   while (operation < 0) {
      /* some other computation here */
      operation = 10;
      foo(&operation);   
   } 
   /* result is available here */

It is a good idea to combine reentrant interpreters with superoperators. Superoperators produce long instructions that perform more work during each iteration, and as a result the number of dispatches (i.e. loop iterations) is reduced. In other words, if you want to frustrate dynamic analysis that looks for evidence of the dispatch loop in the instruction trace, superoperators combined with reentrant interpreters will reduce the presence of such artifacts.

OptionArgumentsDescription
--VirtualizeReentrant Make the function reentrant. Default=false.
 

Encoding the Program Array

Setting --VirtualizeEncodeByteArray=allAtOnce results in each program instruction being xor:ed with a constant value, thus ensuring that it is not in cleartext until it is run. The generated code looks something like this:

unsigned char _5_obf3_$array[1][141]  = 
     {{
        (unsigned char)formal$result_STA_0$value_LIT_0 ^ (unsigned char)16,
        (unsigned char)1 ^ (unsigned char)16,
        (unsigned char)0 ^ (unsigned char)16,
        (unsigned char)0 ^ (unsigned char)16,
        (unsigned char)0 ^ (unsigned char)16,
        (unsigned char)load_int$left_STA_0$result_STA_0 ^ (unsigned char)16,
        (unsigned char)formal$result_STA_0$value_LIT_0 ^ (unsigned char)16,
        (unsigned char)0 ^ (unsigned char)16,
        (unsigned char)0 ^ (unsigned char)16,
        (unsigned char)0 ^ (unsigned char)16,
        (unsigned char)0 ^ (unsigned char)16,
        ...
     }}

This does not work for --VirtualizeDispatch=direct.

Additionally, setting --VirtualizeObfuscateDecodeByteArray=true and --VirtualizeOpaqueStructs=input,env ensures that the decoded bytecode array depends on input. The decoding procedure looks like this:

  strcmp_result17 = (int)strlen(*(argv + (argc - 1)));
  decodeVar16 = (strcmp_result17 - 1 < 0) + (strcmp_result17 - 10 > 0) ? currentOp : (unsigned char)16;
  copyIndex15 = 0;
  while (copyIndex15 < 141) {
    localArrayCopy11[0][copyIndex15] = array[0][copyIndex15] ^ decodeVar16;
    copyIndex15 ++;
  }
  $pc[0] = localArrayCopy11[0];

Here's an example script:

tigress --Environment=... --Seed=0 \
   --Transform=InitImplicitFlow \
   --Transform=InitEntropy \
   --Transform=InitOpaque --Functions=main --InitOpaqueStructs=input \
   --Transform=UpdateEntropy --Functions=main --UpdateEntropyVar=argv,argc \
   --Inputs="+1:int:42,-1:length:1?10" \
   --Transform=Virtualize --InitOpaqueStructs=input,env \
   --VirtualizeDispatch=interpolation --Functions=obf3 \
   --VirtualizeEncodeByteArray=allAtOnce \
   --VirtualizeObfuscateDecodeByteArray=true \
   --VirtualizeOpaqueStructs=input,env \
      arith.c --out=arith_out.c

--Inputs="+1:int:42,-1:length:1?10" is a specfication of invariants over the command line. It specifies:

  • the first argument on the command line should be the integer 42
  • the last argument should have a length between 1 and 10

--VirtualizeOpaqueStructs=input,env ensures that the program array will be encoded based on these command line invariants. If the program is invoked with a set of command line arguments that violate the invariants it is likely to crash.

Starting with version 3.3.3, setting --VirtualizeEncodeByteArray=incrementally results in the bytecode array being decoded as each entry is encountered. The instruction handler code now looks like this, where 3472328296227680304UL is the xor encoding constant:

  __4_callInd__constant_unsigned_long$result_STA_0$value_LIT_0: ;
  (__4_callInd_$pc[0]) ++;
  (__4_callInd_$sp[0] + 1)->_unsigned_long = *((unsigned long *)__4_callInd_$pc[0]) ^ 3472328296227680304UL;
  (__4_callInd_$sp[0]) ++;
  __4_callInd_$pc[0] += 8;
OptionArgumentsDescription
--VirtualizeOpaqueStructs list, array, input, env, * Default=list,array.
  • list = Generate opaque expressions using linked lists
  • array = Generate opaque expressions using arrays
  • input = Generate opaque expressions that depend on input. Requires --Inputs to set invariants over input.
  • env = Generate opaque expressions from entropy. Requires --InitEntropy.
  • * = Same as list,array,input,env
--VirtualizeEncodeByteArray allAtOnce, incrementally Encode the bytecode array. Doesn't work for direct dispatch. Requires opaque expressions. Prior to version 3.3.3 this used to be a boolean, which triggered allAtOnce. Default=NONE.
  • allAtOnce = Decode the entire bytecode array on entry to the function.
  • incrementally = Decode each entry in the bytecode array as they are encountered.
--VirtualizeObfuscateDecodeByteArray BOOLSPEC Obfuscates the program array decoded with opaque expressions. --VirtualizeOpaqueStructs=input,env are the preferable opaque kinds, since it means that the bytecode array depends on input. Default=false.
 

Dynamic Program Arrays

Setting --VirtualizeDynamicBytecode=true results in the program array being constantly modified at runtime, much in the same way that happens to jitted code in the JitDynamic transformation. In fact, exactly the same mechanisms are used for both transformations, and they share all the same options. Here's an example script:

tigress --Seed=0 \
   --Transform=Virtualize \
      --Functions=foo \
      --VirtualizeDispatch=switch \
      --VirtualizeDynamicBytecode=true \
      --VirtualizeDynamicCodecs=xtea \
      --VirtualizeDynamicKeyTypes=data \
      --VirtualizeDynamicBlockFraction=%100 \
      --VirtualizeDynamicReEncode=true \
      --VirtualizeDynamicRandomizeBlocks=false \
      --VirtualizeDynamicDumpCFG=false \
      --VirtualizeDynamicAnnotateTree=false \
      --VirtualizeDynamicDumpTree=false \
      --VirtualizeDynamicDumpIntermediate=false \
      --VirtualizeDynamicTrace=0 \
      --VirtualizeDynamicTraceExec=false \
      --VirtualizeDynamicTraceBlock=false \
      --VirtualizeDynamicCompileCommand="gcc -o %o %i -lm" \
      arith.c --out=arith_out.c
OptionArgumentsDescription
--VirtualizeDynamicBytecode BOOLSPEC Similar to the JitDynamic transform, make the virtualized bytecode self-modifying. Default=hard.
--VirtualizeDynamicOptimize BOOLSPEC Clean up the generated code by removing jumps-to-jumps. Default=true.
--VirtualizeDynamicTrace INTSPEC Insert runtime tracing of instructions. Set to 1 to turn it on. Default=0.
--VirtualizeDynamicTraceExec BOOLSPEC Annotate each instruction, showing from where it was generated, and the results of execution. Default=false.
--VirtualizeDynamicDumpTree BOOLSPEC Print the tree representation of the function, prior to generating the jitting code. Default=false.
--VirtualizeDynamicAnnotateTree BOOLSPEC Annotate the generated code with the corresponding intermediate tree code instructions. Default=false.
--VirtualizeDynamicCodecs none, ident, ident_loop, xor_transfer, xor_byte_loop, xor_word_loop, xor_qword_loop, xor_call, xor_call_trace, xtea, xtea_trace, stolen_byte, stolen_short, stolen_word How blocks should be encoded/decoded. Default=*.
  • none = No encoding
  • ident = The identity encoding using a single copy JIT instruction
  • ident_loop = The identity encoding using a copy loop of primitive JIT instructions
  • xor_transfer = An xor encoding using a single xor JIT instruction
  • xor_byte_loop = An xor encoding using a copy loop of byte-size primitive JIT instructions
  • xor_word_loop = An xor encoding using a copy loop of word-size primitive JIT instructions
  • xor_qword_loop = An xor encoding using a copy loop of qword-size primitive JIT instructions
  • xor_call = An xor encoding calling a xor function
  • xor_call_trace = An xor encoding calling a xor function with tracing turned on (for debugging)
  • xtea = An xtea encryption
  • xtea_trace = An xtea encryption with tracing turned on (for debugging)
  • stolen_byte = A byte-sized stolen bytes encoding
  • stolen_short = A short-sized stolen bytes encoding
  • stolen_word = A word-sized stolen bytes encoding
--VirtualizeDynamicKeyTypes data, code Where the encoding/decoding key is stored (for xor and xtea encodings) Default=data.
  • data = In the data segment
  • code = In the code segment (not implemented)
--VirtualizeDynamicBlockFraction FRACSPEC Fraction of the basic blocks in a function to encode Default=all.
--VirtualizeDynamicRandomizeBlocks BOOLSPEC Randomize the order of basic blocks Default=true.
--VirtualizeDynamicReEncode BOOLSPEC If true, blocks will be re-encoded after being executed. If false, blocks will be decoded once, and stay in cleartext. ('False' is not implemented; this option is always set to 'true'.) Default=true.
--VirtualizeDynamicDumpCFG BOOLSPEC Print the jitter's Control Flow Graph. This requires graphviz to be installed (the dot command is used). A number of pdf files get generated that shows the CFG at various stages of processing: CFGAfterInsertingAnnotations.pdf, CFGAfterSimplifyingJumps.pdf, CFGAfterTranslatingAnnotations.pdf, CFGBeforeInsertingAnnotations.pdf, CFGDumpingFunctionFinal.pdf, CFGDumpingFunctionInitial.pdf, CFGFixupIndirecJumps.pdf, CFGReplaceWithCompiledBlock.pdf, CFGSplitOutBranches.pdf, CFGSplitOutDataReferences.pdf, OriginalCFG.pdf Default=false.
--VirtualizeDynamicTraceBlock BOOLSPEC Print out a message before each block is executed. Default=false.
--VirtualizeDynamicTraceBlock STRING Print out a message before each block is executed. (Not currently implemented.) Default="".
--VirtualizeDynamicCompileCommand STRING A string of the form "gcc -std=c99 -o %o %i", where "%i" will be replaced with the name of the input file and "%o" with the name of the output file. For example, if your program uses the math library, you should set --VirtualizeDynamicCompileCommand="gcc -std=c99 -o %o %i -lm". Default="gcc -std=c99 -o %o %i".
 

Function Regions

You can virtualize a piece of a function, rather than the whole thing. Simpy put the name of the region after the function name, like this:

tigress ... --Transform=Virtualize  --Functions=fac:region2 ...

The region has to be defined like this in the code:

#include "tigress.h"

void fac(int n) {
  int s=1;
  int i;
  for (i=2; i<=n; i++) {
    TIGRESS_REGION(region2,
      s *= i;
    );
  }
  printf("fac(%i)=%i\n",n,s);
}

Note that the region needs to be ``self-contained,'' i.e. you can't jump into it, or out of it. Also, there will be some performance overhead.

 

Notes

As you are reading the code, there are a couple of interesting things to note:

  • Much of the symbolic information present in the transformed source files (such as types, enumerations, and structured control flow) that help make the code easy to read and understand, disappears once the source has been compiled, linked, and stripped. A successful attack will (at least partially) have to recover this information.
  • The code after two levels of virtualization looks very similar to the code after one level of virtualization. This is because the dispatch loop of the first virtualization gets coded into the bytecode program of the second. It's an interesting question to ask to what extent this hinders de-virtualization.
  • The direct and call dispatch methods result in much larger bytecode programs than the other methods. This is particularly evident on 64-bit machines where every opcode gets encoded in 8 bytes, in contrast with a single byte for the other methods. For this reason, if you are contemplating using two levels of interpretation, it's a good idea to make the second level not use direct or call dispatch, to keep the size of the program down. Future versions of Tigress will use more compact encodings for these types of dispatch.
 

Issues

Here are some of the known issues with the Virtualize transformation:

  • Several dispatch methods make use of gcc's and clang labels-as-values. For other compilers only the switch and ifnest dispatch methods should be used.
  • --VirtualizeEncodeByteArray=true does not work for the direct dispatch method.
  • Our current implementation of reentrant interpreters doesn't handle function results, so make sure your function is void, and returns the result in a global or in a formal parameter.
  • The --VirtualizeConditionalKinds=flag option seems to have multiple issues on MacOS/llvm. Presumably this is due to some compiler problem related to inline assembly.
  • Consider this example taken from gcc's comp-goto-1.c torture test:
    goto *(base_addr + insn.f1.offset);
    
    This kind of arithmetic on the program counter is going to fail for transformations that completely restructure the code, such as virtualization.
  • By default, Virtualize transforms Variable Length Arrays into calls to alloca. Depending on the platform and the size of the arrays, the generated code can blow out the stack, and the program willcrash. If so, try the option --TransformVLA=malloc which converts VLAs to calls to malloc/free instead.