/* Copyright (C) 2011 de4dot@gmail.com This file is part of de4dot. de4dot is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. de4dot is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with de4dot. If not, see . */ using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.MyStuff; using de4dot.deobfuscators; using de4dot.blocks; using de4dot.blocks.cflow; using de4dot.AssemblyClient; namespace de4dot { class ObfuscatedFile : IObfuscatedFile, IDeobfuscatedFile { Options options; ModuleDefinition module; IList allMethods; IDeobfuscator deob; AssemblyModule assemblyModule; IAssemblyClient assemblyClient; DynamicStringDecrypter dynamicStringDecrypter; IAssemblyClientFactory assemblyClientFactory; SavedMethodBodies savedMethodBodies; bool userStringDecrypterMethods = false; class SavedMethodBodies { Dictionary savedMethodBodies = new Dictionary(); class SavedMethodBody { MethodDefinition method; IList instructions; IList exceptionHandlers; public SavedMethodBody(MethodDefinition method) { this.method = method; DotNetUtils.copyBody(method, out instructions, out exceptionHandlers); } public void restore() { DotNetUtils.restoreBody(method, instructions, exceptionHandlers); } } public void save(MethodDefinition method) { if (isSaved(method)) return; savedMethodBodies[method] = new SavedMethodBody(method); } public void restoreAll() { foreach (var smb in savedMethodBodies.Values) smb.restore(); savedMethodBodies.Clear(); } public bool isSaved(MethodDefinition method) { return savedMethodBodies.ContainsKey(method); } } public class Options { public string Filename { get; set; } public string MethodsFilename { get; set; } public string NewFilename { get; set; } public string ForcedObfuscatorType { get; set; } public DecrypterType StringDecrypterType { get; set; } public List StringDecrypterMethods { get; private set; } public bool RenameSymbols { get; set; } public bool ControlFlowDeobfuscation { get; set; } public bool KeepObfuscatorTypes { get; set; } public Options() { StringDecrypterType = DecrypterType.Default; StringDecrypterMethods = new List(); } } public string Filename { get { return options.Filename; } } public string NewFilename { get { return options.NewFilename; } } public ModuleDefinition ModuleDefinition { get { return module; } } public Func IsValidName { get { return deob.IsValidName; } } public bool RenameResourcesInCode { get { return deob.TheOptions.RenameResourcesInCode; } } public bool RenameSymbols { get { return options.RenameSymbols; } } public IDeobfuscator Deobfuscator { get { return deob; } } public ObfuscatedFile(Options options, IAssemblyClientFactory assemblyClientFactory) { this.assemblyClientFactory = assemblyClientFactory; this.options = options; userStringDecrypterMethods = options.StringDecrypterMethods.Count > 0; options.Filename = Utils.getFullPath(options.Filename); assemblyModule = new AssemblyModule(options.Filename, options.MethodsFilename); if (options.NewFilename == null) options.NewFilename = getDefaultNewFilename(); if (string.Equals(options.Filename, options.NewFilename, StringComparison.OrdinalIgnoreCase)) throw new UserException(string.Format("filename is same as new filename! ({0})", options.Filename)); } string getDefaultNewFilename() { int dotIndex = options.Filename.LastIndexOf('.'); string noExt, ext; if (dotIndex != -1) { noExt = options.Filename.Substring(0, dotIndex); ext = options.Filename.Substring(dotIndex); } else { noExt = options.Filename; ext = ""; } return noExt + "-fixed" + ext; } public void load(IEnumerable deobfuscators) { module = assemblyModule.load(); AssemblyResolver.Instance.addSearchDirectory(Utils.getDirName(Filename)); AssemblyResolver.Instance.addSearchDirectory(Utils.getDirName(NewFilename)); allMethods = getAllMethods(); detectObfuscator(deobfuscators); if (deob == null) throw new ApplicationException("Could not detect obfuscator!"); initializeDeobfuscator(); } void initializeDeobfuscator() { if (options.StringDecrypterType == DecrypterType.Default) options.StringDecrypterType = deob.DefaultDecrypterType; if (options.StringDecrypterType == DecrypterType.Default) options.StringDecrypterType = DecrypterType.Static; deob.Operations = createOperations(); } IOperations createOperations() { var op = new Operations(); switch (options.StringDecrypterType) { case DecrypterType.None: op.DecryptStrings = OpDecryptString.None; break; case DecrypterType.Static: op.DecryptStrings = OpDecryptString.Static; break; default: op.DecryptStrings = OpDecryptString.Dynamic; break; } op.RenameSymbols = options.RenameSymbols; op.KeepObfuscatorTypes = options.KeepObfuscatorTypes; return op; } void detectObfuscator(IEnumerable deobfuscators) { // The deobfuscators may call methods to deobfuscate control flow and decrypt // strings (statically) in order to detect the obfuscator. if (!options.ControlFlowDeobfuscation || options.StringDecrypterType == DecrypterType.None) savedMethodBodies = new SavedMethodBodies(); foreach (var deob in deobfuscators) { deob.init(module); deob.DeobfuscatedFile = this; } if (options.ForcedObfuscatorType != null) { foreach (var deob in deobfuscators) { if (string.Equals(options.ForcedObfuscatorType, deob.Type, StringComparison.OrdinalIgnoreCase)) { deob.earlyDetect(); deob.detect(); this.deob = deob; return; } } } else { this.deob = earlyDetectObfuscator(deobfuscators); if (this.deob == null) this.deob = detectObfuscator2(deobfuscators); else this.deob.detect(); } } IDeobfuscator earlyDetectObfuscator(IEnumerable deobfuscators) { IDeobfuscator detected = null; int detectVal = 0; foreach (var deob in deobfuscators) { int val = deob.earlyDetect(); if (val > 0) Log.v("{0,3}: {1}", val, deob.Type); if (val > detectVal) { detectVal = val; detected = deob; } } return detected; } IDeobfuscator detectObfuscator2(IEnumerable deobfuscators) { IDeobfuscator detected = null; int detectVal = 0; foreach (var deob in deobfuscators) { int val = deob.detect(); Log.v("{0,3}: {1}", val, deob.Type); if (val > detectVal) { detectVal = val; detected = deob; } } return detected; } public void save() { Log.n("Saving {0}", options.NewFilename); assemblyModule.save(options.NewFilename); } IList getAllMethods() { var list = new List(); foreach (var type in module.GetTypes()) { foreach (var method in type.Methods) list.Add(method); } return list; } public void deobfuscateBegin() { switch (options.StringDecrypterType) { case DecrypterType.None: checkSupportedStringDecrypter(StringFeatures.AllowNoDecryption); break; case DecrypterType.Static: checkSupportedStringDecrypter(StringFeatures.AllowStaticDecryption); break; case DecrypterType.Delegate: case DecrypterType.Emulate: checkSupportedStringDecrypter(StringFeatures.AllowDynamicDecryption); assemblyClient = assemblyClientFactory.create(); assemblyClient.connect(); break; default: throw new ApplicationException(string.Format("Invalid string decrypter type '{0}'", options.StringDecrypterType)); } } public void checkSupportedStringDecrypter(StringFeatures feature) { if ((deob.StringFeatures & feature) == feature) return; throw new UserException(string.Format("Deobfuscator {0} does not support this string decryption type", deob.Type)); } public void deobfuscate() { Log.n("Cleaning {0}", options.Filename); initAssemblyClient(); byte[] fileData = null; Dictionary dumpedMethods = null; if (deob.getDecryptedModule(ref fileData, ref dumpedMethods)) reloadModule(fileData, dumpedMethods); deob.deobfuscateBegin(); deobfuscateMethods(); deob.deobfuscateEnd(); } void reloadModule(byte[] newModuleData, Dictionary dumpedMethods) { Log.v("Decrypted data. Reloading decrypted data (original filename: {0})", Filename); simpleDeobfuscatorFlags.Clear(); module = assemblyModule.reload(newModuleData, dumpedMethods); allMethods = getAllMethods(); deob = deob.moduleReloaded(module); initializeDeobfuscator(); deob.DeobfuscatedFile = this; updateDynamicStringDecrypter(); } void initAssemblyClient() { if (assemblyClient == null) return; assemblyClient.waitConnected(); assemblyClient.Service.loadAssembly(options.Filename); if (options.StringDecrypterType == DecrypterType.Delegate) assemblyClient.Service.setStringDecrypterType(AssemblyData.StringDecrypterType.Delegate); else if (options.StringDecrypterType == DecrypterType.Emulate) assemblyClient.Service.setStringDecrypterType(AssemblyData.StringDecrypterType.Emulate); else throw new ApplicationException(string.Format("Invalid string decrypter type '{0}'", options.StringDecrypterType)); dynamicStringDecrypter = new DynamicStringDecrypter(assemblyClient); updateDynamicStringDecrypter(); } void updateDynamicStringDecrypter() { if (dynamicStringDecrypter != null) dynamicStringDecrypter.init(getMethodTokens()); } IEnumerable getMethodTokens() { var tokens = new List(); if (!userStringDecrypterMethods) { options.StringDecrypterMethods.Clear(); options.StringDecrypterMethods.AddRange(deob.getStringDecrypterMethods()); } foreach (var val in options.StringDecrypterMethods) { var tokenStr = val.Trim(); if (Utils.StartsWith(tokenStr, "0x", StringComparison.OrdinalIgnoreCase)) tokenStr = tokenStr.Substring(2); int methodToken; if (int.TryParse(tokenStr, NumberStyles.HexNumber, null, out methodToken)) tokens.Add(methodToken); else tokens.AddRange(findMethodTokens(val)); } return tokens; } IEnumerable findMethodTokens(string methodDesc) { var tokens = new List(); string typeString, methodName; string[] argsStrings; splitMethodDesc(methodDesc, out typeString, out methodName, out argsStrings); foreach (var type in module.GetTypes()) { if (typeString != null && typeString != type.FullName) continue; foreach (var method in type.Methods) { if (!method.IsStatic || method.MethodReturnType.ReturnType.FullName != "System.String") continue; if (methodName != null && methodName != method.Name) continue; if (argsStrings == null) { if (method.Parameters.Count == 0) continue; } else { if (argsStrings.Length != method.Parameters.Count) continue; for (int i = 0; i < argsStrings.Length; i++) { if (argsStrings[i] != method.Parameters[i].ParameterType.FullName) continue; } } Log.v("Adding string decrypter; token: {0:X8}, method: {1}", method.MetadataToken.ToInt32(), method.FullName); tokens.Add(method.MetadataToken.ToInt32()); } } return tokens; } static void splitMethodDesc(string methodDesc, out string type, out string name, out string[] args) { string stringArgs = null; args = null; type = null; name = null; var remaining = methodDesc; int index = remaining.LastIndexOf("::"); if (index >= 0) { type = remaining.Substring(0, index); remaining = remaining.Substring(index + 2); } index = remaining.IndexOf('('); if (index >= 0) { name = remaining.Substring(0, index); remaining = remaining.Substring(index); } else { name = remaining; remaining = ""; } if (Utils.StartsWith(remaining, "(", StringComparison.Ordinal)) { stringArgs = remaining; } else if (remaining.Length > 0) throw new UserException(string.Format("Invalid method desc: '{0}'", methodDesc)); if (stringArgs != null) { if (Utils.StartsWith(stringArgs, "(", StringComparison.Ordinal)) stringArgs = stringArgs.Substring(1); if (stringArgs.EndsWith(")", StringComparison.Ordinal)) stringArgs = stringArgs.Substring(0, stringArgs.Length - 1); args = stringArgs.Split(','); for (int i = 0; i < args.Length; i++) args[i] = args[i].Trim(); } if (type == "") type = null; if (name == "") name = null; } public void deobfuscateEnd() { deobfuscateCleanUp(); } public void deobfuscateCleanUp() { if (assemblyClient != null) { assemblyClient.Dispose(); assemblyClient = null; } } void deobfuscateMethods() { if (savedMethodBodies != null) { savedMethodBodies.restoreAll(); savedMethodBodies = null; } deob.DeobfuscatedFile = null; Log.v("Deobfuscating methods"); var methodPrinter = new MethodPrinter(); var cflowDeobfuscator = new BlocksCflowDeobfuscator(); foreach (var method in allMethods) { Log.v("Deobfuscating {0} ({1:X8})", method, method.MetadataToken.ToUInt32()); Log.indent(); if (hasNonEmptyBody(method)) { var blocks = new Blocks(method); int numRemovedLocals = 0; int oldNumInstructions = method.Body.Instructions.Count; deob.deobfuscateMethodBegin(blocks); if (options.ControlFlowDeobfuscation) { cflowDeobfuscator.init(blocks); cflowDeobfuscator.deobfuscate(); } if (deob.deobfuscateOther(blocks) && options.ControlFlowDeobfuscation) { cflowDeobfuscator.init(blocks); cflowDeobfuscator.deobfuscate(); } if (options.ControlFlowDeobfuscation) { numRemovedLocals = blocks.optimizeLocals(); blocks.repartitionBlocks(); } deobfuscateStrings(blocks); deob.deobfuscateMethodEnd(blocks); IList allInstructions; IList allExceptionHandlers; blocks.getCode(out allInstructions, out allExceptionHandlers); DotNetUtils.restoreBody(method, allInstructions, allExceptionHandlers); if (numRemovedLocals > 0) Log.v("Removed {0} unused local(s)", numRemovedLocals); int numRemovedInstructions = oldNumInstructions - method.Body.Instructions.Count; if (numRemovedInstructions > 0) Log.v("Removed {0} dead instruction(s)", numRemovedInstructions); const Log.LogLevel dumpLogLevel = Log.LogLevel.veryverbose; if (Log.isAtLeast(dumpLogLevel)) { Log.log(dumpLogLevel, "Deobfuscated code:"); Log.indent(); methodPrinter.print(dumpLogLevel, allInstructions, allExceptionHandlers); Log.deIndent(); } } removeNoInliningAttribute(method); Log.deIndent(); } } class MethodPrinter { Log.LogLevel logLevel; IList allInstructions; IList allExceptionHandlers; Dictionary targets = new Dictionary(); Dictionary labels = new Dictionary(); class ExInfo { public List tryStarts = new List(); public List tryEnds = new List(); public List filterStarts = new List(); public List handlerStarts = new List(); public List handlerEnds = new List(); } Dictionary exInfos = new Dictionary(); ExInfo lastExInfo; public void print(Log.LogLevel logLevel, IList allInstructions, IList allExceptionHandlers) { try { this.logLevel = logLevel; this.allInstructions = allInstructions; this.allExceptionHandlers = allExceptionHandlers; lastExInfo = new ExInfo(); print(); } finally { this.allInstructions = null; this.allExceptionHandlers = null; targets.Clear(); labels.Clear(); exInfos.Clear(); lastExInfo = null; } } void initTargets() { foreach (var instr in allInstructions) { switch (instr.OpCode.OperandType) { case OperandType.ShortInlineBrTarget: case OperandType.InlineBrTarget: setTarget(instr.Operand as Instruction); break; case OperandType.InlineSwitch: foreach (var targetInstr in (Instruction[])instr.Operand) setTarget(targetInstr); break; } } foreach (var ex in allExceptionHandlers) { setTarget(ex.TryStart); setTarget(ex.TryEnd); setTarget(ex.FilterStart); setTarget(ex.HandlerStart); setTarget(ex.HandlerEnd); } var sortedTargets = new List(targets.Keys); sortedTargets.Sort((a, b) => { if (a.Offset < b.Offset) return -1; if (a.Offset > b.Offset) return 1; return 0; }); for (int i = 0; i < sortedTargets.Count; i++) labels[sortedTargets[i]] = string.Format("label_{0}", i); } void setTarget(Instruction instr) { if (instr != null) targets[instr] = true; } void initExHandlers() { foreach (var ex in allExceptionHandlers) { if (ex.TryStart != null) { getExInfo(ex.TryStart).tryStarts.Add(ex); getExInfo(ex.TryEnd).tryEnds.Add(ex); } if (ex.FilterStart != null) getExInfo(ex.FilterStart).filterStarts.Add(ex); if (ex.HandlerStart != null) { getExInfo(ex.HandlerStart).handlerStarts.Add(ex); getExInfo(ex.HandlerEnd).handlerEnds.Add(ex); } } } ExInfo getExInfo(Instruction instruction) { if (instruction == null) return lastExInfo; ExInfo exInfo; if (!exInfos.TryGetValue(instruction, out exInfo)) exInfos[instruction] = exInfo = new ExInfo(); return exInfo; } void print() { initTargets(); initExHandlers(); Log.indent(); foreach (var instr in allInstructions) { if (targets.ContainsKey(instr)) { Log.deIndent(); Log.log(logLevel, "{0}:", getLabel(instr)); Log.indent(); } ExInfo exInfo; if (exInfos.TryGetValue(instr, out exInfo)) printExInfo(exInfo); var instrString = instr.GetOpCodeString(); var operandString = getOperandString(instr); var memberReference = instr.Operand as MemberReference; if (operandString == "") Log.log(logLevel, "{0}", instrString); else if (memberReference != null) Log.log(logLevel, "{0,-9} {1} // {2:X8}", instrString, operandString, memberReference.MetadataToken.ToUInt32()); else Log.log(logLevel, "{0,-9} {1}", instrString, operandString); } printExInfo(lastExInfo); Log.deIndent(); } string getOperandString(Instruction instr) { if (instr.Operand is Instruction) return getLabel((Instruction)instr.Operand); else if (instr.Operand is Instruction[]) { var sb = new StringBuilder(); var targets = (Instruction[])instr.Operand; for (int i = 0; i < targets.Length; i++) { if (i > 0) sb.Append(','); sb.Append(getLabel(targets[i])); } return sb.ToString(); } else if (instr.Operand is string) return Utils.toCsharpString((string)instr.Operand); else return instr.GetOperandString(); } void printExInfo(ExInfo exInfo) { Log.deIndent(); foreach (var ex in exInfo.tryStarts) Log.log(logLevel, "// try start: {0}", getExceptionString(ex)); foreach (var ex in exInfo.tryEnds) Log.log(logLevel, "// try end: {0}", getExceptionString(ex)); foreach (var ex in exInfo.filterStarts) Log.log(logLevel, "// filter start: {0}", getExceptionString(ex)); foreach (var ex in exInfo.handlerStarts) Log.log(logLevel, "// handler start: {0}", getExceptionString(ex)); foreach (var ex in exInfo.handlerEnds) Log.log(logLevel, "// handler end: {0}", getExceptionString(ex)); Log.indent(); } string getExceptionString(ExceptionHandler ex) { var sb = new StringBuilder(); if (ex.TryStart != null) sb.Append(string.Format("TRY: {0}-{1}", getLabel(ex.TryStart), getLabel(ex.TryEnd))); if (ex.FilterStart != null) sb.Append(string.Format(", FILTER: {0}", getLabel(ex.FilterStart))); if (ex.HandlerStart != null) sb.Append(string.Format(", HANDLER: {0}-{1}", getLabel(ex.HandlerStart), getLabel(ex.HandlerEnd))); sb.Append(string.Format(", TYPE: {0}", ex.HandlerType)); if (ex.CatchType != null) sb.Append(string.Format(", CATCH: {0}", ex.CatchType)); return sb.ToString(); } string getLabel(Instruction instr) { if (instr == null) return ""; return labels[instr]; } } bool hasNonEmptyBody(MethodDefinition method) { return method.HasBody && method.Body.Instructions.Count > 0; } void deobfuscateStrings(Blocks blocks) { switch (options.StringDecrypterType) { case DecrypterType.None: break; case DecrypterType.Static: deob.deobfuscateStrings(blocks); break; case DecrypterType.Delegate: case DecrypterType.Emulate: dynamicStringDecrypter.decrypt(blocks); break; default: throw new ApplicationException(string.Format("Invalid string decrypter type '{0}'", options.StringDecrypterType)); } } void removeNoInliningAttribute(MethodDefinition method) { method.ImplAttributes = method.ImplAttributes & ~MethodImplAttributes.NoInlining; } public override string ToString() { if (options == null || options.Filename == null) return base.ToString(); return options.Filename; } [Flags] enum SimpleDeobFlags { HasDeobfuscated = 0x1, } Dictionary simpleDeobfuscatorFlags = new Dictionary(); bool check(MethodDefinition method, SimpleDeobFlags flag) { SimpleDeobFlags oldFlags; simpleDeobfuscatorFlags.TryGetValue(method, out oldFlags); simpleDeobfuscatorFlags[method] = oldFlags | flag; return (oldFlags & flag) == flag; } void deobfuscate(MethodDefinition method, string msg, Action handler) { if (savedMethodBodies != null) savedMethodBodies.save(method); Log.v("{0}: {1} ({2:X8})", msg, method, method.MetadataToken.ToUInt32()); Log.indent(); if (hasNonEmptyBody(method)) { var blocks = new Blocks(method); handler(blocks); IList allInstructions; IList allExceptionHandlers; blocks.getCode(out allInstructions, out allExceptionHandlers); DotNetUtils.restoreBody(method, allInstructions, allExceptionHandlers); } Log.deIndent(); } void ISimpleDeobfuscator.deobfuscate(MethodDefinition method) { if (check(method, SimpleDeobFlags.HasDeobfuscated)) return; deobfuscate(method, "Deobfuscating control flow", (blocks) => { var cflowDeobfuscator = new BlocksCflowDeobfuscator(); cflowDeobfuscator.init(blocks); cflowDeobfuscator.deobfuscate(); }); } void ISimpleDeobfuscator.decryptStrings(MethodDefinition method, IDeobfuscator theDeob) { deobfuscate(method, "Static string decryption", (blocks) => theDeob.deobfuscateStrings(blocks)); } void IDeobfuscatedFile.createAssemblyFile(byte[] data, string assemblyName, string extension) { if (extension == null) extension = ".dll"; var baseDir = Utils.getDirName(options.NewFilename); var newName = Path.Combine(baseDir, assemblyName + extension); Log.n("Creating file {0}", newName); using (var writer = new BinaryWriter(new FileStream(newName, FileMode.Create))) { writer.Write(data); } } void IDeobfuscatedFile.stringDecryptersAdded() { updateDynamicStringDecrypter(); } } }