Encode Data

Encode integer variables so that they have a non-standard data representation. The goal is for a variable's real value (and the values of intermediate expressions used to compute it) to never be revealed, until it is printed or otherwise escapes the program. For example, an integer variable v could be replaced with:

v' = a*v + b

where a is a random odd integer and b a random integer. For example, given this program

int main () {
  int arg1 = ...
  int arg2 = ...
  int a = arg1;
  int b = arg2;
  int x = a*b;
  printf("x=%i\n",x);
}

Tigress might produce the following:

a = 1789355803 * arg1 + 1391591831;
b = 1789355803 * arg2 + 1391591831;
x = ((3537017619 * (a * b) - 3670706997 * a) - 3670706997 * b) + 3171898074;
printf("x=%i\n", -757949677 * x - 3670706997);

 

Usage

A typical invokation of this transformation lists a collection of local variables and formal parameters, and global variables:

   --Transform=EncodeData \
      --GlobalVariables='g1,g2' \
      --LocalVariables='fun1:L1,L2;fun2:L3' \
      --EncodeDataCodecs=poly1

These variables should all be integers, pointers to integers, arrays of integers, or combinations of these. In the example above, g1 may be an int, L1 an int*, L2 an array of ints, and L3 an array of pointers to ints.

If you want the return value of a function fun1 to be encoded, specify it this way:

   --Transform=EncodeData \
      --LocalVariables='fun1:return' \
      --EncodeDataCodecs=poly1

