From 4672bd3286d1a81b8d1218103f075fcb96e3ae6e Mon Sep 17 00:00:00 2001 From: Hendrik Eckardt Date: Tue, 17 Jun 2025 16:09:41 +0200 Subject: [PATCH] Add deobfuscator for string encoding seen in an XWorm sample --- .../deobfuscators/RATMalware/Deobfuscator.cs | 218 ++++++++++++++++++ .../RATMalware/StringDecrypter.cs | 88 +++++++ de4dot.cui/Program.cs | 1 + 3 files changed, 307 insertions(+) create mode 100644 de4dot.code/deobfuscators/RATMalware/Deobfuscator.cs create mode 100644 de4dot.code/deobfuscators/RATMalware/StringDecrypter.cs diff --git a/de4dot.code/deobfuscators/RATMalware/Deobfuscator.cs b/de4dot.code/deobfuscators/RATMalware/Deobfuscator.cs new file mode 100644 index 00000000..336d64aa --- /dev/null +++ b/de4dot.code/deobfuscators/RATMalware/Deobfuscator.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.Text; +using de4dot.blocks; +using dnlib.DotNet; +using dnlib.DotNet.Emit; + +namespace de4dot.code.deobfuscators.RATMalware +{ + public class DeobfuscatorInfo : DeobfuscatorInfoBase { + internal const string THE_NAME = "RATMalware"; + public const string THE_TYPE = "rat"; + 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() + }); + } + } + + /** + * Deobfuscator for a type of string obfuscation seen in XWorm RAT samples. + * It uses double base64 encoding and XOR string decryption function for strings considered critical (like IP). + */ + internal class Deobfuscator : DeobfuscatorBase { + private int _score; + private StringDecrypter _stringDecrypter; + + 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; + + protected override int DetectInternal() => _score; + + private static readonly string[] CallChain = new[] { + "System.Byte[] System.Convert::FromBase64String(System.String)", + "System.String System.Text.Encoding::GetString(System.Byte[])", + "System.Byte[] System.Convert::FromBase64String(System.String)", + "System.String System.Text.Encoding::GetString(System.Byte[])" + }; + + protected override void ScanForObfuscator() { + _stringDecrypter = new StringDecrypter(module); + _stringDecrypter.Find(); + /* Don't wanna cause false detections in other binaries + if (_stringDecrypter.Detected) { + _score += 10; + }*/ + + foreach (var type in module.Types) { + if (!type.HasMethods) + continue; + + foreach (var m in type.Methods) { + if (m.Body == null) continue; + + // Check for ldstr followed by a bunch of calls. + int state = -1; + foreach (var instr in m.Body.Instructions) { + if (state == -1) { + if (instr.OpCode.Code == Code.Ldstr) + state = 0; + } + else if (instr.OpCode.Code is Code.Call or Code.Callvirt + && instr.Operand is IMethod calledMethod + && calledMethod.FullName == CallChain[state]) { + if (++state == CallChain.Length) { + _score += 50; + return; + } + } + else // no call after ldstr + state = -1; + } + } + } + } + + /** + * Gets the value of a static string field for a given type. + */ + private static string FindStaticFieldAssignment(TypeDef typeDef, string name) { + var cctor = typeDef.FindStaticConstructor(); + var blocks = new Blocks(cctor); + DecodeDoubleBase64(blocks); + + string currentStr = null; + foreach (var block in blocks.MethodBlocks.GetAllBlocks()) { + foreach (var instr in block.Instructions) { + if (instr.OpCode.Code == Code.Ldstr) { + currentStr = instr.Operand as string; + } + else if (instr.OpCode.Code == Code.Stsfld && instr.Operand is IField field && field.Name == name) { + return currentStr; + } + } + } + + return null; + } + + public override void DeobfuscateBegin() { + base.DeobfuscateBegin(); + + foreach (var decrypterMethod in _stringDecrypter.StringDecrypters) { + var blocks = new Blocks(decrypterMethod); + DecodeDoubleBase64(blocks); + _stringDecrypter.ObtainKey(decrypterMethod, blocks); + + staticStringInliner.Add(decrypterMethod, + (method, gim, args) => { + string str; + if (args[0] is FieldDef fieldDef) { + str = FindStaticFieldAssignment(fieldDef.DeclaringType, fieldDef.Name); + if (str == null) + throw new Exception("Unable to find assignment to " + fieldDef.FullName); + } + else + str = (string)args[0]; + + return _stringDecrypter.Decrypt(method, str); + }); + } + DeobfuscatedFile.StringDecryptersAdded(); + } + + public override IEnumerable GetStringDecrypterMethods() => new List(); + + public override void DeobfuscateMethodBegin(Blocks blocks) + { + base.DeobfuscateMethodBegin(blocks); + + DecodeDoubleBase64(blocks); + } + + /** + * Decodes string literals that have been encoded with base64 twice. + */ + private static void DecodeDoubleBase64(Blocks blocks) { + /* + nop + call class [mscorlib]System.Text.Encoding [mscorlib]System.Text.Encoding::get_UTF8() + nop + call class [mscorlib]System.Text.Encoding [mscorlib]System.Text.Encoding::get_UTF8() + ldstr "BASE64-LITERAL" + call uint8[] [mscorlib]System.Convert::FromBase64String(string) + callvirt instance string [mscorlib]System.Text.Encoding::GetString(uint8[]) + call uint8[] [mscorlib]System.Convert::FromBase64String(string) + callvirt instance string [mscorlib]System.Text.Encoding::GetString(uint8[]) + */ + foreach (var block in blocks.MethodBlocks.GetAllBlocks()) { + int state = 0; + for (int i = 1; i < block.Instructions.Count; i++) { + var instr = block.Instructions[i]; + if (instr.OpCode.Code is Code.Call or Code.Callvirt + && instr.Operand is IMethod calledMethod + && calledMethod.FullName == CallChain[state]) { + if (++state == CallChain.Length) { + // Check for ldstr above calls. + if (block.Instructions[i - CallChain.Length].OpCode.Code == Code.Ldstr) { + var b64 = (string)block.Instructions[i - CallChain.Length].Operand; + var decoded = Encoding.UTF8.GetString( + Convert.FromBase64String(Encoding.UTF8.GetString(Convert.FromBase64String(b64)))); + block.Replace(i - CallChain.Length, 5, OpCodes.Ldstr.ToInstruction(decoded)); + i -= CallChain.Length - 1; + // Not sure if the nops between get_UTF8 are always present, so we handle it separately. + i -= KillGetUtf8(block, i - 2); + } + + state = 0; + } + } + else { + state = 0; + } + } + } + } + + /** + * Removes a sequence of nop/call Encoding.UTF8 going upwards from the start index. + * Returns the number of removed instructions. + */ + private static int KillGetUtf8(Block block, int startIndex) { + for (int i = startIndex; i >= 0; i--) { + var instr = block.Instructions[i]; + if ((instr.OpCode.Code == Code.Call && instr.Operand is IMethod { FullName: "System.Text.Encoding System.Text.Encoding::get_UTF8()" }) + || instr.OpCode.Code == Code.Nop) { + if (i == 0) { + block.Remove(0, startIndex + 1); + return startIndex + 1; + } + continue; + } + + if (startIndex - i > 0) { + block.Remove(i + 1, startIndex - i); + } + return startIndex - i; + } + + return 0; + } + } +} diff --git a/de4dot.code/deobfuscators/RATMalware/StringDecrypter.cs b/de4dot.code/deobfuscators/RATMalware/StringDecrypter.cs new file mode 100644 index 00000000..2a075662 --- /dev/null +++ b/de4dot.code/deobfuscators/RATMalware/StringDecrypter.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using dnlib.DotNet; +using dnlib.DotNet.Emit; +using de4dot.blocks; + +namespace de4dot.code.deobfuscators.RATMalware { + class StringDecrypter { + readonly ModuleDefMD _module; + + readonly MethodDefAndDeclaringTypeDict _stringDecrypterMethods = new(); + + private class StringDecrypterInfo { + public readonly MethodDef Method; + public string Key; + public StringDecrypterInfo(MethodDef method) => Method = method; + } + + public bool Detected => _stringDecrypterMethods.Count > 0; + + public IEnumerable StringDecrypters => _stringDecrypterMethods.GetKeys(); + + public StringDecrypter(ModuleDefMD module) => _module = module; + + public void Find() { + foreach (var type in _module.GetTypes()) { + FindStringDecrypterMethods(type); + } + } + + void FindStringDecrypterMethods(TypeDef type) + { + foreach (var method in DotNetUtils.FindMethods(type.Methods, "System.String", + new[] { "System.String" })) { + if (!DotNetUtils.CallsMethod(method, "System.Char Microsoft.VisualBasic.Strings::Chr(System.Int32)") + || !DotNetUtils.CallsMethod(method, "System.Int32 Microsoft.VisualBasic.Strings::Asc(System.Char)") + || !DotNetUtils.CallsMethod(method, "System.Byte[] System.Convert::FromBase64String(System.String)")) + continue; + + // We don't look for the key string here right away, because it might still be encoded. + var info = new StringDecrypterInfo(method); + _stringDecrypterMethods.Add(info.Method, info); + Logger.v("Found string decrypter method " + Utils.RemoveNewlines(info.Method)); + } + } + + /** + * This callback is called for each call to one of the string decrypter methods. + */ + public string Decrypt(MethodDef method, string str) { + var info = _stringDecrypterMethods.Find(method); + if (info == null) + throw new ArgumentException("Passed method is not a string decrypter"); + + var key = info.Key; + + var bytes = Convert.FromBase64String(str); + var chrArr = new char[bytes.Length]; + int i = 0, keyIndex = 0; + foreach (byte b in bytes) { + chrArr[i++] = (char)(b ^ (byte)key[keyIndex]); + keyIndex = (keyIndex + 1) % key.Length; + } + + return string.Intern(new string(chrArr)); + } + + /** + * Should be called to associate a decrypter method with its key, once it has been decrypted. + * We assume the key is simply the first ldstr instruction. + */ + public void ObtainKey(MethodDef method, Blocks blocks) { + var info = _stringDecrypterMethods.Find(method); + if (info == null) throw new ArgumentException("Passed method is not a string decrypter"); + + foreach (var block in blocks.MethodBlocks.GetAllBlocks()) { + foreach (var instr in block.Instructions.Where(instr => instr.OpCode.Code == Code.Ldstr)) + { + info.Key = instr.Operand as string; + return; + } + } + + throw new Exception("Could not obtain key for " + method.FullName); + } + } +} diff --git a/de4dot.cui/Program.cs b/de4dot.cui/Program.cs index 2bafd724..85a89308 100644 --- a/de4dot.cui/Program.cs +++ b/de4dot.cui/Program.cs @@ -88,6 +88,7 @@ static IList CreateDeobfuscatorInfos() { new de4dot.code.deobfuscators.MPRESS.DeobfuscatorInfo(), new de4dot.code.deobfuscators.Obfuscar.DeobfuscatorInfo(), new de4dot.code.deobfuscators.Phoenix_Protector.DeobfuscatorInfo(), + new de4dot.code.deobfuscators.RATMalware.DeobfuscatorInfo(), new de4dot.code.deobfuscators.Rummage.DeobfuscatorInfo(), new de4dot.code.deobfuscators.Skater_NET.DeobfuscatorInfo(), new de4dot.code.deobfuscators.SmartAssembly.DeobfuscatorInfo(),