The goal of these transformations is to make functions self-modifying. Here's how Tigress is invoked:
> tigress --Environment=x86_64:Darwin:Clang:5.1 \
--Transform=Virtualize\
--Functions=add \
--VirtualizeDispatch=direct \
--Transform=SelfModify \
--Functions=add \
--SelfModifyFraction=%100 \
--SelfModifySubExpressions=false \
--SelfModifyOperators=\* \
--SelfModifyKinds=\* \
--SelfModifyBogusInstructions=0 \
inc.c --out=obf.c
> gcc -segprot __TEXT rwx rwx obf.c -o obf.exe # Darwin (early versions)
> tigress_post --Action=writable obf.exe # Darwin (newer versions)
> gcc --static -Wl,--omagic obf.c -o obf.exe # Linux
The transformation inserts a binary code template at the top of the function. There's one template for each type (int/long) and one for arithmetic, one for comparisons, and one for branches. They look roughly like this:
addrPtr10 = (unsigned long *)((unsigned long )(&& Lab_1) + 5);
*addrPtr10 = (unsigned long )(& A);
addrPtr10 = (unsigned long *)((unsigned long )(&& Lab_1) + 18);
*addrPtr10 = (unsigned long )(& B);
addrPtr10 = (unsigned long *)((unsigned long )(&& Lab_1) + 41);
*addrPtr10 = (unsigned long )(& C);
addrPtr10 = (unsigned long *)((unsigned long )(&& Lab_1) + 61);
*addrPtr10 = (unsigned long )(&& Lab_2);
Lab_1:
__asm__ volatile (
".byte 0x50;\n" // push %eax/rax
".byte 0x51;\n" // push %ecx/rcx
".byte 0x56;\n" // push %rsi/esi
".byte 0x48, 0xb8,0xc,0x7b,0x21,0x3d,0x54,0x2d,0xc0,0x1;\n" //movabs &A,%rax
".byte 0x8b, 0x00;\n" // movl (%rax),%eax
".byte 0x48, 0xb9,0x4f,0x40,0x2f,0xa3,0xd0,0xdc,0x24,0x42;\n" //movabs &B,%rcx
".byte 0x8b, 0x09;\n" // (%rcx),%ecx
".byte 0x31, 0xf6;\n" // xorl %rsi,%rsi
".byte 0x39, 0xc8;\n" // cmpl %rcx, %rax
".byte 0x40, 0x0f, 0x9c, 0xc6;\n" // setl %sil
".byte 0x48, 0xb9,0x7f,0x91,0x1c,0xaf,0xab,0x5f,0x2d,0x6a;\n" // movabs &C,%rcx
".byte 0x67, 0x89, 0x31;\n" // movl %esi,(%rcx)
".byte 0x5e;\n" // popq %rsi/esi
".byte 0x59;\n" // popq %ecx/rcx
".byte 0x58;\n" // popq %eax/rax
".byte 0xff, 0x25, 00, 00, 00, 00, 0xba,0xee,0x19,0x51,0x13,0x4b,0x12,0xdb;\n" // jmp (%rip)
:);
Lab_2:
Then, for each binary operation in the remainder of the function, the following code is inserted:
A = i;
B = n;
addrPtr10 = (unsigned long *)((unsigned long )(&& Lab_3) + 6);
*addrPtr10 = (unsigned long )(&& Lab_1);
addrPtr10 = (unsigned long *)((unsigned long )(&& Lab_1) + 61);
*addrPtr10 = (unsigned long )(&& Lab_4);
opPtr11 = (int *)((unsigned long )(&& Lab_1) + 35);
*opPtr11 = 0xc69c0f40L; // setl %sil
Lab_3:
__asm__ volatile (
".byte 0xff, 0x25, 00, 00, 00, 00, 0x3b,0x21,0x2e,0xd7,0xc5,0x44,0xf0,0xdd;\n" //jmp (%rip)
:);
Lab_4:
At runtime, this makes the following changes to the executable code:
This transformation is particularly useful after transformations that introduce indirect branches, such as virtualization and flattening with a direct or indirect dispatch. Below is an example:
void add(void) {
...
//----------------- Instruction Handler ----------------------
Lab_1: _1_add__load_int$left_STA_0$result_STA_0:
(_1_add_$pc[0]) ++;
(_1_add_$sp[0] + 0)->_int = *((int *)(_1_add_$sp[0] + 0)->_void_star);
// BEGIN indirect branch to the next instruction starts here
branchAddr10 = (unsigned long *)((unsigned long )(&& Lab_3) + 6);
*branchAddr10 = *(_1_add_$pc[0]);
Lab_3: /* CIL Label */
__asm__ volatile (".byte 0xff, 0x25, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00":);
// END
//----------------- Instruction Handler ----------------------
_1_add__returnVoid$:
(_1_add_$pc[0]) ++;
return;
...
}
Here, each indirect branch turns into the following code, where the byte sequence corresponds to the X86 8-byte direct jump:
addr = (unsigned long *)((unsigned long )(&& Lab) + 6);
*addr = address-to-jump-to;
Lab: asm volatile(".byte 0xff, 0x25, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00":);
This is interesting, since some de-virtualization transformations look for indirect branches, and these are now gone from the code.
Option | Arguments | Description |
---|---|---|
--Transform | SelfModify | Transform the function by adding code that modifies itself. |
--SelfModifyKinds | indirectBranch, arithmetic, comparisons | Types of instructions to self-modify. Default=indirectBranch.
|
--SelfModifyFraction | FRACSPEC | How many statements should be self-modified. Default=%100. |
--SelfModifySubExpressions | BOOLSPEC | Recurse into sub-expressions with transforming an expression. Can cause bugs. Default=false. |
--SelfModifyBogusInstructions | PlusA, MinusA, Mult, Div, Mod, Shiftlt, Shiftrt, Lt, Gt, Le, Ge, Eq, Ne, * | Which binary operators to modify. Default=*.
|
--SelfModifyBogusInstructions | INTSPEC | How many bogus instructions to insert inside the self-modify template to avoid pattern-matching attacks. Right now, the inserted instructions are simple 1-byte x86 instructions that are equivalent to NOPs. Default=0. |