OptionArgumentsDescription
--Transform EncodeData Replace integer variables with a different encoding. Use --GlobalVariables and --LocalVariables to specify the variables that should be transformed. In addition to the variables specifed, any other variables that are related through aliasing will be transformed. Only integer variables, arrays of integers, and pointers to integers are currently supported. Avoid structs, since our alias analysis algorithm conflates all fields.
--EncodeDataCodecs automatic, poly1, polyn, addshare, xorshare, addxorshare, lineargf2, bitperm, feistel, split_bits, split_nibbles, split_bytes, bignum, bcd, rational, montgomery, xor, xorfloat, fixed, softfloat, null, add, rnc, poly_xor, xor_poly, poly_rnc, xor_rnc, * Comma-separated list of the kinds of codecs that may be used. The bijective integer codecs poly1 and polyn (and xor) are the ones that make sense in general; the others have caveats noted below. Default=poly1.
  • automatic = Choose a codec per variable by fit rather than at random: for each variable, pick (at random among the best) a codec whose set of operators performed in encoded space best covers the operators the program actually applies to that variable, so the operations that matter stay encoded. The candidate pool is the other codecs in this list, or -- when automatic is the only entry -- all of the exact (bijective, no range restriction) codecs. Range-limited codecs (rnc, rational) are not chosen automatically, since without a value-range analysis a variable cannot be shown to stay in range. Codecs are grouped by type, so all variables of one type share a codec. From version 4.1.1
  • poly1 = Linear transformation of the form a*x+b.
  • polyn = Degree-d permutation polynomial a_d*x^d + ... + a_1*x + a_0, a bijection modulo 2^w (Rivest's characterisation; the compositional inverse used for decoding is computed by the method of Barthelemy et al., SPRO 2016). Generalises poly1 (which is the degree-1 case); the degree is set with --EncodeDataPolyDegree. Exact for every value of every integer type, since it wraps modulo 2^w exactly as C does. Its benefit over poly1 is diversity -- a far larger key space with no recognisable affine shape. Note that at degree >= 2 there is no closed form for arithmetic in encoded space, so only equality tests stay encoded; other operations decode, compute, and re-encode.
  • addshare = Additive secret-sharing: the value is split into pieces that sum to it modulo 2^w, with pieces drawn fresh at runtime (so it is dynamic -- equal values encode differently). Homomorphic and exact for +, - and * in encoded space with no range restriction (the multiply uses a masked-multiplication gadget); bitwise ops and comparisons decode. Needs a prior --Transform=InitEntropy. Piece count is --EncodeDataNumberOfPieces. From version 4.1.1
  • xorshare = Boolean secret-sharing: pieces that XOR to the value, drawn fresh at runtime (dynamic). Homomorphic for ^, ~, & and | in encoded space (bitwise ops use a masked-AND gadget); arithmetic and comparisons decode. Needs a prior --Transform=InitEntropy. The boolean dual of addshare. From version 4.1.1
  • addxorshare = Combined additive+boolean secret-sharing: keeps two additive shares and does both arithmetic (+ - *) and bitwise (^ & | ~) ops in encoded space, converting between arithmetic and boolean sharing internally (Goubin A2B/B2A) so the value is never reconstructed. Dynamic; needs --Transform=InitEntropy. Bitwise ops are costly (each pays an A2B/B2A round-trip). From version 4.1.1
  • lineargf2 = GF(2)-linear mixing bijection E(x) = L(x) ^ c, with L an invertible chain of xorshift shears -- strong bit diffusion, a different algebra from poly1. Deterministic; homomorphic for ^ and ~ (and ==/!=); arithmetic and & | decode. From version 4.1.1
  • bitperm = Bit-permutation bijection: E(x) permutes the value's bits by a fresh random permutation (emitted as a handful of masked shifts OR-ed together). Because moving bit positions commutes with every bitwise operator, it is homomorphic for all of &, |, ^ and ~ (and ==/!=) -- strictly more than lineargf2 -- while arithmetic decodes. Deterministic and exact; the deterministic counterpart of xorshare. From version 4.1.1
  • feistel = A small keyed Feistel cipher on the word: the value is split into halves and put through 4-8 rounds of (L,R) -> (R, L ^ F(R,k)) with a keyed nonlinear round function; decode runs the rounds in reverse. A deterministic exact bijection, but a pseudorandom one with no algebraic shape to model -- the "encrypt the variable" codec, trading homomorphism for strength. Only ==/!= stay encoded (a bijection preserves equality); everything else decodes. From version 4.1.1
  • split_bits = Bit-slice split: a w-bit value is cut into w one-bit pieces (piece j = bit j), reassembled by OR_j (piece_j << j). A deterministic exact multi-piece bijection whose point is representational diversity, not encoded arithmetic -- it composes as the outermost codec of a chain (e.g. poly1 then split). Every operator decodes. From version 4.1.1
  • split_nibbles = Bit-slice split into 4-bit nibbles (w/4 pieces); otherwise as split_bits. From version 4.1.1
  • split_bytes = Bit-slice split into 8-bit bytes (w/8 pieces); otherwise as split_bits. From version 4.1.1
  • bignum = Multi-limb (bignum) positional split into --EncodeDataNumberOfPieces limbs of base 2^b; arithmetic becomes limb-wise carry/borrow code, like an arbitrary-precision integer. Deterministic and exact; homomorphic for + - & | ^ ~ and == != in encoded space; *, shifts and ordering comparisons decode. From version 4.1.1
  • bcd = Binary-coded decimal: the value stored as its decimal digits (one 0-9 digit per piece); a decimal representation that reads very differently from binary. Deterministic; homomorphic only in == != (decimal carry cannot match binary wrap-around, so arithmetic and bitwise ops decode). From version 4.1.1
  • rational = Rational a/b over a prime field (p > 2^2w): the only codec homomorphic in both + and * in encoded space (==/!= too; -, unary - and bitwise ops decode). Field products need p^2, so only narrow types are supported -- char, and short when 128-bit integers are allowed; wider types are rejected. Exact while the running value stays below p (an rnc-style range limit); decode does a modular inverse, so it is the most expensive codec. From version 4.1.1
  • montgomery = Montgomery form E(x) = x*R mod M with a random odd modulus M > 2^2w. Homomorphic in + and * (and ==/!=); the multiply is Montgomery's division-free REDC reduction, which also does encode and decode. Like rational but single-piece and division-free (no a/b, no runtime modular inverse), so it supports the same narrow types (char, and short with 128-bit integers) and rejects wider ones. Deterministic; exact while the running value stays below M (an rnc-style range limit); -, unary - and bitwise ops decode. From version 4.1.1
  • xor = Exclusive-or with a constant.
  • xorfloat = Exclusive-or with a constant on floating point values.
  • fixed = Fixed-point (Q-format) encoding of a real value; fraction bits set by --EncodeDataFixedFraction.
  • softfloat = Software floating-point representation.
  • null = No encoding (the identity codec); the no-op baseline.
  • add = Add a constant and promote to next largest integer type. Will fail for the largest integer type.
  • rnc = Residue Number Coding: the value is held as its residues modulo several pairwise-coprime moduli n_i, plus a fresh random multiple of each, so equal values do not encode equally. Correct at every integer type from version 4.1.0 (before that it silently computed wrong values at unsigned short, unsigned int, long and unsigned long).

    Range limitation. The codec is exact only while the value being reconstructed stays below half the product of its moduli. A single operation on in-range values always satisfies this, so ordinary arithmetic is correct. It is not guaranteed for a value that keeps growing, because the limit applies to the running value and not to the type: a variable ACCUMULATED in a loop (a counter, a running total, a repeated product) eventually passes it, after which the obfuscated program silently computes a different answer from the original. Large multiplications reach the limit fastest, since a product of two w-bit values is 2w bits wide. Prefer poly1 or xor for such variables -- both are exact for every value of every integer type, because they are bijections modulo 2^w and so wrap exactly as C does. Tracked as tigress_issues#183.

  • poly_xor = First encode with poly1, then with xor
  • xor_poly = First encode with xor, then with poly1
  • poly_rnc = First encode with poly1, then with rnc
  • xor_rnc = First encode with xor, then with rnc
  • * = All options
--EncodeDataComposeCodecs Compose codecs into nested encodings, as an alternative to the built-in shortcuts (poly_xor, xor_rnc, ...). The value is a semicolon-separated list of chains, each a comma-separated list of at least two codecs; one chain is chosen at random per variable and applied outermost-to-innermost, so a,b,c encodes as a(b(c(x))). For example --EncodeDataComposeCodecs='poly1,xor;rnc,poly1' encodes each variable as either poly1(xor(x)) or rnc(poly1(x)). Every codec except the outermost (first) must be single-piece. This combines with --EncodeDataCodecs: the plain codecs and the composition chains form one pool to choose from per variable. From version 4.1.1 Default=NONE.
--EncodeDataDebug pointsToGraph, aliasConstraints, aliasProgress, aliasDebug, aliasTypes, mayAliasRelationships, traceUpdates Debugging options. Default=NONE.
  • pointsToGraph = Print the alias graph.
  • aliasConstraints = Print alias constraints.
  • aliasProgress = Print progress of the alias computation.
  • aliasDebug = Print alias debugging information
  • aliasTypes = Print alias type information
  • mayAliasRelationships = Print may alias relationships
  • traceUpdates = Trace how functions are transformed
--EncodeDataOptimize Simplify expressions. Default=true.
--EncodeDataTrace Trace execution of encoded data. Default=false.
--EncodeDataPolyDegree INTSPEC Degree of the permutation polynomial used by the polyn codec (must be >= 1; degree 1 is equivalent to poly1). A higher degree gives more diversity. The inverse polynomial used for decoding has a bounded but larger degree (roughly the word size), so decoding cost grows modestly with the degree. From version 4.1.1 Default=3.
--EncodeDataNumberOfPieces INTSPEC Number of pieces in which to break up a value. Only applies to codecs that split values, such as rnc.

The count also decides whether rnc can encode a 64-bit value on a target without 128-bit integers (--Allow128BitInts=false). Its usual decode reconstructs through the product of all the moduli, which must exceed the type's range and so exceeds 64 bits; the narrow alternative (Garner's mixed-radix reconstruction) needs an unsigned type and at least 3 pieces, because a digit step costs twice the width of one modulus. With 2 pieces on a 64-bit value and no 128-bit integers available, rnc reports that it cannot encode the value. From version 4.1.0 Default=2.

