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()