From 7d747ca2886c5427d14ba614774ec1fc73480b44 Mon Sep 17 00:00:00 2001 From: RandomCrocodile Date: Sat, 4 Jul 2026 20:39:00 +0200 Subject: [PATCH 1/3] feature: randomize compressor feedback constant per run (#69) The compressor packer baked the fixed feedback 0x3ddb2819 into both the encryption loop (CompressorContext.Encrypt) and the injected runtime Decrypt method, giving de4dot a stable signature for packed output. Generate a random feedback per pack, use it in Encrypt for both the main module and every embedded library, and rewrite the literal in the runtime Decrypt IL so encrypt/decrypt stay in sync. Validated end-to-end by CompressorWithResx.Test (12 cases: compat x deriver x resource modes, covering both Compressor and CompressorCompat runtimes). --- Confuser.Protections/Compress/Compressor.cs | 11 ++++++++++- Confuser.Protections/Compress/CompressorContext.cs | 3 ++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Confuser.Protections/Compress/Compressor.cs b/Confuser.Protections/Compress/Compressor.cs index ca81339db..de28dd08a 100644 --- a/Confuser.Protections/Compress/Compressor.cs +++ b/Confuser.Protections/Compress/Compressor.cs @@ -242,6 +242,11 @@ void InjectStub(ConfuserContext context, CompressorContext compCtx, ProtectionPa new MemberRefUser(stubModule, ".ctor", ctorSig, attrType))); } + // Randomize the encryption feedback constant (baked-in 0x3ddb2819 fingerprints the + // stub). Any value works since it is purely additive; the same value is injected into + // the runtime Decrypt method below so encrypt/decrypt stay in sync. + compCtx.Feedback = random.NextUInt32(); + uint seed = random.NextUInt32(); compCtx.OriginModule = context.OutputModules[compCtx.ModuleIndex]; @@ -263,7 +268,11 @@ void InjectStub(ConfuserContext context, CompressorContext compCtx, ProtectionPa List instrs = decrypter.Body.Instructions.ToList(); for (int i = 0; i < instrs.Count; i++) { Instruction instr = instrs[i]; - if (instr.OpCode == OpCodes.Call) { + if (instr.OpCode == OpCodes.Ldc_I4 && (int)instr.Operand == 0x3ddb2819) { + // Sync the runtime feedback constant with the value used during encryption. + instr.Operand = (int)compCtx.Feedback; + } + else if (instr.OpCode == OpCodes.Call) { var method = (IMethod)instr.Operand; if (method.DeclaringType.Name == "Mutation" && method.Name == "Crypt") { diff --git a/Confuser.Protections/Compress/CompressorContext.cs b/Confuser.Protections/Compress/CompressorContext.cs index 0e0b11ae9..a84a44aa7 100644 --- a/Confuser.Protections/Compress/CompressorContext.cs +++ b/Confuser.Protections/Compress/CompressorContext.cs @@ -19,6 +19,7 @@ internal class CompressorContext { public byte[] OriginModule; public ModuleDef OriginModuleDef; public bool CompatMode; + public uint Feedback; public byte[] Encrypt(ICompressionService compress, byte[] data, uint seed, Action progressFunc) { data = (byte[])data.Clone(); @@ -46,7 +47,7 @@ public byte[] Encrypt(ICompressionService compress, byte[] data, uint seed, Acti for (int i = 0; i < data.Length; i += 4) { var datum = (uint)(data[i + 0] | (data[i + 1] << 8) | (data[i + 2] << 16) | (data[i + 3] << 24)); uint encrypted = datum ^ key[keyIndex & 0xf]; - key[keyIndex & 0xf] = (key[keyIndex & 0xf] ^ datum) + 0x3ddb2819; + key[keyIndex & 0xf] = (key[keyIndex & 0xf] ^ datum) + Feedback; encryptedData[i + 0] = (byte)(encrypted >> 0); encryptedData[i + 1] = (byte)(encrypted >> 8); encryptedData[i + 2] = (byte)(encrypted >> 16); From 322c518c8571ee0b9252e35ef154359280123489 Mon Sep 17 00:00:00 2001 From: RandomCrocodile Date: Sat, 4 Jul 2026 20:42:00 +0200 Subject: [PATCH 2/3] feature: randomize compressor library-seed hash constants per run (#69) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rolling hash that derives each embedded library's decryption seed from its name used fixed constants 0x6fff61 (init) and 0x5e3f1f (multiplier) in both PackModules and the injected runtime Resolve method — another de4dot signature. Generate a random init and a random odd multiplier per pack, use them in PackModules, and rewrite the literals in the runtime Resolve IL so the seed derivation stays in sync. Validated by CompressorWithResx.Test, whose 'de' satellite assembly is resolved+decrypted through this exact path at runtime (asserts 'Test (deutsch)' across all 12 cases). --- Confuser.Protections/Compress/Compressor.cs | 22 +++++++++++++++++-- .../Compress/CompressorContext.cs | 2 ++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Confuser.Protections/Compress/Compressor.cs b/Confuser.Protections/Compress/Compressor.cs index de28dd08a..8b1290ca7 100644 --- a/Confuser.Protections/Compress/Compressor.cs +++ b/Confuser.Protections/Compress/Compressor.cs @@ -160,9 +160,9 @@ void PackModules(ConfuserContext context, CompressorContext compCtx, ModuleDef s for (int i = 0; i < name.Length; i++) name[i] *= key[i + 4]; - uint state = 0x6fff61; + uint state = compCtx.LcgInit; foreach (byte chr in name) - state = state * 0x5e3f1f + chr; + state = state * compCtx.LcgMultiplier + chr; byte[] encrypted = compCtx.Encrypt(comp, entry.Value, state, progress => { progress = (progress + moduleIndex) / modules.Count; context.ProgressReporter.Progress((int)(progress * 10000), 10000); @@ -247,6 +247,12 @@ void InjectStub(ConfuserContext context, CompressorContext compCtx, ProtectionPa // the runtime Decrypt method below so encrypt/decrypt stay in sync. compCtx.Feedback = random.NextUInt32(); + // Randomize the rolling-hash used to derive each library's seed from its name + // (baked-in 0x6fff61 / 0x5e3f1f). The multiplier is kept odd for good distribution. + // The same values are injected into the runtime Resolve method below. + compCtx.LcgInit = random.NextUInt32(); + compCtx.LcgMultiplier = random.NextUInt32() | 1; + uint seed = random.NextUInt32(); compCtx.OriginModule = context.OutputModules[compCtx.ModuleIndex]; @@ -295,6 +301,18 @@ void InjectStub(ConfuserContext context, CompressorContext compCtx, ProtectionPa foreach (Instruction instr in instrs) decrypter.Body.Instructions.Add(instr); + // Resolve — sync the runtime rolling-hash constants with the values used when + // deriving each library's seed from its name (see PackModules). + MethodDef resolver = defs.OfType().Single(method => method.Name == "Resolve"); + foreach (Instruction instr in resolver.Body.Instructions) { + if (instr.OpCode != OpCodes.Ldc_I4) + continue; + if ((int)instr.Operand == 0x6fff61) + instr.Operand = (int)compCtx.LcgInit; + else if ((int)instr.Operand == 0x5e3f1f) + instr.Operand = (int)compCtx.LcgMultiplier; + } + // Pack modules PackModules(context, compCtx, stubModule, comp, random); } diff --git a/Confuser.Protections/Compress/CompressorContext.cs b/Confuser.Protections/Compress/CompressorContext.cs index a84a44aa7..4500d0dfa 100644 --- a/Confuser.Protections/Compress/CompressorContext.cs +++ b/Confuser.Protections/Compress/CompressorContext.cs @@ -20,6 +20,8 @@ internal class CompressorContext { public ModuleDef OriginModuleDef; public bool CompatMode; public uint Feedback; + public uint LcgInit; + public uint LcgMultiplier; public byte[] Encrypt(ICompressionService compress, byte[] data, uint seed, Action progressFunc) { data = (byte[])data.Clone(); From c21435940a7a87edf6d541b1222699928764f3f6 Mon Sep 17 00:00:00 2001 From: RandomCrocodile Date: Mon, 21 Sep 2026 19:24:25 +0200 Subject: [PATCH 3/3] refactor: inject compressor constants via mutation placeholders (#69) Replace the magic-literal find/replace used to sync the randomized feedback and rolling-hash constants with the native Mutation.KeyI* placeholder mechanism. The runtime Decrypt/Resolve methods now expose the constants as Mutation.KeyI0/KeyI1 placeholders instead of baked-in hex literals, and the injector uses MutationHelper plus a presence check that throws a ConfuserException if an expected placeholder is missing. A future change to the runtime source can no longer silently desync encrypt/decrypt; it fails the build instead. Validated by CompressorWithResx.Test (12/12). --- Confuser.Protections/Compress/Compressor.cs | 62 ++++++++++++++------- Confuser.Runtime/Compressor.Compat.cs | 6 +- Confuser.Runtime/Compressor.cs | 6 +- 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/Confuser.Protections/Compress/Compressor.cs b/Confuser.Protections/Compress/Compressor.cs index d4143d30d..40800f27c 100644 --- a/Confuser.Protections/Compress/Compressor.cs +++ b/Confuser.Protections/Compress/Compressor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Runtime.CompilerServices; @@ -242,14 +243,14 @@ void InjectStub(ConfuserContext context, CompressorContext compCtx, ProtectionPa new MemberRefUser(stubModule, ".ctor", ctorSig, attrType))); } - // Randomize the encryption feedback constant (baked-in 0x3ddb2819 fingerprints the - // stub). Any value works since it is purely additive; the same value is injected into - // the runtime Decrypt method below so encrypt/decrypt stay in sync. + // Randomize the encryption feedback constant (a fixed value in the stub is a + // fingerprint). Any value works since it is purely additive; the same value is injected + // into the runtime Decrypt method below (Mutation.KeyI0) so encrypt/decrypt stay in sync. compCtx.Feedback = random.NextUInt32(); - // Randomize the rolling-hash used to derive each library's seed from its name - // (baked-in 0x6fff61 / 0x5e3f1f). The multiplier is kept odd for good distribution. - // The same values are injected into the runtime Resolve method below. + // Randomize the rolling-hash used to derive each library's seed from its name. The + // multiplier is kept odd for good distribution. The same values are injected into the + // runtime Resolve method below (Mutation.KeyI0 = init, Mutation.KeyI1 = multiplier). compCtx.LcgInit = random.NextUInt32(); compCtx.LcgMultiplier = random.NextUInt32() | 1; @@ -274,11 +275,7 @@ void InjectStub(ConfuserContext context, CompressorContext compCtx, ProtectionPa List instrs = decrypter.Body.Instructions.ToList(); for (int i = 0; i < instrs.Count; i++) { Instruction instr = instrs[i]; - if (instr.OpCode == OpCodes.Ldc_I4 && (int)instr.Operand == 0x3ddb2819) { - // Sync the runtime feedback constant with the value used during encryption. - instr.Operand = (int)compCtx.Feedback; - } - else if (instr.OpCode == OpCodes.Call) { + if (instr.OpCode == OpCodes.Call) { var method = (IMethod)instr.Operand; if (method.DeclaringType.Name == "Mutation" && method.Name == "Crypt") { @@ -301,22 +298,45 @@ void InjectStub(ConfuserContext context, CompressorContext compCtx, ProtectionPa foreach (Instruction instr in instrs) decrypter.Body.Instructions.Add(instr); - // Resolve — sync the runtime rolling-hash constants with the values used when - // deriving each library's seed from its name (see PackModules). + // 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 rolling-hash constants (init + odd multiplier) used to derive each + // library's seed from its name (see PackModules) with the Mutation.KeyI0 / KeyI1 + // placeholders the runtime Resolve exposes. MethodDef resolver = defs.OfType().Single(method => method.Name == "Resolve"); - foreach (Instruction instr in resolver.Body.Instructions) { - if (instr.OpCode != OpCodes.Ldc_I4) - continue; - if ((int)instr.Operand == 0x6fff61) - instr.Operand = (int)compCtx.LcgInit; - else if ((int)instr.Operand == 0x5e3f1f) - instr.Operand = (int)compCtx.LcgMultiplier; - } + InjectStubKeys(context, resolver, new[] { 0, 1 }, + new[] { (int)compCtx.LcgInit, (int)compCtx.LcgMultiplier }); // Pack modules PackModules(context, compCtx, stubModule, comp, random); } + // Injects mutation key literals into an injected runtime stub method and first verifies + // every expected Mutation.KeyI* placeholder is actually present. If the runtime source is + // changed so a placeholder is removed or renamed, this throws instead of silently emitting + // a stub whose baked-in constants no longer match the obfuscator side. + static void InjectStubKeys(ConfuserContext context, MethodDef method, int[] keyIds, int[] values) { + var missing = new HashSet( + keyIds.Select(id => "KeyI" + id.ToString(CultureInfo.InvariantCulture))); + foreach (Instruction instr in method.Body.Instructions) { + if (instr.OpCode == OpCodes.Ldsfld && instr.Operand is IField field && + field.DeclaringType?.FullName == "Mutation") + missing.Remove(field.Name); + } + if (missing.Count != 0) { + context.Logger.LogError( + "Compressor stub is out of sync: runtime method '{Method}' is missing expected mutation placeholder(s) {Missing}. The runtime source and the injector must be updated together.", + method.Name, string.Join(", ", missing.OrderBy(name => name, StringComparer.Ordinal))); + throw new ConfuserException(null); + } + + MutationHelper.InjectKeys(method, keyIds, values); + } + void ImportAssemblyTypeReferences(ModuleDef originModule, ModuleDef stubModule) { var assembly = stubModule.Assembly; foreach (var ca in assembly.CustomAttributes) { diff --git a/Confuser.Runtime/Compressor.Compat.cs b/Confuser.Runtime/Compressor.Compat.cs index 3dbe11874..ec2225939 100644 --- a/Confuser.Runtime/Compressor.Compat.cs +++ b/Confuser.Runtime/Compressor.Compat.cs @@ -23,7 +23,7 @@ static byte[] Decrypt(uint[] data, uint seed) { uint h = 0; for (int i = 0; i < data.Length; i++) { uint d = data[i] ^ w[i & 0xf]; - w[i & 0xf] = (w[i & 0xf] ^ d) + 0x3ddb2819; + w[i & 0xf] = (w[i & 0xf] ^ d) + (uint)Mutation.KeyI0; b[h + 0] = (byte)(d >> 0); b[h + 1] = (byte)(d >> 8); b[h + 2] = (byte)(d >> 16); @@ -86,9 +86,9 @@ static Assembly Resolve(object sender, ResolveEventArgs e) { Buffer.BlockCopy(t, 0, d, o, r); o += r; } - uint s = 0x6fff61; + uint s = (uint)Mutation.KeyI0; foreach (byte c in b) - s = s * 0x5e3f1f + c; + s = s * (uint)Mutation.KeyI1 + c; byte[] f = Decrypt(d, s); Assembly a = Assembly.Load(f); Array.Clear(f, 0, f.Length); diff --git a/Confuser.Runtime/Compressor.cs b/Confuser.Runtime/Compressor.cs index 5e3d3b09b..b69e0fe12 100644 --- a/Confuser.Runtime/Compressor.cs +++ b/Confuser.Runtime/Compressor.cs @@ -23,7 +23,7 @@ static byte[] Decrypt(uint[] data, uint seed) { uint h = 0; for (int i = 0; i < data.Length; i++) { uint d = data[i] ^ w[i & 0xf]; - w[i & 0xf] = (w[i & 0xf] ^ d) + 0x3ddb2819; + w[i & 0xf] = (w[i & 0xf] ^ d) + (uint)Mutation.KeyI0; b[h + 0] = (byte)(d >> 0); b[h + 1] = (byte)(d >> 8); b[h + 2] = (byte)(d >> 16); @@ -92,9 +92,9 @@ static Assembly Resolve(object sender, ResolveEventArgs e) { Buffer.BlockCopy(t, 0, d, o, r); o += r; } - uint s = 0x6fff61; + uint s = (uint)Mutation.KeyI0; foreach (byte c in b) - s = s * 0x5e3f1f + c; + s = s * (uint)Mutation.KeyI1 + c; byte[] f = Decrypt(d, s); Assembly a = Assembly.Load(f); Array.Clear(f, 0, f.Length);