From f89d4239a1fa256b1995bfe8f45552face688840 Mon Sep 17 00:00:00 2001 From: Melissa Eckardt Date: Wed, 22 Apr 2026 15:37:26 +0200 Subject: [PATCH] Babel.NET: Fix string and resource decryption for v10/v11 --- .../deobfuscators/Babel_NET/BabelInflater.cs | 27 ++++-- .../deobfuscators/Babel_NET/BabelUtils.cs | 27 +++++- .../Babel_NET/InflaterCreator.cs | 91 ++++++++++++++++++- .../Babel_NET/ResourceDecrypter.cs | 85 ++++++++++++++++- .../Babel_NET/StringDecrypter.cs | 91 +++++++++++++++++-- 5 files changed, 296 insertions(+), 25 deletions(-) diff --git a/de4dot.code/deobfuscators/Babel_NET/BabelInflater.cs b/de4dot.code/deobfuscators/Babel_NET/BabelInflater.cs index 9cfdb886d..6eb461aa6 100644 --- a/de4dot.code/deobfuscators/Babel_NET/BabelInflater.cs +++ b/de4dot.code/deobfuscators/Babel_NET/BabelInflater.cs @@ -17,17 +17,25 @@ You should have received a copy of the GNU General Public License along with de4dot. If not, see . */ +using System; using ICSharpCode.SharpZipLib; using ICSharpCode.SharpZipLib.Zip.Compression; namespace de4dot.code.deobfuscators.Babel_NET { class BabelInflater : Inflater { - int magic; + static readonly int[] DefaultMapping = new int[] { 1, 5, 6 }; // STORED_BLOCK (0), STATIC_TREES (1), DYN_TREES (2) + readonly int _typeSize; + readonly int _magic; + readonly int[] _blockTypeMapping; - public BabelInflater(bool noHeader, int magic) : base(noHeader) => this.magic = magic; + public BabelInflater(bool noHeader, int typeSize, int magic, int[] blockTypeMapping) : base(noHeader) { + _typeSize = typeSize; + _magic = magic; + _blockTypeMapping = blockTypeMapping ?? DefaultMapping; + } protected override bool ReadHeader(ref bool isLastBlock, out int blockType) { - const int numBits = 4; + int numBits = 1 + _typeSize; int type = input.PeekBits(numBits); if (type < 0) { @@ -38,12 +46,11 @@ protected override bool ReadHeader(ref bool isLastBlock, out int blockType) { if ((type & 1) != 0) isLastBlock = true; - switch (type >> 1) { - case 1: blockType = STORED_BLOCK; break; - case 5: blockType = STATIC_TREES; break; - case 6: blockType = DYN_TREES; break; - default: throw new SharpZipBaseException("Unknown block type: " + type); - } + + blockType = Array.IndexOf(_blockTypeMapping, type >> 1); + if (blockType == -1) + throw new SharpZipBaseException("Unknown block type: " + type); + return true; } @@ -52,7 +59,7 @@ protected override bool DecodeStoredLength() { return false; input.DropBits(16); - uncomprLen ^= magic; + uncomprLen ^= _magic; return true; } diff --git a/de4dot.code/deobfuscators/Babel_NET/BabelUtils.cs b/de4dot.code/deobfuscators/Babel_NET/BabelUtils.cs index dd669c87c..26027b09a 100644 --- a/de4dot.code/deobfuscators/Babel_NET/BabelUtils.cs +++ b/de4dot.code/deobfuscators/Babel_NET/BabelUtils.cs @@ -37,7 +37,7 @@ public static EmbeddedResource FindEmbeddedResource(ModuleDefMD module, TypeDef public static EmbeddedResource FindEmbeddedResource(ModuleDefMD module, TypeDef decrypterType, Action fixMethod) { foreach (var method in decrypterType.Methods) { - if (!DotNetUtils.IsMethod(method, "System.String", "()")) + if (!DotNetUtils.IsMethod(method, "System.String", "()") && !DotNetUtils.IsMethod(method, "System.String", "(System.Int32)")) continue; if (!method.IsStatic) continue; @@ -66,9 +66,18 @@ static EmbeddedResource FindEmbeddedResource2(ModuleDefMD module, MethodDef meth if (!GetXorKey2(method, out int xorKey)) return null; + int xorKeyArg = 0; + foreach (var methodCaller in method.DeclaringType.Methods) { + if (!methodCaller.HasBody) continue; + + var insns = methodCaller.Body.Instructions; + if (GetXorKeyArgument(methodCaller.Body.Instructions, ref xorKeyArg)) + break; + } + var sb = new StringBuilder(encryptedString.Length); foreach (var c in encryptedString) - sb.Append((char)(c ^ xorKey)); + sb.Append((char)(c ^ xorKey ^ xorKeyArg)); return DotNetUtils.GetResource(module, sb.ToString()) as EmbeddedResource; } @@ -94,6 +103,18 @@ static bool GetXorKey2(MethodDef method, out int xorKey) { return false; } + static bool GetXorKeyArgument(IList insns, ref int xorKey) { + for (var i = 0; i < insns.Count - 4; i++) { + if (insns[i].OpCode == OpCodes.Ldsfld && insns[i + 1].IsLdcI4() + && insns[i + 2].OpCode == OpCodes.Callvirt + && insns[i + 3].OpCode == OpCodes.Callvirt) { + xorKey = insns[i + 1].GetLdcI4Value(); + return true; + } + } + return false; + } + public static bool FindRegisterMethod(TypeDef type, out MethodDef regMethod, out MethodDef handler) { foreach (var method in type.Methods) { if (!method.IsStatic || method.Body == null) @@ -114,6 +135,8 @@ public static bool FindRegisterMethod(TypeDef type, out MethodDef regMethod, out handler = DotNetUtils.GetMethod(type, handlerRef); if (handler == null) continue; + if (handler.Body != null && handler.Body.Instructions.Count == 4 && handler.Body.Instructions[2].OpCode.Code == Code.Call) + handler = (MethodDef)handler.Body.Instructions[2].Operand; if (handler.Body == null || handler.Body.ExceptionHandlers.Count != 1) continue; diff --git a/de4dot.code/deobfuscators/Babel_NET/InflaterCreator.cs b/de4dot.code/deobfuscators/Babel_NET/InflaterCreator.cs index 91e1bc43f..323da3bee 100644 --- a/de4dot.code/deobfuscators/Babel_NET/InflaterCreator.cs +++ b/de4dot.code/deobfuscators/Babel_NET/InflaterCreator.cs @@ -17,6 +17,8 @@ You should have received a copy of the GNU General Public License along with de4dot. If not, see . */ +using System.Collections.Generic; +using System.Linq; using ICSharpCode.SharpZipLib.Zip.Compression; using dnlib.DotNet; using dnlib.DotNet.Emit; @@ -24,18 +26,22 @@ You should have received a copy of the GNU General Public License namespace de4dot.code.deobfuscators.Babel_NET { class InflaterCreator { - public static Inflater Create(MethodDef method, bool noHeader) => Create(FindInflaterType(method), noHeader); + public static Inflater Create(MethodDef method, ISimpleDeobfuscator deobfuscator, bool noHeader) => Create(FindInflaterType(method), deobfuscator, noHeader); - public static Inflater Create(TypeDef inflaterType, bool noHeader) { + public static Inflater Create(TypeDef inflaterType, ISimpleDeobfuscator deobfuscator, bool noHeader) { if (inflaterType == null) return CreateNormal(noHeader); var initHeaderMethod = FindInitHeaderMethod(inflaterType); if (initHeaderMethod == null) return CreateNormal(noHeader, "Could not find inflater init header method"); + deobfuscator.Deobfuscate(initHeaderMethod); var magic = GetMagic(initHeaderMethod); if (!magic.HasValue) return CreateNormal(noHeader); - return new BabelInflater(noHeader, magic.Value); + + var mapping = GetBlockTypeMapping(initHeaderMethod); + var size = GetTypeSize(initHeaderMethod); + return new BabelInflater(noHeader, size ?? 3, magic.Value, mapping); } static Inflater CreateNormal(bool noHeader) => CreateNormal(noHeader, null); @@ -113,5 +119,84 @@ static MethodDef FindInitHeaderMethod2(TypeDef nested) { return null; } + + /// + /// Analyzes a chain of "if (this.blockType == x)" checks in the initHeader method. + /// + /// initHeader method + /// An array with 3 values corresponding to STORED_BLOCK, STATIC_TREES and DYN_TREES; null if analysis failed. + static int[] GetBlockTypeMapping(MethodDef method) { + if (!method.HasBody || !method.Body.HasInstructions) + return null; + + var instrs = method.Body.Instructions; + var blocks = new List<(Instruction Ldarg, Instruction Target, int Value)>(); + for (int i = 0; i < instrs.Count - 3; i++) { + var i0 = instrs[i]; // load "this" + var i1 = instrs[i + 1]; // field load for blockType + var i2 = instrs[i + 2]; // type value to check against + var i3 = instrs[i + 3]; // bne to next check + + if (!i0.IsLdarg()) continue; + if (i1.OpCode.Code != Code.Ldfld) continue; + if (!i2.IsLdcI4()) continue; + if (i3.OpCode.Code is not (Code.Bne_Un or Code.Bne_Un_S)) continue; + if (i3.Operand is not Instruction target) continue; + + blocks.Add((i0, target, i2.GetLdcI4Value())); + } + + if (blocks.Count != 3) + return null; + + // Each block starts with ldarg; create a mapping ldarg -> block + var map = blocks.ToDictionary(b => b.Ldarg); + + // Find start of chain (where ldarg is not a target of any other) + var start = blocks.FirstOrDefault(b => !blocks.Any(x => x.Target == b.Ldarg)); + if (start.Ldarg == null) + return null; + + var result = new List(); + var current = start; + var visited = new HashSet(); + + while (true) { + if (!visited.Add(current.Ldarg)) + return null; // cycle, shouldn't happen + + result.Add(current.Value); + + if (!map.TryGetValue(current.Target, out var next)) + break; // last bne.un ends the chain + + current = next; + } + + return result.Count == 3 ? result.ToArray() : null; + } + + /// + /// Determines the size (in bits) of the block type. This does not include the "last block" bit. + /// + /// initHeader method + /// Number of bits or null + static int? GetTypeSize(MethodDef method) { + if (method == null || method.Body == null) + return null; + var instrs = method.Body.Instructions; + for (int i = 0; i < instrs.Count - 6; i++) { + if (!instrs[i].IsLdarg()) continue; + if (!instrs[i + 1].IsLdarg()) continue; + if (instrs[i + 2].OpCode != OpCodes.Ldfld) continue; + if (!instrs[i + 3].IsLdcI4()) continue; + if (instrs[i + 4].OpCode != OpCodes.Callvirt) continue; + if (instrs[i + 5].OpCode != OpCodes.Stfld) continue; + + return instrs[i + 3].GetLdcI4Value(); + } + + return null; + } } } diff --git a/de4dot.code/deobfuscators/Babel_NET/ResourceDecrypter.cs b/de4dot.code/deobfuscators/Babel_NET/ResourceDecrypter.cs index 14893fd3a..f00da1b88 100644 --- a/de4dot.code/deobfuscators/Babel_NET/ResourceDecrypter.cs +++ b/de4dot.code/deobfuscators/Babel_NET/ResourceDecrypter.cs @@ -147,9 +147,9 @@ class Decrypter3 : IDecrypter { ModuleDefMD module; Inflater inflater; - public Decrypter3(ModuleDefMD module, MethodDef decryptMethod) { + public Decrypter3(ModuleDefMD module, MethodDef decryptMethod, ISimpleDeobfuscator deobfuscator) { this.module = module; - inflater = InflaterCreator.Create(decryptMethod, true); + inflater = InflaterCreator.Create(decryptMethod, deobfuscator, true); } public byte[] Decrypt(byte[] encryptedData) { @@ -199,6 +199,80 @@ bool GetKeyIv(byte[] headerData, out byte[] key, out byte[] iv) { } } + // v10/v11 + class Decrypter4 : IDecrypter { + ModuleDefMD module; + Inflater inflater; + + public Decrypter4(ModuleDefMD module, MethodDef decryptMethod, ISimpleDeobfuscator deobfuscator) { + this.module = module; + inflater = InflaterCreator.Create(decryptMethod, deobfuscator, true); + } + + public byte[] Decrypt(byte[] encryptedData) { + int index = 0; + ParseHeader(GetHeaderData(encryptedData, ref index, out var iv), + out var key, + out var flag, + out var cipherType); + bool isEncrypted = (flag & 2) != 0; + bool isCompressed = (flag & 1) != 0; + + byte[] data = new byte[encryptedData.Length - index]; + Array.Copy(encryptedData, index, data, 0, encryptedData.Length - index); + + if (isEncrypted) { + if (cipherType == 1) + data = DeobUtils.DesDecrypt(data, 0, data.Length, key, iv); + else if (cipherType is 2 or 4) + data = DeobUtils.AesDecrypt(data, key, iv); + else if (cipherType == 3) + data = DeobUtils.Des3Decrypt(data, key, iv); + else + throw new Exception($"Unsupported cipher type {cipherType}"); + } + + if (isCompressed) { + data = DeobUtils.Inflate(data, inflater); + } + + return data; + } + + byte[] GetHeaderData(byte[] encryptedData, ref int index, out byte[] iv) { + var headerData = new byte[BitConverter.ToUInt16(encryptedData, index)]; + Array.Copy(encryptedData, index + 2, headerData, 0, headerData.Length); + index += headerData.Length + 2; + + iv = new byte[encryptedData[index++]]; + Array.Copy(encryptedData, index, iv, 0, iv.Length); + index += iv.Length; + for (int i = 0; i < headerData.Length; i++) + headerData[i] ^= iv[i % iv.Length]; + + return headerData; + } + + void ParseHeader(byte[] headerData, out byte[] key, out byte flag, out byte cipherType) { + var reader = new BinaryReader(new MemoryStream(headerData)); + + /*var license =*/ reader.ReadString(); + flag = reader.ReadByte(); + cipherType = reader.ReadByte(); + byte pubKeyOffset = reader.ReadByte(); + + key = reader.ReadBytes(reader.ReadByte()); + if (pubKeyOffset < 64) { + if (reader.BaseStream.Position < reader.BaseStream.Length) + throw new Exception("Expected end of header"); + } + else { + Array.Copy(module.Assembly.PublicKey.Data, pubKeyOffset + 12, key, 0, key.Length); + //key[5] |= 0x80; + } + } + } + public MethodDef DecryptMethod { set { if (value == null) @@ -222,7 +296,8 @@ public static MethodDef FindDecrypterMethod(MethodDef method) { var calledMethod = instr.Operand as MethodDef; if (calledMethod == null || !calledMethod.IsStatic || calledMethod.Body == null) continue; - if (!DotNetUtils.IsMethod(calledMethod, "System.IO.MemoryStream", "(System.IO.Stream)")) + if (!DotNetUtils.IsMethod(calledMethod, "System.IO.MemoryStream", "(System.IO.Stream)") + && !DotNetUtils.IsMethod(calledMethod, "System.IO.Stream", "(System.IO.Stream)")) continue; return calledMethod; @@ -238,8 +313,10 @@ public byte[] Decrypt(byte[] encryptedData) { } IDecrypter CreateDecrypter(byte[] encryptedData) { + if (decryptMethod != null && DeobUtils.HasInteger(decryptMethod, 64)) + return new Decrypter4(module, decryptMethod, simpleDeobfuscator); if (decryptMethod != null && DeobUtils.HasInteger(decryptMethod, 6)) - return new Decrypter3(module, decryptMethod); + return new Decrypter3(module, decryptMethod, simpleDeobfuscator); if (IsV30(encryptedData)) return new Decrypter1(module); return new Decrypter2(module); diff --git a/de4dot.code/deobfuscators/Babel_NET/StringDecrypter.cs b/de4dot.code/deobfuscators/Babel_NET/StringDecrypter.cs index 999f50f41..d5cf89739 100644 --- a/de4dot.code/deobfuscators/Babel_NET/StringDecrypter.cs +++ b/de4dot.code/deobfuscators/Babel_NET/StringDecrypter.cs @@ -20,6 +20,7 @@ You should have received a copy of the GNU General Public License using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using dnlib.DotNet; using dnlib.DotNet.Emit; @@ -145,11 +146,11 @@ IDecrypterInfo CheckDecrypterType(TypeDef type) { return null; if (type.NestedTypes.Count > 2) return null; - if (type.Fields.Count > 1) + if (type.Fields.Count > 3) return null; foreach (var nested in type.NestedTypes) { - var info = CheckNested(type, nested); + var info = CheckNested(type, nested) ?? CheckNested2(type, nested); if (info != null) return info; } @@ -277,11 +278,37 @@ IDecrypterInfo CheckNested(TypeDef type, TypeDef nested) { return null; } + IDecrypterInfo CheckNested2(TypeDef type, TypeDef nested) { + // 10.0/11.0, possibly earlier versions too + if (nested.HasProperties || nested.HasEvents) + return null; + + var decrypterBuilderMethod = DotNetUtils.GetMethod(nested, "System.Reflection.Emit.MethodBuilder", "(System.Reflection.Emit.TypeBuilder)"); + if (decrypterBuilderMethod == null) + return null; + + var cctor = type.FindStaticConstructor(); + if (cctor == null) return null; + resourceDecrypter.DecryptMethod = ResourceDecrypter.FindDecrypterMethod(cctor); + + var decrypter = DotNetUtils.GetMethod(type, "System.String", "(System.Int32)"); + if (decrypter is not { IsStatic: true }) + return null; + + simpleDeobfuscator.Deobfuscate(decrypterBuilderMethod); + return new DecrypterInfoV3(resourceDecrypter) { + Decrypter = decrypter, + OffsetCalcInstructions = GetOffsetCalcInstructions(decrypterBuilderMethod), + }; + } + class ReflectionToDNLibMethodCreator { MethodDef method; List instructions = new List(); InstructionEmulator emulator; int index; + private int _skipSwitchLoopIndex = -1; + private byte[] _xorValues; class UserValue : UnknownValue { public readonly object obj; @@ -294,10 +321,41 @@ public override string ToString() { } public List Instructions => instructions; + public int ParsedValue { get; private set; } public ReflectionToDNLibMethodCreator(MethodDef method) { this.method = method; emulator = new InstructionEmulator(method); + AnalyzeSwitch(); + } + + private void AnalyzeSwitch() { + var switchIns = method.Body.Instructions.FirstOrDefault(ins => ins.OpCode.Code == Code.Switch); + if (switchIns == null) + return; + + for (int i = 0; i < method.Body.Instructions.IndexOf(switchIns); i++) { + if (method.Body.Instructions[i].OpCode.Code is Code.Bge or Code.Bge_S) { + _skipSwitchLoopIndex = method.Body.Instructions.IndexOf((Instruction)method.Body.Instructions[i].Operand); + break; + } + } + if (_skipSwitchLoopIndex == -1) + throw new Exception("decrypterBuilderMethod analysis failed"); + + int x = 0; + _xorValues = new byte[((Instruction[])switchIns.Operand).Length]; + foreach (var targetIns in (Instruction[])switchIns.Operand) { + _xorValues[x++] = (byte)GetSwitchXor(method.Body.Instructions.IndexOf(targetIns)); + } + } + + private int GetSwitchXor(int caseStart) { + for (int i = caseStart; i < Math.Min(caseStart + 10, method.Body.Instructions.Count); i++) { + if (method.Body.Instructions[i].IsLdcI4()) + return method.Body.Instructions[i].GetLdcI4Value(); + } + throw new Exception("Ldc.i4 not found in switch case"); } public bool Create() { @@ -364,6 +422,20 @@ public bool Create() { emulator.Push(new UserValue((IField)instr.Operand)); break; + case Code.Switch: + emulator.Pop(); + if (_xorValues != null) { + // Directly handle the xoring logic here and skip it. + array = emulator.GetLocal(1); + if (array is UserValue { obj: byte[] b }) { + for (int i = 0; i < b.Length; i++) { + b[i] ^= _xorValues[i % _xorValues.Length]; + } + } + index = _skipSwitchLoopIndex; + } + break; + default: emulator.Emulate(instr); break; @@ -386,7 +458,8 @@ bool DoCall(Instruction instr) { return true; } else if (fn == "System.Int32 System.Int32::Parse(System.String)") { - emulator.Push(new Int32Value(int.Parse(((StringValue)emulator.Pop()).value))); + ParsedValue = int.Parse(((StringValue)emulator.Pop()).value); + emulator.Push(new Int32Value(ParsedValue)); return true; } else if (fn == "System.String[] System.String::Split(System.Char[])") { @@ -449,8 +522,14 @@ static List GetOffsetCalcInstructions(MethodDef method) { int endInstr = index - 1; var transformInstructions = new List(); - for (int i = startInstr; i <= endInstr; i++) - transformInstructions.Add(instrs[i]); + for (int i = startInstr; i <= endInstr; i++) { + if (instrs[i].IsLdloc() && creator.ParsedValue != 0) { + transformInstructions.Add(OpCodes.Ldc_I4.ToInstruction(creator.ParsedValue)); + } + else + transformInstructions.Add(instrs[i]); + } + return transformInstructions; } @@ -533,7 +612,7 @@ public void Initialize() { return; if (decrypterInfo.NeedsResource) { - encryptedResource = BabelUtils.FindEmbeddedResource(module, decrypterType); + encryptedResource = BabelUtils.FindEmbeddedResource(module, decrypterType, method => simpleDeobfuscator.Deobfuscate(method)); if (encryptedResource == null) return; }