Pack

 

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.

OptionArgumentsDescription
--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.

When, Where, What

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.

OptionArgumentsDescription
--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.
  • once = Decrypt each region once, at program startup.
  • call = Decrypt a region on entry to it and (optionally) re-encrypt it on return.
--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.

Ciphers

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.

OptionArgumentsDescription
--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.
  • xtea = The XTEA block cipher (64-bit block, 128-bit key).
  • xor = A fast 32-bit-word XOR.
  • rc4 = The RC4 stream cipher (128-bit key, byte-granular).
  • feistel = A random Feistel network (64-bit block): its round function (round count, rotations and constants) is randomized per build, so the emitted code differs every time. The key is folded into the code, so no key array appears in the binary.
  • des = The DES block cipher (64-bit block, 64-bit key). With --PackKeySource=folded the 16 round subkeys are folded into the emitted decoder, so it carries no key schedule and no key array.
--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.

Keys

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.

OptionArgumentsDescription
--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.
  • literal = The key is stored as an array in the binary (essentially cleartext, though the reading decoder may itself be obfuscated). Works with any cipher.
  • folded = The key is baked into the decryptor's code, so no key array appears in the binary. Only the foldable ciphers support this: xtea and des fold on request, and feistel is always folded.
  • plugin = The key is computed at run time by a user-supplied function, letting you bind decryption to the run-time environment.
--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.
  • null = cleartext --- store the key verbatim (the default; no encoding).
  • add = additive --- store the key offset by a per-slot constant.
  • xor = xor --- store the key xored with a per-slot constant.
  • poly1 = affine --- store a*key + b (an invertible degree-1 map).
  • polyn = permutation polynomial --- store a degree-d Rivest permutation polynomial of the key (a higher-degree bijection than poly1).
  • lineargf2 = linear over GF(2) --- an invertible bit-linear map.
  • bitperm = bit permutation --- a fixed permutation of the key's bits.
  • feistel = Feistel network --- a keyed, randomized invertible mixing.
  • split_bits = bit split --- scatter each key word across one global per bit.
  • split_nibbles = nibble split --- scatter each key word across one global per nibble.
  • split_bytes = byte split --- scatter each key word across one global per byte.
--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.