
This transformation packs the selected functions: their code bytes are encrypted in the compiled binary and decrypted, in place, only when the program runs. An attacker who inspects the executable at rest sees ciphertext where the packed functions used to be, so static analysis and disassembly are frustrated until the program is actually executed. The decryptor Tigress emits can itself be obfuscated with Tigress' other transformations.
Packing has two halves. The Pack transformation runs at the source
level: it emits the decryptor, a startup bootstrap that decrypts each function
before it is first used, and a specification file (pack.json). The
actual encryption of the compiled code bytes is done afterwards, on the compiled
executable, by tigress_post:
tigress --Environment=x86_64:Linux:Gcc:12 \
--Transform=Pack \
--PackCiphers=xtea \
--Functions=secret \
program.c --out=obf.c
gcc -o program.exe -no-pie obf.c # on Linux
gcc -o program.exe obf.c # on Darwin
tigress_post \
--Action=pack\
--PackSpecFile=pack.json \
program.exe
tigress_post --Action=writable program.exe
codesign -f -s - program.exe # on Darwin
Because the packed code is decrypted back into its original location, in-place packing is currently supported on ELF/Linux only. On Mach-O, the text segments are first copied before they are decrypted.
| Option | Arguments | Description |
|---|---|---|
| --Transform | Pack | Encrypt the code bytes of the selected functions. Tigress emits a decryptor and a startup bootstrap that decrypts each function in place before it runs; the actual encryption of the compiled bytes is done afterwards by tigress_post --Action=pack, which reads the emitted pack.json. In-place packing is currently supported on ELF/Linux. |
| --PackSpecFile | filename | The name of the pack specification file Tigress emits, listing each packed region's cipher, key and parameters. tigress_post --Action=pack reads this file to encrypt the compiled binary. Several --Transform=Pack passes that name the same file accumulate their regions into it, so one file describes every packed region. Default=pack.json. |
| --PackTrace | BOOLSPEC | Insert run-time announcements around each region's decryption (and, with --PackWhen=call --PackRepack=true, re-encryption). Useful for debugging or demonstrating a packed binary. Default=false. |
| --PackDecryptorName | IDENTSPEC | Give the emitted decryptor a predictable name so that a following transformation can obfuscate it. The decryptor for encryption level i is named <name>_i --- e.g. --PackDecryptorName=decrypt names the level-1 decryptor decrypt_1, which you can then harden with, say, --Transform=Virtualize --Functions=decrypt_1. Default=NONE. |
An encrypted function can be decrypted at startup or right before it is called. It can remain decrypted, or it can be re-encrypted after the call has completed.
--PackSections=true will encrypt the selected functions as one blob rather than one
function: Tigress places all of them into a single dedicated section and emits a bootstrap that decrypts
the whole section in place at startup. This removes the per-function length globals and
the intra-section function boundaries so an attacker sees one opaque encrypted section
instead of individually decrypted functions. This is done as an outer layer on top of the
per-function packing.
| Option | Arguments | Description |
|---|---|---|
| --PackRepack | BOOLSPEC | Whether to re-encrypt a region after it has been used. Only meaningful with --PackWhen=call, where it shrinks the window during which the code is in the clear. Default=false. |
| --PackWhen | once, call | When a packed region is decrypted. Default=once.
|
| --PackSections | BOOLSPEC | Encrypt the selected functions as one blob rather than one region per function. Currently ELF/Linux only (on Darwin the option is ignored and each function is packed on its own). Default=false. |
Tigress supports several ciphers and you can specify which ones to use with the --PackCiphers=...
option. The --PackLevels option specifies how many times the binary should be encrypted.
For example, you can say --PackCiphers=xtea,xor,rc4 --PackLevels=3 to specify that the
code should be encrypted 3 times, with 3 specific ciphers.
| Option | Arguments | Description |
|---|---|---|
| --PackCiphers | xtea, xor, rc4, feistel, des | The pool of ciphers each encryption level may use. With --PackLevels > 1 the levels draw from this pool in turn. Default=xtea.
|
| --PackLevels | INTSPEC | Number of nested encryption layers to apply to each region. Each layer uses its own cipher (drawn from --PackCiphers) and its own key. Default=1. |
Ciphers need keys and those keys need to be stored somewhere, and they need to be hidden. In this
transform, --PackKeySource=... lets you store the key in cleartext in the binary,
or baked into the decryptor's code, or computed at runtime by a user-supplied plugin.
The --PackKeyCodecs=... option lets you encode the key using one of Tigress' value codecs,
making sure they key is not in cleartext at runtime. Each key word is encoded at obfuscation time
and decoded into a local buffer, just beforethe cipher reads it. This resists
entropy-based key scanning. An encoded key is not kept as one contiguous array: each stored
slot becomes its own global, emitted in a shuffled order, so no run of bytes in the data reads
as the key. The split codecs take this furthest, spreading each 32-bit key word across
many one-bit / nibble / byte slots. Applies only to a stored
literal key: a folded key lives in the decoder's code and a
plugin key is produced at run time, so neither leaves cleartext in the data to hide.
| Option | Arguments | Description |
|---|---|---|
| --PackKeySource | literal, folded, plugin | Where each region's key lives, given as a preference-ordered list: for each key slot, the first source that can supply a key for that slot's cipher is used. For example --PackKeySource=plugin,literal uses a plugin key where one is available and falls back to a literal otherwise. Default=literal.
|
| --PackKeyCodecs | null, add, xor, poly1, polyn, lineargf2, bitperm, feistel, split_bits, split_nibbles, split_bytes | The pool of value codecs used to hide a stored key, one drawn per key slot for diversity. Default=null.
|
| --PackKeyComposeCodecs | Composition chains for a stored literal key, pooled with --PackKeyCodecs (one is drawn per key slot). Each chain is a comma-separated list of two or more codecs, chains separated by semicolons; a chain is applied outermost-to-innermost, so split_bits,poly1 means split(poly1(key)) --- poly1-encode the key word, then scatter its bits. The outermost codec may be a split (multi-piece); every inner codec must be single-piece. Only the constEncodable key codecs are allowed (add, xor, poly1, lineargf2, bitperm, feistel, split_*). Default=none. |