private DevirtualisationContext CreateDevirtualisationContext(DevirtualisationOptions options) { // Open target file. Logger.Log(Tag, $"Opening target file {options.InputFile}..."); var assembly = WindowsAssembly.FromFile(options.InputFile); var header = assembly.NetDirectory?.MetadataHeader; if (header == null) { throw new BadImageFormatException("Assembly does not contain a valid .NET header."); } // Hook into md stream parser. var parser = new KoiVmAwareStreamParser(options.KoiStreamName, Logger); if (options.OverrideKoiStreamData) { string path = Path.IsPathRooted(options.KoiStreamDataFile) ? options.KoiStreamDataFile : Path.Combine(Path.GetDirectoryName(options.InputFile), options.KoiStreamDataFile); Logger.Log(Tag, $"Opening external Koi stream data file {path}..."); parser.ReplacementData = File.ReadAllBytes(path); var streamHeader = header.StreamHeaders.FirstOrDefault(h => h.Name == options.KoiStreamName); if (streamHeader == null) { streamHeader = new MetadataStreamHeader(options.KoiStreamName) { Stream = KoiStream.FromReadingContext( new ReadingContext() { Reader = new MemoryStreamReader(parser.ReplacementData) }, Logger) }; header.StreamHeaders.Add(streamHeader); } } header.StreamParser = parser; // Ignore invalid / encrypted method bodies when specified. if (options.IgnoreInvalidMethodBodies) { var table = header.GetStream <TableStream>().GetTable <MethodDefinitionTable>(); table.MethodBodyReader = new DefaultMethodBodyReader { ThrowOnInvalidMethodBody = false }; } // Lock image and set custom md resolver. var image = header.LockMetadata(); image.MetadataResolver = new DefaultMetadataResolver( new DefaultNetAssemblyResolver(Path.GetDirectoryName(options.InputFile))); var runtimeImage = ResolveRuntimeImage(options, image); return(new DevirtualisationContext(options, image, runtimeImage, Logger)); }
public InferenceDisassembler(VMConstants constants, KoiStream koiStream) { Constants = constants ?? throw new ArgumentNullException(nameof(constants)); KoiStream = koiStream ?? throw new ArgumentNullException(nameof(koiStream)); _decoder = new InstructionDecoder(constants, KoiStream.Contents.CreateReader()); _processor = new InstructionProcessor(this); _cfgBuilder = new ControlFlowGraphBuilder(); }
private DevirtualisationContext CreateDevirtualisationContext(DevirtualisationOptions options) { string workingDirectory = Path.GetDirectoryName(options.InputFile); // Open target PE. Logger.Log(Tag, $"Opening target file {options.InputFile}..."); var peFile = PEFile.FromFile(options.InputFile); // Create #Koi stream aware metadata reader. var streamReader = options.OverrideKoiStreamData ? new DefaultMetadataStreamReader() : (IMetadataStreamReader) new KoiVmAwareStreamReader(options.KoiStreamName, Logger); var peImage = PEImage.FromFile(peFile, new PEReaderParameters { MetadataStreamReader = streamReader }); var metadata = peImage.DotNetDirectory?.Metadata; if (metadata is null) { throw new BadImageFormatException("Assembly does not contain a valid .NET header."); } // If custom koi stream data was provided, inject it. KoiStream koiStream; if (!options.OverrideKoiStreamData) { koiStream = metadata.GetStream <KoiStream>() ?? throw new DevirtualisationException( "Koi stream was not found in the target PE. This could be because the input file is " + "not protected with KoiVM, or the metadata stream uses a name that is different " + "from the one specified in the input parameters."); } else { string path = Path.IsPathRooted(options.KoiStreamDataFile) ? options.KoiStreamDataFile : Path.Combine(workingDirectory, options.KoiStreamDataFile); Logger.Log(Tag, $"Opening external Koi stream data file {path}..."); var contents = File.ReadAllBytes(path); // Replace original koi stream if it existed. koiStream = new KoiStream(options.KoiStreamName, new DataSegment(contents), Logger); } // Ignore invalid / encrypted method bodies when specified. var moduleReadParameters = new ModuleReaderParameters(workingDirectory, options.IgnoreInvalidMD ? (IErrorListener)ThrowErrorListener.Instance : EmptyErrorListener.Instance); var module = ModuleDefinition.FromImage(peImage, moduleReadParameters); var runtimeModule = ResolveRuntimeModule(options, module); koiStream.ResolutionContext = module; return(new DevirtualisationContext(options, module, runtimeModule, koiStream, Logger)); }
public uint?ResolveExitKey(ILogger logger, KoiStream koiStream, VMConstants constants, VMFunction function) { // Strategy: // // Find CALL references to the function, and try every possible key in the key space that // decrypts the next instruction to either a PUSHR_xxxx R0 or a PUSHR_DWORD SP, as they // appear in the post-call to either store the return value, or adjust SP to clean up // the stack. // Since the actual decryption of the opcode only uses the 8 least significant bits, we can // make an optimisation that shaves off around 8 bits of the key space. By attempting to // decrypt the first byte using just the first 8 bits, and verifying whether this output // results to one of the instructions mentioned in the above, we can quickly rule out many // potential keys. var callReferences = function.References.Where(r => r.ReferenceType == FunctionReferenceType.Call).ToArray(); if (callReferences.Length == 0) { logger.Warning(Tag, $"Cannot brute-force the exit key of function_{function.EntrypointAddress:X4} as it has no recorded call references."); return(null); } var data = koiStream.Data; // Find any call reference. foreach (var callReference in callReferences) { var call = callReference.Caller.Instructions[callReference.Offset]; long targetOffset = call.Offset + call.Size; // Go over all possible LSBs. for (uint lsb = 0; lsb < byte.MaxValue; lsb++) { // Check whether the LSB decodes to a PUSHR_xxxx. if (IsPotentialLSB(constants, data[targetOffset], lsb)) { // Go over all remaining 24 bits. for (uint i = 0; i < 0x00FFFFFF; i++) { uint currentKey = (i << 8) | lsb; // Try new key. if (IsValidKey(constants, data, targetOffset, currentKey)) { // We have found a key! return(currentKey); } } // for all other 24 bits. } // if potential LSB } // foreach LSB } // foreach call reference return(null); }
public DevirtualisationContext(DevirtualisationOptions options, ModuleDefinition targetModule, ModuleDefinition runtimeModule, KoiStream koiStream, ILogger logger) { Options = options ?? throw new ArgumentNullException(nameof(options)); TargetModule = targetModule ?? throw new ArgumentNullException(nameof(targetModule)); RuntimeModule = runtimeModule ?? throw new ArgumentNullException(nameof(runtimeModule)); KoiStream = koiStream ?? throw new ArgumentNullException(nameof(koiStream)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); ReferenceImporter = new ReferenceImporter(targetModule); }
public MetadataStream ReadStream(string streamName, ReadingContext context) { if (streamName != KoiStreamName) { return(_parser.ReadStream(streamName, context)); } if (ReplacementData != null) { context = new ReadingContext() { Reader = new MemoryStreamReader(ReplacementData) }; } return(KoiStream.FromReadingContext(context, Logger)); }
public uint?ResolveExitKey(ILogger logger, KoiStream koiStream, VMConstants constants, VMFunction function) { // Strategy: // // Find CALL references to the function, and try every possible key in the key space that // decrypts the next instruction to either a PUSHR_xxxx R0 or a PUSHR_DWORD SP, as they // appear in the post-call to either store the return value, or adjust SP to clean up // the stack. // Since the actual decryption of the opcode only uses the 8 least significant bits, we can // make an optimisation that shaves off around 8 bits of the key space. By attempting to // decrypt the first byte using just the first 8 bits, and verifying whether this output // results to one of the instructions mentioned in the above, we can quickly rule out many // potential keys. var callReferences = function.References .Where(r => r.ReferenceType == FunctionReferenceType.Call) .ToArray(); if (callReferences.Length == 0) { logger.Warning(Tag, $"Cannot brute-force the exit key of function_{function.EntrypointAddress:X4} as it has no recorded call references."); return(null); } var reader = koiStream.Contents.CreateReader(); byte[] encryptedOpCodes = new byte[3]; var watch = new Stopwatch(); // Find any call reference. for (int i = 0; i < callReferences.Length; i++) { var callReference = callReferences[i]; logger.Debug(Tag, $"Started bruteforcing key for call reference {i.ToString()} ({callReference.ToString()})."); watch.Restart(); var call = callReference.Caller.Instructions[callReference.Offset]; long targetOffset = call.Offset + call.Size; reader.FileOffset = (uint)targetOffset; reader.ReadBytes(encryptedOpCodes, 0, encryptedOpCodes.Length); // Go over all possible LSBs. for (uint lsb = 0; lsb < byte.MaxValue; lsb++) { // Check whether the LSB decodes to a PUSHR_xxxx. if (IsPotentialLSB(constants, encryptedOpCodes[0], lsb)) { // Go over all remaining 24 bits. for (uint j = 0; j < 0x00FFFFFF; j++) { uint currentKey = (j << 8) | lsb; // Try new key. if (IsValidKey(constants, encryptedOpCodes, currentKey)) { // We have found a key! watch.Stop(); logger.Debug(Tag, $"Found key after {watch.Elapsed.TotalSeconds:0.00}s."); return(currentKey); } } // for all other 24 bits. } // if potential LSB } // foreach LSB watch.Stop(); logger.Debug(Tag, $"Exhausted key space after {watch.Elapsed.TotalSeconds:0.00}s without finding key."); } return(null); }