diff --git a/Confuser.Protections/Compress/Compressor.cs b/Confuser.Protections/Compress/Compressor.cs index 40800f27c..14b2f981c 100644 --- a/Confuser.Protections/Compress/Compressor.cs +++ b/Confuser.Protections/Compress/Compressor.cs @@ -254,6 +254,13 @@ void InjectStub(ConfuserContext context, CompressorContext compCtx, ProtectionPa compCtx.LcgInit = random.NextUInt32(); compCtx.LcgMultiplier = random.NextUInt32() | 1; + // Randomize the key-derivation generator moduli (were the fixed 0x143fc089 / 0x444d56fb + // / 0x8a5cb7). Picked from curated sets that preserve key-stream quality; the same values + // are injected into the runtime Decrypt below (Mutation.KeyI1 / KeyI2 / KeyI3). + compCtx.StatePrime = CompressorPrimes.State[random.NextInt32(CompressorPrimes.State.Length)]; + compCtx.KeyWordPrime = CompressorPrimes.KeyWord[random.NextInt32(CompressorPrimes.KeyWord.Length)]; + compCtx.StreamPrime = CompressorPrimes.Stream[random.NextInt32(CompressorPrimes.Stream.Length)]; + uint seed = random.NextUInt32(); compCtx.OriginModule = context.OutputModules[compCtx.ModuleIndex]; @@ -298,11 +305,16 @@ void InjectStub(ConfuserContext context, CompressorContext compCtx, ProtectionPa foreach (Instruction instr in instrs) decrypter.Body.Instructions.Add(instr); - // Sync the runtime feedback constant with the value used during encryption. The - // runtime Decrypt exposes it as the Mutation.KeyI0 placeholder; injection is verified - // so a runtime-source change that drops the placeholder fails the build loudly instead - // of silently shipping a stub that can no longer decrypt what we encrypted. - InjectStubKeys(context, decrypter, new[] { 0 }, new[] { (int)compCtx.Feedback }); + // Sync the runtime Decrypt constants with the values used during encryption: the feedback + // constant (KeyI0) and the three key-derivation moduli (KeyI1/KeyI2/KeyI3). The runtime + // exposes them as Mutation.KeyI* placeholders; injection is verified so a runtime-source + // change that drops a placeholder fails the build loudly instead of silently shipping a + // stub that can no longer decrypt what we encrypted. + InjectStubKeys(context, decrypter, new[] { 0, 1, 2, 3 }, + new[] { + (int)compCtx.Feedback, (int)compCtx.StatePrime, + (int)compCtx.KeyWordPrime, (int)compCtx.StreamPrime + }); // Sync the runtime rolling-hash constants (init + odd multiplier) used to derive each // library's seed from its name (see PackModules) with the Mutation.KeyI0 / KeyI1 diff --git a/Confuser.Protections/Compress/CompressorContext.cs b/Confuser.Protections/Compress/CompressorContext.cs index 4500d0dfa..0d8da766e 100644 --- a/Confuser.Protections/Compress/CompressorContext.cs +++ b/Confuser.Protections/Compress/CompressorContext.cs @@ -22,6 +22,9 @@ internal class CompressorContext { public uint Feedback; public uint LcgInit; public uint LcgMultiplier; + public uint StatePrime; + public uint KeyWordPrime; + public uint StreamPrime; public byte[] Encrypt(ICompressionService compress, byte[] data, uint seed, Action progressFunc) { data = (byte[])data.Clone(); @@ -29,17 +32,17 @@ public byte[] Encrypt(ICompressionService compress, byte[] data, uint seed, Acti var src = new uint[0x10]; ulong state = seed; for (int i = 0; i < 0x10; i++) { - state = (state * state) % 0x143fc089; + state = (state * state) % StatePrime; src[i] = (uint)state; - dst[i] = (uint)((state * state) % 0x444d56fb); + dst[i] = (uint)((state * state) % KeyWordPrime); } uint[] key = Deriver.DeriveKey(dst, src); - var z = (uint)(state % 0x8a5cb7); + var z = (uint)(state % StreamPrime); for (int i = 0; i < data.Length; i++) { data[i] ^= (byte)state; if ((i & 0xff) == 0) - state = (state * state) % 0x8a5cb7; + state = (state * state) % StreamPrime; } data = compress.Compress(data, progressFunc); Array.Resize(ref data, (data.Length + 3) & ~3); diff --git a/Confuser.Protections/Compress/CompressorPrimes.cs b/Confuser.Protections/Compress/CompressorPrimes.cs new file mode 100644 index 000000000..e2c74eda7 --- /dev/null +++ b/Confuser.Protections/Compress/CompressorPrimes.cs @@ -0,0 +1,39 @@ +namespace Confuser.Protections.Compress { + /// + /// Curated moduli for the compressor key-derivation generator (state = state * state % m). + /// One value from each set is chosen per pack and injected into both the obfuscator-side + /// and the runtime Decrypt method (via mutation + /// keys), replacing the historic fixed constants that let de4dot/AV fingerprint the stub. + /// + /// The round-trip is correct for any modulus (encrypt and decrypt share it), so these are + /// selected purely to preserve key-stream quality: each is prime and congruent to 3 mod 4 + /// (so squaring permutes the quadratic residues and the stream cannot collapse), matches the + /// bit-length of the original constant, and was validated to mix at least as well as it. + /// Generated by scripts/curate_primes.py (seed 69). + /// + internal static class CompressorPrimes { + // state advance -> src[i] (was 0x143fc089, 29-bit) + internal static readonly uint[] State = { + 0x11d61057, 0x11f35013, 0x120a8c8f, 0x1215d013, 0x12218207, 0x125bf20b, 0x125e2b3b, 0x1290f8cb, + 0x12980867, 0x12aa3107, 0x12cf99af, 0x12e6d2b7, 0x12f71a9b, 0x13048633, 0x13494d0f, 0x134f4aaf, + 0x135e2afb, 0x13903b77, 0x13a49a63, 0x13df88cb, 0x144c881f, 0x147c68e7, 0x1530e5f3, 0x153e189f, + 0x156e4fb3, 0x15a22ab3, 0x15c89ad3, 0x15dc05eb, 0x1622512f, 0x1657087f, 0x165d0cd3, 0x169cc6f3 + }; + + // key-word derivation -> dst[i] (was 0x444d56fb, 31-bit) + internal static readonly uint[] KeyWord = { + 0x4049d5b7, 0x40ba619f, 0x413406a3, 0x4193e83b, 0x42668e5b, 0x427747c3, 0x43e4871f, 0x43e4f6eb, + 0x43ee3c57, 0x44591adf, 0x446a5873, 0x44e756df, 0x4502f48b, 0x4528b713, 0x45422afb, 0x45630b9b, + 0x45efbb6b, 0x4743e4eb, 0x476bf247, 0x47d6f76f, 0x48bb09db, 0x48be1ce7, 0x494d9b8f, 0x49cd1e3f, + 0x4a07494f, 0x4a169e4b, 0x4a18a703, 0x4a76dba3, 0x4ae9ea1b, 0x4aedf543, 0x4b3edb83, 0x4bc34bd3 + }; + + // data-stream advance (was 0x8a5cb7, 24-bit) + internal static readonly uint[] Stream = { + 0x00807263, 0x00812957, 0x008226fb, 0x00837437, 0x0084506b, 0x0085d83f, 0x00866623, 0x0086cf7b, + 0x0088824b, 0x008903e7, 0x008a8c33, 0x008b20a7, 0x008b409b, 0x008b7b0f, 0x008bddef, 0x008de30f, + 0x008eabc3, 0x008ed3db, 0x00901ba7, 0x00908497, 0x0090a2e3, 0x0090df37, 0x0090e6a7, 0x0091306f, + 0x0091922f, 0x00925733, 0x00938e2b, 0x0093aaff, 0x00940987, 0x009825b3, 0x0099edb3, 0x009a9ac3 + }; + } +} diff --git a/Confuser.Runtime/Compressor.Compat.cs b/Confuser.Runtime/Compressor.Compat.cs index ec2225939..4486a7ac4 100644 --- a/Confuser.Runtime/Compressor.Compat.cs +++ b/Confuser.Runtime/Compressor.Compat.cs @@ -12,9 +12,9 @@ static byte[] Decrypt(uint[] data, uint seed) { var k = new uint[0x10]; ulong s = seed; for (int i = 0; i < 0x10; i++) { - s = (s * s) % 0x143fc089; + s = (s * s) % (uint)Mutation.KeyI1; k[i] = (uint)s; - w[i] = (uint)((s * s) % 0x444d56fb); + w[i] = (uint)((s * s) % (uint)Mutation.KeyI2); } Mutation.Crypt(w, k); Array.Clear(k, 0, 0x10); @@ -34,11 +34,11 @@ static byte[] Decrypt(uint[] data, uint seed) { byte[] j = Lzma.Decompress(b); Array.Clear(b, 0, b.Length); - var z = (uint)(s % 0x8a5cb7); + var z = (uint)(s % (uint)Mutation.KeyI3); for (int i = 0; i < j.Length; i++) { j[i] ^= (byte)s; if ((i & 0xff) == 0) - s = (s * s) % 0x8a5cb7; + s = (s * s) % (uint)Mutation.KeyI3; } return j; } diff --git a/Confuser.Runtime/Compressor.cs b/Confuser.Runtime/Compressor.cs index b69e0fe12..bec5b2678 100644 --- a/Confuser.Runtime/Compressor.cs +++ b/Confuser.Runtime/Compressor.cs @@ -12,9 +12,9 @@ static byte[] Decrypt(uint[] data, uint seed) { var k = new uint[0x10]; ulong s = seed; for (int i = 0; i < 0x10; i++) { - s = (s * s) % 0x143fc089; + s = (s * s) % (uint)Mutation.KeyI1; k[i] = (uint)s; - w[i] = (uint)((s * s) % 0x444d56fb); + w[i] = (uint)((s * s) % (uint)Mutation.KeyI2); } Mutation.Crypt(w, k); Array.Clear(k, 0, 0x10); @@ -34,11 +34,11 @@ static byte[] Decrypt(uint[] data, uint seed) { byte[] j = Lzma.Decompress(b); Array.Clear(b, 0, b.Length); - var z = (uint)(s % 0x8a5cb7); + var z = (uint)(s % (uint)Mutation.KeyI3); for (int i = 0; i < j.Length; i++) { j[i] ^= (byte)s; if ((i & 0xff) == 0) - s = (s * s) % 0x8a5cb7; + s = (s * s) % (uint)Mutation.KeyI3; } return j; } diff --git a/scripts/curate_primes.py b/scripts/curate_primes.py new file mode 100644 index 000000000..828e00203 --- /dev/null +++ b/scripts/curate_primes.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""Curate the compressor key-derivation moduli baked into CompressorPrimes.cs. + +The compressor derives its key stream with state = state * state % m on both the +obfuscator side (CompressorContext.Encrypt) and the injected runtime (Decrypt). The +round-trip is correct for ANY modulus because both sides share it, so the historic +fixed constants (0x143fc089 / 0x444d56fb / 0x8a5cb7 — which are not even prime) can be +replaced per pack to kill the de4dot/AV fingerprint. This script selects the curated +replacement sets, choosing values that preserve key-stream quality: + + * prime and congruent to 3 mod 4 -> squaring permutes the quadratic residues, so the + stream cannot collapse to a fixed point for residue seeds; + * same bit-length as the original -> preserves value ranges (no (uint) truncation); + * mixing health at least as good as the original, measured by simulating the exact + ulong generator over thousands of random 32-bit seeds. + +Deterministic (seed 69) so the emitted set is reproducible. Run from the repo root: + python scripts/curate_primes.py +""" +import math +import os +import random + +random.seed(69) +MASK64 = (1 << 64) - 1 +ORIG = {"State": 0x143fc089, "KeyWord": 0x444d56fb, "Stream": 0x8a5cb7} +COUNT = 32 +OUT = os.path.join(os.path.dirname(__file__), "..", "Confuser.Protections", "Compress", "CompressorPrimes.cs") + + +def is_prime(n): + if n < 2: + return False + for p in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37): + if n % p == 0: + return n == p + d, r = n - 1, 0 + while d % 2 == 0: + d //= 2 + r += 1 + for a in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37): # deterministic for n < 3.3e24 + x = pow(a, d, n) + if x in (1, n - 1): + continue + for _ in range(r - 1): + x = x * x % n + if x == n - 1: + break + else: + return False + return True + + +def health(m, seeds=4000, iters=64): + """Return (collapse_count, min_distinct_ratio) for state = state*state % m over ulong.""" + collapse, worst = 0, 1.0 + for _ in range(seeds): + s, seen, stalled = random.getrandbits(32), set(), False + for _ in range(iters): + s = ((s * s) & MASK64) % m + if s in (0, 1): + stalled = True + break + seen.add(s) + collapse += stalled + worst = min(worst, len(seen) / iters) + return collapse, worst + + +def curate(orig, min_w, band=0.12): + lo = max(int(orig * (1 - band)), 1 << (orig.bit_length() - 1)) + hi = min(int(orig * (1 + band)), (1 << orig.bit_length()) - 1) + seen, out = set(), [] + while len(out) < COUNT: + n = (random.randrange(lo, hi) & ~3) | 3 # odd, == 3 mod 4 + if n in seen or n.bit_length() != orig.bit_length(): + continue + seen.add(n) + if not is_prime(n): + continue + c, w = health(n, seeds=1500) + if c == 0 and w >= min_w: + out.append(n) + return sorted(out) + + +def fmt(arr): + rows = ["\t\t\t" + ", ".join(f"0x{p:08x}" for p in arr[i:i + 8]) + "," for i in range(0, len(arr), 8)] + return "\n".join(rows).rstrip(",") + + +def main(): + cur, bars = {}, {} + for name, v in ORIG.items(): + _, w = health(v) + bars[name] = min(w * 0.98, 0.95) + cur[name] = curate(v, bars[name]) + print(f"{name}: original 0x{v:08x} prime={is_prime(v)} bitlen={v.bit_length()} " + f"minDistinct={w:.3f} -> {len(cur[name])} curated primes (bar>={bars[name]:.3f})") + bits = math.log2(len(cur["State"]) * len(cur["KeyWord"]) * len(cur["Stream"])) + print(f"per-pack identity contribution: {bits:.1f} bits") + + cs = f"""namespace Confuser.Protections.Compress {{ + /// + /// Curated moduli for the compressor key-derivation generator (state = state * state % m). + /// One value from each set is chosen per pack and injected into both the obfuscator-side + /// and the runtime Decrypt method (via mutation + /// keys), replacing the historic fixed constants that let de4dot/AV fingerprint the stub. + /// + /// The round-trip is correct for any modulus (encrypt and decrypt share it), so these are + /// selected purely to preserve key-stream quality: each is prime and congruent to 3 mod 4 + /// (so squaring permutes the quadratic residues and the stream cannot collapse), matches the + /// bit-length of the original constant, and was validated to mix at least as well as it. + /// Generated by scripts/curate_primes.py (seed 69). + /// + internal static class CompressorPrimes {{ + // state advance -> src[i] (was 0x143fc089, 29-bit) + internal static readonly uint[] State = {{ +{fmt(cur['State'])} + }}; + + // key-word derivation -> dst[i] (was 0x444d56fb, 31-bit) + internal static readonly uint[] KeyWord = {{ +{fmt(cur['KeyWord'])} + }}; + + // data-stream advance (was 0x8a5cb7, 24-bit) + internal static readonly uint[] Stream = {{ +{fmt(cur['Stream'])} + }}; + }} +}} +""" + with open(OUT, "w", newline="\n") as fh: + fh.write(cs) + print(f"wrote {os.path.normpath(OUT)}") + + +if __name__ == "__main__": + main()