--EncodeDataStorage struct, split How variabls split into multiple pieces should be stored. Default=struct.
  • struct = Store pieces in a struct.
  • split = Store pieces in individual variables. Only partially implemented.
 

Codec summary

The codecs differ in the algebra they preserve. A codec is homomorphic in an operation when that operation is performed directly on the encoded value, so the cleartext never appears in a register; other operations fall back to decode / operate / re-encode. This table summarizes each codec.

 

Issues

If you ask Tigress to encode a variable x, then all variables related to x through aliasing must also be encoded. In cases where our alias analysis algorithm isn't precise enough, encoding will fail at transformation time. For large programs, the alias analysis routine may take a very long time.

 

References

This transformation is based on ideas from several Cloakware/IRDETO papers and patents:


Codec Representation Homomorphic in (done in encoded space) Dynamic Pieces Types & restrictions
poly1affine a*x+b mod 2w + - * & | ^, == !=no1 integer/pointer; exact bijection, no range limit
polyndegree-d permutation polynomial mod 2w == !=no1 integer/pointer; exact bijection; degree = --EncodeDataPolyDegree
addshareadditive split, pieces sum to x mod 2w + -, unary -, *yes≥2 (default 2; * needs 2) integer/pointer; exact, no range limit; needs InitEntropy
xorshareboolean split, pieces XOR to x ^ ~ & | (& | need 2 pieces)yes≥2 integer/pointer; needs InitEntropy
addxorsharetwo additive shares; converts to boolean shares on demand (Goubin A2B/B2A) + - *, unary -, ^ & | ~yes2 integer/pointer; exact, no range limit; needs InitEntropy; combines addshare and xorshare so both arithmetic and bitwise ops stay encoded (bitwise ops pay an A2B/B2A round-trip)
lineargf2L(x) ^ c, GF(2)-linear (xorshift shears) ^ ~, == !=no1 integer/pointer; exact bijection; strong bit diffusion
bitpermbits of x rearranged by a random permutation & | ^ ~, == !=no1 integer/pointer; exact bijection; homomorphic in all bitwise ops (deterministic dual of xorshare)
feistela small keyed Feistel cipher on the word (halves, 4-8 rounds) == !=no1 integer/pointer; exact bijection; pseudorandom, no algebraic structure to model — the “encrypt the variable” codec (trades homomorphism for strength)
bignummulti-limb positional split, base 2b + - & | ^ ~, == !=no≥2 limbs integer/pointer; exact, no range limit; arithmetic is limb-wise carry/borrow code (hard to fold back)
bcddecimal digits, one 0-9 per piece == !=nodigits of 2w integer/pointer; representational (decimal); arithmetic/bitwise decode, since decimal carry ≠ binary wrap
rationalfraction a/b over a prime field, x = a·b-1 mod p + *, == !=no2 narrow types only (char; short with 128-bit ints); range-limited like rnc; costly decode (modular inverse); the only codec homomorphic in both + and *
montgomeryMontgomery form x·R mod M, M > 22w + *, == !=no1 narrow types only (char; short with 128-bit ints); range-limited like rnc; single-piece and division-free (REDC), unlike rational (no a/b, no runtime modular inverse)
rncresidues mod coprime moduli + random multiples + -, and * for narrow typesyes≥2 integer; range-limited (exact only while the running value stays below half the modulus product — a loop or accumulation eventually overflows, issue #183); needs InitEntropy; a 64-bit value needs 128-bit ints or ≥3 pieces
addx + k, promoted one size + - *, all comparisonsno1 integer; promotes to the next larger type, so it fails at the largest type
xorx ^ k == != (pointer arithmetic passes through)no1 integer/pointer; storage hiding (arithmetic and bitwise ops decode)
fixedfixed-point Q-format of a real value + - * /, shifts, == !=no1 real values; fraction bits = --EncodeDataFixedFraction; fixed-point precision/overflow limits
xorfloatx ^ k on the floating bit-pattern == !=no1 floating types
softfloatsoftware floating-point representation none (all ops decode)no1 floating types
nullcleartext (identity) all (it is the identity)no1 any; the no-op baseline
poly_xor, xor_poly,
poly_rnc, xor_rnc
stacked pair (one codec applied over another) the operations both layers keepinheritedinherited the intersection of the two layers' constraints