From 89ddb6dd8d1a63b67524cb9959cca0b7c8d6f814 Mon Sep 17 00:00:00 2001 From: RandomCrocodile Date: Mon, 21 Sep 2026 21:11:07 +0200 Subject: [PATCH] feature: randomize constants xorshift triple (#69) The Constants protection expands its key with a 32-bit xorshift (n ^= n >> a; n ^= n << b; n ^= n >> c) on both the obfuscator (EncodePhase) and the injected runtime (Constant.Initialize), using the fixed 12/25/27 that de4dot/AV pattern-match to fingerprint the stub. The triple is now chosen per module from a curated set and injected into the runtime Initialize via mutation keys (KeyI2/KeyI3/KeyI4). Both sides share it, so the round-trip holds for any shifts; the curated triples are additionally full-period (period 2^32-1, verified by GF(2) matrix order) with non-extreme shifts, so the key stream is well mixed. The historic 12/25/27 is not even full period. ~7 bits of added per-module identity. Injection goes through a new MutationHelper.InjectKeysEnsured that verifies each placeholder is present, so a runtime-source change that drops one fails the build instead of silently desyncing. scripts/curate_xorshift.py regenerates the set deterministically. Validated by 270_EnumArrayConstantProtection.Test and 193_ConstantsInlining.Test. --- Confuser.Core/Helpers/MutationHelper.cs | 31 +++++ .../Constants/ConstantXorshift.cs | 48 ++++++++ Confuser.Protections/Constants/EncodePhase.cs | 17 ++- Confuser.Runtime/Constant.cs | 6 +- scripts/curate_xorshift.py | 112 ++++++++++++++++++ 5 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 Confuser.Protections/Constants/ConstantXorshift.cs create mode 100644 scripts/curate_xorshift.py diff --git a/Confuser.Core/Helpers/MutationHelper.cs b/Confuser.Core/Helpers/MutationHelper.cs index a33ba5061..618a70b56 100644 --- a/Confuser.Core/Helpers/MutationHelper.cs +++ b/Confuser.Core/Helpers/MutationHelper.cs @@ -73,6 +73,37 @@ public static void InjectKeys(MethodDef method, int[] keyIds, int[] keys) { } } + /// + /// Replaces the mutation key placeholders like , but first + /// verifies that every requested key id actually has a placeholder in the method. Throws + /// if one is missing, so a runtime-source change that drops or renames a placeholder + /// fails loudly instead of silently emitting a stub whose injected values no longer + /// match the obfuscator side. + /// + /// The method to process. + /// The mutation key IDs. + /// The actual keys. + public static void InjectKeysEnsured(MethodDef method, int[] keyIds, int[] keys) { + var found = new bool[keyIds.Length]; + foreach (Instruction instr in method.Body.Instructions) { + if (instr.OpCode != OpCodes.Ldsfld || !(instr.Operand is IField field)) + continue; + if (field.DeclaringType.FullName == mutationType && + field2index.TryGetValue(field.Name, out int keyIndex)) { + int pos = Array.IndexOf(keyIds, keyIndex); + if (pos != -1) + found[pos] = true; + } + } + for (int i = 0; i < found.Length; i++) + if (!found[i]) + throw new InvalidOperationException( + $"Mutation placeholder KeyI{keyIds[i]} not found in method '{method.Name}'; " + + "the injected runtime source and the injector are out of sync."); + + InjectKeys(method, keyIds, keys); + } + /// /// Replaces the placeholder call in method with actual instruction sequence. /// diff --git a/Confuser.Protections/Constants/ConstantXorshift.cs b/Confuser.Protections/Constants/ConstantXorshift.cs new file mode 100644 index 000000000..4c4033668 --- /dev/null +++ b/Confuser.Protections/Constants/ConstantXorshift.cs @@ -0,0 +1,48 @@ +namespace Confuser.Protections.Constants { + /// + /// Curated full-period xorshift32 triples (a, b, c) for the constant key generator + /// n ^= n >> a; n ^= n << b; n ^= n >> c. One triple is chosen per module and injected + /// into the runtime Constant.Initialize (Mutation.KeyI2/KeyI3/KeyI4), replacing the fixed + /// 12/25/27 that de4dot/AV pattern-match. Both the obfuscator and the runtime use the same + /// triple, so the round-trip holds for any shifts; each listed triple is additionally a + /// full-period generator (period 2^32-1, verified by GF(2) matrix order) with non-extreme + /// shifts, so the expanded key stream is well mixed -- unlike the historic 12/25/27, which + /// is not full period. Generated by scripts/curate_xorshift.py. + /// + internal static class ConstantXorshift { + internal static readonly byte[][] Triples = { + new byte[] { 2, 5, 15 }, new byte[] { 2, 5, 21 }, new byte[] { 2, 7, 7 }, new byte[] { 2, 7, 9 }, + new byte[] { 2, 7, 25 }, new byte[] { 2, 9, 15 }, new byte[] { 2, 15, 17 }, new byte[] { 2, 15, 25 }, + new byte[] { 2, 21, 9 }, new byte[] { 3, 3, 26 }, new byte[] { 3, 3, 28 }, new byte[] { 3, 3, 29 }, + new byte[] { 3, 5, 20 }, new byte[] { 3, 5, 22 }, new byte[] { 3, 5, 25 }, new byte[] { 3, 7, 29 }, + new byte[] { 3, 13, 7 }, new byte[] { 3, 23, 25 }, new byte[] { 3, 25, 24 }, new byte[] { 3, 27, 11 }, + new byte[] { 4, 3, 17 }, new byte[] { 4, 3, 27 }, new byte[] { 4, 5, 15 }, new byte[] { 5, 3, 21 }, + new byte[] { 5, 7, 22 }, new byte[] { 5, 9, 7 }, new byte[] { 5, 9, 28 }, new byte[] { 5, 13, 6 }, + new byte[] { 5, 15, 17 }, new byte[] { 5, 17, 13 }, new byte[] { 5, 21, 12 }, new byte[] { 5, 27, 8 }, + new byte[] { 5, 27, 21 }, new byte[] { 5, 27, 25 }, new byte[] { 5, 27, 28 }, new byte[] { 6, 3, 17 }, + new byte[] { 6, 13, 5 }, new byte[] { 6, 17, 9 }, new byte[] { 6, 21, 7 }, new byte[] { 6, 21, 13 }, + new byte[] { 7, 7, 2 }, new byte[] { 7, 9, 5 }, new byte[] { 7, 13, 3 }, new byte[] { 7, 13, 25 }, + new byte[] { 7, 17, 21 }, new byte[] { 7, 21, 6 }, new byte[] { 7, 25, 12 }, new byte[] { 7, 25, 20 }, + new byte[] { 8, 7, 23 }, new byte[] { 8, 9, 23 }, new byte[] { 8, 27, 5 }, new byte[] { 9, 5, 14 }, + new byte[] { 9, 5, 25 }, new byte[] { 9, 7, 2 }, new byte[] { 9, 11, 19 }, new byte[] { 9, 17, 6 }, + new byte[] { 9, 21, 2 }, new byte[] { 9, 21, 16 }, new byte[] { 10, 9, 21 }, new byte[] { 10, 9, 25 }, + new byte[] { 11, 7, 12 }, new byte[] { 11, 7, 16 }, new byte[] { 11, 17, 13 }, new byte[] { 11, 21, 13 }, + new byte[] { 11, 27, 3 }, new byte[] { 12, 7, 11 }, new byte[] { 12, 9, 23 }, new byte[] { 12, 21, 5 }, + new byte[] { 12, 25, 7 }, new byte[] { 13, 3, 17 }, new byte[] { 13, 3, 27 }, new byte[] { 13, 5, 19 }, + new byte[] { 13, 17, 5 }, new byte[] { 13, 17, 11 }, new byte[] { 13, 17, 15 }, new byte[] { 13, 21, 6 }, + new byte[] { 13, 21, 11 }, new byte[] { 14, 5, 9 }, new byte[] { 14, 13, 15 }, new byte[] { 15, 5, 2 }, + new byte[] { 15, 5, 4 }, new byte[] { 15, 9, 2 }, new byte[] { 15, 13, 14 }, new byte[] { 15, 17, 13 }, + new byte[] { 16, 7, 11 }, new byte[] { 16, 21, 9 }, new byte[] { 17, 3, 4 }, new byte[] { 17, 3, 6 }, + new byte[] { 17, 3, 13 }, new byte[] { 17, 15, 2 }, new byte[] { 17, 15, 5 }, new byte[] { 17, 15, 20 }, + new byte[] { 17, 15, 23 }, new byte[] { 17, 15, 26 }, new byte[] { 19, 5, 13 }, new byte[] { 19, 11, 9 }, + new byte[] { 20, 5, 3 }, new byte[] { 20, 15, 17 }, new byte[] { 20, 25, 7 }, new byte[] { 21, 3, 5 }, + new byte[] { 21, 5, 2 }, new byte[] { 21, 9, 10 }, new byte[] { 21, 17, 7 }, new byte[] { 21, 27, 5 }, + new byte[] { 22, 5, 3 }, new byte[] { 22, 7, 5 }, new byte[] { 23, 7, 8 }, new byte[] { 23, 9, 8 }, + new byte[] { 23, 9, 12 }, new byte[] { 23, 15, 17 }, new byte[] { 24, 25, 3 }, new byte[] { 25, 5, 3 }, + new byte[] { 25, 5, 9 }, new byte[] { 25, 7, 2 }, new byte[] { 25, 9, 10 }, new byte[] { 25, 13, 7 }, + new byte[] { 25, 15, 2 }, new byte[] { 25, 23, 3 }, new byte[] { 25, 27, 5 }, new byte[] { 26, 3, 3 }, + new byte[] { 26, 15, 17 }, new byte[] { 27, 3, 4 }, new byte[] { 27, 3, 13 }, new byte[] { 28, 3, 3 }, + new byte[] { 28, 9, 5 }, new byte[] { 28, 27, 5 }, new byte[] { 29, 3, 3 }, new byte[] { 29, 7, 3 } + }; + } +} diff --git a/Confuser.Protections/Constants/EncodePhase.cs b/Confuser.Protections/Constants/EncodePhase.cs index b3db5bd68..0c25f1933 100644 --- a/Confuser.Protections/Constants/EncodePhase.cs +++ b/Confuser.Protections/Constants/EncodePhase.cs @@ -90,12 +90,17 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa // encrypt uint keySeed = moduleCtx.Random.NextUInt32(); + // Randomize the xorshift triple (was the fixed 12/25/27, a de4dot/AV fingerprint) from a + // curated set of full-period triples; the same shifts are injected into the runtime + // Initialize below (Mutation.KeyI2/KeyI3/KeyI4) so both sides expand the same key stream. + byte[] shifts = ConstantXorshift.Triples[moduleCtx.Random.NextInt32(ConstantXorshift.Triples.Length)]; + int shiftA = shifts[0], shiftB = shifts[1], shiftC = shifts[2]; var key = new uint[0x10]; uint state = keySeed; for (int i = 0; i < 0x10; i++) { - state ^= state >> 12; - state ^= state << 25; - state ^= state >> 27; + state ^= state >> shiftA; + state ^= state << shiftB; + state ^= state >> shiftC; key[i] = state; } @@ -113,9 +118,9 @@ protected override void Execute(ConfuserContext context, ProtectionParameters pa moduleCtx.DataField.InitialValue = encryptedBuffer; moduleCtx.DataField.HasFieldRVA = true; moduleCtx.DataType.ClassLayout = new ClassLayoutUser(0, (uint)encryptedBuffer.Length); - MutationHelper.InjectKeys(moduleCtx.InitMethod, - new[] { 0, 1 }, - new[] { encryptedBuffer.Length / 4, (int)keySeed }); + MutationHelper.InjectKeysEnsured(moduleCtx.InitMethod, + new[] { 0, 1, 2, 3, 4 }, + new[] { encryptedBuffer.Length / 4, (int)keySeed, shiftA, shiftB, shiftC }); MutationHelper.ReplacePlaceholder(moduleCtx.InitMethod, arg => { var repl = new List(); repl.AddRange(arg); diff --git a/Confuser.Runtime/Constant.cs b/Confuser.Runtime/Constant.cs index cc64d3e81..6027b169d 100644 --- a/Confuser.Runtime/Constant.cs +++ b/Confuser.Runtime/Constant.cs @@ -13,9 +13,9 @@ static void Initialize() { var k = new uint[0x10]; var n = (uint)Mutation.KeyI1; for (int i = 0; i < 0x10; i++) { - n ^= n >> 12; - n ^= n << 25; - n ^= n >> 27; + n ^= n >> Mutation.KeyI2; + n ^= n << Mutation.KeyI3; + n ^= n >> Mutation.KeyI4; k[i] = n; } diff --git a/scripts/curate_xorshift.py b/scripts/curate_xorshift.py new file mode 100644 index 000000000..cfd8b1292 --- /dev/null +++ b/scripts/curate_xorshift.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Curate full-period xorshift32 triples for the Constants protection key generator. + +Constant.Initialize (runtime) and EncodePhase (obfuscator) both seed a 32-bit xorshift +of the exact form + + n ^= n >> a; n ^= n << b; n ^= n >> c; + +to expand a seed into the key stream. Both sides must use the same (a, b, c); the historic +fixed 12/25/27 fingerprints the stub. A replacement triple is only valid if the generator +still has full period 2**32 - 1 (otherwise the key stream degenerates). + +The step map is linear over GF(2), so the whole round is a 32x32 bit matrix M. The period +is the multiplicative order of M and equals 2**32 - 1 iff + + M**(2**32-1) == I and M**((2**32-1)/p) != I for every prime p | (2**32-1). + +2**32-1 = 3 * 5 * 17 * 257 * 65537. This is the standard primitivity test and is exact. +Emits Confuser.Protections/Constants/ConstantXorshift.cs. Deterministic; run from repo root: + python scripts/curate_xorshift.py +""" +import math +import os + +MASK = 0xFFFFFFFF +N = 32 +PERIOD = (1 << 32) - 1 +FACTORS = (3, 5, 17, 257, 65537) +IDENT = [1 << j for j in range(N)] +OUT = os.path.join(os.path.dirname(__file__), "..", "Confuser.Protections", "Constants", "ConstantXorshift.cs") + + +def step(n, a, b, c): + n &= MASK + n ^= n >> a + n ^= (n << b) & MASK + n ^= n >> c + return n & MASK + + +def apply(M, v): # matrix (columns) times bit-vector, over GF(2) + r = j = 0 + while v: + if v & 1: + r ^= M[j] + v >>= 1 + j += 1 + return r & MASK + + +def matmul(A, B): # apply B then A + return [apply(A, B[j]) for j in range(N)] + + +def matpow(M, e): + r = IDENT + while e: + if e & 1: + r = matmul(r, M) + M = matmul(M, M) + e >>= 1 + return r + + +def full_period(a, b, c): + M = [step(1 << j, a, b, c) for j in range(N)] # column j = image of basis vector e_j + if matpow(M, PERIOD) != IDENT: + return False + return all(matpow(M, PERIOD // p) != IDENT for p in FACTORS) + + +def fmt(triples): + rows = [] + for i in range(0, len(triples), 4): + rows.append("\t\t\t" + " ".join(f"new byte[] {{ {a}, {b}, {c} }}," for a, b, c in triples[i:i + 4])) + return "\n".join(rows).rstrip(",") + + +def main(): + # non-extreme shifts (2..30) give better avalanche over the 16-word expansion + good = [(a, b, c) + for a in range(2, N - 1) for b in range(2, N - 1) for c in range(2, N - 1) + if full_period(a, b, c)] + print(f"original 12/25/27 full-period: {full_period(12, 25, 27)}") + print(f"curated full-period (>>a,<>c) triples, shifts 2..30: {len(good)} " + f"({math.log2(len(good)):.1f} bits)") + + cs = f"""namespace Confuser.Protections.Constants {{ +\t/// +\t/// Curated full-period xorshift32 triples (a, b, c) for the constant key generator +\t/// n ^= n >> a; n ^= n << b; n ^= n >> c. One triple is chosen per module and injected +\t/// into the runtime Constant.Initialize (Mutation.KeyI2/KeyI3/KeyI4), replacing the fixed +\t/// 12/25/27 that de4dot/AV pattern-match. Both the obfuscator and the runtime use the same +\t/// triple, so the round-trip holds for any shifts; each listed triple is additionally a +\t/// full-period generator (period 2^32-1, verified by GF(2) matrix order) with non-extreme +\t/// shifts, so the expanded key stream is well mixed -- unlike the historic 12/25/27, which +\t/// is not full period. Generated by scripts/curate_xorshift.py. +\t/// +\tinternal static class ConstantXorshift {{ +\t\tinternal static readonly byte[][] Triples = {{ +{fmt(good)} +\t\t}}; +\t}} +}} +""" + with open(OUT, "w", newline="\n") as fh: + fh.write(cs) + print(f"wrote {os.path.normpath(OUT)}") + + +if __name__ == "__main__": + main()