diff --git a/de4dot.blocks/DotNetUtils.cs b/de4dot.blocks/DotNetUtils.cs index 0aa2d080..347648ae 100644 --- a/de4dot.blocks/DotNetUtils.cs +++ b/de4dot.blocks/DotNetUtils.cs @@ -277,7 +277,7 @@ public static IEnumerable GetNormalMethods(TypeDef type) { return null; } - public static IEnumerable GetMethodCalls(MethodDef method) { + public static List GetMethodCalls(MethodDef method) { var list = new List(); if (method.HasBody) { foreach (var instr in method.Body.Instructions) { diff --git a/de4dot.blocks/cflow/InstructionAndBranchEmulator.cs b/de4dot.blocks/cflow/InstructionAndBranchEmulator.cs new file mode 100644 index 00000000..f657fa27 --- /dev/null +++ b/de4dot.blocks/cflow/InstructionAndBranchEmulator.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using dnlib.DotNet; +using dnlib.DotNet.Emit; + +namespace de4dot.blocks.cflow; + +public class InstructionAndBranchEmulator : IBranchHandler { + public InstructionEmulator Emulator { get; } + readonly BranchEmulator _branchEmulator; + readonly IList _instructions; + + protected int InsnIndex; + + public InstructionAndBranchEmulator(MethodDef method) { + Emulator = new InstructionEmulator(method); + _branchEmulator = new BranchEmulator(Emulator, this); + _instructions = method.Body.Instructions; + } + + public Value? Run(int startIndex = 0, params int[] @params) { + int x = 0; + foreach (int param in @params) + Emulator.SetArg(new Parameter(x++), new Int32Value(param)); + + for (InsnIndex = startIndex; InsnIndex < _instructions.Count; InsnIndex++) { + var instr = _instructions[InsnIndex]; + + if (!OnInstruction(instr, out var shouldSkip)) + return null; + if (shouldSkip) + continue; + + if (instr.OpCode == OpCodes.Ret) + return Emulator.Pop(); + + if (instr.OpCode.FlowControl is FlowControl.Branch or FlowControl.Cond_Branch) { + if (_branchEmulator.Emulate(instr)) + InsnIndex--; // Loop update will do ++, which is bad + else + return null; + } + else + Emulator.Emulate(instr); + } + + return null; + } + + /// + /// Called before an instruction is processed. If false is returned, emulation is stopped. + /// + /// The next instruction to be emulated. + /// Whether to continue with the next instruction without passing this one to + /// the emulator. + /// Whether to continue emulation. + protected virtual bool OnInstruction(Instruction instr, out bool shouldSkip) { + shouldSkip = false; + return true; + } + + void IBranchHandler.HandleNormal(int stackArgs, bool isTaken) { + if (!isTaken) + InsnIndex++; + else + InsnIndex = _instructions.IndexOf((Instruction)_instructions[InsnIndex].Operand); + } + + bool IBranchHandler.HandleSwitch(Int32Value switchIndex) { + if (!switchIndex.AllBitsValid()) + return false; + var instr = _instructions[InsnIndex]; + var targets = (Instruction[])instr.Operand; + if (switchIndex.Value >= 0 && switchIndex.Value < targets.Length) + InsnIndex = _instructions.IndexOf(targets[switchIndex.Value]); + else + InsnIndex++; + return true; + } +} diff --git a/de4dot.blocks/cflow/InstructionEmulator.cs b/de4dot.blocks/cflow/InstructionEmulator.cs index c8337f9f..d7492c63 100644 --- a/de4dot.blocks/cflow/InstructionEmulator.cs +++ b/de4dot.blocks/cflow/InstructionEmulator.cs @@ -367,12 +367,7 @@ public void Emulate(Instruction instr) { case Code.Conv_Ovf_U8: Emulate_Conv_Ovf_U8(instr); break; case Code.Conv_Ovf_U8_Un: Emulate_Conv_Ovf_U8_Un(instr); break; - case Code.Ldelem_I1: valueStack.Pop(2); valueStack.Push(Int32Value.CreateUnknown()); break; - case Code.Ldelem_I2: valueStack.Pop(2); valueStack.Push(Int32Value.CreateUnknown()); break; case Code.Ldelem_I8: valueStack.Pop(2); valueStack.Push(Int64Value.CreateUnknown()); break; - case Code.Ldelem_U1: valueStack.Pop(2); valueStack.Push(Int32Value.CreateUnknownUInt8()); break; - case Code.Ldelem_U2: valueStack.Pop(2); valueStack.Push(Int32Value.CreateUnknownUInt16()); break; - case Code.Ldelem_U4: valueStack.Pop(2); valueStack.Push(Int32Value.CreateUnknown()); break; case Code.Ldelem: valueStack.Pop(2); valueStack.Push(GetUnknownValue(instr.Operand as ITypeDefOrRef)); break; case Code.Ldind_I1: valueStack.Pop(); valueStack.Push(Int32Value.CreateUnknown()); break; @@ -383,7 +378,7 @@ public void Emulate(Instruction instr) { case Code.Ldind_U2: valueStack.Pop(); valueStack.Push(Int32Value.CreateUnknownUInt16()); break; case Code.Ldind_U4: valueStack.Pop(); valueStack.Push(Int32Value.CreateUnknown()); break; - case Code.Ldlen: valueStack.Pop(); valueStack.Push(Int32Value.CreateUnknown()); break; + case Code.Ldlen: Emulate_Ldlen(instr); break; case Code.Sizeof: Emulate_Sizeof(instr); break; case Code.Ldfld: Emulate_Ldfld(instr); break; @@ -402,8 +397,15 @@ public void Emulate(Instruction instr) { case Code.Conv_R8: Emulate_Conv_R8(instr); break; case Code.Newarr: Emulate_Newarr(instr); break; - case Code.Stelem_I4: Emulate_Stelem_I4(instr); break; - case Code.Ldelem_I4: Emulate_Ldelem_I4(instr); break; + case Code.Stelem_I1: + case Code.Stelem_I2: + case Code.Stelem_I4: Emulate_Stelem_I1I2I4(instr); break; + case Code.Ldelem_I1: + case Code.Ldelem_U1: + case Code.Ldelem_I2: + case Code.Ldelem_U2: + case Code.Ldelem_I4: + case Code.Ldelem_U4: Emulate_Ldelem_IU1IU2IU4(instr); break; case Code.Arglist: case Code.Beq: @@ -473,8 +475,6 @@ public void Emulate(Instruction instr) { case Code.Rethrow: case Code.Stelem: case Code.Stelem_I: - case Code.Stelem_I1: - case Code.Stelem_I2: case Code.Stelem_I8: case Code.Stelem_R4: case Code.Stelem_R8: @@ -532,9 +532,9 @@ void Emulate_Sizeof(Instruction instr){ void Emulate_Newarr(Instruction instr) { var val = valueStack.Pop(); - if (val.IsInt32() && val is Int32Value { Value: < 500000 } arrSize) + if (val.IsInt32() && val is Int32Value { Value: < 500000 and >= 0 } arrSize) { - List arr = new List(arrSize.Value); + var arr = new List(arrSize.Value); for (int i = 0; i < arrSize.Value; i++) { arr.Add(new UnknownValue()); } @@ -546,27 +546,42 @@ void Emulate_Newarr(Instruction instr) } } - void Emulate_Stelem_I4(Instruction instr) + void Emulate_Ldlen(Instruction instr) { + var obj = valueStack.Pop(); + valueStack.Push(obj is ObjectValue { obj: List arr } + ? new Int32Value(arr.Count) + : Int32Value.CreateUnknown()); + } + + void Emulate_Stelem_I1I2I4(Instruction instr) { var val = valueStack.Pop(); var idxValue = valueStack.Pop(); var obj = valueStack.Pop(); - if (val is Int32Value && - idxValue is Int32Value idx && idx.AllBitsValid() && - obj is ObjectValue { obj: List arr } && arr.Count > idx.Value) { - arr[idx.Value] = val; + if (val is Int32Value valI32 + && idxValue is Int32Value idx && idx.AllBitsValid() + && obj is ObjectValue { obj: List arr } && idx.Value >= 0 && arr.Count > idx.Value) { + arr[idx.Value] = instr.OpCode.Code switch { + Code.Stelem_I1 => valI32.ToUInt8(), + Code.Stelem_I2 => valI32.ToUInt16(), + _ => val + }; } } - void Emulate_Ldelem_I4(Instruction instr) { + void Emulate_Ldelem_IU1IU2IU4(Instruction instr) { var idxValue = valueStack.Pop(); var obj = valueStack.Pop(); - if (idxValue is Int32Value idx && idx.AllBitsValid() && - obj is ObjectValue { obj: List arr } && arr.Count > idx.Value) { - valueStack.Push(arr[idx.Value]); + if (idxValue is Int32Value idx && idx.AllBitsValid() + && obj is ObjectValue { obj: List arr } && idx.Value >= 0 && arr.Count > idx.Value) { + valueStack.Push(arr[idx.Value]); // We assume stelem or whatever populated the array put in the correct size } else { - valueStack.Push(Int32Value.CreateUnknown()); + valueStack.Push(instr.OpCode.Code switch { + Code.Ldelem_U1 => Int32Value.CreateUnknownUInt8(), + Code.Ldelem_U2 => Int32Value.CreateUnknownUInt16(), + _ => Int32Value.CreateUnknown() + }); } } diff --git a/de4dot.code/StringInliner.cs b/de4dot.code/StringInliner.cs index d1842efc..8c67304c 100644 --- a/de4dot.code/StringInliner.cs +++ b/de4dot.code/StringInliner.cs @@ -177,6 +177,8 @@ protected override void InlineAllCalls() { } protected override CallResult CreateCallResult(IMethod method, MethodSpec gim, Block block, int callInstrIndex) { + if (method is MemberRef memberRef) + method = memberRef.ResolveMethod(); if (stringDecrypters.Find(method) == null) return null; return new MyCallResult(block, callInstrIndex, method, gim); diff --git a/de4dot.code/deobfuscators/ArrayFinder.cs b/de4dot.code/deobfuscators/ArrayFinder.cs index eaa88dd5..b432978d 100644 --- a/de4dot.code/deobfuscators/ArrayFinder.cs +++ b/de4dot.code/deobfuscators/ArrayFinder.cs @@ -132,6 +132,7 @@ public static Value[] GetInitializedArray(int arraySize, MethodDef method, ref i emulator.Push(theArray); var instructions = method.Body.Instructions; + FieldDef arrayField = null; int i; for (i = newarrIndex + 1; i < instructions.Count; i++) { var instr = instructions[i]; @@ -154,8 +155,20 @@ public static Value[] GetInitializedArray(int arraySize, MethodDef method, ref i case Code.Starg_S: case Code.Stsfld: case Code.Stfld: - if (emulator.Peek() == theArray && i != newarrIndex + 1 && i != newarrIndex + 2) - goto done; + if (emulator.Peek() == theArray) { + if (i != newarrIndex + 1 && i != newarrIndex + 2) + goto done; + if (instr.OpCode.Code is Code.Stsfld or Code.Stfld) + arrayField = (FieldDef)instr.Operand; + } + break; + + case Code.Ldsfld: + case Code.Ldfld: + if (arrayField != null && instr.Operand == arrayField) { + emulator.Push(theArray); + continue; + } break; } diff --git a/de4dot.code/deobfuscators/DeobfuscatorBase.cs b/de4dot.code/deobfuscators/DeobfuscatorBase.cs index 5ac2ccdc..ec5045f8 100644 --- a/de4dot.code/deobfuscators/DeobfuscatorBase.cs +++ b/de4dot.code/deobfuscators/DeobfuscatorBase.cs @@ -165,7 +165,8 @@ public virtual void DeobfuscateEnd() { RestoreBaseType(); FixMDHeaderVersion(); - module.Mvid = Guid.NewGuid(); + // NOTE: Changing Mvid breaks samples where Mason Protector is used as a second layer. + //module.Mvid = Guid.NewGuid(); module.EnableTypeDefFindCache = cacheState; } @@ -677,7 +678,7 @@ public virtual bool IsValidNamespaceName(string ns) { public virtual bool IsValidResourceKeyName(string name) => name != null && CheckValidName(name); public virtual void OnWriterEvent(ModuleWriterBase writer, ModuleWriterEvent evt) { } protected void FindAndRemoveInlinedMethods() => RemoveInlinedMethods(InlinedMethodsFinder.Find(module)); - protected void RemoveInlinedMethods(List inlinedMethods) => + protected void RemoveInlinedMethods(IEnumerable inlinedMethods) => AddMethodsToBeRemoved(new UnusedMethodsFinder(module, inlinedMethods, GetRemovedMethods()).Find(), "Inlined method"); protected MethodCollection GetRemovedMethods() { diff --git a/de4dot.code/deobfuscators/MasonProtector/AntiDebugKiller.cs b/de4dot.code/deobfuscators/MasonProtector/AntiDebugKiller.cs new file mode 100644 index 00000000..7d463d73 --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/AntiDebugKiller.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using de4dot.blocks; +using de4dot.blocks.cflow; +using dnlib.DotNet; +using dnlib.DotNet.Emit; + +namespace de4dot.code.deobfuscators.MasonProtector; + +/// +/// (Applies to older versions of the protector only)
+/// Removes "if (Debugger.IsAttached) Environment.Exit(-1) or Environment.FailFast("")" sequences. +/// This pass is mandatory because the presence of those instructions can otherwise hinder constant inlining when +/// they were placed in the middle of other code. +///
+class AntiDebugKiller : IBlocksDeobfuscator { + public bool ExecuteIfNotModified => true; + + public void DeobfuscateBegin(Blocks blocks) { + } + + public bool Deobfuscate(List allBlocks) { + foreach (var block in allBlocks) { + if (!block.IsConditionalBranch() || block.Instructions.Count < 2) + continue; + var instr = block.Instructions[block.Instructions.Count - 2]; + if (instr.OpCode != OpCodes.Call) + continue; + if (instr.Operand is not IMethod { FullName: "System.Boolean System.Diagnostics.Debugger::get_IsAttached()" }) + continue; + + foreach (var tBlock in block.GetTargets()) { + if (tBlock.Instructions.Count < 2) + continue; + var exitCall = tBlock.Instructions[1]; + if (exitCall.OpCode != OpCodes.Call) + continue; + if (exitCall.Operand is not IMethod method + || (method.FullName != "System.Void System.Environment::Exit(System.Int32)" + && method.FullName != "System.Void System.Environment::FailFast(System.String)")) + continue; + var firstInstr = tBlock.Instructions[0]; + if (firstInstr.OpCode != OpCodes.Ldstr || (string)firstInstr.Operand != "") + if (!firstInstr.IsLdcI4() || firstInstr.GetLdcI4Value() != -1) + continue; + + instr.Instruction.OpCode = OpCodes.Nop; // Nop IsAttached() + block.ReplaceBccWithBranch(tBlock == block.FallThrough); + return true; + } + } + + return false; + } +} diff --git a/de4dot.code/deobfuscators/MasonProtector/AntiMiscKiller.cs b/de4dot.code/deobfuscators/MasonProtector/AntiMiscKiller.cs new file mode 100644 index 00000000..24514866 --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/AntiMiscKiller.cs @@ -0,0 +1,39 @@ +using System.Linq; +using de4dot.blocks; +using dnlib.DotNet; +using dnlib.DotNet.Emit; + +namespace de4dot.code.deobfuscators.MasonProtector; + +public static class AntiMiscKiller { + static readonly string[] MasonCheck = new[] { + "System.Boolean System.Diagnostics.Debugger::get_IsAttached()", + "System.String System.Reflection.Assembly::get_Location()", + "System.Byte[] System.IO.File::ReadAllBytes(System.String)", + }; + + public static bool ShouldKill(MethodDef method) { + if (!method.IsStatic || !method.HasBody || method.Parameters.Count != 0) + return false; + + var called = method.Body.Instructions + .Where(ins => ins.OpCode.Code is Code.Call or Code.Callvirt && ins.Operand is IMethod) + .Select(ins => ((IMethod)ins.Operand).FullName).ToHashSet(); + + if (called.IsSupersetOf(MasonCheck) && HasNativeCall(method)) { + return true; + } + + if (DotNetUtils.CallsMethod(method, "System.Diagnostics.Process[] System.Diagnostics.Process::GetProcesses()") + && DotNetUtils.HasString(method, "extremedumper") + && HasNativeCall(method)) { + return true; + } + + return false; + } + + private static bool HasNativeCall(MethodDef method) => method.Body.Instructions.Any(ins => + ins.OpCode == OpCodes.Call && ins.Operand is MethodDef { HasBody: true, IsStatic: true } md + && md.Body.Instructions.Any(ins2 => ins2.OpCode == OpCodes.Calli)); +} diff --git a/de4dot.code/deobfuscators/MasonProtector/ArrayExtractor.cs b/de4dot.code/deobfuscators/MasonProtector/ArrayExtractor.cs new file mode 100644 index 00000000..8ce1c4a7 --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/ArrayExtractor.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using de4dot.blocks.cflow; +using dnlib.DotNet; +using dnlib.DotNet.Emit; + +namespace de4dot.code.deobfuscators.MasonProtector; + +public class ArrayExtractor : InstructionAndBranchEmulator { + FieldDef _targetField; + bool _reached; + readonly byte[] _guidValue; + + public int InstructionIndex => InsnIndex; + + public ArrayExtractor(MethodDef method, byte[] guidValue = null) : base(method) => _guidValue = guidValue; + + /// If this method returns true, the array belonging to the specified field can be popped or obtained via GetArrayValues(). + public bool Run(FieldDef targetField, int startIndex = 0) { + _targetField = targetField; + + Run(startIndex); + + return _reached; + } + + protected override bool OnInstruction(Instruction instr, out bool shouldSkip) { + shouldSkip = false; + + if (instr.OpCode == OpCodes.Stsfld && instr.Operand == _targetField) { + _reached = true; + return false; + } + if (instr.OpCode == OpCodes.Call && instr.Operand is IMethod { + FullName: + "System.Void System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(System.Array,System.RuntimeFieldHandle)" + }) { + shouldSkip = true; + var token = ((ObjectValue)Emulator.Pop()).obj as FieldDef; + var array = ((ObjectValue)Emulator.Pop()).obj as List; + if (token == null || array == null || !token.HasFieldRVA) + return true; + + var elemType = ((SZArraySig)_targetField.FieldType).Next.ElementType; + int elementSize = elemType switch { + ElementType.I1 or ElementType.U1 => 1, + ElementType.I2 or ElementType.Char => 2, + ElementType.I4 => 4, + _ => 0 + }; + + if (elementSize == 0 || token.InitialValue.Length != array.Count * elementSize) + return true; + + for (int i = 0, offset = 0; i < array.Count; i++, offset += elementSize) { + int value = 0; + for (int j = 0; j < elementSize; j++) + value |= token.InitialValue[offset + j] << (j * 8); + + array[i] = new Int32Value(value); + } + return true; + } + if (_guidValue != null && instr.OpCode == OpCodes.Call + && instr.Operand is IMethod { FullName: "System.Byte[] System.Guid::ToByteArray()"}) { + shouldSkip = true; + Emulator.Pop(); + Emulator.Push(new ObjectValue(new List(_guidValue.Select(b => new Int32Value(b))))); + } + + return true; + } + + public ObjectValue GetListObjectValue() { + var ourArrayVal = Emulator.Pop(); + if (ourArrayVal is not ObjectValue { obj: List valList } objVal + || !valList.All(v => v is Int32Value i32 && i32.AllBitsValid())) { + return null; + } + return objVal; + } + + /// Returns the array after Run(), or null if the top stack value is not an array or does not consist of concrete values. + public T[] GetArrayValues() { + var ourArrayVal = Emulator.Pop(); + if (ourArrayVal is not ObjectValue objVal || objVal.obj is not List valList + || !valList.All(v => v is Int32Value i32 && i32.AllBitsValid())) { + return null; + } + return valList.Select(v => (T)Convert.ChangeType(((Int32Value)v).Value, typeof(T))).ToArray(); + } +} diff --git a/de4dot.code/deobfuscators/MasonProtector/CiphertextTransform.cs b/de4dot.code/deobfuscators/MasonProtector/CiphertextTransform.cs new file mode 100644 index 00000000..6de47765 --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/CiphertextTransform.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using de4dot.blocks.cflow; +using dnlib.DotNet; +using dnlib.DotNet.Emit; + +namespace de4dot.code.deobfuscators.MasonProtector; + +public class CiphertextTransform { + readonly MethodDef _method; + readonly int _startIndex; + readonly Dictionary _fields; + + public static CiphertextTransform Analyze(MethodDef method) { + int startIndex = -1, endIndex = -1; + Dictionary fields = new(); + + var instrs = method.Body.Instructions; + for (int i = 2; i < instrs.Count; i++) { + var insn = instrs[i]; + if (insn.OpCode == OpCodes.Ldelem_U1 && instrs[i - 1].IsLdloc() && instrs[i - 2].OpCode == OpCodes.Ldarg_0) { + startIndex = i - 2; + } + else if (insn.OpCode == OpCodes.Ldsfld) { + var field = (FieldDef)insn.Operand; + if (!field.FieldType.IsSZArray || field.FieldType.Next.ElementType != ElementType.U1) + throw new Exception("Unexpected array field type for extra ciphertext transform"); + + var ex = new ArrayExtractor(field.DeclaringType.FindStaticConstructor()); + if (!ex.Run(field)) + throw new Exception("Failed to get required field data for extra ciphertext transform"); + + fields[field] = ex.GetListObjectValue(); + } + else if (startIndex != -1 && insn.OpCode == OpCodes.Stelem_I1) { + endIndex = i; + break; + } + } + + if (endIndex == -1) + throw new Exception("Analyzing extra ciphertext transform method failed"); + + return new CiphertextTransform(method, startIndex, fields); + } + + public CiphertextTransform(MethodDef method, int startIndex, Dictionary fields) { + _method = method; + _startIndex = startIndex; + _fields = fields; + } + + public void TransformArray(byte[] array) { + var emu = new InstructionEmulator(_method); + emu.SetArg(new Parameter(0), ByteArrayToValue(array)); + + var instrs = _method.Body.Instructions; + var local = (Local)instrs[_startIndex + 1].Operand; + + for (int i = 0; i < array.Length; i++) { + emu.SetLocal(local, new Int32Value(i)); + for (int j = _startIndex; j < instrs.Count; j++) { + if (instrs[j].OpCode == OpCodes.Ldsfld) { + if (_fields.TryGetValue((FieldDef)instrs[j].Operand, out var value)) + emu.Push(value); + else + emu.Push(new UnknownValue()); + } + else if (instrs[j].OpCode == OpCodes.Stelem_I1) { + var val = emu.Pop(); + if (val is not Int32Value i32 || !i32.AllBitsValid()) + throw new Exception("Emulation didn't yield value"); + + array[i] = (byte)i32.Value; + break; + } + else { + emu.Emulate(instrs[j]); + } + } + } + } + + static ObjectValue ByteArrayToValue(byte[] array) + => new ObjectValue(new List(array.Select(b => new Int32Value(b)))); +} diff --git a/de4dot.code/deobfuscators/MasonProtector/ConstantsInliner.cs b/de4dot.code/deobfuscators/MasonProtector/ConstantsInliner.cs new file mode 100644 index 00000000..3cf565a4 --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/ConstantsInliner.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using de4dot.blocks; +using de4dot.blocks.cflow; +using dnlib.DotNet; +using dnlib.DotNet.Emit; + +namespace de4dot.code.deobfuscators.MasonProtector; + +class ConstantsInliner : IBlocksDeobfuscator { + readonly ModuleDefMD _module; + readonly ISimpleDeobfuscator _simpleDeobfuscator; + readonly Dictionary _constFields = new(); + readonly Dictionary _constArrays = new(); + + readonly Dictionary _inlinedMethods = new(); + public IEnumerable Methods => _inlinedMethods.Keys; + + public ConstantsInliner(ModuleDefMD module, ISimpleDeobfuscator simpleDeobfuscator) { + _module = module; + _simpleDeobfuscator = simpleDeobfuscator; + Find(); + } + + void Find() { + var moduleCctor = DotNetUtils.GetModuleTypeCctor(_module); + if (moduleCctor == null) + return; + WalkMethod(moduleCctor, true); + + foreach (var type in _module.Types) { + var cctor = type.FindStaticConstructor(); + if (cctor is not { HasBody: true } || cctor.Body.Instructions.Count < 20) continue; + if (cctor.Body.Instructions[1].OpCode != OpCodes.Newarr) continue; + var stsfld = cctor.Body.Instructions[cctor.Body.Instructions.Count - 2]; + if (stsfld.OpCode != OpCodes.Stsfld || stsfld.Operand is not FieldDef { FieldType: SZArraySig } field) continue; + + WalkClassCctor(cctor, field); + } + } + + void WalkMethod(MethodDef method, bool allowRecursion = false) { + _simpleDeobfuscator.Deobfuscate(method); + var instrs = method.Body.Instructions; + for (var i = 0; i < instrs.Count; i++) { + var instr = instrs[i]; + if (allowRecursion && instr.OpCode.Code == Code.Call && instr.Operand is MethodDef { IsStatic: true } methodDef) { + WalkMethod(methodDef); + continue; + } + + if (instr.OpCode == OpCodes.Newarr && instrs[i - 1].IsLdcI4()) { + int len = instrs[i - 1].GetLdcI4Value(); + int index = i; + int[] theirInts = ArrayFinder.GetInitializedInt32Array(len, method, ref index); + if (theirInts == null) { + continue; + } + + FieldDef arrayField = null; + if (instrs[i + 1].OpCode == OpCodes.Stsfld) { + arrayField = (FieldDef)instrs[i + 1].Operand; + } + else if (instrs[index + 1].OpCode == OpCodes.Stsfld) { + index += 1; + arrayField = (FieldDef)instrs[index].Operand; + } + + if (arrayField != null) { + Console.WriteLine($"Found array of len {len}, [0]={theirInts[0]}, field={arrayField}"); + _constArrays[arrayField] = theirInts; + } + i = index; + continue; + } + + if (!instr.IsLdcI4()) + continue; + if (i + 1 >= instrs.Count) + continue; + var store = instrs[i + 1]; + if (store.OpCode.Code != Code.Stsfld) + continue; + if (store.Operand is not FieldDef key) + continue; + + _constFields[key] = instr.GetLdcI4Value(); + } + _simpleDeobfuscator.MethodModified(method); // flush from cache so it can be properly handled once all consts are collected + } + + /* This is a support method for string decryption. It causes parameters to the decryption calls to become foldable. */ + void WalkClassCctor(MethodDef method, FieldDef field) { + var emu = new ArrayExtractor(method); + if (!emu.Run(field)) { + return; + } + + if (emu.GetArrayValues() is { } array) + _constArrays[field] = array; + } + + public bool ExecuteIfNotModified => true; + + public void DeobfuscateBegin(Blocks blocks) { + } + + public bool Deobfuscate(List allBlocks) { + if (_constFields.Count == 0) + return false; + + bool result = false; + foreach (var block in allBlocks) { + var instrs = block.Instructions; + for (var i = 0; i < instrs.Count; i++) { + var load = instrs[i]; + if (load.OpCode.Code == Code.Call && GetMethod((IMethod)load.Operand) is { IsStatic: true } called) { + if (!IsFunnyMethod(called)) + continue; + + int paramCount = called.GetParamCount(); + if (i >= paramCount && Enumerable.Range(1, paramCount).All(offset => instrs[i - offset].IsLdcI4())) { + var args = Enumerable.Range(1, paramCount) + .Reverse() + .Select(offset => instrs[i - offset].GetLdcI4Value()) + .ToArray(); + + var callResult = new Evaluator(called, _constArrays).Run(0, args); + if (callResult is Int32Value i32 && i32.AllBitsValid()) { + for (int offset = 1; offset <= paramCount; offset++) + instrs[i - offset].Instruction.OpCode = OpCodes.Nop; + + instrs[i].Instruction.OpCode = OpCodes.Ldc_I4; + instrs[i].Instruction.Operand = i32.Value; + _inlinedMethods[called] = true; + result = true; + } + else { + //Console.WriteLine("No value from " + called); + } + } + } + + if (load.OpCode.Code != Code.Ldsfld) + continue; + if (load.Operand is not FieldDef loadField) + continue; + // Inline an int load + if (_constFields.TryGetValue(loadField, out var value)) { + instrs[i].Instruction.OpCode = OpCodes.Ldc_I4; + instrs[i].Instruction.Operand = value; + result = true; + continue; + } + // Inline an array int load + if (_constArrays.TryGetValue(loadField, out var values) && instrs[i + 1].IsLdcI4() + && instrs[i + 2].OpCode == OpCodes.Ldelem_I4) { + instrs[i].Instruction.OpCode = OpCodes.Ldc_I4; + instrs[i].Instruction.Operand = values[instrs[i + 1].GetLdcI4Value()]; + instrs[i + 1].Instruction.OpCode = OpCodes.Nop; + instrs[i + 2].Instruction.OpCode = OpCodes.Nop; + result = true; + } + } + } + + return result; + } + + private static MethodDef GetMethod(IMethod method) { + if (method is MethodDef md) + return md; + if (method is MemberRef mr) + return mr.ResolveMethod(); + return null; + } + + private static bool IsFunnyMethod(MethodDef method) => + IsTrivialMethod(method) + || IsTrivialIntLogic(method) + || IsSingleTableMethod(method) + || IsManyTablesMethod(method); + + /* + return A_0; + */ + private static bool IsTrivialMethod(MethodDef method) => + method.HasBody && method.GetParamCount() == 1 && + method.Parameters[0].Type.ElementType == ElementType.I4 && + method.HasReturnType && method.Body.Instructions.Count == 2; + + /* + int num = int_0 ^ ~int_1; + return num ^ ~int_1; + */ + private static bool IsTrivialIntLogic(MethodDef method) => + method.HasBody && method.GetParamCount() > 0 && + method.Parameters.All(p => p.Type.ElementType == ElementType.I4) && + method.HasReturnType && method.Body.Instructions.All( + ins => ins.OpCode.FlowControl is FlowControl.Next or FlowControl.Return + && ins.OpCode.OpCodeType is OpCodeType.Primitive or OpCodeType.Macro); + + /* + int num = Class32.int_0[int_6]; + int num2 = Class32.int_0[int_7]; + return ~num + num2; + */ + private static bool IsSingleTableMethod(MethodDef method) { + if (!method.HasBody || method.GetParamCount() != 2) return false; + + var instrs = method.Body.Instructions; + if (instrs.Count < 10) return false; + for (int i = 0; i < 8; i += 4) { + if (instrs[i].OpCode != OpCodes.Ldsfld + || !instrs[i + 1].IsLdarg() + || instrs[i + 2].OpCode != OpCodes.Ldelem_I4 + || (!instrs[i + 3].IsStloc() && (i != 4 || !instrs[i + 3].IsLdloc()))) + return false; + } + return true; + } + + /* + int num = int_21 % 12; + if (num == 0) + { + int num2 = Class31.int_4[int_22]; + return Class31.int_4[int_23] ^ ~num2; + } + ... + */ + private static bool IsManyTablesMethod(MethodDef method) { + if (!method.HasBody || method.GetParamCount() != 3) return false; + + var instrs = method.Body.Instructions; + if (instrs.Count < 10) return false; + return instrs[0].IsLdarg() + && instrs[1].IsLdcI4() + && instrs[2].OpCode == OpCodes.Rem + && instrs[3].IsStloc() + && instrs[4].IsLdloc() + && instrs[5].IsLdcI4(); + } + + private class Evaluator : InstructionAndBranchEmulator { + readonly Dictionary _constArrays; + + public Evaluator(MethodDef method, Dictionary constArrays) : base(method) + => _constArrays = constArrays; + + protected override bool OnInstruction(Instruction instr, out bool shouldSkip) { + shouldSkip = false; + if (instr.OpCode == OpCodes.Ldsfld) { + if (_constArrays.TryGetValue((FieldDef)instr.Operand, out var values)) { + var valsList = values.Select(v => new Int32Value(v)).ToList(); + Emulator.Push(new ObjectValue(valsList)); + shouldSkip = true; + } + } + + return true; + } + } +} diff --git a/de4dot.code/deobfuscators/MasonProtector/Deobfuscator.cs b/de4dot.code/deobfuscators/MasonProtector/Deobfuscator.cs new file mode 100644 index 00000000..f5a62faf --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/Deobfuscator.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using de4dot.blocks; +using de4dot.blocks.cflow; +using dnlib.DotNet; +using dnlib.DotNet.Emit; + +namespace de4dot.code.deobfuscators.MasonProtector; + +public class DeobfuscatorInfo : DeobfuscatorInfoBase { + internal const string THE_NAME = "Mason Protector"; + public const string THE_TYPE = "mp"; + private const string DEFAULT_REGEX = DeobfuscatorBase.DEFAULT_ASIAN_VALID_NAME_REGEX; + + public DeobfuscatorInfo() + : base(DEFAULT_REGEX) { + } + + public override string Name => THE_NAME; + public override string Type => THE_TYPE; + + public override IDeobfuscator CreateDeobfuscator() { + return new Deobfuscator(new DeobfuscatorBase.OptionsBase { + RenameResourcesInCode = false, ValidNameRegex = validNameRegex.Get() + }); + } +} + +internal class Deobfuscator : DeobfuscatorBase { + private int _score; + + public Deobfuscator(OptionsBase options) + : base(options) + { + } + + public override string Type => DeobfuscatorInfo.THE_TYPE; + public override string TypeLong => DeobfuscatorInfo.THE_NAME; + public override string Name => TypeLong; + + private ConstantsInliner _constantsInliner; + private MasonMethodCallInliner _methodCallInliner; + + private readonly List _proxies = new(); + + public override IEnumerable BlocksDeobfuscators { + get { + var list = new List(); + if (_constantsInliner != null) + list.Add(_constantsInliner); + list.Add(new AntiDebugKiller()); + list.Add(_methodCallInliner ??= new MasonMethodCallInliner(_proxies)); + list.Add(new InlineStringDeobfuscator()); + return list; + } + } + + protected override int DetectInternal() => _score; + + protected override void ScanForObfuscator() { + var v = new Vault(module, DeobfuscatedFile); + v.Find(); + if (v.Found) + _score += 100; + + var strDec2 = new StringDecrypter2(module); + strDec2.Find(); + if (strDec2.StringDecrypterInfos.Count > 0) + _score += 100; + + // Old versions have obvious [GeneratedCode("Freemasonry", "(c) Mason Protector")] attributes + // Newer versions use a custom attribute type defined in the assembly, with AttributeUsage(AttributeTargets.All) + } + + static readonly Regex IsRandomName = new("^[A-Za-z]{40,90}[0-9]{4,8}$"); + + public override bool IsValidMethodName(string name) => name != null && !IsRandomName.IsMatch(name) && CheckValidName(name); + public override bool IsValidFieldName(string name) => name != null && !IsRandomName.IsMatch(name) && CheckValidName(name); + public override bool IsValidTypeName(string name) => name != null && !IsRandomName.IsMatch(name) && CheckValidName(name); + public override bool IsValidMethodArgName(string name) => name != null && !IsRandomName.IsMatch(name) && CheckValidName(name); + + public override void DeobfuscateBegin() { + base.DeobfuscateBegin(); + + var moduleCctor = DotNetUtils.GetModuleTypeCctor(module); + + _proxies.AddRange(ProxyDeclutterer.Run(module)); + + _constantsInliner = new ConstantsInliner(module, DeobfuscatedFile); + + var strDec = new StringDecrypter(module); + strDec.Find(); + foreach (var info in strDec.StringDecrypterInfos) + staticStringInliner.Add(info.Method, (method, gim, args) => strDec.Decrypt(info, (string)args[0], (string)args[1], (int)args[2])); + + var strDec2 = new StringDecrypter2(module); + strDec2.Find(); + foreach (var info in strDec2.StringDecrypterInfos) + staticStringInliner.Add(info.Method, (method, gim, args) => strDec2.Decrypt(info, (int)args[0], (int)args[1], (int)args[2], (int)args[3])); + + var vault = new Vault(module, DeobfuscatedFile); + vault.Find(); + if (vault.Found) { + try { + vault.Initialize(this); + vault.UnvaultAll(); + } + catch (Exception ex) { + Logger.w("Mason Protector: Unvault failed: {0}", ex.Message); + //Console.WriteLine(ex.StackTrace); + } + } + if (vault.CanRemove) { + AddTypeToBeRemoved(vault.VaultType, "Vault runtime type"); + AddResourceToBeRemoved(vault.VaultResource, "Vault resource"); + if (moduleCctor != null) { + foreach (var called in moduleCctor.Body.Instructions.Where(ins => ins.OpCode == OpCodes.Call + && ins.Operand is MethodDef md && md.DeclaringType == vault.VaultType).Select(ins => (MethodDef)ins.Operand)) + AddCallToBeRemoved(moduleCctor, called); + } + } + + IResourceDecrypter resDec = new ResourceDecrypter(module, DeobfuscatedFile); + resDec.Find(); + if (!resDec.Found) { + resDec = new ResourceDecrypter2(module, DeobfuscatedFile); + resDec.Find(); + } + if (resDec.Found) { + try { + resDec.Initialize(this); + resDec.DecryptResources(); + AddMethodsToBeRemoved(resDec.MethodsToRemove, "Resource decrypter"); + if (moduleCctor != null) + AddCallToBeRemoved(moduleCctor, resDec.VarsInitMethod); + } + catch (Exception ex) { + Logger.e("Resource decryption failed: {0}", ex.Message); + } + } + } + + public override void DeobfuscateEnd() { + IntShenanigans.Deobfuscate(module); + + if (_constantsInliner != null) + RemoveInlinedMethods(_constantsInliner.Methods); + if (_methodCallInliner != null) + RemoveInlinedMethods(_methodCallInliner.InlinedMethods); + + var antiMethods = new HashSet(); + foreach (var type in module.GetTypes()) + foreach (var method in type.Methods) + if (AntiMiscKiller.ShouldKill(method)) { + AddMethodToBeRemoved(method, "Anti"); + antiMethods.Add(method); + } + + foreach (var type in module.GetTypes()) + foreach (var method in type.Methods) + if (method.HasBody) + foreach (var instruction in method.Body.Instructions) + if (instruction.OpCode == OpCodes.Call && antiMethods.Contains((IMethod)instruction.Operand)) + instruction.OpCode = OpCodes.Nop; + + base.DeobfuscateEnd(); + } + + public override IEnumerable GetStringDecrypterMethods() => new List(); +} diff --git a/de4dot.code/deobfuscators/MasonProtector/InlineStringDeobfuscator.cs b/de4dot.code/deobfuscators/MasonProtector/InlineStringDeobfuscator.cs new file mode 100644 index 00000000..92382e75 --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/InlineStringDeobfuscator.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using de4dot.blocks; +using de4dot.blocks.cflow; +using dnlib.DotNet; +using dnlib.DotNet.Emit; + +namespace de4dot.code.deobfuscators.MasonProtector; + +public class InlineStringDeobfuscator : IBlocksDeobfuscator { + public bool ExecuteIfNotModified => false; + + MethodDef _curMethod; + + public void DeobfuscateBegin(Blocks blocks) => _curMethod = blocks.Method; + + public bool Deobfuscate(List allBlocks) { + bool modified = false; + + foreach (var block in allBlocks) { + var instrs = block.Instructions; + for (int i = 5; i < instrs.Count - 1; i++) { + var instr = instrs[i]; + if (instr.OpCode == OpCodes.Newobj + && instr.Operand is IMethod { FullName: "System.Void System.String::.ctor(System.Char[])" } + && instrs[i + 1].OpCode == OpCodes.Call + && instrs[i + 1].Operand is IMethod { FullName: "System.String System.String::Intern(System.String)" }) + modified |= CheckAndProcessBlock(block, i + 1); + } + } + + return modified; + } + + bool CheckAndProcessBlock(Block block, int internIndex) { + var instrs = block.Instructions; + int storeCount = 0, startIndex = -1; + for (int i = internIndex - 1; i >= 0; i--) { + var instr = instrs[i]; + if (instr.OpCode == OpCodes.Stelem_I2) { + storeCount++; + continue; + } + + if (instr.OpCode != OpCodes.Newarr) + continue; + if (!instrs[i - 1].IsLdcI4()) { + return false; + } + + if (instrs[i - 1].GetLdcI4Value() != storeCount) { + return false; + } + + startIndex = i - 1; + break; + } + + if (startIndex == -1) + return false; + + return ProcessBlock(block, startIndex, internIndex); + } + + bool ProcessBlock(Block block, int startIndex, int internIndex) { + var instrs = block.Instructions; + var instrRange = instrs.GetRange(startIndex, internIndex - startIndex); + + var fields = new Dictionary(); + + var arrayFields = instrRange + .Where(ins => ins.OpCode == OpCodes.Ldsfld) + .Select(ins => ins.Operand as FieldDef) + .Where(fld => fld != null && fld.FieldType.IsSZArray) + .Distinct() + .ToArray(); + if (arrayFields.Length > 1) { + Logger.w("Encountered more than one field in {0} ({1:X8})", _curMethod, _curMethod.MDToken.ToInt32()); + return false; + } + + if (arrayFields.Length == 1) { + char[] fieldData = GetCharArray(arrayFields[0]); + if (fieldData == null) { + Logger.w("Unable to obtain field data for {0} in {1} ({2:X8})", arrayFields[0], _curMethod, _curMethod.MDToken.ToInt32()); + return false; + } + fields[arrayFields[0]] = new ObjectValue(fieldData.Select(Value (c) => new Int32Value(c)).ToList()); + } + + var intFields = instrRange + .Where(ins => ins.OpCode == OpCodes.Ldsfld) + .Select(ins => ins.Operand as FieldDef) + .Where(fld => fld != null && fld.FieldType.ElementType == ElementType.I4) + .ToArray(); + if (intFields.Length > 0) { + if (intFields.Any(f => f.DeclaringType != intFields[0].DeclaringType)) { + Logger.w("Differing field declaring types"); + return false; + } + GrabInts(intFields[0].DeclaringType, fields); + } + + var emu = new InstructionEmulator(_curMethod); + for (int i = startIndex; i < internIndex; i++) { + if (instrs[i].OpCode == OpCodes.Ldsfld) { + if (fields.TryGetValue((FieldDef)instrs[i].Operand, out var fVal)) { + emu.Push(fVal); + continue; + } + } + else if (instrs[i].OpCode == OpCodes.Newobj) { + break; + } + emu.Emulate(instrs[i].Instruction); + } + + var charVals = emu.Pop(); + if (charVals is not ObjectValue { obj: List values } + || !values.All(v => v is Int32Value i32 && i32.AllBitsValid())) { + return false; + } + + var str = new string(values.Select(v => (char)((Int32Value)v).Value).ToArray()); + //Console.WriteLine("[IDEC] " + str); + block.Replace(internIndex - 1, 1, OpCodes.Pop.ToInstruction()); // newobj -> pop + block.Replace(internIndex, 1, OpCodes.Ldstr.ToInstruction(str)); // call -> ldstr + return true; + } + + static char[] GetCharArray(FieldDef field) { + var cctor = field.DeclaringType.FindStaticConstructor(); + if (cctor == null) + return null; + + var emu = new ArrayExtractor(cctor); + if (!emu.Run(field)) { + return null; + } + + return emu.GetArrayValues(); + } + + static void GrabInts(TypeDef declType, Dictionary fieldValues) { + var cctor = declType.FindStaticConstructor(); + if (cctor == null) + return; + + var emu = new InstructionEmulator(cctor); + foreach (var ins in cctor.Body.Instructions) { + if (ins.OpCode == OpCodes.Stsfld && ins.Operand is FieldDef field + && field.FieldType.ElementType == ElementType.I4) { + fieldValues[field] = emu.Pop(); + } + else { + emu.Emulate(ins); + } + } + } +} diff --git a/de4dot.code/deobfuscators/MasonProtector/IntShenanigans.cs b/de4dot.code/deobfuscators/MasonProtector/IntShenanigans.cs new file mode 100644 index 00000000..f2e0ab59 --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/IntShenanigans.cs @@ -0,0 +1,77 @@ +using dnlib.DotNet; +using dnlib.DotNet.Emit; + +namespace de4dot.code.deobfuscators.MasonProtector; + +/// +/// Removes calls to idempotent functions that essentially ignore the second argument. +/// +public static class IntShenanigans { + // int_0 ^ ~int_1 ^ ~_int_1 + static readonly Code[] NotXor = new Code[] { + Code.Ldarg_0, Code.Ldarg_1, Code.Not, Code.Xor, Code.Stloc_0, Code.Ldloc_0, Code.Ldarg_1, Code.Not, Code.Xor + }; + // int_0 ^ (int_1 * 1) ^ int_1 + static readonly Code[] Mul1Xor = new Code[] { + Code.Ldarg_0, Code.Ldarg_1, Code.Mul, Code.Xor, Code.Stloc_0, Code.Ldloc_0, Code.Ldarg_1, Code.Xor + }; + // int_0 - int_1 + int_1 + static readonly Code[] SubAdd = new Code[] { + Code.Ldarg_0, Code.Ldarg_1, Code.Sub, Code.Stloc_0, Code.Ldloc_0, Code.Ldarg_1, Code.Add + }; + // int_0 + int_1 - int_1 + static readonly Code[] AddSub = new Code[] { + Code.Ldarg_0, Code.Ldarg_1, Code.Add, Code.Stloc_0, Code.Ldloc_0, Code.Ldarg_1, Code.Sub + }; + // int_0 + (int_1 * 0) + static readonly Code[] Mul0Add = new Code[] { + Code.Ldarg_0, Code.Ldarg_1, Code.Ldc_I4_0, Code.Mul, Code.Add + }; + // int_0 ^ int_1 ^ int_1 + static readonly Code[] XorXor = new Code[] { + Code.Ldarg_0, Code.Ldarg_1, Code.Xor, Code.Stloc_0, Code.Ldloc_0, Code.Ldarg_1, Code.Xor + }; + + static readonly Code[][] Patterns = new[] { NotXor, Mul1Xor, SubAdd, AddSub, Mul0Add, XorXor }; + + static bool IsMatch(MethodDef method) { + var instrs = method.Body.Instructions; + foreach (var pattern in Patterns) { + if (instrs.Count != pattern.Length + 1) { + continue; + } + bool match = true; + for (int i = 0; i < pattern.Length; i++) { + if (instrs[i].OpCode.Code != pattern[i]) { + match = false; + break; + } + } + + if (match) + return true; + } + + return false; + } + + public static void Deobfuscate(ModuleDefMD module) { + foreach (var type in module.Types) { + foreach (var method in type.Methods) { + if (!method.HasBody) continue; + + var instrs = method.Body.Instructions; + for (int i = 2; i < instrs.Count; i++) { + var instr = instrs[i]; + if (instr.OpCode.Code != Code.Call) continue; + if (instr.Operand is not MethodDef { IsStatic: true, HasBody: true } md || md.Parameters.Count != 2) continue; + + if (IsMatch(md) && instrs[i - 1].IsLdcI4()) { + instrs[i].OpCode = OpCodes.Nop; + instrs[i - 1].OpCode = OpCodes.Nop; + } + } + } + } + } +} diff --git a/de4dot.code/deobfuscators/MasonProtector/LocalSigReader.cs b/de4dot.code/deobfuscators/MasonProtector/LocalSigReader.cs new file mode 100644 index 00000000..c5e26691 --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/LocalSigReader.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using dnlib.DotNet; +using dnlib.IO; + +namespace de4dot.code.deobfuscators.MasonProtector; + +/// +/// This is a heavily stripped down version of dnlib's SignatureReader that only supports reading LocalSig. +/// The difference is that Class and ValueType have obfuscator-specific handling for their tokens. +/// +public struct LocalSigReader { + const uint MaxArrayRank = 64; + + readonly ISignatureReaderHelper helper; + readonly ICorLibTypes corLibTypes; + DataReader reader; + TokenMapper _tokenMapper; + + public static LocalSig ReadSig(ModuleDefMD module, TokenMapper tokenMapper, byte[] signature) => + ReadSig(module, module.CorLibTypes, tokenMapper, ByteArrayDataReaderFactory.CreateReader(signature)); + + public static LocalSig ReadSig(ModuleDefMD module, TokenMapper tokenMapper, DataReader signature) => + ReadSig(module, module.CorLibTypes, tokenMapper, signature); + + public static LocalSig ReadSig(ISignatureReaderHelper helper, ICorLibTypes corLibTypes, TokenMapper tokenMapper, DataReader signature) { + try { + var reader = new LocalSigReader(helper, corLibTypes, tokenMapper, ref signature); + if (reader.reader.Length == 0) + return null; + return reader.ReadSig(); + } + catch { + return null; + } + } + + LocalSigReader(ISignatureReaderHelper helper, ICorLibTypes corLibTypes, TokenMapper tokenMapper, ref DataReader reader) { + this.helper = helper; + this.corLibTypes = corLibTypes; + this._tokenMapper = tokenMapper; + this.reader = reader; + } + + LocalSig ReadSig() { + var callingConvention = (CallingConvention)reader.ReadByte(); + if ((callingConvention & CallingConvention.Mask) == CallingConvention.LocalSig) + return ReadLocalSig(); + + throw new InvalidOperationException("Tried to read non-LocalSig"); + } + + LocalSig ReadLocalSig() { + if (!reader.TryReadCompressedUInt32(out uint count) || count > 0x10000 || count > reader.BytesLeft) + return null; + var sig = new LocalSig(); + var locals = sig.Locals; + for (uint i = 0; i < count; i++) + locals.Add(ReadType()); + return sig; + } + + /// + /// Reads the next type + /// + /// A new instance or null if invalid element type + TypeSig ReadType() { + TypeSig result = null; + switch ((ElementType)reader.ReadByte()) { + case ElementType.Void: result = corLibTypes.Void; break; + case ElementType.Boolean: result = corLibTypes.Boolean; break; + case ElementType.Char: result = corLibTypes.Char; break; + case ElementType.I1: result = corLibTypes.SByte; break; + case ElementType.U1: result = corLibTypes.Byte; break; + case ElementType.I2: result = corLibTypes.Int16; break; + case ElementType.U2: result = corLibTypes.UInt16; break; + case ElementType.I4: result = corLibTypes.Int32; break; + case ElementType.U4: result = corLibTypes.UInt32; break; + case ElementType.I8: result = corLibTypes.Int64; break; + case ElementType.U8: result = corLibTypes.UInt64; break; + case ElementType.R4: result = corLibTypes.Single; break; + case ElementType.R8: result = corLibTypes.Double; break; + case ElementType.String: result = corLibTypes.String; break; + case ElementType.TypedByRef:result = corLibTypes.TypedReference; break; + case ElementType.I: result = corLibTypes.IntPtr; break; + case ElementType.U: result = corLibTypes.UIntPtr; break; + case ElementType.Object: result = corLibTypes.Object; break; + + case ElementType.Ptr: result = new PtrSig(ReadType()); break; + case ElementType.ByRef: result = new ByRefSig(ReadType()); break; + case ElementType.ValueType: result = ReadToken(ElementType.ValueType); break; + case ElementType.Class: result = ReadToken(ElementType.Class); break; + case ElementType.SZArray: result = new SZArraySig(ReadType()); break; + case ElementType.Pinned: result = new PinnedSig(ReadType()); break; + + case ElementType.Array: + var nextType = ReadType(); + uint rank; + if (!reader.TryReadCompressedUInt32(out rank)) + break; + if (rank > MaxArrayRank) + break; + uint num; + if (!reader.TryReadCompressedUInt32(out num)) + break; + if (num > rank) + break; + var sizes = new List((int)num); + uint i; + for (i = 0; i < num; i++) { + if (!reader.TryReadCompressedUInt32(out uint size)) + goto exit; + sizes.Add(size); + } + if (!reader.TryReadCompressedUInt32(out num)) + break; + if (num > rank) + break; + var lowerBounds = new List((int)num); + for (i = 0; i < num; i++) { + if (!reader.TryReadCompressedInt32(out int size)) + goto exit; + lowerBounds.Add(size); + } + result = new ArraySig(nextType, rank, sizes, lowerBounds); + break; + + default: + throw new InvalidOperationException(); + } +exit: + return result; + } + + TypeSig ReadToken(ElementType et) { + int theirToken = reader.ReadInt32(); + int realToken = _tokenMapper.Map(theirToken); + + uint codedToken = new MDToken(realToken).Rid << 2; + switch (realToken >> 24) { + case 1: // TypeRef + codedToken |= 1; + break; + case 2: // TypeDef + break; // |= 0 + case 0x1B: // TypeSpec + codedToken |= 2; + break; + default: + throw new InvalidOperationException("Unexpected token kind: " + (realToken >> 24)); + } + + var resolved = helper.ResolveTypeDefOrRef(codedToken, default); + //Console.WriteLine($"Resolved {theirToken:X8} to {realToken:X8} --> {resolved}"); + if (realToken >> 24 == 0x1B) + return resolved.TryGetGenericInstSig(); + + return et switch + { + ElementType.Class => new ClassSig(resolved), + ElementType.ValueType => new ValueTypeSig(resolved), + _ => throw new InvalidOperationException() + }; + } +} diff --git a/de4dot.code/deobfuscators/MasonProtector/MasonMethodCallInliner.cs b/de4dot.code/deobfuscators/MasonProtector/MasonMethodCallInliner.cs new file mode 100644 index 00000000..00d2ff42 --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/MasonMethodCallInliner.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using de4dot.blocks.cflow; +using dnlib.DotNet; + +namespace de4dot.code.deobfuscators.MasonProtector; + +/// +/// Inlines trivial methods found in <Module> and proxy methods. +/// +public class MasonMethodCallInliner : MethodCallInliner { + readonly Dictionary _inlined = new(); + public IEnumerable InlinedMethods => _inlined.Keys; + + readonly List _proxies; + + public MasonMethodCallInliner(List proxies) : base(false) => _proxies = proxies; + + protected override bool CanInline(MethodDef method) => IsSimpleGlobalModuleMethod(method) || _proxies.Contains(method); + + static bool IsSimpleGlobalModuleMethod(MethodDef method) => + method.IsStatic + && method.HasBody + && method.Parameters.Count == 0 + && method.HasReturnType + && method.IsPublic + && method.DeclaringType.IsGlobalModuleType; + + protected override void OnInlinedMethod(MethodDef methodToInline, bool inlinedMethod) { + if (inlinedMethod) + _inlined[methodToInline] = true; + } +} diff --git a/de4dot.code/deobfuscators/MasonProtector/MethodBodyReader.cs b/de4dot.code/deobfuscators/MasonProtector/MethodBodyReader.cs new file mode 100644 index 00000000..dfeda1b5 --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/MethodBodyReader.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using dnlib.DotNet; +using dnlib.DotNet.Emit; +using dnlib.IO; + +namespace de4dot.code.deobfuscators.MasonProtector; + +/// +/// Custom reader that is used for decrypted IL code. Tokens in there are non-standard and need special handling. +/// +public class MethodBodyReader : MethodBodyReaderBase { + ModuleDefMD _module; + TokenMapper _tokenMapper; + ushort _maxStackSize; + GenericParamContext gpContext; + + public MethodBodyReader(ModuleDefMD module, TokenMapper tokenMapper, DataReader reader) : base(reader) { + _module = module; + _tokenMapper = tokenMapper; + } + + public void ReadBody(MethodDef method, IList realLocals, int maxStackSize) { + gpContext = GenericParamContext.Create(method); + parameters = method.Parameters; + SetLocals(realLocals); + + _maxStackSize = (ushort)maxStackSize; + ReadInstructionsNumBytes(reader.Length); + } + + protected override IField ReadInlineField(Instruction instr) { + var realToken = _tokenMapper.Map(reader.ReadInt32()); + return _module.ResolveToken(realToken, gpContext) as IField; + } + + protected override IMethod ReadInlineMethod(Instruction instr) { + var realToken = _tokenMapper.Map(reader.ReadInt32()); + return _module.ResolveToken(realToken, gpContext) as IMethod; + } + + protected override MethodSig ReadInlineSig(Instruction instr) => throw new System.NotImplementedException(); + + protected override string ReadInlineString(Instruction instr) => _tokenMapper.MapString(reader.ReadInt32()); + + protected override ITokenOperand ReadInlineTok(Instruction instr) { + var realToken = _tokenMapper.Map(reader.ReadInt32()); + return _module.ResolveToken(realToken, gpContext) as ITokenOperand; + } + + protected override ITypeDefOrRef ReadInlineType(Instruction instr) { + var realToken = _tokenMapper.Map(reader.ReadInt32()); + return _module.ResolveToken(realToken, gpContext) as ITypeDefOrRef; + } + + public override void RestoreMethod(MethodDef method) { + base.RestoreMethod(method); + method.Body.MaxStack = _maxStackSize; + } + + public void ReadExceptionHandlers(int ehCount, DataReader ehReader) { + exceptionHandlers = new ExceptionHandler[ehCount]; + for (int i = 0; i < ehCount; i++) { + int flags = ehReader.ReadInt32(); + uint tryStart = ehReader.ReadUInt32(); + uint tryLen = ehReader.ReadUInt32(); + uint handlerStart = ehReader.ReadUInt32(); + uint handlerLen = ehReader.ReadUInt32(); + uint filterStart = ehReader.ReadUInt32(); + int catchType = ehReader.ReadInt32(); + + var eh = new ExceptionHandler((ExceptionHandlerType)flags); + eh.TryStart = GetInstructionThrow(tryStart); + eh.TryEnd = GetInstruction(tryStart + tryLen); + eh.HandlerStart = GetInstructionThrow(handlerStart); + eh.HandlerEnd = GetInstruction(handlerStart + handlerLen); + if (flags == 0) { + eh.CatchType = _module.ResolveToken(_tokenMapper.Map(catchType)) as ITypeDefOrRef; + } else if (flags == 1) + eh.FilterStart = GetInstructionThrow(filterStart); + + exceptionHandlers[i] = eh; + } + } +} diff --git a/de4dot.code/deobfuscators/MasonProtector/ProxyDeclutterer.cs b/de4dot.code/deobfuscators/MasonProtector/ProxyDeclutterer.cs new file mode 100644 index 00000000..cb5ce5c1 --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/ProxyDeclutterer.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Linq; +using de4dot.blocks; +using dnlib.DotNet; +using dnlib.DotNet.Emit; + +namespace de4dot.code.deobfuscators.MasonProtector; + +/// +/// Removes junk from proxy methods in order to make them classic inline candidates (ldarg..., call, ret). +/// +static class ProxyDeclutterer { + public static List Run(ModuleDefMD module) { + var result = new List(); + foreach (var type in module.Types) { + foreach (var method in type.Methods) { + if (!method.IsStatic || !method.HasBody || method.Body.Instructions.Count is < 4 or > 30) + continue; + + if (!method.Body.Instructions[0].IsLdcI4() || method.Body.Instructions[1].OpCode != OpCodes.Call) + continue; + if (method.Body.Instructions[1].Operand is not IMethod { + FullName: "System.Void System.Diagnostics.Debug::Assert(System.Boolean)" + }) + continue; + + var blocks = new Blocks(method); + var allBlocks = blocks.MethodBlocks.GetAllBlocks(); + + var realBlock = allBlocks.FirstOrDefault(b => + b.LastInstr.OpCode == OpCodes.Ret && b.FirstInstr.OpCode.Code is Code.Ldarg_0 or Code.Call or Code.Newobj); + if (realBlock == null) + continue; + + if (realBlock != allBlocks[0]) { + allBlocks[0].Remove(0, allBlocks[0].Instructions.Count); + allBlocks[0].SetNewFallThrough(realBlock); + } + else { // older protector versions only have the Assert call and no other blocks + allBlocks[0].Remove(0, 2); + } + + blocks.RemoveDeadBlocks(); + blocks.GetCode(out var code, out var eh); + DotNetUtils.RestoreBody(method, code, eh); + + result.Add(method); + } + } + + return result; + } +} diff --git a/de4dot.code/deobfuscators/MasonProtector/ResourceDecrypter.cs b/de4dot.code/deobfuscators/MasonProtector/ResourceDecrypter.cs new file mode 100644 index 00000000..13e697eb --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/ResourceDecrypter.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Security.Cryptography; +using de4dot.blocks; +using de4dot.blocks.cflow; +using dnlib.DotNet; +using dnlib.DotNet.Emit; + +namespace de4dot.code.deobfuscators.MasonProtector; + +public interface IResourceDecrypter { + public void Find(); + public void Initialize(IDeobfuscator deob); + public void DecryptResources(); + + public bool Found { get; } + public IList MethodsToRemove { get; } + public MethodDef VarsInitMethod { get; } +} + +public class ResourceDecrypter : IResourceDecrypter { + readonly ModuleDefMD _module; + readonly ISimpleDeobfuscator _methodDeobfuscator; + + MethodDef _decrypterMethod; + public MethodDef VarsInitMethod { get; private set; } + + string[] _resourceNames; + byte[] _key, _iv; + byte _xorKey; + + public bool Found => VarsInitMethod != null && _decrypterMethod != null; + public IList MethodsToRemove { get; private set; } = Array.Empty(); + + public ResourceDecrypter(ModuleDefMD module, ISimpleDeobfuscator methodDeobfuscator) { + _module = module; + _methodDeobfuscator = methodDeobfuscator; + } + + public void Find() { + foreach (var type in _module.Types) { + foreach (var method in type.Methods) { + if (!method.IsStatic) + continue; + if (!DotNetUtils.IsMethod(method, "System.IO.Stream", "(System.Reflection.Assembly,System.String)")) + continue; + if (!DotNetUtils.CallsMethod(method, "System.IO.Stream System.Reflection.Assembly::GetManifestResourceStream(System.String)")) + continue; + + var ldins = method.Body.Instructions.FirstOrDefault(ins => ins.OpCode == OpCodes.Ldsfld + && ins.Operand is FieldDef def + && def.FieldType.FullName == "System.String[]"); + if (ldins == null) + continue; + + foreach (var initMethod in type.Methods) { + if (!initMethod.IsStatic || !initMethod.HasBody) continue; + if (!DotNetUtils.IsMethod(initMethod, "System.Void", "()")) continue; + if (!initMethod.Body.Instructions.Any(ins => + ins.OpCode == OpCodes.Stsfld && ins.Operand == ldins.Operand)) continue; + if (DotNetUtils.CallsMethod(initMethod, + "System.Void System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(System.Array,System.RuntimeFieldHandle)")) + return; // then it's the newer variant, handled by ResourceDecrypter2 + VarsInitMethod = initMethod; + break; + } + _decrypterMethod = method; + return; + } + } + } + + public void Initialize(IDeobfuscator deob) { + if (!Found) + throw new InvalidOperationException("Must not call Initialize() when Found is false"); + + _methodDeobfuscator.Deobfuscate(VarsInitMethod); + _methodDeobfuscator.DecryptStrings(VarsInitMethod, deob); + + int arrIndex = 0; + var instrs = VarsInitMethod.Body.Instructions; + while (ArrayFinder.FindNewarr(VarsInitMethod, ref arrIndex, out int arrSize)) { + var cts = _module.CorLibTypes.GetCorLibTypeSig((ITypeDefOrRef)instrs[arrIndex].Operand); + if (cts == null) + continue; + if (cts.ElementType == ElementType.U1) { + var ba = ArrayFinder.GetInitializedByteArray(arrSize, VarsInitMethod, ref arrIndex); + if (arrSize == 32) + _key = ba; + else if (arrSize == 16) + _iv = ba; + } + else if (cts.ElementType == ElementType.String) { + var sa = ArrayFinder.GetInitializedArray(arrSize, VarsInitMethod, ref arrIndex, Code.Stelem_Ref); + _resourceNames = sa.OfType().Select(v => v.value).ToArray(); + } + else { + arrIndex++; + } + } + + bool hasXorKey = false; + for (int i = 0; i < VarsInitMethod.Body.Instructions.Count; i++) { + var ins = VarsInitMethod.Body.Instructions[i]; + if (ins.OpCode == OpCodes.Stsfld && VarsInitMethod.Body.Instructions[i - 1].IsLdcI4()) { + _xorKey = (byte)VarsInitMethod.Body.Instructions[i - 1].GetLdcI4Value(); + hasXorKey = true; + } + } + + if (_key == null || _iv == null || _resourceNames == null || !hasXorKey) + throw new Exception($"Not all Mason resource details found (key: {_key != null}, iv: {_iv != null}, names: {_resourceNames != null}, xorKey: {hasXorKey})"); + } + + public void DecryptResources() { + foreach (var res in _resourceNames) { + if (DotNetUtils.GetResource(_module, res) is not EmbeddedResource rsrc) + throw new Exception($"Resource {res} not found"); + + var data = rsrc.CreateReader().ToArray(); + for (int i = 0; i < data.Length; i++) { + data[i] ^= (byte)(_xorKey ^ (i & 255)); + } + + byte[] decrypted; + using (var aes = Aes.Create()) { + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + aes.Key = _key; + aes.IV = _iv; + using (var cryptoTransform = aes.CreateDecryptor()) + decrypted = cryptoTransform.TransformFinalBlock(data, 0, data.Length); + } + + byte[] decompressedBytes; + using (var wrapper = new MemoryStream(decrypted)) { + using (var deflateStream = new DeflateStream(wrapper, CompressionMode.Decompress)) { + using (var decompressed = new MemoryStream()) { + deflateStream.CopyTo(decompressed); + decompressedBytes = decompressed.ToArray(); + } + } + } + + _module.Resources.Remove(rsrc); + _module.Resources.Add(new EmbeddedResource(rsrc.Name, decompressedBytes, rsrc.Attributes)); + } + + // Replace decrypt call with plain Get() + var gmrs = _module.GetMemberRefs().First(mr => mr.Name == "GetManifestResourceStream"); + foreach (var type in _module.GetTypes()) { + foreach (var method in type.Methods) { + if (!method.HasBody) continue; + foreach (var ins in method.Body.Instructions) { + if (ins.OpCode == OpCodes.Call && ins.Operand == _decrypterMethod) { + ins.Operand = gmrs; + } + } + } + } + + MethodsToRemove = new List { _decrypterMethod, VarsInitMethod }; + } +} diff --git a/de4dot.code/deobfuscators/MasonProtector/ResourceDecrypter2.cs b/de4dot.code/deobfuscators/MasonProtector/ResourceDecrypter2.cs new file mode 100644 index 00000000..0a026601 --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/ResourceDecrypter2.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using de4dot.blocks; +using dnlib.DotNet; +using dnlib.DotNet.Emit; + +namespace de4dot.code.deobfuscators.MasonProtector; + +public class ResourceDecrypter2 : IResourceDecrypter { + readonly ModuleDefMD _module; + readonly ISimpleDeobfuscator _methodDeobfuscator; + + MethodDef _decrypterMethod; + public MethodDef VarsInitMethod { get; private set; } + + string[] _resourceNames; + byte[] _array0, _array1; + CiphertextTransform _extraTransform; + + public bool Found => VarsInitMethod != null && _decrypterMethod != null; + public IList MethodsToRemove { get; private set; } = Array.Empty(); + + public ResourceDecrypter2(ModuleDefMD module, ISimpleDeobfuscator methodDeobfuscator) { + _module = module; + _methodDeobfuscator = methodDeobfuscator; + } + + public void Find() { + foreach (var type in _module.Types) { + foreach (var method in type.Methods) { + if (!method.IsStatic) + continue; + if (!DotNetUtils.IsMethod(method, "System.IO.Stream", "(System.Reflection.Assembly,System.String)")) + continue; + if (!DotNetUtils.CallsMethod(method, "System.IO.Stream System.Reflection.Assembly::GetManifestResourceStream(System.String)")) + continue; + + var ldins = method.Body.Instructions.FirstOrDefault(ins => ins.OpCode == OpCodes.Ldsfld + && ins.Operand is FieldDef def + && def.FieldType.FullName == "System.String[]"); + if (ldins == null) + continue; + + foreach (var initMethod in type.Methods) { + if (!initMethod.IsStatic || !initMethod.HasBody) continue; + if (!DotNetUtils.IsMethod(initMethod, "System.Void", "()")) continue; + if (!initMethod.Body.Instructions.Any(ins => + ins.OpCode == OpCodes.Stsfld && ins.Operand == ldins.Operand)) continue; + if (!DotNetUtils.CallsMethod(initMethod, + "System.Void System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(System.Array,System.RuntimeFieldHandle)")) + return; // either the older variant (ResourceDecrypter) or something unknown + + VarsInitMethod = initMethod; + break; + } + _decrypterMethod = method; + return; + } + } + } + + public void Initialize(IDeobfuscator deob) { + if (!Found) + throw new InvalidOperationException("Must not call Initialize() when Found is false"); + + _methodDeobfuscator.Deobfuscate(VarsInitMethod); + _methodDeobfuscator.DecryptStrings(VarsInitMethod, deob); + + var theFields = VarsInitMethod.Body.Instructions + .Where(ins => ins.OpCode == OpCodes.Stsfld) + .Select(ins => (FieldDef)ins.Operand) + .Where(field => field.FieldType.FullName == "System.Byte[]"); + + var aEx = new ArrayExtractor(VarsInitMethod, _module.Mvid!.Value.ToByteArray()); + int i = 0; + foreach (var field in theFields) { + if (!aEx.Run(field, aEx.InstructionIndex)) { + throw new Exception("Run failed"); + } + var data = aEx.GetArrayValues(); + if (data == null) + throw new Exception("Emulation didn't yield data"); + + if (i == 0) _array0 = data; + else if (i == 1) _array1 = data; + else break; + i++; + } + + if (_array1 == null) + throw new Exception("Couldn't find both arrays"); + + int arrIndex = aEx.InstructionIndex; + if (!ArrayFinder.FindNewarr(VarsInitMethod, ref arrIndex, out int arrSize)) + throw new Exception("newarr for strings not found"); + var arr = ArrayFinder.GetInitializedByteArray(arrSize, VarsInitMethod, ref arrIndex); + if (arr == null) + throw new Exception("Unable to obtain resource name data"); + + _resourceNames = new string[BitConverter.ToInt32(arr, 0)]; + int offset = 4; + for (int s = 0; s < _resourceNames.Length; s++) { + int strLen = BitConverter.ToInt32(arr, offset); + offset += 4; + _resourceNames[s] = Encoding.UTF8.GetString(arr, offset, strLen); + offset += strLen; + } + + var extra = _decrypterMethod.Body.Instructions.FirstOrDefault(ins => + ins.OpCode == OpCodes.Call && ins.Operand is MethodDef md && md.DeclaringType == _decrypterMethod.DeclaringType); + if (extra != null) + _extraTransform = CiphertextTransform.Analyze((MethodDef)extra.Operand); + } + + public void DecryptResources() { + foreach (var res in _resourceNames) { + if (DotNetUtils.GetResource(_module, res) is not EmbeddedResource rsrc) + throw new Exception($"Resource {res} not found"); + + var data = rsrc.CreateReader().ToArray(); + //Console.WriteLine("Handling " + res); + + byte[] array2 = _array0; + byte[] array3 = _array1; + byte[] bytes = Encoding.UTF8.GetBytes(res); + byte[] array4 = new byte[32 + array3.Length + bytes.Length]; + Array.Copy(array2, 0, array4, 0, 32); + Array.Copy(array3, 0, array4, 32, array3.Length); + Array.Copy(bytes, 0, array4, 32 + array3.Length, bytes.Length); + var sha = SHA256.Create(); + byte[] array5 = sha.ComputeHash(array4); + array4 = new byte[array5.Length + bytes.Length + 32]; + Array.Copy(array5, 0, array4, 0, array5.Length); + Array.Copy(bytes, 0, array4, array5.Length, bytes.Length); + Array.Copy(array2, 0, array4, array5.Length + bytes.Length, 32); + sha = SHA256.Create(); + byte[] array6 = sha.ComputeHash(array4); + array4 = new byte[array3.Length + array6.Length + array5.Length]; + Array.Copy(array3, 0, array4, 0, array3.Length); + Array.Copy(array6, 0, array4, array3.Length, array6.Length); + Array.Copy(array5, 0, array4, array3.Length + array6.Length, array5.Length); + sha = SHA256.Create(); + byte[] array7 = sha.ComputeHash(array4); + if (data.Length <= 16) { + throw new Exception(); + } + byte[] array8 = new byte[16]; + Array.Copy(data, array8, 16); + + byte[] ciphertext = new byte[data.Length - 16]; + Array.Copy(data, 16, ciphertext, 0, data.Length - 16); + _extraTransform?.TransformArray(ciphertext); + + var aes = Aes.Create(); + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + aes.Key = array7; + aes.IV = array8; + var cryptoTransform = aes.CreateDecryptor(); + byte[] decrypted = cryptoTransform.TransformFinalBlock(ciphertext, 0, ciphertext.Length); + byte[] mvid = _module.Mvid!.Value.ToByteArray(); + array4 = new byte[mvid.Length + array3.Length + bytes.Length]; + Array.Copy(mvid, 0, array4, 0, mvid.Length); + Array.Copy(array3, 0, array4, mvid.Length, array3.Length); + Array.Copy(bytes, 0, array4, mvid.Length + array3.Length, bytes.Length); + sha = SHA256.Create(); + byte[] array12 = sha.ComputeHash(array4); + int j = 0; + int num2 = 0; + while (j < decrypted.Length) { + array4 = new byte[array12.Length + 4]; + Array.Copy(array12, 0, array4, 0, array12.Length); + array4[array12.Length] = (byte)num2; + array4[array12.Length + 1] = (byte)((uint)num2 >> 8); + array4[array12.Length + 2] = (byte)((uint)num2 >> 16); + array4[array12.Length + 3] = (byte)((uint)num2 >> 24); + sha = SHA256.Create(); + byte[] array13 = sha.ComputeHash(array4); + int num3 = decrypted.Length - j; + if (num3 > 32) { + num3 = 32; + } + for (int k = 0; k < num3; k++) { + decrypted[j + k] = (byte)(decrypted[j + k] ^ array13[k]); + } + j += num3; + num2++; + } + + byte[] decompressedBytes; + using (var wrapper = new MemoryStream(decrypted)) { + using (var deflateStream = new DeflateStream(wrapper, CompressionMode.Decompress)) { + using (var decompressed = new MemoryStream()) { + deflateStream.CopyTo(decompressed); + decompressedBytes = decompressed.ToArray(); + } + } + } + + _module.Resources.Remove(rsrc); + _module.Resources.Add(new EmbeddedResource(rsrc.Name, decompressedBytes, rsrc.Attributes)); + } + + // Replace decrypt call with plain Get() + var gmrs = _module.GetMemberRefs().First(mr => mr.Name == "GetManifestResourceStream"); + foreach (var type in _module.GetTypes()) { + foreach (var method in type.Methods) { + if (!method.HasBody) continue; + foreach (var ins in method.Body.Instructions) { + if (ins.OpCode == OpCodes.Call && ins.Operand == _decrypterMethod) { + ins.Operand = gmrs; + } + } + } + } + + MethodsToRemove = new List { _decrypterMethod, VarsInitMethod }; + } +} diff --git a/de4dot.code/deobfuscators/MasonProtector/StringDecrypter.cs b/de4dot.code/deobfuscators/MasonProtector/StringDecrypter.cs new file mode 100644 index 00000000..03378034 --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/StringDecrypter.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using de4dot.blocks; +using de4dot.blocks.cflow; +using dnlib.DotNet; +using dnlib.DotNet.Emit; + +namespace de4dot.code.deobfuscators.MasonProtector; + +public class StringDecrypter { + readonly ModuleDefMD _module; + + public List StringDecrypterInfos { get; } = new(); + + public StringDecrypter(ModuleDefMD module) { + _module = module; + } + + public void Find() { + foreach (var type in _module.Types) { + foreach (var method in type.Methods) { + if (!method.IsStatic) + continue; + if (!DotNetUtils.IsMethod(method, "System.String", "(System.String,System.String,System.Int32)")) + continue; + if (!DotNetUtils.CallsMethod(method, "System.Void System.String::.ctor(System.Char[])")) + continue; + + var instrs = method.Body.Instructions; + int startIndex = -1, endIndex = -1; + for (int i = 0; i < instrs.Count - 10; i++) { + var instr = instrs[i]; + if (instr.OpCode != OpCodes.Callvirt || ((IMethod)instr.Operand).FullName != "System.Char System.String::get_Chars(System.Int32)") + continue; + + // ld new array (for stelem ~10 insns down) + // ld index (for stelem ~10 insns down) + // ld current char + // ld num = (int_4 >> 16) & 255 + if (!instrs[i + 2].IsLdloc() || !instrs[i + 3].IsLdloc() || !instrs[i + 4].IsLdloc() + || !instrs[i + 5].IsLdloc()) { + Console.WriteLine("Instruction mismatch in strdec"); + continue; + } + + startIndex = i + 2; + for (int j = startIndex; j < instrs.Count; j++) { + if (instrs[j].OpCode == OpCodes.Stelem_I2) { + endIndex = j; + break; + } + } + + if (endIndex != -1) + break; + } + + if (endIndex != -1) { + var decryptCharInsns = new List(); + for (int j = startIndex; j < endIndex; j++) + decryptCharInsns.Add(instrs[j].Clone()); + + StringDecrypterInfos.Add(new StrDecrypterInfo(method, decryptCharInsns)); + } + } + } + } + + public string Decrypt(StrDecrypterInfo info, string arg1, string arg2, int arg3) { + var emu = new InstructionEmulator(info.Method); + emu.SetLocal(new Local(null, null, 1), new Int32Value((arg3 >> 16) & 255)); + emu.SetLocal(new Local(null, null, 2), new Int32Value((arg3 >> 8) & 255)); + + var localI = (Local)info.DecryptChar[1].Operand; + var localC = (Local)info.DecryptChar[2].Operand; + string text = arg1 + arg2; + char[] result = new char[text.Length]; + for (int i = 0; i < text.Length; i++) { + emu.SetLocal(localI, new Int32Value(i)); + emu.SetLocal(localC, new Int32Value(text[i])); + foreach (var insn in info.DecryptChar) + emu.Emulate(insn); + var newValue = emu.Pop(); + if (newValue is not Int32Value newCharacter || !newCharacter.AllBitsValid()) + throw new Exception(); + result[i] = (char)newCharacter.Value; + } + + return new string(result); + } + + public class StrDecrypterInfo { + public readonly MethodDef Method; + public readonly List DecryptChar; + + public StrDecrypterInfo(MethodDef method, List decryptChar) { + Method = method; + DecryptChar = decryptChar; + } + } +} diff --git a/de4dot.code/deobfuscators/MasonProtector/StringDecrypter2.cs b/de4dot.code/deobfuscators/MasonProtector/StringDecrypter2.cs new file mode 100644 index 00000000..1880e744 --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/StringDecrypter2.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using de4dot.blocks; +using de4dot.blocks.cflow; +using dnlib.DotNet; +using dnlib.DotNet.Emit; + +namespace de4dot.code.deobfuscators.MasonProtector; + +public class StringDecrypter2 { + readonly ModuleDefMD _module; + readonly Dictionary _fieldData = new(); + + public List StringDecrypterInfos { get; } = new(); + + public StringDecrypter2(ModuleDefMD module) => _module = module; + + public void Find() { + foreach (var type in _module.Types) { + foreach (var method in type.Methods) { + if (!method.IsStatic) + continue; + if (!DotNetUtils.IsMethod(method, "System.String", "(System.Int32,System.Int32,System.Int32,System.Int32)")) + continue; + if (!DotNetUtils.CallsMethod(method, "System.String System.String::Intern(System.String)")) + continue; + + var blocks = new Blocks(method); + var loopBody = blocks.MethodBlocks.GetAllBlocks().FirstOrDefault( + b => b.Instructions.Count > 10 && b.Instructions.FindIndex(ins => ins.OpCode == OpCodes.Ldelem_U2) is >= 0 and < 10); + if (loopBody?.FallThrough == null) { + Logger.w("Proper loop body not found in decryptor method"); + continue; + } + if (loopBody.FallThrough.FirstInstr.OpCode != OpCodes.Ldloc) { + Logger.w("Block following loop body does not load index Local"); + continue; + } + var indexLocal = (Local)loopBody.FallThrough.FirstInstr.Operand; + + var fields = loopBody.Instructions + .Where(ins => ins.OpCode == OpCodes.Ldsfld) + .Select(ins => (FieldDef)ins.Operand) + .Distinct() + .ToArray(); + if (fields.Any(f => f.FieldType.ElementType != ElementType.SZArray)) + continue; + + if (!fields.All(EnsureFieldData)) { + continue; + } + + var stelemIndex = loopBody.Instructions.FindIndex(ins => ins.OpCode == OpCodes.Stelem_I2); + if (stelemIndex == -1) + continue; + + var decryptCharInsns = loopBody.Instructions.Take(stelemIndex).Select(ins => ins.Instruction.Clone()).ToArray(); + StringDecrypterInfos.Add(new StrDecrypterInfo(method, indexLocal, decryptCharInsns)); + } + } + } + + /// Extracts array field data that is initialized in the declaring type's cctor. + bool EnsureFieldData(FieldDef field) { + if (_fieldData.ContainsKey(field)) + return true; + + var cctor = field.DeclaringType.FindStaticConstructor(); + if (cctor == null) + return false; + + var emu = new ArrayExtractor(cctor); + if (!emu.Run(field)) { + Logger.w("[StrDec2] Stsfld for {0} not found in cctor", field); + return false; + } + + var value = emu.Emulator.Pop(); + if (value is not ObjectValue { obj: List valList } + || !valList.All(v => v is Int32Value i32 && i32.AllBitsValid())) { + return false; + } + + _fieldData[field] = value; + return true; + } + + public string Decrypt(StrDecrypterInfo info, int arg0, int arg1, int arg2, int arg3) { + var emu = new InstructionEmulator(info.Method); + int num = arg2 ^ (arg3 & 0xFFFFFF); + emu.SetArg(new Parameter(0), new Int32Value(arg0)); + emu.SetArg(new Parameter(3), new Int32Value(arg3)); + emu.SetLocal(new Local(null, null, 0), new Int32Value((num >> 16) & 255)); + emu.SetLocal(new Local(null, null, 1), new Int32Value((num >> 8) & 255)); + + char[] result = new char[arg1]; + for (int i = 0; i < result.Length; i++) { + emu.SetLocal(info.IndexLocal, new Int32Value(i)); + foreach (var insn in info.DecryptChar) { + if (insn.OpCode == OpCodes.Ldsfld && _fieldData.TryGetValue((FieldDef)insn.Operand, out var value)) + emu.Push(value); + else + emu.Emulate(insn); + } + + var newValue = emu.Pop(); + if (newValue is not Int32Value newCharacter || !newCharacter.AllBitsValid()) + throw new Exception(); + result[i] = (char)newCharacter.Value; + } + + //Console.WriteLine("[dec] " + new string(result)); + return new string(result); + } + + public class StrDecrypterInfo { + public readonly MethodDef Method; + /// A Local that is expected to be set to the current array loop index. + public readonly Local IndexLocal; + /// The instructions that need to be executed in order to decrypt a single character. + public readonly IList DecryptChar; + + public StrDecrypterInfo(MethodDef method, Local indexLocal, IList decryptChar) { + Method = method; + IndexLocal = indexLocal; + DecryptChar = decryptChar; + } + } +} diff --git a/de4dot.code/deobfuscators/MasonProtector/TokenMapper.cs b/de4dot.code/deobfuscators/MasonProtector/TokenMapper.cs new file mode 100644 index 00000000..742d3355 --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/TokenMapper.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +namespace de4dot.code.deobfuscators.MasonProtector; + +/// +/// Helper class for Vault that manages stolen tokens for vaulted IL code. +/// +public class TokenMapper { + + const int NumNonStrTables = 5; // Number of Handle arrays created in the Vault cctor + + private readonly List[] _tokens = new List[NumNonStrTables]; + private readonly List _strings = new(); + + public TokenMapper() { + for (int i = 0; i < _tokens.Length; i++) + _tokens[i] = new(); + } + + public void AddToken(int index, int token) => _tokens[index].Add(token); + public void AddString(string str) => _strings.Add(str); + + public int Map(int theirToken) { + if (MapInternal(theirToken) is int t) + return t; + throw new InvalidOperationException($"{theirToken:X8} does not map to int"); + } + + public string MapString(int theirToken) { + if (MapInternal(theirToken) is string s) + return s; + throw new InvalidOperationException($"{theirToken:X8} does not map to string"); + } + + object MapInternal(int theirToken) { + if (((uint)theirToken & 0x80000000u) == 0) + return theirToken; + + int tokenType = (theirToken >> 24) & 0x7F; + int index = theirToken & 0xFFFFFF; + + switch (tokenType) { + case 0: // methods + return _tokens[0][index]; // [1] is decltypes + case 1: // fields + return _tokens[2][index]; // [3] is decltypes + case 2: // types + return _tokens[4][index]; + case 3: + return _strings[index]; + default: + throw new IndexOutOfRangeException($"Got unknown token type {tokenType}"); + } + } +} diff --git a/de4dot.code/deobfuscators/MasonProtector/Vault.cs b/de4dot.code/deobfuscators/MasonProtector/Vault.cs new file mode 100644 index 00000000..49db890b --- /dev/null +++ b/de4dot.code/deobfuscators/MasonProtector/Vault.cs @@ -0,0 +1,525 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Security.Cryptography; +using de4dot.blocks; +using de4dot.blocks.cflow; +using dnlib.DotNet; +using dnlib.DotNet.Emit; +using dnlib.IO; + +namespace de4dot.code.deobfuscators.MasonProtector; + +public class Vault { + readonly ModuleDefMD _module; + readonly ISimpleDeobfuscator _methodDeobfuscator; + + public TypeDef VaultType { get; private set; } + public Resource VaultResource { get; private set; } + MethodDef _runMethod; + + int[] _decryptedTable; + byte[] _decryptedBlob; + int _intFromCctor; + byte[] _bytesFromCctor; + int _numBlockCopy; + CiphertextTransform _extraTransform; + byte[] _extraHmacKey; + bool _complicatedDerive; + + readonly TokenMapper _tokenMapper = new(); + + public bool CanRemove { get; private set; } + public bool Found => VaultType != null; + + public Vault(ModuleDefMD module, ISimpleDeobfuscator methodDeobfuscator) { + _module = module; + _methodDeobfuscator = methodDeobfuscator; + } + + public void Find() { + foreach (var type in _module.Types) { + foreach (var method in type.Methods) { + if (!method.IsStatic) + continue; + if (!DotNetUtils.IsMethod(method, "System.Void", "()")) + continue; + if (!DotNetUtils.CallsMethod(method, "System.IO.Stream System.Reflection.Assembly::GetManifestResourceStream(System.String)")) + continue; + if (!DotNetUtils.CallsMethod(method, "System.Byte[] System.Security.Cryptography.HashAlgorithm::ComputeHash(System.Byte[])")) + continue; + if (!DotNetUtils.CallsMethod(method,"System.Byte[] System.Security.Cryptography.ICryptoTransform::TransformFinalBlock(System.Byte[],System.Int32,System.Int32)")) + continue; + if (!DotNetUtils.CallsMethod(method,"System.Void System.IO.Compression.DeflateStream::.ctor(System.IO.Stream,System.IO.Compression.CompressionMode)")) + continue; + if (!DotNetUtils.CallsMethod(method,"System.Void System.Array::Clear(System.Array,System.Int32,System.Int32)")) + continue; + + _numBlockCopy = DotNetUtils.GetMethodCalls(method, + "System.Void System.Buffer::BlockCopy(System.Array,System.Int32,System.Array,System.Int32,System.Int32)"); + _complicatedDerive = !DotNetUtils.CallsMethod(method, "System.Byte[] System.Security.Cryptography.DeriveBytes::GetBytes(System.Int32)"); + + VaultType = type; + return; + } + } + } + + public void Initialize(IDeobfuscator deob) { + if (VaultType == null) + throw new InvalidOperationException("Vault type was not found"); + + _runMethod = VaultType.Methods.FirstOrDefault(m => m.IsStatic + && DotNetUtils.IsMethod(m, "System.Object", "(System.Int32,System.RuntimeMethodHandle,System.Object[])")); + if (_runMethod == null) + throw new Exception("Vault run method not found"); + + var vaultCctor = VaultType.FindStaticConstructor(); + if (vaultCctor == null) + throw new Exception("Vault does not have cctor"); + + _methodDeobfuscator.Deobfuscate(vaultCctor); + _methodDeobfuscator.DecryptStrings(vaultCctor, deob); + + if (DotNetUtils.GetResource(_module, DotNetUtils.GetCodeStrings(vaultCctor)) is not EmbeddedResource rsrc) + throw new Exception("Vault resource not found"); + + var insns = vaultCctor.Body.Instructions; + int i, stlocHmacKey = -1; + for (i = 1; i < insns.Count - 2; i++) { + var insn = insns[i]; + if (insn.OpCode == OpCodes.Stsfld && insns[i - 1].IsLdcI4()) { + _intFromCctor = insns[i - 1].GetLdcI4Value(); + } + else if (insn.OpCode == OpCodes.Ldtoken && insn.Operand is FieldDef fieldDef + && fieldDef.Attributes.HasFlag(FieldAttributes.HasFieldRVA)) { + _bytesFromCctor = fieldDef.InitialValue; + if (insns[i + 2].IsStloc()) + stlocHmacKey = i + 2; + } + if (_intFromCctor != 0 && _bytesFromCctor != null) + break; + } + + if (_intFromCctor == 0 || _bytesFromCctor == null) + throw new Exception("Required data not found in vault cctor"); + + // Newer versions of the protector do some transformations involving the ModuleVersionId. + if (stlocHmacKey != -1 && _module.Mvid.HasValue) { + int stlocMvid = -1; + for (i = stlocHmacKey + 1; i < Math.Min(stlocHmacKey + 15, insns.Count); i++) { + if (insns[i].OpCode == OpCodes.Call && insns[i].Operand is IMethod { FullName: "System.Byte[] System.Guid::ToByteArray()" } + && insns[i + 1].IsStloc()) { + stlocMvid = i + 1; + break; + } + } + + if (stlocMvid != -1) { + var emu = new HmacKeyEmu(vaultCctor); + emu.Emulator.SetLocal((Local)insns[stlocHmacKey].Operand, HmacKeyEmu.ByteArrayToValue(_bytesFromCctor)); + emu.Emulator.SetLocal((Local)insns[stlocMvid].Operand, HmacKeyEmu.ByteArrayToValue(_module.Mvid.Value.ToByteArray())); + emu.Run(stlocMvid + 1); + var arrayValue = emu.Emulator.Pop(); + if (arrayValue is ObjectValue { obj: List vals } && vals.All(v => v is Int32Value i32 && i32.AllBitsValid())) { + _bytesFromCctor = vals.Select(v => (byte)((Int32Value)v).Value).ToArray(); + } + else { + Logger.w("ModuleVersionId-based HmacKey transforms failed"); + } + } + } + + FindExtraHmacKeyProtection(); + + for (i = 0; i < insns.Count - 2; i++) { + var insn = insns[i]; + if (insn.OpCode == OpCodes.Ldftn && insns[i + 2].OpCode == OpCodes.Stsfld + && insn.Operand is MethodDef md) { + _extraTransform = CiphertextTransform.Analyze(md); + break; + } + } + + WalkCctor(vaultCctor, deob); + DecryptResource(rsrc.CreateReader().ToArray()); + + VaultResource = rsrc; + } + + void DecryptResource(byte[] rsrcBlob) { + if (rsrcBlob.Length < 65) + throw new InvalidOperationException("Resource too short: " + rsrcBlob.Length); + + byte[] salt = new byte[16]; + Buffer.BlockCopy(rsrcBlob, 0, salt, 0, 16); + byte[] ivRaw = new byte[16]; + byte[] keyRaw = new byte[32]; + Buffer.BlockCopy(rsrcBlob, 16, ivRaw, 0, 16); + Buffer.BlockCopy(rsrcBlob, 32, keyRaw, 0, 32); + byte b = rsrcBlob[64]; + int num = (salt[0] & 63) + 1; + int cryptedOffset = 65 + num; + if (_numBlockCopy > 5) + cryptedOffset += 32; + if (rsrcBlob.Length <= cryptedOffset) + throw new InvalidOperationException("Resource too short: " + rsrcBlob.Length); + + int cryptedCount = rsrcBlob.Length - cryptedOffset; + byte[] crypted = new byte[cryptedCount]; + Buffer.BlockCopy(rsrcBlob, cryptedOffset, crypted, 0, cryptedCount); + + _extraTransform?.TransformArray(crypted); + + byte[] hmacKey = new byte[_bytesFromCctor.Length]; + Buffer.BlockCopy(_bytesFromCctor, 0, hmacKey, 0, hmacKey.Length); + if (_extraHmacKey != null) + for (int i = 0; i < hmacKey.Length; i++) + hmacKey[i] ^= _extraHmacKey[i % _extraHmacKey.Length]; + + byte[] password; + using (var hmacsha = new HMACSHA256(hmacKey)) + password = hmacsha.ComputeHash(salt); + + byte[] bytes; + if (_complicatedDerive) { + bytes = ComplicatedDerive(password, salt, _intFromCctor); + } + else { +#pragma warning disable SYSLIB0041 + using var rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, salt, _intFromCctor); + bytes = rfc2898DeriveBytes.GetBytes(49); +#pragma warning restore SYSLIB0041 + } + + byte[] aesIv = new byte[16]; + byte[] aesKey = new byte[32]; + for (int i = 0; i < 16; i++) + aesIv[i] = (byte)(ivRaw[i] ^ bytes[i]); + + for (int j = 0; j < 32; j++) + aesKey[j] = (byte)(keyRaw[j] ^ bytes[16 + j]); + + byte cryptedXor = (byte)(b ^ bytes[48]); + for (int k = 0; k < crypted.Length; k++) + crypted[k] ^= (byte)(cryptedXor ^ (k & 255)); + + byte[] decrypted; + using (var aes = Aes.Create()) { + aes.Key = aesKey; + aes.IV = aesIv; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + using (var cryptoTransform = aes.CreateDecryptor()) + decrypted = cryptoTransform.TransformFinalBlock(crypted, 0, crypted.Length); + } + + byte[] blob; + using (var wrapper = new MemoryStream(decrypted)) { + using (var deflateStream = new DeflateStream(wrapper, CompressionMode.Decompress)) { + using (var decompressed = new MemoryStream()) { + deflateStream.CopyTo(decompressed); + blob = decompressed.ToArray(); + } + } + } + + int tableCount = ReadInt(blob, 0); + int tableIndex = 4; + int[] table = new int[tableCount]; + for (int l = 0; l < tableCount; l++) { + table[l] = ReadInt(blob, tableIndex); + tableIndex += 4; + } + + _decryptedTable = table; + _decryptedBlob = blob; + } + + static int ReadInt(byte[] array, int offset) + => array[offset] | (array[offset + 1] << 8) | (array[offset + 2] << 16) | (array[offset + 3] << 24); + + void Unvault(MethodDef method, int index) { + if (index < 0 || index >= _decryptedTable.Length) + throw new IndexOutOfRangeException("Illegal vault index: " + index); + + int start = _decryptedTable[index]; + var readerFactory = ByteArrayDataReaderFactory.Create(_decryptedBlob, null); + var reader = readerFactory.CreateReader(start, 36); + + int ilOffset = reader.ReadInt32(); + int ilLength = reader.ReadInt32(); // 4 + int localSigOffset = reader.ReadInt32(); // 8 + int localSigLength = reader.ReadInt32(); // 12 + int maxStack = reader.ReadInt32(); // 16 + /*int tokenOffset = */reader.ReadInt32(); // contains a list of places in IL that have tokens; handled automatically by MethodBodyReader + /*int tokenCount = */reader.ReadInt32(); // 24 + int ehOffset = reader.ReadInt32(); // 28 + int ehCount = reader.ReadInt32(); // 32 + + IList theLocals; + if (localSigLength > 0) { + var sigDataReader = readerFactory.CreateReader(localSigOffset, localSigLength); + var parsedSig = LocalSigReader.ReadSig(_module, _tokenMapper, sigDataReader); + theLocals = parsedSig.Locals.Select(sig => new Local(sig)).ToArray(); + } + else + theLocals = Array.Empty(); + + var ilReader = readerFactory.CreateReader(ilOffset, ilLength); + var mReader = new MethodBodyReader(_module, _tokenMapper, ilReader); + mReader.ReadBody(method, theLocals, maxStack); + + if (ehCount > 0) { + var ehReader = readerFactory.CreateReader(ehOffset, ehCount * 28); + mReader.ReadExceptionHandlers(ehCount, ehReader); + } + + mReader.RestoreMethod(method); + } + + public void UnvaultAll() { + int numFound = 0, numProcessed = 0; + foreach (var type in _module.GetTypes()) { + foreach (var method in type.Methods) { + if (!method.HasBody) continue; + + if (!method.Body.Instructions.Any(i => i.OpCode == OpCodes.Call && i.Operand == _runMethod)) + continue; + + numFound++; + + _methodDeobfuscator.Deobfuscate(method); + + int callIndex = -1; + for (int i = 0; i < method.Body.Instructions.Count; i++) { + var instr = method.Body.Instructions[i]; + if (instr.OpCode == OpCodes.Call && instr.Operand == _runMethod) { + callIndex = i; + break; + } + } + if (callIndex == -1) + throw new Exception("Call lost during deobfuscation"); + + var emu = new InstructionEmulator(method); + var instrs = method.Body.Instructions; + for (int i = 0; i < callIndex; i++) + emu.Emulate(instrs[i]); + emu.Pop(2); + + var indexValue = emu.Pop(); + if (indexValue is not Int32Value indexI32 || !indexI32.AllBitsValid()) { + Logger.w("Failed to determine vault index for {0:X8}", method.MDToken.ToInt32()); + continue; + } + + Logger.n("[Vault] Restoring index " + indexI32.Value); + Unvault(method, indexI32.Value); + numProcessed++; + } + } + + CanRemove = numFound == numProcessed; + } + + void WalkCctor(MethodDef method, IDeobfuscator deob) { + int arrayCounter = 0, start = 0; + var instrs = method.Body.Instructions; + + for (int i = 0; i < instrs.Count; i++) { + var instr = instrs[i]; + if (instr.OpCode == OpCodes.Call && instr.Operand is MethodDef { IsStatic: true } called + && called.DeclaringType == VaultType) { + start = i; + break; + } + } + + for (int i = start; i < instrs.Count; i++) { + var instr = instrs[i]; + if (instr.OpCode == OpCodes.Newarr) { + arrayCounter++; + continue; + } + + if (instr.OpCode == OpCodes.Call && instr.Operand is MethodDef { IsStatic: true } called) { + AnalyzeInitializer(called, deob, arrayCounter); + } + } + } + + void AnalyzeInitializer(MethodDef method, IDeobfuscator deob, int arrayIndex) { + _methodDeobfuscator.Deobfuscate(method); + _methodDeobfuscator.DecryptStrings(method, deob); + + var instrs = method.Body.Instructions; + //int index = -1; + foreach (var insn in instrs) { + /*if (insn.IsLdcI4()) { + index = insn.GetLdcI4Value(); + } + else */if (insn.OpCode == OpCodes.Ldtoken) { + object value = insn.Operand; + _tokenMapper.AddToken(arrayIndex, ((IMDTokenProvider)value).MDToken.ToInt32()); + } + else if (insn.OpCode == OpCodes.Ldstr) { + _tokenMapper.AddString((string)insn.Operand); + } + } + } + + void FindExtraHmacKeyProtection() { + foreach (var method in VaultType.Methods) { + if (method.Parameters.Count != 0 || !method.HasBody || method.Body.Instructions.Count > 200) + continue; + var calls = DotNetUtils.GetMethodCalls(method); + if (calls.Count > 1 && calls.All(m => + DotNetUtils.IsMethod(m, "System.Int32", + "(System.Byte[],System.Byte[],System.Int32,System.Int32)"))) { + _extraHmacKey = new ExtraHmacKeyProtection(method).ConstructKey(); + break; + } + } + } + + static byte[] ComplicatedDerive(byte[] password, byte[] salt, int iters) { + byte[] first; +#pragma warning disable SYSLIB0041 + using (var deriver1 = new Rfc2898DeriveBytes(password, salt, iters)) + first = deriver1.GetBytes(32); + byte[] nextPassword = ScrambleDerive(first, salt, 32768); + byte[] result; + using (var deriver2 = new Rfc2898DeriveBytes(nextPassword, salt, 2048)) + result = deriver2.GetBytes(49); +#pragma warning restore SYSLIB0041 + return result; + } + + static byte[] ComputeSha(byte[] data) + { + using var sha = SHA256.Create(); + return sha.ComputeHash(data); + } + + static byte[] HashBlocks(byte[] arrayIn) + { + byte[] hash1 = ComputeSha(arrayIn); + byte[] hash2 = ComputeSha(hash1); + byte[] output = new byte[64]; + Buffer.BlockCopy(hash1, 0, output, 0, 32); + Buffer.BlockCopy(hash2, 0, output, 32, 32); + return output; + } + + static byte[] ScrambleDerive(byte[] firstDerive, byte[] salt, int number) + { + byte[] array = new byte[firstDerive.Length + salt.Length]; + Buffer.BlockCopy(firstDerive, 0, array, 0, firstDerive.Length); + Buffer.BlockCopy(salt, 0, array, firstDerive.Length, salt.Length); + byte[] array2 = HashBlocks(array); + byte[][] array3 = new byte[number][]; + for (int i = 0; i < number; i++) { + array3[i] = array2; + array2 = HashBlocks(array2); + } + for (int j = 0; j < number; j++) { + int num = (array2[0] | (array2[1] << 8) | (array2[2] << 16) | (array2[3] << 24)) & (number - 1); + byte[] array4 = new byte[64]; + byte[] array5 = array3[num]; + for (int k = 0; k < 64; k++) { + array4[k] = (byte)(array2[k] ^ array5[k]); + } + array2 = HashBlocks(array4); + } + return array2; + } + + private class HmacKeyEmu : InstructionAndBranchEmulator { + public HmacKeyEmu(MethodDef method) : base(method) { + } + + protected override bool OnInstruction(Instruction instr, out bool shouldSkip) { + shouldSkip = false; + return instr.OpCode != OpCodes.Stsfld; + } + + public static Value ByteArrayToValue(byte[] array) + => new ObjectValue(new List(array.Select(b => new Int32Value(b)))); + } + + private class ExtraHmacKeyProtection { + readonly MethodDef _method; + readonly int _startIndex; + readonly ObjectValue _field; + + public ExtraHmacKeyProtection(MethodDef method) { + _method = method; + + var insns = _method.Body.Instructions; + if (insns[0].OpCode != OpCodes.Ldsfld + || insns[0].Operand is not FieldDef fd + || !insns[1].IsStloc()) + throw new Exception("[ExtraHmacKeyProtection] First instr is not ldsfld"); + + for (int i = 2; i < insns.Count; i++) { + var ins = insns[i]; + if (ins.OpCode == OpCodes.Newarr && insns[i - 1].IsLdcI4()) { + _startIndex = i - 1; + break; + } + } + if (_startIndex == 0) + throw new Exception("[ExtraHmacKeyProtection] Start index not found"); + + var ex = new ArrayExtractor(fd.DeclaringType.FindStaticConstructor(), method.Module.Mvid!.Value.ToByteArray()); + if (!ex.Run(fd)) + throw new Exception("[ExtraHmacKeyProtection] Failed to get required field data"); + + _field = ex.GetListObjectValue(); + if (_field == null) + throw new Exception("[ExtraHmacKeyProtection] Failed to get required field data (2)"); + } + + public byte[] ConstructKey() { + var emu = new InstructionEmulator(_method); + emu.SetLocal(_method.Body.Variables[0], _field); + + var insns = _method.Body.Instructions; + for (int i = _startIndex; i < insns.Count; i++) { + if (insns[i].OpCode == OpCodes.Call) { + var called = (MethodDef)insns[i].Operand; + var subEmu = new InstructionEmulator(called); + subEmu.SetArg(new Parameter(3), emu.Pop()); + subEmu.SetArg(new Parameter(2), emu.Pop()); + subEmu.SetArg(new Parameter(1), emu.Pop()); + subEmu.SetArg(new Parameter(0), emu.Pop()); + foreach (var subIns in called.Body.Instructions) { + if (subIns.OpCode == OpCodes.Ret) { + emu.Push(subEmu.Pop()); + break; + } + subEmu.Emulate(subIns); + } + + continue; + } + + if (insns[i].OpCode == OpCodes.Stsfld) { + var value = emu.Pop(); + if (value is not ObjectValue { obj: List vals } + || vals.Any(v => v is not Int32Value i32 || !i32.AllBitsValid())) { + throw new Exception("[ExtraHmacKeyProtection] Non-concrete values"); + } + return vals.Select(v => (byte)((Int32Value)v).Value).ToArray(); + } + + emu.Emulate(insns[i]); + } + + throw new Exception("[ExtraHmacKeyProtection] Stsfld not encountered"); + } + } +} diff --git a/de4dot.cui/Program.cs b/de4dot.cui/Program.cs index 8cf87c98..2b0f54ad 100644 --- a/de4dot.cui/Program.cs +++ b/de4dot.cui/Program.cs @@ -96,6 +96,7 @@ static IList CreateDeobfuscatorInfos() { new de4dot.code.deobfuscators.VirtualGuard.DeobfuscatorInfo(), new de4dot.code.deobfuscators.Xenocode.DeobfuscatorInfo(), new de4dot.code.deobfuscators.DoubleZero.DeobfuscatorInfo(), + new de4dot.code.deobfuscators.MasonProtector.DeobfuscatorInfo(), }; var dict = new Dictionary(); foreach (var d in local)