Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions Confuser.Protections/Compress/Compressor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions Confuser.Protections/Compress/CompressorContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,27 @@ 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<double> progressFunc) {
data = (byte[])data.Clone();
var dst = new uint[0x10];
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);
Expand Down
39 changes: 39 additions & 0 deletions Confuser.Protections/Compress/CompressorPrimes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace Confuser.Protections.Compress {
/// <summary>
/// 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
/// <see cref="CompressorContext.Encrypt" /> 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).
/// </summary>
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
};
}
}
8 changes: 4 additions & 4 deletions Confuser.Runtime/Compressor.Compat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down
8 changes: 4 additions & 4 deletions Confuser.Runtime/Compressor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down
140 changes: 140 additions & 0 deletions scripts/curate_primes.py
Original file line number Diff line number Diff line change
@@ -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 {{
/// <summary>
/// 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
/// <see cref="CompressorContext.Encrypt" /> 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).
/// </summary>
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()