From 06a88dcd9b90f84d34de61d0e8ca61cf15b90c15 Mon Sep 17 00:00:00 2001 From: Melissa Eckardt Date: Mon, 30 Mar 2026 19:48:46 +0200 Subject: [PATCH 1/5] .NET Reactor: Fix CflowConstantsInliner for recent versions --- .../v4/CflowConstantsInliner.cs | 30 ++++--- .../dotNET_Reactor/v4/Deobfuscator.cs | 1 - .../dotNET_Reactor/v4/StringDecrypter.cs | 86 +------------------ 3 files changed, 18 insertions(+), 99 deletions(-) diff --git a/de4dot.code/deobfuscators/dotNET_Reactor/v4/CflowConstantsInliner.cs b/de4dot.code/deobfuscators/dotNET_Reactor/v4/CflowConstantsInliner.cs index 54f58dccb..d023bb6c7 100644 --- a/de4dot.code/deobfuscators/dotNET_Reactor/v4/CflowConstantsInliner.cs +++ b/de4dot.code/deobfuscators/dotNET_Reactor/v4/CflowConstantsInliner.cs @@ -38,18 +38,13 @@ void Find() { continue; if (i + 1 >= instrs.Count) continue; - var stsfld = instrs[i + 1]; - if (stsfld.OpCode.Code != Code.Stsfld) + var store = instrs[i + 1]; + if (store.OpCode.Code is not (Code.Stsfld or Code.Stfld)) continue; - var key = stsfld.Operand as FieldDef; - if (key == null) + if (store.Operand is not FieldDef key) continue; - var value = ldcI4.GetLdcI4Value(); - if (!dictionary.ContainsKey(key)) - dictionary.Add(key, value); - else - dictionary[key] = value; + dictionary[key] = ldcI4.GetLdcI4Value(); } if (dictionary.Count < 100) { @@ -76,14 +71,21 @@ public void InlineAllConstants() { var instrs = method.Body.Instructions; for (var i = 0; i < instrs.Count; i++) { - var ldsfld = instrs[i]; - if (ldsfld.OpCode.Code != Code.Ldsfld) + bool nopNext = false; + var load = instrs[i]; + if (load.OpCode.Code != Code.Ldsfld) continue; - var ldsfldValue = ldsfld.Operand as FieldDef; - if (ldsfldValue == null) + if (i < instrs.Count - 1 && instrs[i + 1].OpCode.Code == Code.Ldfld) { + load = instrs[i + 1]; + nopNext = true; + } + if (load.Operand is not FieldDef loadField) continue; - if (dictionary.TryGetValue(ldsfldValue, out var value)) + if (dictionary.TryGetValue(loadField, out var value)) { instrs[i] = Instruction.CreateLdcI4(value); + if (nopNext) + instrs[i + 1] = Instruction.Create(OpCodes.Nop); + } } } } diff --git a/de4dot.code/deobfuscators/dotNET_Reactor/v4/Deobfuscator.cs b/de4dot.code/deobfuscators/dotNET_Reactor/v4/Deobfuscator.cs index 64f7db4a9..74d7f5576 100644 --- a/de4dot.code/deobfuscators/dotNET_Reactor/v4/Deobfuscator.cs +++ b/de4dot.code/deobfuscators/dotNET_Reactor/v4/Deobfuscator.cs @@ -640,7 +640,6 @@ public override void DeobfuscateMethodEnd(Blocks blocks) { metadataTokenObfuscator.Deobfuscate(blocks); FixTypeofDecrypterInstructions(blocks); RemoveAntiStrongNameCode(blocks); - stringDecrypter.DeobfuscateXored(blocks); base.DeobfuscateMethodEnd(blocks); } diff --git a/de4dot.code/deobfuscators/dotNET_Reactor/v4/StringDecrypter.cs b/de4dot.code/deobfuscators/dotNET_Reactor/v4/StringDecrypter.cs index 610468fb1..70c6b194c 100644 --- a/de4dot.code/deobfuscators/dotNET_Reactor/v4/StringDecrypter.cs +++ b/de4dot.code/deobfuscators/dotNET_Reactor/v4/StringDecrypter.cs @@ -151,7 +151,8 @@ public void Initialize(MyPEImage peImage, byte[] fileData, ISimpleDeobfuscator s Logger.v("Adding string decrypter. Resource: {0}", Utils.ToCsharpString(encryptedResource.Resource.Name)); decryptedData = encryptedResource.Decrypt(); } - catch { + catch (Exception ex) { + Logger.w($"Initializing StringDecrypter failed: {ex.Message}"); encryptedResource.Method = null; decryptStrings = false; } @@ -245,88 +246,5 @@ public string Decrypt(MethodDef method, int offset) { } public string Decrypt(string s) => Encoding.Unicode.GetString(Convert.FromBase64String(s)); - - /** - * Determines whether a method has many fields that receive a constant int - * by checking the ratio between ldc.i4 and stfld. - */ - private bool IsConstantsInitializer(MethodDef method) { - int numLdcI4 = 0, numStfld = 0; - foreach (var ins in method.Body.Instructions) { - if (ins.IsLdcI4()) { - numLdcI4++; - } else if (ins.OpCode.Code == Code.Stfld) { - numStfld++; - } - } - - return numStfld > 5 && numLdcI4 / (double)numStfld > 0.9; - } - - /** - * Creates a mapping of field name -> value for int assignments. - */ - private Dictionary GetConstantFields(MethodDef method) { - var result = new Dictionary(); - - int val = 0; - foreach (var ins in method.Body.Instructions) { - if (ins.IsLdcI4()) { - val = ins.GetLdcI4Value(); - } else if (ins.OpCode.Code == Code.Stfld) { - result[((IField)ins.Operand).Name] = val; - } - } - - return result; - } - - private Dictionary LoadConstantFields(TypeDef type) { - foreach (var method in type.Methods) { - if (IsConstantsInitializer(method)) { - return GetConstantFields(method); - } - } - - return null; - } - - /** - * Replaces all references to the string decryption function by a load of the respective decrypted string. - * This is for special cases where de4dot's normal static decryptor is unable to obtain the offset constants. - */ - public void DeobfuscateXored(Blocks blocks) { - if (decrypterInfos.Count != 1) return; - - foreach (var block in blocks.MethodBlocks.GetAllBlocks()) { - var instrs = block.Instructions; - for (int i = 4; i < instrs.Count; i++) { - var instr = instrs[i]; - - if (instr.OpCode != OpCodes.Call || instr.Operand != decrypterInfos[0].method) - continue; - - if (instrs[i - 1].OpCode != OpCodes.Xor - || instrs[i - 2].OpCode != OpCodes.Ldfld - || instrs[i - 3].OpCode != OpCodes.Ldsfld - || instrs[i - 4].OpCode != OpCodes.Ldc_I4) - continue; - - int xorConst = instrs[i - 4].GetLdcI4Value(); - if (constantFields == null) { - constantFields = LoadConstantFields(((FieldDef)instrs[i - 3].Operand).DeclaringType); - if (constantFields == null) { - Logger.w("Failed to load constant fields"); - return; - } - } - int xorField = constantFields[((FieldDef)instrs[i - 2].Operand).Name]; - int offset = xorConst ^ xorField; - - var decryptedString = Decrypt(decrypterInfos[0].method, offset); - block.Replace(i - 4, 5, OpCodes.Ldstr.ToInstruction(decryptedString)); - } - } - } } } From f088d4a32c4526b18beafc9f3b866768778a8757 Mon Sep 17 00:00:00 2001 From: Melissa Eckardt Date: Mon, 30 Mar 2026 19:56:22 +0200 Subject: [PATCH 2/5] .NET Reactor: Fix string/resource decryption for v6.9+ --- .../dotNET_Reactor/v4/EncryptedResource.cs | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/de4dot.code/deobfuscators/dotNET_Reactor/v4/EncryptedResource.cs b/de4dot.code/deobfuscators/dotNET_Reactor/v4/EncryptedResource.cs index 6a091c059..2689bbc12 100644 --- a/de4dot.code/deobfuscators/dotNET_Reactor/v4/EncryptedResource.cs +++ b/de4dot.code/deobfuscators/dotNET_Reactor/v4/EncryptedResource.cs @@ -555,8 +555,10 @@ bool Initialize() { int count = emuEndIndex - emuStartIndex + 1; instructions = new List(count); - for (int i = 0; i < count; i++) - instructions.Add(origInstrs[emuStartIndex + i].Clone()); + for (int i = 0; i < count; i++) { + var ins = origInstrs[emuStartIndex + i]; + instructions.Add(ins.Clone()); + } return true; } @@ -702,8 +704,30 @@ uint CalculateMagic(uint input) { instrEmulator.Initialize(method, method.Parameters, locals, method.Body.InitLocals, false); instrEmulator.SetLocal(emuLocal, new Int32Value((int)input)); - foreach (var instr in instructions) - instrEmulator.Emulate(instr); + foreach (var instr in instructions) { + if (instr.OpCode == OpCodes.Bne_Un || instr.OpCode == OpCodes.Bne_Un_S) { + /* The emulator doesn't handle branches, and some DNR builds have a zero-check branch gating a division + // if (num12 == 0U) + /* 0x00002E98 FE0C2900 * / IL_01E4: ldloc V_41 + /* 0x00002E9C 16 * / IL_01E8: ldc.i4.0 + /* 0x00002E9D 400A000000 * / IL_01E9: bne.un IL_01F8 + // num12 -= 1U; + /* 0x00002EA2 FE0C2900 * / IL_01EE: ldloc V_41 + /* 0x00002EA6 17 * / IL_01F2: ldc.i4.1 + /* 0x00002EA7 59 * / IL_01F3: sub + /* 0x00002EA8 FE0E2900 * / IL_01F4: stloc V_41 + */ + var rhs = instrEmulator.Pop(); + var lhs = instrEmulator.Pop(); + if (rhs is Int32Value rhsInt && lhs is Int32Value lhsInt && rhsInt.IsZero() && lhsInt.IsZero()) { + var local = (Local)instructions[instructions.IndexOf(instr) - 2].Operand; + instrEmulator.SetLocal(local, new Int32Value(-1)); + } + } + else { + instrEmulator.Emulate(instr); + } + } var tos = instrEmulator.Pop() as Int32Value; if (tos == null || !tos.AllBitsValid()) From 9a14be6ba8342dd8f1d70d0ea17397c466065866 Mon Sep 17 00:00:00 2001 From: Melissa Eckardt Date: Mon, 30 Mar 2026 19:57:09 +0200 Subject: [PATCH 3/5] .NET Reactor: Fix switch deobfuscation for v7.0+ --- de4dot.blocks/cflow/CflowUtils.cs | 32 ++++++++++++++++++- de4dot.code/ObfuscatedFile.cs | 8 ++++- .../v4/DotNetReactorCflowDeobfuscator.cs | 15 +++------ 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/de4dot.blocks/cflow/CflowUtils.cs b/de4dot.blocks/cflow/CflowUtils.cs index 99da35796..f3d6bd668 100644 --- a/de4dot.blocks/cflow/CflowUtils.cs +++ b/de4dot.blocks/cflow/CflowUtils.cs @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License */ using System.Collections.Generic; +using dnlib.DotNet.Emit; namespace de4dot.blocks.cflow { static class CflowUtils { @@ -27,9 +28,38 @@ static class CflowUtils { int index = intValue.Value; if (targets == null || index < 0 || index >= targets.Count) - return fallThrough; + return TryResolveConditionalFallthrough(fallThrough, intValue); else return targets[index]; } + + /** + * Some obfuscators have default blocks that dispatch additional cases. + * Example: 4e86d71a19f7f69471776817dc67585064b4b60542bc60e9450739bca63226ee + */ + private static Block? TryResolveConditionalFallthrough(Block? block, Int32Value value) { + while (true) { + if (block == null) return null; + + var instrs = block.Instructions; + if (instrs.Count < 3) return block; + if (!instrs[0].IsLdloc()) return block; + if (!instrs[1].IsLdcI4()) return block; + if (instrs[2].OpCode.Code is not (Code.Beq or Code.Beq_S or Code.Bne_Un or Code.Bne_Un_S)) return block; + + int constant = instrs[1].GetLdcI4Value(); + var branch = instrs[2]; + + int v = value.Value; + + bool taken = + branch.OpCode.Code is Code.Beq or Code.Beq_S ? (v == constant) : + branch.OpCode.Code is Code.Bne_Un or Code.Bne_Un_S && (v != constant); + + if (taken) + return block.Targets![0]; + block = block.FallThrough; + } + } } } diff --git a/de4dot.code/ObfuscatedFile.cs b/de4dot.code/ObfuscatedFile.cs index 72b2bcdf7..dc76ee201 100644 --- a/de4dot.code/ObfuscatedFile.cs +++ b/de4dot.code/ObfuscatedFile.cs @@ -609,7 +609,13 @@ void Deobfuscate(MethodDef method, BlocksCflowDeobfuscator cflowDeobfuscator, Me deob.DeobfuscateMethodBegin(blocks); if (options.ControlFlowDeobfuscation) { cflowDeobfuscator.Initialize(blocks); - cflowDeobfuscator.Deobfuscate(); + try { + cflowDeobfuscator.Deobfuscate(); + } + catch (Exception) { + Logger.e("Error during cflow deobfuscation of {0} ({1:X8})", Utils.RemoveNewlines(method), method.MDToken.ToUInt32()); + throw; + } } if (deob.DeobfuscateOther(blocks) && options.ControlFlowDeobfuscation) diff --git a/de4dot.code/deobfuscators/dotNET_Reactor/v4/DotNetReactorCflowDeobfuscator.cs b/de4dot.code/deobfuscators/dotNET_Reactor/v4/DotNetReactorCflowDeobfuscator.cs index 66b858df4..aef4a6a7d 100644 --- a/de4dot.code/deobfuscators/dotNET_Reactor/v4/DotNetReactorCflowDeobfuscator.cs +++ b/de4dot.code/deobfuscators/dotNET_Reactor/v4/DotNetReactorCflowDeobfuscator.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using de4dot.blocks; using de4dot.blocks.cflow; using dnlib.DotNet; @@ -6,24 +7,16 @@ namespace de4dot.code.deobfuscators.dotNET_Reactor.v4 { class DotNetReactorCflowDeobfuscator : IBlocksDeobfuscator { - bool isContainsSwitch; + bool _hasSwitch; public bool ExecuteIfNotModified { get; } public void DeobfuscateBegin(Blocks blocks) { - var contains = false; - foreach (var instr in blocks.Method.Body.Instructions) { - if (instr.OpCode == OpCodes.Switch) { - contains = true; - break; - } - } - - isContainsSwitch = contains; + _hasSwitch = blocks.Method.Body.Instructions.Any(instr => instr.OpCode == OpCodes.Switch); } public bool Deobfuscate(List allBlocks) { - if (!isContainsSwitch) + if (!_hasSwitch) return false; var modified = false; From b1a65b48a9dd21c4bb1a674c6263e08988dbc6d5 Mon Sep 17 00:00:00 2001 From: Melissa Eckardt Date: Tue, 31 Mar 2026 11:44:45 +0200 Subject: [PATCH 4/5] .NET Reactor: Implement devirtualization --- .../dotNET_Reactor/v4/Deobfuscator.cs | 28 +- .../dotNET_Reactor/v4/vm/Devirtualizer.cs | 185 +++++ .../dotNET_Reactor/v4/vm/HandlerMapper.cs | 178 +++++ .../dotNET_Reactor/v4/vm/IPattern.cs | 53 ++ .../dotNET_Reactor/v4/vm/PatternHelper.cs | 78 ++ .../dotNET_Reactor/v4/vm/PatternMatcher.cs | 115 +++ .../dotNET_Reactor/v4/vm/Patterns.cs | 718 ++++++++++++++++++ .../v4/vm/PatternsArithmetic.cs | 621 +++++++++++++++ .../dotNET_Reactor/v4/vm/PatternsBranch.cs | 411 ++++++++++ .../dotNET_Reactor/v4/vm/PatternsConv.cs | 546 +++++++++++++ .../dotNET_Reactor/v4/vm/PatternsElem.cs | 141 ++++ .../dotNET_Reactor/v4/vm/ResourceParser.cs | 180 +++++ .../dotNET_Reactor/v4/vm/VMElemType.cs | 73 ++ .../v4/vm/VMExceptionHandler.cs | 38 + .../dotNET_Reactor/v4/vm/VMHandler.cs | 39 + .../dotNET_Reactor/v4/vm/VMInstruction.cs | 38 + .../dotNET_Reactor/v4/vm/VMMethod.cs | 164 ++++ 17 files changed, 3605 insertions(+), 1 deletion(-) create mode 100644 de4dot.code/deobfuscators/dotNET_Reactor/v4/vm/Devirtualizer.cs create mode 100644 de4dot.code/deobfuscators/dotNET_Reactor/v4/vm/HandlerMapper.cs create mode 100644 de4dot.code/deobfuscators/dotNET_Reactor/v4/vm/IPattern.cs create mode 100644 de4dot.code/deobfuscators/dotNET_Reactor/v4/vm/PatternHelper.cs create mode 100644 de4dot.code/deobfuscators/dotNET_Reactor/v4/vm/PatternMatcher.cs create mode 100644 de4dot.code/deobfuscators/dotNET_Reactor/v4/vm/Patterns.cs create mode 100644 de4dot.code/deobfuscators/dotNET_Reactor/v4/vm/PatternsArithmetic.cs create mode 100644 de4dot.code/deobfuscators/dotNET_Reactor/v4/vm/PatternsBranch.cs create mode 100644 de4dot.code/deobfuscators/dotNET_Reactor/v4/vm/PatternsConv.cs create mode 100644 de4dot.code/deobfuscators/dotNET_Reactor/v4/vm/PatternsElem.cs create mode 100644 de4dot.code/deobfuscators/dotNET_Reactor/v4/vm/ResourceParser.cs create mode 100644 de4dot.code/deobfuscators/dotNET_Reactor/v4/vm/VMElemType.cs create mode 100644 de4dot.code/deobfuscators/dotNET_Reactor/v4/vm/VMExceptionHandler.cs create mode 100644 de4dot.code/deobfuscators/dotNET_Reactor/v4/vm/VMHandler.cs create mode 100644 de4dot.code/deobfuscators/dotNET_Reactor/v4/vm/VMInstruction.cs create mode 100644 de4dot.code/deobfuscators/dotNET_Reactor/v4/vm/VMMethod.cs diff --git a/de4dot.code/deobfuscators/dotNET_Reactor/v4/Deobfuscator.cs b/de4dot.code/deobfuscators/dotNET_Reactor/v4/Deobfuscator.cs index 74d7f5576..7102f70af 100644 --- a/de4dot.code/deobfuscators/dotNET_Reactor/v4/Deobfuscator.cs +++ b/de4dot.code/deobfuscators/dotNET_Reactor/v4/Deobfuscator.cs @@ -25,6 +25,7 @@ You should have received a copy of the GNU General Public License using dnlib.DotNet.Writer; using de4dot.blocks; using de4dot.blocks.cflow; +using de4dot.code.deobfuscators.dotNET_Reactor.v4.vm; namespace de4dot.code.deobfuscators.dotNET_Reactor.v4 { public class DeobfuscatorInfo : DeobfuscatorInfoBase { @@ -42,6 +43,7 @@ public class DeobfuscatorInfo : DeobfuscatorInfoBase { BoolOption removeNamespaces; BoolOption removeAntiStrongName; BoolOption renameShort; + BoolOption devirtualize; public DeobfuscatorInfo() : base(DEFAULT_REGEX) { @@ -55,6 +57,7 @@ public DeobfuscatorInfo() removeNamespaces = new BoolOption(null, MakeArgName("ns1"), "Clear namespace if there's only one class in it", true); removeAntiStrongName = new BoolOption(null, MakeArgName("sn"), "Remove anti strong name code", true); renameShort = new BoolOption(null, MakeArgName("sname"), "Rename short names", false); + devirtualize = new BoolOption(null, MakeArgName("devirtualize"), "Devirtualize methods", true); } public override string Name => THE_NAME; @@ -73,6 +76,7 @@ public override IDeobfuscator CreateDeobfuscator() => RemoveNamespaces = removeNamespaces.Get(), RemoveAntiStrongName = removeAntiStrongName.Get(), RenameShort = renameShort.Get(), + Devirtualize = devirtualize.Get(), }); protected override IEnumerable