// Detect It Easy: detection rule file /* ██████╗ ███████╗███╗ ██╗███████╗██████╗ ██╗ ██████╗ ██╔════╝ ██╔════╝████╗ ██║██╔════╝██╔══██╗██║██╔════╝ ██║ ███╗█████╗ ██╔██╗ ██║█████╗ ██████╔╝██║██║ ██║ ██║██╔══╝ ██║╚██╗██║██╔══╝ ██╔══██╗██║██║ ╚██████╔╝███████╗██║ ╚████║███████╗██║ ██║██║╚██████╗ ╚═════╝ ╚══════╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═════╝ ██╗ ██╗ ██╗███████╗██╗ ██╗██████╗ ██╗███████╗████████╗██╗ ██████╗ ██╗ ██╔╝ ██║ ██║██╔════╝██║ ██║██╔══██╗██║██╔════╝╚══██╔══╝██║██╔════╝ ╚██╗ ██╔╝ ███████║█████╗ ██║ ██║██████╔╝██║███████╗ ██║ ██║██║ ╚██╗ ╚██╗ ██╔══██║██╔══╝ ██║ ██║██╔══██╗██║╚════██║ ██║ ██║██║ ██╔╝ ╚██╗ ██║ ██║███████╗╚██████╔╝██║ ██║██║███████║ ██║ ██║╚██████╗ ██╔╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ █████╗ ███╗ ██╗ █████╗ ██╗ ██╗ ██╗███████╗██╗███████╗ ██╔══██╗████╗ ██║██╔══██╗██║ ╚██╗ ██╔╝██╔════╝██║██╔════╝ ███████║██╔██╗ ██║███████║██║ ╚████╔╝ ███████╗██║███████╗ ██╔══██║██║╚██╗██║██╔══██║██║ ╚██╔╝ ╚════██║██║╚════██║ ██║ ██║██║ ╚████║██║ ██║███████╗██║ ███████║██║███████║ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝╚═╝ ╚══════╝╚═╝╚══════╝ */ // ┌────────────────────────┤ ≡ ABOUT ≡ ├───────────────[–][▫][×]┐ // │ │ // │ This heuristic engine helps detect the presence of │ // │ obfuscation, packers, licensing systems, debugging │ // │ information, file integrity issues, or even potential │ // │ malicious code using advanced and complex checks. It │ // │ includes pinpoint scanning, intelligent inspections, │ // │ signature-based analysis, native code emulation, │ // │ thorough file content evaluation, and much more! >:) │ // │ │ // │ Supported architectures: │ // │ 1. I386 (x86-32) 2. AMD64 (x86-64) │ // │ 3. ARM (A32) 4. THUMB (T32) │ // │ 5. ARMNT (ARMv7 Thumb-2) 6. ARM64 (AArch64) │ // │ │ // └─────────────────────────────────────────────────────────────┘ // ┌──────────────────────┤ ⌂ CONTACTS ⌂ ├───────────────────────┐ // │ │ // │ ┌ Author is DosX │ // │ └┬─ GitHub: https://github.com/DosX-dev │ // │ ├─ Website: https://dosx.su │ // │ └─ Telegram: @DosX_dev │ // │ │ // ├─────────────────────────────────────────────────────────────┤ // │ If I don't respond to email, message to Telegram │ // └─────────────────────────────────────────────────────────────┘ // ┌───────────────────────┤ ♥ DONATE ♥ ├────────────────────────┐ // │ │ // │ ┌ Did you like my work? Thank you! What about donation? │ // │ │ This will greatly help the development! │ // │ └─┐ │ // │ ├─ BTC: bc1qclzlujunh8zyyxaar8y8t0chwfardesq9uvphh │ // │ │ │ // │ ├─ SOL: 6693kpkspRU2fauVpMkVznnETGX7GdMsLFQED6QFfaWa │ // │ │ │ // │ ├─ TRX: TYezKdq5YkrSmmUy6d9e212KcHxQigtTqJ │ // │ │ │ // │ ├─ ETH: 0xd5f1e450a4A9325165ed6b5ed5E28Da882AB932a │ // │ │ ERC-20 ─┘ │ // │ │ │ // │ └─ USDT: TYezKdq5YkrSmmUy6d9e212KcHxQigtTqJ │ // │ TRC-20 ─┘ │ // │ │ // └─────────────────────────────────────────────────────────────┘ // ┌───────────────────────┤ © LICENSE © ├───────────────────────┐ // │ │ // │ ┌ Copyright (C) 2026 DosX. MIT License. │ // │ └─┐ │ // │ ├─ √ You CAN: │ // │ │ ├─ Use in personal and commercial projects │ // │ │ ├─ Modify and adapt the code │ // │ │ └─ Redistribute, including in modified form │ // │ │ │ // │ └─ × You CANNOT: │ // │ ├─ Remove or alter the copyright notice │ // │ ├─ Claim authorship of the original code │ // │ └─ Hold the author liable for any damages │ // │ │ // └─────────────────────────────────────────────────────────────┘ // ┌───────────────────────────────────────────────┐ // │ For the module to work correctly, official │ // │ Detect It Easy components are recommended │ // ├───────────────────────────────────────────────┘ // └─┐ // └── Please don't read the code out loud // unless you have exorcism skills "use strict"; const PE_Cached = {}, // PE-related cache-map logType = { warning: -2, // Service warnings about: -1, // Information about a module nothing: 0, // General information about the scanning process any: 1, // Native and .NET net: 2, // .NET only emulation: 3 // Instruction emulation }, heurLabel = "HEUR"; var lastOffsetDetected = "0x00"; /** Entry point used by DIE; rejects unsupported environments before scanning. */ function detect() { if (stubForWrongEnvironment()) return null; // 'PE' is undefined if (stubForLegacyEngines()) return null; // old DIE version return main(); } /** Initializes shared PE data, runs the enabled heuristic passes, and releases the cache. */ function main() { initializeCache(); if (PE.isHeuristicScan()) { if (!PE_Cached.isVerbose) { log(logType.warning, "To get the full heuristic scan result use \"--verbose\""); } log(logType.about, "Generic Heuristic Analysis by DosX (@DosX_dev)"); log(logType.nothing, "Scanning has begun!"); if (PE_Cached.isDotNet) { // Detection obfuscation and anti-analysis techniques specific to .NET scanForObfuscations_NET(); scanForAntiAnalysis_NET(); } else { // Detection of obfuscation and anti-analysis techniques specific to Native scanForObfuscations_Native(); scanForAntiAnalysis_Native(); } // Detection of anomalies most commonly found in packers, cryptors, protectors... scanForPackersAndCryptors_NET_and_Native(); // Detection licensing information (detecting possible DRM systems or licensing strings) scanForLicensingSystems_NET_and_Native(); // A cursory checks of the file format integrity scanForBadFileFormat_NET_and_Native(); // Detection of debug information (headers, sections, strings...) scanForDebugData_NET_and_Native(); // Detection for suspicious or potentially malicious patterns scanForMaliciousCode_NET_and_Native(); if (PE_Cached.isVerbose) { // Detection of programming language, compiler, linker scanForLanguagesAndCompilers_NET_and_Native(); // Detection of specific markers and pointers that are read by the operating system scanForInterestingMarkers_NET_and_Native(); } // Detection of an executable file extension type other than the standard .exe/.dll scanNameForAnomalies_FileSystem_NET_and_Native(); // >> Happy end << log(logType.nothing, "Scan completed."); } else { log(logType.warning, "Heuristic scan is disabled. Use '--heuristicscan' to enable"); } unloadCache(); } /** @returns {boolean} Whether the DIE engine lacks APIs required by this heuristic. */ function stubForLegacyEngines() { if (typeof PE.getNetAssemblyName !== "function" || !PE.section || !PE.section[0] || typeof PE.section[0].Offset !== "number") { stdout(">>> Update DIE Engine to 4.0 and higher for using Heuristic-analyzer by DosX <<<"); debugger; return true; } return false; } /** @returns {boolean} Whether the PE scripting environment is unavailable. */ function stubForWrongEnvironment() { if (typeof PE === "undefined") { stdout(">>> Wrong environment! 'PE' is undefined. Check DIE-engine for correct installation <<<"); debugger; return true; } if (PE.isHeuristicScan() && PE.isVerbose() && !_getNumberOfResults("operation system")) { stdout(">>> Script is running outside the environment! Are you in debug mode? <<<"); debugger; } return false; } /** * Outputs a warning message to the appropriate output stream. * * Depending on the environment, this function will: * - Use `console.warn` if `console` is available. * - Use `_setResult` if `File` is available. * - Use `_error` if it is defined as a function. * - Throw an error if none of the above conditions are met. * * @param {string} stringToOut - The warning message to output. * @returns {void} No return value; */ function stdout(stringToOut) { if (typeof console === "object") { console.warn(stringToOut); } else if (typeof File === "object") { _setResult("~warning", stringToOut, String(), String()); } else { if (typeof _error === "function") { _error(stringToOut); } else { throw new Error(stringToOut); } } } /** * Initialize the PE-related cache by reading values from the PE API and storing them * into the global PE_Cached object. * * The function constructs a temporary map containing typed entries sourced from the PE * interface, then copies each entry into PE_Cached. * * Additionally compiles the obfuscation whitelist into case-sensitive and * case-insensitive regular expressions for repeated token checks. These cached * expressions are valid until {@link unloadCache} runs at the end of the scan. * The linear entry-point disassembly cache is also reset for the current image. * * Side effects: * - Mutates the global PE_Cached object by adding/updating cached values. * - Evaluates local and PE API functions stored in the cache map. * * @function initializeCache * @returns {void} No return value; updates PE_Cached as a side effect. */ function initializeCache() { const architecture = getPeArchitecture(), fileSize = PE.getSize(), overlaySize = PE.getOverlaySize(), dosStubOffset = PE.getDosStubOffset(), dosStubSize = PE.getDosStubSize(), imageBase = PE.getImageBase(), addressOfUnmanagedEntryPoint = PE.getAddressOfEntryPoint(), isDynamicLinkLibrary = PE.isDll(), isDriver = PE.isDriver(), isEntryPointCorrect = PE.isEntryPointCorrect(); // A DLL with AddressOfEntryPoint zero has no loader initializer. Requiring a valid, // file-backed address keeps resource-only and deliberately entryless DLLs out of every // EP rule. Drivers and executable images retain their established analysis policy. const hasUnmanagedEntryPoint = addressOfUnmanagedEntryPoint > imageBase; const isEntryPointAnalysisAllowed = !isDynamicLinkLibrary || (!isDriver && hasUnmanagedEntryPoint && isEntryPointCorrect && PE.VAToOffset(addressOfUnmanagedEntryPoint) !== -1); var obfuscationWhitelistSources = [], obfuscationWhitelistInsensitiveSources = []; // The engine context may be reused for several files. Start every scan with an empty // linear-disassembly cache so instructions from a previous image cannot be returned. epAsmInstructions = []; epAsmInstructionAddresses = []; epDirectCallReturnCache = {}; // Preserve individual anchors while combining patterns for one test per group. for (var i = 0; i < obfuscationWhitelistPatterns.length; i++) { obfuscationWhitelistSources.push("(?:" + obfuscationWhitelistPatterns[i].source + ")"); } for (var i = 0; i < obfuscationWhitelistInsensitivePatterns.length; i++) { obfuscationWhitelistInsensitiveSources.push("(?:" + obfuscationWhitelistInsensitivePatterns[i].source + ")"); } var cacheMap = { /* Computed values */ /* Number */ imageBodyOffset: function () { return dosStubOffset + dosStubSize; }, /* Number */ imageBodySize: function () { return Math.max(0, fileSize - overlaySize - dosStubOffset - dosStubSize); }, /* Number */ fileSize: function () { return fileSize; }, /* Number */ imageSubsystem: function () { return PE.getImageOptionalHeader("Subsystem"); }, /* RegExp */ obfuscationWhitelist: function () { return new RegExp(obfuscationWhitelistSources.join("|")); }, /* RegExp */ obfuscationWhitelistInsensitive: function () { return new RegExp(obfuscationWhitelistInsensitiveSources.join("|"), "i"); }, /* Boolean */ hasUnmanagedEntryPoint: function () { return hasUnmanagedEntryPoint; }, /* Boolean */ isEntryPointAnalysisAllowed: function () { return isEntryPointAnalysisAllowed; }, /* Native functions */ /* Boolean */ is64bit: PE["is64"], /* Boolean */ isDotNet: PE["isNet"], /* Boolean */ isDynamicLinkLibrary: function () { return isDynamicLinkLibrary; }, /* Boolean */ isEntryPointCorrect: function () { return isEntryPointCorrect; }, /* Boolean */ isDriver: function () { return isDriver; }, /* Boolean */ isOverlayPresent: PE["isOverlayPresent"], /* Boolean */ isTlsPresent: PE["isTLSPresent"], /* Boolean */ isVerbose: PE["isVerbose"], /* Boolean */ isNetGlobalCctorPresent: PE["isNetGlobalCctorPresent"], /* Boolean */ isRichSignaturePresent: PE["isRichSignaturePresent"], /* Number */ addressOfUnmanagedEntryPoint: function () { return addressOfUnmanagedEntryPoint; }, /* Number */ imageBase: function () { return imageBase; }, /* Number */ dosStubOffset: function () { return dosStubOffset; }, /* Number */ dosStubSize: function () { return dosStubSize; }, /* Number */ overlayOffset: PE["getOverlayOffset"], /* Number */ overlaySize: function () { return overlaySize; }, /* Number */ numberOfSections: PE["getNumberOfSections"], /* Number */ numberOfRichIDs: PE["getNumberOfRichIDs"], /* Number */ numberOfUnmanagedResources: PE["getNumberOfResources"], /* Number */ numberOfUnmanagedImports: PE["getNumberOfImports"], /* Number */ numberOfUnmanagedExports: PE["getNumberOfExports"], /* Number */ indexOfEntryPointSection: PE["getEntryPointSection"], /* Number */ indexOfImportsSection: PE["getImportSection"], /* Number */ indexOfExportsSection: PE["getExportSection"], /* Number */ indexOfResourceSection: PE["getResourceSection"], /* Number */ indexOfTlsSection: PE["getTLSSection"], /* Number */ indexOfRelocsSection: PE["getRelocsSection"], /* String */ nameOfNetAssemblyName: PE["getNetAssemblyName"], /* String */ nameOfNetModuleName: PE["getNetModuleName"], /* Local functions */ /* String */ architecture: function () { return architecture; }, /* Boolean */ isI386: function () { return architecture === "I386"; }, /* Boolean */ isAMD64: function () { return architecture === "AMD64"; }, /* Boolean */ isARM: function () { return architecture === "ARM"; }, /* Boolean */ isThumb: function () { return architecture === "ARMNT" || architecture === "THUMB"; }, /* Boolean */ isARM64: function () { return architecture === "ARM64"; }, /* Boolean */ isArchX86: function () { return architecture === "I386" || architecture === "AMD64"; }, /* Boolean */ isCppClrLikeApp: isCppClrLikeApp, /* Boolean */ isVbNetStandardLibraryPresent: isVbNetStandardLibraryPresent, /* Boolean */ isJscriptNetStandardLibraryPresent: isJscriptNetStandardLibraryPresent, /* String */ firstEpAsmInstruction: function () { return isEntryPointAnalysisAllowed ? getFirstEpAsmInstruction() : undefined; } }; for (var key in cacheMap) { const functionBody = cacheMap[key]; if (functionBody && typeof functionBody === "function") { PE_Cached[key] = functionBody(); } else if (functionBody) { stdout(">>> PE API function '" + key + "' is not a function! <<<"); debugger; } else { stdout(">>> PE API function '" + key + "' is undefined! <<<"); debugger; } } } /** * Clears the PE and linear-disassembly caches after analysis so no values from * the previous image can be reused. * * @function unloadCache * @returns {void} No return value; clears PE_Cached properties as a side effect. */ function unloadCache() { for (var key in PE_Cached) { delete PE_Cached[key]; } epAsmInstructions = []; epAsmInstructionAddresses = []; epDirectCallReturnCache = {}; } // #region "obfuscations (.NET)" /** Detects managed-code obfuscation indicators and reports their combined evidence. */ function scanForObfuscations_NET() { log(logType.nothing, "Scanning for obfuscation..."); var options = String(); var isDetected = Boolean(); // Is managed entry point modified? Let's check it using default .NET EP names var isEntryPointModified = false; const vbNetEntries = [ "Main", "main", "MAIN", "MyApplication" ], defaultEntries = [ // like MSIL, C#, C++ NET etc "Main", "_Main", // Small Basic "main", // F# entry "main@", // F# entry "
$", // For programs with top-level operators (C#) "mainCRTStartup", // C++ CLR .NET (CLI) "_mainCRTStartup", // C++ CLR .NET (x86 CLI) "wmainCRTStartup", // C++ CLR .NET (Unicode CLI) "_wmainCRTStartup", // C++ CLR .NET (x86 Unicode CLI) "WinMainCRTStartup", // C++ CLR .NET (GUI) "wWinMainCRTStartup", // C++ CLR .NET (GUI) "_wWinMainCRTStartup", // C++ CLR .NET (x86 Unicode GUI) "_WinMainCRTStartup" ] if (!PE_Cached.isDynamicLinkLibrary) { if (PE_Cached.isVbNetStandardLibraryPresent) { if (isAllNetObjectsMissing(vbNetEntries)) { isEntryPointModified = true; } } else if (isAllNetObjectsMissing(defaultEntries)) { isEntryPointModified = true; } if (isEntryPointModified) { log(logType.net, "No \"Main\" method found"); } } if (isEntryPointModified) options = "Modified managed EP"; // .cctor can execute code before the Main method is called var isNetCctorPresent = false; if (PE_Cached.isNetGlobalCctorPresent && !PE_Cached.isCppClrLikeApp && !isUnpackagedWindowsAppSdkLinked()) { log(logType.net, "Global constructor detected!"); isNetCctorPresent = true; } if (isNetCctorPresent) options = addOption(options, "CLR constructor"); // If .NET meta headers are not present in the default .NET section, then the file is mutant var isStrangeEpPosition = false; const netMetaHeaders = [ "~", "Strings", "US", "GUID", "Blob" ]; // Specify the default .NET section index const defaultNetSection = 0; // Check conditions for a strange entry point position (not for CLR apps). Reference and // interop DLLs legitimately use layouts that violate the executable-image convention. if (!PE_Cached.isDynamicLinkLibrary && PE_Cached.numberOfSections > 1 && !PE_Cached.isCppClrLikeApp) { // Iterate through .NET metadata headers for (var s = 0; s < netMetaHeaders.length && !isStrangeEpPosition; s++) { const headerName = netMetaHeaders[s]; // Both optimized #~ and unoptimized #- tables belong to ordinary CLR metadata. if (!PE.isSignatureInSectionPresent(defaultNetSection, "00'#" + headerName + "'00") && (headerName !== "~" || !PE.isSignatureInSectionPresent(defaultNetSection, "00'#-'00"))) { log(logType.net, "Managed entry point position appears unusual"); isStrangeEpPosition = true; } } } if (isStrangeEpPosition) options = addOption(options, "Strange EP position"); // .NET files has a specific pattern of the entry point var isNativeEntryPointModified = false; // Managed interop DLLs may expose a legitimate native loader entry point, so the // executable-only CLR startup convention cannot be applied to them safely. if (!PE_Cached.isDynamicLinkLibrary && !PE_Cached.isCppClrLikeApp) { // not for CLR apps if (PE_Cached.isI386) { // FF2500????00: jmp dword ptr [ ... ] const firstOpCode = getFirstEpAsmOpCode(); if (firstOpCode !== "JMP") { if (PE.VAToOffset(PE_Cached.addressOfUnmanagedEntryPoint) !== -1) { log(logType.emulation, "Very strange EP pattern: " + emulateAndGetEpAsmPattern(true, 4).split("|").join(" .. ")); } else { log(logType.net, "No native entry point"); } isNativeEntryPointModified = true; } } else if (PE_Cached.isAMD64 || PE_Cached.isARM64) { if (PE.VAToOffset(PE_Cached.addressOfUnmanagedEntryPoint) !== 0x00) { isNativeEntryPointModified = true; // The AMD64 CLR emitter may load its sole _CorExeMain IAT slot before JMP RAX. // Verify the actual slot, not just a loader-like instruction pattern. var clrEntryPointOffset = PE.VAToOffset(PE_Cached.addressOfUnmanagedEntryPoint), clrOptionalHeaderOffset = getOptHeaderOffset(); if (PE_Cached.isAMD64 && isVirtualAddressRangeFileBacked(PE_Cached.addressOfUnmanagedEntryPoint, 12, SECTION_FLAGS_EXECUTE) && PE.compareEP("48 A1 ................ FF E0") && PE_Cached.numberOfUnmanagedImports === 1 && PE.getNumberOfImportThunks(0) === 1 && /(?:^|[\\/])mscoree(?:\.dll)?$/i.test(PE.getImportLibraryName(0)) && PE.getImportFunctionName(0, 0) === "_CorExeMain" && clrOptionalHeaderOffset >= 24 && clrOptionalHeaderOffset + 0xD8 <= PE_Cached.fileSize && PE.readWord(clrOptionalHeaderOffset - 4) >= 0xD8 && PE.readWord(clrOptionalHeaderOffset) === 0x20B && PE.read_uint32(clrOptionalHeaderOffset + 0x6C) >= 13) { var clrImportsRva = PE.read_uint32(clrOptionalHeaderOffset + 0x78), clrIatRva = PE.read_uint32(clrOptionalHeaderOffset + 0xD0), clrImportDescriptorOffset = PE.VAToOffset(PE_Cached.imageBase + clrImportsRva); if (PE.read_uint32(clrOptionalHeaderOffset + 0x7C) >= 20 && isVirtualAddressRangeFileBacked(PE_Cached.imageBase + clrImportsRva, 20, 0) && isVirtualAddressRangeFileBacked(PE_Cached.imageBase + clrIatRva, 16, 0) && PE.read_uint32(clrOptionalHeaderOffset + 0xD4) >= 16 && PE.read_uint32(clrImportDescriptorOffset + 16) === clrIatRva && readUnsignedQword(clrEntryPointOffset + 2) === PE_Cached.imageBase + clrIatRva) { isNativeEntryPointModified = false; } } } } } if (isNativeEntryPointModified) options = addOption(options, "Modified native EP"); // Check if the specified DOS message is not found in the DOS stub var isDosMessageModified = false; if (PE.findSignature(PE_Cached.dosStubOffset, PE_Cached.dosStubSize, "'This program cannot be run in DOS mode.'") === -1) { log(logType.any, "DOS-stub modified!"); isDosMessageModified = true; } if (isDosMessageModified) options = addOption(options, "Modified DOS"); // PE file must contain at least two sections: one for code and one for resources. var isOneSectionFile = false; if (PE_Cached.numberOfSections === 1) { log(logType.any, "Only one section found!"); isOneSectionFile = true; } if (isOneSectionFile) options = addOption(options, "Only one section"); // Check for strange sections var isStrangeSectionsPresent = false; const badChars = '_-=+~!@#$%^&*()"№;%:?*():;,/\\|\'`<>.0123456789'; // Very very bad! if (PE_Cached.numberOfSections > (!PE_Cached.isCppClrLikeApp ? 6 : 10) || !PE.section[".text"]) { log(logType.any, "Unusual section layout detected"); isStrangeSectionsPresent = true; } else { // Iterate through each section for (var i = 0; i < PE_Cached.numberOfSections && !isStrangeSectionsPresent; i++) { const sectionName = PE.getSectionName(i); // Check if the first character is not "." and the length of name is less than 3 if (sectionName[0] !== "." && sectionName.length < 3) { log(logType.any, "Unusual section name detected: \"" + File.cleanString(sectionName) + "\""); isStrangeSectionsPresent = true; break; } // Iterate through characters after "." // Check if the character is in the badChars list for (var d = 0; d < badChars.length && !isStrangeSectionsPresent; d++) { if (sectionName.substring(1).indexOf(badChars[d]) !== -1) { log(logType.any, "Unusual section name detected: \"" + File.cleanString(sectionName) + "\""); isStrangeSectionsPresent = true; } } if (!isAsciiString(sectionName) && !isStrangeSectionsPresent) { log(logType.any, "Non-ASCII section name detected"); isStrangeSectionsPresent = true; } } } if (isStrangeSectionsPresent) options = addOption(options, "Strange sections"); // Check for strange resources var isStrangeUnmanagedResourcesPresent = false; for (var i = 0; i < PE_Cached.numberOfUnmanagedResources && !isStrangeUnmanagedResourcesPresent; i++) { const unmanagedResourceName = PE.getResourceNameByNumber(i); if (unmanagedResourceName && (!isAsciiString(unmanagedResourceName) || isNameObfuscated(unmanagedResourceName))) { log(logType.any, "Strange unmanaged resource name: \"" + File.cleanString(unmanagedResourceName) + "\""); isStrangeUnmanagedResourcesPresent = true; } } if (isStrangeUnmanagedResourcesPresent) options = addOption(options, "Strange unmanaged resources"); // Check for a lot of sections (more than 10 -> this is a lot for .NET) var tooManySectionsPresent = false; if (PE_Cached.numberOfSections > 10) { log(logType.any, "Unusually high section count detected"); tooManySectionsPresent = true; } if (tooManySectionsPresent) options = addOption(options, "Too many sections"); const opCodes = new NetOpCodes(); // A popular way to obfuscate numbers/booleans var isIntConfusionPresent = false; const intConfusionXorPattern = opCodes.ldc_i4 + opCodes.ldc_i4 + opCodes.xor + opCodes.ldc_i4; if (validateNetByteCode(intConfusionXorPattern)) { if (validateNetByteCodeAny([ // samples by: Inx Obfuscator intConfusionXorPattern + (opCodes.setStrict(opCodes.bne_un_s, "09") + opCodes.ldc_i4_2 + opCodes.stloc_0 + opCodes.sizeof + opCodes.add), // samples by: MindLated, NetShield intConfusionXorPattern + (opCodes.bne_un + opCodes.ldc_i4 + opCodes.stloc + opCodes.sizeof + opCodes.add), // samples by: VavilonProtect intConfusionXorPattern + (opCodes.bne_un + opCodes.ldc_i4_2 + opCodes.stloc_s + opCodes.sizeof + opCodes.add) ])) { log(logType.net, "Int confusion detected at offset " + lastOffsetDetected); isIntConfusionPresent = true; } } if (isIntConfusionPresent) options = addOption(options, "Int confusion"); // Virtualization is a method of protection in which some code segments are rewritten into instructions inherent in the built-in virtual machine and executed by it var isVirtualizationPresent = false; if ( isAllNetObjectsPresent( [ "System.Reflection", // System.Reflection.dll "GetILGenerator", // MSIL: 'System.Reflection.Emit.DynamicMethod::GetILGenerator()' "BeginInvoke", "EndInvoke", "OpCode" // MSIL: 'System.Reflection.Emit.OpCode' ] ) && ( validateNetObject("Ldarg_0") || // MSIL: 'System.Reflection.Emit.OpCodes.Ldarg_0' validateNetObject("CreateDelegate") // MSIL: 'System.Delegate.CreateDelegate' ) && !isFrameworkComponent() ) { log(logType.net, "Virtualization-like behavior detected!"); isVirtualizationPresent = true; } if (isVirtualizationPresent) options = addOption(options, "Virtualization"); // Hiding calls using delegate tricks var callsEncrypt = false; if ( isAllNetObjectsPresent( [ "GetTypeFromHandle", // MSIL: 'System.Type::GetTypeFromHandle( ... )' "BinaryReader", // MSIL: 'System.IO.BinaryReader' "CreateDelegate", // MSIL: '[Delegate].CreateDelegate' "MakeByRefType", // MSIL: 'System.Type::MakeByRefType()' "DynamicMethod" // MSIL: 'System.Reflection.Emit.DynamicMethod' ] ) && !isFrameworkComponent() ) { log(logType.net, "Delegate-based call indirection may be present"); callsEncrypt = true; } if (callsEncrypt) options = addOption(options, "Calls encrypt"); // https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.suppressildasmattribute var isAntiIldasmPresent = false; if (validateNetObject("SuppressIldasmAttribute")) { log(logType.net, "IL disassembly suppression attribute detected"); isAntiIldasmPresent = true; } if (isAntiIldasmPresent) options = addOption(options, "Anti-ILDASM"); // Anti de4dot via inheritance var isAntiDe4dotPresent = false; if ( validateSignature("'Form'******00'Form'******00'Form'******00") || // samples by: NetShield validateNetObject("Form0") // samples by: MindLated ) { log(logType.net, "File may be protected by de4dot!"); isAntiDe4dotPresent = true; } if (isAntiDe4dotPresent) options = addOption(options, "Anti-de4dot"); // An obfuscation method in which calli is used instead of regular calls var isCalliInvokesPresent = false; if (validateNetByteCodeAny([ // samples by: MindLated opCodes.setStrict(opCodes.ldftn, "** ?? 00 0A") + opCodes.setStrict(opCodes.calli, "** 00 00 11"), // samples by: ArmDot, DarksProtector opCodes.ldelem_i + opCodes.setStrict(opCodes.calli, "** 00 00 11") ])) { log(logType.net, "Calli invokes detected at offset " + lastOffsetDetected); isCalliInvokesPresent = true; } if (isCalliInvokesPresent) options = addOption(options, "Calli invokes"); // An obfuscation method that uses ldftn to create pointers to methods, which are then used in the code var isLdftnPointersPresent = false; if (validateNetByteCodeAny([ opCodes.nop + opCodes.setStrict(opCodes.ldftn, "** 00 00 06") + opCodes.stelem_i, opCodes.nop + opCodes.setStrict(opCodes.ldftn, "** 00 00 0A") + opCodes.stelem_i, // samples by: Quantum (private) opCodes.setStrict(opCodes.ldftn, "** 00 00 0A") + opCodes.setStrict(opCodes.calli, "** 00 00 11") ])) { log(logType.net, "Ldftn pointers method-obfuscation detected at offset " + lastOffsetDetected); isLdftnPointersPresent = true; } if (isLdftnPointersPresent) options = addOption(options, "Ldftn pointers"); // Turns the code into spaghetti by splitting it into blocks that it executes depending on the situation var isCtrlFlowPresent = false; if (validateNetByteCodeAny([ // samples by: ConfuserEx opCodes.nop + opCodes.ldloc_0 + opCodes.ldc_i4 + opCodes.mul + opCodes.ldc_i4 + opCodes.xor + opCodes.br_s + opCodes.nop + opCodes.ldloc_0 + opCodes.ldc_i4 + opCodes.mul + opCodes.ldc_i4 + opCodes.xor + opCodes.br_s, // samples by: ConfuserEx (neo mod) opCodes.ldc_i4 + opCodes.ldc_i4 + opCodes.xor + opCodes.dup + opCodes.stloc_0 + opCodes.ldc_i4_3 + opCodes.rem_un + opCodes.switch__nobody, // samples by: .NET Reactor (v6.9.8) opCodes.setStrict(opCodes.ldc_i4, "00 00 00 00") + opCodes.br + opCodes.br + opCodes.ldloc, // samples by: .NET Reactor opCodes.ldsfld + opCodes.brfalse + opCodes.pop + opCodes.setStrict(opCodes.ldc_i4, "01 00 00 00") + // MSIL: 'ldc.4 1' opCodes.br + opCodes.nop, // samples by: .NET Reactor opCodes.setNullValue(opCodes.ldc_i4) + opCodes.ldsfld + opCodes.brtrue + opCodes.pop + opCodes.ldc_i4 + opCodes.br, // samples by: .NET Reactor (legacy~~) opCodes.stloc + opCodes.ldloc + opCodes.joinNoBodyAndValue(opCodes.switch__nobody, "** ** ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 00 00", "switch__nobody") + opCodes.ldc_i4 + opCodes.br, // Crypto Obfuscator opCodes.ldc_i4_3 + opCodes.joinNoBodyAndValue(opCodes.switch__nobody, "01 00 00 00 F6 FF FF FF") + opCodes.ldc_i4_1 + opCodes.brtrue_s + opCodes.ldtoken + opCodes.pop, // samples by: MindLated, NetShield opCodes.setNullValue(opCodes.ldc_i4) + // MSIL: 'ldc.i4 0' opCodes.stloc + opCodes.br + opCodes.nop + opCodes.ldloc + opCodes.setStrict(opCodes.ldc_i4, "01 00 00 00") + // MSIL: 'ldc.i4 1' opCodes.ceq + opCodes.brfalse, // samples by: Rose Obfuscator opCodes.setNullValue(opCodes.ldc_i4) + // MSIL: 'ldc.i4 0' opCodes.stloc + opCodes.br + opCodes.nop + opCodes.ldloc + opCodes.ldc_i4 + opCodes.ceq + opCodes.brfalse, // samples by: Smart Assembly opCodes.ldc_i4 + opCodes.br + opCodes.ldloc_s + opCodes.ldc_i4_s + opCodes.ldc_i4_0 + opCodes.stelem_i1 + opCodes.ldc_i4 + opCodes.br, // samples by: ConfuserEx (Beds mod) opCodes.ldc_i4 + opCodes.ldc_i4 + opCodes._unknown + opCodes.ldc_i4 + opCodes._unknown + opCodes.stloc_0 + opCodes.nop + opCodes.ldloc_0 + opCodes.ldc_i4 + opCodes.ldc_i4 + opCodes._unknown + opCodes.ldc_i4 + opCodes._unknown + opCodes.ceq + opCodes.brfalse_s, // samples by: DotNetPatcher opCodes.setStrict(opCodes.stloc_s, "05") + opCodes.nop + opCodes.ldloc_s + opCodes._unknown + opCodes.ceq + opCodes.brfalse_s + opCodes._unknown + opCodes.setStrict(opCodes.stloc_s, "05") + opCodes.nop + opCodes.ldloc_s + opCodes._unknown + opCodes.ceq + opCodes.brfalse_s, // samples by: VMProtect opCodes.ldloc_0 + opCodes.setStrict(opCodes.ldc_i4, "?? ** ** **") + opCodes._unknown + opCodes.stloc_0 + opCodes.ldloc_0 + opCodes.setStrict(opCodes.ldc_i4, "?? ** ** **") + opCodes.xor + opCodes.stloc_0, // samples by: VMProtect opCodes.setStrict(opCodes.ldc_i4, "?? ** ** **") + opCodes._unknown + opCodes.stloc_0 + opCodes.setStrict(opCodes.ldftn, "** ?? ?? ??") ])) { log(logType.net, "Control flow obfuscation detected at offset " + lastOffsetDetected); isCtrlFlowPresent = true; } if (isCtrlFlowPresent) options = addOption(options, "Ctrl flow"); const ctorSignatureOffset = PE.findSignature(PE_Cached.imageBodyOffset, PE_Cached.imageBodySize, "''00**"); const afterCtorOffset = ctorSignatureOffset !== -1 ? ctorSignatureOffset + 8 : -1; // Indicates that the file uses short object names. Typically this indicates the presence of an obfuscation // There are two ways to detect short names - find one-letter objects or just use signature mask at Ctor offset :D var isShortNamesPresent = false; if (afterCtorOffset !== -1) { if (PE.compare("00**00**00", afterCtorOffset) || PE.compare("00****00****00****00", afterCtorOffset) || PE.compare("00******00******00******00", afterCtorOffset) || PE.compare("00********00********00********00", afterCtorOffset) || PE.compare("00****00****00", afterCtorOffset) || PE.compare("00****00**00", afterCtorOffset) || PE.compare("00**00****00", afterCtorOffset)) { log(logType.net, "Short names detected! (mask)"); isShortNamesPresent = true; } if (!isShortNamesPresent && PE.compare("00**00", afterCtorOffset)) { // the second way :D var shortNamesFound = 0; const chars = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm"; for (var i = 0; i < chars.length && !isShortNamesPresent; i++) { if (PE.isNetObjectPresent(chars[i])) { log(logType.net, "Short name found: \"" + chars[i] + "\" (" + shortNamesFound + "/20)"); shortNamesFound++; } if (shortNamesFound === 20) { isShortNamesPresent = true; } } } } if (isShortNamesPresent) options = addOption(options, "Short names"); // Scan for strange patterns after .ctor (like "a1b2c3d4e5f6g7h8i9j0") var badNamings = false; var buffer = String(); // Keep the name window inside the searched PE body; overlay bytes are not metadata names. var afterCtorSearchSize = afterCtorOffset !== -1 ? Math.min(0x12c, PE_Cached.fileSize - afterCtorOffset, PE_Cached.imageBodyOffset + PE_Cached.imageBodySize - afterCtorOffset) : 0; for (var i = 0; i < afterCtorSearchSize; i++) { var currentByte = PE.readByte(afterCtorOffset + i).toString(16); if (currentByte.length === 1) currentByte = '0' + currentByte; buffer += currentByte + ' '; } var bufferString = String(); // 0x00 to [0x20, 0x20, 0x20] const bufferArray = buffer.replaceAll('00 ', '20 20 20 ').split(' '); // buffer to a string for (var i = 0; i < bufferArray.length; i++) bufferString += String.fromCharCode(parseInt(bufferArray[i], 16)); const patternsToExplore = bufferString.split(" "); var strangePatternsCounter = 0; // https://web.archive.org/web/20190719134346/http://bartdesmet.net/blogs/bart/archive/2008/08/21/how-c-array-initializers-work.aspx var isPrivateImplementationDetailsPresent = validateNetObject(""); for (var i = 0; i < patternsToExplore.length && !badNamings; i++) { const currentStringPattern = patternsToExplore[i]; if (isPrivateImplementationDetailsPresent && currentStringPattern.length === 40) { break; } if (currentStringPattern.indexOf("<") === -1 && !/^(?:[0-9A-F]{64})$/.test(currentStringPattern) && // These names may be generated by the compiler isNameObfuscated(currentStringPattern)) { strangePatternsCounter++; } if (strangePatternsCounter > 2) { log(logType.net, "Obfuscated naming patterns detected"); badNamings = true; } } if (badNamings) options = addOption(options, "Bad namings"); // AntiTamper protects the file from modification var isAntiTamperPresent = false; if ( validateNetUnicodeString(" is tampered.") || // samples by: .NET Reactor validateNetUnicodeString("ping 127.0.0.1 > nul") || // samples by: ConfuserEx (Trinity, SkiDzEx like) validateNetUnicodeString("/C ping 1.1.1.1 -n 1 -w 3000 > Nul & Del \"") || // samples by: MindLated validateNetByteCode( // samples by: ConfuserEx opCodes.ldloc_s + opCodes.ldc_i4_0 + opCodes.ldloc_s + opCodes.ldc_i4_0 + opCodes.ldelem_u4 + opCodes.ldloc_s + opCodes.ldc_i4_0 + opCodes.ldelem_u4 + opCodes._unknown + opCodes.stelem_i4 + opCodes.ldloc_s + opCodes.ldc_i4_1 + opCodes.ldloc_s + opCodes.ldc_i4_1 + opCodes.ldelem_u4 + opCodes.ldloc_s + opCodes.ldc_i4_1 + opCodes.ldelem_u4 + opCodes._unknown + opCodes.stelem_i4 + opCodes.ldloc_s + opCodes.ldc_i4_2 + opCodes.ldloc_s + opCodes.ldc_i4_2 + opCodes.ldelem_u4 + opCodes.ldloc_s + opCodes.ldc_i4_2 + opCodes.ldelem_u4 + opCodes._unknown + opCodes.stelem_i4 + opCodes.ldloc_s + opCodes.ldc_i4_3 + opCodes.ldloc_s + opCodes.ldc_i4_3 + opCodes.ldelem_u4 + opCodes.ldloc_s + opCodes.ldc_i4_3 + opCodes.ldelem_u4 + opCodes._unknown + opCodes.stelem_i4 + opCodes.ldloc_s + opCodes.ldc_i4_4 + opCodes.ldloc_s + opCodes.ldc_i4_4 + opCodes.ldelem_u4 + opCodes.ldloc_s + opCodes.ldc_i4_4 + opCodes.ldelem_u4 + opCodes._unknown + opCodes.stelem_i4 ) || validateNetByteCode( // samples by: ConfuserEx (Beds mod, private) opCodes.ldloc_s + opCodes._unknown + opCodes.shr_un + opCodes.ldloc_s + opCodes.ldc_i4_s + opCodes.shl + opCodes.or + opCodes.stloc_s + opCodes.ldloc_s + opCodes._unknown + opCodes.shr_un + opCodes.ldloc_s + opCodes.ldc_i4_s + opCodes.shl + opCodes.or + opCodes.stloc_s + opCodes.ldloc_s + opCodes._unknown + opCodes.shr_un + opCodes.ldloc_s + opCodes.ldc_i4_s + opCodes.shl + opCodes.or + opCodes.stloc_s ) ) { log(logType.net, "Anti-tamper detected!"); isAntiTamperPresent = true; } if (isAntiTamperPresent) options = addOption(options, "Anti-tamper"); // TODO: Check suspicious suffixes through TypeDef names, not arbitrary strings or backing fields. /* var isModuleCtorMultiple = false; var currentCtorOffset = PE.findSignature(PE_Cached.imageBodyOffset, PE_Cached.imageBodySize, "00''00"); if (currentCtorOffset !== -1) { var secondCtorSearchOffset = currentCtorOffset + 10, secondCtorSearchSize = PE_Cached.fileSize - PE_Cached.overlaySize - secondCtorSearchOffset, secondCtorNameOffset = secondCtorSearchSize > 0 ? PE.findSignature(secondCtorSearchOffset, secondCtorSearchSize, "''") : -1; var secondCtorSuffix = secondCtorNameOffset !== -1 && secondCtorNameOffset + 8 < PE_Cached.fileSize - PE_Cached.overlaySize ? PE.readByte(secondCtorNameOffset + 8) : 0; if (secondCtorSuffix > 0 && secondCtorSuffix < 0x80 && !(secondCtorSuffix >= 0x41 && secondCtorSuffix <= 0x5A) && !(secondCtorSuffix >= 0x61 && secondCtorSuffix <= 0x7A)) { log(logType.net, "Fake detected at offset 0x" + Number(secondCtorNameOffset).toString(16)); isModuleCtorMultiple = true; } } if (isModuleCtorMultiple) options = addOption(options, "Fake .cctor name"); */ // Detects the use of unusual mathematical expressions that would be simplified by the compiler. // For example, an expression like "912874 + 39188124^834" var isMutationsPresent = false; const mathOpCodes = [ opCodes.add, opCodes.sub, opCodes.mul, opCodes.div, opCodes.xor, opCodes.shr, opCodes.shl, opCodes.or, opCodes.not, opCodes.and ]; const mathTemplates = [ // %s = math opcode opCodes.ldc_i4 + opCodes.ldc_i4 + "%s" + opCodes.stloc, // samples by: .NET Reactor opCodes.ldc_i4 + opCodes.ldc_i4 + "%s" + opCodes.ldsfld, // samples by: .NET Reactor opCodes.ldc_i4 + opCodes.ldc_i4 + "%s" + opCodes.ldc_i4 + opCodes.add, // samples by: ConfuserEx (Beds mod) opCodes.ldloc_1 + opCodes.ldc_i4 + opCodes.ldc_i4 + "%s" + opCodes.ldc_i4 + opCodes.ldc_i4, // samples by: SkiDzEX opCodes.ldloc + opCodes.ldc_i4 + opCodes.ldc_i4 + opCodes.ldc_i4 + "%s" + opCodes.stelem_i1, // samples by: .NET Reactor opCodes.ldc_i4 + opCodes.ldc_i4 + "%s" + opCodes.br_s // samples by: [Unknown protector, only samples] ]; for (var y = 0; y < mathTemplates.length && !isMutationsPresent; y++) { const template = mathTemplates[y]; for (var e = 0; e < mathOpCodes.length && !isMutationsPresent; e++) { if (e === 0 && !validateNetByteCode(template.replaceAll("%s", opCodes._unknown))) break; // No math mutations const pattern = template.replaceAll("%s", mathOpCodes[e]); if (validateNetByteCode(pattern)) { log(logType.net, "Math mutations detected at offset " + lastOffsetDetected); isMutationsPresent = true; } } } if (isMutationsPresent) options = addOption(options, "Math mutations"); // VB NET apps with resources only var isStringsEncryptionPresent = false; if (PE_Cached.isVbNetStandardLibraryPresent && !isFrameworkComponent()) { if (validateNetObject("Resources") && !validateGlobalUnicodeString(".Resources")) { log(logType.net, "It appears that the strings are hidden/encrypted and can be loaded dynamically."); isStringsEncryptionPresent = true; } } if (isStringsEncryptionPresent) options = addOption(options, "Strings encryption"); // A type of obfuscation of numbers in which they are inverted several times from positive to negative and vice versa... var isMathInversionsPresent = false; if (validateNetByteCode(opCodes.ldc_i4 + opCodes.not) && validateNetByteCodeAny([ // ~(-(~(-(~(-(~(-( num )))))))) opCodes.ldc_i4 + opCodes.not + opCodes.neg + opCodes.not + opCodes.neg + opCodes.not + opCodes.neg + opCodes.not + opCodes.neg, // ~(~(-(-(~(~( num )))))) opCodes.ldc_i4 + opCodes.not + opCodes.not + opCodes.neg + opCodes.neg + opCodes.not + opCodes.not, // ~(-(~(~(-(-( num )))))) opCodes.ldc_i4 + opCodes.not + opCodes.neg + opCodes.not + opCodes.not + opCodes.neg + opCodes.neg, // ~(-(~(-(~(~( num )))))) opCodes.ldc_i4 + opCodes.not + opCodes.neg + opCodes.not + opCodes.neg + opCodes.not + opCodes.not, // ~(-(~(-(~(-( num )))))) opCodes.ldc_i4 + opCodes.not + opCodes.neg + opCodes.not + opCodes.neg + opCodes.not + opCodes.neg ])) { log(logType.net, "Math inversions detected at offset " + lastOffsetDetected); isMathInversionsPresent = true; } if (isMathInversionsPresent) options = addOption(options, "Math inversions"); // A technique that allows you to avoid code decompilation. dnSpy gives a parsing error when trying to open such a file var invalidOpCodes = false; if ( /* validateNetByteCode( // samples by: SugarGuard opCodes.setStrict(opCodes.box, "?? 00 00 01") + opCodes.ret ) || */ validateNetByteCode( // samples by: ConfuserEx (Beds mod) opCodes.setStrict(opCodes.calli, "FF FF FF FF") + opCodes.setStrict(opCodes.sizeof, "FF FF FF FF") ) ) { log(logType.net, "Invalid OpCodes detected at offset " + lastOffsetDetected); invalidOpCodes = true; } if (invalidOpCodes) options = addOption(options, "Invalid OpCodes"); // Some protections include their own dependencies in the form of runtime libraries var isProtectionRuntimePresent = false; var runtimeFound = String(); var protectionsRuntime = [ // Need more { lib: "haspdnert.dll", name: "SafeNet Sentinel LDK" }, { lib: "AgileDotNet.VMRuntime.dll", name: "Agile" }, { lib: "Xerin.Runtime.dll", name: "XerinFuscator" }, { lib: "CliSecureRd64.dll", name: "CliSecure" }, { lib: "Xerin.Runtime", name: "XerinFuscator" }, { lib: "CliSecureRd.dll", name: "CliSecure" }, { lib: "Protect32.dll", name: "ILProtector" }, { lib: "Protect64.dll", name: "ILProtector" }, { lib: "AgileDotNetRT64.dll", name: "Agile" }, { lib: "AgileDotNetRT.dll", name: "Agile" }, { lib: "OneVM.Runtime.dll", name: "OneVM" }, { lib: "MRuntime3.dll", name: "Maxtocode" }, { lib: "NecroVM.Runtime", name: "NecroVM" }, { lib: "Attick.dll", name: "Maxtocode" }, { lib: "HVMRun64.dll", name: "DNGuard" }, { lib: "HVMRuntm.dll", name: "DNGuard" } ]; for (var i = 0; i < protectionsRuntime.length; i++) { const runtimeInfo = protectionsRuntime[i], runtimeLibraryName = runtimeInfo.lib, protectorName = runtimeInfo.name; if (validateNetObject(runtimeLibraryName) || // "runtime.dll" (/\.dll$/i.test(runtimeLibraryName) && validateNetObject(runtimeLibraryName.substring(0, runtimeLibraryName.length - 4)))) { // "runtime" log(logType.net, protectorName + " runtime detected!"); isProtectionRuntimePresent = true; runtimeFound = protectorName; break; } } // Clean up: release the dictionary protectionsRuntime = undefined; if (isProtectionRuntimePresent) options = addOption(options, runtimeFound + " runtime"); const obfuscatorsAttributes = [ "Xenocode.Client.Attributes.AssemblyAttributes.ProcessedByXenocode", // Xenocode "CryptoObfuscator.ProtectedWithCryptoObfuscatorAttribute", // Crypto Obfuscator "SecureTeam.Attributes.ObfuscatedByAgileDotNetAttribute", // Agile.NET "Xenocode.Client.Attributes.AssemblyAttributes", // Xenocode "SmartAssembly.Attributes.PoweredByAttribute", // Smart Assembly "ObfuscatedByAgileDotNetAttribute", // Agile.NET "NineRays.Obfuscator.Evaluation", // Spices.Net "ObfuscatedByCliSecureAttribute", // CliSecure "BabelObfuscatorAttribute", // Babel "GeneratedWithBatchToApp", // BatchToApp "AsStrongAsFuckAttribute", // AsStrongAsFuck "ProtectedByDotnetsafer", // DotNetSafer "Macrobject.Obfuscator", // Macrobject "DotfuscatorAttribute", // Dotfuscator "CodeWallTrialVersion", // CodeWall "ConfusedByAttribute", // ConfuserEx "ObfuscatedByGoliath", // Goliath "NETSpider.Attribute", // NETSpider "NineRays.Obfuscator", // Spices.Net "PoweredByAttribute", // Smart Assembly "RustemSoft.Skater", // Skater "Beds-Protector", // fake Beds Protector "BabelAttribute", // Babel "MRuntime3.dll", // Maxtocode "YanoAttribute", // Yano "EMyPID_8234_", // Eazfuscator.NET "ZYXDNGuarder", // DNGuard "Sixxpack", // Sixxpack "____KILL", // CodeVeil "SkiDzEX", // ConfuserEx based "Reactor", // Fake .NET Reactor ]; var isFakeSignaturesPresent = false; var isWatermarkPresent = false; var signaturesCounter = 0; var obfuscatorAttributeFound = String(); // Iterate through obfuscators attributes for (var t = 0; t < obfuscatorsAttributes.length; t++) { if (validateNetObject(obfuscatorsAttributes[t])) { obfuscatorAttributeFound = obfuscatorsAttributes[t]; signaturesCounter++; if (signaturesCounter > 1) { log(logType.net, "Multiple obfuscator attributes detected"); isFakeSignaturesPresent = true; break; } } } if (!isFakeSignaturesPresent) { // "Watermark" is only possible in the absence of fake signatures if (signaturesCounter === 1) { log(logType.net, "Watermark (Attribute) found: \"" + obfuscatorAttributeFound + "\""); isWatermarkPresent = true; } if ((!isWatermarkPresent && ( validateSignature("'Obfuscated'") || validateSignature("'obfuscated'") || validateSignature("'ByAttribute'") || validateSignature("'ObfuscatorAttribute'") || validateNetObject("ObfuscationAttribute") )) && !isFrameworkComponent()) { // System.Reflection.ObfuscationAttribute log(logType.net, "Obfuscation watermark pattern detected"); isWatermarkPresent = true; } } if (isFakeSignaturesPresent) options = addOption(options, "Fake signatures"); if (isWatermarkPresent) options = addOption(options, "Watermark"); // Compare ordinary assembly/module naming conventions only when an assembly name is present. // Standalone netmodules have no Assembly record: ECMA-335, II.22.2 (Assembly). var isBuildInfoModified = false; var buildAssemblyName = PE_Cached.nameOfNetAssemblyName, buildModuleName = PE_Cached.nameOfNetModuleName; if (buildAssemblyName.indexOf("Interop.") === 0 && buildAssemblyName !== getFileNameWithoutExtension(buildModuleName)) { buildAssemblyName = buildAssemblyName.substring(8, buildAssemblyName.length); } if (buildAssemblyName && (buildAssemblyName !== getFileNameWithoutExtension(buildModuleName) || buildAssemblyName === buildModuleName) && !(PE_Cached.isJscriptNetStandardLibraryPresent && buildModuleName === "JScript Module")) { log(logType.net, "Managed assembly and module names do not match"); isBuildInfoModified = true; } if (isBuildInfoModified) options = addOption(options, "Modified build info"); const protectorsLabelsToRemove = [ // Protectors with these names will be removed from results "SafeNet Sentinel LDK .NET", "Xenocode Postbuild", "Smart Assembly", "Dotfuscator", "Babel .NET", "Spices.Net", "Maxtocode", "FISH .NET", "CliSecure", "CodeWall", "CodeVeil", "Sixxpack", "DNGuard", "Goliath", "Agile", "Yano" ], packersLabelsToRemove = [ "Quest PowerGUI", "DataAnubis", "NsPack", "ASPack" ], protectionsLabelsToRemove = [ "Sentinel SuperPro dongle reference", "Unikey/Activator dongle reference", "Eutron SmartKey dongle reference", "SenseLock dongle reference", "Hardlock dongle reference", "WIBU Key dongle reference", "Wizzkey dongle reference", "SoftLok dongle reference", "NetHASP dongle reference" ]; // Волки делают АУФ 🐺☝️ if (isFakeSignaturesPresent) { for (var i = 0; i < protectorsLabelsToRemove.length; i++) { _removeResult("protector", protectorsLabelsToRemove[i]); } for (var i = 0; i < packersLabelsToRemove.length; i++) { _removeResult("cryptor", packersLabelsToRemove[i]); _removeResult("packer", packersLabelsToRemove[i]); } for (var i = 0; i < protectionsLabelsToRemove.length; i++) { _removeResult("protection", protectionsLabelsToRemove[i]); } } if (options.length !== 0) isDetected = true; if (isDetected) { _setResult("~protection", "Obfuscation", String(), PE_Cached.isVerbose ? options : String()); } } // #endregion // #region "anti-analysis (.NET)" /** Detects anti-analysis behavior in managed PE files. */ function scanForAntiAnalysis_NET() { log(logType.nothing, "Scanning for anti-analysis tricks..."); var options = String(); var isDetected = false; // Assumes the file can detect debugging protection var isAntiDebugPresent = false; const debuggerObject = "Debugger", // MSIL: 'System.Diagnostics.Debugger' from System.Diagnostics.dll isAttached = "get_IsAttached", // MSIL: '*.Debugger::get_IsAttached()' isLogging = "IsLogging" // MSIL: '*.Debugger::IsLogging()' if ( ( ( // .NET Functions validateNetObject(debuggerObject) || validateNetUnicodeString(debuggerObject) // Check for 'Debugger' ) && ( (validateNetObject(isAttached) || validateNetUnicodeString(isAttached)) || // Check for 'get_IsAttached' property (validateNetObject(isLogging) || validateNetUnicodeString(isLogging)) // Check for 'IsLogging' function ) || ( // Native (WinAPI) functions validateNetObject("CheckRemoteDebuggerPresent") || validateNetObject("IsDebuggerPresent") || validateNetObject("NtRemoveProcessDebug") || validateNetObject("NtQueryInformationProcess") ) ) && !isFrameworkComponent() ) { log(logType.net, "Debugger detection pattern detected"); isAntiDebugPresent = true; } if (isAntiDebugPresent) options = addOption(options, "Anti-debug"); // A type of protection in which, after launching an application, it erases the headers and/or PE signature behind itself var isAntiDumpPresent = false; if ( !isFrameworkComponent() && validateNetObject("VirtualProtect") && ( // from 'kernel32.dll', WinAPI // Need to check isAllNetObjectsPresent( // samples by: ConfuserEx, SkiDzEX [ "System.Runtime.InteropServices", // System.Runtime.InteropServices.dll "Marshal", // MSIL: '*.Marshal::GetHINSTANCE( ... )' "GetHINSTANCE", // MSIL: '*.Marshal::GetHINSTANCE( ... )' "IntPtr", // MSIL: 'System.IntPtr' "op_Explicit" // MSIL: 'System.IntPtr::op_Explicit' ] ) || isAllNetObjectsPresent( // samples by: Inx Obfuscator [ "System.Diagnostics", // System.Diagnostics "memcpy", // from 'msvcrt.dll', WinAPI "IntPtr", // MSIL: 'System.IntPtr' "get_MainModule", // MSIL: '*.Process::get_MainModule()' "get_BaseAddress" // MSIL: '*.ProcessModule::get_BaseAddress()' ] ) || isAllNetObjectsPresent( // samples by: MindLated [ "System.Runtime.InteropServices", // System.Runtime.InteropServices.dll "Marshal", // MSIL: '*.Marshal::GetHINSTANCE( ... )' "GetHINSTANCE", // MSIL: '*.Marshal::GetHINSTANCE( ... )' "IntPtr", // MSIL: 'System.IntPtr' "CopyBlock", "InitBlock" ] ) ) ) { log(logType.net, "Memory-based anti-dump pattern detected"); isAntiDumpPresent = true; } if (isAntiDumpPresent) options = addOption(options, "Anti-dump"); // https://medium.com/@tarunrd77/dnspy-static-analysis-of-a-net-malware-012806424acf const antiDnSpyTriggers = [ "dnspy", "dnSpy", "DNSPY" ]; var isAntiDnSpyPresent = false; // Iterate through anti-DnSpy triggers for (var l = 0; l < antiDnSpyTriggers.length && !isAntiDnSpyPresent; l++) { const dnSpyName = antiDnSpyTriggers[l]; // Match complete managed strings, with or without the executable extension. if (validateNetUnicodeString(dnSpyName) || validateNetUnicodeString(dnSpyName + ".exe")) { log(logType.net, "dnSpy-related anti-analysis references detected"); isAntiDnSpyPresent = true; } } if (isAntiDnSpyPresent) options = addOption(options, "Anti-dnSpy"); // https://github.com/icsharpcode/ILSpy const antiIlSpyTriggers = [ "ilspy", "ilSpy", "ILSpy", "ILSPY" ]; var isAntiIlSpyPresent = false; for (var l = 0; l < antiIlSpyTriggers.length && !isAntiIlSpyPresent; l++) { const ilSpyName = antiIlSpyTriggers[l]; // Match complete managed strings, with or without the executable extension. if (validateNetUnicodeString(ilSpyName) || validateNetUnicodeString(ilSpyName + ".exe")) { log(logType.net, "ILSpy-related anti-analysis references detected"); isAntiIlSpyPresent = true; } } if (isAntiIlSpyPresent) options = addOption(options, "Anti-ILSpy"); const isGetModuleHandleDetected = validateSignature("'GetModuleHandle'"); // from 'kernel32.dll' // https://en.wikipedia.org/wiki/Sandboxie const sbieVariants = [ "sbiedll", "SbieDll", "SBIEDLL" ]; var isAntiSbiePresent = false; if (isGetModuleHandleDetected) { // Iterate through Sandboxie variants for (var l = 0; l < sbieVariants.length && !isAntiSbiePresent; l++) { const sbieLib = sbieVariants[l]; // Match complete managed module names; GetModuleHandle also accepts names without .dll. if (validateNetObject("IsSandboxiePresent") || // https://github.com/AdvDebug/AntiCrack-DotNet/blob/f3cd8104e952580e0edf298101139d15befe008e/AntiCrack-DotNet/AntiVirtualization.cs#L53 validateNetUnicodeString(sbieLib) || validateNetUnicodeString(sbieLib + ".dll")) { log(logType.any, "Sandboxie-related environment markers detected"); isAntiSbiePresent = true; } } } if (isAntiSbiePresent) options = addOption(options, "Anti-SandBoxie"); // https://help.comodo.com/topic-394-1-767-9229-the-sandbox.html const comodoVariants = [ "cmdvrt32", "cmdvrt64", "CMDVRT32", "CMDVRT64" ]; var isAntiComodoPresent = false; if (isGetModuleHandleDetected) { // Iterate through Comodo Sandbox variants for (var l = 0; l < comodoVariants.length && !isAntiComodoPresent; l++) { const comodoLib = comodoVariants[l]; // Match complete managed module names, with or without .dll. if (PE.isNetObjectPresent("IsComodoSandboxPresent") || // https://github.com/AdvDebug/AntiCrack-DotNet/blob/f3cd8104e952580e0edf298101139d15befe008e/AntiCrack-DotNet/AntiVirtualization.cs#L64 validateNetUnicodeString(comodoLib) || validateNetUnicodeString(comodoLib + ".dll")) { log(logType.any, "Comodo sandbox markers detected"); isAntiComodoPresent = true; } } } if (isAntiComodoPresent) options = addOption(options, "Anti-Comodo"); // https://blog.360totalsecurity.com/en/how-to-use-360-sandbox/ const qihoo360Variants = [ "SxIn", "SXIn" ]; var isAntiQihoo360Present = false; if (isGetModuleHandleDetected) { // Iterate through Qihoo 360 Sandbox variants for (var l = 0; l < qihoo360Variants.length && !isAntiQihoo360Present; l++) { const qihoo360Lib = qihoo360Variants[l]; // Match complete managed module names, with or without .dll. if (PE.isNetObjectPresent("IsQihoo360SandboxPresent") || // https://github.com/AdvDebug/AntiCrack-DotNet/blob/f3cd8104e952580e0edf298101139d15befe008e/AntiCrack-DotNet/AntiVirtualization.cs#L75 validateNetUnicodeString(qihoo360Lib) || validateNetUnicodeString(qihoo360Lib + ".dll")) { log(logType.any, "Qihoo 360 sandbox markers detected"); isAntiQihoo360Present = true; } } } if (isAntiQihoo360Present) options = addOption(options, "Anti-Qihoo 360"); // https://www.networkintelligence.ai/blogs/cuckoo-sandbox/ const cuckooVariants = [ "cuckoomon", "CUCKOOMON" ]; var isAntiCuckooPresent = false; if (isGetModuleHandleDetected) { // Iterate through Cuckoo Sandbox variants for (var l = 0; l < cuckooVariants.length && !isAntiCuckooPresent; l++) { const cuckooLib = cuckooVariants[l]; // Match complete managed module names, not documentation or similarly named objects. if (PE.isNetObjectPresent("IsCuckooSandboxPresent") || // https://github.com/AdvDebug/AntiCrack-DotNet/blob/f3cd8104e952580e0edf298101139d15befe008e/AntiCrack-DotNet/AntiVirtualization.cs#L86 validateNetUnicodeString(cuckooLib) || validateNetUnicodeString(cuckooLib + ".dll") || validateNetUnicodeString(cuckooLib + ".DLL")) { log(logType.any, "Cuckoo sandbox markers detected"); isAntiCuckooPresent = true; } } } if (isAntiCuckooPresent) options = addOption(options, "Anti-Cuckoo"); // https://www.winehq.org/ var isAntiWinePresent = false; var wineGetUnixFileName = "wine_get_unix_file_name"; // An exact P/Invoke name or GetProcAddress string excludes longer names and explanatory text. if (isGetModuleHandleDetected && (validateNetObject(wineGetUnixFileName) || validateNetUnicodeString(wineGetUnixFileName))) { log(logType.any, "Wine environment marker detected"); isAntiWinePresent = true; } if (isAntiWinePresent) options = addOption(options, "Anti-Wine"); var isAntiVmPresent = false; if (validateNetUnicodeString("VirtualBox Graphics Adapters") || validateNetUnicodeString("VMWARE svga 2") || validateNetUnicodeString("VMware SVGA II") || validateNetUnicodeString("VirtualBox") || validateNetUnicodeString("VMBusHID") || validateNetUnicodeString("vmware") || validateNetUnicodeString("WDAGUtilityAccount")) { log(logType.any, "Virtual-machine environment markers detected"); isAntiVmPresent = true; } if (isAntiVmPresent) options = addOption(options, "Anti-VM"); if (options.length !== 0) isDetected = true; if (isDetected) { _setResult("~protection", "Anti analysis", String(), PE_Cached.isVerbose ? options : String()); } } // #endregion // #region "anti-analysis (Native)" /** Detects native anti-analysis behavior near the entry point and in PE metadata. */ function scanForAntiAnalysis_Native() { log(logType.nothing, "Scanning for anti-analysis tricks..."); var options = String(); var isDetected = Boolean(); // Direct PEB debug-field reads and debug-register accesses are two compact forms // of native anti-debugging. They share one verdict while retaining the source type. var isNativeAntiDebugPresent = false, nativeAntiDebugType = String(); if (PE_Cached.isEntryPointAnalysisAllowed && !PE_Cached.isDriver && PE_Cached.isArchX86 && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3)) { const antiDebugNativeRegister = PE_Cached.isI386 ? "E(?:AX|BX|CX|DX|SI|DI|BP)" : "R(?:AX|BX|CX|DX|SI|DI|BP|8|9|1[0-5])", antiDebugGeneralPurposeRegister = "(?:E(?:AX|BX|CX|DX|SI|DI|BP)|R(?:AX|BX|CX|DX|SI|DI|BP|8D?|9D?|1[0-5]D?))", antiDebugRegister32BitPattern = /^(?:E(?:AX|BX|CX|DX|SI|DI|BP)|R(?:8|9|1[0-5])D)$/i, antiDebugPebSegment = PE_Cached.isI386 ? "FS" : "GS", antiDebugPebAddress = PE_Cached.isI386 ? "(?:0X0*30|48)" : "(?:0X0*60|96)", antiDebugPebFieldOffset = PE_Cached.isI386 ? "(?:0X0*(?:2|68)|2|104)" : "(?:0X0*(?:2|BC)|2|188)", antiDebugPebLoadPattern = new RegExp("^MOV (" + antiDebugNativeRegister + "), (?:(?:DWORD|QWORD) PTR )?" + antiDebugPebSegment + ":\\[" + antiDebugPebAddress + "\\]$", "i"), antiDebugPebFieldAccessPattern = new RegExp("(?:^| )(?:(?:CS|DS|ES|SS):)?\\[(" + antiDebugNativeRegister + ") ?\\+ ?" + antiDebugPebFieldOffset + "\\]", "i"), antiDebugRegisterCopyPattern = new RegExp("^MOV (" + antiDebugNativeRegister + "), (" + antiDebugNativeRegister + ")$", "i"), antiDebugRegisterWritePattern = new RegExp("^(?:MOV(?:ABS)?|MOVZX|MOVSX|MOVSXD|LEA|XOR|ADD|SUB|ADC|SBB|AND|OR|NOT|NEG|INC|DEC|POP) (" + antiDebugGeneralPurposeRegister + ")(?:,|$)", "i"), antiDebugPartialRegisterWritePattern = /^(?:MOV|XOR|ADD|SUB|AND|OR|NOT|NEG|INC|DEC) ((?:[ABCD][HL]|[ABCD]X|SI|DI|BP|SIL|DIL|BPL|R(?:8|9|1[0-5])(?:B|W)))(?:,|$)/i, antiDebugRegisterAccessPattern = /\bDR[0-7]\b/i, antiDebugSelfMovePattern = new RegExp("^(?:MOV|XCHG) (" + antiDebugGeneralPurposeRegister + "), \\1$|^LEA (" + antiDebugGeneralPurposeRegister + "), \\[\\2\\]$", "i"), antiDebugValuePreservingInstructionPattern = new RegExp("^(?:ADD|SUB|OR|XOR|SHL|SHR|SAR|ROL|ROR) (" + antiDebugGeneralPurposeRegister + "), (?:0X0+|0+)$|^IMUL (" + antiDebugGeneralPurposeRegister + "), \\2, (?:0X0*1|1)$", "i"), antiDebugStopOpCodes = [ "CALL", "HLT", "INT", "INT1", "INT3", "INTO", "IRET", "IRETD", "IRETQ", "JMP", "JMPF", "LOOP", "LOOPE", "LOOPNE", "LOOPNZ", "LOOPZ", "RET", "RETF", "RETN", "SYSCALL", "SYSENTER", "SYSRET", "SYSEXIT", "UD0", "UD1", "UD2" ]; // A PEB pointer may be copied through several registers. Track all proven aliases // and invalidate only registers that are explicitly overwritten by polymorphic junk. var antiDebugPebRegisters = {}, antiDebugFallthrough = {}; // Walk the bounded EP stream shared by native heuristics. Stop as soon as one // complete anti-debug proof is found because the verdict retains only its type. for (var i = 0; i < 512 && !isNativeAntiDebugPresent; i++) { var currentEpInstruction = emulateAndGetAsmInstructionByIndex(i), currentEpOpCode = getAsmOpCode(currentEpInstruction); if (!currentEpOpCode) break; // Safety check // Classify the instruction once so the state transitions below remain explicit. var pebLoad = currentEpInstruction.match(antiDebugPebLoadPattern), pebDebugFieldAccess = currentEpInstruction.match(antiDebugPebFieldAccessPattern), registerCopy = currentEpInstruction.match(antiDebugRegisterCopyPattern), registerWrite = currentEpInstruction.match(antiDebugRegisterWritePattern), partialRegisterWrite = currentEpInstruction.match(antiDebugPartialRegisterWritePattern), selfMove = currentEpInstruction.match(antiDebugSelfMovePattern), valuePreservingInstruction = currentEpInstruction.match(antiDebugValuePreservingInstructionPattern), isPebDebugFieldAccess = pebDebugFieldAccess && antiDebugPebRegisters[getNativeRegisterKey(pebDebugFieldAccess[1])] && /^(?:MOV|MOVZX|MOVSX|CMP|TEST|AND)$/.test(currentEpOpCode); if (currentEpOpCode === "MOV" && antiDebugRegisterAccessPattern.test(currentEpInstruction)) { // Any explicit MOV involving DR0-DR7 is already a complete register-based proof. log(logType.emulation, "Debug-register access near EP: " + currentEpInstruction); nativeAntiDebugType = "REG"; isNativeAntiDebugPresent = true; } else if (isPebDebugFieldAccess) { // Accept a debug-field read only through a register proven to contain the PEB. // FS/GS add their own base to that pointer, so their reads do not prove a PEB field access. log(logType.emulation, "PEB debug-field access near EP: " + currentEpInstruction); nativeAntiDebugType = "PEB"; isNativeAntiDebugPresent = true; } else if (pebLoad) { // Seed pointer provenance from the architecture-specific FS/GS PEB slot. antiDebugPebRegisters[getNativeRegisterKey(pebLoad[1])] = true; } else if (registerCopy) { var registerCopyDestination = getNativeRegisterKey(registerCopy[1]), registerCopySource = getNativeRegisterKey(registerCopy[2]); // Copy or clear only the destination proof according to the source provenance. if (antiDebugPebRegisters[registerCopySource]) { antiDebugPebRegisters[registerCopyDestination] = true; } else { delete antiDebugPebRegisters[registerCopyDestination]; } } else if (selfMove) { var selfMoveRegister = selfMove[1] || selfMove[2], selfMoveRegisterKey = getNativeRegisterKey(selfMoveRegister); // A PE32+ r32 self-move zero-extends its destination. It cannot retain // a 64-bit PEB pointer, but aliases held in other registers remain valid. if (PE_Cached.is64bit && antiDebugRegister32BitPattern.test(selfMoveRegister)) { delete antiDebugPebRegisters[selfMoveRegisterKey]; } } else if (valuePreservingInstruction) { var valuePreservingRegister = valuePreservingInstruction[1] || valuePreservingInstruction[2], valuePreservingRegisterKey = getNativeRegisterKey(valuePreservingRegister); // Full-width neutral arithmetic preserves a pointer; an r32 form truncates it. if (PE_Cached.is64bit && antiDebugRegister32BitPattern.test(valuePreservingRegister)) { delete antiDebugPebRegisters[valuePreservingRegisterKey]; } } else if (currentEpOpCode === "NOP" || currentEpOpCode === "FNOP" || currentEpOpCode === "PAUSE" || /^(?:CMP|TEST)$/.test(currentEpOpCode) || /^J(?!MP)[A-Z]+$/.test(currentEpOpCode)) { // General-purpose registers survive these operations on the fall-through path. } else if (registerWrite) { var registerWriteKey = getNativeRegisterKey(registerWrite[1]); delete antiDebugPebRegisters[registerWriteKey]; } else if (partialRegisterWrite) { delete antiDebugPebRegisters[getEpPartialRegisterKey(partialRegisterWrite[1])]; } else { // Unknown semantics may clobber any alias, so discard the whole proof set. antiDebugPebRegisters = {}; // A returning CALL may start a new match, but its previous register facts are gone. if (isEpLinearScanStopped(currentEpOpCode, epAsmInstructionAddresses[i], antiDebugStopOpCodes)) break; } if (isEpLinearFallthroughUnproven(currentEpInstruction, epAsmInstructionAddresses[i], antiDebugFallthrough)) break; } } if (isNativeAntiDebugPresent) options = "Anti-debug near EP (" + nativeAntiDebugType + ")"; if (options.length !== 0) isDetected = true; if (isDetected) { _setResult("~protection", "Anti analysis", String(), PE_Cached.isVerbose ? options : String()); } } // #endregion /** * Determines if the current PE file is a C++/CLR .NET application. * * @returns {boolean} True if the PE file matches the C++/CLR .NET application heuristics, otherwise false. */ function isCppClrLikeApp() { return PE_Cached.isDotNet && PE_Cached.isRichSignaturePresent && PE.isLibraryPresent("KERNEL32.DLL") && PE_Cached.isNetGlobalCctorPresent; } /** * Checks if the PE file is linked with an unpackaged Windows App SDK. * * This function verifies the presence of specific .NET objects that indicate * the application uses the Windows App SDK in an unpackaged deployment scenario. * * @returns {boolean} True if both "AutoInitialize" and "InitializeWindowsAppSDK" * .NET objects are present in the PE file, false otherwise. */ function isUnpackagedWindowsAppSdkLinked() { return PE.isNetObjectPresent("AutoInitialize") && PE.isNetObjectPresent("InitializeWindowsAppSDK"); } /** * Microsoft Intermediate Language (MSIL) OpCodes class for .NET assembly analysis. * * This class provides a comprehensive collection of MSIL instruction opcodes with their * corresponding bytecode patterns for signature-based detection in .NET assemblies. * All opcode values are verified against the official Microsoft .NET Runtime source code * and ECMA-335 Common Language Infrastructure (CLI) specification. * * @class NetOpCodes * @description Represents MSIL opcodes as hexadecimal string patterns for bytecode matching. * * Features: * - Complete MSIL instruction set coverage (93 opcodes) * - Support for both single-byte (0x00-0xFF) and two-byte (0xFE00-0xFEFF) opcodes * - Wildcard pattern support using '?' for variable operands * - Prefix instruction handling (volatile, tail, readonly, etc.) * - Helper methods for strict value substitution and null value setting * * Pattern Format: * - Fixed bytes: "58" (add instruction) * - Variable operands: "28????????" (call with 4-byte method token) * - Short operands: "2B??" (br.s with 1-byte offset) * - Two-byte opcodes: "FE01" (ceq instruction) * * Helper Methods: * - setStrict(opcode, value): Sets specific operand values for opcodes * - setNullValue(opcode): Replaces wildcards with zero bytes * * @see {@link https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.opcodes} Microsoft OpCodes Documentation * @see {@link https://www.ecma-international.org/publications-and-standards/standards/ecma-335/} ECMA-335 CLI Specification * @see {@link https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Reflection/Emit/OpCodes.cs} .NET Runtime Source */ function NetOpCodes() { // ========== BASIC STACK OPERATIONS ========== this.dup = "25"; // MSIL: 'dup' - Duplicate top stack value this.nop = "00"; // MSIL: 'nop' - No operation this.pop = "26"; // MSIL: 'pop' - Remove top stack value // ========== ARITHMETIC OPERATIONS ========== this.add = "58"; // MSIL: 'add' - Add two values this.add_ovf = "D6"; // MSIL: 'add.ovf' - Add signed integer values with overflow check this.add_ovf_un = "D7"; // MSIL: 'add.ovf.un' - Add unsigned integer values with overflow check this.div = "5B"; // MSIL: 'div' - Divide two values this.div_un = "5C"; // MSIL: 'div.un' - Divide unsigned values this.mul = "5A"; // MSIL: 'mul' - Multiply two values this.mul_ovf = "D8"; // MSIL: 'mul.ovf' - Multiply signed integer values with overflow check this.mul_ovf_un = "D9"; // MSIL: 'mul.ovf.un' - Multiply unsigned integer values with overflow check this.neg = "65"; // MSIL: 'neg' - Negate value this.rem = "5D"; // MSIL: 'rem' - Remainder when dividing two values this.rem_un = "5E"; // MSIL: 'rem.un' - Remainder (unsigned) this.sub = "59"; // MSIL: 'sub' - Subtract two values this.sub_ovf = "DA"; // MSIL: 'sub.ovf' - Subtract native int with overflow check this.sub_ovf_un = "DB"; // MSIL: 'sub.ovf.un' - Subtract unsigned int with overflow check // ========== LOGICAL OPERATIONS ========== this.and = "5F"; // MSIL: 'and' - Bitwise AND this.not = "66"; // MSIL: 'not' - Bitwise NOT this.or = "60"; // MSIL: 'or' - Bitwise OR this.shl = "62"; // MSIL: 'shl' - Shift left this.shr = "63"; // MSIL: 'shr' - Shift right this.shr_un = "64"; // MSIL: 'shr.un' - Shift right (unsigned) this.xor = "61"; // MSIL: 'xor' - Bitwise XOR // ========== COMPARISON OPERATIONS ========== this.ceq = "FE01"; // MSIL: 'ceq' - Compare equal this.cgt = "FE02"; // MSIL: 'cgt' - Compare greater than this.cgt_un = "FE03"; // MSIL: 'cgt.un' - Compare greater than (unsigned) this.clt = "FE04"; // MSIL: 'clt' - Compare less than this.clt_un = "FE05"; // MSIL: 'clt.un' - Compare less than (unsigned) // ========== CONSTANT LOADING ========== this.ldc_i4_m1 = "15"; // MSIL: 'ldc.i4.m1' - Load constant -1 this.ldc_i4 = "20????????"; // MSIL: 'ldc.i4' - Load 32-bit integer constant this.ldc_i4_0 = "16"; // MSIL: 'ldc.i4.0' - Load constant 0 this.ldc_i4_1 = "17"; // MSIL: 'ldc.i4.1' - Load constant 1 this.ldc_i4_2 = "18"; // MSIL: 'ldc.i4.2' - Load constant 2 this.ldc_i4_3 = "19"; // MSIL: 'ldc.i4.3' - Load constant 3 this.ldc_i4_4 = "1A"; // MSIL: 'ldc.i4.4' - Load constant 4 this.ldc_i4_5 = "1B"; // MSIL: 'ldc.i4.5' - Load constant 5 this.ldc_i4_6 = "1C"; // MSIL: 'ldc.i4.6' - Load constant 6 this.ldc_i4_7 = "1D"; // MSIL: 'ldc.i4.7' - Load constant 7 this.ldc_i4_8 = "1E"; // MSIL: 'ldc.i4.8' - Load constant 8 this.ldc_i4_s = "1F??"; // MSIL: 'ldc.i4.s' - Load small integer constant this.ldc_i8 = "21????????????????"; // MSIL: 'ldc.i8' - Load 64-bit integer constant this.ldc_r4 = "22????????"; // MSIL: 'ldc.r4' - Load 32-bit float constant this.ldc_r8 = "23????????????????"; // MSIL: 'ldc.r8' - Load 64-bit float constant this.ldnull = "14"; // MSIL: 'ldnull' - Load null reference // ========== ARGUMENT OPERATIONS ========== this.ldarg = "FE09????"; // MSIL: 'ldarg' - Load argument this.ldarg_0 = "02"; // MSIL: 'ldarg.0' - Load argument 0 this.ldarg_1 = "03"; // MSIL: 'ldarg.1' - Load argument 1 this.ldarg_2 = "04"; // MSIL: 'ldarg.2' - Load argument 2 this.ldarg_3 = "05"; // MSIL: 'ldarg.3' - Load argument 3 this.ldarg_s = "0E??"; // MSIL: 'ldarg.s' - Load argument (short form) this.starg_s = "10??"; // MSIL: 'starg.s' - Store to argument (short form) this.ldarga = "FE0A????"; // MSIL: 'ldarga' - Load argument address this.ldarga_s = "0F??"; // MSIL: 'ldarga.s' - Load argument address (short form) this.starg = "FE0B????"; // MSIL: 'starg' - Store to argument this.ldloca = "FE0D????"; // MSIL: 'ldloca' - Load local variable address this.ldloca_s = "12??"; // MSIL: 'ldloca.s' - Load local variable address (short form) // ========== LOCAL VARIABLE OPERATIONS ========== this.ldloc = "FE0C????"; // MSIL: 'ldloc' - Load local variable this.ldloc_0 = "06"; // MSIL: 'ldloc.0' - Load local variable 0 this.ldloc_1 = "07"; // MSIL: 'ldloc.1' - Load local variable 1 this.ldloc_2 = "08"; // MSIL: 'ldloc.2' - Load local variable 2 this.ldloc_3 = "09"; // MSIL: 'ldloc.3' - Load local variable 3 this.ldloc_s = "11??"; // MSIL: 'ldloc.s' - Load local variable (short form) this.stloc = "FE0E????"; // MSIL: 'stloc' - Store to local variable this.stloc_0 = "0A"; // MSIL: 'stloc.0' - Store to local variable 0 this.stloc_1 = "0B"; // MSIL: 'stloc.1' - Store to local variable 1 this.stloc_2 = "0C"; // MSIL: 'stloc.2' - Store to local variable 2 this.stloc_3 = "0D"; // MSIL: 'stloc.3' - Store to local variable 3 this.stloc_s = "13??"; // MSIL: 'stloc.s' - Store to local variable (short form) // ========== FIELD OPERATIONS ========== this.ldfld = "7B????????"; // MSIL: 'ldfld' - Load instance field this.ldflda = "7C????????"; // MSIL: 'ldflda' - Load field address this.ldsfld = "7E????????"; // MSIL: 'ldsfld' - Load static field this.ldsflda = "7F????????"; // MSIL: 'ldsflda' - Load static field address this.stfld = "7D????????"; // MSIL: 'stfld' - Store to instance field this.stsfld = "80????????"; // MSIL: 'stsfld' - Store to static field // ========== ARRAY OPERATIONS ========== this.ldelem = "A3????????"; // MSIL: 'ldelem' - Load array element this.ldelem_i = "97"; // MSIL: 'ldelem.i' - Load array element (native int) this.ldelem_i1 = "90"; // MSIL: 'ldelem.i1' - Load array element (8-bit signed) this.ldelem_i2 = "92"; // MSIL: 'ldelem.i2' - Load array element (16-bit signed) this.ldelem_i4 = "94"; // MSIL: 'ldelem.i4' - Load array element (32-bit signed) this.ldelem_i8 = "96"; // MSIL: 'ldelem.i8' - Load array element (64-bit signed) this.ldelem_r4 = "98"; // MSIL: 'ldelem.r4' - Load array element (32-bit float) this.ldelem_r8 = "99"; // MSIL: 'ldelem.r8' - Load array element (64-bit float) this.ldelem_ref = "9A"; // MSIL: 'ldelem.ref' - Load array element (object reference) this.ldelem_u1 = "91"; // MSIL: 'ldelem.u1' - Load array element (unsigned 8-bit) this.ldelem_u2 = "93"; // MSIL: 'ldelem.u2' - Load array element (unsigned 16-bit) this.ldelem_u4 = "95"; // MSIL: 'ldelem.u4' - Load array element (unsigned 32-bit) this.ldelema = "8F????????"; // MSIL: 'ldelema' - Load array element address this.ldlen = "8E"; // MSIL: 'ldlen' - Load array length this.newarr = "8D????????"; // MSIL: 'newarr' - Create new array this.stelem = "A4????????"; // MSIL: 'stelem' - Store to array element this.stelem_i = "9B"; // MSIL: 'stelem.i' - Store to array element (native int) this.stelem_i1 = "9C"; // MSIL: 'stelem.i1' - Store to array element (8-bit) this.stelem_i2 = "9D"; // MSIL: 'stelem.i2' - Store to array element (16-bit) this.stelem_i4 = "9E"; // MSIL: 'stelem.i4' - Store to array element (32-bit) this.stelem_i8 = "9F"; // MSIL: 'stelem.i8' - Store to array element (64-bit) this.stelem_r4 = "A0"; // MSIL: 'stelem.r4' - Store to array element (32-bit float) this.stelem_r8 = "A1"; // MSIL: 'stelem.r8' - Store to array element (64-bit float) this.stelem_ref = "A2"; // MSIL: 'stelem.ref' - Store to array element (object reference) // ========== INDIRECT OPERATIONS ========== this.ldind_i = "4D"; // MSIL: 'ldind.i' - Load indirect native int this.ldind_i1 = "46"; // MSIL: 'ldind.i1' - Load indirect 8-bit value this.ldind_i2 = "48"; // MSIL: 'ldind.i2' - Load indirect 16-bit value this.ldind_i4 = "4A"; // MSIL: 'ldind.i4' - Load indirect 32-bit value this.ldind_i8 = "4C"; // MSIL: 'ldind.i8' - Load indirect 64-bit value this.ldind_r4 = "4E"; // MSIL: 'ldind.r4' - Load indirect 32-bit float this.ldind_r8 = "4F"; // MSIL: 'ldind.r8' - Load indirect 64-bit float this.ldind_ref = "50"; // MSIL: 'ldind.ref' - Load indirect object reference this.ldind_u1 = "47"; // MSIL: 'ldind.u1' - Load indirect unsigned 8-bit value this.ldind_u2 = "49"; // MSIL: 'ldind.u2' - Load indirect unsigned 16-bit value this.ldind_u4 = "4B"; // MSIL: 'ldind.u4' - Load indirect unsigned 32-bit value this.stind_i = "DF"; // MSIL: 'stind.i' - Store indirect native int this.stind_i1 = "52"; // MSIL: 'stind.i1' - Store indirect 8-bit value this.stind_i2 = "53"; // MSIL: 'stind.i2' - Store indirect 16-bit value this.stind_i4 = "54"; // MSIL: 'stind.i4' - Store indirect 32-bit value this.stind_i8 = "55"; // MSIL: 'stind.i8' - Store indirect 64-bit value this.stind_r4 = "56"; // MSIL: 'stind.r4' - Store indirect 32-bit float this.stind_r8 = "57"; // MSIL: 'stind.r8' - Store indirect 64-bit float this.stind_ref = "51"; // MSIL: 'stind.ref' - Store indirect object reference // ========== CONTROL FLOW ========== this.beq = "3B????????"; // MSIL: 'beq' - Branch if equal this.beq_s = "2E??"; // MSIL: 'beq.s' - Branch if equal (short form) this.bge = "3C????????"; // MSIL: 'bge' - Branch if greater or equal this.bge_s = "2F??"; // MSIL: 'bge.s' - Branch if greater or equal (short form) this.bge_un = "41????????"; // MSIL: 'bge.un' - Branch if greater or equal (unsigned/unordered) this.bge_un_s = "34??"; // MSIL: 'bge.un.s' - Branch if greater or equal (unsigned/unordered, short) this.bgt = "3D????????"; // MSIL: 'bgt' - Branch if greater than this.bgt_s = "30??"; // MSIL: 'bgt.s' - Branch if greater than (short form) this.bgt_un = "42????????"; // MSIL: 'bgt.un' - Branch if greater than (unsigned/unordered) this.bgt_un_s = "35??"; // MSIL: 'bgt.un.s' - Branch if greater than (unsigned/unordered, short) this.ble = "3E????????"; // MSIL: 'ble' - Branch if less or equal this.ble_s = "31??"; // MSIL: 'ble.s' - Branch if less or equal (short form) this.ble_un = "43????????"; // MSIL: 'ble.un' - Branch if less or equal (unsigned/unordered) this.ble_un_s = "36??"; // MSIL: 'ble.un.s' - Branch if less or equal (unsigned/unordered, short) this.blt = "3F????????"; // MSIL: 'blt' - Branch if less than this.blt_s = "32??"; // MSIL: 'blt.s' - Branch if less than (short form) this.blt_un = "44????????"; // MSIL: 'blt.un' - Branch if less than (unsigned/unordered) this.blt_un_s = "37??"; // MSIL: 'blt.un.s' - Branch if less than (unsigned/unordered, short) this.bne_un = "40????????"; // MSIL: 'bne.un' - Branch if not equal (unsigned) this.bne_un_s = "33??"; // MSIL: 'bne.un.s' - Branch if not equal (unsigned, short) this.br = "38????????"; // MSIL: 'br' - Unconditional branch this.br_s = "2B??"; // MSIL: 'br.s' - Unconditional branch (short form) this.break_ = "01"; // MSIL: 'break' - Breakpoint instruction this.brfalse = "39????????"; // MSIL: 'brfalse' - Branch if false/null/zero this.brfalse_s = "2C??"; // MSIL: 'brfalse.s' - Branch if false/null/zero (short) this.brtrue = "3A????????"; // MSIL: 'brtrue' - Branch if true/non-null/non-zero this.brtrue_s = "2D??"; // MSIL: 'brtrue.s' - Branch if true/non-null/non-zero (short) this.call = "28????????"; // MSIL: 'call' - Call method this.calli = "29????????"; // MSIL: 'calli' - Call indirect this.callvirt = "6F????????"; // MSIL: 'callvirt' - Call virtual method this.jmp = "27????????"; // MSIL: 'jmp' - Jump to method this.ldtoken = "D0????????"; // MSIL: 'ldtoken' - Load metadata token this.ldftn = "FE06????????"; // MSIL: 'ldftn' - Load function pointer this.ldvirtftn = "FE07????????"; // MSIL: 'ldvirtftn' - Load virtual function pointer this.leave = "DD????????"; // MSIL: 'leave' - Exit protected region this.leave_s = "DE??"; // MSIL: 'leave.s' - Exit protected region (short form) this.ret = "2A"; // MSIL: 'ret' - Return from method this.switch__nobody = "45"; // MSIL: 'switch' - Switch statement // ========== TYPE OPERATIONS ========== this.box = "8C????????"; // MSIL: 'box' - Convert value type to object reference this.castclass = "74????????"; // MSIL: 'castclass' - Cast object to class this.ckfinite = "C3"; // MSIL: 'ckfinite' - Check if value is finite number this.conv_i = "D3"; // MSIL: 'conv.i' - Convert to native int this.conv_i1 = "67"; // MSIL: 'conv.i1' - Convert to 8-bit integer this.conv_i2 = "68"; // MSIL: 'conv.i2' - Convert to 16-bit integer this.conv_i4 = "69"; // MSIL: 'conv.i4' - Convert to 32-bit integer this.conv_i8 = "6A"; // MSIL: 'conv.i8' - Convert to 64-bit integer this.conv_ovf_i = "D4"; // MSIL: 'conv.ovf.i' - Convert to native int with overflow check this.conv_ovf_i_un = "8A"; // MSIL: 'conv.ovf.i.un' - Convert unsigned to native int with overflow check this.conv_ovf_i1 = "B3"; // MSIL: 'conv.ovf.i1' - Convert to 8-bit integer with overflow check this.conv_ovf_i1_un = "82"; // MSIL: 'conv.ovf.i1.un' - Convert to unsigned 8-bit integer with overflow check this.conv_ovf_i2 = "B5"; // MSIL: 'conv.ovf.i2' - Convert to 16-bit integer with overflow check this.conv_ovf_i2_un = "83"; // MSIL: 'conv.ovf.i2.un' - Convert to unsigned 16-bit integer with overflow check this.conv_ovf_i4 = "B7"; // MSIL: 'conv.ovf.i4' - Convert to 32-bit integer with overflow check this.conv_ovf_i4_un = "84"; // MSIL: 'conv.ovf.i4.un' - Convert to unsigned 32-bit integer with overflow check this.conv_ovf_i8 = "B9"; // MSIL: 'conv.ovf.i8' - Convert to 64-bit integer with overflow check this.conv_ovf_i8_un = "85"; // MSIL: 'conv.ovf.i8.un' - Convert to unsigned 64-bit integer with overflow check this.conv_ovf_u = "D5"; // MSIL: 'conv.ovf.u' - Convert to native unsigned int with overflow check this.conv_ovf_u_un = "8B"; // MSIL: 'conv.ovf.u.un' - Convert unsigned to native unsigned int with overflow check this.conv_ovf_u1 = "B4"; // MSIL: 'conv.ovf.u1' - Convert to unsigned 8-bit integer with overflow check this.conv_ovf_u1_un = "86"; // MSIL: 'conv.ovf.u1.un' - Convert unsigned to unsigned 8-bit integer with overflow check this.conv_ovf_u2 = "B6"; // MSIL: 'conv.ovf.u2' - Convert to unsigned 16-bit integer with overflow check this.conv_ovf_u2_un = "87"; // MSIL: 'conv.ovf.u2.un' - Convert unsigned to unsigned 16-bit integer with overflow check this.conv_ovf_u4 = "B8"; // MSIL: 'conv.ovf.u4' - Convert to unsigned 32-bit integer with overflow check this.conv_ovf_u4_un = "88"; // MSIL: 'conv.ovf.u4.un' - Convert unsigned to unsigned 32-bit integer with overflow check this.conv_ovf_u8 = "BA"; // MSIL: 'conv.ovf.u8' - Convert to unsigned 64-bit integer with overflow check this.conv_ovf_u8_un = "89"; // MSIL: 'conv.ovf.u8.un' - Convert unsigned to unsigned 64-bit integer with overflow check this.conv_r_un = "76"; // MSIL: 'conv.r.un' - Convert unsigned integer to floating-point this.conv_r4 = "6B"; // MSIL: 'conv.r4' - Convert to 32-bit float this.conv_r8 = "6C"; // MSIL: 'conv.r8' - Convert to 64-bit float this.conv_u = "E0"; // MSIL: 'conv.u' - Convert to native unsigned int this.conv_u1 = "D2"; // MSIL: 'conv.u1' - Convert to unsigned 8-bit integer this.conv_u2 = "D1"; // MSIL: 'conv.u2' - Convert to unsigned 16-bit integer this.conv_u4 = "6D"; // MSIL: 'conv.u4' - Convert to unsigned 32-bit integer this.conv_u8 = "6E"; // MSIL: 'conv.u8' - Convert to unsigned 64-bit integer this.isinst = "75????????"; // MSIL: 'isinst' - Test if object is instance of class this.mkrefany = "C6????????"; // MSIL: 'mkrefany' - Push typed reference to value this.refanytype = "FE1D"; // MSIL: 'refanytype' - Push type token from typed reference this.refanyval = "C2????????"; // MSIL: 'refanyval' - Push address from typed reference this.sizeof = "FE1C????????"; // MSIL: 'sizeof' - Get size of value type this.unbox = "79????????"; // MSIL: 'unbox' - Convert object reference to value type this.unbox_any = "A5????????"; // MSIL: 'unbox.any' - Convert boxed type to value // ========== OBJECT OPERATIONS ========== this.cpblk = "FE17"; // MSIL: 'cpblk' - Copy block of memory this.cpobj = "70????????"; // MSIL: 'cpobj' - Copy value type this.initblk = "FE18"; // MSIL: 'initblk' - Initialize block of memory this.initobj = "FE15????????"; // MSIL: 'initobj' - Initialize value type this.ldobj = "71????????"; // MSIL: 'ldobj' - Load value type from address this.ldstr = "72????????"; // MSIL: 'ldstr' - Load string literal this.localloc = "FE0F"; // MSIL: 'localloc' - Allocate space from local memory pool this.newobj = "73????????"; // MSIL: 'newobj' - Create new object instance this.stobj = "81????????"; // MSIL: 'stobj' - Store value type to address // ========== EXCEPTION HANDLING ========== this.endfilter = "FE11"; // MSIL: 'endfilter' - End exception filter clause this.endfinally = "DC"; // MSIL: 'endfinally' - End finally clause this.rethrow = "FE1A"; // MSIL: 'rethrow' - Rethrow current exception this._throw = "7A"; // MSIL: 'throw' - Throw exception // ========== PREFIX INSTRUCTIONS ========== this.constrained = "FE16????????"; // MSIL: 'constrained' - Constrain type for virtual method call this.no_ = "FE19??"; // MSIL: 'no.' - Skip fault check (typecheck, rangecheck, nullcheck) this.readonly = "FE1E"; // MSIL: 'readonly' - Subsequent array address operation performs no type check this.unaligned = "FE12??"; // MSIL: 'unaligned.' - Subsequent pointer instruction might be unaligned (alignment: 1, 2, or 4) this.volatile = "FE13"; // MSIL: 'volatile' - Subsequent pointer reference is volatile // ========== UTILITY OPCODES ========== this._unknown = "**"; // MSIL: - - Unknown opcode pattern (any byte except 0x00) this._any = "??"; // MSIL: - - Any single byte pattern this.arglist = "FE00"; // MSIL: 'arglist' - Return argument list handle /** * Sets strict operand values for MSIL opcodes with variable operands. * * This method replaces wildcard patterns ('??') in opcode masks with specific * hexadecimal values, allowing for precise bytecode pattern matching. * * @method setStrict * @param {string} opCodeMask - The opcode pattern with wildcards (e.g., "20????????") * @param {string} value - The hexadecimal value to substitute (e.g., "01 00 00 00") * @returns {string} The opcode pattern with substituted values * * @example * // Set ldc.i4 instruction to load constant value 1 * var pattern = opCodes.setStrict(opCodes.ldc_i4, "01 00 00 00"); * // Result: "2001000000" (ldc.i4 1) * * @example * // Set specific method token for call instruction * var callPattern = opCodes.setStrict(opCodes.call, "10 00 00 06"); * // Result: "2810000006" (call method token 0x06000010) * * @throws {Error} If operand size doesn't match the expected size */ this.setStrict = function (opCodeMask, value) { // Remove spaces from opcode mask and value opCodeMask = removeWhitespaces(opCodeMask); value = removeWhitespaces(value); // Find the index of the special pattern "??" in the opcode mask var indexOfSpecialPattern = opCodeMask.indexOf("??"); // Check if the opcode mask has a body (contains the special pattern "??") var isOpCodeMaskHasBody = indexOfSpecialPattern !== -1; // -1 if not found // Extract the opcode in hexadecimal var opCodeInHex = isOpCodeMaskHasBody ? opCodeMask.substring(0x00, indexOfSpecialPattern) : opCodeMask; // Check if the opcode mask has a body and the length of the body matches the length of the value if (isOpCodeMaskHasBody && opCodeMask.substring(opCodeInHex.length).length !== value.length) { _error("The size of the input values does not match."); } // Combine the opcode in hexadecimal with the value return opCodeInHex + value; } /** * Sets all wildcard operands in an opcode pattern to zero bytes. * * This method replaces all wildcard patterns ('??') in the opcode mask with * zero bytes ('00'), useful for matching instructions with null/empty operands. * * @method setNullValue * @param {string} opCodeMask - The opcode pattern containing wildcards * @returns {string} The opcode pattern with all wildcards replaced by '00' * * @example * // Set ldc.i4 instruction to load zero value * var pattern = opCodes.setNullValue(opCodes.ldc_i4); * // Result: "2000000000" (ldc.i4 0) * * @example * // Create null branch instruction * var brPattern = opCodes.setNullValue(opCodes.br); * // Result: "3800000000" (br with zero offset) * * @throws {Error} If the opcode mask contains no wildcards to replace */ this.setNullValue = function (opCodeMask) { if (opCodeMask.indexOf("??") === -1) { _error("Instruction does not have a body to overwrite the value."); } return opCodeMask.replaceAll("??", "00"); } /** * Joins an opcode without wildcards with additional bytes. * * This method concatenates opcodes that don't have wildcards (like switch) * with specific byte sequences, useful for instructions with variable-length operands. * * @method joinNoBodyAndValue * @param {string} opCode - The opcode without wildcards (e.g., "45" for switch) * @param {string} value - The hexadecimal bytes to append (e.g., "02 00 00 00 XX XX XX XX") * @param {string} [opCodeName] - Optional: name of the opcode variable for validation * @returns {string} The combined opcode pattern * * @example * // Create switch instruction with 2 targets * var switchPattern = opCodes.joinNoBodyAndValue(opCodes.switch__nobody, "02 00 00 00 10 00 00 00 20 00 00 00", "switch__nobody"); * // Result: "45020000001000000020000000" (switch with 2 targets at offsets 0x10 and 0x20) * * @throws {Error} If the opcode contains wildcards (should use setStrict instead) * @throws {Error} If opCodeName is provided but doesn't contain '__nobody' */ this.joinNoBodyAndValue = function (opCode, value, opCodeName) { // Type validation if (!opCode || typeof opCode !== "string") { _error("Invalid opcode provided."); } if (!value || typeof value !== "string") { _error("Invalid value provided."); } // Check that opcode doesn't contain wildcards if (opCode.indexOf("??") !== -1) { _error("Opcode contains wildcards. Use setStrict() instead."); } // Optional validation: check if opcode name contains '__nobody' if (opCodeName && typeof opCodeName === "string") { if (opCodeName.indexOf("__nobody") === -1) { _error("joinNoBodyAndValue should only be used with '__nobody' opcodes (variable-length instructions)."); } } return opCode + removeWhitespaces(value); } } /** * Removes all whitespace characters from a string including spaces, tabs, newlines, and carriage returns. * * @param {string} inputString - The input string to remove whitespaces from * @returns {string} The string with all whitespace characters removed, or an empty string if input is invalid */ function removeWhitespaces(inputString) { // Type validation to prevent runtime errors if (!inputString || typeof inputString !== "string") { return String(); } // Regex removes all whitespace: spaces, tabs, newlines, carriage returns return inputString.replace(/\s+/g, String()); } /** * Sanitizes section names to prevent DiE output corruption from malformed PE files. * Removes control characters that could break console output or cause display issues. * * @param {string} sectionName - The section name to clean and sanitize. * @returns {string} - The sanitized section name safe for DiE output. */ function cleanSectionName(sectionName) { // Type validation to prevent runtime errors if (!sectionName || typeof sectionName !== "string") { return String(); } // Remove control characters (0x00-0x1F) and extended control chars (0x7F-0x9F) // This prevents newlines, tabs, null bytes from breaking DiE output format var cleaned = sectionName.replace(/[\x00-\x1F\x7F-\x9F]/g, String()); // Remove leading/trailing whitespace cleaned = cleaned.trim(); return cleaned; } /** * Checks if a given string consists only of ASCII printable characters (ASCII range 0x20 to 0x7E). * * @param {string} inputString - The string to validate. * @returns {boolean} Returns true if the string contains only ASCII printable characters, otherwise false. */ function isAsciiString(inputString) { // Type validation to prevent runtime errors if (!inputString || typeof inputString !== "string") { return false; } return /^[\x20-\x7E]+$/.test(inputString); } /** * Returns the file name without its extension. * * @param {string} fileName - The name of the file, possibly including an extension. * @returns {string} The file name without the extension. If no extension is found, returns the original file name. */ function getFileNameWithoutExtension(fileName) { // Type validation to prevent runtime errors if (!fileName || typeof fileName !== "string") { return String(); } // Remove the file extension by splitting on the last dot and returning the first part var lastDotIndex = fileName.lastIndexOf("."); if (lastDotIndex === -1) { return fileName; // No extension found, return original name } return fileName.substring(0, lastDotIndex); } const SECTION_FLAGS_DISCARDABLE = 0x02000000, SECTION_FLAGS_EXECUTE = 0x20000000, SECTION_FLAGS_READ = 0x40000000, SECTION_FLAGS_WRITE = 0x80000000, SECTION_FLAGS_CODE = 0x00000020, IMAGE_FILE_RELOCS_STRIPPED = 0x0001, IMAGE_DIRECTORY_ENTRY_EXCEPTION = 3, IMAGE_DIRECTORY_ENTRY_SECURITY = 4, IMAGE_DIRECTORY_ENTRY_BASERELOC = 5, IMAGE_DIRECTORY_ENTRY_DEBUG = 6, IMAGE_DIRECTORY_ENTRY_ARCHITECTURE = 7, IMAGE_DIRECTORY_ENTRY_GLOBALPTR = 8, IMAGE_DIRECTORY_ENTRY_TLS = 9, IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG = 10, IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT = 13, IMAGE_DIRECTORY_ENTRY_RESERVED = 15; // #region "packers and cryptors (.NET and Native)" /** Detects generic packing and encryption indicators in managed and native PE files. */ function scanForPackersAndCryptors_NET_and_Native() { // For .NET and Native apps log(logType.nothing, "Scanning for packers and cryptors..."); var options = String(); var isDetected = Boolean(), isCryptor = Boolean(); if (PE_Cached.isDotNet) { var isAssemblyInvokeFound = false; // TODO: Verify method owners when the engine can do so safely; metadata-name co-occurrence does not prove a call. // Check the selective names first to avoid broader engine searches on ordinary assemblies. if (!isFrameworkComponent() && PE_Cached.nameOfNetModuleName !== "System.dll" && isAllNetObjectsPresent([ "get_EntryPoint", // MSIL: '*.Assembly::get_EntryPoint()' "Load", // MSIL: '*.Assembly::Load(uint8[])' "Invoke", // MSIL: '*.MethodBase::Invoke(object, object[])' "Assembly", // MSIL: 'System.Reflection.Assembly' from System.Reflection.dll "System.Reflection" // System.Reflection.dll ])) { log(logType.net, "Dynamic assembly invocation pattern detected"); isAssemblyInvokeFound = true; } if (isAssemblyInvokeFound) options = "Assembly invoke"; // Check if any class from System.Security.Cryptography namespace is used (non-full name) - for cryptors var cryptoClassSign = String(); if (isAssemblyInvokeFound && validateNetObject("System.Security.Cryptography")) { const cryptoClasses = [ "TripleDESCryptoServiceProvider", "RSACryptoServiceProvider", "DSACryptoServiceProvider", "DESCryptoServiceProvider", "AesCryptoServiceProvider", "Rfc2898DeriveBytes", "TripleDES", "Rijndael", "ECDsaCng", "AesAEAD", "Aes192Cbc", "Aes256Cbc", "Aes128Cbc", "AesManaged", "AesCng", "RC2CryptoServiceProvider", "RNGCryptoServiceProvider" ]; for (var i = 0; i < cryptoClasses.length && !isCryptor; i++) { var currentCryptoClassSign = cryptoClasses[i], cryptoClassResult = validateNetObject(currentCryptoClassSign); if (cryptoClassResult) { log(logType.net, "Crypto class present: " + currentCryptoClassSign); cryptoClassSign = currentCryptoClassSign; isCryptor = true; } } } if (isCryptor) options = addOption(options, cryptoClassSign); // Check if any class from System.IO.Compression namespace is used (non-full name) var compressionClassSign = String(); if (isAssemblyInvokeFound && validateNetObject("System.IO.Compression")) { const compressionClasses = [ "DeflateStream", "GZipStream" ]; for (var i = 0; i < compressionClasses.length; i++) { var currentCompressionClassSign = compressionClasses[i], compressionClassResult = validateNetObject(currentCompressionClassSign); if (compressionClassResult) { log(logType.net, "Compression class present: " + currentCompressionClassSign); compressionClassSign = currentCompressionClassSign; break; } } } if (compressionClassSign && isCryptor) options = addOption(options, compressionClassSign); // RunPE is a method for running payload from RAM, with minimal writing to disk. var isRunPePresent = false; if (validateNetObject("RunPE") || validateNetObject("PELoader") || // https://github.com/nettitude/RunPE/blob/main/RunPE/Internals/PELoader.cs validateNetObject("CMemoryExecute") || // https://github.com/wojciech-kulik/Sample-Projects/blob/master/Windows%20Desktop/WIN32_MemoryAppLoader/MemoryAppLoader/CMemoryExecute.cs (validateNetObject("GetProcAddress") && validateNetUnicodeString("WriteProcessMemory") && validateNetUnicodeString("VirtualAllocEx") && validateNetUnicodeString("ZwUnmapViewOfSection")) || ( (validateNetObject("WriteProcessMemory") || validateNetObject("NtWriteVirtualMemory")) && (validateNetObject("ZwUnmapViewOfSection") || validateNetObject("NtUnmapViewOfSection")) && (validateNetObject("CreateProcess") || validateNetObject("NtCreateProcess") || validateNetObject("CreateProcessA") || validateNetObject("CreateProcessW")) && validateNetObject("VirtualAllocEx")) ) { log(logType.net, "RunPE-like behavior detected!"); isRunPePresent = true; } if (isRunPePresent) options = addOption(options, "RunPE"); } // Check for packers and protections by .NET objects // Only user-defined metadata names (classes, methods, resources) that directly participate IN CODE EXECUTION are added. // Attributes and watermarks are not included. var isObjectLikePacker = false; // [{Name}, {Version}, {ObjectNames}] var dbCollectionOfObjectNamesDict = [ ["ConfuserEx", null, ["Decrypt", "LzmaDecoder", "BitTreeDecoder", "LenDecoder", "Resolve", "OutWindow"]], // https://github.com/yck1509/ConfuserEx ["CodeVeil (demo)", null, ["CodeWallTrialVersion", "CodeWallTrialVersion\u0001", "CodeWallTrialVersion\u0002"]], // https://marketplace.visualstudio.com/items?itemName=XHEOINC.DeployLXCodeVeil ["BatchToApp", null, ["RunApplication", "ReadResource", "Decompress", "embeddedBatchScript"]], // https://github.com/DosX-dev/BatchToApp [".NETZ", null, ["NetzStarter", "StartApp"]], // https://github.com/madebits/msnet-netz-compressor ["MPRESS", "2.XX", ["mpress", "lzmat"]], // https://www.autohotkey.com/mpress/mpress_web.htm ["Costura.Fody", null, ["Costura", "AssemblyLoader"]], // https://github.com/fody/costura ["Phoenix", "1.7-1.8", ["?0?", "?3?", "?5?"]], // https://ntcore.com/phoenix-protector/ ["Phoenix", "1.7-1.8", ["?0?", "?1?", "?7?"]], // . ["netcrypt", "1.1", ["stub", "program", "key", "IV", "GENERIC_ERROR"]], // https://github.com/friedkiwi/netcrypt ["NETPacker", "1.0", ["Stub", "Decrypt", "Decompress", "Stub.Properties.Resources.resources"]], // https://github.com/Mageland29/NETPacker ["DotNetPatcher", "4.X.X", ["SevenZipHelper", "pKey", "hash_table"]], // https://github.com/3DotDev/DotNetPatcher ["DekkCrypt", null, ["frmOne", "IX", "Reader", "CreateAPI"]], // https://github.com/Hoaxr/DekkCrypt ["L-Crypt", null, ["DoMain", "Decrypt", "Decompress", "SkipsCheck"]], // https://github.com/Asaf-Zanjiri/L-Crypt ["BorrCod", null, ["StubEch", "DAR", "RC4"]], // https://github.com/BorrCod/Crypter ["ReNET-pack", null, ["GetStreamAsByteArray", "Aced.Compression", "AcedInflator"]], ["NecroVM", null, ["VmLoad", "necro", "lz"]] ]; const objectNamesValidatingResult = validateNetObjectNames(dbCollectionOfObjectNamesDict); var versionByObjectNameDetected = String(); if (objectNamesValidatingResult !== null) { versionByObjectNameDetected = objectNamesValidatingResult[1]; log(logType.net, "Objects like " + objectNamesValidatingResult[0] + (versionByObjectNameDetected ? " (v" + versionByObjectNameDetected + ")" : String())); isObjectLikePacker = true; } // Clean up: release the dictionary dbCollectionOfObjectNamesDict = undefined; if (isObjectLikePacker) options = addOption(options, "Object names like " + objectNamesValidatingResult[0] + (versionByObjectNameDetected ? " (v" + versionByObjectNameDetected + ")" : String())); // Self-Extracting archives // TODO: Upgrade var isSfx = false; if (!PE_Cached.isDynamicLinkLibrary && PE_Cached.isOverlayPresent) { const overlayPatterns = [ "'Rar!'", // samples by: WinRAR "'PK'03", // samples by: Zip SFX (by Intel) "';!@Install@!UTF-8!'", "'7z'BCAF271C", "efbbbf';!@Install@!UTF-8!'" // samples by: 7z ] for (var l = 0; l < overlayPatterns.length; l++) { if (PE.compareOverlay(overlayPatterns[l])) { log(logType.any, "SFX archive pattern detected in overlay"); isSfx = true; } } if (!isSfx && !PE_Cached.isDotNet) { const sfxEntries = [ // "E8$$$$$$$$558BEC83EC..A1........8365....8365....5357BF........3BC7BB........74..85C374..F7D0", // samples by: WinZip // "558BEC6A..68........68........64A1........50648925........83....5356578965..FF15", // samples by: Zip SFX // "E8$$$$$$$$558BEC83EC..8365....8365....A1........5657BF........BE........3BC7", // samples by: WinRAR Installer // "E8$$$$$$$$8BFF558BEC83EC..A1........8365....8365....5357BF........BB", // samples by: Zip SFX // "558BEC83C4..B8........E8........33C05568........64FF30648920E8", // samples by: Zip SFX // "4883EC..E8$$$$$$$$48895C24..55488BEC4883EC..488365....48BB................488B05........483BC375", // samples by: WinRAR Installer "83EC..5657FF15........8BF08D4424..50C74424..........FF15........8A068B3D........3C..75..56FFD7", // samples by: Zip SFX "E9$$$$$$$$558BEC81EC........830D..........5356576A..33DBBF........68........895D..881D", // samples by: Microsoft Cabinet "558BEC83EC..56FF15........8BF08A003C..75..84C074..803E..74..46803E..75..803E..75..46EB", // samples by: Zip SFX "6A..33C0505050FF15........50E8$$$$$$$$55B8........8BECE8........53B9........5657BE", // samples by: Zip SFX "6A..68........E8........66813D............75..A1........81B8................75..", // samples by: Microsoft Cabinet "558BEC83EC..565733FFFF15........8BF0897D..8D45..50FF15........8A063C..75..56FF15", // samples by: Zip SFX "51526A..2EFF15........506A..6A..2EFF15........50E8........502EFF15........5A59C3", // samples by: WinIMP "558BEC81EC........535657FF15........A3........FF15........A1........6625....3D", // samples by: Microsoft Cabinet "558BECB8........E8........5356BE........578D45..5633DB5053FF15........85C00F84", // samples by: Zip SFX "A1........C1E0..A3........575133C0BF........B9........3BCF76..2BCFFCF3AA595F", // samples by: WinRAR Installer "558BEC83C4..5356E8$$$$$$$$E8........6A..E8........8905........E8........8905", // samples by: Zip SFX "FF15........B1..380874..B1..4080....74..380874..4080....75..80....74..4033", // samples by: WinZip "53FF15........B3..38..74..80C3..4033D28A083ACA74..3ACB74..408A083ACA75", // samples by: WinZip "558BEC83C4..535657E8........E8........33C05568........64FF30648920E8", // samples by: WinRAR "53FF15........B3..38..74..80C3..8A48..4033D23ACA74..3ACB74..8A48..40", // samples by: WinZip "E8$$$$$$$$53BB........E8........85C074..33D28A1083FA..75..40EB", // samples by: WinRAR "FFFE2A002A002A006D0065007300730061006700650073002A002A002A00", // samples by: WinRAR Installer "E8$$$$$$$$558BEC83C4..B8........53", // samples by: WinRAR Installer "8A48014033D23ACA740A3ACB74068A4801" // samples by: WinZip ] if (PE.isSectionNamePresent("_winzip_")) { isSfx = true; } if (PE_Cached.isArchX86) { for (var k = 0; k < sfxEntries.length; k++) { if (PE.compareEP(sfxEntries[k])) { log(logType.any, "SFX-style entry point pattern detected"); isSfx = true; } } } } } if (isSfx) options = addOption(options, "SFX"); var entryLikePacker = false; const entries = [ "53565755488D35........488DBE", // samples by: UPX (x64) "6A6068........E8........BF94", // samples by: PELock (x32) "68........E8........C3C3", // samples by: KByS (x32) "B8........68........64", // samples by: Petite (x32) "60..................E8", // samples by: Anticrack Software (x32) "33C08BC068........68", // samples by: ExE Pack (x32) "74..E9........60E8", // samples by: PE-PACK "EB0668........C39C", // samples by: PECompact (x32) "93071F05....8ED0BC", // samples by: aPack (x32) "558BEC83C4E05333C0", // samples by: NTPacker (x32) "60BE........8DBE", // samples by: UPX (x32) "B8........6A..68", // samples by: Petite (x32) "BE........AD8BF8", // samples by: WinUPack (x32) "68........9C60E8", // samples by: XComp, XPACK (x32) "53558BE833DBEB60", // samples by: WWPack (x32) "E8000000005D81ED", // samples by: ASPack (x32), TTP Pack (x32) "BD........C745", // samples by: kkrunchy (x32) "57565351524150", // samples by: mpress (x64) "B8........5064", // samples by: PECompact (x32) "8CCBBA....03DA", // samples by: aPack (x32) "B8........669C", // samples by: Petite, Themida (x32) "E8........53", // samples by: Themida (x32), Safengine Shielden "8CC0FA8ED0BC", // samples by: PACKWIN (x32) "B8........60", // samples by: Petite, Themida (x32) "60FC0FB605", // samples by: NakedPacker (x32) "8B44240456", // samples by: ASDPack (x32) "1E068CC88E", // samples by: aPack (x32) "1E068CCBBA", // samples by: aPack (x32) "EB..9C60E8", // samples by: PECompact (x32) "9C60E8CA", // samples by: Petite (x??) "60FCBED4", // samples by: ANDPakk (x32) "60EB..5D", // samples by: ASPack (x32) "60EB..E8", // samples by: G!X Protector "64FF35", // samples by: Petite (x32) "6033C0", // samples by: yzPack (x32) "669C60", // samples by: Petite (x??) "EB..60", // samples by: kkryptor, dePACK (x32) "60E8", // samples by: mpress, Packman, Pack Master, Yodas Crypter, DxPack, ASPack, MSLRH, tElock (x32), WinUPack "6068", // samples by: BeRo, ExE Pack, AHPacker (x32) "60B8", // samples by: NakedPacker (x32) "9C60" // samples by: PEBundle (x32) ]; // Iterate through the entries to check against the entry point for (var e = 0; e < entries.length && !entryLikePacker; e++) { if (PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86) { const entryToCheck = entries[e]; // If the entry point matches the current entry, set the flag to true and break the loop if (PE.compareEP(entryToCheck)) { log(logType.any, "Packer-like entry point pattern detected"); entryLikePacker = true; } } } if (entryLikePacker) options = addOption(options, "EntryPoint"); // Check for UPX-like structure: 3 sections with specific characteristics and specific imports // (common for UPX, but can be used by other packers as well) var isUpxLikeStructure = false; var isUpxLikeImports = false; if (PE_Cached.numberOfSections === 3 && PE.section[0].FileSize === 0 && PE.section[1].FileSize !== 0 && PE.section[2].FileSize !== 0 && PE.section[0].IsReadWriteExecute && PE.section[1].IsReadWriteExecute && !PE.section[2].IsReadWriteExecute && ( (PE.section[2].Flags & SECTION_FLAGS_READ) === SECTION_FLAGS_READ || (PE.section[2].Flags & (SECTION_FLAGS_READ | SECTION_FLAGS_WRITE)) === (SECTION_FLAGS_READ | SECTION_FLAGS_WRITE) ) ) { log(logType.any, "UPX-like section layout detected"); isUpxLikeStructure = true; if (PE.isLibraryPresentExp(/^kernel32(\.dll)?$/i) && PE.isFunctionPresent("GetProcAddress") && ( PE.isFunctionPresent("VirtualProtect") || PE.isFunctionPresent("LoadLibraryA") ) && ( PE.isFunctionPresent("ExitProcess") || // Original PE.isFunctionPresent("CopyContext") // https://github.com/DosX-dev/UPX-Patcher ) ) { isUpxLikeImports = true; } } if (isUpxLikeStructure) options = addOption(options, "Structure" + (isUpxLikeImports ? " and IAT " : " ") + "like UPX"); // Check if the first instruction at entry point starts with a stack operation var isStartsWithStackOperation = false, firstEpAsmOpCode; // Get the opcode of the first instruction at entry point and check for specific stack operations if (PE_Cached.isArchX86) { firstEpAsmOpCode = getFirstEpAsmOpCode(); switch (firstEpAsmOpCode) { case "PUSHAL": case "PUSHA": case "PUSHF": case "POPA": log(logType.emulation, "\"" + firstEpAsmOpCode + "\" at EP"); isStartsWithStackOperation = true; } } else if (PE_Cached.isARM || PE_Cached.isThumb) { firstEpAsmOpCode = getFirstEpAsmOpCode(); switch (firstEpAsmOpCode) { case "POP": case "STMDB": case "LDMIA": case "STMFD": case "LDMFD": log(logType.emulation, "\"" + firstEpAsmOpCode + "\" at EP"); isStartsWithStackOperation = true; } } else if (PE_Cached.isARM64) { firstEpAsmOpCode = getFirstEpAsmOpCode(); if (firstEpAsmOpCode === "LDP") { log(logType.emulation, "\"" + firstEpAsmOpCode + "\" at EP"); isStartsWithStackOperation = true; } } if (isStartsWithStackOperation) options = addOption(options, firstEpAsmOpCode[0] + firstEpAsmOpCode.toLowerCase().substring(1) + " at EP"); // Check if the entry point is in the last section var isLastSectionEP = false, isEpImportThunk = false; if (PE_Cached.isEntryPointAnalysisAllowed) { if (PE_Cached.numberOfSections > 1 && PE_Cached.isEntryPointCorrect) { // Raw offsets do not identify virtual sections: a zero-filled .bss can have offset zero. var lastSectionIndex = PE_Cached.numberOfSections - 1, entryPointAddress = PE_Cached.addressOfUnmanagedEntryPoint, lastSectionAddress = PE_Cached.imageBase + PE.getSectionVirtualAddress(lastSectionIndex), lastSectionSize = Math.max(PE.getSectionVirtualSize(lastSectionIndex), PE.getSectionFileSize(lastSectionIndex)); if (PE_Cached.indexOfEntryPointSection === lastSectionIndex && entryPointAddress >= lastSectionAddress && entryPointAddress < lastSectionAddress + lastSectionSize) { log(logType.any, "EP VA 0x" + entryPointAddress.toString(16) + " is inside the last section at VA 0x" + lastSectionAddress.toString(16)); isLastSectionEP = true; var epIndirectJump = PE_Cached.firstEpAsmInstruction && PE_Cached.firstEpAsmInstruction.match(/^JMP (?:DWORD|QWORD) PTR \[(0X[0-9A-F]+|\d+)\]$/i); if (PE_Cached.isArchX86 && epIndirectJump) { // A final-section jump through the declared IAT is an ordinary import thunk. var epOptionalHeaderOffset = getOptHeaderOffset(), epOptionalHeaderEnd = Math.min(epOptionalHeaderOffset + PE.readWord(epOptionalHeaderOffset - 4), PE_Cached.fileSize), epDirectoryCountOffset = epOptionalHeaderOffset + (PE_Cached.is64bit ? 0x6C : 0x5C), epIatDirectoryOffset = epOptionalHeaderOffset + (PE_Cached.is64bit ? 0x70 : 0x60) + 12 * 8; if (epDirectoryCountOffset + 4 <= epOptionalHeaderEnd && epIatDirectoryOffset + 8 <= epOptionalHeaderEnd && PE.read_uint32(epDirectoryCountOffset) > 12) { var epIatRva = PE.read_uint32(epIatDirectoryOffset), epIatSize = PE.read_uint32(epIatDirectoryOffset + 4), epThunkRva = parseInt(epIndirectJump[1], 0) - PE_Cached.imageBase, epPointerSize = PE_Cached.is64bit ? 8 : 4; isEpImportThunk = epIatRva !== 0 && epIatSize >= epPointerSize && epThunkRva >= epIatRva && epThunkRva + epPointerSize <= epIatRva + epIatSize; } } } } } if (isLastSectionEP && !isEpImportThunk) options = addOption(options, "Last section EP"); // Check for strange calls if entry point is in the last section var isStrangeCallOrJmpPresent = false, firstOpCode; if (PE_Cached.isArchX86) { firstOpCode = getAsmOpCode(PE_Cached.firstEpAsmInstruction); if (PE_Cached.isEntryPointAnalysisAllowed) { if (isLastSectionEP && !isEpImportThunk && !PE.compareEP("E8 00 00 00 00") && (firstOpCode === "CALL" || firstOpCode === "JMP")) { isStrangeCallOrJmpPresent = true; } } } else if (PE_Cached.isARM || PE_Cached.isThumb) { // ARM/Thumb: Check for strange branch instructions firstOpCode = getAsmOpCode(PE_Cached.firstEpAsmInstruction); if (PE_Cached.isEntryPointAnalysisAllowed) { // ARM uses B (branch) and BL (branch with link) for jumps and calls if (isLastSectionEP && (firstOpCode === "B" || firstOpCode === "BL" || firstOpCode === "BX" || firstOpCode === "BLX")) { isStrangeCallOrJmpPresent = true; } } } else if (PE_Cached.isARM64) { firstOpCode = getAsmOpCode(PE_Cached.firstEpAsmInstruction); if (PE_Cached.isEntryPointAnalysisAllowed && isLastSectionEP && (firstOpCode === "B" || firstOpCode === "BL" || firstOpCode === "BR" || firstOpCode === "BLR")) { isStrangeCallOrJmpPresent = true; } } if (isStrangeCallOrJmpPresent) { var strangeEpDirectTarget = PE_Cached.firstEpAsmInstruction.match(/^(?:CALL|JMP|B|BL|BX|BLX|BR|BLR) #?(0X[0-9A-F]+|\d+)$/i); log(logType.emulation, strangeEpDirectTarget ? "Strange " + firstOpCode.toLowerCase() + " at EP to VA " + strangeEpDirectTarget[1].toLowerCase() : "Strange indirect " + firstOpCode.toLowerCase() + " at EP: " + PE_Cached.firstEpAsmInstruction); } if (isStrangeCallOrJmpPresent) options = addOption(options, "Strange " + firstOpCode.toLowerCase() + " at EP"); // Check for packers and protections by imports (hashes) var isImportsLikePacker = false; // [{Name}, {Version}, {ImportLibraryIndex}, {Hash/Hashes}, {Type (0 = packer, 1 = cryptor, 2 = protector, 3 = protection)}] // If {ImportLibraryIndex} is -1, it means the hash has no index var dbCollectionOfHashesDict = [ // packers ["UPX", "0.59-0.93", 0, 0xd4fdcab1, 0], // https://github.com/upx/upx ["UPX", "0.94-1.93", 0, 0x1d51299a, 0], // . ["UPX", "1.94-2.03", 0, [0xb3318086, 0x3778aab9], 0], // . ["UPX", "2.90-3.XX", 0, 0xf375ee03, 0], // . ["UPX", "2.90-3.XX", -1, 0xf737d853, 0], // . ["UPX", "3.91+", -1, [0x82a048fc, 0x554a1748], 0], // . ["PESpin", null, 2, 0xd4fdcab1, 0], ["NsPack", null, -1, 0xf375ee03, 0], // https://nspack.apponic.com/ ["NsPack", "3.X", 0, 0x86111e49, 0], // . (for .NET) ["ASPack", "1.XX-2.XX", 0, 0x1272f45b, 0], // https://www.aspack.com/ ["ASPack", null, 0, 0xee6ea174, 0], ["MKFPACK", null, 0, 0x42b3e7f9, 0], ["MPRESS", null, 0, 0x174efb84, 0], // https://www.autohotkey.com/mpress/mpress_web.htm ["Packman", "0.0.0.1", 0, 0x174efb84, 0], // https://packmanpacker.sourceforge.net/ ["Packman", "1.0", 0, 0x69076a83, 0], // . ["PECompact", "0.90-0.91", -1, 0xbea416d1, 0], // https://bitsum.com/portfolio/pecompact/ ["PECompact", "0.92-0.94", -1, 0x93312c2e, 0], // . ["PECompact", "0.97-0.971b", -1, 0xe6aa8495, 0], // . ["PECompact", "1.10b7-1.34", -1, 0xe4c11305, 0], // . ["PECompact", "1.30-1.40", 0, 0x9b3305ed, 0], // . ["PECompact", "1.40-1.84", 0, 0xcc5b2a3c, 0], // . ["PECompact", "2.40-3.XX", 0, 0x2652ce4f, 0], // . ["PECompact", "2.40-3.XX", -1, 0xdb8fbb75, 0], // . ["tElock", "1.0", -1, 0x051946f7, 0], // https://www.softpedia.com/get/Programming/Packers-Crypters-Protectors/Telock.shtml ["JDPack", "1.01", 0, 0xd9380ce5, 0], ["JDPack", "2.00", 0, 0xc002db0e, 0], ["CRINKLER", null, 0, 0x0b0e1fbf, 0], ["XComp", "0.97-0.98", -1, 0xea1e66e4, 0], ["XPack", null, -1, [0x2ac44dd2, 0x6c170ab3], 0], ["kkrunchy", null, 0, 0x29188619, 0], // https://www.farbrausch.de/~fg/kkrunchy/ ["ANDPakk", "0.18", -1, 0x29188619, 0], ["RLPack", "1.1-1.2", 0, 0x5bd1d713, 0], ["RLPack", "1.16+", 0, 0x6889715b, 0], ["Petite", "2.1", -1, [0x69a2b86d, 0x69a2b86d, 0xaf362d6a], 0], // https://www.un4seen.com/petite/ ["Petite", "2.2", -1, 0x61e53682, 0], // . ["Petite", "2.X", 0, 0x6d73bcd2, 0], // . ["Petite", "2.X", -1, 0x61088397, 0], // . ["bambam", "0.1-0.4", 0, 0xb5b153cd, 0], ["py2exe", null, 2, 0xe5907ffa, 0], // https://pypi.org/project/py2exe/ ["py2exe", null, -1, 0xc544ca52, 0], ["MEW", null, 0, 0x87214e52, 0], ["nPack", "1.1+", 0, 0xba8bf4be, 0], ["CExe", "1.0a", 0, 0xdcf764d2, 0], ["PE-PACK", "1.0", 0, 0xa4d96677, 0], ["BoxedApp", null, 0, 0x6206c0c2, 0], // https://www.softanics.com/boxedapp ["VPacker", "0.02.X", 0, 0xebc3bc90, 0], ["PKLITE32", "1.1", 0, [0x621f19fb, 0xee04c332], 0], ["PKLITE32", "1.1", 1, 0x184539e7, 0], ["PeX", "0.99", 0, 0x5fbf5fd8, 0], ["XComp", null, 0, 0x4bc52e77, 0], ["Fatpack", null, 0, 0x74244911, 0], ["NTPacker", "1.0", 0, 0x1a721d2d, 0], ["NakedPacker", "1.0", 0, 0x64910c88, 0], ["ezip", "1.X", 1, 0x174efb84, 0], ["Rusty", null, 10, 0xa2260c1d, 0], // https://github.com/Azr43lKn1ght/Rusty-PE-Packer ["SimpleDpack", null, 0, [0x6facb4b4, 0x47bc97fd], 0], // https://github.com/YuriSizuku/win-SimpleDpack ["nexe", "2026", 0, 0xce10417c, 0], // https://github.com/nexe/nexe // cryptors ["Atyonekilla Native-Cryptor", null, 0, 0x4d0979fc, 1], // https://github.com/atyonekilla/Native-Cryptor ["Huan", null, 0, [0x66bc5f20, 0x4b3d7cea], 1], // https://github.com/frkngksl/Huan ["Daemon Crypt", "2.0", 0, [0x2c52a22e, 0xa253d95d], 1], // https://www.all-for-rus.narod.ru/files/pack/daemon_crypt_2.0.rar (pass: "all-for-rus.narod.ru") ["Strongbit EXECryptor", "1.4-1.5", -1, 0xd1de53b5, 1], ["Yoda's Crypter", "1.X", -1, 0x1303a51b, 1], ["Strongbit EXECryptor", "2.X", -1, 0xf51b2817, 1], ["Amber", null, -1, 0x97c72051, 1], // https://github.com/EgeBalci/amber ["Lumy", null, -1, 0x821669d5, 1], // protectors ["ASProtect", "1.XX-2.XX", 0, 0x1272f45b, 2], // https://www.asprotect.ru/ ["Shrinker", "3.2", 0, [0xb2a64858, 0x158af2d0, 0x49e8aa1f], 2], ["Shrinker", "3.5", 0, [0xe9ea0851, 0x3344b95d, 0x586088f3], 2], ["Enigma", "1.00-3.60", 0, 0xc002db0e, 2], // https://enigmaprotector.com/ ["Enigma", "2.XX", 0, 0xdd92de10, 2], // . ["Enigma", "3.70+", 0, 0xd04c7a50, 2], // . ["PCGUARD", "5.04-5.05", 0, [0x5a169c7a, 0x0b0b2965], 2], ["Vbs To Exe", null, 1, [0x2bc5f1f9, 0x009a2869, 0x5a7ce7d6, 0xe513a67d, 0x4d65f922], 2], ["eXPressor", "1.2", -1, [0x66b35c6e, 0x32f4466c], 2], // https://www.cgsoftlabs.ro/express.html ["eXPressor", "1.3", -1, [0x921d0280, 0xf51eba68, 0xbc84ce09], 2], // . ["eXPressor", "1.4.5.X", 0, [0x427816ab, 0x3c705cae, 0x4d02e093, 0x958a9ea2], 2], // . ["eXPressor", "1.5.0.X", -1, [0x7ababb5a, 0x95ca15e4, 0xbd41da20], 2], // . ["eXPressor", "1.6", -1, 0xca58fa0c, 2], // . ["eXPressor", "1.6", 0, 0x30bc0039, 2], // . ["eXPressor", "1.6.1", 0, 0xa5792a26, 2], // . ["eXPressor", "1.6.1", -1, 0x48ffd359, 2], // . ["VMProtect", "1.70", -1, 0x1ff3103f, 2], // https://vmpsoft.com/ ["VMProtect", "2.0.3-2.13", -1, 0x9d12b153, 2], // . ["VMProtect", "3.0.0", -1, 0x1e5500c1, 2], // . ["VMProtect", "3.0.9", -1, 0xc5fb6a4b, 2], // . ["VMProtect", "3.2.0-3.5.0", -1, 0x5caa99c7, 2], // . ["VMProtect", "3.6.0+", -1, [0x66e03954, 0xdae9f570], 2], // . ["YodasProtector", "1.0b", -1, 0x1303a51b, 2], ["ASM Guard", "2.XX+", -1, 0xf1e0d63b, 2], // https://github.com/DosX-dev/ASM-Guard ["Themida", "2.XX-3.XX", 0, 0x3ffccc8a, 2], // https://www.oreans.com/Themida.php ["Themida", null, 0, 0xad97c503, 2], // . [".NET Reactor", null, 0, 0x96be8e26, 2], // https://www.eziriz.com/dotnet_reactor.htm [".NET Reactor", null, 1, 0xb4cda32f, 2], // . ["Bat To Exe Converter", null, 0, 0x72a2ca64, 2], ["Vbs To Exe Converter", null, 0, 0x182aac68, 2], ["DNGuard", null, 0, 0x38432571, 2], // https://www.dnguard.net/ ["DNGuard", "4.9+", 0, 0x99f8b58e, 2], ["obfus.h", null, -1, 0x02f4771f, 2], // https://github.com/DosX-dev/obfus.h ; default, antidebug v1 ["Crunch", "1.0-2.0", 0, 0x615665dd, 2], ["Private EXE Protector", null, 0, 0xd89af68d, 2], // https://www.privacy-tutorials.com/private-exe-protector/ ["CodeVirtualizer", null, 4, 0x4fbeea23, 2], // https://www.oreans.com/CodeVirtualizer.php ["PELock", null, 0, [0x6eb8f6f1, 0xe571f715], 2], ["PELock", "2.X", 0, 0xe1689d7c, 2], ["CrypToCrack", "0.9.X", 0, 0x8b28bb4e, 2], ["DBPE", null, 0, [0x301ad755, 0x33406ae3], 2], ["Virbox", null, 0, 0xe6eb008d, 2], // https://lm-global.virbox.com/detail/virboxProtector.html ["Quick Batch File Compiler", null, -1, 0x347060fa, 2], // https://www.abyssmedia.com/quickbfc/ ["CodeVeil", null, 0, 0xd2c88a9c, 2], ["VCasm-Protector", "1.1-1.2", 0, 0xcfed7c1e, 2], ["Excalibur", "1.0X", 0, 0x7f364e1f, 2] ]; const importValidatingResult = validateImportHashes(dbCollectionOfHashesDict); var versionByImportsDetected; if (importValidatingResult !== null) { versionByImportsDetected = importValidatingResult[1]; log(logType.any, "Imports hash like " + importValidatingResult[0] + (versionByImportsDetected ? " (version " + versionByImportsDetected + ")" : String())); isImportsLikePacker = true; } // Clean up: release the dictionary dbCollectionOfHashesDict = undefined; if (isImportsLikePacker) options = addOption(options, "Imports like " + importValidatingResult[0] + (versionByImportsDetected ? " (v" + importValidatingResult[1] + ")" : String())); // Check for a large executable with unusually few imported functions. // Managed images normally need only a CLR entry import, so their native IAT size is not informative. var isStrangeSparseImportsPresent = false; if (!PE_Cached.isDynamicLinkLibrary && !PE_Cached.isDriver && !PE_Cached.isDotNet && PE_Cached.imageBodySize > 300 * 1024 && PE_Cached.numberOfUnmanagedImports > 0) { var importFunctionCount = PE.getNumberOfImportFunctions(), importResolutionBonus = 0; if (importFunctionCount === 8) { const importResolverPairs = [ ["LoadLibraryA", "GetProcAddress"], ["LoadLibraryW", "GetProcAddress"], ["LoadLibraryExA", "GetProcAddress"], ["LoadLibraryExW", "GetProcAddress"], ["GetModuleHandleA", "GetProcAddress"], ["GetModuleHandleW", "GetProcAddress"], ["LdrLoadDll", "LdrGetProcedureAddress"] ]; for (var i = 0; i < importResolverPairs.length; i++) { var importResolverPair = importResolverPairs[i]; if (PE.isFunctionPresent(importResolverPair[0]) && PE.isFunctionPresent(importResolverPair[1])) { importResolutionBonus = 1; break; } } } if (importFunctionCount > 0 && importFunctionCount < 8 + importResolutionBonus) { log(logType.any, "Large image with sparse imports; imported functions: " + importFunctionCount + (importResolutionBonus ? " (import resolution bonus: +1)" : String())); isStrangeSparseImportsPresent = true; } } if (isStrangeSparseImportsPresent) options = addOption(options, "Strange sparse imports"); // Check for packers and protections by section names var isSectionNameLikePacker = false; // [{Name}, {Version}, {SectionNameSignature}] var dbCollectionOfSectionNamesDict = [ ["Ramnit (infected)", null, ".rmnet"], ["Parite (infected)", null, /\.[a-z]{3}[\u0006\u0007\u0009]$/], ["Win9X.Dupator (infected)", null, "DUPATOR!"], ["Mars Stealer (payload)", null, "LLCPPC"], ["Themida", "2.XX", " "], // https://www.oreans.com/Themida.php ["Themida", "3.XX", [".loadcon", ".themida", ".winlice"]], // . ["Themida", null, [".stub01", "WinLicen", ".v-lizer", "Themida "]], // . ["DBPE", null, " "], ["PEBundle", null, /^(?:PEB|peb)undle$/], // https://bitsum.com/pebundle.htm ["DZA Patcher", null, "yoda"], ["UPX", null, /^UPX[0-3]$/], // https://github.com/upx/upx ["VMProtect", null, /^\.vmp[0-3]$/], // https://vmpsoft.com/ [".NET Reactor", "2.XX", ".reacto"], // https://www.eziriz.com/dotnet_reactor.htm ["ACProtect", null, ".perplex"], // https://acprotect-standard.soft112.com/ ["ANDpakk", "2.0", "ANDpakk2"], ["ASM Guard", "2.XX", [".asmg", "ASMGUARD"]], // https://github.com/DosX-dev/ASM-Guard ["ASPack", "1.08-2.XX", ".adata"], // https://www.aspack.com/ ["ASPack", "2.XX", ".aspack"], // . ["ASPack", null, /^(?:\.)?ASPack$/], // . ["Alienyze", null, ".alien"], // https://alienyze.com/ ["BoxedApp", null, ".bxpck"], // https://www.boxedapp.com/ ["CodeVirtualizer", null, ".vlizer"], // https://www.oreans.com/CodeVirtualizer.php ["CompotProtector", null, ".compot"], // https://github.com/aronpaaul/CompotProtector ["Enigma", null, /^\.enigma[12]$/], // https://enigmaprotector.com/ ["Eronana", null, ".packer"], // https://github.com/Eronana/packer ["MPRESS", "2.XX", /^\.MPRESS[12]$/], // https://www.autohotkey.com/mpress/mpress_web.htm ["NsPack", null, [/^(?:\.)?nsp[01]$/, /^PE[Pp][01]$/, ".Packer!"]], // https://nspack.apponic.com/ ["PE Diminisher", null, ".teraphy"], // https://web.archive.org/web/20060111104142/http://www.exetools.com/files/compressors/win/ped.zip ["PE-SHiELD", null, ["PESHiELD", "ANAKiN98"]], // https://webscene.ir/tools/show/PE-SHIELD-0.25 ["PECompact", null, ["PEC2MO", /^pec[12]?$/]], // https://bitsum.com/portfolio/pecompact/ ["PELock", null, ["PELOCKnt", ".pelock"]], // https://www.pelock.com/ 💩 ["Petite", null, /^(?:\.)?petite$/], // https://www.un4seen.com/petite/ ["SecuROM", null, [/^\.cms_[dt]$/, ".securom", ".dsstext"]], // https://en.wikipedia.org/wiki/SecuROM ["StarForce", "3.X", [".sforce3", ".brick"]], // https://www.star-force.com/ ["Wise Installer", null, [".wise", ".WISE"]], // https://wpkg.org/WISE_installer ["Gentee Installer", null, ".gentee"], // https://www.gentee.com/download/ ["Nullsoft Installer", null, ".ndata"], // https://sourceforge.net/projects/nsis/ ["WiX Installer", null, ".wixburn"], // https://github.com/wixtoolset ["BeRoEXEPacker", null, ["packerBY", "bero^fr "]], // https://blog.rosseaux.net/page/875fbe6549aa072b5ee0ac9cefff4827/BeRoEXEPacker ["Warbird", null, ["?g_Encry", "PROTDATA", /^PAGE(?:VRFY|wx[1-6])$/]], // https://security-explorations.com/microsoft-warbird-pmp.html ["YodasCrypter", "1.X", "yC"], // https://sourceforge.net/projects/yodap/files/Yoda%20Crypter/1.3/yC1.3.zip/download ["eXPressor", null, /^\.ex_(?:cod|rsc)$/], // https://www.cgsoftlabs.ro/express.html ["kkrunchy", null, "kkrunchy"], // https://www.farbrausch.de/~fg/kkrunchy/ ["tElock", null, "UPX!"], // https://www.softpedia.com/get/Programming/Packers-Crypters-Protectors/Telock.shtml ["Private EXE Protector", null, [".-PEP-", ".TRIAL!", ".const"]], // https://github.com/NIKJOO/PEP ["AtomPePacker", null, ".ATOM"], // https://web.archive.org/web/20221012050538/https://github.com/ORCx41/AtomPePacker ["ExeStealth", null, "ExeS"], // https://web.archive.org/web/20250124130104/https://www.webtoolmaster.com/exestealth.htm ["RLPack", null, [".RLPack", ".packed", ".Shield"]], ["RLPack", null, "Obsidium"], // (Fake Obsidium section name) ["BattlEye", null, /^\.be[0-2]$/], // https://www.battleye.com/ (VMProtect-based) ["YodasProtector", "1.0b", [".y0da", ".yP"]], // https://yodap.sourceforge.net/ ["obfus.h", null, /^\.obfh(?:[0-9]{2})?$/], // https://github.com/DosX-dev/obfus.h ["Windows PE Packer by Chenzs108", null, ".shell"], // https://github.com/czs108/Windows-PE-Packer ["SoftSentry", null, [/^_(?:stext|rdata|data|idata|rsrc|reloc)$/, ".prdata"]], // https://forum.exetools.com/showthread.php?t=1181 ["PE Lock Phantasm", "1.X", ["DINGBOY", ".Sign"]], // https://www.arteam.accessroot.com/download39d1.html ["XComp", null, /^\.XComp(?:0)?$/], // http://www.soft-lab.de/JoKo/ ["XVolkolak [unpacked]", null, ".xvlk"], // (unpacker & reconstructor) https://github.com/horsicq/XVolkolak ["XVolkolak [unpacked]", "1.0.0", /^\.sec[0-9][0-9]$/], // . ["Intel PIN [unpacked]", null, [".charmve", ".pinclie"]], // (unpacker & DBI framework) https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool ["ClamAV [unpacked]", null, /^\.clam0[1-9]$/], // (unpacker & recostructor, Antivirus) https://www.clamav.net/ ["Scylla [dumped]", null, ["unpacked", ".SCY", ".newimp"]], // (dumper & reconstructor) https://github.com/ntquery/scylla ["ImpRec [dumped]", null, ".mackt"], // (dumper & reconstructor) https://forum.tuts4you.com/files/file/2367-import-reconstructor-imprec/ ["OllyDump [dumped]", null, /^\.[Nn]ewIID$/], // (dumper & reconstructor) https://www.openrce.org/downloads/details/108/ollydump ["Process Dump [dumped]", null, /^pd_rec[0-9]$/], // (dumper & reconstructor) https://github.com/glmcdona/process-dump ["LordPE [edited]", null, ".NewSec"], // (dumper & reconstructor & editor) https://www.softpedia.com/get/Programming/File-Editors/LordPE.shtml ["DeDe [dumped]", null, ".dafixer"], // (dumper & reconstructor, Delphi) https://www.softpedia.com/get/Programming/Debuggers-Decompilers-Dissasemblers/DeDe.shtml ["PE-Add-Imports", null, ".at4re"], // (imports adder) https://github.com/fatrolls/PE-Add-Imports ["NetShield", null, "!Sugar"], // https://github.com/AdvDebug/NetShield_Protector ["Fatpack", null, ".fpack "], // https://github.com/Fatmike-GH/Fatpack ["Safengine Shielden", null, ".sedat"], // https://safengine.com/downloads/get-demo ["Inquartos Obfuscator", null, ".inq"], // http://www.vbnet.ru/forum/show.aspx?id=175877 ["WinUPack", null, [".ByDwing", ".Upack"]], // https://www.softpedia.com/get/PORTABLE-SOFTWARE/Compression-Tools/Windows-Portable-Applications-Portable-WinUpack.shtml ["Xenocode Postbuild", null, ".xcpad"], // https://download.cnet.com/xenocode-postbuild/3000-10250_4-10506240.html ["Denuvo", null, [/^\.[ex](?:code|text)$/, /^(?:\.)?v(?:3[5-9]|40)$/, ".xtls", ".arch", ".ooa"]], // https://en.wikipedia.org/wiki/Denuvo ["SteamStub", null, ".bind"], // https://gameindustry.eu/blog/steamstub-drm/ ["Huan", null, ".huan"], // https://github.com/frkngksl/Huan ["inject2pe", null, ".inj"], // https://github.com/0xballistics/inject2pe ["Helios", null, [".helios", ".htext"]], // https://t.me/helios_robot (protection online service) ["XerinFuscator", null, ".Xerin"], // https://xerinfuscator.com/ ["XerinFuscator", "8.X.X", ".xerin"], // . ["Eclipse-Protector", "1.X.X", ".eclipse"], // https://github.com/Av3lure1337/Eclipse-Protector ["PE Packer by levanvn", null, "packed"], // https://github.com/levanvn/PE_Packer ["SimpleDPack", null, ".dpack"], // https://github.com/YuriSizuku/win-SimpleDpack ["xor_packer", null, ".new"], // https://github.com/eternaleclipse/xor_packer ["Packed-Infected-PE", null, ".infect"], // https://github.com/huykingsofm/Packed-Infected-PE ["SafeDisc", null, /^stxt(?:774|371)$/], // https://ru.wikipedia.org/wiki/SafeDisc ["Origami", null, ".origami"], // https://github.com/dr4k0nia/Origami ["notcpuid", null, ".ptext"], // https://github.com/notcpuid/pe-packer ["WinZip SFX", null, "_winzip_"], // https://www.winzip.com/en/product/self-extractor/ ["RDG Tejon Crypter", "1.3", "RDGMax"], // http://crypter.unblog.fr/rdg-tejon-crypter-v13/ ["EsProtect", "1.3-3.9", [/^Esp[01]$/, /^\.Esp[! ]$/]], // https://www.greenxf.com/soft/29156.html ["troglodyte9", null, ".pkr"], // https://github.com/troglodyte9/packer ["xObf", null, ".xObf"], // https://github.com/d35ha/xObf ["Ryujin", null, ".Ryujin"], // https://github.com/keowu/Ryujin ["RPacked", "1.0", ".kek"], // https://github.com/BadRyuner/RPacked ["Alcatraz", null, ".0Dev"], // https://github.com/weak1337/Alcatraz ["ReWolf Virtualizer", null, ".VM"], // https://github.com/rwfpl/rewolf-x86-virtualizer ["TinyLoad", "7.X", ".pxkey"], // https://github.com/iamsopotatoe-coder/TinyLoad ["Wibu CodeMeter", null, /^__wibu0[01]$/], // https://www.wibu.com/products/codemeter.html ["AverCryptor", "1.0-1.02", [".avc", ".Polyene"]], // https://web.archive.org/web/20071012084924/http://secnull.org ["StarForce", "4.X-5.X", ".ps4"], // https://www.star-force.ru/ ["ExeShield", null, ".shield"], // https://exeshield.software.informer.com/ ["NoodleCrypt", "2.X", [".Ncryo ", ".De-vir "]], // https://www.all-for-rus.narod.ru/files/pack/noodlecrypt_2.rar ["hmimys", "1.3", "hmimys"], // https://www.52pojie.cn/thread-32248-1-1.html ["NakedPacker", null, [/^\.naked[12]$/, /^\.Kaos(?:2 |12)$/]], // https://www.all-for-rus.narod.ru/files/pack/nakedpacker_1.0.rar ["TSULoader", null, /^\.tsu(?:stub|arch)$/], // https://hybrid-analysis.com/sample/729976df3cfe53b4d82a3539ac558c7de79ba8897bb5ea03171a59efabecadae/6a5c2d7afcb6f0954104aae7 ["ARM Protector", null, ".armp"], // https://www.all-for-rus.narod.ru/files/pack/armp_0.3.rar (pass: "all-for-rus.narod.ru") ["pinkie-pie", "0.4", ".kucd"], // https://github.com/senko37/pe-packer ["Packman", "1.X", ".PACKMAN"], // https://packmanpacker.sourceforge.net/ ["BinstyShield", null, ".binshld"], // https://github.com/connorjaydunn/BinaryShield ["WProtect", null, ".WProtec"], // https://github.com/xiaoweime/WProtect ["UPX-Patcher", null, [".dosx", ".fish"]], // https://github.com/DosX-dev/UPX-Patcher ["ExeSafeGuard", "1.0", ".sg"], // https://github.com/DosX-dev/ExeSafeGuard-archived ["Ahri", null, ".ahri"], // https://github.com/aaaddress1/Ahri ["PE64shell", null, ".coleak"], // https://github.com/coleak2021/PE64shell ["theArk", null, ["30cm", ".tw"]], // https://github.com/aaaddress1/theArk ["SeauSFX", null, ".seau"], ["Code Entanglement Model", null, ".vmdata"], ["vQPU Protection Tech", null, [".vQPU", ".vqmeta"]], ["Rockey4nd Enveloper", "2.XX", "RY4SHL"], ["Rockey4nd Feitian Shell Protect Center", "1.X", ".ftsafe"], ["Alloy", null, ".alloy32"], ["Crinkler", null, "lz32.dll"], ["Crunch", "2.0", "BitArts"], ["CrypToCrack", null, ".ccp3p"], ["DxPack", "1.0", ["coderpub", ".reloc1"]], ["ExE Pack", "1.0-1.2", "!EPack"], ["FishPE Shield", null, ".FishPE"], ["FishPE", null, ".FISHPEP"], ["G!X Protector", null, [".g!x", ".guruX"]], ["JDPack", null, ".jdpack"], ["karity-protector", null, ".kvm"], // https://github.com/karity-0/karity-protector ["Krypton", null, ["krypton", "YADO", "_!_!_!_"]], ["MEW", null, ["MEW", ".decode"]], ["MaskPE", null, ".MaskPE"], ["Morphnah", "1.0.X", ".nah"], ["NativeCryptor", null, /^(?:NATIVES|CONFIG|(?:F)?CRYPT)~$/], ["NeoLite", null, ".neolite"], ["PE-PACK", "1.0", "PEPACK!!"], ["RLP", null, ".rlp"], ["ORiEN", "2.XX", ".loader"], ["SC Pack", null, ".scpack"], ["SVK Protector", null, ["SVKP", /\.svkp(?: )?/]], ["SafeNet", null, /^\.AKS[1-3]$/], ["Shrinker", null, ".shrink1"], ["Simple Pack", null, ".spack"], ["Software Compress", null, "SoftComp"], ["VCL", null, ".vcl"], ["VirtualizeProtect", null, "VProtect"], ["WWPack", null, ".WWPACK"], ["WWPack32", null, ".WWP32"], ["dePack", "1.3.5", ".depack"], ["nPack", null, ".nPack"], ["yzPack", null, ".yzpack2"], ["NeoLite", null, ".neolit"], ["bambam", null, ".bedrock"], ["WibuKey", null, ".wibu"], ["RPCrypt", null, /^(?:\.)?R(Cryptor|PCrypt)$/], ["ExE Pack", null, ["!EPack", ".!ep"]], ["AZProtect 0001", null, "AZPR0001"], ["XPack", null, /^\.XPack(?:0)?$/], ["KByS", null, ".shoooo"], ["HyperTech Crackproof", null, "peC"], ["Byfron", null, /^\.byfron(?:1)?$/], ["DYAMAR", "1.3.X", /^\.dyamar[CD]$/], ["Squishy", null, "logicoma"], ["DragonArmor", "0.0.4.1", /^(?:\.)?DAStub$/], ["EXECrypt", "1.0", "CRPT"], ["N-Code", "0.2", [".pepsi", "n-coded", ".vrs"]], ["Virbox", null, /^\.v(?:data[1-9]|irbox[1-9]?)$/], ["ElecKey", null, ".sstb"], ["PKLITE32", "1.1", ".pklstb"], [".BJFnt", "1.X", ".BJFnt"], ["Goliath", null, ".GOLIATH"], ["PE Encrypt", null, [".ice", ".lea"]], ["PECRYPT32", null, ".ficken"], ["PolyCrypt", "2.8", /^sec[0-9]$/], ["SDProtector", null, ".data "], ["Shielden", null, ".sedata"], ["SimbiOZ", null, ".ximera"], ["LARP", "2.X", ["SnD ", "LARP64"]], ["DalKrypt", "1.X", ".DalKiT"], ["Fish PE", "1.2-1.4", ".PEDATA"], ["NoobyProtect (Safengine)", null, "SE"], ["KeySec", null, ".ksec"], ["Lumy", null, ".lumy"], ["TomatoX", null, ".tomato"], ["PwdProtect", null, ".pwdprot"], ["ID Application", null, /^\.Prt(?:1)?$/], ["PETetris", null, "PETETRIS"], ["VCasm-Protector", null, "vcasm"], ["Pandora Obfuscator", null, ".pandora"], ["SafeGuard", null, /^\.safe[0-9]$/], ["SexCrypt", null, ".sex"], ["PEBizo", null, ".pebizo"], ["BlackMirror", null, /^\.bl(?:a)?ck$/], ["FUD Cryptor", null, ".fud"], ["Anskya NTPacker", null, "Anskya"], ["Bad UPX Patcher", null, /^BAD[0-1]$/], ["Feoktist Packer", null, "Feokt."], ["Snaker", null, ".snaker"], ["NEzZ Patcher", null, [".HACK", ".NEzZ"]], ["Zprotect", "1.X", ["PackedBy", ".Shadowz"]], ["Guy UPX Mod", null, ["Hello", "Guy !"]], ["AiR Cryptor", null, "TEAM AiR"], ["Stone's PE Encrypter", "1.13-2.0", /^\.Stone(?:r)?$/], ["RedWolf", null, [".!rc!", ".redwolf"]], ["SIS Cryptor", null, "SISCrypt"], ["ProCrypt", null, "ProCrypt"], ["YaWiGonaDown Team's Cryptor", null, ".WeGonaD"], ["BinPakker", null, ".pk00"], ["Triloxy", null, ".triex"], ["CCG Packer", null, ".ccg"], ["LZ4Loader", null, ".lz4"], ["KittyKat", null, ".meow"], ["!EProt", null, "!eprort"], ["Backdoor PE", null, ".BCPack"], ["TTProtect", null, ".TTP"], ["Xtreme-Protector", null, "CODE "], ["Soft Defender", null, /^SDP[CDI]$/], ["pepack's Protect", null, "okpack"] ]; const sectionNamesValidatingResult = validateSectionNames(dbCollectionOfSectionNamesDict.concat( (function () { var dbCollectionOfFakeSectionNamesDict = [ ["Themida", [/^themida$/i, ".Themida"]], ["Enigma", /^(?:\.)?enigma$/i], ["UPX", [/^\.upx/i, /^upx/]], ["VMProtect", /^(?:\.)?vmp$/i], ["ACProtect", /^perplex/i], ["PE-SHiELD", "peshield"], ["Alienyze", /^alien$/i], ["MPRESS", /^mpress/i], ["Denuvo", /denuvo|irdeto/i] ]; return dbCollectionOfFakeSectionNamesDict.map(function (entry) { return ["fake " + entry[0], null, entry[1]]; }); })() ).concat( [ ["a packer", null, [/p(?:a)?ck|sh(?:e)?ll|exe|^\.pe/i, /^PE/]], ["a cryptor", null, [/crypt|crpt|stub|enc|inj/i, /stb/]], ["a protector", null, [/prot|safe|lock|sec|virt|obf|guard|trial|sh(?:ie)?ld/i, /DRM|drm/]] ] ).concat([ [null, null, [ /^_guard_[cd]$/i, // Unity /^prot$/i // Chromium ]]] )); var versionBySectionDetected = String(); if (sectionNamesValidatingResult !== null) { versionBySectionDetected = sectionNamesValidatingResult[1]; log(logType.any, "Sections like " + sectionNamesValidatingResult[0] + (versionBySectionDetected ? " (v" + versionBySectionDetected + ")" : String())); isSectionNameLikePacker = true; } // Clean up: release the dictionary dbCollectionOfSectionNamesDict = undefined; if (isSectionNameLikePacker) options = addOption(options, "Sections like " + sectionNamesValidatingResult[0] + (versionBySectionDetected ? " (v" + sectionNamesValidatingResult[1] + ")" : String())); // Check for packers and protections by resource names var isResourceNameLikePacker = false; // [{Name}, {Version}, {ResourceNameSignature}] var dbCollectionOfResourceNamesDict = [ ["Bat To Exe Converter", null, /^(?=[0-9A-F]{40}$)(?=.*[0-9])(?=.*[A-F])[0-9A-F]{40}$/], ["ASM Guard", null, /^(?:ASMG\.DLL$|ASM_GUARD)/], ["Fatpack", null, "FPACK"], ["Quick Batch File Compiler", null, "PLATFORMTARGETS"], ["ScriptCryptor", null, "AOPT"], ["ExeStealth", null, "TN"], [".NET Reactor", null, "__"], ["SoftEntry", null, "SENTRYABORTDLG"], ["UPX Protector", null, "SCAREBYTE"], ["Vbs To Exe", null, "B"], ["QQProtect", null, "QQPROTECT"] ]; const resourceNamesValidatingResult = validateResourceNames(dbCollectionOfResourceNamesDict); var versionByResourceDetected = String(); if (resourceNamesValidatingResult !== null) { versionByResourceDetected = resourceNamesValidatingResult[1]; log(logType.any, "Resources like " + resourceNamesValidatingResult[0] + (versionByResourceDetected ? " (v" + versionByResourceDetected + ")" : String())); isResourceNameLikePacker = true; } // Clean up: release the dictionary dbCollectionOfResourceNamesDict = undefined; if (isResourceNameLikePacker) options = addOption(options, "Resources like " + resourceNamesValidatingResult[0] + (versionByResourceDetected ? " (v" + versionByResourceDetected + ")" : String())); // Check if there is a collision in sections var isCollisionInSectionsPresent = false; // Get section name collision between "0" and "1" const sectionNameCollision = PE.getSectionNameCollision("0", "1"); // Check if there is a collision if (sectionNameCollision.length) { log(logType.any, "Section names collision: \"" + File.cleanString(sectionNameCollision) + "\""); isCollisionInSectionsPresent = true; } if (isCollisionInSectionsPresent) options = addOption(options, "Sections collision (\"" + cleanSectionName(sectionNameCollision) + "\")"); // Check if there are repeating section names var isSectionNamesRepeatingPresent = false; var sectionNamesDict = {}; // Prefix external names so inherited Object members cannot hide repetitions. for (var i = 0; i < PE_Cached.numberOfSections && !isSectionNamesRepeatingPresent; i++) { var sectionName = PE.getSectionName(i), sectionNameKey = "$" + sectionName; sectionNamesDict[sectionNameKey] = (sectionNamesDict[sectionNameKey] || 0) + 1; if (sectionNamesDict[sectionNameKey] > 1) { log(logType.any, "Section names repeating: \"" + File.cleanString(sectionName) + "\""); isSectionNamesRepeatingPresent = true; } } // Clean up: release the dictionary sectionNamesDict = undefined; if (isSectionNamesRepeatingPresent) options = addOption(options, "Section names repeating"); // Check for compressed sections using entropy var isCompressedSectionPresent = false; var compressedSectionIndex = -1; var compressedSectionName = String(); // Reuse only an exact measured subrange for whole-image bounds, never a section estimate. // The largest measured range gives the tightest bounds. var largestMeasuredSectionEntropy = -1; var largestMeasuredSectionSize = 0; for (var t = 0; t < PE_Cached.numberOfSections && !isCompressedSectionPresent; t++) { compressedSectionIndex = t; if (PE.section[t].FileOffset === 0 || PE.section[t].FileSize < 0x1000) continue; var sectionEntropyRange = {}, sectionEntropy = calculateStructuredDataEntropy(PE.getSectionFileOffset(compressedSectionIndex), PE.getSectionFileSize(compressedSectionIndex), sectionEntropyRange); if (sectionEntropyRange.Size > largestMeasuredSectionSize && PE.section[t].FileOffset >= PE_Cached.imageBodyOffset && PE.section[t].FileOffset + PE.section[t].FileSize <= PE_Cached.imageBodyOffset + PE_Cached.imageBodySize) { largestMeasuredSectionEntropy = sectionEntropyRange.Entropy; largestMeasuredSectionSize = sectionEntropyRange.Size; } if (sectionEntropy > 7.4) { compressedSectionName = cleanSectionName(PE.getSectionName(compressedSectionIndex)); log(logType.any, "Section #" + compressedSectionIndex + " (\"" + compressedSectionName + "\") entropy lower bound: " + sectionEntropy); isCompressedSectionPresent = true; } } if (isCompressedSectionPresent) options = addOption(options, "Section #" + compressedSectionIndex + " (\"" + compressedSectionName + "\") compressed"); var standardSectionsWithUnusualValuesDict = [ ".bss", "BSS", ".tls", "hydrated", // .NET Native ".imrsiv" // UWP ]; // Check for sections with strange characteristics var isSectionHasWrongOffsetOrSize = false; var sectionWithWrongOffsetOrSizeIndex = -1; var wrongOffset = false, wrongSize = false; var sectionWithWrongOffsetOrSizeLabel = String(), wrongOffsetOrSizeDetails = String(), wrongOffsetOrSizeLogDetails = String(); for (var t = 0; t < PE_Cached.numberOfSections && !isSectionHasWrongOffsetOrSize; t++) { sectionWithWrongOffsetOrSizeIndex = t; if (standardSectionsWithUnusualValuesDict.indexOf(PE.getSectionName(sectionWithWrongOffsetOrSizeIndex)) === -1) { const currentSection = PE.section[sectionWithWrongOffsetOrSizeIndex], imagePhysicalEndOffset = PE_Cached.fileSize - PE_Cached.overlaySize; if (currentSection.FileOffset === 0x00 || currentSection.FileOffset >= imagePhysicalEndOffset) { wrongOffset = true; } // Validate the complete physical interval. Testing offset and size independently // misses truncated sections whose individually valid values extend past the image. if (currentSection.FileSize === 0x00 || currentSection.FileSize > PE_Cached.imageBodySize || currentSection.FileOffset + currentSection.FileSize > imagePhysicalEndOffset) { wrongSize = true; } isSectionHasWrongOffsetOrSize = wrongOffset || wrongSize; if (isSectionHasWrongOffsetOrSize) { sectionWithWrongOffsetOrSizeLabel = "Section #" + sectionWithWrongOffsetOrSizeIndex + " (\"" + cleanSectionName(PE.getSectionName(sectionWithWrongOffsetOrSizeIndex)) + "\")"; wrongOffsetOrSizeDetails = (wrongOffset ? "offset" : String()) + (wrongOffset && wrongSize ? "/" : String()) + (wrongSize ? "size" : String()); wrongOffsetOrSizeLogDetails = (wrongOffset ? "offset 0x" + currentSection.FileOffset.toString(16) : String()) + (wrongOffset && wrongSize ? ", " : String()) + (wrongSize ? "size 0x" + currentSection.FileSize.toString(16) : String()); log(logType.any, sectionWithWrongOffsetOrSizeLabel + " has wrong " + wrongOffsetOrSizeLogDetails); } } } if (isSectionHasWrongOffsetOrSize) options = addOption(options, sectionWithWrongOffsetOrSizeLabel + " has wrong " + wrongOffsetOrSizeDetails); // Standard zero-filled sections are allowed above, but they must not be executable var isExecutableZeroFilledSectionPresent = false; var executableZeroFilledSectionIndex = -1; for (var t = 0; t < PE_Cached.numberOfSections && !isExecutableZeroFilledSectionPresent; t++) { const currentSection = PE.section[t]; if (standardSectionsWithUnusualValuesDict.indexOf(PE.getSectionName(t)) !== -1 && currentSection.FileSize === 0x00 && (currentSection.Flags & SECTION_FLAGS_EXECUTE)) { log(logType.any, "Executable zero-filled section detected: #" + t + " (\"" + cleanSectionName(PE.getSectionName(t)) + "\")"); isExecutableZeroFilledSectionPresent = true; executableZeroFilledSectionIndex = t; } } if (isExecutableZeroFilledSectionPresent) options = addOption(options, "Section #" + executableZeroFilledSectionIndex + " (\"" + cleanSectionName(PE.getSectionName(executableZeroFilledSectionIndex)) + "\") executable zero-fill"); // Check for sections with identical offsets var isSectionHasDuplicateOffset = false; var currentSectionIndex = -1; var firstSectionIndex = 0, firstSectionName = String(), secondSectionIndex = 0, secondSectionName = String(); var sectionOffsetsDict = {}; for (var i = 0; i < PE_Cached.numberOfSections && !isSectionHasDuplicateOffset; i++) { currentSectionIndex = i; if (standardSectionsWithUnusualValuesDict.indexOf(PE.getSectionName(currentSectionIndex)) === -1) { var currentSectionOffset = PE.section[currentSectionIndex].FileOffset; if (currentSectionOffset !== 0) { if (typeof sectionOffsetsDict[currentSectionOffset] === "undefined") { sectionOffsetsDict[currentSectionOffset] = [currentSectionIndex]; } else { log(logType.any, "Section #" + sectionOffsetsDict[currentSectionOffset][0] + " (\"" + cleanSectionName(PE.getSectionName(sectionOffsetsDict[currentSectionOffset][0])) + "\") and #" + currentSectionIndex + " (\"" + cleanSectionName(PE.getSectionName(currentSectionIndex)) + "\") share offset 0x" + currentSectionOffset.toString(16)); firstSectionIndex = sectionOffsetsDict[currentSectionOffset][0]; secondSectionIndex = currentSectionIndex; firstSectionName = cleanSectionName(PE.getSectionName(firstSectionIndex)); secondSectionName = cleanSectionName(PE.getSectionName(secondSectionIndex)); isSectionHasDuplicateOffset = true; } } } } // Clean up: release the dictionary sectionOffsetsDict = undefined; if (isSectionHasDuplicateOffset) options = addOption(options, "Section #" + firstSectionIndex + " (\"" + firstSectionName + "\") and #" + secondSectionIndex + " (\"" + secondSectionName + "\") have one offset"); // Check for partially overlapping physical section ranges. Equal starting offsets are // reported by the preceding rule, while this rule covers every remaining intersection. var isSectionDataOverlappingPresent = false; for (var i = 0; i < PE_Cached.numberOfSections && !isSectionDataOverlappingPresent; i++) { const firstSectionFileOffset = PE.getSectionFileOffset(i), firstSectionFileSize = PE.getSectionFileSize(i), firstSectionFileEnd = firstSectionFileOffset + firstSectionFileSize; if (firstSectionFileOffset === 0 || firstSectionFileSize === 0) continue; for (var d = i + 1; d < PE_Cached.numberOfSections && !isSectionDataOverlappingPresent; d++) { const secondSectionFileOffset = PE.getSectionFileOffset(d), secondSectionFileSize = PE.getSectionFileSize(d), secondSectionFileEnd = secondSectionFileOffset + secondSectionFileSize; if (secondSectionFileOffset === 0 || secondSectionFileSize === 0 || firstSectionFileOffset === secondSectionFileOffset) continue; if (firstSectionFileOffset < secondSectionFileEnd && secondSectionFileOffset < firstSectionFileEnd) { log(logType.any, "Section #" + i + " (\"" + cleanSectionName(PE.getSectionName(i)) + "\") and #" + d + " (\"" + cleanSectionName(PE.getSectionName(d)) + "\") data overlap"); isSectionDataOverlappingPresent = true; } } } if (isSectionDataOverlappingPresent) options = addOption(options, "Overlapping section data"); // Authenticode is stored outside the mapped image, but a valid trailing certificate is // metadata rather than payload and must not influence size or entropy measurements. const overlayCertificateSize = PE_Cached.isOverlayPresent ? getWinAuthCertificateSize() : 0, overlayPayloadSize = Math.max(0, PE_Cached.overlaySize - overlayCertificateSize); // Verified PE headers and section data in the non-certificate overlay are packer evidence. var isEmbeddedPeInOverlayPresent = false; if (PE_Cached.isOverlayPresent && findEmbeddedPeInRange(PE_Cached.overlayOffset, overlayPayloadSize, Math.min(overlayPayloadSize, 0x400000)) !== -1) { log(logType.any, "Verified PE image in overlay"); isEmbeddedPeInOverlayPresent = true; } if (isEmbeddedPeInOverlayPresent) options = addOption(options, "Embedded PE in overlay"); // Check for a strange overlay in the PE file var hasStrangeOverlay = false; // Cache the payload entropy so the dedicated rule below can reuse the measurement. var overlayEntropy = -1; // A payload larger than the mapped image is independently anomalous, so entropy is // calculated only for smaller overlays where it can actually change the verdict. if (!isEmbeddedPeInOverlayPresent && !isSfx && (overlayPayloadSize > 150 || overlayPayloadSize > PE_Cached.imageBodySize) && !PE.isSigned()) { if (overlayPayloadSize > PE_Cached.imageBodySize) { log(logType.any, "Overlay payload exceeds the executable image: " + overlayPayloadSize + " bytes"); hasStrangeOverlay = true; } else { overlayEntropy = calculateStructuredDataEntropy(PE_Cached.overlayOffset, overlayPayloadSize); if (overlayEntropy > 7) { log(logType.any, "Overlay payload size: " + overlayPayloadSize + " bytes; entropy lower bound: " + overlayEntropy); hasStrangeOverlay = true; } } } if (hasStrangeOverlay) options = addOption(options, "Strange overlay"); // Verified PE headers and section data are stronger evidence than a bare MZ prefix. var isEmbeddedPeInResourcesPresent = false; var embeddedResourceSearchBudget = 0x800000; for (var i = 0; i < PE_Cached.numberOfUnmanagedResources && embeddedResourceSearchBudget >= 0x80 && !isEmbeddedPeInResourcesPresent; i++) { var embeddedResource = PE.resource[i], embeddedResourceSize = embeddedResource.FileSize; if (embeddedResourceSize < 0x80) continue; var embeddedResourceOffset = embeddedResource.FileOffset, embeddedResourceSearchSize = Math.min(embeddedResourceSize, embeddedResourceSearchBudget, 0x200000); if (embeddedResourceOffset < 0) continue; if (findEmbeddedPeInRange(embeddedResourceOffset, embeddedResourceSize, embeddedResourceSearchSize) !== -1) { log(logType.any, "Verified PE image in resources"); isEmbeddedPeInResourcesPresent = true; } embeddedResourceSearchBudget -= embeddedResourceSearchSize; } if (isEmbeddedPeInResourcesPresent) options = addOption(options, "Embedded PE in resources"); // Flag to indicate high entropy. Calculate only if the file is not a .dll with resources and no compressed sections are present. var isHighEntropy = false; if (!(PE_Cached.isDynamicLinkLibrary && (PE.section[".rdata"] || PE.isSectionNamePresent(".rsrc"))) && // .dll with resources !isCompressedSectionPresent) { const wholeImageEntropyBounds = calculateEntropyBounds(largestMeasuredSectionEntropy, largestMeasuredSectionSize, PE_Cached.imageBodySize); // A strict bound can settle obvious cases using entropy already calculated for the // largest section. Ambiguous mixtures retain the original exact whole-image pass. if (wholeImageEntropyBounds.minimum > 7.3 || (wholeImageEntropyBounds.maximum > 7.3 && PE.calculateEntropy(PE_Cached.imageBodyOffset, PE_Cached.imageBodySize) > 7.3)) { log(logType.any, "High overall image entropy detected"); isHighEntropy = true; } } if (isHighEntropy) options = addOption(options, "High entropy"); // A dominant signed or otherwise exempt overlay can carry packed data while the mapped // PE body remains ordinary. Unsigned overlays already reported by the broader rule above // are excluded to keep the two verdicts independent. var isOverlayHighEntropy = false; if (!hasStrangeOverlay && !isHighEntropy && overlayPayloadSize >= 0x1000 && overlayPayloadSize > PE_Cached.imageBodySize) { if (overlayEntropy < 0) { overlayEntropy = calculateStructuredDataEntropy(PE_Cached.overlayOffset, overlayPayloadSize); } if (overlayEntropy > 7.3) { log(logType.any, "High-entropy overlay dominates the executable image: " + overlayPayloadSize + " bytes; entropy lower bound: " + overlayEntropy); isOverlayHighEntropy = true; } } if (isOverlayHighEntropy) options = addOption(options, "Overlay high entropy"); // Check for recurring import names var recurringImports = false; var importNamesDict = {}; if (!_isResultPresent("linker", "Turbo Linker")) { // Ignore Delphi for (var i = 0; i < PE_Cached.numberOfUnmanagedImports && !recurringImports; i++) { var name = PE.getImportLibraryName(i), importNameKey = "$" + name; importNamesDict[importNameKey] = (importNamesDict[importNameKey] || 0) + 1; if (importNamesDict[importNameKey] >= 3) { log(logType.any, "Recurring import name: \"" + File.cleanString(name) + "\""); recurringImports = true; } } } // Clean up: release the dictionary importNamesDict = undefined; if (recurringImports) options = addOption(options, "Recurring import names"); var isPackerOrProtectionNotDetected = !_getNumberOfResults("packer") && !_getNumberOfResults("cryptor") && !_getNumberOfResults("protector") && !_getNumberOfResults("protection"); if (isPackerOrProtectionNotDetected) { if (isImportsLikePacker) { var typeOfDetectedItem = String(); switch (importValidatingResult[4]) { case 0: typeOfDetectedItem = "packer"; break; case 1: typeOfDetectedItem = "cryptor"; break; case 2: typeOfDetectedItem = "protector"; break; case 3: typeOfDetectedItem = "protection"; break; } if (typeOfDetectedItem) { var version = String(), importAndSectionValidationVerdictsDiffer = false; if (importValidatingResult[1]) { version = importValidatingResult[1]; } else if (isSectionNameLikePacker && !/^(?:a [pc]|fake )$/.test(sectionNamesValidatingResult[0])) { if (importValidatingResult[0] === sectionNamesValidatingResult[0]) { // is same result if (sectionNamesValidatingResult[1]) { version = sectionNamesValidatingResult[1]; } } else { // Inconsistency of verdicts importAndSectionValidationVerdictsDiffer = true; } } _setResult("~" + typeOfDetectedItem, importValidatingResult[0] + (importAndSectionValidationVerdictsDiffer ? "-like" : String()), version, "Suspicion only"); } } else if (isUpxLikeStructure && isUpxLikeImports) { _setResult("~packer", "UPX-like", String(), "Suspicion only"); } } if (options.length !== 0) isDetected = true; if (isDetected) { var detectedType = isCryptor ? "cryptor" : "packer"; _setResult("~" + detectedType, "Generic", String(), PE_Cached.isVerbose ? options : String()); } } // #endregion // #region "licensing systems (.NET and Native)" /** Detects licensing-system markers in managed and native PE files. */ function scanForLicensingSystems_NET_and_Native() { // For .NET and Native apps log(logType.nothing, "Scanning for licensing systems..."); var options = String(); var isDetected = Boolean(); if (PE_Cached.isDotNet) { var isLicenseCheckingPresent = false; if (validateNetObject("CheckLicense") || validateNetObject("set_License") || validateNetObject("Licensing")) { log(logType.net, "Managed image contains licensing-related methods"); isLicenseCheckingPresent = true; } if (isLicenseCheckingPresent) options = "DotNET methods"; // https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.licenseproviderattribute var isLicenseProviderPresent = false; if (validateNetObject("LicenseProviderAttribute")) { log(logType.net, "Managed image uses the standard .NET license provider attribute"); isLicenseProviderPresent = true; } if (isLicenseProviderPresent) options = addOption(options, "Provider attribute"); // https://learn.microsoft.com/ru-ru/dotnet/api/system.componentmodel.licensemanager var isLicenseManagerPresent = false; if (validateNetObject("LicenseManager")) { log(logType.net, "Managed image uses the standard .NET license manager"); isLicenseManagerPresent = true; } if (isLicenseManagerPresent) options = addOption(options, "License manager"); } // https://en.wikipedia.org/wiki/Denuvo var isDenuvoLibraryPresent = false; if (PE.isLibraryPresentExp(/^(?:Core[\\/]Activation(?:64)?\.dll)$/i) || ( PE_Cached.numberOfSections > 10 && PE_Cached.numberOfUnmanagedImports > 10 && ( PE.isSectionNamePresentExp(/^\.[ex](?:code|text)$/) || PE.isExportFunctionPresentExpFastAnchor("getdenuvo", /^GetDenuvo(?:TimeTicketRequest|TicketLocation)$/) ) )) { log(logType.any, "Denuvo-like licensing layout detected"); isDenuvoLibraryPresent = true; } if (isDenuvoLibraryPresent) options = addOption(options, "Denuvo DRM"); // https://gameindustry.eu/blog/steamstub-drm/ var isSteamStubPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86 && PE.compareEP("E800000000") && PE.section[PE_Cached.numberOfSections - 1].Name === ".bind") { log(logType.any, "SteamStub-like DRM layout detected"); isSteamStubPresent = true; } if (isSteamStubPresent) options = addOption(options, "SteamStub DRM"); // Check for licensing strings var isInterestingStringsFound = false; // Generic strings are a fallback for images without a stronger licensing marker. // Besides avoiding redundant details, this prevents many full-body searches in // large protected images after the licensing system has already been identified. if (options.length === 0) { const licensingStringContextLength = 256; // Entry: { marker, strings[, prefixVariants] }; variant: { prefix, strings }. // Each marker requires an expensive full-area engine search, so share suffixes. // strings holds lowercase local context and must not repeat marker; null makes // marker sufficient. prefixVariants classifies characters immediately before // marker and adds variant-only context without anchor self-validation. const licensingStringsDictionary = [ { marker: /* [T/Se] */ "rial ", strings: ["enter", "key", "invalid", "wrong", "expired", "required", "activation", "activate", "license", "full version", "purchase", "unlock", "registration", "free", "version", "period", "mode", "ended", "subscription", "upgrade", "demo"], prefixVariants: [{ prefix: "t", strings: ["serial"] }, { prefix: "se", strings: ["trial"] }] }, { marker: /* [A] */ "ctivat", strings: ["wrong", "license", "serial", "product", "subscription", "trial", "demo", "purchase", "your copy", "key", "invalid", "expired", "required", "full version", "unlock", "registration"] }, { marker: /* [L] */ "icen", strings: ["expire", "key", "wrong", "valid", "invalid", "required", "not found", "missing", "revoked", "activation", "activate", "validation", "verification", "serial", "trial", "demo", "subscription", "purchase", "full version", "unlock", "registration"] }, { marker: /* [F] */ "ull version", strings: ["unlock", "purchase", "upgrade", "activate", "require", "license", "serial", "trial", "demo", "subscription", "key", "registration"] }, { marker: /* [P] */ "urchase a ", strings: ["license", "subscription", "full version", "unlock", "serial", "activation", "trial", "demo", "upgrade"] } ]; function isLicensingStringGroupPresent(areaOffset, areaSize, dictionaryEntry, isUnicode) { var currentMarker = dictionaryEntry.marker, validatingStrings = dictionaryEntry.strings, markerPattern = isUnicode ? convertStringToUnicodeSignature(currentMarker) : currentMarker, characterSize = isUnicode ? 2 : 1, currentOffset = areaOffset, remainingSize = areaSize; // A null dictionary entry means the marker itself is the complete // original pattern, so no candidate extraction is necessary. if (validatingStrings === null) { return (isUnicode ? PE.findSignature(areaOffset, areaSize, markerPattern) : PE.findString(areaOffset, areaSize, markerPattern)) !== -1; } while (remainingSize >= currentMarker.length * characterSize) { var markerOffset = isUnicode ? PE.findSignature(currentOffset, remainingSize, markerPattern) : PE.findString(currentOffset, remainingSize, currentMarker); if (markerOffset === -1) break; var advanced = markerOffset - currentOffset + characterSize; currentOffset += advanced; remainingSize -= advanced; // Read the short prefix in one operation, then decode it locally. // This lets a shared anchor validate phrases which begin before it // without issuing a separate engine comparison for every phrase. var prefixSize = Math.min(licensingStringContextLength * characterSize, markerOffset - areaOffset); prefixSize -= prefixSize % characterSize; var prefixBytes = PE.readBytes(markerOffset - prefixSize, prefixSize), candidateString = String(); for (var i = 0; i < prefixBytes.length; i += characterSize) { var characterCode = prefixBytes[i]; if (isUnicode) characterCode |= prefixBytes[i + 1] << 8; candidateString += String.fromCharCode(characterCode); } // Ignore adjacent null-terminated strings so their unrelated words // cannot validate the marker found in the current string. var stringBoundary = candidateString.lastIndexOf("\0"); if (stringBoundary !== -1) candidateString = candidateString.substring(stringBoundary + 1); // Prefix variants let several complete words share one engine anchor. // Reject unrelated suffix matches before requesting the remaining string. var markerVariantStrings = null; if (dictionaryEntry.prefixVariants !== undefined) { for (var i = 0; i < dictionaryEntry.prefixVariants.length; i++) { var currentPrefixVariant = dictionaryEntry.prefixVariants[i], prefixOffset = candidateString.length - currentPrefixVariant.prefix.length; if (prefixOffset >= 0 && candidateString.substring(prefixOffset).toLowerCase() === currentPrefixVariant.prefix) { markerVariantStrings = currentPrefixVariant.strings; break; } } if (markerVariantStrings === null) continue; } // The remaining characters are obtained in one engine call. Keep // the read inside the current scan area to preserve its boundaries. var remainingCharacterCount = Math.min(licensingStringContextLength, Math.floor((areaOffset + areaSize - markerOffset) / characterSize)); candidateString += isUnicode ? File.read_unicodeString(markerOffset, remainingCharacterCount) : PE.getString(markerOffset, remainingCharacterCount); candidateString = candidateString.toLowerCase(); for (var i = 0; i < validatingStrings.length; i++) { if (candidateString.indexOf(validatingStrings[i]) !== -1) return true; } if (markerVariantStrings !== null) { for (var i = 0; i < markerVariantStrings.length; i++) { if (candidateString.indexOf(markerVariantStrings[i]) !== -1) return true; } } } return false; } function isLicensingStringPresentInArea(areaOffset, areaSize) { for (var i = 0; i < licensingStringsDictionary.length; i++) { const currentDictionaryEntry = licensingStringsDictionary[i]; if ((!PE_Cached.isDotNet && isLicensingStringGroupPresent(areaOffset, areaSize, currentDictionaryEntry, false)) || isLicensingStringGroupPresent(areaOffset, areaSize, currentDictionaryEntry, true)) return true; } return false; } for (var i = 0; i < PE_Cached.numberOfSections && !isInterestingStringsFound; i++) { var sectionOffset = PE.getSectionFileOffset(i), sectionSize = PE.getSectionFileSize(i), sectionName = PE.getSectionName(i); // Skip only canonical areas whose role excludes licensing messages. // Resources are not excluded by role because user-facing text commonly lives there. if (PE_Cached.numberOfSections > 1) { if (sectionOffset === 0 || sectionSize < 0x2000) continue; var isLicensingPrimaryNativeCodeSection = !PE_Cached.isDotNet && i === PE_Cached.indexOfEntryPointSection && sectionName === ".text" && i === 0; var isLicensingDedicatedImportsSection = i === PE_Cached.indexOfImportsSection && sectionName === ".idata" && i !== PE_Cached.indexOfResourceSection && i !== PE_Cached.indexOfEntryPointSection && i !== PE_Cached.indexOfTlsSection && i !== PE_Cached.indexOfRelocsSection; var isLicensingDedicatedRelocsSection = i === PE_Cached.indexOfRelocsSection && sectionName === ".reloc" && i !== PE_Cached.indexOfResourceSection && i !== PE_Cached.indexOfEntryPointSection && i !== PE_Cached.indexOfTlsSection && i !== PE_Cached.indexOfImportsSection; if (isLicensingPrimaryNativeCodeSection || isLicensingDedicatedImportsSection || isLicensingDedicatedRelocsSection) continue; } if (sectionOffset > 0 && sectionOffset < PE_Cached.fileSize) { sectionSize = Math.min(sectionSize, PE_Cached.fileSize - sectionOffset); if (isLicensingStringPresentInArea(sectionOffset, sectionSize)) { log(logType.any, "Licensing-related string context detected"); isInterestingStringsFound = true; } } } } if (isInterestingStringsFound) options = "Strings"; if (options.length !== 0) isDetected = true; if (isDetected) { _setResult("~licensing", "Contains", String(), PE_Cached.isVerbose ? options : String()); } } // #endregion // #region "corrupted data (.NET and Native)" /** Checks PE structure and metadata for corruption and assigns a corruption severity. */ function scanForBadFileFormat_NET_and_Native() { log(logType.nothing, "Scanning for corrupted data..."); var options = String(); var isDetected = Boolean(); const corruptedDataLevel = { uncritical: 0, degraded: 1, critical: 2 }; const corruptedDataLevelName = ["Uncritical", "Degraded", "Critical"]; var detectedCorruptedDataLevel = corruptedDataLevel.uncritical; function addCorruptedDataOption(level, option) { detectedCorruptedDataLevel = Math.max(detectedCorruptedDataLevel, level); options = addOption(options, option); } // EP check var isEntryPointIncorrect = false; var isTlsVirtualEntryPoint = false; if (PE_Cached.isTlsPresent) { const corruptedDataEntryPointRva = PE_Cached.addressOfUnmanagedEntryPoint - PE_Cached.imageBase; if (corruptedDataEntryPointRva > 0) { for (var i = 0; i < PE_Cached.numberOfSections && !isTlsVirtualEntryPoint; i++) { var corruptedDataSectionRva = PE.getSectionVirtualAddress(i), corruptedDataSectionFileSize = PE.getSectionFileSize(i), corruptedDataSectionMappedSize = Math.max(PE.getSectionVirtualSize(i), corruptedDataSectionFileSize), corruptedDataEntryPointOffset = corruptedDataEntryPointRva - corruptedDataSectionRva; // The loader maps this range as zero-filled memory even though no bytes exist // in the file. A TLS callback may populate it before normal EP execution. if (corruptedDataEntryPointOffset >= corruptedDataSectionFileSize && corruptedDataEntryPointOffset >= 0 && corruptedDataEntryPointOffset < corruptedDataSectionMappedSize && (PE.section[i].Flags & SECTION_FLAGS_EXECUTE)) { isTlsVirtualEntryPoint = true; } } } } // A DB pseudo-instruction normally means that the EP is damaged or points to data. // It is expected when a TLS callback owns a deliberately virtual-only startup area. if (!PE_Cached.isDotNet && (!PE_Cached.isDynamicLinkLibrary || PE_Cached.hasUnmanagedEntryPoint) && !isTlsVirtualEntryPoint && (!PE_Cached.isEntryPointCorrect || /^DB /.test(PE_Cached.firstEpAsmInstruction))) { log(logType.any, "Entry point appears invalid or points to data"); isEntryPointIncorrect = true; } if (isEntryPointIncorrect) addCorruptedDataOption(corruptedDataLevel.critical, "EntryPoint"); // Section alignment check var isSectionAlignmentIncorrect = false; if (!PE.isSectionAlignmentCorrect()) { log(logType.any, "Section alignment appears invalid"); isSectionAlignmentIncorrect = true; } if (isSectionAlignmentIncorrect) addCorruptedDataOption(corruptedDataLevel.critical, "Section alignment"); // File alignment check var isFileAlignmentIncorrect = false; if (!PE.isFileAlignmentCorrect()) { log(logType.any, "File alignment appears invalid"); isFileAlignmentIncorrect = true; } if (isFileAlignmentIncorrect) addCorruptedDataOption(corruptedDataLevel.critical, "File alignment"); // Check for correct header var isHeaderIncorrect = false; if (!PE.isHeaderCorrect()) { log(logType.any, "PE header validation failed"); isHeaderIncorrect = true; } if (isHeaderIncorrect) addCorruptedDataOption(corruptedDataLevel.critical, "Header"); // Validate the section table as one virtual and physical map. Individual fields may // remain in range while two sections overlap, appear out of RVA order, begin inside // the headers, or extend beyond the memory reserved by SizeOfImage. var isInvalidSectionLayoutPresent = false; var invalidSectionLayoutReason = String(); const sectionLayoutOptionalHeaderOffset = getOptHeaderOffset(), sectionLayoutOptionalHeaderSize = PE.read_uint32(sectionLayoutOptionalHeaderOffset - 4) & 0xFFFF, sectionLayoutHeadersEnd = sectionLayoutOptionalHeaderOffset + sectionLayoutOptionalHeaderSize + PE_Cached.numberOfSections * 40, sectionLayoutSizeOfImage = PE.getImageOptionalHeader("SizeOfImage"); var previousMappedSectionRva = -1; for (var i = 0; i < PE_Cached.numberOfSections && !isInvalidSectionLayoutPresent; i++) { const currentSectionRva = PE.getSectionVirtualAddress(i), currentSectionRawOffset = PE.getSectionFileOffset(i), currentSectionRawSize = PE.getSectionFileSize(i), currentSectionVirtualSize = PE.getSectionVirtualSize(i), currentSectionMappedSize = currentSectionVirtualSize !== 0 ? currentSectionVirtualSize : currentSectionRawSize, currentSectionVirtualEnd = currentSectionRva + currentSectionMappedSize; // SizeOfHeaders may include alignment padding beyond the section table. Only bytes // occupied by actual PE structures make an early raw offset structurally invalid. // A zero raw offset is already reported by the dedicated offset/size rule. if (currentSectionRawSize !== 0 && currentSectionRawOffset !== 0 && currentSectionRawOffset < sectionLayoutHeadersEnd) { invalidSectionLayoutReason = "section #" + i + " raw data begins inside headers"; isInvalidSectionLayoutPresent = true; } else if (currentSectionMappedSize !== 0 && sectionLayoutSizeOfImage !== 0 && currentSectionVirtualEnd > sectionLayoutSizeOfImage) { invalidSectionLayoutReason = "section #" + i + " exceeds SizeOfImage"; isInvalidSectionLayoutPresent = true; } else if (currentSectionMappedSize !== 0 && previousMappedSectionRva !== -1 && currentSectionRva < previousMappedSectionRva) { invalidSectionLayoutReason = "section #" + i + " is out of RVA order"; isInvalidSectionLayoutPresent = true; } if (currentSectionMappedSize !== 0) previousMappedSectionRva = currentSectionRva; // Compare declared mapped intervals without alignment rounding. Adjacent sections // remain valid; any positive intersection makes the loader view ambiguous. for (var d = i + 1; d < PE_Cached.numberOfSections && !isInvalidSectionLayoutPresent; d++) { const comparedSectionRva = PE.getSectionVirtualAddress(d), comparedSectionVirtualSize = PE.getSectionVirtualSize(d), comparedSectionMappedSize = comparedSectionVirtualSize !== 0 ? comparedSectionVirtualSize : PE.getSectionFileSize(d), comparedSectionVirtualEnd = comparedSectionRva + comparedSectionMappedSize; if (currentSectionMappedSize !== 0 && comparedSectionMappedSize !== 0 && currentSectionRva < comparedSectionVirtualEnd && comparedSectionRva < currentSectionVirtualEnd) { invalidSectionLayoutReason = "sections #" + i + " and #" + d + " overlap in memory"; isInvalidSectionLayoutPresent = true; } } } if (isInvalidSectionLayoutPresent) { log(logType.any, "Invalid section layout: " + invalidSectionLayoutReason); } if (isInvalidSectionLayoutPresent) addCorruptedDataOption(corruptedDataLevel.critical, "Invalid section layout"); // Check for correct section headers var isRelocsTableIncorrect = false; if (!PE.isRelocsTableCorrect()) { log(logType.any, "Relocation table appears malformed"); isRelocsTableIncorrect = true; } if (isRelocsTableIncorrect) addCorruptedDataOption(corruptedDataLevel.degraded, "Relocs"); // Check IAT (Import Address Table) // Can be destroyed when the PE dumped from memory var isImportTableIncorrect = false; if (!PE.isImportTableCorrect()) { log(logType.any, "Import table appears malformed"); isImportTableIncorrect = true; } if (isImportTableIncorrect) addCorruptedDataOption(corruptedDataLevel.critical, "IAT"); // Check EAT (only for .dll) var isExportTableIncorrect = false; // Resource-only and compatibility DLLs may deliberately retain an empty // IMAGE_EXPORT_DIRECTORY. The protection pass reports that unusual state on its // own; table correctness is meaningful here only after an export was resolved. if (PE_Cached.isDynamicLinkLibrary && PE_Cached.numberOfUnmanagedExports > 0 && !PE.isExportTableCorrect()) { log(logType.any, "Export table appears malformed"); isExportTableIncorrect = true; } if (isExportTableIncorrect) addCorruptedDataOption(corruptedDataLevel.degraded, "EAT"); // Check for resources table var isResourcesTableIncorrect = false; if (!PE.isResourcesTableCorrect()) { log(logType.any, "Resource table appears malformed"); isResourcesTableIncorrect = true; } if (isResourcesTableIncorrect) addCorruptedDataOption(corruptedDataLevel.degraded, "Resources"); // Validate every loader-mapped PE data-directory range against the loaded image. // The certificate table stores a file offset and is handled by the dedicated checks // below; treating that offset as mapped-image metadata produces false corruption. var isUnmappedDataDirectoryPresent = false; var unmappedDataDirectoryName = String(); const dataDirectoryOptionalHeaderOffset = getOptHeaderOffset(), dataDirectoryNumberOffset = dataDirectoryOptionalHeaderOffset + (PE_Cached.is64bit ? 0x6C : 0x5C), dataDirectoryTableOffset = dataDirectoryOptionalHeaderOffset + (PE_Cached.is64bit ? 0x70 : 0x60), dataDirectoryOptionalHeaderSize = PE.readWord(dataDirectoryOptionalHeaderOffset - 4), dataDirectoryOptionalHeaderEnd = Math.min( dataDirectoryOptionalHeaderOffset + dataDirectoryOptionalHeaderSize, PE_Cached.fileSize ); // Every rule below still validates its physical slot; caching only avoids repeated reads. const declaredDataDirectoryCount = dataDirectoryNumberOffset + 4 <= dataDirectoryOptionalHeaderEnd ? PE.read_uint32(dataDirectoryNumberOffset) : 0; var dataDirectoryMappedRanges; /** * Checks continuous image coverage across section boundaries and alignment padding. * * @param {number} beginRva - First required RVA. * @param {number} endRva - Last required RVA, inclusive. * @returns {boolean} True when no unmapped gap interrupts the complete range. */ function isBadFormatRangeCovered(beginRva, endRva) { if (beginRva < 0 || endRva < beginRva || endRva >= sectionLayoutSizeOfImage) return false; if (!dataDirectoryMappedRanges) { // SectionAlignment spaces sections; only hardware pages are committed around their actual contents. var dataDirectoryMappingAlignment = PE_Cached.architecture === "IA64" ? 0x2000 : 0x1000; dataDirectoryMappedRanges = [[0, Math.min(sectionLayoutSizeOfImage, Math.ceil(PE.getImageOptionalHeader("SizeOfHeaders") / dataDirectoryMappingAlignment) * dataDirectoryMappingAlignment)]]; for (var d = 0; d < PE_Cached.numberOfSections; d++) { var dataDirectorySectionRva = PE.getSectionVirtualAddress(d), dataDirectorySectionSize = Math.max(PE.getSectionVirtualSize(d), PE.getSectionFileSize(d)); if (dataDirectorySectionSize > 0) dataDirectoryMappedRanges.push([dataDirectorySectionRva, Math.min(sectionLayoutSizeOfImage, dataDirectorySectionRva + Math.ceil(dataDirectorySectionSize / dataDirectoryMappingAlignment) * dataDirectoryMappingAlignment)]); } dataDirectoryMappedRanges.sort(function (leftRange, rightRange) { return leftRange[0] - rightRange[0]; }); } var coveredRva = beginRva; for (var d = 0; d < dataDirectoryMappedRanges.length && coveredRva <= endRva; d++) { if (dataDirectoryMappedRanges[d][0] > coveredRva) break; coveredRva = Math.max(coveredRva, dataDirectoryMappedRanges[d][1]); } return coveredRva > endRva; } var dataDirectoryNames = [ "EAT", "Imports", "Resources", "Exceptions", "Security", "Relocs", "Debug", "Architecture", "GlobalPtr", "TLS", "Load config", "Bound imports", "IAT", "Delay imports", "CLR" ]; if (dataDirectoryNumberOffset + 4 <= dataDirectoryOptionalHeaderEnd && dataDirectoryTableOffset < dataDirectoryOptionalHeaderEnd) { // The reserved entry is ignored by the loader and has no mapping semantics. const dataDirectoryCount = Math.min( declaredDataDirectoryCount, IMAGE_DIRECTORY_ENTRY_RESERVED, Math.floor((dataDirectoryOptionalHeaderEnd - dataDirectoryTableOffset) / 8) ); for (var i = 0; i < dataDirectoryCount && !isUnmappedDataDirectoryPresent; i++) { var currentDataDirectoryOffset = dataDirectoryTableOffset + i * 8, currentDataDirectoryRva = PE.read_uint32(currentDataDirectoryOffset), currentDataDirectorySize = PE.read_uint32(currentDataDirectoryOffset + 4); // A partially empty entry is tolerated here because old linkers occasionally // preserve a stale RVA. Only a complete declared range is used as strong proof. if (currentDataDirectoryRva === 0 || currentDataDirectorySize === 0) continue; // Security is not mapped by the image loader. Its offset can be stale in // generated uninstallers without changing the loaded image or executable data. if (i === IMAGE_DIRECTORY_ENTRY_SECURITY) continue; var currentDataDirectoryEndRva = currentDataDirectoryRva + currentDataDirectorySize - 1; // VirtualSize beyond SizeOfRawData is mapped as zero-filled memory. Accept // such ranges here; dedicated directory parsers separately report content // that must be present in the file but cannot be read. var isCurrentDataDirectoryMapped = isVirtualAddressRangeMapped(PE_Cached.imageBase + currentDataDirectoryRva, currentDataDirectorySize, 0); if (!isCurrentDataDirectoryMapped && currentDataDirectoryRva < sectionLayoutSizeOfImage && currentDataDirectoryEndRva < sectionLayoutSizeOfImage) { // Raw translations omit valid zero-filled page tails and cannot prove an intervening page is committed. isCurrentDataDirectoryMapped = isBadFormatRangeCovered(currentDataDirectoryRva, currentDataDirectoryEndRva); } if (currentDataDirectoryRva >= sectionLayoutSizeOfImage || currentDataDirectoryEndRva >= sectionLayoutSizeOfImage || !isCurrentDataDirectoryMapped) { isUnmappedDataDirectoryPresent = true; } if (isUnmappedDataDirectoryPresent) { unmappedDataDirectoryName = dataDirectoryNames[i]; log(logType.any, unmappedDataDirectoryName + " data directory extends outside the mapped image"); } } } // Clean up: release the data-directory name collection dataDirectoryNames = undefined; if (isUnmappedDataDirectoryPresent) addCorruptedDataOption(corruptedDataLevel.degraded, "Unmapped data directory (" + unmappedDataDirectoryName + ")"); // Delay descriptors may store RVAs or legacy VAs. Validate explicit pointers // using the declared mode; an unmapped name/table cannot be consumed by delayimp. var isInvalidDelayImportDescriptorPresent = false; if (declaredDataDirectoryCount > IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT && dataDirectoryTableOffset + (IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT + 1) * 8 <= dataDirectoryOptionalHeaderEnd) { var delayImportDirectoryEntryOffset = dataDirectoryTableOffset + IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT * 8, delayImportDirectoryRva = PE.read_uint32(delayImportDirectoryEntryOffset), delayImportDirectorySize = PE.read_uint32(delayImportDirectoryEntryOffset + 4); if (delayImportDirectoryRva !== 0 && delayImportDirectorySize !== 0) { var delayImportDirectoryAddress = PE_Cached.imageBase + delayImportDirectoryRva, delayImportDescriptorCount = Math.min(Math.floor(delayImportDirectorySize / 0x20), 128), delayImportDirectoryData = delayImportDescriptorCount > 0 ? readFileBackedVirtualBytes(delayImportDirectoryAddress, delayImportDescriptorCount * 0x20) : undefined; // Descriptors follow loaded RVAs, not the physical adjacency of section data. // Keep the 128-record read budget while validating the complete declared image range. if (delayImportDescriptorCount === 0 || (!isVirtualAddressRangeMapped(delayImportDirectoryAddress, delayImportDirectorySize, 0) && !isBadFormatRangeCovered(delayImportDirectoryRva, delayImportDirectoryRva + delayImportDirectorySize - 1))) { isInvalidDelayImportDescriptorPresent = true; } else { for (var i = 0; i < delayImportDescriptorCount && !isInvalidDelayImportDescriptorPresent; i++) { var delayImportDescriptorOffset = i * 0x20, delayImportDescriptorFields = [], isEmptyDelayImportDescriptor = true; for (var d = 0; d < 8; d++) { var delayImportDescriptorField = delayImportDirectoryData ? readUnsignedDwordFromBuffer(delayImportDirectoryData, delayImportDescriptorOffset + d * 4) : readMappedUnsignedInteger(delayImportDirectoryAddress + delayImportDescriptorOffset + d * 4, 4); // A zero-filled descriptor terminates the list; unreadable raw bytes do not become zeros. if (delayImportDescriptorField === -1) { isInvalidDelayImportDescriptorPresent = true; break; } delayImportDescriptorFields.push(delayImportDescriptorField); if (delayImportDescriptorField !== 0) isEmptyDelayImportDescriptor = false; } if (isInvalidDelayImportDescriptorPresent || isEmptyDelayImportDescriptor) break; var isDelayImportRvaBased = (delayImportDescriptorFields[0] & 1) !== 0; // A nonempty descriptor needs a DLL name and both thunk tables. if (delayImportDescriptorFields[1] === 0 || delayImportDescriptorFields[3] === 0 || delayImportDescriptorFields[4] === 0) { isInvalidDelayImportDescriptorPresent = true; break; } // Name, IAT, INT and optional bound/unload tables require file bytes. // The module-handle slot may be virtual-only writable storage. for (var d = 1; d <= 6 && !isInvalidDelayImportDescriptorPresent; d++) { var delayImportPointer = delayImportDescriptorFields[d]; if (delayImportPointer === 0) continue; var delayImportPointerAddress = delayImportPointer + (isDelayImportRvaBased ? PE_Cached.imageBase : 0); if (d === 2) { // delayimp stores an entire HMODULE here, not merely its first byte. var delayImportModuleHandleSize = PE_Cached.is64bit ? 8 : 4; // Writable storage may span adjacent sections, including their zero-filled tails. isInvalidDelayImportDescriptorPresent = !isVirtualAddressRangeMapped(delayImportPointerAddress, delayImportModuleHandleSize, 0) && !readFileBackedVirtualBytes(delayImportPointerAddress, delayImportModuleHandleSize) && !isBadFormatRangeCovered(delayImportPointerAddress - PE_Cached.imageBase, delayImportPointerAddress - PE_Cached.imageBase + delayImportModuleHandleSize - 1); } else { var delayImportPointerOffset = PE.VAToOffset(delayImportPointerAddress), delayImportPointerSize = d === 1 ? 1 : (PE_Cached.is64bit ? 8 : 4); // A complete thunk may cross sections whose raw bytes are not adjacent. isInvalidDelayImportDescriptorPresent = (delayImportPointerOffset < 0 || delayImportPointerOffset + delayImportPointerSize > PE_Cached.fileSize || PE.VAToOffset(delayImportPointerAddress + delayImportPointerSize - 1) !== delayImportPointerOffset + delayImportPointerSize - 1) && !readFileBackedVirtualBytes(delayImportPointerAddress, delayImportPointerSize); if (d === 1 && !isInvalidDelayImportDescriptorPresent && PE.readByte(delayImportPointerOffset) === 0) isInvalidDelayImportDescriptorPresent = true; } } } } delayImportDirectoryData = undefined; if (isInvalidDelayImportDescriptorPresent) log(logType.any, "Delay-import descriptor is incomplete, truncated, or references unmapped data"); } } if (isInvalidDelayImportDescriptorPresent) addCorruptedDataOption(corruptedDataLevel.degraded, "Invalid delay imports"); // Reserve values describe the complete address-space reservation; committing more // than that range is impossible regardless of linker defaults or loader policy. var isCommitAboveReservePresent = false; var commitAboveReserveDetails = String(); const optionalHeaderCommitFieldsEnd = dataDirectoryOptionalHeaderOffset + (PE_Cached.is64bit ? 0x68 : 0x58); if (optionalHeaderCommitFieldsEnd <= dataDirectoryOptionalHeaderEnd) { var stackReserve, stackCommit, heapReserve, heapCommit; if (PE_Cached.is64bit) { stackReserve = readUnsignedQword(dataDirectoryOptionalHeaderOffset + 0x48); stackCommit = readUnsignedQword(dataDirectoryOptionalHeaderOffset + 0x50); heapReserve = readUnsignedQword(dataDirectoryOptionalHeaderOffset + 0x58); heapCommit = readUnsignedQword(dataDirectoryOptionalHeaderOffset + 0x60); } else { stackReserve = PE.read_uint32(dataDirectoryOptionalHeaderOffset + 0x48); stackCommit = PE.read_uint32(dataDirectoryOptionalHeaderOffset + 0x4C); heapReserve = PE.read_uint32(dataDirectoryOptionalHeaderOffset + 0x50); heapCommit = PE.read_uint32(dataDirectoryOptionalHeaderOffset + 0x54); } if (stackReserve !== -1 && stackCommit !== -1 && stackCommit > stackReserve) { commitAboveReserveDetails = "Stack"; log(logType.any, "Stack commit size exceeds its reserve size"); isCommitAboveReservePresent = true; } if (heapReserve !== -1 && heapCommit !== -1 && heapCommit > heapReserve) { commitAboveReserveDetails += (commitAboveReserveDetails.length !== 0 ? "/" : String()) + "Heap"; log(logType.any, "Heap commit size exceeds its reserve size"); isCommitAboveReservePresent = true; } } if (isCommitAboveReservePresent) addCorruptedDataOption(corruptedDataLevel.degraded, commitAboveReserveDetails + " commit exceeds reserve"); // Reserved optional-header fields and directory slots have no loader-defined payload. // A non-zero value therefore cannot be explained by an uncommon but valid mapping. var isReservedPeDataPresent = false; if (dataDirectoryOptionalHeaderOffset + 0x38 <= dataDirectoryOptionalHeaderEnd && PE.read_uint32(dataDirectoryOptionalHeaderOffset + 0x34) !== 0) { log(logType.any, "Reserved Win32VersionValue field is populated"); isReservedPeDataPresent = true; } const loaderFlagsOffset = dataDirectoryOptionalHeaderOffset + (PE_Cached.is64bit ? 0x68 : 0x58); // Windows metadata images legitimately use LoaderFlags for managed metadata policy. if (!isReservedPeDataPresent && !PE_Cached.isDotNet && loaderFlagsOffset + 4 <= dataDirectoryOptionalHeaderEnd && PE.read_uint32(loaderFlagsOffset) !== 0) { log(logType.any, "Reserved LoaderFlags field is populated"); isReservedPeDataPresent = true; } if (!isReservedPeDataPresent && declaredDataDirectoryCount > IMAGE_DIRECTORY_ENTRY_ARCHITECTURE && dataDirectoryTableOffset + (IMAGE_DIRECTORY_ENTRY_ARCHITECTURE + 1) * 8 <= dataDirectoryOptionalHeaderEnd) { const architectureDirectoryOffset = dataDirectoryTableOffset + IMAGE_DIRECTORY_ENTRY_ARCHITECTURE * 8; if (PE.read_uint32(architectureDirectoryOffset) !== 0 || PE.read_uint32(architectureDirectoryOffset + 4) !== 0) { log(logType.any, "Reserved Architecture data directory is populated"); isReservedPeDataPresent = true; } } if (!isReservedPeDataPresent && declaredDataDirectoryCount > IMAGE_DIRECTORY_ENTRY_GLOBALPTR && dataDirectoryTableOffset + (IMAGE_DIRECTORY_ENTRY_GLOBALPTR + 1) * 8 <= dataDirectoryOptionalHeaderEnd) { const globalPointerDirectoryOffset = dataDirectoryTableOffset + IMAGE_DIRECTORY_ENTRY_GLOBALPTR * 8; // GlobalPtr may contain an RVA, but its Size field is explicitly reserved. if (PE.read_uint32(globalPointerDirectoryOffset + 4) !== 0) { log(logType.any, "GlobalPtr data directory has a non-zero reserved size"); isReservedPeDataPresent = true; } } if (isReservedPeDataPresent) addCorruptedDataOption(corruptedDataLevel.degraded, "Reserved PE data populated"); // A present relocation directory contradicts the COFF declaration that relocation // information was stripped. This remains invalid even when ASLR itself is disabled. var isRelocationStateMismatchPresent = false; if (declaredDataDirectoryCount > IMAGE_DIRECTORY_ENTRY_BASERELOC && dataDirectoryTableOffset + (IMAGE_DIRECTORY_ENTRY_BASERELOC + 1) * 8 <= dataDirectoryOptionalHeaderEnd) { const baseRelocationDirectoryOffset = dataDirectoryTableOffset + IMAGE_DIRECTORY_ENTRY_BASERELOC * 8, baseRelocationDirectoryRva = PE.read_uint32(baseRelocationDirectoryOffset), baseRelocationDirectorySize = PE.read_uint32(baseRelocationDirectoryOffset + 4), badFormatCoffCharacteristics = PE.readWord(dataDirectoryOptionalHeaderOffset - 2); if ((badFormatCoffCharacteristics & IMAGE_FILE_RELOCS_STRIPPED) && baseRelocationDirectoryRva !== 0 && baseRelocationDirectorySize !== 0) { log(logType.any, "Relocations are marked stripped while a relocation directory is present"); isRelocationStateMismatchPresent = true; } } if (isRelocationStateMismatchPresent) addCorruptedDataOption(corruptedDataLevel.degraded, "Relocation state mismatch"); // Validate AMD64 exception-table layout, code ranges, and unwind references. // Leaf functions may legitimately have no entry, so only present metadata is inspected // and only impossible ranges or record relationships are reported. var isInvalidUnwindMetadataPresent = false; if (PE_Cached.isAMD64 && declaredDataDirectoryCount > IMAGE_DIRECTORY_ENTRY_EXCEPTION && dataDirectoryTableOffset + (IMAGE_DIRECTORY_ENTRY_EXCEPTION + 1) * 8 <= dataDirectoryOptionalHeaderEnd) { // A physically present but undeclared slot is not an exception directory. // NumberOfRvaAndSizes bounds the table: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-data-directories-image-only const exceptionDirectoryEntryOffset = dataDirectoryTableOffset + IMAGE_DIRECTORY_ENTRY_EXCEPTION * 8, exceptionDirectoryRva = PE.read_uint32(exceptionDirectoryEntryOffset), exceptionDirectorySize = PE.read_uint32(exceptionDirectoryEntryOffset + 4), exceptionDirectoryAddress = PE_Cached.imageBase + exceptionDirectoryRva; // The loader consumes records across adjacent sections even when their raw data is separate. var exceptionDirectoryData = exceptionDirectoryRva !== 0 && exceptionDirectorySize !== 0 ? readFileBackedVirtualBytes(exceptionDirectoryAddress, exceptionDirectorySize) : undefined; if (exceptionDirectoryData) { // AMD64 uses fixed 12-byte RUNTIME_FUNCTION records. A non-zero remainder // cannot describe even a future unwind format without changing the directory. // The native implementation preserves these checks and reports the first // failing record. Engines without it, or unavailable reads, use the same // script path below. Status: 0 valid, 1 partial entry, 2 body, 3 metadata. var nativeUnwindResult = typeof PE.validateAmd64Unwind === "function" ? PE.validateAmd64Unwind(exceptionDirectoryRva, exceptionDirectorySize) : null; if (nativeUnwindResult !== null) { if (nativeUnwindResult.status === 1) { log(logType.any, "AMD64 unwind table ends with an incomplete function entry"); } else if (nativeUnwindResult.status === 2) { log(logType.any, "AMD64 unwind function entry #" + nativeUnwindResult.entryIndex + " references an unmapped range from VA 0x" + (PE_Cached.imageBase + nativeUnwindResult.beginRva).toString(16) + " to VA 0x" + (PE_Cached.imageBase + nativeUnwindResult.endRva).toString(16)); } else if (nativeUnwindResult.status === 3) { log(logType.any, "AMD64 unwind function entry #" + nativeUnwindResult.entryIndex + " references invalid metadata at VA 0x" + (PE_Cached.imageBase + nativeUnwindResult.unwindRva).toString(16)); } isInvalidUnwindMetadataPresent = nativeUnwindResult.status !== 0; } else if (exceptionDirectorySize < 12 || exceptionDirectorySize % 12 !== 0) { log(logType.any, "AMD64 unwind table ends with an incomplete function entry"); isInvalidUnwindMetadataPresent = true; } else { // Nonzero unwind RVAs can address loaded headers; zero still denotes absent metadata. var runtimeFunctionMappedRanges = [{ Rva: 1, EndRva: Math.min(PE.getImageOptionalHeader("SizeOfHeaders"), PE_Cached.fileSize, sectionLayoutSizeOfImage) }]; // A prefix-maximum interval index answers whether one section contains a // complete range in O(log n). Unlike a monotonic cursor, it remains exact // for unsorted exception records and overlapping protector-made sections. function prepareRuntimeFunctionRangeIndex(ranges) { ranges.sort(function (leftRange, rightRange) { return leftRange.Rva - rightRange.Rva; }); var maximumEndRva = 0, maximumEndRangeIndex = -1; for (var r = 0; r < ranges.length; r++) { if (ranges[r].EndRva > maximumEndRva) { maximumEndRva = ranges[r].EndRva; maximumEndRangeIndex = r; } ranges[r].MaximumEndRva = maximumEndRva; ranges[r].MaximumEndRangeIndex = maximumEndRangeIndex; } } function findRuntimeFunctionRangeIndex(ranges, beginRva, endRva) { var lowerIndex = 0, upperIndex = ranges.length; // Find the last interval beginning at or before the requested range. while (lowerIndex < upperIndex) { var middleIndex = lowerIndex + Math.floor((upperIndex - lowerIndex) / 2); if (ranges[middleIndex].Rva <= beginRva) { lowerIndex = middleIndex + 1; } else { upperIndex = middleIndex; } } var matchingRangeIndex = lowerIndex - 1; return matchingRangeIndex >= 0 && ranges[matchingRangeIndex].MaximumEndRva >= endRva ? ranges[matchingRangeIndex].MaximumEndRangeIndex : -1; } /** * Validates the complete shape of a WORD-aligned AMD64 UNWIND_INFO record. * * The Microsoft x64 ABI specifies a DWORD-aligned UNWIND_INFO structure. * Current Microsoft-produced images also contain WORD-aligned records which * the Windows unwinder accepts. Such a compatibility record is admitted only * after its header, unwind-code slots and optional trailer are completely readable * from proven loaded bytes and structurally valid; alignment alone never bypasses validation. * * @param {number} unwindRva - RVA of the candidate UNWIND_INFO record. * @returns {boolean} true for a complete supported record. * * @see {@link https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64?view=msvc-170} Microsoft x64 RUNTIME_FUNCTION, UNWIND_INFO and UNWIND_CODE specification */ function isValidWordAlignedUnwindInfo(unwindRva) { var unwindAddress = PE_Cached.imageBase + unwindRva, unwindHeader = readFileBackedVirtualBytes(unwindAddress, 4); if (!unwindHeader) { var unwindHeaderValue = readMappedUnsignedInteger(unwindAddress, 4); if (unwindHeaderValue === -1) return false; unwindHeader = [unwindHeaderValue & 0xFF, (unwindHeaderValue >>> 8) & 0xFF, (unwindHeaderValue >>> 16) & 0xFF, unwindHeaderValue >>> 24]; } var versionAndFlags = unwindHeader[0], version = versionAndFlags & 0x07, flags = versionAndFlags >>> 3, prologSize = unwindHeader[1], unwindCodeCount = unwindHeader[2], frameRegister = unwindHeader[3] & 0x0F, frameOffset = unwindHeader[3] >>> 4; // The published x64 format defines version 1. Only the three published // UNW_FLAG bits are valid, and CHAININFO is mutually exclusive with // language-specific handler flags. if (version !== 1 || flags > 0x07 || ((flags & 0x04) !== 0 && (flags & 0x03) !== 0) || (frameRegister === 0 && frameOffset !== 0)) return false; var alignedUnwindCodeCount = unwindCodeCount + (unwindCodeCount % 2), unwindInfoSize = 4 + alignedUnwindCodeCount * 2; if ((flags & 0x04) !== 0) { unwindInfoSize += 12; // Chained RUNTIME_FUNCTION } else if ((flags & 0x03) !== 0) { unwindInfoSize += 4; // Exception-handler RVA } var unwindInfo = unwindInfoSize === 4 ? unwindHeader : readFileBackedVirtualBytes(unwindAddress, unwindInfoSize); // A complete loaded record may include virtual zeros; raw decoys outside the section are irrelevant. if (!unwindInfo) { unwindInfo = []; for (var unwindDataOffset = 0; unwindDataOffset < unwindInfoSize; unwindDataOffset += 4) { var unwindDataValue = readMappedUnsignedInteger(unwindAddress + unwindDataOffset, 4); if (unwindDataValue === -1) return false; unwindInfo.push(unwindDataValue & 0xFF, (unwindDataValue >>> 8) & 0xFF, (unwindDataValue >>> 16) & 0xFF, unwindDataValue >>> 24); } } var unwindCodeIndex = 0; while (unwindCodeIndex < unwindCodeCount) { var unwindCodeOffset = 4 + unwindCodeIndex * 2, codeOffset = unwindInfo[unwindCodeOffset], operationAndInfo = unwindInfo[unwindCodeOffset + 1], unwindOperation = operationAndInfo & 0x0F, operationInfo = operationAndInfo >>> 4, occupiedSlots = 1; if (codeOffset > prologSize) return false; if (unwindOperation === 1) { if (operationInfo === 0) occupiedSlots = 2; else if (operationInfo === 1) occupiedSlots = 3; else return false; } else if (unwindOperation === 3) { if (operationInfo !== 0) return false; } else if (unwindOperation === 4 || unwindOperation === 8) { occupiedSlots = 2; } else if (unwindOperation === 5 || unwindOperation === 9) { occupiedSlots = 3; } else if (unwindOperation === 6 || unwindOperation === 7 || unwindOperation > 10 || (unwindOperation === 10 && operationInfo > 1)) { return false; } if (unwindCodeIndex + occupiedSlots > unwindCodeCount) return false; unwindCodeIndex += occupiedSlots; } return true; } // Snapshot and index the section table once; exception directories may // contain hundreds of thousands of fixed-size records. for (var d = 0; d < PE_Cached.numberOfSections; d++) { var runtimeFunctionSectionRva = PE.getSectionVirtualAddress(d), runtimeFunctionSectionEndRva = runtimeFunctionSectionRva + Math.max(PE.getSectionVirtualSize(d), PE.getSectionFileSize(d)); runtimeFunctionMappedRanges.push({ Rva: runtimeFunctionSectionRva, EndRva: runtimeFunctionSectionEndRva }); } prepareRuntimeFunctionRangeIndex(runtimeFunctionMappedRanges); var cachedRuntimeFunctionMappedRangeIndex = -1, cachedRuntimeFunctionBodyRangeIndex = -1; // Large native images can contain hundreds of thousands of fixed-size entries. // Decode this bounded hot path inline while preserving unsigned RVA values. for (var i = 0; i < exceptionDirectoryData.length && !isInvalidUnwindMetadataPresent; i += 12) { var runtimeFunctionIndex = i / 12, runtimeFunctionBeginRva = (exceptionDirectoryData[i] | (exceptionDirectoryData[i + 1] << 8) | (exceptionDirectoryData[i + 2] << 16) | (exceptionDirectoryData[i + 3] << 24)) >>> 0, runtimeFunctionEndRva = (exceptionDirectoryData[i + 4] | (exceptionDirectoryData[i + 5] << 8) | (exceptionDirectoryData[i + 6] << 16) | (exceptionDirectoryData[i + 7] << 24)) >>> 0, runtimeFunctionUnwindRva = (exceptionDirectoryData[i + 8] | (exceptionDirectoryData[i + 9] << 8) | (exceptionDirectoryData[i + 10] << 16) | (exceptionDirectoryData[i + 11] << 24)) >>> 0, runtimeFunctionSize = runtimeFunctionEndRva - runtimeFunctionBeginRva, isRuntimeFunctionBodyRangeValid = false, isRuntimeFunctionUnwindPointerValid = runtimeFunctionBeginRva === 0 && runtimeFunctionEndRva === 0 && runtimeFunctionUnwindRva === 0, isRuntimeFunctionIndirect = runtimeFunctionUnwindRva % 2 !== 0; if (runtimeFunctionSize > 0) { var cachedRuntimeFunctionBodyRange = runtimeFunctionMappedRanges[cachedRuntimeFunctionBodyRangeIndex]; // Section flags cannot prove whether a mapped range contains code. // Adjacent records usually reference the same section. if (cachedRuntimeFunctionBodyRange && runtimeFunctionBeginRva >= cachedRuntimeFunctionBodyRange.Rva && runtimeFunctionEndRva <= cachedRuntimeFunctionBodyRange.EndRva) { isRuntimeFunctionBodyRangeValid = true; } else { var runtimeFunctionBodyRangeIndex = findRuntimeFunctionRangeIndex(runtimeFunctionMappedRanges, runtimeFunctionBeginRva, runtimeFunctionEndRva); if (runtimeFunctionBodyRangeIndex !== -1) { cachedRuntimeFunctionBodyRangeIndex = runtimeFunctionBodyRangeIndex; isRuntimeFunctionBodyRangeValid = true; } } } // RUNTIME_FUNCTION_INDIRECT stores another .pdata entry RVA with bit 0 // set instead of an UNWIND_INFO RVA. Validate that tagged reference // against the complete exception table before checking normal pointers. if (isRuntimeFunctionIndirect) { var indirectRuntimeFunctionRva = runtimeFunctionUnwindRva - 1, indirectRuntimeFunctionOffset = indirectRuntimeFunctionRva - exceptionDirectoryRva; // The untagged RVA must select the beginning of another complete // record in this exact table; merely mapping somewhere is insufficient. isRuntimeFunctionUnwindPointerValid = indirectRuntimeFunctionOffset >= 0 && indirectRuntimeFunctionOffset + 12 <= exceptionDirectorySize && indirectRuntimeFunctionOffset % 12 === 0; } // The documented ABI requires DWORD alignment. A small number of current // Microsoft images use WORD-aligned records; those take the strict // structural compatibility path above instead of being accepted merely // because their first bytes fall inside a mapped section. if (!isRuntimeFunctionIndirect && runtimeFunctionUnwindRva % 2 === 0) { var cachedRuntimeFunctionMappedRange = runtimeFunctionMappedRanges[cachedRuntimeFunctionMappedRangeIndex]; if (cachedRuntimeFunctionMappedRange && runtimeFunctionUnwindRva >= cachedRuntimeFunctionMappedRange.Rva && runtimeFunctionUnwindRva + 4 <= cachedRuntimeFunctionMappedRange.EndRva) { isRuntimeFunctionUnwindPointerValid = runtimeFunctionUnwindRva % 4 === 0 || isValidWordAlignedUnwindInfo(runtimeFunctionUnwindRva); } else { var runtimeFunctionMappedRangeIndex = findRuntimeFunctionRangeIndex(runtimeFunctionMappedRanges, runtimeFunctionUnwindRva, runtimeFunctionUnwindRva + 4); if (runtimeFunctionMappedRangeIndex !== -1) { cachedRuntimeFunctionMappedRangeIndex = runtimeFunctionMappedRangeIndex; isRuntimeFunctionUnwindPointerValid = runtimeFunctionUnwindRva % 4 === 0 || isValidWordAlignedUnwindInfo(runtimeFunctionUnwindRva); } else if (runtimeFunctionUnwindRva !== 0) { // Split DWORD headers retain format compatibility; only WORD alignment needs the strict layout check. isRuntimeFunctionUnwindPointerValid = runtimeFunctionUnwindRva % 4 === 0 ? readMappedUnsignedInteger(PE_Cached.imageBase + runtimeFunctionUnwindRva, 4) !== -1 : isValidWordAlignedUnwindInfo(runtimeFunctionUnwindRva); } } } // Current Microsoft images may retain zero-length or reversed entries. // Neither can cover a control PC, so range ordering alone is not evidence // of corruption; positive ranges and every metadata pointer remain strict. if (runtimeFunctionSize > 0 && !isRuntimeFunctionBodyRangeValid) { log(logType.any, "AMD64 unwind function entry #" + runtimeFunctionIndex + " references an unmapped range from VA 0x" + (PE_Cached.imageBase + runtimeFunctionBeginRva).toString(16) + " to VA 0x" + (PE_Cached.imageBase + runtimeFunctionEndRva).toString(16)); isInvalidUnwindMetadataPresent = true; } else if (!isRuntimeFunctionUnwindPointerValid) { log(logType.any, "AMD64 unwind function entry #" + runtimeFunctionIndex + " references invalid metadata at VA 0x" + (PE_Cached.imageBase + runtimeFunctionUnwindRva).toString(16)); isInvalidUnwindMetadataPresent = true; } } // Clean up: release the potentially large exception-directory buffer exceptionDirectoryData = undefined; runtimeFunctionMappedRanges = undefined; } } } if (isInvalidUnwindMetadataPresent) addCorruptedDataOption(corruptedDataLevel.degraded, "Invalid unwind metadata"); // IMAGE_DEBUG_DIRECTORY is a fixed 28-byte record. Validate only relationships that // are invariant across debug formats; the payload itself remains producer-specific. var isMalformedDebugDirectoryPresent = false; if (declaredDataDirectoryCount > IMAGE_DIRECTORY_ENTRY_DEBUG && dataDirectoryTableOffset + (IMAGE_DIRECTORY_ENTRY_DEBUG + 1) * 8 <= dataDirectoryOptionalHeaderEnd) { const debugDirectoryEntryOffset = dataDirectoryTableOffset + IMAGE_DIRECTORY_ENTRY_DEBUG * 8, debugDirectoryRva = PE.read_uint32(debugDirectoryEntryOffset), debugDirectorySize = PE.read_uint32(debugDirectoryEntryOffset + 4), debugDirectoryAddress = PE_Cached.imageBase + debugDirectoryRva; // Validate the same loaded-order bytes used by the debug-data readers, including split records. var debugDirectoryData = debugDirectoryRva !== 0 && debugDirectorySize !== 0 ? readFileBackedVirtualBytes(debugDirectoryAddress, debugDirectorySize) : undefined; if (debugDirectoryData) { if (debugDirectorySize % 28 !== 0) { log(logType.any, "Debug directory ends with an incomplete entry"); isMalformedDebugDirectoryPresent = true; } else { for (var i = 0; i < debugDirectoryData.length && !isMalformedDebugDirectoryPresent; i += 28) { var debugEntryIndex = i / 28, debugEntryCharacteristics = readUnsignedDwordFromBuffer(debugDirectoryData, i), debugEntryDataSize = readUnsignedDwordFromBuffer(debugDirectoryData, i + 16), debugEntryDataRva = readUnsignedDwordFromBuffer(debugDirectoryData, i + 20), debugEntryDataOffset = readUnsignedDwordFromBuffer(debugDirectoryData, i + 24); if (debugEntryCharacteristics !== 0) { log(logType.any, "Debug directory entry #" + debugEntryIndex + " has reserved characteristics set"); isMalformedDebugDirectoryPresent = true; } else if (debugEntryDataSize !== 0 && debugEntryDataOffset !== 0 && (debugEntryDataOffset >= PE_Cached.fileSize || debugEntryDataOffset + debugEntryDataSize > PE_Cached.fileSize) && (debugEntryDataRva === 0 || !isVirtualAddressRangeFileBacked( PE_Cached.imageBase + debugEntryDataRva, debugEntryDataSize, 0))) { log(logType.any, "Debug directory entry #" + debugEntryIndex + " references data outside the file"); isMalformedDebugDirectoryPresent = true; } } // Clean up: release the debug-directory buffer debugDirectoryData = undefined; } } } if (isMalformedDebugDirectoryPresent) addCorruptedDataOption(corruptedDataLevel.uncritical, "Malformed debug directory"); // The certificate table must remain outside every mapped image region. Unlike a strict // end-of-file requirement, overlap validation still accepts legitimate trailing overlays. var isCertificateTableOverlapPresent = false; if (declaredDataDirectoryCount > IMAGE_DIRECTORY_ENTRY_SECURITY && dataDirectoryTableOffset + (IMAGE_DIRECTORY_ENTRY_SECURITY + 1) * 8 <= dataDirectoryOptionalHeaderEnd) { const certificateOverlapDirectoryOffset = dataDirectoryTableOffset + IMAGE_DIRECTORY_ENTRY_SECURITY * 8, certificateOverlapOffset = PE.read_uint32(certificateOverlapDirectoryOffset), certificateOverlapSize = PE.read_uint32(certificateOverlapDirectoryOffset + 4), certificateOverlapEnd = certificateOverlapOffset + certificateOverlapSize; if (certificateOverlapOffset !== 0 && certificateOverlapSize !== 0 && certificateOverlapEnd <= PE_Cached.fileSize) { if (certificateOverlapOffset < sectionLayoutHeadersEnd) { log(logType.any, "Certificate table overlaps PE headers"); isCertificateTableOverlapPresent = true; } for (var i = 0; i < PE_Cached.numberOfSections && !isCertificateTableOverlapPresent; i++) { var certificateOverlapSectionOffset = PE.getSectionFileOffset(i), certificateOverlapSectionSize = PE.getSectionFileSize(i), certificateOverlapSectionEnd = certificateOverlapSectionOffset + certificateOverlapSectionSize; if (certificateOverlapSectionOffset !== 0 && certificateOverlapSectionSize !== 0 && certificateOverlapOffset < certificateOverlapSectionEnd && certificateOverlapSectionOffset < certificateOverlapEnd) { log(logType.any, "Certificate table overlaps section #" + i + " raw data"); isCertificateTableOverlapPresent = true; } } } } if (isCertificateTableOverlapPresent) addCorruptedDataOption(corruptedDataLevel.uncritical, "Certificate table overlaps image data"); // Walk every WIN_CERTIFICATE entry according to the PE quadword-alignment rules. // Certificate revisions and content types are intentionally left open for compatibility; // only a chain which cannot fit the declared Security directory is considered malformed. var isMalformedCertificateTablePresent = false; if (declaredDataDirectoryCount > IMAGE_DIRECTORY_ENTRY_SECURITY && dataDirectoryTableOffset + (IMAGE_DIRECTORY_ENTRY_SECURITY + 1) * 8 <= dataDirectoryOptionalHeaderEnd) { // Unlike the other data directories, Security stores a raw file offset rather than // an RVA. Passing it through VAToOffset would corrupt validation of signed overlays. const certificateDirectoryEntryOffset = dataDirectoryTableOffset + IMAGE_DIRECTORY_ENTRY_SECURITY * 8, certificateTableOffset = PE.read_uint32(certificateDirectoryEntryOffset), certificateTableSize = PE.read_uint32(certificateDirectoryEntryOffset + 4); if (certificateTableOffset !== 0 && certificateTableSize !== 0 && certificateTableOffset + certificateTableSize <= PE_Cached.fileSize) { // Both the table start and aggregate size follow the same quadword boundary // consumed by ImageEnumerateCertificates and Authenticode tooling. if (certificateTableOffset % 8 !== 0 || certificateTableSize % 8 !== 0) { log(logType.any, "Certificate table range violates the required 8-byte alignment"); isMalformedCertificateTablePresent = true; } else { /** * Recognizes one complete DER OCTET STRING with a bounded alignment trailer. * * PE/COFF normally requires the Security directory to be consumed entirely * by quadword-aligned WIN_CERTIFICATE entries. Windows signature tooling also * accepts signed vendor images with an auxiliary DER object after at least one * valid entry. This compatibility form is accepted only when the DER identifier, * canonical definite length and alignment tail account for every remaining byte. * Besides zero padding, a four-byte vendor trailer is recognized only when it is * an exact absolute file-offset self-reference to the DER contents. * * @param {number} tailOffset - File offset of the candidate DER object. * @param {number} tailSize - Complete remaining Security-directory size. * @returns {boolean} true when one DER OCTET STRING consumes the tail. * * @see {@link https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-attribute-certificate-table-image-only} Microsoft PE/COFF attribute certificate table * @see {@link https://www.itu.int/rec/T-REC-X.690} ITU-T X.690 / ISO/IEC 8825-1 ASN.1 DER */ function isValidDerOctetStringCertificateTail(tailOffset, tailSize) { if (tailSize < 2 || PE.readByte(tailOffset) !== 0x04) return false; var firstLengthByte = PE.readByte(tailOffset + 1), lengthOctetCount = 0, contentLength = 0, headerSize = 2; if (firstLengthByte < 0x80) { contentLength = firstLengthByte; } else { lengthOctetCount = firstLengthByte & 0x7F; // DER forbids indefinite length and requires the shortest possible // definite form. Four length octets cover the complete PE32 range. if (lengthOctetCount === 0 || lengthOctetCount > 4 || headerSize + lengthOctetCount > tailSize || PE.readByte(tailOffset + headerSize) === 0) return false; for (var lengthIndex = 0; lengthIndex < lengthOctetCount; lengthIndex++) { contentLength = contentLength * 0x100 + PE.readByte(tailOffset + headerSize + lengthIndex); } if (contentLength < 0x80) return false; headerSize += lengthOctetCount; } var encodedSize = headerSize + contentLength, alignedEncodedSize = Math.ceil(encodedSize / 8) * 8; if (encodedSize < headerSize || alignedEncodedSize !== tailSize) return false; var paddingSize = tailSize - encodedSize, isZeroPadding = true; for (var paddingOffset = encodedSize; paddingOffset < tailSize; paddingOffset++) { if (PE.readByte(tailOffset + paddingOffset) !== 0) { isZeroPadding = false; break; } } if (isZeroPadding) return true; // A known Authenticode-compatible producer form terminates the aligned // object with a DWORD pointing back to its first contents octet. Requiring // the exact self-reference keeps arbitrary non-DER tails malformed. return paddingSize === 4 && PE.read_uint32(tailOffset + encodedSize) === tailOffset + headerSize; } var certificateRecordOffset = 0, certificateRecordIndex = 0, certificateTableDataOffset = 0, certificateTableData; const maxStreamedCertificateRecords = 64; while (certificateRecordOffset < certificateTableSize && !isMalformedCertificateTablePresent) { // dwLength, wRevision and wCertificateType form the mandatory // eight-byte header; checking the remainder prevents partial reads. if (certificateTableSize - certificateRecordOffset < 8) { log(logType.any, "Certificate entry #" + certificateRecordIndex + " has a truncated WIN_CERTIFICATE header"); isMalformedCertificateTablePresent = true; break; } // Ordinary Authenticode contains only a few records, so payload bytes do // not need to enter JS. A dense adversarial table crosses over to one // bounded buffer instead of causing unbounded engine calls. if (certificateRecordIndex === maxStreamedCertificateRecords) { certificateTableDataOffset = certificateRecordOffset; certificateTableData = PE.readBytes(certificateTableOffset + certificateRecordOffset, certificateTableSize - certificateRecordOffset); } var certificateRecordLength = certificateTableData ? readUnsignedDwordFromBuffer(certificateTableData, certificateRecordOffset - certificateTableDataOffset) : PE.read_uint32(certificateTableOffset + certificateRecordOffset), certificateNextRecordOffset = Math.ceil((certificateRecordOffset + certificateRecordLength) / 8) * 8; // dwLength excludes alignment padding. Round the next record up without // interpreting the certificate payload, revision or content type. if (certificateRecordLength < 8) { log(logType.any, "Certificate entry #" + certificateRecordIndex + " is shorter than its WIN_CERTIFICATE header"); isMalformedCertificateTablePresent = true; } else if (certificateRecordOffset + certificateRecordLength > certificateTableSize || certificateNextRecordOffset > certificateTableSize) { // Some valid vendor signatures append one bounded DER object after // their WIN_CERTIFICATE chain. Never apply this compatibility rule to // an empty/invalid chain or to an object which leaves unexplained data. if (certificateRecordIndex > 0 && isValidDerOctetStringCertificateTail(certificateTableOffset + certificateRecordOffset, certificateTableSize - certificateRecordOffset)) { certificateRecordOffset = certificateTableSize; } else { log(logType.any, "Certificate entry #" + certificateRecordIndex + " exceeds the declared certificate table"); isMalformedCertificateTablePresent = true; } } else { certificateRecordOffset = certificateNextRecordOffset; certificateRecordIndex++; } } if (!isMalformedCertificateTablePresent && certificateRecordOffset !== certificateTableSize) { // Exact consumption rejects an orphan tail while permitting any number // of independently aligned certificates in the declared directory. log(logType.any, "Certificate entries do not consume the declared table size"); isMalformedCertificateTablePresent = true; } certificateTableData = undefined; } } } if (isMalformedCertificateTablePresent) addCorruptedDataOption(corruptedDataLevel.uncritical, "Malformed certificate table"); // Validate only CFG fields physically present in the declared Load Config structure. // Optional empty tables remain valid; contradictory pointer/count pairs and impossible // mapped ranges are strong corruption evidence independent of compiler generation. var isInvalidGuardCfMetadataPresent = false; if (declaredDataDirectoryCount > IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG && dataDirectoryTableOffset + (IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG + 1) * 8 <= dataDirectoryOptionalHeaderEnd) { // Load Config is versioned by its leading Size field. First prove that the directory // itself exists, then never read beyond the smaller of both advertised sizes. const loadConfigDirectoryEntryOffset = dataDirectoryTableOffset + IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG * 8, loadConfigDirectoryRva = PE.read_uint32(loadConfigDirectoryEntryOffset), loadConfigDirectorySize = PE.read_uint32(loadConfigDirectoryEntryOffset + 4), loadConfigDirectoryAddress = PE_Cached.imageBase + loadConfigDirectoryRva, guardCfMinimumLoadConfigSize = PE_Cached.is64bit ? 0x94 : 0x5C; // Only the consumed prefix needs bytes; loaded fields may cross unrelated raw section offsets. if (loadConfigDirectoryRva !== 0 && loadConfigDirectorySize >= 4 && (isVirtualAddressRangeFileBacked(loadConfigDirectoryAddress, loadConfigDirectorySize, 0) || readFileBackedVirtualBytes(loadConfigDirectoryAddress, Math.min(loadConfigDirectorySize, guardCfMinimumLoadConfigSize)))) { var declaredLoadConfigSize = readMappedUnsignedInteger(loadConfigDirectoryAddress, 4), effectiveLoadConfigSize = Math.min(loadConfigDirectorySize, declaredLoadConfigSize); if (effectiveLoadConfigSize >= guardCfMinimumLoadConfigSize) { // Field offsets differ between PE32 and PE32+ because every VA-sized member // expands to eight bytes in PE32+. GuardFlags itself remains a DWORD. var guardCfCheckPointerOffset = PE_Cached.is64bit ? 0x70 : 0x48, guardCfDispatchPointerOffset = PE_Cached.is64bit ? 0x78 : 0x4C, guardCfFunctionTableOffset = PE_Cached.is64bit ? 0x80 : 0x50, guardCfFunctionCountOffset = PE_Cached.is64bit ? 0x88 : 0x54, guardCfFlagsOffset = PE_Cached.is64bit ? 0x90 : 0x58, guardCfCheckPointer = readMappedUnsignedInteger(loadConfigDirectoryAddress + guardCfCheckPointerOffset, PE_Cached.is64bit ? 8 : 4), guardCfDispatchPointer = readMappedUnsignedInteger(loadConfigDirectoryAddress + guardCfDispatchPointerOffset, PE_Cached.is64bit ? 8 : 4), guardCfFunctionTable = readMappedUnsignedInteger(loadConfigDirectoryAddress + guardCfFunctionTableOffset, PE_Cached.is64bit ? 8 : 4), guardCfFunctionCount = readMappedUnsignedInteger(loadConfigDirectoryAddress + guardCfFunctionCountOffset, PE_Cached.is64bit ? 8 : 4), guardCfFlags = readMappedUnsignedInteger(loadConfigDirectoryAddress + guardCfFlagsOffset, 4), isGuardCfMetadataDeclared = (PE.getImageOptionalHeader("DllCharacteristics") & IMAGE_DLLCHARACTERISTICS_GUARD_CF) || (guardCfFlags & 0x00000700); // Old Load Config versions can physically contain these offsets without // enabling CFG. Validate them only when the image explicitly declares CFG. // Values beyond JavaScript's exact integer range cannot be safely classified. // Skip those theoretical high-VA images instead of manufacturing corruption. if (isGuardCfMetadataDeclared && guardCfCheckPointer !== -1 && guardCfDispatchPointer !== -1 && guardCfFunctionTable !== -1 && guardCfFunctionCount !== -1) { var nativePointerSize = PE_Cached.is64bit ? 8 : 4; // Check and dispatch fields are VAs of pointer-storage cells, not direct // function targets. Their storage may span sections or end in committed page padding. if (guardCfCheckPointer !== 0 && !isVirtualAddressRangeMapped(guardCfCheckPointer, nativePointerSize, 0) && !isBadFormatRangeCovered(guardCfCheckPointer - PE_Cached.imageBase, guardCfCheckPointer - PE_Cached.imageBase + nativePointerSize - 1)) { log(logType.any, "Guard CF check-function pointer is outside the mapped image: VA 0x" + guardCfCheckPointer.toString(16)); isInvalidGuardCfMetadataPresent = true; } else if (guardCfDispatchPointer !== 0 && !isVirtualAddressRangeMapped(guardCfDispatchPointer, nativePointerSize, 0) && !isBadFormatRangeCovered(guardCfDispatchPointer - PE_Cached.imageBase, guardCfDispatchPointer - PE_Cached.imageBase + nativePointerSize - 1)) { log(logType.any, "Guard CF dispatch-function pointer is outside the mapped image: VA 0x" + guardCfDispatchPointer.toString(16)); isInvalidGuardCfMetadataPresent = true; } else if ((guardCfFunctionTable === 0) !== (guardCfFunctionCount === 0)) { // A table without a count is unused, and a count without a table is // impossible for the loader to enumerate; only the mismatched pair fails. log(logType.any, "Guard CF function-table pointer and entry count are inconsistent"); isInvalidGuardCfMetadataPresent = true; } else if (guardCfFunctionTable !== 0) { // GuardFlags[31:28] extends each four-byte RVA with per-entry metadata. // Multiplication is checked before any conversion back to file offsets. var guardCfFunctionEntrySize = 4 + ((guardCfFlags >>> 28) & 0x0F), guardCfFunctionTableSize = guardCfFunctionCount * guardCfFunctionEntrySize, guardCfFunctionTableEnd = guardCfFunctionTable + guardCfFunctionTableSize - 1, isGuardCfFunctionTableFileBacked = isVirtualAddressRangeFileBacked(guardCfFunctionTable, guardCfFunctionTableSize, 0); // Valid endpoints do not prove that the loader can read intervening // entries; admit cross-section tables only with continuous image coverage. if (guardCfFunctionTableSize <= 0 || guardCfFunctionTableSize > 0x1FFFFFFFFFFFFF || guardCfFunctionTableEnd > 0x1FFFFFFFFFFFFF || PE.VAToOffset(guardCfFunctionTable) === -1 || PE.VAToOffset(guardCfFunctionTableEnd) === -1 || (!isGuardCfFunctionTableFileBacked && !isBadFormatRangeCovered(guardCfFunctionTable - PE_Cached.imageBase, guardCfFunctionTableEnd - PE_Cached.imageBase))) { log(logType.any, "Guard CF function table exceeds file-backed image data: VA 0x" + guardCfFunctionTable.toString(16)); isInvalidGuardCfMetadataPresent = true; } else if (isGuardCfFunctionTableFileBacked) { // A physically contiguous table can be decoded in one engine call. // Legal cross-section tables are left to the loader instead of // being misclassified through non-contiguous file offsets. var guardCfFunctionTableFileOffset = PE.VAToOffset(guardCfFunctionTable), guardCfFunctionTableData = PE.readBytes(guardCfFunctionTableFileOffset, guardCfFunctionTableSize), previousGuardCfFunctionRva = -1; for (var i = 0; i < guardCfFunctionTableData.length && !isInvalidGuardCfMetadataPresent; i += guardCfFunctionEntrySize) { var guardCfFunctionIndex = i / guardCfFunctionEntrySize, guardCfFunctionRva = readUnsignedDwordFromBuffer(guardCfFunctionTableData, i); // The loader binary-searches this table, so descending RVAs are // contradictory. Equal RVAs remain allowed for toolchain metadata. if (guardCfFunctionRva < previousGuardCfFunctionRva) { log(logType.any, "Guard CF function entry #" + guardCfFunctionIndex + " is out of order at VA 0x" + (PE_Cached.imageBase + guardCfFunctionRva).toString(16)); isInvalidGuardCfMetadataPresent = true; } else if (!isVirtualAddressRangeFileBacked(PE_Cached.imageBase + guardCfFunctionRva, 1, SECTION_FLAGS_EXECUTE)) { log(logType.any, "Guard CF function entry #" + guardCfFunctionIndex + " points outside executable code at VA 0x" + (PE_Cached.imageBase + guardCfFunctionRva).toString(16)); isInvalidGuardCfMetadataPresent = true; } previousGuardCfFunctionRva = guardCfFunctionRva; } // Clean up: release the potentially large Guard CF table buffer guardCfFunctionTableData = undefined; } } } } } } if (isInvalidGuardCfMetadataPresent) addCorruptedDataOption(corruptedDataLevel.degraded, "Invalid Guard CF metadata"); // Validate relationships inside the TLS directory without assigning significance to // ordinary templates or callbacks. Only loader-impossible metadata is rejected. var isInvalidTlsMetadataPresent = false; if (declaredDataDirectoryCount > IMAGE_DIRECTORY_ENTRY_TLS && PE_Cached.isTlsPresent && dataDirectoryTableOffset + (IMAGE_DIRECTORY_ENTRY_TLS + 1) * 8 <= dataDirectoryOptionalHeaderEnd) { const tlsDirectoryEntryOffset = dataDirectoryTableOffset + IMAGE_DIRECTORY_ENTRY_TLS * 8, tlsDirectoryRva = PE.read_uint32(tlsDirectoryEntryOffset), tlsDirectorySize = PE.read_uint32(tlsDirectoryEntryOffset + 4), tlsDirectoryMinimumSize = PE_Cached.is64bit ? 0x28 : 0x18, tlsDirectoryAddress = PE_Cached.imageBase + tlsDirectoryRva, tlsDirectoryFields = tlsDirectoryRva !== 0 && tlsDirectorySize >= tlsDirectoryMinimumSize ? readFileBackedTlsDirectory(tlsDirectoryAddress) : undefined; if (tlsDirectorySize >= tlsDirectoryMinimumSize && !tlsDirectoryFields) { log(logType.any, "TLS directory is not file-backed"); isInvalidTlsMetadataPresent = true; } if (tlsDirectoryFields) { var tlsTemplateStartAddress = tlsDirectoryFields[0], tlsTemplateEndAddress = tlsDirectoryFields[1], tlsIndexAddress = tlsDirectoryFields[2], tlsCallbacksAddress = tlsDirectoryFields[3]; if (tlsTemplateStartAddress !== -1 && tlsTemplateEndAddress !== -1 && tlsTemplateStartAddress !== 0 && tlsTemplateEndAddress !== 0 && tlsTemplateStartAddress > tlsTemplateEndAddress) { log(logType.any, "TLS template range is reversed"); isInvalidTlsMetadataPresent = true; } else if (tlsIndexAddress !== -1 && tlsIndexAddress !== 0 && !isVirtualAddressRangeMapped(tlsIndexAddress, 4, 0) && !readFileBackedVirtualBytes(tlsIndexAddress, 4) && !isBadFormatRangeCovered(tlsIndexAddress - PE_Cached.imageBase, tlsIndexAddress - PE_Cached.imageBase + 3)) { log(logType.any, "TLS index storage is outside the mapped image"); isInvalidTlsMetadataPresent = true; } if (!isInvalidTlsMetadataPresent && tlsCallbacksAddress !== -1 && tlsCallbacksAddress !== 0) { var tlsCallbackPointerSize = PE_Cached.is64bit ? 8 : 4; // Section boundaries do not terminate this RVA-based array. Exhausting // the bounded read is inconclusive; only an unmapped end proves a missing NULL. for (var i = 0; i < 64; i++) { var tlsCallbackPointerAddress = tlsCallbacksAddress + i * tlsCallbackPointerSize, tlsCallbackAddress = readMappedUnsignedInteger(tlsCallbackPointerAddress, tlsCallbackPointerSize); if (tlsCallbackAddress === 0) break; if (tlsCallbackAddress === -1) { if (!isVirtualAddressRangeMapped(tlsCallbackPointerAddress, 1, 0) && PE.VAToOffset(tlsCallbackPointerAddress) === -1) { log(logType.any, "TLS callback array is not null-terminated"); isInvalidTlsMetadataPresent = true; } break; } } } } } if (isInvalidTlsMetadataPresent) addCorruptedDataOption(corruptedDataLevel.degraded, "Invalid TLS metadata"); // Resolve the TLS callback array and require every declared callback to point to // physically backed executable code. Merely having a TLS directory is not suspicious. var isTlsCallbackOutsideCodePresent = false; if (declaredDataDirectoryCount > IMAGE_DIRECTORY_ENTRY_TLS && PE_Cached.isTlsPresent && dataDirectoryTableOffset + (IMAGE_DIRECTORY_ENTRY_TLS + 1) * 8 <= dataDirectoryOptionalHeaderEnd) { const tlsCallbackDirectoryEntryOffset = dataDirectoryTableOffset + IMAGE_DIRECTORY_ENTRY_TLS * 8, tlsCallbackDirectoryRva = PE.read_uint32(tlsCallbackDirectoryEntryOffset), tlsCallbackDirectorySize = PE.read_uint32(tlsCallbackDirectoryEntryOffset + 4), tlsCallbackDirectoryMinimumSize = PE_Cached.is64bit ? 0x28 : 0x18, tlsCallbackDirectoryAddress = PE_Cached.imageBase + tlsCallbackDirectoryRva, tlsCallbackDirectoryFields = tlsCallbackDirectoryRva !== 0 && tlsCallbackDirectorySize >= tlsCallbackDirectoryMinimumSize ? readFileBackedTlsDirectory(tlsCallbackDirectoryAddress) : undefined; if (tlsCallbackDirectoryFields) { var tlsCallbackArrayAddress = tlsCallbackDirectoryFields[3]; if (tlsCallbackArrayAddress !== -1 && tlsCallbackArrayAddress !== 0) { var tlsCallbackAddressSize = PE_Cached.is64bit ? 8 : 4; // Follow virtual addresses rather than physical adjacency: the loader can // terminate the array in a section's zero-filled virtual tail. for (var i = 0; i < 64 && !isTlsCallbackOutsideCodePresent; i++) { var tlsCallbackPointerAddress = tlsCallbackArrayAddress + i * tlsCallbackAddressSize, tlsCallbackAddress = readMappedUnsignedInteger(tlsCallbackPointerAddress, tlsCallbackAddressSize); if (tlsCallbackAddress === -1) { log(logType.any, "TLS callback array contains a pointer outside file-backed or zero-filled data"); isTlsCallbackOutsideCodePresent = true; break; } if (tlsCallbackAddress === 0) break; var tlsCallbackRva = tlsCallbackAddress - PE_Cached.imageBase, isTlsCallbackExecutable = false; for (var d = 0; d < PE_Cached.numberOfSections && !isTlsCallbackExecutable; d++) { var tlsCallbackSectionRva = PE.getSectionVirtualAddress(d), tlsCallbackSectionRawSize = PE.getSectionFileSize(d); if (tlsCallbackRva >= tlsCallbackSectionRva && tlsCallbackRva < tlsCallbackSectionRva + tlsCallbackSectionRawSize && (PE.section[d].Flags & SECTION_FLAGS_EXECUTE) && PE.VAToOffset(tlsCallbackAddress) !== -1) { isTlsCallbackExecutable = true; } } if (!isTlsCallbackExecutable) { log(logType.any, "TLS callback outside executable code at VA 0x" + tlsCallbackAddress.toString(16)); isTlsCallbackOutsideCodePresent = true; } } } } } if (isTlsCallbackOutsideCodePresent) addCorruptedDataOption(corruptedDataLevel.critical, "TLS callback outside code"); // Check for corrupted DotNET data var isNetImportsCorrupted = false; if (!PE_Cached.isDotNet && isNetMetaDataPresent() && PE_Cached.numberOfUnmanagedImports === 0) { log(logType.net, "Managed metadata has no usable import table"); isNetImportsCorrupted = true; } if (isNetImportsCorrupted) addCorruptedDataOption(corruptedDataLevel.critical, "DotNET headers/meta"); // Ordinary managed builds retain the global module type name; its absence does not prove a loading failure. var isModuleTypeMissing = false; if (PE_Cached.isDotNet && !PE.isNetObjectPresent("")) { log(logType.net, "The standard type name is missing"); isModuleTypeMissing = true; } if (isModuleTypeMissing) addCorruptedDataOption(corruptedDataLevel.degraded, "Missing module type"); // Check for corrupted DotNET CLR version var isNetClrVersionCorrupted = false; if (PE_Cached.isDotNet && !isAsciiString(PE.getNETVersion())) { log(logType.net, "CLR version string appears malformed"); isNetClrVersionCorrupted = true; } if (isNetClrVersionCorrupted) addCorruptedDataOption(corruptedDataLevel.degraded, "DotNET CLR version"); // Check for stripped Windows Authenticode signature var isWindowsAuthenticodeStripped = false; if (declaredDataDirectoryCount > IMAGE_DIRECTORY_ENTRY_SECURITY && dataDirectoryTableOffset + (IMAGE_DIRECTORY_ENTRY_SECURITY + 1) * 8 <= dataDirectoryOptionalHeaderEnd) { var securityDirOffset = dataDirectoryTableOffset + IMAGE_DIRECTORY_ENTRY_SECURITY * 8, securityDataOffset = PE.read_uint32(securityDirOffset), securityDataSize = PE.read_uint32(securityDirOffset + 4); // A small complete certificate is valid; missing data is proved by the declared range. if (securityDataOffset !== 0 && securityDataSize !== 0 && securityDataOffset + securityDataSize > PE_Cached.fileSize) { log(logType.any, "Authenticode directory appears to reference missing data"); isWindowsAuthenticodeStripped = true; } } if (isWindowsAuthenticodeStripped) addCorruptedDataOption(corruptedDataLevel.uncritical, "Authenticode tampered"); if (options.length !== 0) isDetected = true; if (isDetected) { _setResult("~corrupted data", "Generic", corruptedDataLevelName[detectedCorruptedDataLevel], PE_Cached.isVerbose ? options : String()); } } // #endregion // #region "debug data (.NET and Native)" /** Reports embedded debug information and recognizable build-tool artifacts. */ function scanForDebugData_NET_and_Native() { // For .NET and Native apps log(logType.nothing, "Scanning for debug data..."); var options = String(); var isDetected = Boolean(); // Check for sections with debug data by name var isDebugSectionPresent = false; var debugSectionIndex = -1; var debugSectionName = String(); for (var i = 0; i < PE_Cached.numberOfSections && !isDebugSectionPresent; i++) { debugSectionIndex = i; const currentSectionName = PE.getSectionName(i); if (/(?:debug|^\.stab(?:str)?$)/i.test(currentSectionName)) { // Detects TCC/GCC and modern LLVM debug sections debugSectionName = cleanSectionName(currentSectionName); log(logType.any, "Debug section detected: \"" + debugSectionName + "\""); isDebugSectionPresent = true; } } if (isDebugSectionPresent) options = "Section #" + debugSectionIndex + " (\"" + debugSectionName + "\")"; // .NET Native debug header, EAT function var isNetNativeDebugDataPresent = false; if (!PE_Cached.isDotNet && PE.isExportFunctionPresent("DotNetRuntimeDebugHeader")) { log(logType.net, ".NET Native debug symbols detected"); isNetNativeDebugDataPresent = true; } if (isNetNativeDebugDataPresent) options = addOption(options, "DotNET runtime header"); // Check for -Wl,--export-all-symbols linker option var isSymbolsExported = false; if (!PE_Cached.isDynamicLinkLibrary && PE.isExportFunctionPresent("main")) { log(logType.any, "The \"--export-all-symbols\" linker option was probably used"); isSymbolsExported = true; } if (isSymbolsExported) options = addOption(options, "Symbols exported"); // Check for absolute PDB path var isPdbFullPathPresent = false; var debugOptionalHeaderOffset = PE_Cached.fileSize >= 0x40 ? getOptHeaderOffset() : -1, debugDirectoryEntryOffset = -1; // Engine record enumeration may read a stale Debug slot outside NumberOfRvaAndSizes. // Debug is directory index 6: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-data-directories-image-only if (debugOptionalHeaderOffset >= 24 && debugOptionalHeaderOffset <= PE_Cached.fileSize - 2) { var debugOptionalHeaderSize = PE.readWord(debugOptionalHeaderOffset - 4), debugOptionalHeaderMagic = PE.readWord(debugOptionalHeaderOffset), debugDirectoryStart = debugOptionalHeaderMagic === 0x20B ? 0x70 : 0x60; if ((debugOptionalHeaderMagic === 0x10B || debugOptionalHeaderMagic === 0x20B) && debugOptionalHeaderSize >= debugDirectoryStart + 56 && debugOptionalHeaderOffset + debugOptionalHeaderSize <= PE_Cached.fileSize && PE.read_uint32(debugOptionalHeaderOffset + debugDirectoryStart - 4) > 6) { debugDirectoryEntryOffset = debugOptionalHeaderOffset + debugDirectoryStart + 48; } } var debugDirectoryRva = debugDirectoryEntryOffset !== -1 ? PE.read_uint32(debugDirectoryEntryOffset) : 0, debugDirectorySize = debugDirectoryEntryOffset !== -1 ? PE.read_uint32(debugDirectoryEntryOffset + 4) : 0, debugCompleteRecordsSize = Math.floor(debugDirectorySize / 28) * 28; // Legacy enumeration rounds partial records up and assumes contiguous raw offsets. // Only complete declared records supply a path; a corrupt size cannot exceed the file's record capacity. var numberOfDebugDataRecords = debugDirectoryRva ? Math.min(debugCompleteRecordsSize / 28, Math.floor(PE_Cached.fileSize / 28)) : 0, pdbPathBeginOffset = -1, pdbDebugDataOffset = 0, pdbDebugDataSize = 0; // CodeView stores the path after its fixed header; its extension and case are unrestricted. // https://github.com/llvm/llvm-project/blob/main/llvm/include/llvm/Object/CVDebugRecord.h for (var i = 0; i < numberOfDebugDataRecords; i++) { // A complete record may itself cross sections; use loaded order rather than raw adjacency. var currentDebugRecordAddress = PE_Cached.imageBase + debugDirectoryRva + i * 28; var currentDebugRecordData = readFileBackedVirtualBytes(currentDebugRecordAddress, 28); if (!currentDebugRecordData) break; var currentDebugDataOffset = readUnsignedDwordFromBuffer(currentDebugRecordData, 24), currentDebugDataSize = readUnsignedDwordFromBuffer(currentDebugRecordData, 16); if (readUnsignedDwordFromBuffer(currentDebugRecordData, 12) === 2 && currentDebugDataOffset > 0 && currentDebugDataSize > 16 && currentDebugDataSize <= PE_Cached.fileSize - currentDebugDataOffset) { var codeViewHeaderSize = PE.compare("'RSDS'", currentDebugDataOffset) ? 24 : PE.compare("'NB10'", currentDebugDataOffset) ? 16 : 0; // Require a complete header and a terminator inside this record, not in adjacent data. if (codeViewHeaderSize && currentDebugDataSize > codeViewHeaderSize && PE.findSignature(currentDebugDataOffset + codeViewHeaderSize, currentDebugDataSize - codeViewHeaderSize, "00") !== -1) { var candidatePdbPathOffset = currentDebugDataOffset + codeViewHeaderSize, candidatePdbPath = PE.getString(candidatePdbPathOffset, Math.min(256, currentDebugDataSize - codeViewHeaderSize)), isCandidatePdbAbsolute = /^(?:\/|[a-zA-Z]:[\\/]|\\\\[^\\]+\\[^\\]+\\)/.test(candidatePdbPath); // An empty filename must not hide a usable path in a later CodeView record. if (!candidatePdbPath) continue; // Native-image records may precede the original record; retain an absolute path if either has one. if (pdbPathBeginOffset === -1 || isCandidatePdbAbsolute) { pdbPathBeginOffset = candidatePdbPathOffset; pdbDebugDataOffset = currentDebugDataOffset; pdbDebugDataSize = currentDebugDataSize; } if (isCandidatePdbAbsolute) break; } } } var pdbPath = String(); if (pdbPathBeginOffset !== -1) { var pdbPathMaxSize = Math.min(256, pdbDebugDataOffset + pdbDebugDataSize - pdbPathBeginOffset); pdbPath = File.cleanString(PE.getString(pdbPathBeginOffset, pdbPathMaxSize)); if (pdbPath.length > 255) { const pdbPathSeparator = " . . . . ", pdbPathFragmentLength = Math.floor((255 - pdbPathSeparator.length) / 2); pdbPath = pdbPath.substring(0, pdbPathFragmentLength) + pdbPathSeparator + pdbPath.substring(pdbPath.length - pdbPathFragmentLength); } pdbPath = pdbPath.replace(/[\n\r]/g, String()); // A leading slash denotes a POSIX absolute path, including paths remapped by the build. if (pdbPath.match(/^(?:\/|[a-zA-Z]:[\\/]|\\\\[^\\]+\\[^\\]+\\)/) !== null) { log(logType.any, "PDB absolute path: \"" + pdbPath + "\""); isPdbFullPathPresent = true; } } if (isPdbFullPathPresent) options = addOption(options, "Absolute PDB path"); // A relative CodeView path does not establish PDB format, embedding or release mode. var isPdbRelativePathPresent = false; if (pdbPath && !isPdbFullPathPresent) { log(logType.any, "PDB relative path: \"" + pdbPath + "\""); isPdbRelativePathPresent = true; } if (isPdbRelativePathPresent) options = addOption(options, "Relative PDB path (release)"); // Type 17 declares an embedded Portable PDB; an RSDS path can coexist with it. var isEmbeddedPdbPresent = false; // https://github.com/dotnet/runtime/blob/main/docs/design/specs/PE-COFF.md#embedded-portable-pdb-debug-directory-entry-type-17 if (debugDirectoryEntryOffset !== -1) { if (debugDirectoryRva !== 0 && debugDirectorySize >= 28 && debugDirectorySize % 28 === 0) { for (var i = 0; i < numberOfDebugDataRecords && !isEmbeddedPdbPresent; i++) { var embeddedPdbRecordAddress = PE_Cached.imageBase + debugDirectoryRva + i * 28; // A record may cross sections even when its payload has one ordinary raw offset. var embeddedPdbRecordData = readFileBackedVirtualBytes(embeddedPdbRecordAddress, 28); if (!embeddedPdbRecordData) break; if (readUnsignedDwordFromBuffer(embeddedPdbRecordData, 12) !== 17 || readUnsignedDwordFromBuffer(embeddedPdbRecordData, 0) !== 0 || readUnsignedDwordFromBuffer(embeddedPdbRecordData, 4) !== 0 || (embeddedPdbRecordData[10] | (embeddedPdbRecordData[11] << 8)) !== 0x100) continue; var embeddedPdbDataSize = readUnsignedDwordFromBuffer(embeddedPdbRecordData, 16), embeddedPdbDataOffset = readUnsignedDwordFromBuffer(embeddedPdbRecordData, 24); if (embeddedPdbDataSize <= 8 || embeddedPdbDataOffset <= 0 || embeddedPdbDataOffset > PE_Cached.fileSize - embeddedPdbDataSize) continue; if (!PE.compare("'MPDB'", embeddedPdbDataOffset)) continue; var embeddedPdbUncompressedSize = PE.read_uint32(embeddedPdbDataOffset + 4); // The high bit is reserved for a future format; no decompression or integrity claim is made here. if (embeddedPdbUncompressedSize === 0 || embeddedPdbUncompressedSize >= 0x80000000) continue; log(logType.net, "Embedded Portable PDB: " + embeddedPdbDataSize + " bytes, uncompressed size " + embeddedPdbUncompressedSize); isEmbeddedPdbPresent = true; } } } if (isEmbeddedPdbPresent) options = addOption(options, "Embedded PDB (release)"); // Check for Costura.Fody embedded PDB. It is a .NET tool (library) that embeds DLL builds into the main assembly. var isCosturaEmbeddedDebugData = false; if (PE_Cached.isDotNet && validateNetObject("costura.costura.pdb.compressed")) { log(logType.any, "Costura embedded PDB detected"); isCosturaEmbeddedDebugData = true; } if (isCosturaEmbeddedDebugData) options = addOption(options, "Costura.Fody embedded PDB"); if (options.length !== 0) isDetected = true; if (isDetected) { _setResult("~debug data", "Contains", String(), PE_Cached.isVerbose ? options : String()); } } // #endregion /** * Checks if the .NET Visual Basic standard library is present. * * @returns {boolean} True if the Microsoft.VisualBasic library is found; otherwise false. */ function isVbNetStandardLibraryPresent() { return PE.isNetObjectPresent("Microsoft.VisualBasic"); } /** * Checks if the .NET JScript standard library is present. * * @returns {boolean} True if the Microsoft.JScript library is found; otherwise false. */ function isJscriptNetStandardLibraryPresent() { return PE.isNetObjectPresent("Microsoft.JScript"); } /** * Determines whether the current PE is a framework-style .NET component. * * A component is identified as a System or System.* .NET DLL whose module name * matches its assembly name and which has no global CLR constructor. * * @returns {boolean} True for a matching System.* framework DLL; otherwise false. */ function isFrameworkComponent() { if (!PE_Cached.isDotNet || !PE_Cached.isDynamicLinkLibrary || PE_Cached.isNetGlobalCctorPresent) { return false; } const buildAssemblyName = PE_Cached.nameOfNetAssemblyName, buildModuleName = PE_Cached.nameOfNetModuleName; if ((buildAssemblyName === "System" || buildAssemblyName.indexOf("System.") === 0) && buildModuleName === (buildAssemblyName + ".dll")) { return true; } return false; } /** * Searches the PE body for a DIE signature pattern. * * Searches from the end of the DOS stub to the end of the PE body, excluding * the overlay. On success it logs the pattern and updates `lastOffsetDetected`. * * @param {string} pattern - The pattern to search for within the file. * @returns {boolean} True if the pattern is found, false otherwise. * @example * if (validateSignature("'UPX0'")) { * // A matching signature was found in the PE body. * } */ function validateSignature(pattern) { const offsetFound = PE.findSignature(PE_Cached.imageBodyOffset, PE_Cached.imageBodySize, pattern), resultBool = offsetFound !== -1; if (resultBool) { lastOffsetDetected = "0x" + Number(offsetFound).toString(16); log(logType.any, "Pattern found: " + pattern); } return resultBool; } /** * Searches eligible PE sections for any of the supplied byte-code signatures. * Preserves pattern priority: the first matching signature determines the log and offset. * Uses a native batch search for large ranges when available, with a sequential fallback. * * @param {string[]} patterns - Byte-code signatures in priority order. * @returns {boolean} True if any signature is found; otherwise false. * @sideEffects Updates `lastOffsetDetected` and writes a log entry on success. */ function validateNetByteCodeAny(patterns) { // Check the first pattern separately to preserve its short-circuit behavior and log. if (validateNetByteCode(patterns[0])) return true; var scanBytes = 0; // Batch search is worthwhile only when the eligible file-backed sections are large enough. for (var i = 0; i < PE_Cached.numberOfSections; i++) { if (isNetByteCodeSectionEligible(i)) scanBytes += Math.max(0, PE.section[i].FileSize); } // Keep short-circuit searches unless native batching can replace at least two large scans. if (typeof PE.findSignatures !== "function" || patterns.length < 3 || scanBytes < 65536) { for (var p = 1; p < patterns.length; p++) { if (validateNetByteCode(patterns[p])) return true; } return false; } // Keep result indices aligned with patterns; slot zero was already checked above. var offsets = []; for (var p = 0; p < patterns.length; p++) offsets.push(-1); for (var i = 0; i < PE_Cached.numberOfSections; i++) { if (!isNetByteCodeSectionEligible(i)) continue; // Search only patterns not found in earlier sections; indices map batch results back to priority order. var query = [], indices = []; for (var p = 1; p < patterns.length; p++) { if (offsets[p] === -1) { query.push(patterns[p]); indices.push(p); } } var found = PE.findSignatures(PE.section[i].FileOffset, PE.section[i].FileSize, query); // An unavailable or invalid batch result must not change the verdict: retry the original searches. if (!found || found.length !== query.length) { for (var p = 1; p < patterns.length; p++) { if (validateNetByteCode(patterns[p])) return true; } return false; } for (var p = 0; p < indices.length; p++) { if (found[p] !== -1) offsets[indices[p]] = found[p]; } // No lower-priority pattern can beat the first remaining alternative. if (offsets[1] !== -1) break; } // Report the first matching pattern, not the earliest offset across all patterns. for (var p = 1; p < patterns.length; p++) { if (offsets[p] === -1) continue; lastOffsetDetected = "0x" + Number(offsets[p]).toString(16); log(logType.net, "ByteCode detected: " + patterns[p]); return true; } return false; } /** * Checks whether a PE section should be searched for .NET byte code. * * @param {number} i - Index of the section to check. * @returns {boolean} True for an eligible section; otherwise false. */ function isNetByteCodeSectionEligible(i) { const section = PE.section[i]; // A single section may contain both code and directories; never exclude it by name or size. if (PE_Cached.numberOfSections <= 1) return true; if (section.FileOffset === 0 || section.FileSize === 0) return false; // Directory ownership and a conventional name do not make a section code-free. if (section.Flags & (SECTION_FLAGS_CODE | SECTION_FLAGS_EXECUTE)) return true; // Skip standalone resource/relocation sections, but keep any section shared with code or a PE directory. var isDedicatedResourceSection = i === PE_Cached.indexOfResourceSection && section.Name === ".rsrc" && i !== PE_Cached.indexOfRelocsSection && i !== PE_Cached.indexOfEntryPointSection && i !== PE_Cached.indexOfTlsSection && i !== PE_Cached.indexOfImportsSection; var isDedicatedRelocsSection = i === PE_Cached.indexOfRelocsSection && section.Name === ".reloc" && i !== PE_Cached.indexOfResourceSection && i !== PE_Cached.indexOfEntryPointSection && i !== PE_Cached.indexOfTlsSection && i !== PE_Cached.indexOfImportsSection; return !isDedicatedResourceSection && !isDedicatedRelocsSection; } /** * Searches eligible PE sections for the specified byte-code signature. * * @param {string} byteCode - The byte-code signature to locate. * @returns {boolean} True if the signature is found; otherwise false. * @sideEffects Updates `lastOffsetDetected` and writes a log entry on success. */ function validateNetByteCode(byteCode) { for (var i = 0; i < PE_Cached.numberOfSections; i++) { if (!isNetByteCodeSectionEligible(i)) continue; var section = PE.section[i]; var offsetFound = PE.findSignature(section.FileOffset, section.FileSize, byteCode); if (offsetFound !== -1) { lastOffsetDetected = "0x" + Number(offsetFound).toString(16); log(logType.net, "ByteCode detected: " + byteCode); return true; } } return false; } /** * Checks whether a .NET metadata object is present in the current PE. * * @param {string} object - The metadata object name to search for. * @returns {boolean} True if the object is present; otherwise false. * @sideEffects Writes a .NET log entry when the object is found. */ function validateNetObject(object) { const result = PE.isNetObjectPresent(object); if (result) log(logType.net, "Object present: " + object); return result; } /** * Checks whether a .NET user string is present in the current PE metadata. * * @param {string} ustring - The Unicode string to validate. * @returns {boolean} True if the string is present; otherwise false. * @sideEffects Writes a .NET log entry when the string is found. */ function validateNetUnicodeString(ustring) { const result = PE.isNetUStringPresent(ustring); if (result) log(logType.net, "String present: \"" + ustring + "\""); return result; } /** * Validates if a given Unicode string exists within the PE file. * * This function converts the input to a UTF-16LE signature and searches the PE * body from the end of the DOS stub to the end of the file body, excluding the * overlay. It is independent of the .NET metadata string heap. * * @param {string} ustring - The Unicode string to search for within the PE file. * @returns {boolean} True if the UTF-16LE sequence is found; otherwise false. * @sideEffects Writes a general log entry when the string is found. */ function validateGlobalUnicodeString(ustring) { const result = PE.findSignature(PE_Cached.imageBodyOffset, PE_Cached.imageBodySize, convertStringToUnicodeSignature(ustring)) !== -1; if (result) log(logType.any, "Unicode string found: \"" + ustring + "\""); return result; } /** * Generates a precise UTF-16LE hex signature mask for a given string. * Translates characters into their true byte representation (Little-Endian) * instead of naively injecting null bytes. Safely handles non-ASCII characters. * * Example (ASCII): "test" -> "7400650073007400" * Example (Cyrillic): "Тест" -> "2204350441044204" * * @param {string} ustring - The input string to translate into a signature. * @returns {string} The formatted UTF-16LE hex signature mask. * @example * convertStringToUnicodeSignature("Test"); // "5400650073007400" */ function convertStringToUnicodeSignature(ustring) { var hexMask = []; for (var i = 0; i < ustring.length; i++) { var code = ustring.charCodeAt(i); // Extract Little-Endian bytes (low byte first, then high byte) var low = (code & 0xFF).toString(16).toUpperCase(), high = ((code >> 8) & 0xFF).toString(16).toUpperCase(); low = low.length === 1 ? "0" + low : low; high = high.length === 1 ? "0" + high : high; hexMask.push(low + high); } return hexMask.join(String()); } /** * Checks that none of the provided .NET metadata objects are present. * * @param {Array} objects - An array of .NET object names to check. * @returns {boolean} - Returns true if all .NET objects are missing, otherwise false. * @see isAllNetObjectsPresent */ function isAllNetObjectsMissing(objects) { // Iterate through the array of .NET objects for (var i = 0; i < objects.length; i++) { // Get the current object name const objectName = objects[i]; // If the current .NET object is present, return false if (PE.isNetObjectPresent(objectName)) { return false; } } // If all .NET objects are missing, return true return true; } /** * Checks that every provided .NET metadata object is present. * * @param {Array} objects - An array of .NET object names to check. * @returns {boolean} - Returns true if all .NET objects are present, otherwise false. * @see isAllNetObjectsMissing */ function isAllNetObjectsPresent(objects) { // Iterate through the array of .NET objects for (var i = 0; i < objects.length; i++) { // Get the current object name const objectName = objects[i]; // If the current .NET object is not present, return false if (!PE.isNetObjectPresent(objectName)) { return false; } } // If all .NET objects are present, return true return true; } /** * Determines if a given string name is obfuscated. * * The function checks for obfuscation in two cases: * 1. If the name contains spaces, it splits the name into tokens and checks each token for obfuscation. * 2. If the name does not contain spaces, it checks each chunk of the string (of size 20) for obfuscation. * * Logs a message if an obfuscated segment or chunk is found. * * @param {string} name - The string to analyze for obfuscation. * @returns {boolean} True if the name or any of its segments/chunks are obfuscated, false otherwise. * @example * isNameObfuscated("a1b2c3d4e5f6"); // true * isNameObfuscated("GetWindowText"); // false */ function isNameObfuscated(name) { if (!name || name.length === 0) return false; if (PE_Cached.obfuscationWhitelist.test(name) || PE_Cached.obfuscationWhitelistInsensitive.test(name)) return false; if (name.indexOf(' ') !== -1) { const tokens = name.split(/\s+/); for (var i = 0; i < tokens.length; i++) { var currentToken = tokens[i]; // If it starts with an abbreviation and the string is separated by a space, then it is not obfuscated (samples: The Last Of Us: II Remastered resource files) if (i === 0 && currentToken.match(/^[A-Z]{3,}/)) { continue; } if (isTokenObfuscated(currentToken)) { log(logType.any, "Obfuscated string segment: \"" + File.cleanString(currentToken) + "\" in \"" + File.cleanString(name) + "\""); return true; } } return false; } // Split the name into chunks const CHUNK_SIZE = 20; for (var start = 0; start < name.length; start += CHUNK_SIZE) { const chunk = name.substring(start, start + CHUNK_SIZE); if (isTokenObfuscated(chunk)) { log(logType.any, "Obfuscated string chunk: \"" + File.cleanString(chunk) + "\" in \"" + File.cleanString(name) + "\""); return true; } } return false; } const obfuscationWhitelistPatterns = [ // ── NUMBERS · DATES · TIMESTAMPS ───────────────────────────────────────── // Year / date: 1999, 2005--2022, 2020 - 2024, 31.12.2020, 2020-12-31 /^(?:\d{4}(?:\s*(?:-{1,2}|[–\/.])\s*\d{1,4}(?:\s*[-\/.]\s*\d{1,4})?)?|\d{1,2}\s*[-\/.]\s*\d{1,2}\s*[-\/.]\s*\d{2,4})[,.]?$/, // Pure digit stamps (build, changelist, Unix-ms): 35398403, 20240101120000, 1720000000000000000 /^\d{6,19}$/, // IP address: 192.168.1.1 /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, // ── VERSION STRINGS ────────────────────────────────────────────────────── // Version tag prefix (no end anchor): v1.0-release, v21.9.1-beta /^[Vv]\d{1,2}\.\d{1,4}/, // SemVer with pre-release/metadata: 1.0.0-rc1, 2.0.0-alpha.1, 3.5.0+build.1 /^\d+\.\d+(?:\.\d+)*[-+][a-zA-Z0-9][a-zA-Z0-9._-]{0,29}$/, // OID (3+ components): 1.2.840, 1.2.840.113549.1.1.11 /^\d+(?:\.\d+){2,}$/, // Assembly/module with embedded version: MyLib.2.5, Component.1.0 /^[a-zA-Z][a-zA-Z0-9]{1,30}\.\d+\.\d+$/, // ── IDENTIFIER NAMING CONVENTIONS ───────────────────────────────────────── // SCREAMING_SNAKE_CASE: IDR_MSCORLIB_4030319, MY_CONST_VALUE /^[A-Z][A-Z0-9]*_[A-Z0-9_]+$/, // PascalCase (optional 1-3 digit suffix): GetWindowText, BthMtpContextHandler, CreateFile202 /^(?:[A-Z][a-z]{2,}){2,}\d{0,3}$/, // camelCase (optional 1-3 digit suffix): createFile, readBuffer, sendMessage202 /^[a-z]{1,12}(?:[A-Z][a-z]{2,})+\d{0,3}$/, // snake_case: create_file, get_process_id /^[a-z]{2,}(?:_[a-z]{2,})+$/, // Hungarian notation: lpszFileName, dwProcessId, pbBuffer /^(?:lp(?:sz)?|dw|hw|sz|cb|pb|ul|ui|hg)[A-Z][a-z]{3,}/, // ── PLATFORM & WINDOWS SPECIFICS ────────────────────────────────────────── // Hardware chip/model: AX88179_178A, Acx01000, CUDART64_132_51 /^[A-Za-z]{1,10}\d{3,}(?:[_-][A-Za-z0-9]{1,16})*$/, // All-caps abbreviation + 1-5 digits: DX11, Win32, BCB401, MSVC2022 /^[A-Z]{2,}[0-9]{1,5}$/, // ── PRODUCT, PACKAGE & API IDENTIFIERS ──────────────────────────────────── // Word (6+ letters) + year/date suffix: release20240101, AppBuild20231205 /^[a-zA-Z]{6,}(?:19|20)\d{2,8}$/, ], obfuscationWhitelistInsensitivePatterns = [ // ── NUMBERS · DATES · TIMESTAMPS ───────────────────────────────────────── // Copyright year lists with an attached company name: (C)1995,2019,2020SQUARE ENIX CO., LTD. /^(?:(?:\(\s*C\s*\)|©)\s*)?[12]\d{3}(?:\s*,\s*[12]\d{3}){1,5}\s*[a-z][a-z&.,'®™()_-]*(?:\s+[a-z][a-z&.,'®™()_-]*){0,7}$/i, // Copyright/build years: © 2019-2026, Copyright (C) 2019; 2020; 2021 Company, All Rights Reserved /^(?:copyright\s*)?(?:(?:\(\s*c\s*\)|©)\s*)?[12]\d{3}(?:\s*(?:,|;|[-–—]|to)\s*[12]\d{3}){1,5}(?:\s+(?:all\s+rights\s+reserved[.!]?|[a-z][a-z&.,'®™()_-]*)){0,8}$/i, // Copyright/build year: ©2026, (C) 2026, Copyright 2026 Company™, All Rights Reserved. /^(?:(?:copyright\s*)?(?:\(\s*c\s*\)|©)\s*|copyright\s+)[12]\d{3}(?:\s*(?:,|;|[-–—]|to)\s*[12]\d{3}){0,5}(?:\s+(?:all\s+rights\s+reserved[.!]?|[a-z][a-z&.,'®™()_-]*)){0,8}$/i, // Reversed legal notice: All Rights Reserved © 2026 Company /^all\s+rights\s+reserved[.!]?\s*(?:\(\s*c\s*\)|©)\s*[12]\d{3}(?:\s*(?:,|;|[-–—]|to)\s*[12]\d{3}){0,5}(?:\s+[a-z][a-z&.,'®™()_-]*){0,8}$/i, // Version Info output labels: Dev-output: ©2026, Build Output - Copyright 2026 Company /^(?:(?:dev(?:elopment)?|build)[-_ ]?output\s*(?::|-|–|—)?\s*)(?:(?:copyright\s*)?(?:\(\s*c\s*\)|©)\s*|copyright\s+)[12]\d{3}(?:\s*(?:,|;|[-–—]|to)\s*[12]\d{3}){0,5}(?:\s+(?:all\s+rights\s+reserved[.!]?|[a-z][a-z&.,'®™()_-]*)){0,8}$/i, // ISO 8601 compact datetime: 20250527T143022, 20250527T143022Z, 20250527t143022.123z /^\d{8}T\d{6}(?:\.\d{1,3})?Z?$/i, // ISO 8601 / RFC 3339 datetime: 2025-05-27T14:30:22Z, 2025-05-27T14:30:22.123+03:00 /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/i, // File size: 123KB, 45.6 MB, 1.5GB /^\d+(?:\.\d+)?\s*(?:bytes?|[kmgt]ib?|[kmgt]b)$/i, // Hex literal: 0x1234ABCD /^0x[0-9a-f]+$/i, // Time: 15:20:19, 15:20, 15:20:19.123 (separated as words) /^(?:\d{1,2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?)$/, // ── VERSION STRINGS ────────────────────────────────────────────────────── // Version number: v1.2.3, v.1.2.3, 1.0.0.0, 10.15.7, 1.2.3.4.5 /^(?:v(?:\.)?)?\d+(?:\.\d+){1,5}$/i, // Comma-separated VERSIONINFO value: 10,0,22621,3233 /^\d{1,5}(?:,\d{1,5}){1,5}$/i, // VERSIONINFO text fields: FileVersion: 1.2.3.4, Product Version=10.0.19045.1 /^(?:file|product|assembly|informational)[ _-]?version\s*[:=]?\s*v?\d+(?:\.\d+){1,5}$/i, // Version with hexadecimal VCS metadata: 10.0.9-servicing.26270.113+901ca941248413c79832d2fdbd709da0c4386353 /^\d+(?:\.\d+){1,5}(?:-[a-z][a-z0-9-]*(?:\.\d+){1,4})?\+[0-9a-f]{7,64}$/i, // Version with an explicit commit annotation: 10.0.9 @Commit: 901ca941248413c79832d2fdbd709da0c4386353 /^(?:\d{1,5}(?:,\d{1,5}){1,5}|\d+(?:\.\d+){1,5}(?:-[a-z][a-z0-9-]*(?:\.\d+){1,4})?)\s+@?commit:\s*[0-9a-f]{7,64}$/i, // Windows build branch stamp: (WinBuild.160101.0800), (WinRelRS6.050727-9100) /^\((?:[a-z][a-z0-9]*(?:\([a-z]+\))?)\.\d{6}[-.]\d{4}\)$/i, // Library version (underscore/dot): lz4_1_9_4, zlib_1_2_13, openssl_3_0_9 /^[a-z]{2,12}\d{0,3}(?:[_.]\d{1,5}){1,}$/i, // Runtime/tool + version (no delimiter): node20.11.1, python3.12.10, dotnet9.0.202 /^[a-z]{2,}\d{1,3}\.\d+(?:\.\d+)*$/i, // .NET TFM: net8.0, netstandard2.1, net8.0-windows10.0.19041, net48 /^net(?:coreapp|standard)?\d+(?:\.\d+)?(?:-[a-z0-9.]+)*$/i, // ── GUIDS · HASHES · CRYPTO ─────────────────────────────────────────────── // UUID: 550e8400-e29b-41d4-a716-446655440000 /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, // UUID with braces: {550e8400-e29b-41d4-a716-446655440000} /^\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}$/i, // Partial GUID fragments: 69fe178f-26e7-43a9-a, c4d66f00-b6f0-4439-a /^[0-9a-f]{4,8}(?:-[0-9a-f]{1,8}){2,}$/i, // Build/version hash: 2b7-0fc9cc7187f5_Hyp, 620-2cfb01a9318d_Hyp /^[0-9a-f]{2,8}-[0-9a-f]{8,}(?:[_-][a-zA-Z]+)?$/i, // ECC named curves: secp256k1, secp384r1, prime256v1 /^(?:secp|sect|prime|brainpoolp|c2[tp]nb)\d{2,4}[a-z]\d{1,2}(?:v\d)?$/i, // .NET PublicKeyToken: PublicKeyToken=b77a5c561934e089 /^PublicKeyToken=[0-9a-f]{16}$/i, // .NET assembly identity: Name, Version=1.2.3.4, Culture=neutral, PublicKeyToken=null /^[a-z][a-z0-9._-]{0,127},\s*version=\d+(?:\.\d+){1,3},\s*culture=(?:neutral|[a-z]{2}(?:-[a-z]{2})?),\s*publickeytoken=(?:null|[0-9a-f]{16})$/i, // Explicit cryptographic digest: SHA-256: 64hex, SHA1=40hex, MD5 32hex /^(?:md5\s*[:=]\s*[0-9a-f]{32}|sha(?:-?1)?\s*[:=]\s*[0-9a-f]{40}|sha-?256\s*[:=]\s*[0-9a-f]{64}|sha-?512\s*[:=]\s*[0-9a-f]{128})$/i, // ── PLATFORM & WINDOWS SPECIFICS ────────────────────────────────────────── // DirectX module names: D3D10_1, D3D11On12, D3D9on12 /^d3d\d{1,2}/i, // Kernel debugger transport: kd_02_1137, kdnet_uart16550 /^kd(?:_|net_)/i, // Windows driver/service suffixes: kbldfltr, WSDScDrv, hnswfpdriver, driver.sys /(?:driver|drv|fltr|clx|form|srv|svc|port|bus|hub|hid)(?:\.[a-z0-9]{1,5})?$/i, // Charset/codepage names: c_is2022, cp1251, utf32le /^(?:c_|cp|iso|utf|ucs|gb|koi|win|mac|ascii|latin|ansi)\w*\d/i, // Windows kernel/networking + build/CI keywords /(?:ndis|acpi|storport|classpnp|fltmgr|hidclass|usbhub|usbstor|kbdclass|mouclass|ksuser|nsiproxy|tcpip|netbt|wfplwf|pacer|krnl|sql|mgr|tmp|vds|word|runtime|buildbot|builder|jenkins|gitlab|runner|beta|alpha|client|relclient|steam|win(?:32|64|2)|x64|x86|cuda|iscsi|scsi|sstp|uart|nightly|canary|stable|preview|insider|hotfix|snapshot|release|update|patch|master|develop)/i, // ── PRODUCT, PACKAGE & API IDENTIFIERS ──────────────────────────────────── // OOP/API noun suffixes: GetWindowHandler, CreateContextManager, OnEventCallback /(?:handler|manager|factory|provider|helper|callback|context|interface|service|listener|controller|observer|processor|validator|converter|iterator|descriptor|dispatcher|allocator|builder|writer|reader|parser|encoder|decoder)$/i, // Product installer/config suffixes: ProductInstaller, AppSetup2024, Product-Setup /(?:installer|launcher|setup|updater|patcher|bootstrapper|uninstaller|configuration|settings|preferences|options|properties)(?:[-_.]?\d{1,4})?$/i, // Binary name with an explicit version tag: PresentationNative_v0400.dll /^[a-z][a-z0-9]{2,48}(?:[_-]v?\d{2,8}(?:\.\d+){0,3})+\.(?:exe|dll|sys|msi|msu|msp|cpl|scr|ocx|drv)$/i, // Multiword build description with a VCS reference: ASP.NET Core Module. Commit: 1d696053fcd6735aaac1902afdb0b92edbf43e71 /^(?:[a-z][a-z0-9._()\/-]*\s+){2,20}[a-z][a-z0-9._()\/-]*\s+@?commit:\s*[0-9a-f]{7,64}$/i, // Delimiter-separated identifiers: dotnet-sdk-9.0.202-w, libssl-3.0.9-dev, api-2.1 /^[a-z]{3,}(?:[-._](?:[a-z]+|\d+))*$/i, ]; /** * Determines whether a given token is considered obfuscated based on advanced heuristics. * * The function checks for: * - Tokens shorter than 7 characters are automatically considered clean * - Common legitimate patterns (dates, versions, UUIDs, etc.) * - Hexadecimal strings (8+ chars) like 'd3adc0de' * - Regular expression patterns indicating mixed alphanumeric sequences * - Statistical character analysis with entropy detection * - Repetitive pattern detection * * Whitelist matches are checked before every heuristic and always return false. * A positive result is a suspicious-name signal, not proof of malware. * * @param {string} token - The string token to analyze * @returns {boolean} True if token matches obfuscation patterns, false otherwise * @example * isTokenObfuscated("a1b2c3d4e5"); // true * isTokenObfuscated("550e8400-e29b-41d4-a716-446655440000"); // false */ function isTokenObfuscated(token) { if (!token || token.length < 7) return false; // === WHITELIST: Common legitimate patterns === if (PE_Cached.obfuscationWhitelist.test(token) || PE_Cached.obfuscationWhitelistInsensitive.test(token)) return false; // === OBFUSCATION DETECTION === var stats = { digits: 0, lower: 0, upper: 0, special: 0, transitions: 0 }; var charTypes = []; for (var i = 0; i < token.length; i++) { var char = token[i]; var prevType = charTypes[charTypes.length - 1]; var currType; if (char >= '0' && char <= '9') { stats.digits++; currType = 'digit'; } else if (char >= 'a' && char <= 'z') { stats.lower++; currType = 'lower'; } else if (char >= 'A' && char <= 'Z') { stats.upper++; currType = 'upper'; } else { stats.special++; currType = 'special'; } charTypes.push(currType); if (prevType && prevType !== currType) { stats.transitions++; } } const totalChars = token.length, alphaChars = stats.lower + stats.upper; // Detect excessive character type transitions (obfuscation indicator) if ((stats.transitions / totalChars) > 0.6 && stats.digits >= 4) return true; // Detect repetitive patterns (e.g., "a1b2c3d4e5") if (token.match(/^(?:[a-z]\d){4,}$/i)) return true; // Pattern: letter-digit-letter with 4+ total digits if (token.match(/^(?=\d|[a-z])(?=.*[a-z]\d[a-z])(?=(?:.*\d){4,})/i)) return true; // Detect pure hex strings (8+ chars) - likely hashes if (token.match(/^[0-9a-f]{8,}$/i)) { // Exception: common words that are hex-valid (like "decade", "faded") if (!token.match(/^(?:dead|beef|cafe|babe|fade|deed|feed|face|decade|deface)$/i)) { return true; } } // Repeating syllables/segments (e.g., "jnfasjknjnjnjn" - "nj" repeats 3+ times) // Detects 2-3 char segments repeating 3+ times consecutively if (token.match(/(.{2,3})\1{2,}/)) return true; // Mixed letters and digit clusters (e.g., "ac8a8sic8i19o9oi", "j9813jd1039i098") // Pattern: letter(s) + digit(s), repeated 4+ times with varying lengths if (token.match(/(?:[a-z]+\d+){4,}/i) && stats.digits >= 4) return true; // Chaotic alternating: single letter + multiple digits (e.g., "j9813jd1039i098") // Pattern: letter + 3+ digits, repeated 2+ times if (token.match(/(?:[a-z]\d{3,}){2,}/i)) return true; // Random consonant clusters without vowels (e.g., "jnfsjknjn", "bcdfgh") // 8+ consonants in a row (excluding common patterns and abbreviation-style names with doubled consonants like "spprgrss.dll") // Doubled consonants (pp, ss, rr, etc.) indicate vowel-stripped abbreviations, not obfuscation if (token.match(/[bcdfghjklmnpqrstvwxz]{8,}/i) && !token.match(/str|chr|thr|sch|scr|spr/i) && !token.match(/([bcdfghjklmnpqrstvwxz])\1/i)) return true; // Single consonant repeated excessively (e.g., "jnfasjknjnjnjn") // Same consonant appears 7+ times in strings shorter than 15 chars // (threshold 7 avoids FPs from "luainstall.dll" l=6, "rassstp.sys" s=5, "msiscsi.sys" s=5) if (token.length < 15) { var consonantCounts = {}; for (var i = 0; i < token.length; i++) { var ch = token[i].toLowerCase(); if (/[bcdfghjklmnpqrstvwxz]/.test(ch)) { if ((consonantCounts[ch] = (consonantCounts[ch] || 0) + 1) >= 7) return true; } } } // Keyboard walk patterns — horizontal rows, 5+ chars (forward and backward). // No legitimate abbreviation or identifier contains these sequences. if (token.match(/qwert|werty|ertyu|rtyui|tyuio|yuiop|asdfg|sdfgh|dfghj|fghjk|ghjkl|zxcvb|xcvbn|cvbnm|poiuy|oiuyt|iuyte|uyter|ytrew|trewq|lkjhg|kjhgf|jhgfd|hgfds|gfdsa|mnbvc|nbvcx|bvcxz/i)) return true; // Keyboard walk patterns — vertical columns, 6+ chars (e.g., "qazwsx", "wsxedc"). // Single 3-char columns (qaz, wsx) may appear in code; 6+ chars are unambiguous. if (token.match(/qazwsx|wsxedc|edcrfv|rfvtgb|tgbyhn|yhnujm|zaqxsw|xswcde|cdevfr|vfrbgt|bgtnhy|nhymju/i)) return true; // Number row sequences — 5+ consecutive digits in ascending or descending order. if (token.match(/01234|12345|23456|34567|45678|56789|98765|87654|76543|65432|54321/)) return true; // Excessive digit-letter boundaries (e.g., "a1b2c3d4e5f6") // Count transitions specifically between letters and digits (8+ transitions) var digitLetterTransitions = 0; for (var i = 1; i < token.length; i++) { var curr = token[i], prev = token[i - 1], currIsDigit = (curr >= '0' && curr <= '9'), prevIsDigit = (prev >= '0' && prev <= '9'), currIsLetter = ((curr >= 'a' && curr <= 'z') || (curr >= 'A' && curr <= 'Z')), prevIsLetter = ((prev >= 'a' && prev <= 'z') || (prev >= 'A' && prev <= 'Z')); if ((currIsDigit && prevIsLetter) || (currIsLetter && prevIsDigit)) { digitLetterTransitions++; } } if (digitLetterTransitions >= 8) return true; // Product filenames may combine a short name, release year and build number. // Keep this below deterministic obfuscation checks so a valid-looking suffix // cannot hide a keyboard walk or another unambiguous anomalous pattern. if (token.match(/^[a-z]{2,}[a-z0-9_-]{0,30}(?:19|20)\d{2}(?:[._-]\d{1,5}){0,3}(?:\.(?:exe|dll|sys|msi|msu|msp|cpl|scr|ocx|drv))?$/i)) return false; // Statistical thresholds for obfuscation return ( (stats.digits >= 7) || // Too many digits (stats.digits >= 4 && stats.digits / totalChars > 0.4) || // High digit density (raised from 0.35) (stats.digits >= 5 && stats.lower >= 3 && stats.upper >= 3) || // Balanced mix (suspicious) (stats.digits >= 6 && stats.lower >= 5) || // Many digits + lowercase (alphaChars >= 5 && stats.digits >= 5 && stats.upper === 0 && stats.lower === alphaChars) // No uppercase variation ); } const IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA = 0x0020, IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE = 0x0040, IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY = 0x0080, IMAGE_DLLCHARACTERISTICS_NX_COMPAT = 0x0100, IMAGE_DLLCHARACTERISTICS_NO_ISOLATION = 0x0200, IMAGE_DLLCHARACTERISTICS_NO_SEH = 0x0400, IMAGE_DLLCHARACTERISTICS_NO_BIND = 0x0800, IMAGE_DLLCHARACTERISTICS_APPCONTAINER = 0x1000, IMAGE_DLLCHARACTERISTICS_WDM_DRIVER = 0x2000, IMAGE_DLLCHARACTERISTICS_GUARD_CF = 0x4000, IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE = 0x8000; // #region "obfuscations (Native)" /** * Proves a transfer into executable bytes rewritten by the reachable EP stub. * * Only a single statically resolved path is followed. Unknown branches, calls, * memory reads and arithmetic stop the proof instead of supplying guessed values. * The candidate write must change file-backed bytes in a writable code section. * * @returns {Object|undefined} Proven write and transfer virtual addresses. */ function traceEpWrittenCodeTransfer() { if (!PE_Cached.isEntryPointAnalysisAllowed || !PE_Cached.isArchX86 || PE_Cached.isDriver) return undefined; const registerPattern = "(?:E(?:AX|BX|CX|DX|SI|DI|BP|SP)|R(?:AX|BX|CX|DX|SI|DI|BP|SP|8|9|1[0-5])D?)", numericPattern = "(?:0X[0-9A-F]+|[0-9]+)", writePattern = new RegExp("^MOV (BYTE|WORD|DWORD) PTR \\[(" + registerPattern + ")(?: ([+-]) (" + numericPattern + "))?\\], (" + numericPattern + ")$", "i"), leaPattern = new RegExp("^LEA (" + registerPattern + "), ", "i"), moveLiteralPattern = new RegExp("^MOV(?:ABS)? (" + registerPattern + "), (" + numericPattern + ")$", "i"), moveRegisterPattern = new RegExp("^MOV (" + registerPattern + "), (" + registerPattern + ")$", "i"), arithmeticPattern = new RegExp("^(ADD|SUB|XOR) (" + registerPattern + "), (" + numericPattern + ")$", "i"), comparePattern = new RegExp("^CMP (" + registerPattern + "), (" + numericPattern + ")$", "i"), testPattern = new RegExp("^TEST (" + registerPattern + "), \\1$", "i"), directBranchPattern = new RegExp("^(?:JMP|JE|JNE|JZ|JNZ) (" + numericPattern + ")$", "i"), indirectBranchPattern = new RegExp("^JMP (" + registerPattern + ")$", "i"), stackPattern = new RegExp("^(PUSH|POP) (" + registerPattern + "|" + numericPattern + ")$", "i"), maximumSafeAddress = 0x1FFFFFFFFFFFFF; function registerKey(registerName) { return getNativeRegisterKey(registerName); } function assignRegister(registers, registerName, value) { var key = registerKey(registerName), isDwordRegister = PE_Cached.isI386 || /^(?:E[A-Z]+|R(?:8|9|1[0-5])D)$/i.test(registerName); if (value === undefined || !isFinite(value) || value > maximumSafeAddress || (!isDwordRegister && value < 0)) { delete registers[key]; } else { registers[key] = isDwordRegister ? value >>> 0 : value; } } var currentAddress = PE_Cached.addressOfUnmanagedEntryPoint, registers = {}, stack = [], visited = {}, zeroFlag, writtenRanges = [], writtenBytes = {}; function getWrittenRange(target) { // A write matters only when a reachable instruction still differs from the file. for (var i = 0; i < writtenRanges.length; i++) { var range = writtenRanges[i]; var targetLength = target < range.Address && range.Address - target < 15 ? PE.getDisasmLength(target) : 0; // An instruction is rewritten when its operand bytes change, too. if (target < range.Address + range.Size && (target >= range.Address || (targetLength > range.Address - target && isVirtualAddressRangeFileBacked(target, targetLength, SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE)))) { for (var address = target; address < range.Address + range.Size;) { var length = PE.getDisasmLength(address), opCode = getAsmOpCode(PE.getDisasmString(address)); if (!opCode || length <= 0) break; for (var byteIndex = address; byteIndex < address + length && byteIndex < range.Address + range.Size; byteIndex++) { if (writtenBytes.hasOwnProperty(byteIndex.toString())) return range; } if (/^(?:J[A-Z]*|LOOP[A-Z]*|CALLF?|RETF?N?|INT[A-Z0-9]*|IRET[A-Z]*|HLT|UD[012]|SYSCALL|SYSENTER|SYSRET|SYSEXIT)$/.test(opCode)) break; address += length; } } } return undefined; } // A writable executable EP is a necessary condition for this narrow proof. if (!isVirtualAddressRangeFileBacked(currentAddress, 1, SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE)) return undefined; for (var instructionIndex = 0; instructionIndex < 96; instructionIndex++) { if (visited[currentAddress] || getWrittenRange(currentAddress) || !isVirtualAddressRangeFileBacked(currentAddress, 1, SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE)) break; visited[currentAddress] = true; var instruction = PE.getDisasmString(currentAddress), instructionLength = PE.getDisasmLength(currentAddress), opCode = getAsmOpCode(instruction), nextAddress = currentAddress + instructionLength, match; if (!opCode || instructionLength <= 0 || !isVirtualAddressRangeFileBacked(currentAddress, instructionLength, SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE)) break; // A proven null pointer cannot supply the memory or stack access required by this path. if (isEpProvenFaultingInstruction(instruction, opCode, registers.RSP === 0, registers)) break; match = instruction.match(writePattern); if (match) { var baseAddress = registers[registerKey(match[2])], displacement = match[4] ? parseInt(match[4], 0) * (match[3] === "-" ? -1 : 1) : 0, size = match[1] === "BYTE" ? 1 : match[1] === "WORD" ? 2 : 4, writeAddress = baseAddress + displacement, writeValue = parseInt(match[5], 0); // Address-size overrides use the low dword and wrap the complete effective address. if (/^(?:E[A-Z]+|R(?:8|9|1[0-5])D)$/i.test(match[2]) && baseAddress !== undefined) writeAddress = ((baseAddress >>> 0) + displacement) >>> 0; if (baseAddress === undefined || !isFinite(writeAddress) || writeAddress < nextAddress || !isFinite(writeValue) || writeValue < 0 || writeValue >= Math.pow(0x100, size) || !isVirtualAddressRangeFileBacked(writeAddress, size, SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE)) break; if (writtenRanges.length >= 4) break; // A write overlapping a simulated stack slot invalidates its stored return-address proof. if (stack.length && registers.RSP !== undefined && writeAddress < registers.RSP + stack.length * (PE_Cached.isI386 ? 4 : 8) && writeAddress + size > registers.RSP) { stack = []; } writtenRanges.push({ Address: writeAddress, Size: size }); for (var byteIndex = 0; byteIndex < size; byteIndex++) { var byteAddress = writeAddress + byteIndex, byteValue = Math.floor(writeValue / Math.pow(0x100, byteIndex)) & 0xFF; if (PE.readByte(PE.VAToOffset(byteAddress)) === byteValue) delete writtenBytes[byteAddress.toString()]; else writtenBytes[byteAddress.toString()] = byteValue; } } else if ((match = instruction.match(leaPattern))) { // Raw widths and displacements also cover I386, no-index SIB and segment-overridden forms. assignRegister(registers, match[1], getEpEncodedLeaTarget(currentAddress, instruction, instructionLength)); if (registerKey(match[1]) === "RSP") stack = []; } else if ((match = instruction.match(moveLiteralPattern))) { assignRegister(registers, match[1], parseInt(match[2], 0)); if (registerKey(match[1]) === "RSP") stack = []; } else if ((match = instruction.match(moveRegisterPattern))) { assignRegister(registers, match[1], registers[registerKey(match[2])]); if (registerKey(match[1]) === "RSP") stack = []; } else if ((match = instruction.match(arithmeticPattern))) { // Unknown operands must not reuse a function-scoped result from an earlier instruction. var oldValue = registers[registerKey(match[2])], immediate = parseInt(match[3], 0), newValue = undefined; // x64 arithmetic encodes an imm32 sign-extended to 64 bits. Refuse // ambiguous large immediates rather than choosing a wrong branch. if (PE_Cached.is64bit && /^R(?:AX|BX|CX|DX|SI|DI|BP|SP|8|9|1[0-5])$/i.test(match[2]) && immediate > 0x7FFFFFFF) break; if (oldValue !== undefined) { if (match[1] === "ADD") newValue = oldValue + immediate; else if (match[1] === "SUB") newValue = oldValue - immediate; else if (!PE_Cached.is64bit || /^(?:E[A-Z]+|R(?:8|9|1[0-5])D)$/i.test(match[2])) { newValue = (oldValue ^ immediate) >>> 0; } } assignRegister(registers, match[2], newValue); if (registerKey(match[2]) === "RSP") stack = []; zeroFlag = registers[registerKey(match[2])] === undefined ? undefined : registers[registerKey(match[2])] === 0; } else if ((match = instruction.match(comparePattern))) { var comparedValue = registers[registerKey(match[1])]; // Provenance aliases share storage, but a dword comparison ignores the upper half. if (comparedValue !== undefined && /^(?:E[A-Z]+|R(?:8|9|1[0-5])D)$/i.test(match[1])) comparedValue = comparedValue >>> 0; if (PE_Cached.is64bit && /^R(?:AX|BX|CX|DX|SI|DI|BP|SP|8|9|1[0-5])$/i.test(match[1]) && parseInt(match[2], 0) > 0x7FFFFFFF) break; zeroFlag = comparedValue === undefined ? undefined : comparedValue === parseInt(match[2], 0); } else if ((match = instruction.match(testPattern))) { var testedValue = registers[registerKey(match[1])]; if (testedValue !== undefined && /^(?:E[A-Z]+|R(?:8|9|1[0-5])D)$/i.test(match[1])) testedValue = testedValue >>> 0; zeroFlag = testedValue === undefined ? undefined : testedValue === 0; } else if ((match = instruction.match(stackPattern))) { var stackPointerValue = registers.RSP, poppedValue; if (match[1] === "PUSH") { if (stack.length >= 32) break; var pushIsImmediate = /^(?:0X[0-9A-F]+|[0-9]+)$/i.test(match[2]); // An immediate has no register alias exposing a word-sized PUSH; only native stack slots are modeled. if (pushIsImmediate && getEpPushOperandSize(currentAddress, instructionLength) !== (PE_Cached.isI386 ? 4 : 8)) break; // PUSH writes after decrementing SP; a proven zero destination faults before any later pivot. if (stackPointerValue === (PE_Cached.isI386 ? 4 : 8)) break; if (PE_Cached.is64bit && /^(?:0X[0-9A-F]+|[0-9]+)$/i.test(match[2]) && parseInt(match[2], 0) > 0x7FFFFFFF) break; stack.push(pushIsImmediate ? parseInt(match[2], 0) : registers[registerKey(match[2])]); } else { if (!stack.length) break; poppedValue = stack.pop(); } // PUSH reads its operand before decrementing SP; POP increments SP before writing its destination. assignRegister(registers, PE_Cached.isI386 ? "ESP" : "RSP", stackPointerValue === undefined ? undefined : stackPointerValue + (match[1] === "PUSH" ? -1 : 1) * (PE_Cached.isI386 ? 4 : 8)); if (match[1] === "PUSH" && registers.RSP !== undefined) { // PUSH writes memory too; restored or unknown bytes cannot retain an earlier mutation proof. var pushedValue = stack[stack.length - 1], pushedSize = PE_Cached.isI386 ? 4 : 8; for (var pushedByteIndex = 0; pushedByteIndex < pushedSize; pushedByteIndex++) { var pushedByteAddress = registers.RSP + pushedByteIndex, pushedByteKey = pushedByteAddress.toString(); if (!writtenBytes.hasOwnProperty(pushedByteKey)) continue; var pushedByteValue = pushedValue === undefined ? undefined : Math.floor(pushedValue / Math.pow(0x100, pushedByteIndex)) & 0xFF; if (pushedByteValue === undefined || pushedByteValue === PE.readByte(PE.VAToOffset(pushedByteAddress))) delete writtenBytes[pushedByteKey]; else writtenBytes[pushedByteKey] = pushedByteValue; } } if (match[1] === "POP") { assignRegister(registers, match[2], poppedValue); if (registerKey(match[2]) === "RSP") stack = []; } } else if (opCode === "RET" || opCode === "RETN") { if (!/^(?:RET|RETN)$/.test(instruction) || !stack.length || !isEpNativeNearTransfer(currentAddress, instruction, instructionLength)) break; var returnTarget = stack.pop(); var returnRange = getWrittenRange(returnTarget); if (returnRange) return { WriteAddress: returnRange.Address, TargetAddress: returnTarget }; break; } else if ((match = instruction.match(indirectBranchPattern))) { if (!isEpNativeNearTransfer(currentAddress, instruction, instructionLength)) break; var indirectTarget = registers[registerKey(match[1])]; var indirectRange = getWrittenRange(indirectTarget); if (indirectRange) return { WriteAddress: indirectRange.Address, TargetAddress: indirectTarget }; if (indirectTarget === undefined) break; nextAddress = indirectTarget; } else if ((match = instruction.match(directBranchPattern))) { var branchTarget = parseInt(match[1], 0); if (opCode === "JMP") { if (!isEpNativeNearTransfer(currentAddress, instruction, instructionLength)) break; var branchRange = getWrittenRange(branchTarget); if (branchRange) return { WriteAddress: branchRange.Address, TargetAddress: branchTarget }; nextAddress = branchTarget; } else { if (zeroFlag === undefined) break; if ((opCode === "JE" || opCode === "JZ") === zeroFlag) { if (!isEpNativeNearTransfer(currentAddress, instruction, instructionLength)) break; // A proven taken conditional transfer needs the same write check as Jmp. var conditionalRange = getWrittenRange(branchTarget); if (conditionalRange) return { WriteAddress: conditionalRange.Address, TargetAddress: branchTarget }; nextAddress = branchTarget; } } } else if (opCode !== "NOP" && opCode !== "FNOP") { break; } currentAddress = nextAddress; } return undefined; } /** * Verify export parsing, hashing and indirect transfer on one reachable EP path. * Facts from alternative branches never merge. The bounded graph fails closed if * too many states are needed, and revisiting one instruction adds no new evidence. * Byte-dependency masks reject discarded inputs without treating every shift as a clobber. * @param {number[]} starts - EP and synthetic-transfer starting addresses. * @param {Object} successors - Decoded executable CFG edges indexed by address. * @param {Object} instructions - Decoded instructions indexed by address. * @param {number[]} indirectAddresses - Candidate indirect transfer addresses. * @param {Object} patterns - Architecture-specific resolver patterns. * @returns {boolean} True when a single path contains the complete chain. */ function isEpResolverPathProven(starts, successors, instructions, indirectAddresses, patterns) { var indirect = {}, pending = [], nextIndex = 0, visited = {}; function swapFacts(facts, left, right) { var value = facts[left]; if (facts[right] === undefined) delete facts[left]; else facts[left] = facts[right]; if (value === undefined) delete facts[right]; else facts[right] = value; } function registerKey(name) { var register = name; if (/^[ABCD][HL]$/.test(register)) return "R" + register[0] + "X"; if (/^(?:AX|BX|CX|DX|SI|DI|BP|SP)$/.test(register)) return "R" + register; if (/^(?:SIL|DIL|BPL|SPL)$/.test(register)) return "R" + register.substring(0, 2); return register.replace(/^E/, "R").replace(/(?:B|W|D)$/, String()); } /** * Gets an operand alias's data-bearing bits, including the AH byte offset. * * @param {string} name - Decoded register alias. * @returns {Object} Low/high dword masks for its operand bits. */ function dependencyMask(name) { if (/^R(?:AX|BX|CX|DX|SI|DI|BP|SP|8|9|1[0-5])$/i.test(name)) return { Low: 0xFFFFFFFF, High: 0xFFFFFFFF }; if (/^(?:E[A-Z]+|R(?:8|9|1[0-5])D)$/i.test(name)) return { Low: 0xFFFFFFFF, High: 0 }; return { Low: /^[ABCD]H$/i.test(name) ? 0xFF00 : /^(?:[ABCD]L|SIL|DIL|BPL|SPL|R(?:8|9|1[0-5])B)$/i.test(name) ? 0xFF : 0xFFFF, High: 0 }; } /** * Clips copied dependency bits to the destination width without mutating shared path facts. * * @param {Object|undefined} facts - Existing dependency masks. * @param {string} name - Full-register copy destination. * @returns {Object|undefined} Immutable masks after its width conversion. */ function copyDependency(facts, name) { if (!facts) return undefined; var mask = dependencyMask(name), low = (facts.Low & mask.Low) >>> 0, high = (facts.High & mask.High) >>> 0; return low || high ? { Low: low, High: high } : undefined; } for (var i = 0; i < indirectAddresses.length; i++) indirect[indirectAddresses[i].toString()] = true; for (var i = 0; i < starts.length; i++) { pending.push({ Address: starts[i], PebState: {}, PebProven: false, Stage: 0, Fields: {}, ByteRead: false, ByteRegisters: {}, Linked: {}, Kinds: {}, UnsafeTargets: {}, Backward: false }); } while (nextIndex < pending.length && nextIndex < 16384) { var state = pending[nextIndex++], key = state.Address.toString(), instruction = instructions[key]; if (!instruction || !successors.hasOwnProperty(key)) continue; var stage = state.Stage, fields = {}, kinds = {}, pebState = {}, byteRegisters = {}, linked = {}, unsafeTargets = {}, pebProven = state.PebProven; for (var field in state.Fields) fields[field] = true; for (var pebRegister in state.PebState) pebState[pebRegister] = state.PebState[pebRegister]; for (var byteRegister in state.ByteRegisters) byteRegisters[byteRegister] = state.ByteRegisters[byteRegister]; for (var linkedRegister in state.Linked) linked[linkedRegister] = true; for (var unsafeRegister in state.UnsafeTargets) unsafeTargets[unsafeRegister] = true; for (var register in state.Kinds) { kinds[register] = {}; for (var kind in state.Kinds[register]) kinds[register][kind] = true; } var pebLoad = instruction.match(patterns.PebLoad), ldrLoad = instruction.match(patterns.LdrLoad), listLoad = instruction.match(patterns.ListLoad), registerCopy = instruction.match(patterns.RegisterCopy), registerExchange = instruction.match(patterns.RegisterExchange), registerWrite = instruction.match(patterns.RegisterWrite), partialWrite = instruction.match(patterns.PartialWrite) || instruction.match(/^SET[A-Z]+ ([ABCD][HL]|SIL|DIL|BPL|SPL|R(?:8|9|1[0-5])B)$/i); if (pebLoad) { pebState[getNativeRegisterKey(pebLoad[1])] = 1; } else if (ldrLoad) { var ldrSource = getNativeRegisterKey(ldrLoad[2]), ldrDestination = getNativeRegisterKey(ldrLoad[1]); if (pebState[ldrSource] === 1) pebState[ldrDestination] = 2; else delete pebState[ldrDestination]; } else if (listLoad && pebState[getNativeRegisterKey(listLoad[2])] === 2) { pebProven = true; } else if (registerCopy) { var copySource = getNativeRegisterKey(registerCopy[2]), copyDestination = getNativeRegisterKey(registerCopy[1]); if (pebState[copySource]) pebState[copyDestination] = pebState[copySource]; else delete pebState[copyDestination]; } else if (registerExchange) { swapFacts(pebState, registerKey(registerExchange[1]), registerKey(registerExchange[2])); } else if (registerWrite) { delete pebState[getNativeRegisterKey(registerWrite[1])]; } else if (partialWrite) { delete pebState[getEpPartialRegisterKey(partialWrite[1])]; } else if (!/^(?:NOP|FNOP|PAUSE|CMP|TEST|J[A-Z]+)$/.test(instruction.split(" ")[0])) { pebState = {}; } if (pebProven && stage === 0 && patterns.Header.test(instruction)) stage = 1; else if (stage === 1 && patterns.Directory.test(instruction)) stage = 2; var byteRead = state.ByteRead, hashPresent = false; if (stage === 2) { var fieldMatch = instruction.match(patterns.Field), transform = instruction.match(patterns.Transform), byteShift = instruction.match(/^(SHR|SHL|SAL|SAR) ([A-Z][A-Z0-9]*), (0X[0-9A-F]+|\d+)$/i), byteWiden = instruction.match(/^(?:RCL|RCR|BSWAP) ([A-Z][A-Z0-9]*)(?:,|$)/i); if (fieldMatch) fields[parseInt(fieldMatch[1], 0).toString()] = true; if (patterns.ByteRead.test(instruction)) { byteRead = true; var byteLoad = instruction.match(/^MOV(?:ZX|SX)? ([A-Z][A-Z0-9]*), BYTE PTR (?:[A-Z]+:)?\[/i); if (byteLoad) { var byteDestinationIsDword = /^(?:E[A-Z]+|R(?:8|9|1[0-5])D)$/i.test(byteLoad[1]), byteDestination = registerKey(byteLoad[1]), oldByteMask = byteRegisters[byteDestination] || { Low: 0, High: 0 }, byteWriteMask = dependencyMask(byteLoad[1]), byteSeedMask = /^MOVSX /i.test(instruction) ? byteWriteMask : { Low: /^[ABCD]H$/i.test(byteLoad[1]) ? 0xFF00 : 0xFF, High: 0 }; // Partial loads preserve only already-proven upper dependencies, not arbitrary upper values. byteRegisters[byteDestination] = { Low: ((oldByteMask.Low & ~byteWriteMask.Low) | byteSeedMask.Low) >>> 0, High: byteDestinationIsDword ? 0 : ((oldByteMask.High & ~byteWriteMask.High) | byteSeedMask.High) >>> 0 }; } else if (/^LODSB(?: |$)/i.test(instruction)) { var oldLodsMask = byteRegisters.RAX || { Low: 0, High: 0 }; byteRegisters.RAX = { Low: (oldLodsMask.Low | 0xFF) >>> 0, High: oldLodsMask.High }; } } if (transform) { var hashRegister = registerKey(transform[2]), hashOperands = instruction.substring(instruction.indexOf(", ") + 2).split(", "), hashOperand = hashOperands[0], hashSource = /^[A-Z][A-Z0-9]*$/i.test(hashOperand) ? registerKey(hashOperand) : String(), isReplacingMultiply = transform[1] === "IMUL" && hashOperands.length === 2, hashImmediateText = isReplacingMultiply ? hashOperands[1] : hashOperand, hashImmediate = /^(?:-?0X[0-9A-F]+|-?[0-9]+)$/i.test(hashImmediateText) ? parseInt(hashImmediateText, 0) : undefined, isZeroing = /^(?:XOR|SUB) ([A-Z][A-Z0-9]*), \1$/i.test(instruction) || (transform[1] === "IMUL" && hashImmediate === 0), isIdentity = /^(?:ADD|SUB|XOR)$/.test(transform[1]) && hashImmediate === 0 || transform[1] === "IMUL" && hashImmediate === 1 || /^(?:ROR|ROL)$/.test(transform[1]) && hashImmediate !== undefined && (hashImmediate & (/^R(?:AX|BX|CX|DX|SI|DI|BP|SP|8|9|1[0-5])$/.test(transform[2]) ? 63 : 31)) === 0; var hashOperandMask = dependencyMask(transform[2]), hashDestinationDependency = copyDependency(byteRegisters[hashRegister], transform[2]), hashSourceDependency = copyDependency(byteRegisters[hashSource], hashOperand); if (byteRegisters[hashRegister] && !hashDestinationDependency) { delete byteRegisters[hashRegister]; delete linked[hashRegister]; delete kinds[hashRegister]; } else if (hashDestinationDependency) byteRegisters[hashRegister] = hashDestinationDependency; // Three-operand IMUL replaces the destination with source * immediate. if (isReplacingMultiply && hashSource !== hashRegister) { kinds[hashRegister] = {}; if (kinds[hashSource]) for (var kind in kinds[hashSource]) kinds[hashRegister][kind] = true; if (hashSourceDependency) byteRegisters[hashRegister] = hashSourceDependency; else delete byteRegisters[hashRegister]; if (linked[hashSource] && hashSourceDependency) linked[hashRegister] = true; else delete linked[hashRegister]; } if (!kinds[hashRegister] || isZeroing) kinds[hashRegister] = {}; if (isZeroing) { delete byteRegisters[hashRegister]; delete linked[hashRegister]; } else if (!isIdentity) { if (byteRegisters[hashRegister] || hashSourceDependency) { if (/^(?:ROR|ROL)$/.test(transform[1]) && hashImmediate !== undefined && hashDestinationDependency) { var rotatedLow = hashDestinationDependency.Low, rotatedHigh = hashDestinationDependency.High, hashRotateCount = hashImmediate & (hashOperandMask.High ? 63 : 31); // A known rotate permutes dependency bits; it cannot spread them to the whole operand. // Intel SDM, RCL/RCR/ROL/ROR: https://cdrdv2-public.intel.com/782151/253667-sdm-vol-2b.pdf if (transform[1] === "ROR") hashRotateCount = -hashRotateCount & (hashOperandMask.High ? 63 : 31); var hashRotatePart = hashRotateCount & 31; if (hashRotateCount >= 32) { rotatedLow = hashDestinationDependency.High; rotatedHigh = hashDestinationDependency.Low; } if (hashRotatePart) { var rotateLowSource = rotatedLow, rotateHighSource = rotatedHigh; if (hashOperandMask.High) { rotatedLow = ((rotateLowSource << hashRotatePart) | (rotateHighSource >>> (32 - hashRotatePart))) >>> 0; rotatedHigh = ((rotateHighSource << hashRotatePart) | (rotateLowSource >>> (32 - hashRotatePart))) >>> 0; } else rotatedLow = ((rotateLowSource << hashRotatePart) | (rotateLowSource >>> (32 - hashRotatePart))) >>> 0; } byteRegisters[hashRegister] = { Low: rotatedLow, High: rotatedHigh }; } else if (transform[1] === "IMUL" && byteRegisters[hashRegister] && ((hashImmediate > 1 && hashImmediate <= 0x40000000 && (hashImmediate & (hashImmediate - 1)) === 0) || (!hashOperandMask.High && (hashImmediate === 0x80000000 || hashImmediate === -0x80000000)))) { var multipliedDependency = byteRegisters[hashRegister], multiplyShift = 0, multiplyFactor = hashImmediate >>> 0; // A positive power of two is a shift, including a replacing three-operand IMUL. // Qword IMUL sign-extends imm32; its sign-bit-only value is a shift only for dword writes. while (multiplyFactor > 1) { multiplyFactor /= 2; multiplyShift++; } byteRegisters[hashRegister] = { Low: (multipliedDependency.Low << multiplyShift) >>> 0, High: hashOperandMask.High ? ((multipliedDependency.High << multiplyShift) | (multipliedDependency.Low >>> (32 - multiplyShift))) >>> 0 : 0 }; if (!byteRegisters[hashRegister].Low && !byteRegisters[hashRegister].High) { delete byteRegisters[hashRegister]; delete linked[hashRegister]; kinds[hashRegister] = {}; } } else byteRegisters[hashRegister] = dependencyMask(transform[2]); if (byteRegisters[hashRegister]) linked[hashRegister] = true; } kinds[hashRegister][transform[1]] = true; } } else if (byteShift) { var shiftedRegister = registerKey(byteShift[2]), byteMask = byteRegisters[shiftedRegister], shiftLeft = byteShift[1] === "SHL" || byteShift[1] === "SAL", shiftWidth = /^R(?:AX|BX|CX|DX|SI|DI|BP|SP|8|9|1[0-5])$/i.test(byteShift[2]) ? 64 : /^(?:E[A-Z]+|R(?:8|9|1[0-5])D)$/i.test(byteShift[2]) ? 32 : /^(?:[ABCD][HL]|SIL|DIL|BPL|SPL|R(?:8|9|1[0-5])B)$/i.test(byteShift[2]) ? 8 : 16, shiftCount = parseInt(byteShift[3], 0) & (shiftWidth === 64 ? 63 : 31); // Shift only the operand's dependency bits; AH and partial writes leave unrelated bits untouched. if (byteMask) { var shiftedLow = byteMask.Low, shiftedHigh = byteMask.High; if (shiftWidth === 64 && shiftCount) { if (shiftLeft) { shiftedLow = shiftCount < 32 ? (byteMask.Low << shiftCount) >>> 0 : 0; shiftedHigh = shiftCount < 32 ? ((byteMask.High << shiftCount) | (byteMask.Low >>> (32 - shiftCount))) >>> 0 : (byteMask.Low << (shiftCount - 32)) >>> 0; } else { shiftedLow = shiftCount < 32 ? ((byteMask.Low >>> shiftCount) | (byteMask.High << (32 - shiftCount))) >>> 0 : byteMask.High >>> (shiftCount - 32); shiftedHigh = shiftCount < 32 ? byteMask.High >>> shiftCount : 0; } } else if (shiftWidth === 32) { shiftedLow = shiftLeft ? (byteMask.Low << shiftCount) >>> 0 : byteMask.Low >>> shiftCount; shiftedHigh = 0; } else if (shiftWidth < 32) { var shiftedOperandMask = dependencyMask(byteShift[2]).Low, shiftedOperandOffset = /^[ABCD]H$/i.test(byteShift[2]) ? 8 : 0, shiftedBits = (byteMask.Low & shiftedOperandMask) >>> shiftedOperandOffset; shiftedBits = shiftLeft ? shiftedBits << shiftCount : shiftedBits >>> shiftCount; shiftedLow = ((byteMask.Low & ~shiftedOperandMask) | ((shiftedBits << shiftedOperandOffset) & shiftedOperandMask)) >>> 0; } // SAR broadcasts a dependency only when the original sign bit carried that dependency. if (byteShift[1] === "SAR" && shiftCount) { if (shiftWidth === 64 && (byteMask.High & 0x80000000)) { shiftedHigh = (shiftedHigh | (shiftCount >= 32 ? 0xFFFFFFFF : 0xFFFFFFFF << (32 - shiftCount))) >>> 0; if (shiftCount > 32) shiftedLow = (shiftedLow | (0xFFFFFFFF << (64 - shiftCount))) >>> 0; } else if (shiftWidth === 32 && (byteMask.Low & 0x80000000)) shiftedLow = (shiftedLow | (0xFFFFFFFF << (32 - shiftCount))) >>> 0; else if (shiftWidth < 32 && (byteMask.Low & (1 << (shiftWidth - 1 + shiftedOperandOffset)))) { var signMask = shiftCount >= shiftWidth ? shiftedOperandMask : (((shiftedOperandMask >>> shiftedOperandOffset) << (shiftWidth - shiftCount)) << shiftedOperandOffset) & shiftedOperandMask; shiftedLow = (shiftedLow | signMask) >>> 0; } } if (!shiftedLow && !shiftedHigh) { delete byteRegisters[shiftedRegister]; delete linked[shiftedRegister]; delete kinds[shiftedRegister]; } else byteRegisters[shiftedRegister] = { Low: shiftedLow, High: shiftedHigh }; } } else if (byteWiden) { var widenedRegister = registerKey(byteWiden[1]), previousMask = byteRegisters[widenedRegister], operandMask = dependencyMask(byteWiden[1]); if (previousMask && /^BSWAP /i.test(instruction)) { var swappedLow = 0, swappedHigh = 0; for (var byteIndex = 0; byteIndex < 4; byteIndex++) { swappedLow |= ((previousMask.Low >>> (byteIndex * 8)) & 0xFF) << ((3 - byteIndex) * 8); swappedHigh |= ((previousMask.High >>> (byteIndex * 8)) & 0xFF) << ((3 - byteIndex) * 8); } byteRegisters[widenedRegister] = operandMask.High ? { Low: swappedHigh >>> 0, High: swappedLow >>> 0 } : { Low: swappedLow >>> 0, High: 0 }; if (!byteRegisters[widenedRegister].Low && !byteRegisters[widenedRegister].High) { delete byteRegisters[widenedRegister]; delete linked[widenedRegister]; delete kinds[widenedRegister]; } } else { var rotateImmediate = instruction.match(/^(?:RCL|RCR) [A-Z][A-Z0-9]*, (0X[0-9A-F]+|\d+)$/i), rotateWidth = operandMask.High ? 64 : operandMask.Low === 0xFFFFFFFF ? 32 : operandMask.Low === 0xFFFF ? 16 : 8, effectiveRotateCount = rotateImmediate ? parseInt(rotateImmediate[1], 0) & (rotateWidth === 64 ? 63 : 31) : -1; if (effectiveRotateCount >= 0 && rotateWidth < 32) effectiveRotateCount %= rotateWidth + 1; // A zero effective count cannot spread dependency bits; a dword write still clears the upper half. if (effectiveRotateCount === 0) { if (previousMask && rotateWidth === 32) { byteRegisters[widenedRegister] = copyDependency(previousMask, byteWiden[1]); if (!byteRegisters[widenedRegister]) { delete byteRegisters[widenedRegister]; delete linked[widenedRegister]; delete kinds[widenedRegister]; } } } else if (previousMask && ((previousMask.Low & operandMask.Low) || (previousMask.High & operandMask.High))) { // Other unmodeled transforms may move byte-derived bits anywhere in the written operand. byteRegisters[widenedRegister] = { Low: (previousMask.Low | operandMask.Low) >>> 0, High: /^(?:E[A-Z]+|R(?:8|9|1[0-5])D)$/i.test(byteWiden[1]) ? 0 : (previousMask.High | operandMask.High) >>> 0 }; } } } else { if (registerCopy) { var hashSource = registerKey(registerCopy[2]), hashDestination = registerKey(registerCopy[1]); if (kinds[hashSource]) { kinds[hashDestination] = {}; for (var kind in kinds[hashSource]) kinds[hashDestination][kind] = true; } else delete kinds[hashDestination]; if (byteRegisters[hashSource]) byteRegisters[hashDestination] = copyDependency(byteRegisters[hashSource], registerCopy[1]); else delete byteRegisters[hashDestination]; if (linked[hashSource] && byteRegisters[hashDestination]) linked[hashDestination] = true; else delete linked[hashDestination]; if (unsafeTargets[hashSource]) unsafeTargets[hashDestination] = true; else delete unsafeTargets[hashDestination]; } else if (registerExchange) { var left = registerKey(registerExchange[1]), right = registerKey(registerExchange[2]), leftLinked = !!linked[left], rightLinked = !!linked[right]; // Preserve the hash on its new register but distrust the old indirect target. swapFacts(kinds, left, right); swapFacts(linked, left, right); swapFacts(byteRegisters, left, right); if (byteRegisters[left]) byteRegisters[left] = copyDependency(byteRegisters[left], registerExchange[1]); if (byteRegisters[right]) byteRegisters[right] = copyDependency(byteRegisters[right], registerExchange[2]); if (!byteRegisters[left]) delete linked[left]; if (!byteRegisters[right]) delete linked[right]; swapFacts(unsafeTargets, left, right); if (leftLinked && !rightLinked) unsafeTargets[left] = true; if (rightLinked && !leftLinked) unsafeTargets[right] = true; } else if (registerWrite) { var overwrittenRegister = registerKey(registerWrite[1]); if (linked[overwrittenRegister]) unsafeTargets[overwrittenRegister] = true; delete kinds[overwrittenRegister]; delete linked[overwrittenRegister]; if (!patterns.ByteRead.test(instruction)) delete byteRegisters[overwrittenRegister]; } if (partialWrite) { var partialKey = getEpPartialRegisterKey(partialWrite[1]); delete kinds[partialKey]; delete linked[partialKey]; if (!patterns.ByteRead.test(instruction)) delete byteRegisters[partialKey]; } } for (var register in kinds) { if (byteRegisters[register] && linked[register] && Object.keys(kinds[register]).length >= 2) { hashPresent = true; break; } } } var fieldKeys = Object.keys(fields).sort(), kindKeys = Object.keys(kinds).sort(), kindState = String(); for (var k = 0; k < kindKeys.length; k++) kindState += kindKeys[k] + "=" + Object.keys(kinds[kindKeys[k]]).sort().join(",") + ";"; var pebKeys = Object.keys(pebState).sort(), pebStateKey = String(); for (var k = 0; k < pebKeys.length; k++) pebStateKey += pebKeys[k] + pebState[pebKeys[k]]; var byteKeys = Object.keys(byteRegisters).sort(), byteStateKey = String(); for (var k = 0; k < byteKeys.length; k++) { var byteFact = byteRegisters[byteKeys[k]]; if (byteFact) byteStateKey += byteKeys[k] + "=" + byteFact.Low + "," + byteFact.High + ";"; } var stateKey = key + ":" + pebProven + ":" + pebStateKey + ":" + stage + ":" + fieldKeys.join(",") + ":" + byteRead + ":" + kindState + ":" + byteStateKey + ":" + Object.keys(linked).sort().join(",") + ":" + Object.keys(unsafeTargets).sort().join(",") + ":" + state.Backward; if (visited[stateKey]) continue; visited[stateKey] = true; var indirectRegister = instruction.match(/^(?:CALL|JMP) ([A-Z][A-Z0-9]*)$/i) || instruction.match(/^(?:CALL|JMP) (?:DWORD|QWORD) PTR (?:[A-Z]+:)?\[([A-Z][A-Z0-9]*)(?: [+-] (?:0X[0-9A-F]+|\d+))?\]$/i); if (indirect[key] && stage === 2 && fieldKeys.length >= 2 && byteRead && hashPresent && state.Backward && !(indirectRegister && unsafeTargets[registerKey(indirectRegister[1])])) return true; var next = successors[key]; for (var j = 0; j < next.length; j++) { var backward = state.Backward || (next[j] < state.Address && stage === 2 && fieldKeys.length >= 2 && byteRead && hashPresent); pending.push({ Address: next[j], PebState: pebState, PebProven: pebProven, Stage: stage, Fields: fields, ByteRead: byteRead, ByteRegisters: byteRegisters, Linked: linked, Kinds: kinds, UnsafeTargets: unsafeTargets, Backward: backward }); } } return false; } /** * Proves that bitstream decoding, copying and the final transfer share one EP path. * * Walks only instructions already decoded by the bounded EP traversal. The state * records the required copy and distinct ADC/REP MOVSB instructions. Scalar-copy * states also carry register values, so byte-copy and decoder evidence cannot * be combined from mutually exclusive paths. Reverse reachability excludes dead * branches; loops may exit without inflating evidence. * The state limit makes ambiguous large graphs fail closed. * * @param {number[]} startAddresses - Proven entry addresses, including a synthetic transfer target if present. * @param {number} transferAddress - Address of the candidate final control transfer. * @param {number} copyAddress - Required scalar byte-write address, or -1 for REP MOVSB copying. * @param {Object} successors - Decoded CFG successors indexed by instruction address. * @param {Object} facts - ADC and REP MOVSB facts indexed by instruction address. * @param {Object} [destinationProof] - Scalar-copy register, virtual range and decoded instructions. * @returns {boolean} True if one reachable path contains the required chain. */ function isEpBitstreamPathProven(startAddresses, transferAddress, copyAddress, successors, facts, destinationProof) { var pending = [], nextIndex = 0, examined = 0, visited = {}, adcBits = {}, repBits = {}, adcBitCount = 0, repBitCount = 0, // UPX-like scalar loops expose four distinct ADC sites on an exit path; // additional decoder sites may lie on alternative paths in the same graph. requiredAdc = copyAddress === -1 ? 8 : 4, predecessors = {}, canReachTransfer = {}; const registerPattern = "(?:E(?:AX|BX|CX|DX|SI|DI|BP|SP)|R(?:AX|BX|CX|DX|SI|DI|BP|SP|8|9|1[0-5])D?)", numberPattern = "(?:-?0X[0-9A-F]+|-?[0-9]+)", literalPattern = new RegExp("^MOV(?:ABS)? (" + registerPattern + "), (" + numberPattern + ")$", "i"), copyPattern = new RegExp("^MOV (" + registerPattern + "), (" + registerPattern + ")$", "i"), exchangePattern = new RegExp("^XCHG (" + registerPattern + "), (" + registerPattern + ")$", "i"), leaPattern = new RegExp("^LEA (" + registerPattern + "), \\[(" + registerPattern + ") ([+-]) (" + numberPattern + ")\\]$", "i"), ripPattern = new RegExp("^LEA (" + registerPattern + "), \\[RIP ([+-]) (" + numberPattern + ")\\]$", "i"), absolutePattern = new RegExp("^LEA (" + registerPattern + "), \\[((?:0X[0-9A-F]+|[0-9]+))\\]$", "i"), arithmeticPattern = new RegExp("^(ADD|SUB) (" + registerPattern + "), (" + numberPattern + ")$", "i"), unaryPattern = new RegExp("^(INC|DEC) (" + registerPattern + ")$", "i"), writePattern = new RegExp("^(?:MOV|MOVABS|LEA|ADD|SUB|ADC|SBB|XOR|OR|AND|INC|DEC|NEG|NOT|SHL|SHR|SAR|ROL|ROR|POP) (" + registerPattern + ")(?:,|$)", "i"), partialPattern = /^(?:MOV|XOR|ADD|SUB|AND|OR|NOT|NEG|INC|DEC) ((?:[ABCD][HL]|[ABCD]X|SI|DI|BP|SIL|DIL|BPL|R(?:8|9|1[0-5])(?:B|W)))(?:,|$)/i; function registerKey(name) { return getNativeRegisterKey(name); } function setRegister(values, name, value) { var key = registerKey(name); if (value === undefined || !isFinite(value) || Math.abs(value) > 0x1FFFFFFFFFFFFF) delete values[key]; else values[key] = PE_Cached.isI386 || /^(?:E[A-Z]+|R(?:8|9|1[0-5])D)$/i.test(name) ? value >>> 0 : value; } function advanceValues(values, instruction, address) { var next = {}, match; for (var register in values) next[register] = values[register]; if ((match = instruction.match(literalPattern))) setRegister(next, match[1], parseInt(match[2], 0)); else if ((match = instruction.match(copyPattern))) setRegister(next, match[1], next[registerKey(match[2])]); else if ((match = instruction.match(exchangePattern))) { var left = next[registerKey(match[1])], right = next[registerKey(match[2])]; setRegister(next, match[1], right); setRegister(next, match[2], left); } else if ((match = instruction.match(leaPattern))) { var base = next[registerKey(match[2])], isDwordAddress = PE_Cached.is64bit && /^(?:E[A-Z]+|R(?:8|9|1[0-5])D)$/i.test(match[2]), addressValue = base === undefined ? undefined : (isDwordAddress ? base >>> 0 : base) + parseInt(match[4], 0) * (match[3] === "-" ? -1 : 1); // Address-size truncation precedes the destination write, even when LEA writes a qword register. if (addressValue !== undefined && isDwordAddress) addressValue >>>= 0; setRegister(next, match[1], addressValue); } else if ((match = instruction.match(ripPattern))) { setRegister(next, match[1], address + PE.getDisasmLength(address) + parseInt(match[3], 0) * (match[2] === "-" ? -1 : 1)); } else if ((match = instruction.match(absolutePattern))) setRegister(next, match[1], parseInt(match[2], 0)); else if ((match = instruction.match(arithmeticPattern))) { var oldValue = next[registerKey(match[2])]; // A dword operation truncates its input before arithmetic, avoiding an inexact qword sum. if (oldValue !== undefined && /^(?:E[A-Z]+|R(?:8|9|1[0-5])D)$/i.test(match[2])) oldValue >>>= 0; setRegister(next, match[2], oldValue === undefined ? undefined : oldValue + parseInt(match[3], 0) * (match[1] === "SUB" ? -1 : 1)); } else if ((match = instruction.match(unaryPattern))) { var oldValue = next[registerKey(match[2])]; if (oldValue !== undefined && /^(?:E[A-Z]+|R(?:8|9|1[0-5])D)$/i.test(match[2])) oldValue >>>= 0; setRegister(next, match[2], oldValue === undefined ? undefined : oldValue + (match[1] === "INC" ? 1 : -1)); } else if ((match = instruction.match(writePattern))) { delete next[registerKey(match[1])]; if (instruction.indexOf("POP ") === 0) delete next.RSP; } else if ((match = instruction.match(partialPattern))) { delete next[getEpPartialRegisterKey(match[1])]; } else if (/^PUSH(?: |AL$)/.test(instruction)) delete next.RSP; else if (!/^(?:CMP|TEST|NOP|FNOP|PAUSE|CLC|STC|CMC|J[A-Z]+|LOOP(?:E|NE|NZ|Z)?)(?: |$)/.test(instruction)) next = {}; return next; } if (destinationProof && registerKey(destinationProof.Register) === "RSP") return false; for (var factAddress in facts) { if (facts[factAddress].Adc) { if (adcBitCount >= 30) return false; adcBits[factAddress] = 1 << adcBitCount++; } if (copyAddress === -1 && facts[factAddress].RepMovsb) { if (repBitCount >= 30) return false; repBits[factAddress] = 1 << repBitCount++; } } for (var source in successors) { var next = successors[source]; for (var j = 0; j < next.length; j++) { var target = next[j].toString(); if (!predecessors[target]) predecessors[target] = []; predecessors[target].push(parseInt(source, 10)); } } var reverse = [transferAddress]; for (var reverseIndex = 0; reverseIndex < reverse.length; reverseIndex++) { var reverseKey = reverse[reverseIndex].toString(); if (canReachTransfer[reverseKey]) continue; canReachTransfer[reverseKey] = true; var previous = predecessors[reverseKey] || []; for (var p = 0; p < previous.length; p++) reverse.push(previous[p]); } for (var i = 0; i < startAddresses.length; i++) { pending.push({ Address: startAddresses[i], AdcMask: 0, Adc: 0, RepMask: 0, Rep: 0, ReadSeen: false, CopySeen: false, Values: {} }); } while (nextIndex < pending.length && examined++ < 16384) { var state = pending[nextIndex++], key = state.Address.toString(); if (!canReachTransfer[key] || !successors.hasOwnProperty(key)) continue; var adcBit = adcBits[key] || 0, repBit = repBits[key] || 0, newAdc = state.Adc < requiredAdc && adcBit && !(state.AdcMask & adcBit), adc = state.Adc + (newAdc ? 1 : 0), adcMask = adc >= requiredAdc ? 0 : state.AdcMask | adcBit, repMask = state.RepMask | repBit, rep = state.Rep + (repBit && !(state.RepMask & repBit) ? 1 : 0), readSeen = state.ReadSeen || (destinationProof !== undefined && state.Address === destinationProof.ReadAddress), copySeen = state.CopySeen || (state.Address === copyAddress && (!destinationProof || readSeen)), stateKey = key + ":" + readSeen + ":" + copySeen + ":" + adc + ":" + adcMask + ":" + repMask; if (destinationProof && !state.CopySeen) { var valueKeys = Object.keys(state.Values).sort(); for (var v = 0; v < valueKeys.length; v++) stateKey += ":" + valueKeys[v] + "=" + state.Values[valueKeys[v]]; if (state.Address === copyAddress && readSeen) { var destination = state.Values[registerKey(destinationProof.Register)]; if (!(destination >= destinationProof.Start && destination < destinationProof.End)) continue; } } if (visited[stateKey]) continue; visited[stateKey] = true; if (state.Address === transferAddress) { if (adc >= requiredAdc && (copyAddress === -1 ? rep >= 2 : copySeen)) return true; continue; } var values = destinationProof && !copySeen ? advanceValues(state.Values, destinationProof.Instructions[key], state.Address) : {}; next = successors[key]; for (var j = 0; j < next.length; j++) { if (canReachTransfer[next[j].toString()]) { pending.push({ Address: next[j], AdcMask: adcMask, Adc: adc, RepMask: repMask, Rep: rep, ReadSeen: readSeen, CopySeen: copySeen, Values: values }); } } } return false; } /** Detects native-code obfuscation using PE structure and reachable entry-point code. */ function scanForObfuscations_Native() { log(logType.nothing, "Scanning for obfuscation..."); var options = String(); var isDetected = Boolean(); // PE file must contain at least two sections: one for code and one for resources. var isOneSectionFile = false; if (PE_Cached.numberOfSections === 1) { log(logType.any, "Only one section found!"); isOneSectionFile = true; } if (isOneSectionFile) options = addOption(options, "Only one section"); // Check for section names containing forbidden characters var isStrangeSectionsPresent = false; // Define forbidden characters const badSectionChars = '-=+~!@#$%^&*()"№;%:?*():;,/\\|\'`<> '; // Iterate through sections and characters to check for forbidden characters for (var i = 0; i < PE_Cached.numberOfSections && !isStrangeSectionsPresent; i++) { var sectionName = PE.getSectionName(i); if (sectionName.length < 3 || (/^\.\d.+/.test(sectionName) && sectionName !== ".00cfg") || sectionName[0] === " ") { isStrangeSectionsPresent = true; } var isIdioticMinGwSectionsPresent = false; if (_isResultPresent("linker", "GNU linker ld (GNU Binutils)")) { if (PE.isSectionNamePresent(".build-id")) { isIdioticMinGwSectionsPresent = true; } else { for (var d = 1; d < 10 && !isIdioticMinGwSectionsPresent; d++) { // sections like "/5", "/2" etc if (sectionName.indexOf("/" + d) !== -1) { isIdioticMinGwSectionsPresent = true; } } } } if (isIdioticMinGwSectionsPresent) { isStrangeSectionsPresent = false; break; } for (var d = 0; d < badSectionChars.length && !isStrangeSectionsPresent; d++) { // If forbidden character is found, set flag and break if (sectionName.indexOf(badSectionChars[d]) !== -1) { isStrangeSectionsPresent = true; } } if (!isAsciiString(sectionName) && !isStrangeSectionsPresent) { isStrangeSectionsPresent = true; } } if (isStrangeSectionsPresent) { log(logType.any, "Unusual section names detected"); } if (isStrangeSectionsPresent) options = addOption(options, "Strange sections"); // Check for DOS header in the PE file var isDosMissing = false, isCustomDosPresent = false; // If DOS stub size is 0, set flag for missing DOS if (PE_Cached.dosStubSize === 0) { log(logType.any, "DOS stub is missing"); isDosMissing = true; } else { // Define messages to check for custom DOS const messages = [ "This program cannot be run in DOS mode.", // most popular (standard) "This program requires Microsoft Windows", // MASM / FASM / older assemblers "This program must be run under Windows", // twinBASIC "This program must be run under Win32", // Borland Delphi / C++ Builder (Turbo Linker) "This program must be run under Win64", // . "This program requires Win32", // Legacy Win32 SDK linkers "This is a Windows NT character-mode executable", // Watcom C/C++ "This program was launched in DOS but it requires Windows.", // EuroASM "Win32 only.", // Pellec C ]; isCustomDosPresent = true; // Iterate through messages to check for custom DOS for (var d = 0; d < messages.length && isCustomDosPresent; d++) { if (PE.isSignaturePresent(PE_Cached.dosStubOffset, PE_Cached.dosStubSize, "'" + messages[d] + "'")) { isCustomDosPresent = false; } } if (isCustomDosPresent) { log(logType.any, "Nonstandard DOS stub detected"); } } // Add appropriate option based on DOS presence if (isDosMissing) options = addOption(options, "Missing DOS"); else if (isCustomDosPresent) options = addOption(options, "Custom DOS"); // Checks if the import table is mutated (for example, instead of "kernel32.dll" it is written ".\./kernel32.dll") var isImportTableMutated = false; if (PE.isLibraryPresentExp(/^\.[\\/]\.[\\/]/)) { log(logType.any, "Import library path mutation detected"); isImportTableMutated = true; } if (isImportTableMutated) options = addOption(options, "IAT mutation"); // It works if the file contains an import without an extension (for example, instead of "kernel32.dll" it is written "kernel32"). // Compilers don't do that var isContainsNoExtensionLibrary = false; for (var i = 0; i < PE_Cached.numberOfUnmanagedImports && !isContainsNoExtensionLibrary; i++) { const libraryName = PE.getImportLibraryName(i).toLowerCase(); if (libraryName.length > 4) { if (libraryName[libraryName.length - 4] !== ".") { log(logType.any, "Import library without a filename extension detected: \"" + File.cleanString(libraryName) + "\""); isContainsNoExtensionLibrary = true; } } else { /* if (libraryName.indexOf(".") === -1) */ log(logType.any, "Import library without a filename extension detected: \"" + File.cleanString(libraryName) + "\""); isContainsNoExtensionLibrary = true; } } if (isContainsNoExtensionLibrary) options = addOption(options, "No extension import"); // .exe files in imports are a separate type of sophistication. But this happens. var exeInImports = false; for (var i = 0; i < PE_Cached.numberOfUnmanagedImports && !exeInImports; i++) { const libraryName = PE.getImportLibraryName(i).toLowerCase(); if (libraryName.length > 4) { if (libraryName !== "ntoskrnl.exe" && libraryName.substring(libraryName.length - 4) === ".exe") { log(logType.any, "Executable image referenced as an import: \"" + File.cleanString(libraryName) + "\""); exeInImports = true; } } } if (exeInImports) options = addOption(options, "EXE in imports"); // Checks if application resources can be read or if they are compressed/encrypted var isUnreadableResourcesPresent = false; for (var i = 0; i < PE_Cached.numberOfUnmanagedResources && !isUnreadableResourcesPresent; i++) { if (PE.getResourceOffsetByNumber(i) === -1) { log(logType.any, "One or more resources are not file-backed"); isUnreadableResourcesPresent = true; } } if (isUnreadableResourcesPresent) options = addOption(options, "Unreadable resources"); // Checks is executable application has been compiled or converted to a DLL // Like https://github.com/hasherezade/exe_to_dll var exeAsDll = false; if (PE_Cached.isDynamicLinkLibrary && PE.isExportFunctionPresentExp(/^(?:Start|main|_start|(?:w)?WinMain|EntryPoint)$/)) { log(logType.any, "DLL exports an application-style entry point"); exeAsDll = true; } if (exeAsDll) options = addOption(options, "EXE as DLL"); // The .text section should always come first var isTextSectionNotFirst = false; if (PE_Cached.numberOfSections > 0 && (PE.section[0].Name !== ".text" && PE.section[0].Name !== ".textbss" && PE.section[0].Name !== ".code") && (PE.isSectionNamePresent(".text") || PE.isSectionNamePresent(".textbss"))) { log(logType.any, "Primary code section is not first"); isTextSectionNotFirst = true; } if (isTextSectionNotFirst) options = addOption(options, "Text section is not first"); // If IAT (Import Address Table) is missing or empty var isIatMissing = false, isEmptyIatPresent = false; if (PE_Cached.indexOfImportsSection === -1 && !PE_Cached.isDynamicLinkLibrary) { // DLLs can be resource-only log(logType.any, "Import table is missing"); isIatMissing = true; } else if (PE_Cached.indexOfImportsSection !== -1 && !PE_Cached.numberOfUnmanagedImports) { log(logType.any, "Import table is present but empty"); isEmptyIatPresent = true; } if (isIatMissing) options = addOption(options, "No IAT"); else if (isEmptyIatPresent) options = addOption(options, "Empty IAT"); // If both IAT and EAT are missing (for DLLs only) var isDllIatAndEatMissing = false; // An explicit loader entry point distinguishes executable DLL code from a resource-only image. if (PE_Cached.isDynamicLinkLibrary && PE_Cached.hasUnmanagedEntryPoint && PE_Cached.indexOfImportsSection === -1 && PE_Cached.indexOfExportsSection === -1) { log(logType.any, "DLL import and export tables are missing"); isDllIatAndEatMissing = true; } if (isDllIatAndEatMissing) options = addOption(options, "No IAT and EAT"); // If EAT (Export Address Table) is missing or empty (for DLLs only) var isDllEatMissing = false, isDllEmptyEatPresent = false; if (!isDllIatAndEatMissing && PE_Cached.isDynamicLinkLibrary) { if (PE_Cached.indexOfExportsSection === -1 && PE_Cached.hasUnmanagedEntryPoint) { // DLL without exports and with EP (not a resource-only DLL) log(logType.any, "DLL export table is missing"); isDllEatMissing = true; } else if (PE_Cached.indexOfExportsSection !== -1 && PE_Cached.numberOfUnmanagedExports === 0) { // DLL with empty exports log(logType.any, "DLL export table is present but empty"); isDllEmptyEatPresent = true; } } if (isDllEatMissing) options = addOption(options, "No EAT"); else if (isDllEmptyEatPresent) options = addOption(options, "Empty EAT"); // If the file contains a phantom EAT (Export Address Table) - when the PE header indicates the presence of exports, but there are actually no export entries in the file. // This is a common trick to break some PE parsers and analyzers that rely on the presence of an EAT for certain functionalities. var isPhantomEatPresent = false; if (!PE_Cached.isDynamicLinkLibrary && PE_Cached.numberOfUnmanagedExports === 0 && PE_Cached.indexOfExportsSection !== -1) { log(logType.any, "Export directory has no export entries"); isPhantomEatPresent = true; } if (isPhantomEatPresent) options = addOption(options, "Phantom EAT"); // Check if the entry point starts with NOP or a NOP-like instruction var isStartsWithNop = false; if (PE_Cached.isArchX86) { /* * NOP-equivalent instructions for x86/x64: * * Standard NOP instructions: * - 0x90 -> NOP (standard 1-byte NOP) * - 0x66 0x90 -> XCHG AX, AX (2-byte NOP) * - 0x0F 0x1F 0x00 -> NOP DWORD PTR [EAX/RAX] (3-byte NOP) * - 0x0F 0x1F 0x40 0x00 -> NOP DWORD PTR [EAX/RAX+0x00] (4-byte NOP) * - 0x0F 0x1F 0x44 0x00 0x00 -> NOP DWORD PTR [EAX/RAX+EAX/RAX*1+0x00] (5-byte NOP) * - 0x66 0x0F 0x1F 0x44 0x00 0x00 -> NOP WORD PTR [EAX/RAX+EAX/RAX*1+0x00] (6-byte NOP) * - 0x0F 0x1F 0x80 0x00 0x00 0x00 0x00 -> NOP DWORD PTR [EAX/RAX+0x00000000] (7-byte NOP) * - 0x0F 0x1F 0x84 0x00 0x00 0x00 0x00 0x00 -> NOP DWORD PTR [EAX/RAX+EAX/RAX*1+0x00000000] (8-byte NOP) * - 0x66 0x0F 0x1F 0x84 0x00 0x00 0x00 0x00 0x00 -> NOP WORD PTR [EAX/RAX+EAX/RAX*1+0x00000000] (9-byte NOP) * * Functional equivalents of NOP: * - 0x89 0xF6 -> MOV ESI, ESI (x86) * - 0x48 0x89 0xF6 -> MOV RSI, RSI (x64) * - 0x8D 0x76 0x00 -> LEA ESI, [ESI+0x00] (x86) * - 0x48 0x8D 0x76 0x00 -> LEA RSI, [RSI+0x00] (x64) * - 0x8D 0x74 0x26 0x00 -> LEA ESI, [ESI+EIZ+0x00] (x86) * - 0x48 0x8D 0x74 0x26 0x00 -> LEA RSI, [RSI+RIZ+0x00] (x64) * - 0x87 0xDB -> XCHG EBX, EBX (x86) * - 0x48 0x87 0xDB -> XCHG RBX, RBX (x64) * - 0x87 0xC9 -> XCHG ECX, ECX (x86) * - 0x48 0x87 0xC9 -> XCHG RCX, RCX (x64) * - 0x87 0xD2 -> XCHG EDX, EDX (x86) * - 0x48 0x87 0xD2 -> XCHG RDX, RDX (x64) * - 0x87 0xFF -> XCHG EDI, EDI (x86) * - 0x48 0x87 0xFF -> XCHG RDI, RDI (x64) * - 0x87 0xC0 -> XCHG EAX, EAX (x86) * - 0x48 0x87 0xC0 -> XCHG RAX, RAX (x64) * - 0x8D 0x00 -> LEA EAX, [EAX] (x86) * - 0x48 0x8D 0x00 -> LEA RAX, [RAX] (x64) */ // Universal NOP equivalents (work on both architectures) const nopEquivalentsUniversal = [ "XCHG AX, AX", "XCHG BX, BX", "XCHG CX, CX", "XCHG DX, DX", "XCHG SI, SI", "XCHG DI, DI", "XCHG BP, BP" ]; // Collections of NOP-equivalent instructions based on architecture const nopEquivalents32bit = [ // x86 "MOV ESI, ESI", "XCHG EAX, EAX", "XCHG EBX, EBX", "XCHG ECX, ECX", "XCHG EDX, EDX", "XCHG EDI, EDI", "LEA EAX, [EAX]", "LEA ESI, [ESI]", "LEA ESI, CS:[ESI]" ]; const nopEquivalents64bit = [ // x64 "MOV RSI, RSI", "XCHG RAX, RAX", "XCHG RBX, RBX", "XCHG RCX, RCX", "XCHG RDX, RDX", "XCHG RDI, RDI", "LEA RAX, [RAX]", "LEA RSI, [RSI]", "LEA RSI, CS:[RSI]" ]; // Combine universal and architecture-specific instructions const nopEquivalents = nopEquivalentsUniversal.concat( PE_Cached.is64bit ? nopEquivalents64bit : nopEquivalents32bit ); // First check for standard NOP opcode which covers all NOP variants with different operands if (getFirstEpAsmOpCode() === "NOP" || getFirstEpAsmOpCode() === "FNOP") { isStartsWithNop = true; } else { // Then check for all functional NOP equivalents that have different opcodes for (var i = 0; i < nopEquivalents.length && !isStartsWithNop; i++) { if (PE_Cached.firstEpAsmInstruction === nopEquivalents[i]) { isStartsWithNop = true; } } } } else if (PE_Cached.isARM) { /* * NOP-equivalent instructions for ARM/A32: * Standard NOP instructions: * - 0x00 0xF0 0x20 0xE3 -> NOP (ARM32 explicit) * * Functional equivalents of NOP: * - 0x00 0x00 0xA0 0xE1 -> MOV R0, R0 (ARM32) * - 0x01 0x10 0xA0 0xE1 -> MOV R1, R1 (ARM32) * - 0x02 0x20 0xA0 0xE1 -> MOV R2, R2 (ARM32) * - 0x03 0x30 0xA0 0xE1 -> MOV R3, R3 (ARM32) * - 0x04 0x40 0xA0 0xE1 -> MOV R4, R4 (ARM32) * - 0x05 0x50 0xA0 0xE1 -> MOV R5, R5 (ARM32) * - 0x06 0x60 0xA0 0xE1 -> MOV R6, R6 (ARM32) * - 0x07 0x70 0xA0 0xE1 -> MOV R7, R7 (ARM32) * - 0x08 0x80 0xA0 0xE1 -> MOV R8, R8 (ARM32) * - 0x09 0x90 0xA0 0xE1 -> MOV R9, R9 (ARM32) * - 0x0A 0xA0 0xA0 0xE1 -> MOV R10, R10 (ARM32) * - 0x0B 0xB0 0xA0 0xE1 -> MOV R11, R11 (ARM32) * - 0x0C 0xC0 0xA0 0xE1 -> MOV R12, R12 (ARM32) */ // MOV Rn, Rn is a functional NOP for every A32 general-purpose register. isStartsWithNop = getFirstEpAsmOpCode() === "NOP" || /^MOV R(\d+), R\1$/.test(PE_Cached.firstEpAsmInstruction); } else if (PE_Cached.isThumb) { // Thumb/Thumb-2 also supports MOV Rn, Rn as a functional NOP. isStartsWithNop = getFirstEpAsmOpCode() === "NOP" || /^MOV R(\d+), R\1$/.test(PE_Cached.firstEpAsmInstruction); } else if (PE_Cached.isARM64) { /* * NOP-equivalent instructions for ARM64/AArch64: * - 0x1F 0x20 0x03 0xD5 -> NOP * - 0xE0 0x03 0x00 0xAA -> MOV X0, X0 * - 0xE1 0x03 0x01 0xAA -> MOV X1, X1 * - 0xE2 0x03 0x02 0xAA -> MOV X2, X2 * - 0xE3 0x03 0x03 0xAA -> MOV X3, X3 * - 0xE0 0x03 0x00 0x2A -> MOV W0, W0 * - 0xE1 0x03 0x01 0x2A -> MOV W1, W1 * - 0xE2 0x03 0x02 0x2A -> MOV W2, W2 * - 0xE3 0x03 0x03 0x2A -> MOV W3, W3 */ // MOV Xn, Xn and MOV Wn, Wn are functional NOPs for all integer registers. isStartsWithNop = getFirstEpAsmOpCode() === "NOP" || /^MOV ([XW])(\d+), \1\2$/.test(PE_Cached.firstEpAsmInstruction); } if (isStartsWithNop) { log(logType.emulation, "Instruction with no effect at EP: " + PE_Cached.firstEpAsmInstruction); } if (isStartsWithNop) options = addOption(options, "Nop at EP"); // Check for NOP instructions mixed with real instructions near the entry point var isNopPaddingPresent = false; if (!isStartsWithNop && (PE_Cached.isArchX86 || PE_Cached.isARM || PE_Cached.isThumb || PE_Cached.isARM64)) { var internalNopCount = 0, pendingNopCount = 0, maxInternalNopRun = 0, isRealInstructionSeen = false, // Keeping both x86 and ARM branch mnemonics here lets the counter remain // architecture-neutral while respecting every terminal control transfer. nopPaddingStopOpCodes = [ "B", "BL", "BLR", "BR", "BRK", "BX", "CALL", "CBNZ", "CBZ", "ERET", "HLT", "HVC", "INT", "INT1", "INT3", "INTO", "IRET", "IRETD", "IRETQ", "JMP", "JMPF", "LOOP", "LOOPE", "LOOPNE", "LOOPNZ", "LOOPZ", "RET", "RETF", "RETN", "SMC", "SVC", "SYSCALL", "SYSENTER", "SYSRET", "SYSEXIT", "TBNZ", "TBZ", "UD0", "UD1", "UD2" ]; // Inspect at most the first 64 instructions and do not cross a basic-block boundary. // A pending run is counted only after another real instruction confirms it is internal. for (var i = 0; i < 64 && !isNopPaddingPresent; i++) { var currentInstruction = emulateAndGetAsmInstructionByIndex(i), currentOpCode = getAsmOpCode(currentInstruction); if (!currentOpCode) break; if (currentOpCode === "NOP" || currentOpCode === "FNOP") { if (isRealInstructionSeen) pendingNopCount++; } else { if (pendingNopCount) { internalNopCount += pendingNopCount; if (pendingNopCount > maxInternalNopRun) maxInternalNopRun = pendingNopCount; pendingNopCount = 0; // Two adjacent internal NOPs or three interspersed NOPs are unlikely compiler output. if (maxInternalNopRun >= 2 || internalNopCount >= 3) { log(logType.emulation, "Internal NOP padding near EP: " + internalNopCount + " instruction(s)"); isNopPaddingPresent = true; } } isRealInstructionSeen = true; if (nopPaddingStopOpCodes.indexOf(currentOpCode) !== -1 || /^J[A-Z]+$/.test(currentOpCode) || /^B(?:\.)?(?:EQ|NE|CS|HS|CC|LO|MI|PL|VS|VC|HI|LS|GE|LT|GT|LE)$/.test(currentOpCode)) { break; } } } } if (isNopPaddingPresent) options = addOption(options, "Nop padding near EP"); // Check for "Stack-push address near EP" (CALL $+5) var isAddrToStackTrickPresent = false; if (PE_Cached.isArchX86) { // Check first 30 instructions for "Stack-push address near EP" // This trick uses "CALL $+5" (E8 00 00 00 00) to push next instruction address on stack if (PE_Cached.isEntryPointAnalysisAllowed) { var addrToStackFallthrough = {}; for (var i = 0; i < 32 && !isAddrToStackTrickPresent; i++) { var currentInstruction = emulateAndGetAsmInstructionByIndex(i), addrToStackOpCode = getAsmOpCode(currentInstruction), disasmAddress = epAsmInstructionAddresses[i]; if (!currentInstruction) break; // Safety check // Check if instruction is CALL if (addrToStackOpCode === "CALL") { var currentOffset = PE.VAToOffset(disasmAddress); // Check if machine code matches E8 00 00 00 00 pattern if (currentOffset !== -1 && PE.compare("E8 00 00 00 00", currentOffset)) { log(logType.emulation, "Stack-push address near EP found at offset 0x" + currentOffset.toString(16)); isAddrToStackTrickPresent = true; } if (!isAddrToStackTrickPresent && !isEpDirectCallReturnProven(disasmAddress)) break; } // Do not inspect bytes which cannot execute after a hard control-flow transfer. if (/^(?:HLT|INT(?:1|3|O)?|IRET(?:D|Q)?|JMPF?|LOOP(?:E|NE|NZ|Z)?|RET(?:F|N)?|SYS(?:CALL|ENTER|RET|EXIT)|UD[012])$/.test(addrToStackOpCode)) break; if (isEpLinearFallthroughUnproven(currentInstruction, disasmAddress, addrToStackFallthrough)) break; } } } if (isAddrToStackTrickPresent) options = addOption(options, "Stack-push address near EP"); // The following patterns model x86 register semantics and are consumed only by rules // guarded with isArchX86. EAX/R8D aliases are later canonicalized to RAX/R8 so that // writes through different widths invalidate the same physical register provenance. const epGeneralPurposeRegister = "(?:E(?:AX|BX|CX|DX|SI|DI|BP)|R(?:AX|BX|CX|DX|SI|DI|BP|8D?|9D?|1[0-5]D?))", epFlagsGeneralPurposeRegister = "(?:" + epGeneralPurposeRegister + "|(?:AX|BX|CX|DX|SI|DI|BP)|R(?:8|9|1[0-5])W)", // MOV EDI, EDI is excluded only in I386 because it is the conventional hotpatch // prologue. In PE32+, every 32-bit self-move may intentionally zero the upper half. epRedundantSelfMoveRegister = PE_Cached.isI386 ? "(?:E(?:AX|BX|CX|DX|SI|BP)|(?:AX|BX|CX|DX|SI|DI|BP))" : "(?:(?:AX|BX|CX|DX|SI|DI|BP)|R(?:(?:AX|BX|CX|DX|SI|DI|BP)|(?:8|9|1[0-5])W?))", epRegister32BitPattern = /^(?:E(?:AX|BX|CX|DX|SI|DI|BP)|R(?:8|9|1[0-5])D)$/i, epRegisterCopyPattern = new RegExp("^MOV (" + epGeneralPurposeRegister + "), (" + epGeneralPurposeRegister + ")$", "i"), epZeroRegisterPattern = new RegExp("^(?:(?:XOR|SUB) (" + epGeneralPurposeRegister + "), \\1|AND (" + epGeneralPurposeRegister + "), (?:0X0+|0+))$", "i"), epRedundantSelfMovePattern = new RegExp("^MOV (" + epRedundantSelfMoveRegister + "), \\1$", "i"), // Capture 1 belongs to MOV/XCHG and capture 2 to LEA. Consumers deliberately use // match[1] || match[2] to obtain the affected register from either alternative. epSimplePolymorphicInstructionPattern = new RegExp("^(?:MOV|XCHG) (" + epFlagsGeneralPurposeRegister + "), \\1$|^LEA (" + epGeneralPurposeRegister + "), \\[\\2\\]$", "i"), // These operations preserve the destination value while permitting an obfuscator // to vary both instruction spelling and EFLAGS side effects. epValuePreservingInstructionPattern = new RegExp("^(?:ADD|SUB|OR|XOR|SHL|SHR|SAR|ROL|ROR) (" + epFlagsGeneralPurposeRegister + "), (?:0X0+|0+)$|^IMUL (" + epGeneralPurposeRegister + "), \\2, (?:0X0*1|1)$", "i"), epConditionalBranchPattern = /^(J(?!MP)[A-Z]+) (0X[0-9A-F]+|\d+)$/i, epDirectJumpPattern = /^JMP (0X[0-9A-F]+|\d+)$/i, epStackRoundTripPushPattern = new RegExp("^PUSH (" + epFlagsGeneralPurposeRegister + ")$", "i"), epStackRoundTripPopPattern = new RegExp("^POP (" + epFlagsGeneralPurposeRegister + ")$", "i"), epReversibleBinaryPattern = new RegExp("^(XOR|ADD|SUB) (" + epFlagsGeneralPurposeRegister + "), (0X[0-9A-F]+|\\d+)$", "i"), epReversibleUnaryPattern = new RegExp("^(NOT|NEG|BSWAP) (" + epFlagsGeneralPurposeRegister + ")$", "i"), epLiteralMovePattern = new RegExp("^MOV(?:ABS)? (" + epGeneralPurposeRegister + "), (0X[0-9A-F]+|\\d+)$", "i"), epLiteralComparePattern = new RegExp("^CMP (" + epGeneralPurposeRegister + "), (0X[0-9A-F]+|\\d+)$", "i"), epFlagsNeutralRegisterWritePattern = new RegExp("^(?:MOV(?:ABS)?|MOVZX|MOVSX|MOVSXD|LEA) (" + epGeneralPurposeRegister + ")(?:,|$)", "i"), epTrackedRegisterWritePattern = new RegExp("^(?:XOR|ADD|SUB|ADC|SBB|AND|OR|NOT|NEG|INC|DEC|POP) (" + epGeneralPurposeRegister + ")(?:,|$)", "i"), epPartialRegisterWritePattern = /^(?:MOV|XOR|ADD|SUB|AND|OR|NOT|NEG|INC|DEC) ((?:[ABCD][HL]|[ABCD]X|SI|DI|BP|SIL|DIL|BPL|R(?:8|9|1[0-5])(?:B|W)))(?:,|$)/i, epNeutralInstructionLimit = 4, epMaximumSafeInteger = 0x1FFFFFFFFFFFFF, epComplementaryBranchPairs = [ "JA:JBE", "JAE:JB", "JE:JNE", "JG:JLE", "JGE:JL", "JO:JNO", "JP:JNP", "JS:JNS" ], epCanonicalBranchOpCodes = { "JC": "JB", "JNA": "JBE", "JNAE": "JB", "JNB": "JAE", "JNBE": "JA", "JNC": "JAE", "JNG": "JLE", "JNGE": "JL", "JNL": "JGE", "JNLE": "JG", "JNZ": "JNE", "JPE": "JP", "JPO": "JNP", "JZ": "JE" }, epZeroFlagsAlwaysTakenBranches = [ "JAE", "JNB", "JNC", "JBE", "JNA", "JE", "JZ", "JGE", "JNL", "JLE", "JNG", "JNO", "JNS", "JP", "JPE" ], epZeroFlagsNeverTakenBranches = [ "JA", "JNBE", "JB", "JNAE", "JC", "JG", "JNLE", "JL", "JNGE", "JNE", "JNZ", "JO", "JNP", "JPO", "JS" ], epScanStopOpCodes = [ "CALL", "HLT", "INT", "INT1", "INT3", "INTO", "IRET", "IRETD", "IRETQ", "JMP", "JMPF", "LOOP", "LOOPE", "LOOPNE", "LOOPNZ", "LOOPZ", "RET", "RETF", "RETN", "SYSCALL", "SYSENTER", "SYSRET", "SYSEXIT", "UD0", "UD1", "UD2" ]; // Check for a non-standard entry-point transfer through RET. Normal PE startup code // does not return to a caller, while protected stubs use RET to conceal a direct jump. // Supported variants: direct VA, a VA propagated through registers, and an x86/x64 stack pivot. var isSyntheticEpTransferPresent = false; var isStackPivotEpTransferPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86) { // Track only statically proven image addresses. Unknown instructions invalidate this // lightweight state, preventing a generic PUSH register; RET sequence from matching. var syntheticEpTargetAddress = String(), syntheticEpTargetType = String(), syntheticEpNeutralInstructionCount = 0, // RET consumes one native pointer. A partial DWORD store in PE32+ cannot prove // the resulting 64-bit return address and must therefore never become a target. syntheticEpStackValueSize = PE_Cached.isI386 ? "DWORD" : "QWORD", syntheticEpStackPointer = PE_Cached.isI386 ? "ESP" : "RSP", syntheticEpStackTransferRegister = PE_Cached.isI386 ? "E(?:AX|BX|CX|DX|SI|DI|BP)" : "R(?:AX|BX|CX|DX|SI|DI|BP|8|9|1[0-5])", // This is a plain object: its dynamically added properties are configurable, // so delete object[key] is valid in strict mode. syntheticEpKnownAddresses = {}, // Compile invariant operand forms once; the state loop consumes the same // architecture-specific grammar for every decoded instruction. syntheticEpRegisterLiteralAddressPattern = new RegExp("^MOV(?:ABS)? (" + epGeneralPurposeRegister + "), (0X[0-9A-F]+|\\d+)$", "i"), syntheticEpPushedRegisterPattern = new RegExp("^PUSH (" + epGeneralPurposeRegister + ")$", "i"), syntheticEpStoredRegisterPattern = new RegExp("^MOV " + syntheticEpStackValueSize + " PTR \\[" + syntheticEpStackPointer + "\\], (" + syntheticEpStackTransferRegister + ")$", "i"), syntheticEpPoppedRegisterPattern = new RegExp("^POP (" + epGeneralPurposeRegister + ")$", "i"), syntheticEpAddressArithmeticPattern = new RegExp("^(ADD|SUB) (" + syntheticEpStackTransferRegister + "), (0X[0-9A-F]+|[0-9]+)$", "i"), syntheticEpExchangedRegisterPattern = new RegExp("^XCHG " + syntheticEpStackValueSize + " PTR \\[" + syntheticEpStackPointer + "\\], (" + syntheticEpStackTransferRegister + ")$", "i"), syntheticEpStoredAddressPattern = new RegExp("^MOV " + syntheticEpStackValueSize + " PTR \\[" + syntheticEpStackPointer + "\\], (0X[0-9A-F]+|\\d+)$", "i"), syntheticEpStackArithmeticPattern = new RegExp("^(ADD|SUB) " + syntheticEpStackValueSize + " PTR \\[" + syntheticEpStackPointer + "\\], (-?(?:0X[0-9A-F]+|\\d+))$", "i"), // A dword SP assignment zero-extends in AMD64 and replaces the same native stack. // Scaled/indexed LEA forms are excluded because their result is not tracked. syntheticEpStackPivotLiteralPattern = new RegExp("^MOV(?:ABS)? " + (PE_Cached.isI386 ? "ESP" : "(?:RSP|ESP)") + ", (0X[0-9A-F]+|\\d+)$", "i"), syntheticEpStackPivotRegisterPattern = new RegExp("^(?:MOV|XCHG) " + syntheticEpStackPointer + ", (" + syntheticEpStackTransferRegister + ")$|^LEA " + syntheticEpStackPointer + ", \\[(" + syntheticEpStackTransferRegister + ")\\]$", "i"), syntheticEpMemoryWritePattern = /^(?:MOV|XCHG|ADD|SUB|ADC|SBB|AND|OR|XOR|INC|DEC|NEG|NOT|SHL|SHR|SAR|ROL|ROR|POP|CMPXCHG|XADD) (BYTE|WORD|DWORD|QWORD) PTR (?:[A-Z]+:)?\[([^\]]+)\]/i; // Two related states are tracked independently: known register VAs and the value // currently proven to occupy the top stack slot. RET matches only the latter. // Branches fork independent register and stack proofs; no facts merge at a join. // Direct jumps retain one state; only feasible Jcc successors consume the shared budget. var syntheticEpPendingPaths = [{ ZeroFlag: -1, CarryFlag: -1, StackPointerZero: false, Address: -1, TargetAddress: String(), TargetType: String(), KnownAddresses: {}, MemoryWrites: [], UnknownMemoryWrite: false, NeutralCount: 0, Visited: {} }], syntheticEpInstructionBudget = 0; for (var pathIndex = 0; pathIndex < syntheticEpPendingPaths.length && pathIndex < 8 && !isSyntheticEpTransferPresent; pathIndex++) { var syntheticEpPath = syntheticEpPendingPaths[pathIndex], syntheticEpFollowAddress = syntheticEpPath.Address, syntheticEpVisitedAddresses = syntheticEpPath.Visited; syntheticEpTargetAddress = syntheticEpPath.TargetAddress; syntheticEpTargetType = syntheticEpPath.TargetType; syntheticEpKnownAddresses = syntheticEpPath.KnownAddresses; var syntheticEpMemoryWrites = syntheticEpPath.MemoryWrites, syntheticEpUnknownMemoryWrite = syntheticEpPath.UnknownMemoryWrite; syntheticEpNeutralInstructionCount = syntheticEpPath.NeutralCount; var syntheticEpZeroFlag = syntheticEpPath.ZeroFlag, syntheticEpCarryFlag = syntheticEpPath.CarryFlag, syntheticEpStackPointerZero = syntheticEpPath.StackPointerZero; for (var i = 0; i < 512 && syntheticEpInstructionBudget < 512 && !isSyntheticEpTransferPresent; i++) { var currentEpInstruction = syntheticEpFollowAddress === -1 ? emulateAndGetAsmInstructionByIndex(i) : PE.getDisasmString(syntheticEpFollowAddress), currentEpAddress = syntheticEpFollowAddress === -1 ? epAsmInstructionAddresses[i] : syntheticEpFollowAddress, currentEpOpCode = getAsmOpCode(currentEpInstruction), currentEpLength = syntheticEpFollowAddress === -1 ? 0 : PE.getDisasmLength(currentEpAddress); if (!currentEpOpCode || currentEpOpCode === "DB" || syntheticEpVisitedAddresses[currentEpAddress] || (syntheticEpFollowAddress !== -1 && (currentEpLength <= 0 || !isVirtualAddressRangeFileBacked(currentEpAddress, currentEpLength, SECTION_FLAGS_EXECUTE)))) break; if (/^AAM (?:0X0+|0+)$/i.test(currentEpInstruction) || isEpProvenFaultingInstruction(currentEpInstruction, currentEpOpCode, syntheticEpStackPointerZero, syntheticEpKnownAddresses)) break; syntheticEpStackPointerZero = advanceEpStackPointerZero(currentEpInstruction, currentEpOpCode, syntheticEpStackPointerZero, syntheticEpKnownAddresses); syntheticEpVisitedAddresses[currentEpAddress] = true; syntheticEpInstructionBudget++; // Each path carries only directly proven flag facts; unknown outcomes still fork. syntheticEpZeroFlag = advanceEpZeroFlag(currentEpInstruction, currentEpOpCode, syntheticEpZeroFlag); syntheticEpCarryFlag = advanceEpCarryFlag(currentEpInstruction, currentEpOpCode, syntheticEpCarryFlag); // Extract only register forms whose value can be followed without emulation. var registerLiteralAddress = currentEpInstruction.match(syntheticEpRegisterLiteralAddressPattern), zeroRegister = currentEpInstruction.match(epZeroRegisterPattern), registerCopy = currentEpInstruction.match(epRegisterCopyPattern), pushedRegister = currentEpInstruction.match(syntheticEpPushedRegisterPattern), storedRegister = currentEpInstruction.match(syntheticEpStoredRegisterPattern), poppedRegister = currentEpInstruction.match(syntheticEpPoppedRegisterPattern), exchangedRegister = currentEpInstruction.match(syntheticEpExchangedRegisterPattern), pushedAddress = currentEpInstruction.match(/^PUSH (0X[0-9A-F]+|\d+)$/i), storedAddress = currentEpInstruction.match(syntheticEpStoredAddressPattern), stackArithmetic = currentEpInstruction.match(syntheticEpStackArithmeticPattern), stackPivotLiteral = currentEpInstruction.match(syntheticEpStackPivotLiteralPattern), stackPivotRegister = currentEpInstruction.match(syntheticEpStackPivotRegisterPattern), targetAddress = pushedAddress || storedAddress, targetAddressValue = targetAddress ? parseInt(targetAddress[1], 0) : -1, registerWrite = currentEpInstruction.match(epFlagsNeutralRegisterWritePattern) || currentEpInstruction.match(epTrackedRegisterWritePattern), conditionalBranch = currentEpInstruction.match(epConditionalBranchPattern), simplePolymorphicInstruction = currentEpInstruction.match(epSimplePolymorphicInstructionPattern), simplePolymorphicRegister = simplePolymorphicInstruction ? (simplePolymorphicInstruction[1] || simplePolymorphicInstruction[2]) : String(), valuePreservingInstruction = currentEpInstruction.match(epValuePreservingInstructionPattern), partialRegisterWrite = currentEpInstruction.match(epPartialRegisterWritePattern), isNeutralInstruction = currentEpOpCode === "NOP" || currentEpOpCode === "FNOP" || currentEpOpCode === "PAUSE" || currentEpOpCode === "CLC" || currentEpOpCode === "STC" || currentEpOpCode === "CMC" || (simplePolymorphicInstruction && !(PE_Cached.is64bit && epRegister32BitPattern.test(simplePolymorphicRegister))); // Track writes which might alter a later pivot's on-disk return pointer. // An unknown destination fails closed without discarding unrelated facts. var memoryWrite = currentEpInstruction.match(syntheticEpMemoryWritePattern); if (memoryWrite) { var writeOperand = memoryWrite[2], writeAddress = -1, writeSize = memoryWrite[1] === "BYTE" ? 1 : memoryWrite[1] === "WORD" ? 2 : memoryWrite[1] === "DWORD" ? 4 : 8, absoluteWrite = writeOperand.match(/^(0X[0-9A-F]+|\d+)$/i), ripWrite = writeOperand.match(/^RIP ([+-]) (0X[0-9A-F]+|\d+)$/i), registerWriteAddress = writeOperand.match(new RegExp("^(" + epGeneralPurposeRegister + ")(?: ([+-]) (0X[0-9A-F]+|\\d+))?$", "i")); if (absoluteWrite) writeAddress = parseInt(absoluteWrite[1], 0); else if (ripWrite) { var ripWriteLength = currentEpLength || PE.getDisasmLength(currentEpAddress); if (ripWriteLength > 0) writeAddress = currentEpAddress + ripWriteLength + parseInt(ripWrite[2], 0) * (ripWrite[1] === "-" ? -1 : 1); } else if (registerWriteAddress) { var writeBase = syntheticEpKnownAddresses[getNativeRegisterKey(registerWriteAddress[1])]; if (writeBase !== undefined) writeAddress = parseInt(writeBase, 0) + (registerWriteAddress[3] ? parseInt(registerWriteAddress[3], 0) * (registerWriteAddress[2] === "-" ? -1 : 1) : 0); } if (writeAddress < 0 || writeAddress > epMaximumSafeInteger || !isFinite(writeAddress) || syntheticEpMemoryWrites.length >= 16) syntheticEpUnknownMemoryWrite = true; else syntheticEpMemoryWrites.push({ Start: writeAddress, End: writeAddress + writeSize }); } var implicitWrite = currentEpInstruction.match(/^(REP(?:E|NE)? )?(?:STOS|MOVS|INS)(B|W|D|Q)(?: |$)/i); if (implicitWrite) { var implicitAddress = syntheticEpKnownAddresses.RDI, implicitSize = implicitWrite[2] === "B" ? 1 : implicitWrite[2] === "W" ? 2 : implicitWrite[2] === "D" ? 4 : 8; if (implicitWrite[1] || implicitAddress === undefined || syntheticEpMemoryWrites.length >= 16) syntheticEpUnknownMemoryWrite = true; else syntheticEpMemoryWrites.push({ Start: parseInt(implicitAddress, 0), End: parseInt(implicitAddress, 0) + implicitSize }); } if (stackPivotLiteral || stackPivotRegister) { // A literal or register-propagated pivot is accepted only when the new stack // is writable, physically backed, and already contains an executable image VA. // Normalize subregister spelling to the physical register key used by the // existing address-provenance map; no value is inferred from an unknown key. var stackPivotRegisterName = stackPivotRegister ? (stackPivotRegister[1] || stackPivotRegister[2]) : String(), stackPivotRegisterKey = getNativeRegisterKey(stackPivotRegisterName), stackPivotAddress = stackPivotLiteral ? parseInt(stackPivotLiteral[1], 0) : parseInt(syntheticEpKnownAddresses[stackPivotRegisterKey], 0), stackPivotPointerSize = PE_Cached.isI386 ? 4 : 8, stackOffset = isVirtualAddressRangeFileBacked(stackPivotAddress, stackPivotPointerSize, SECTION_FLAGS_WRITE) ? PE.VAToOffset(stackPivotAddress) : -1, returnAddress = stackOffset === -1 ? -1 : (PE_Cached.isI386 ? PE.read_uint32(stackOffset) : readUnsignedQword(stackOffset)); if (syntheticEpUnknownMemoryWrite) break; for (var writeIndex = 0; writeIndex < syntheticEpMemoryWrites.length; writeIndex++) { var writtenRange = syntheticEpMemoryWrites[writeIndex]; if (writtenRange.Start < stackPivotAddress + stackPivotPointerSize && writtenRange.End > stackPivotAddress) break; } if (writeIndex !== syntheticEpMemoryWrites.length) break; // XCHG puts the old, untracked SP into its source register; the pivot address no longer belongs to it. if (stackPivotRegister && currentEpOpCode === "XCHG") delete syntheticEpKnownAddresses[stackPivotRegisterKey]; // Replacing SP invalidates every earlier top-of-stack fact. The new RET target // is trusted only when its full pointer is present in writable image storage. syntheticEpTargetAddress = String(); syntheticEpTargetType = String(); if (returnAddress !== -1 && isVirtualAddressRangeFileBacked(returnAddress, 1, SECTION_FLAGS_EXECUTE)) { // Requiring a file-backed executable destination prevents ordinary stack // setup, heap stacks and unresolved runtime addresses from becoming hits. syntheticEpTargetAddress = returnAddress; syntheticEpTargetType = "stack pivot"; syntheticEpNeutralInstructionCount = 0; } else { // An unresolved stack replacement invalidates every later linear stack fact. break; } } else if (targetAddress && (PE_Cached.isI386 || targetAddressValue <= 0x7FFFFFFF) && (targetAddressValue === 0 || PE.VAToOffset(targetAddressValue) !== -1)) { // A direct PUSH or a native-width [SP] write proves the next RET target. // AMD64 encodes both forms with a sign-extended imm32, therefore literals // above INT32_MAX cannot denote the unsigned address printed in the operand. syntheticEpTargetAddress = targetAddressValue.toString(); syntheticEpTargetType = pushedAddress ? "push" : "stack write"; syntheticEpNeutralInstructionCount = 0; } else if (stackArithmetic && syntheticEpTargetType) { var stackDelta = parseInt(stackArithmetic[2], 0), stackTarget = parseInt(syntheticEpTargetAddress, 0); // DiE prints sign-extended QWORD immediates as signed literals; DWORD targets still wrap. if (stackDelta >= -0x80000000 && stackDelta <= (PE_Cached.isI386 ? 0xFFFFFFFF : 0x7FFFFFFF)) { stackTarget += stackArithmetic[1] === "ADD" ? stackDelta : -stackDelta; if (PE_Cached.isI386) stackTarget >>>= 0; } else { stackTarget = -1; } syntheticEpTargetAddress = stackTarget === 0 || (stackTarget >= 0 && stackTarget <= epMaximumSafeInteger && isVirtualAddressRangeFileBacked(stackTarget, 1, SECTION_FLAGS_EXECUTE)) ? stackTarget.toString() : String(); syntheticEpTargetType = syntheticEpTargetAddress ? "stack arithmetic" : String(); syntheticEpNeutralInstructionCount = 0; } else if (registerLiteralAddress && (parseInt(registerLiteralAddress[2], 0) === 0 || PE.VAToOffset(parseInt(registerLiteralAddress[2], 0)) !== -1)) { // Retain mapped targets and exact NULL facts; a NULL can reject a later mandatory read, never prove a transfer. syntheticEpKnownAddresses[getNativeRegisterKey(registerLiteralAddress[1])] = parseInt(registerLiteralAddress[2], 0); syntheticEpNeutralInstructionCount = 0; } else if (zeroRegister) { syntheticEpKnownAddresses[getNativeRegisterKey(zeroRegister[1] || zeroRegister[2])] = 0; syntheticEpNeutralInstructionCount = 0; } else if (registerCopy && syntheticEpKnownAddresses[getNativeRegisterKey(registerCopy[2])] !== undefined) { // Preserve provenance through a plain register-to-register copy. A 32-bit // destination zero-extends the truncated value, so validate that VA again. const registerCopyDestination = getNativeRegisterKey(registerCopy[1]); var registerCopySourceAddress = syntheticEpKnownAddresses[getNativeRegisterKey(registerCopy[2])]; if (epRegister32BitPattern.test(registerCopy[1])) { registerCopySourceAddress = parseInt(registerCopySourceAddress, 0) >>> 0; } if (registerCopySourceAddress === 0 || PE.VAToOffset(parseInt(registerCopySourceAddress, 0)) !== -1) { syntheticEpKnownAddresses[registerCopyDestination] = registerCopySourceAddress; } else { delete syntheticEpKnownAddresses[registerCopyDestination]; } syntheticEpNeutralInstructionCount = 0; } else if (poppedRegister && syntheticEpTargetType) { // PUSH ; POP reg is a common polymorphic spelling of MOV reg, . // Moving the proof into a register also clears the top-of-stack proof, // because POP has consumed that slot. syntheticEpKnownAddresses[getNativeRegisterKey(poppedRegister[1])] = parseInt(syntheticEpTargetAddress, 0); syntheticEpTargetAddress = String(); syntheticEpTargetType = String(); syntheticEpNeutralInstructionCount = 0; } else if (pushedRegister && syntheticEpKnownAddresses[getNativeRegisterKey(pushedRegister[1])] !== undefined) { syntheticEpTargetAddress = syntheticEpKnownAddresses[getNativeRegisterKey(pushedRegister[1])]; syntheticEpTargetType = "register push"; syntheticEpNeutralInstructionCount = 0; } else if (storedRegister && syntheticEpKnownAddresses[getNativeRegisterKey(storedRegister[1])] !== undefined) { syntheticEpTargetAddress = syntheticEpKnownAddresses[getNativeRegisterKey(storedRegister[1])]; syntheticEpTargetType = "register stack write"; syntheticEpNeutralInstructionCount = 0; } else if (exchangedRegister && syntheticEpKnownAddresses[getNativeRegisterKey(exchangedRegister[1])] !== undefined) { // XCHG writes the proven target to the stack and loads an unknown old value into reg. syntheticEpTargetAddress = syntheticEpKnownAddresses[getNativeRegisterKey(exchangedRegister[1])]; syntheticEpTargetType = "register stack exchange"; delete syntheticEpKnownAddresses[getNativeRegisterKey(exchangedRegister[1])]; syntheticEpNeutralInstructionCount = 0; } else if (/^RET(?:N)?$/.test(currentEpOpCode) && syntheticEpTargetType !== "call return" && syntheticEpTargetAddress && isEpNativeNearTransfer(currentEpAddress, currentEpInstruction, currentEpLength || PE.getDisasmLength(currentEpAddress)) && isVirtualAddressRangeFileBacked(parseInt(syntheticEpTargetAddress, 0), 1, SECTION_FLAGS_EXECUTE)) { // RET at a process entry point is meaningful here only as a concealed transfer. if (syntheticEpTargetType === "stack pivot") { log(logType.emulation, "Stack pivot redirects Ret near EP to executable code at VA 0x" + parseInt(syntheticEpTargetAddress, 0).toString(16)); isStackPivotEpTransferPresent = true; } else { log(logType.emulation, "Synthetic " + syntheticEpTargetType + "/Ret transfer near EP to VA 0x" + parseInt(syntheticEpTargetAddress, 0).toString(16)); } isSyntheticEpTransferPresent = true; } else if (currentEpOpCode === "CALL") { var mutatedReturnTarget = getEpCallReturnMutationTarget(currentEpAddress), callNextOffset = PE.VAToOffset(currentEpAddress); if (callNextOffset !== -1 && isVirtualAddressRangeFileBacked(currentEpAddress, 5, SECTION_FLAGS_EXECUTE) && PE.compare("E8 00 00 00 00", callNextOffset)) { // CALL-next pushes its exact continuation; an unchanged return alone is not a synthetic transfer. syntheticEpTargetAddress = (currentEpAddress + 5).toString(); syntheticEpTargetType = "call return"; syntheticEpNeutralInstructionCount = 0; } else if (mutatedReturnTarget !== undefined) { log(logType.emulation, "Call return address redirects Ret near EP to executable code at VA 0x" + mutatedReturnTarget.toString(16)); isSyntheticEpTransferPresent = true; } else { break; } } else if (isNeutralInstruction && syntheticEpNeutralInstructionCount < epNeutralInstructionLimit) { // Permit limited stack-preserving junk commonly inserted by protectors. syntheticEpNeutralInstructionCount++; } else if (simplePolymorphicInstruction) { // A PE32+ r32 self-operation zero-extends only its physical register. // Preserve unrelated address proofs and any proven top-of-stack target. var simplePolymorphicRegisterKey = getNativeRegisterKey(simplePolymorphicInstruction[1] || simplePolymorphicInstruction[2]); if (syntheticEpKnownAddresses[simplePolymorphicRegisterKey] !== 0) delete syntheticEpKnownAddresses[simplePolymorphicRegisterKey]; } else if (valuePreservingInstruction) { var valuePreservingRegister = valuePreservingInstruction[1] || valuePreservingInstruction[2], valuePreservingRegisterKey = getNativeRegisterKey(valuePreservingRegister), valuePreservingAddress = syntheticEpKnownAddresses[valuePreservingRegisterKey]; if (valuePreservingAddress !== undefined && PE_Cached.is64bit && epRegister32BitPattern.test(valuePreservingRegister)) { valuePreservingAddress = parseInt(valuePreservingAddress, 0) >>> 0; if (valuePreservingAddress === 0 || PE.VAToOffset(parseInt(valuePreservingAddress, 0)) !== -1) { syntheticEpKnownAddresses[valuePreservingRegisterKey] = valuePreservingAddress; } else { delete syntheticEpKnownAddresses[valuePreservingRegisterKey]; } } } else if (/^(?:ADD|SUB) /.test(currentEpInstruction)) { var addressArithmetic = currentEpInstruction.match(syntheticEpAddressArithmeticPattern); if (!addressArithmetic) break; var addressRegisterKey = getNativeRegisterKey(addressArithmetic[2]), addressValue = syntheticEpKnownAddresses[addressRegisterKey], addressDelta = parseInt(addressArithmetic[3], 0); if (addressValue !== undefined && addressDelta <= (PE_Cached.isI386 ? 0xFFFFFFFF : 0x7FFFFFFF)) { addressValue = parseInt(addressValue, 0) + (addressArithmetic[1] === "ADD" ? addressDelta : -addressDelta); if (PE_Cached.isI386) addressValue >>>= 0; if (addressValue === 0 || (addressValue >= 0 && addressValue <= epMaximumSafeInteger && isVirtualAddressRangeFileBacked(addressValue, 1, SECTION_FLAGS_EXECUTE))) syntheticEpKnownAddresses[addressRegisterKey] = addressValue; else delete syntheticEpKnownAddresses[addressRegisterKey]; } else { delete syntheticEpKnownAddresses[addressRegisterKey]; } } else if (partialRegisterWrite) { delete syntheticEpKnownAddresses[getEpPartialRegisterKey(partialRegisterWrite[1])]; syntheticEpNeutralInstructionCount = 0; } else if (registerWrite) { // An unrelated register write cannot invalidate other proven addresses. // Remove only the overwritten register instead of discarding all provenance. delete syntheticEpKnownAddresses[getNativeRegisterKey(registerWrite[1])]; } else if (conditionalBranch) { var branchTarget = parseInt(conditionalBranch[2], 0), syntheticEpBranchResult = getEpKnownBranchResult(currentEpOpCode, syntheticEpZeroFlag, syntheticEpCarryFlag); // A taken word-sized branch truncates EIP, unlike the native VA printed by the decoder. if (syntheticEpBranchResult !== 0 && !isEpNativeNearTransfer(currentEpAddress, currentEpInstruction, currentEpLength || PE.getDisasmLength(currentEpAddress))) break; if (syntheticEpBranchResult !== 0 && syntheticEpPendingPaths.length < 8 && !syntheticEpVisitedAddresses[branchTarget] && isVirtualAddressRangeFileBacked(branchTarget, 1, SECTION_FLAGS_EXECUTE)) { var branchAddresses = {}, branchVisited = {}; for (var register in syntheticEpKnownAddresses) branchAddresses[register] = syntheticEpKnownAddresses[register]; for (var address in syntheticEpVisitedAddresses) branchVisited[address] = true; syntheticEpPendingPaths.push({ ZeroFlag: syntheticEpZeroFlag, CarryFlag: syntheticEpCarryFlag, StackPointerZero: syntheticEpStackPointerZero, Address: branchTarget, TargetAddress: syntheticEpTargetAddress, TargetType: syntheticEpTargetType, KnownAddresses: branchAddresses, MemoryWrites: syntheticEpMemoryWrites.slice(), UnknownMemoryWrite: syntheticEpUnknownMemoryWrite, NeutralCount: syntheticEpNeutralInstructionCount, Visited: branchVisited }); } // A proven taken branch has no executable physical fall-through. if (syntheticEpBranchResult === 1) break; } else if (/^(?:CMP|TEST)$/.test(currentEpOpCode)) { // Comparisons preserve all register and stack facts on this path. } else if (currentEpOpCode === "JMP") { var directJump = currentEpInstruction.match(epDirectJumpPattern), directTarget = directJump ? parseInt(directJump[1], 0) : -1; if (!directJump || !isEpNativeNearTransfer(currentEpAddress, currentEpInstruction, currentEpLength || PE.getDisasmLength(currentEpAddress)) || !isVirtualAddressRangeFileBacked(directTarget, 1, SECTION_FLAGS_EXECUTE)) break; syntheticEpFollowAddress = directTarget; continue; } else if (epScanStopOpCodes.indexOf(currentEpOpCode) !== -1) { break; } else { // Any other instruction can alter stack/register state, so discard provenance. syntheticEpTargetAddress = String(); syntheticEpTargetType = String(); syntheticEpNeutralInstructionCount = 0; syntheticEpKnownAddresses = {}; } if (syntheticEpFollowAddress !== -1) syntheticEpFollowAddress += currentEpLength; } } } var guardedEpEvidence = getEpGuardedFlowEvidence(); var isGuardedEpTransferPresent = guardedEpEvidence.ReturnTarget !== undefined; if (!isSyntheticEpTransferPresent && isGuardedEpTransferPresent) { log(logType.emulation, "Computed stack/Ret transfer near EP to VA 0x" + guardedEpEvidence.ReturnTarget.toString(16)); } if (isSyntheticEpTransferPresent || isGuardedEpTransferPresent) options = addOption(options, isStackPivotEpTransferPresent ? "Stack pivot near EP" : "Synthetic EP transfer"); // Check for an opaque conditional branch near the entry point. XOR/SUB/CMP reg, reg // and AND reg, 0 always produce CF=OF=SF=0 and ZF=PF=1, making the following Jcc constant. var isOpaqueEpBranchPresent = guardedEpEvidence.OpaqueBranchAddress !== -1; if (isOpaqueEpBranchPresent) { log(logType.emulation, "Arithmetic opaque branch near EP at VA 0x" + guardedEpEvidence.OpaqueBranchAddress.toString(16)); } if (PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86) { const opaqueEpAllOnesFlagsAlwaysTakenBranches = [ "JA", "JNBE", "JAE", "JNB", "JNC", "JNE", "JNZ", "JL", "JNGE", "JLE", "JNG", "JNO", "JS", "JP", "JPE" ], opaqueEpAllOnesFlagsNeverTakenBranches = [ "JB", "JNAE", "JC", "JBE", "JNA", "JE", "JZ", "JG", "JNLE", "JGE", "JNL", "JO", "JNS", "JNP", "JPO" ], opaqueEpCarryClearAlwaysTakenBranches = ["JAE", "JNB", "JNC"], opaqueEpCarryClearNeverTakenBranches = ["JB", "JNAE", "JC"], opaqueEpCarrySetAlwaysTakenBranches = ["JB", "JNAE", "JC"], opaqueEpCarrySetNeverTakenBranches = ["JAE", "JNB", "JNC"], // Match -1 only at the operand's complete width. A shorter mask still fixes // CF/OF but leaves SF dependent on the original value and cannot use this profile. opaqueEpAllOnesPattern = /^(?:OR ((?:AX|BX|CX|DX|SI|DI|BP|R(?:8|9|1[0-5])W)), (?:-1|0X0*FFFF|65535)|OR ((?:E(?:AX|BX|CX|DX|SI|DI|BP)|R(?:8|9|1[0-5])D)), (?:-1|0X0*FFFFFFFF|4294967295)|OR (R(?:AX|BX|CX|DX|SI|DI|BP|8|9|1[0-5])), (?:-1|0X0*(?:FFFFFFFF|FFFFFFFFFFFFFFFF)|4294967295|18446744073709551615))$/i, // These instruction forms are invariant across the entire fall-through scan. opaqueEpZeroFlagsPattern = new RegExp("^(?:(?:XOR|SUB|CMP) (" + epFlagsGeneralPurposeRegister + "), \\1|AND (" + epFlagsGeneralPurposeRegister + "), (?:0X0+|0+)|TEST (" + epFlagsGeneralPurposeRegister + "), (?:0X0+|0+))$", "i"), opaqueEpZeroLiteralPattern = new RegExp("^MOV (" + epGeneralPurposeRegister + "), (?:0X0+|0+)$", "i"), opaqueEpZeroTestPattern = new RegExp("^(?:TEST|OR) (" + epGeneralPurposeRegister + "), \\1$", "i"), opaqueEpRegisterWritePattern = new RegExp("^(?:MOV|MOVZX|MOVSX|LEA|XOR|ADD|SUB|ADC|SBB|AND|OR|NOT|NEG|INC|DEC|POP) (" + epGeneralPurposeRegister + ")(?:,|$)", "i"), opaqueEpSelfMovePattern = new RegExp("^XCHG (" + epGeneralPurposeRegister + "), \\1$", "i"); // Flags-preserving junk is allowed to make the check resilient to light polymorphism. var opaqueEpOriginInstruction = String(), opaqueEpOriginAddress = -1, opaqueEpAlwaysTakenBranches = [], opaqueEpNeverTakenBranches = [], opaqueEpNeutralInstructionCount = 0, // Plain-object entries are configurable, therefore strict-mode delete is safe. opaqueEpKnownValues = {}, opaqueEpFallthroughState = {}; // Zero tests use only exact zeroes; safe literals also prove zero-extending SP replacements. // Register values and the last deterministic EFLAGS producer remain independent; // a MOV may extend the first fact without replacing the flags it preserves. // Scan one physical fall-through path. The first control-flow boundary terminates // the proof so flags from one basic block cannot be paired with another block's Jcc. for (var i = 0; i < 512 && !isOpaqueEpBranchPresent; i++) { var currentEpInstruction = emulateAndGetAsmInstructionByIndex(i), currentEpOpCode = getAsmOpCode(currentEpInstruction); if (!currentEpOpCode) break; // Safety check if (isEpProvenFaultingInstruction(currentEpInstruction, currentEpOpCode, opaqueEpFallthroughState.StackPointerZero, opaqueEpKnownValues)) break; var opaqueEpNextStackPointerZero = advanceEpStackPointerZero(currentEpInstruction, currentEpOpCode, opaqueEpFallthroughState.StackPointerZero, opaqueEpKnownValues); // Split each instruction into flag producers, value-provenance operations, // candidate consumers, and harmless padding before updating either fact set. var zeroFlagsInstruction = currentEpInstruction.match(opaqueEpZeroFlagsPattern), allOnesFlagsInstruction = currentEpInstruction.match(opaqueEpAllOnesPattern), carryFlagsInstruction = currentEpInstruction.match(/^(CLC|STC)$/), zeroLiteralRegister = currentEpInstruction.match(opaqueEpZeroLiteralPattern), zeroTestRegister = currentEpInstruction.match(opaqueEpZeroTestPattern), conditionalBranch = currentEpInstruction.match(epConditionalBranchPattern), registerCopy = currentEpInstruction.match(epRegisterCopyPattern), flagsNeutralRegisterWrite = currentEpInstruction.match(epFlagsNeutralRegisterWritePattern), registerWrite = currentEpInstruction.match(opaqueEpRegisterWritePattern), partialRegisterWrite = currentEpInstruction.match(epPartialRegisterWritePattern), isNeutralInstruction = currentEpOpCode === "NOP" || currentEpOpCode === "FNOP" || currentEpOpCode === "PAUSE" || opaqueEpSelfMovePattern.test(currentEpInstruction); if (zeroFlagsInstruction) { // A zero result fixes CF, OF, SF, ZF, and PF regardless of the input value. opaqueEpOriginInstruction = currentEpInstruction; opaqueEpOriginAddress = epAsmInstructionAddresses[i]; opaqueEpAlwaysTakenBranches = epZeroFlagsAlwaysTakenBranches; opaqueEpNeverTakenBranches = epZeroFlagsNeverTakenBranches; opaqueEpNeutralInstructionCount = 0; if (currentEpOpCode !== "CMP" && currentEpOpCode !== "TEST") { // XOR/SUB reg,reg and AND reg,0 also prove the destination value is zero; // CMP reg,reg and TEST reg,0 prove only flags because they do not write. var zeroFlagsOperand = zeroFlagsInstruction[1] || zeroFlagsInstruction[2], zeroFlagsRegister = getNativeRegisterKey(zeroFlagsOperand); // A word zero keeps the unknown upper bits; an existing exact value may retain them. if (/^(?:AX|BX|CX|DX|SI|DI|BP|R(?:8|9|1[0-5])W)$/.test(zeroFlagsOperand)) { zeroFlagsRegister = getEpPartialRegisterKey(zeroFlagsOperand); if (opaqueEpKnownValues[zeroFlagsRegister] !== undefined) opaqueEpKnownValues[zeroFlagsRegister] -= opaqueEpKnownValues[zeroFlagsRegister] & 0xFFFF; } else { opaqueEpKnownValues[zeroFlagsRegister] = 0; } } } else if (allOnesFlagsInstruction) { // OR reg,-1 fixes the same five flags for an all-one result and destroys // any earlier proof that the affected physical register contains zero. var allOnesFlagsRegister = allOnesFlagsInstruction[1] || allOnesFlagsInstruction[2] || allOnesFlagsInstruction[3]; if (/^(?:AX|BX|CX|DX|SI|DI|BP)$/.test(allOnesFlagsRegister)) { allOnesFlagsRegister = "R" + allOnesFlagsRegister; } else { allOnesFlagsRegister = getNativeRegisterKey(allOnesFlagsRegister).replace(/W$/, String()); } delete opaqueEpKnownValues[allOnesFlagsRegister]; opaqueEpOriginInstruction = currentEpInstruction; opaqueEpOriginAddress = epAsmInstructionAddresses[i]; opaqueEpAlwaysTakenBranches = opaqueEpAllOnesFlagsAlwaysTakenBranches; opaqueEpNeverTakenBranches = opaqueEpAllOnesFlagsNeverTakenBranches; opaqueEpNeutralInstructionCount = 0; } else if (carryFlagsInstruction) { // CLC and STC prove only CF, so accept exclusively carry-dependent branches. opaqueEpOriginInstruction = currentEpInstruction; opaqueEpOriginAddress = epAsmInstructionAddresses[i]; opaqueEpAlwaysTakenBranches = currentEpOpCode === "CLC" ? opaqueEpCarryClearAlwaysTakenBranches : opaqueEpCarrySetAlwaysTakenBranches; opaqueEpNeverTakenBranches = currentEpOpCode === "CLC" ? opaqueEpCarryClearNeverTakenBranches : opaqueEpCarrySetNeverTakenBranches; opaqueEpNeutralInstructionCount = 0; } else if (zeroLiteralRegister) { // MOV reg, 0 preserves prior flags but enables a later TEST/OR reg, reg. opaqueEpKnownValues[getNativeRegisterKey(zeroLiteralRegister[1])] = 0; // A value-propagation instruction still consumes the bounded padding budget // of an older EFLAGS proof; expire that proof without losing known zeroes. if (opaqueEpOriginInstruction && ++opaqueEpNeutralInstructionCount > epNeutralInstructionLimit) { opaqueEpOriginInstruction = String(); opaqueEpOriginAddress = -1; opaqueEpNeutralInstructionCount = 0; } } else if (registerCopy) { // Preserve zero provenance only through an exact register-to-register copy. if (opaqueEpKnownValues[getNativeRegisterKey(registerCopy[2])] === 0) { opaqueEpKnownValues[getNativeRegisterKey(registerCopy[1])] = 0; } else { // The source is unknown, so only the overwritten destination loses proof. delete opaqueEpKnownValues[getNativeRegisterKey(registerCopy[1])]; } if (opaqueEpOriginInstruction && ++opaqueEpNeutralInstructionCount > epNeutralInstructionLimit) { opaqueEpOriginInstruction = String(); opaqueEpOriginAddress = -1; opaqueEpNeutralInstructionCount = 0; } } else if (zeroTestRegister && opaqueEpKnownValues[getNativeRegisterKey(zeroTestRegister[1])] === 0) { // TEST/OR of a proven zero register re-establishes deterministic EFLAGS. opaqueEpOriginInstruction = currentEpInstruction; opaqueEpOriginAddress = epAsmInstructionAddresses[i]; opaqueEpAlwaysTakenBranches = epZeroFlagsAlwaysTakenBranches; opaqueEpNeverTakenBranches = epZeroFlagsNeverTakenBranches; opaqueEpNeutralInstructionCount = 0; } else if (conditionalBranch && opaqueEpOriginInstruction && PE.VAToOffset(parseInt(conditionalBranch[2], 0)) !== -1 && (opaqueEpAlwaysTakenBranches.indexOf(conditionalBranch[1]) !== -1 || opaqueEpNeverTakenBranches.indexOf(conditionalBranch[1]) !== -1)) { var opaqueEpBranchType = opaqueEpAlwaysTakenBranches.indexOf(conditionalBranch[1]) !== -1 ? "always taken" : "never taken", opaqueEpBranchAddress = epAsmInstructionAddresses[i], opaqueEpOriginOffset = PE.VAToOffset(opaqueEpOriginAddress), opaqueEpBranchOffset = PE.VAToOffset(opaqueEpBranchAddress); log(logType.emulation, "Opaque " + opaqueEpBranchType + " branch near EP: " + opaqueEpOriginInstruction + " at VA 0x" + opaqueEpOriginAddress.toString(16) + " (offset 0x" + opaqueEpOriginOffset.toString(16) + "); " + currentEpInstruction + " at VA 0x" + opaqueEpBranchAddress.toString(16) + " (offset 0x" + opaqueEpBranchOffset.toString(16) + ")"); isOpaqueEpBranchPresent = true; } else if (conditionalBranch) { // Any other Jcc ends this linear path. Continuing after it could combine // EFLAGS provenance with bytes that are not executed on the same path. break; } else if (flagsNeutralRegisterWrite) { // Exact safe literals may prove a later zero-extending SP replacement. // Other flags-neutral writes discard only the destination, not candidate EFLAGS. var opaqueLiteral = currentEpInstruction.match(epLiteralMovePattern), opaqueLiteralValue = opaqueLiteral && parseInt(opaqueLiteral[2], 0); if (opaqueLiteral && isFinite(opaqueLiteralValue) && opaqueLiteralValue >= 0 && opaqueLiteralValue <= epMaximumSafeInteger) { opaqueEpKnownValues[getNativeRegisterKey(opaqueLiteral[1])] = epRegister32BitPattern.test(opaqueLiteral[1]) ? opaqueLiteralValue >>> 0 : opaqueLiteralValue; } else { delete opaqueEpKnownValues[getNativeRegisterKey(flagsNeutralRegisterWrite[1])]; } if (opaqueEpOriginInstruction && ++opaqueEpNeutralInstructionCount > epNeutralInstructionLimit) { opaqueEpOriginInstruction = String(); opaqueEpOriginAddress = -1; opaqueEpNeutralInstructionCount = 0; } } else if (partialRegisterWrite) { delete opaqueEpKnownValues[getEpPartialRegisterKey(partialRegisterWrite[1])]; if (currentEpOpCode === "MOV" || currentEpOpCode === "NOT") { if (opaqueEpOriginInstruction && ++opaqueEpNeutralInstructionCount > epNeutralInstructionLimit) { opaqueEpOriginInstruction = String(); opaqueEpOriginAddress = -1; opaqueEpNeutralInstructionCount = 0; } } else { opaqueEpOriginInstruction = String(); opaqueEpOriginAddress = -1; opaqueEpNeutralInstructionCount = 0; } } else if (isNeutralInstruction) { // Pure padding preserves both facts, but only within the configured distance. if (opaqueEpOriginInstruction && ++opaqueEpNeutralInstructionCount > epNeutralInstructionLimit) { opaqueEpOriginInstruction = String(); opaqueEpOriginAddress = -1; opaqueEpNeutralInstructionCount = 0; } } else if (registerWrite) { // A flags-changing write destroys both the candidate and this register's value. // Other known-zero registers remain valid until they are individually overwritten. delete opaqueEpKnownValues[getNativeRegisterKey(registerWrite[1])]; opaqueEpOriginInstruction = String(); opaqueEpOriginAddress = -1; opaqueEpNeutralInstructionCount = 0; } else if (epScanStopOpCodes.indexOf(currentEpOpCode) !== -1) { // Do not linearly join instructions from a subsequent basic block. break; } else { // Any other instruction may alter EFLAGS or the intended control-flow sequence. opaqueEpOriginInstruction = String(); opaqueEpOriginAddress = -1; opaqueEpNeutralInstructionCount = 0; opaqueEpKnownValues = {}; } if (isEpLinearFallthroughUnproven(currentEpInstruction, epAsmInstructionAddresses[i], opaqueEpFallthroughState)) break; opaqueEpFallthroughState.StackPointerZero = opaqueEpNextStackPointerZero; } } if (isOpaqueEpBranchPresent) options = addOption(options, "Opaque EP branch"); // Check for redundant register self-moves used as junk instructions near the entry point. var isRedundantEpRegisterSelfMovePresent = false; // Microsoft hybrid-architecture DLL thunks can contain a genuine register self-move. // Keep this weak standalone signal executable-only instead of labeling compatibility code. if (!PE_Cached.isDynamicLinkLibrary && PE_Cached.isArchX86) { var redundantEpSelfMoveFallthrough = {}; for (var i = 0; i < 32 && !isRedundantEpRegisterSelfMovePresent; i++) { var currentEpInstruction = emulateAndGetAsmInstructionByIndex(i), currentEpOpCode = getAsmOpCode(currentEpInstruction); if (!currentEpOpCode) break; // Safety check if (epRedundantSelfMovePattern.test(currentEpInstruction)) { log(logType.emulation, "Redundant register self-move near EP: " + currentEpInstruction); isRedundantEpRegisterSelfMovePresent = true; } else if (epScanStopOpCodes.indexOf(currentEpOpCode) !== -1 || /^J[A-Z]*$/.test(currentEpOpCode)) { break; } if (isEpLinearFallthroughUnproven(currentEpInstruction, epAsmInstructionAddresses[i], redundantEpSelfMoveFallthrough)) break; } } if (isRedundantEpRegisterSelfMovePresent) options = addOption(options, "Reg self-move near EP"); // Check for conditional branches whose taken and fall-through paths are equivalent. var isDegenerateEpConditionalBranchPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86) { var degenerateEpBranchOpCode = String(), degenerateEpBranchTarget = -1, degenerateEpNeutralInstructionCount = 0, degenerateEpCurrentAddress = PE_Cached.addressOfUnmanagedEntryPoint, degenerateEpStackPointerZero = false, degenerateEpZeroFlag = -1, degenerateEpCarryFlag = -1, degenerateEpInstructionInteriors = {}; for (var i = 0; i < 64 && !isDegenerateEpConditionalBranchPresent; i++) { var currentEpInstruction = emulateAndGetAsmInstructionByIndex(i), currentEpOpCode = getAsmOpCode(currentEpInstruction), currentEpInstructionLength = PE.getDisasmLength(degenerateEpCurrentAddress), fallThroughEpInstructionAddress = degenerateEpCurrentAddress + currentEpInstructionLength, nextEpInstructionAddress = PE.getDisasmNextAddress(degenerateEpCurrentAddress); if (!currentEpOpCode || currentEpInstructionLength <= 0 || nextEpInstructionAddress !== fallThroughEpInstructionAddress || epAsmInstructionAddresses[i] !== degenerateEpCurrentAddress || !isVirtualAddressRangeFileBacked(degenerateEpCurrentAddress, currentEpInstructionLength, SECTION_FLAGS_EXECUTE)) break; // An earlier mandatory fault cannot expose a later pair as equivalent executable paths. if (/^AAM (?:0X0+|0+)$/i.test(currentEpInstruction) || isEpProvenFaultingInstruction(currentEpInstruction, currentEpOpCode, degenerateEpStackPointerZero)) break; degenerateEpStackPointerZero = advanceEpStackPointerZero(currentEpInstruction, currentEpOpCode, degenerateEpStackPointerZero); for (var byteIndex = 1; byteIndex < currentEpInstructionLength; byteIndex++) { degenerateEpInstructionInteriors[(degenerateEpCurrentAddress + byteIndex).toString()] = true; } var conditionalBranch = currentEpInstruction.match(epConditionalBranchPattern), directJump = currentEpInstruction.match(epDirectJumpPattern), simplePolymorphicInstruction = currentEpInstruction.match(epSimplePolymorphicInstructionPattern), simplePolymorphicRegister = simplePolymorphicInstruction ? (simplePolymorphicInstruction[1] || simplePolymorphicInstruction[2]) : String(), isSimplePolymorphicInstruction = currentEpOpCode === "NOP" || currentEpOpCode === "FNOP" || currentEpOpCode === "PAUSE" || (simplePolymorphicInstruction && !(PE_Cached.is64bit && epRegister32BitPattern.test(simplePolymorphicRegister))); // The source encoding must preserve native IP width, not merely print a convergent full VA. if ((conditionalBranch || directJump) && !isEpNativeNearTransfer(degenerateEpCurrentAddress, currentEpInstruction, currentEpInstructionLength)) break; // A pending Jcc may lead to its own fall-through, share a resolved target // with a complementary Jcc, or converge with a later direct JMP. Only // bounded neutral instructions may separate the branch encodings. if (conditionalBranch && currentEpOpCode !== "JMP") { var currentBranchTarget = parseInt(conditionalBranch[2], 0), currentCanonicalBranchOpCode = epCanonicalBranchOpCodes[currentEpOpCode] || currentEpOpCode, previousCanonicalBranchOpCode = epCanonicalBranchOpCodes[degenerateEpBranchOpCode] || degenerateEpBranchOpCode, branchPair = previousCanonicalBranchOpCode + ":" + currentCanonicalBranchOpCode, reverseBranchPair = currentCanonicalBranchOpCode + ":" + previousCanonicalBranchOpCode, branchTargetProof = traceEpDirectJumpChain(currentBranchTarget, 32, true, degenerateEpInstructionInteriors), complementaryBranches = epComplementaryBranchPairs.indexOf(branchPair) !== -1 || epComplementaryBranchPairs.indexOf(reverseBranchPair) !== -1, previousBranchProof = complementaryBranches && degenerateEpBranchTarget !== -1 ? traceEpDirectJumpChain(degenerateEpBranchTarget, 32, true, degenerateEpInstructionInteriors) : undefined; if (branchTargetProof && (currentBranchTarget === fallThroughEpInstructionAddress || (previousBranchProof && previousBranchProof.Target === branchTargetProof.Target && branchTargetProof.Target >= fallThroughEpInstructionAddress))) { log(logType.emulation, "Degenerate conditional branch near EP: " + currentEpInstruction); isDegenerateEpConditionalBranchPresent = true; } else { degenerateEpBranchOpCode = currentEpOpCode; degenerateEpBranchTarget = currentBranchTarget; degenerateEpNeutralInstructionCount = 0; } } else if (directJump) { var degenerateEpJumpTarget = parseInt(directJump[1], 0); // Neither arm may have side effects before its direct-JMP chain converges. var takenPath = degenerateEpBranchOpCode ? traceEpDirectJumpChain(degenerateEpBranchTarget, 32, true) : undefined, fallThroughPath = traceEpDirectJumpChain(degenerateEpJumpTarget, 32, true), branchIntoScannedInstruction = degenerateEpInstructionInteriors[degenerateEpBranchTarget.toString()] || degenerateEpInstructionInteriors[degenerateEpJumpTarget.toString()]; if (!branchIntoScannedInstruction && takenPath && fallThroughPath && takenPath.Target === fallThroughPath.Target) { log(logType.emulation, "Degenerate conditional branch near EP: " + currentEpInstruction); isDegenerateEpConditionalBranchPresent = true; } break; } else if (isSimplePolymorphicInstruction && degenerateEpBranchOpCode && degenerateEpNeutralInstructionCount < epNeutralInstructionLimit) { // Preserve the pending branch only across instructions that cannot change // its target or introduce a meaningful side effect for this structural check. degenerateEpNeutralInstructionCount++; } else { degenerateEpBranchOpCode = String(); degenerateEpBranchTarget = -1; degenerateEpNeutralInstructionCount = 0; if (epScanStopOpCodes.indexOf(currentEpOpCode) !== -1 || /^J[A-Z]+$/.test(currentEpOpCode)) break; } // A proven taken branch cannot expose a later physical pair; unknown fall-through remains possible. if (conditionalBranch && parseInt(conditionalBranch[2], 0) !== fallThroughEpInstructionAddress && getEpKnownBranchResult(currentEpOpCode, degenerateEpZeroFlag, degenerateEpCarryFlag) === 1) { break; } degenerateEpZeroFlag = advanceEpZeroFlag(currentEpInstruction, currentEpOpCode, degenerateEpZeroFlag); degenerateEpCarryFlag = advanceEpCarryFlag(currentEpInstruction, currentEpOpCode, degenerateEpCarryFlag); degenerateEpCurrentAddress = nextEpInstructionAddress; } } if (isDegenerateEpConditionalBranchPresent) options = addOption(options, "Degenerate Jcc near EP"); // Check for a register pushed and restored immediately through simple junk instructions. var isRedundantEpStackRoundTripPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86) { var redundantEpPushedRegister = String(), redundantEpStackNeutralInstructionCount = 0, redundantEpStackFallthrough = {}; for (var i = 0; i < 64 && !isRedundantEpStackRoundTripPresent; i++) { var currentEpInstruction = emulateAndGetAsmInstructionByIndex(i), currentEpOpCode = getAsmOpCode(currentEpInstruction); if (!currentEpOpCode) break; // Safety check var pushedRegister = currentEpInstruction.match(epStackRoundTripPushPattern), poppedRegister = currentEpInstruction.match(epStackRoundTripPopPattern), isSimplePolymorphicInstruction = currentEpOpCode === "NOP" || currentEpOpCode === "FNOP" || currentEpOpCode === "PAUSE" || epSimplePolymorphicInstructionPattern.test(currentEpInstruction); if (pushedRegister) { // Keep the exact operand spelling: PUSH AX/POP AX is balanced, while mixing // register widths changes both the stack delta and the restored value. redundantEpPushedRegister = pushedRegister[1]; redundantEpStackNeutralInstructionCount = 0; } else if (poppedRegister && redundantEpPushedRegister === poppedRegister[1]) { log(logType.emulation, "Redundant stack round-trip near EP: PUSH/POP " + redundantEpPushedRegister); isRedundantEpStackRoundTripPresent = true; } else if (isSimplePolymorphicInstruction && redundantEpPushedRegister && redundantEpStackNeutralInstructionCount < epNeutralInstructionLimit) { redundantEpStackNeutralInstructionCount++; } else { // Any non-neutral instruction makes the PUSH potentially meaningful, so a // later POP must start from a new candidate instead of reusing stale state. redundantEpPushedRegister = String(); redundantEpStackNeutralInstructionCount = 0; if (epScanStopOpCodes.indexOf(currentEpOpCode) !== -1 || /^J[A-Z]+$/.test(currentEpOpCode)) break; } if (isEpLinearFallthroughUnproven(currentEpInstruction, epAsmInstructionAddresses[i], redundantEpStackFallthrough)) break; } } if (isRedundantEpStackRoundTripPresent) options = addOption(options, "Redundant Push/Pop near EP"); // Check for nearby inverse operations which restore the original register value. var isReversibleEpJunkPairPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86) { var reversibleEpOpCode = String(), reversibleEpRegister = String(), reversibleEpLiteral = String(), reversibleEpNeutralInstructionCount = 0, reversibleEpFallthrough = {}; for (var i = 0; i < 64 && !isReversibleEpJunkPairPresent; i++) { var currentEpInstruction = emulateAndGetAsmInstructionByIndex(i), currentEpOpCode = getAsmOpCode(currentEpInstruction); if (!currentEpOpCode) break; // Safety check var reversibleBinaryInstruction = currentEpInstruction.match(epReversibleBinaryPattern), reversibleUnaryInstruction = currentEpInstruction.match(epReversibleUnaryPattern), simplePolymorphicInstruction = currentEpInstruction.match(epSimplePolymorphicInstructionPattern), simplePolymorphicRegister = simplePolymorphicInstruction ? (simplePolymorphicInstruction[1] || simplePolymorphicInstruction[2]) : String(), isSafeReversiblePadding = currentEpOpCode === "NOP" || currentEpOpCode === "FNOP" || currentEpOpCode === "PAUSE" || (simplePolymorphicInstruction && !(PE_Cached.is64bit && epRegister32BitPattern.test(simplePolymorphicRegister))); // A candidate is valid only when the second operation has the same width, // register and literal. Safely representable literals are parsed so equivalent // hexadecimal and decimal spellings pair; larger values retain their exact text. if (reversibleBinaryInstruction) { var currentReversibleOpCode = reversibleBinaryInstruction[1], currentReversibleRegister = reversibleBinaryInstruction[2], currentReversibleLiteralValue = parseInt(reversibleBinaryInstruction[3], 0), // Preserve the exact spelling above Number's safe range. This may skip // an equivalent decimal/hex pair, but cannot merge two different imm64s. currentReversibleLiteral = currentReversibleLiteralValue <= epMaximumSafeInteger ? currentReversibleLiteralValue.toString() : reversibleBinaryInstruction[3], // A PE32+ r32 write clears the upper half, so the full register state // is not restored even when the low 32-bit operation is reversed. isInverseBinaryPair = !(PE_Cached.is64bit && epRegister32BitPattern.test(currentReversibleRegister)) && reversibleEpRegister === currentReversibleRegister && reversibleEpLiteral === currentReversibleLiteral && ((reversibleEpOpCode === "XOR" && currentReversibleOpCode === "XOR") || (reversibleEpOpCode === "ADD" && currentReversibleOpCode === "SUB") || (reversibleEpOpCode === "SUB" && currentReversibleOpCode === "ADD")); if (isInverseBinaryPair) { log(logType.emulation, "Reversible junk pair near EP: " + reversibleEpOpCode + "/" + currentReversibleOpCode + " " + currentReversibleRegister); isReversibleEpJunkPairPresent = true; } else { reversibleEpOpCode = currentReversibleOpCode; reversibleEpRegister = currentReversibleRegister; reversibleEpLiteral = currentReversibleLiteral; reversibleEpNeutralInstructionCount = 0; } } else if (reversibleUnaryInstruction) { var currentReversibleOpCode = reversibleUnaryInstruction[1], currentReversibleRegister = reversibleUnaryInstruction[2]; if (!(PE_Cached.is64bit && epRegister32BitPattern.test(currentReversibleRegister)) && reversibleEpOpCode === currentReversibleOpCode && reversibleEpRegister === currentReversibleRegister) { log(logType.emulation, "Reversible junk pair near EP: " + currentReversibleOpCode + " " + currentReversibleRegister); isReversibleEpJunkPairPresent = true; } else { reversibleEpOpCode = currentReversibleOpCode; reversibleEpRegister = currentReversibleRegister; reversibleEpLiteral = String(); reversibleEpNeutralInstructionCount = 0; } } else if (isSafeReversiblePadding && reversibleEpOpCode && reversibleEpNeutralInstructionCount < epNeutralInstructionLimit) { // Full-width self-moves on any register and architectural NOP/PAUSE // instructions are globally neutral. PE32+ r32 self-moves are excluded // because their implicit zero-extension changes the physical register. reversibleEpNeutralInstructionCount++; } else { reversibleEpOpCode = String(); reversibleEpRegister = String(); reversibleEpLiteral = String(); reversibleEpNeutralInstructionCount = 0; if (epScanStopOpCodes.indexOf(currentEpOpCode) !== -1 || /^J[A-Z]+$/.test(currentEpOpCode)) break; } if (isEpLinearFallthroughUnproven(currentEpInstruction, epAsmInstructionAddresses[i], reversibleEpFallthrough)) break; } } if (isReversibleEpJunkPairPresent) options = addOption(options, "Reversible pair near EP"); // Check for comparisons against a statically proven identical register literal. var isConstantOpaqueEpComparisonPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86) { var constantEpKnownRegisterValues = {}, constantEpComparisonOrigin = String(), constantEpComparisonEqual = false, constantEpNeutralInstructionCount = 0, constantEpComparisonFallthrough = {}; // Values use canonical physical-register keys, but truncation is applied whenever // a 32-bit destination is written. This mirrors x86-64 zero-extension and also lets // PE32 reuse the same propagation logic without treating AX as a full-register write. for (var i = 0; i < 128 && !isConstantOpaqueEpComparisonPresent; i++) { var currentEpInstruction = emulateAndGetAsmInstructionByIndex(i), currentEpOpCode = getAsmOpCode(currentEpInstruction); if (!currentEpOpCode) break; // Safety check // Reuse incoming literal facts before a division or memory write can invalidate its source. if (isEpProvenFaultingInstruction(currentEpInstruction, currentEpOpCode, constantEpComparisonFallthrough.StackPointerZero, constantEpKnownRegisterValues)) break; var constantEpNextStackPointerZero = advanceEpStackPointerZero(currentEpInstruction, currentEpOpCode, constantEpComparisonFallthrough.StackPointerZero, constantEpKnownRegisterValues); var literalMove = currentEpInstruction.match(epLiteralMovePattern), registerCopy = currentEpInstruction.match(epRegisterCopyPattern), literalCompare = currentEpInstruction.match(epLiteralComparePattern), conditionalBranch = currentEpInstruction.match(epConditionalBranchPattern), flagsNeutralRegisterWrite = currentEpInstruction.match(epFlagsNeutralRegisterWritePattern), registerWrite = currentEpInstruction.match(epTrackedRegisterWritePattern), partialRegisterWrite = currentEpInstruction.match(epPartialRegisterWritePattern), simplePolymorphicInstruction = currentEpInstruction.match(epSimplePolymorphicInstructionPattern), valuePreservingInstruction = currentEpInstruction.match(epValuePreservingInstructionPattern), isSimplePolymorphicInstruction = currentEpOpCode === "NOP" || currentEpOpCode === "FNOP" || currentEpOpCode === "PAUSE" || simplePolymorphicInstruction; if (literalCompare) { var comparedRegister = getNativeRegisterKey(literalCompare[1]), comparedLiteral = parseInt(literalCompare[2], 0), knownComparedLiteral = constantEpKnownRegisterValues[comparedRegister]; if (knownComparedLiteral !== undefined && epRegister32BitPattern.test(literalCompare[1])) knownComparedLiteral = knownComparedLiteral >>> 0; // Restrict the proof to unambiguous non-negative signed 32-bit literals. // This avoids JavaScript precision and x86-64 sign-extension ambiguities. if (comparedLiteral <= 0x7FFFFFFF && knownComparedLiteral !== undefined) { constantEpComparisonOrigin = currentEpInstruction; constantEpComparisonEqual = knownComparedLiteral === comparedLiteral; constantEpNeutralInstructionCount = 0; } else { constantEpComparisonOrigin = String(); constantEpNeutralInstructionCount = 0; } } else if (conditionalBranch) { var constantEpBranchOpCode = conditionalBranch[1], constantEpEqualBranch = epZeroFlagsAlwaysTakenBranches.indexOf(constantEpBranchOpCode) !== -1 || epZeroFlagsNeverTakenBranches.indexOf(constantEpBranchOpCode) !== -1, constantEpUnequalBranch = /^(?:JE|JZ|JNE|JNZ)$/.test(constantEpBranchOpCode) && isVirtualAddressRangeFileBacked(PE_Cached.addressOfUnmanagedEntryPoint, 1, SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE); if (constantEpComparisonOrigin && PE.VAToOffset(parseInt(conditionalBranch[2], 0)) !== -1 && (constantEpComparisonEqual ? constantEpEqualBranch : constantEpUnequalBranch)) { log(logType.emulation, "Constant opaque comparison near EP: " + constantEpComparisonOrigin + "; " + currentEpInstruction); isConstantOpaqueEpComparisonPresent = true; } break; } else if (literalMove || registerCopy || flagsNeutralRegisterWrite || isSimplePolymorphicInstruction || valuePreservingInstruction) { // Propagate only operations whose destination value is statically known. // Other flags-neutral writes invalidate their destination without discarding // unrelated register facts accumulated earlier in the linear block. if (literalMove) { var literalMoveRegister = getNativeRegisterKey(literalMove[1]), literalMoveValue = parseInt(literalMove[2], 0); if (literalMoveValue <= epMaximumSafeInteger) { if (epRegister32BitPattern.test(literalMove[1])) literalMoveValue = literalMoveValue >>> 0; constantEpKnownRegisterValues[literalMoveRegister] = literalMoveValue; } else { // JavaScript Number cannot preserve this literal exactly, so retaining // it could turn a later 32-bit truncation into a false known value. delete constantEpKnownRegisterValues[literalMoveRegister]; } } else if (registerCopy) { var constantEpRegisterCopyDestination = getNativeRegisterKey(registerCopy[1]), constantEpRegisterCopySource = getNativeRegisterKey(registerCopy[2]); if (constantEpKnownRegisterValues[constantEpRegisterCopySource] !== undefined) { var registerCopyValue = constantEpKnownRegisterValues[constantEpRegisterCopySource]; if (epRegister32BitPattern.test(registerCopy[1])) registerCopyValue = registerCopyValue >>> 0; constantEpKnownRegisterValues[constantEpRegisterCopyDestination] = registerCopyValue; } else { delete constantEpKnownRegisterValues[constantEpRegisterCopyDestination]; } } else if (simplePolymorphicInstruction) { var simplePolymorphicRegister = simplePolymorphicInstruction[1] || simplePolymorphicInstruction[2], simplePolymorphicRegisterKey = getNativeRegisterKey(simplePolymorphicRegister); if (constantEpKnownRegisterValues[simplePolymorphicRegisterKey] !== undefined && epRegister32BitPattern.test(simplePolymorphicRegister)) { constantEpKnownRegisterValues[simplePolymorphicRegisterKey] = constantEpKnownRegisterValues[simplePolymorphicRegisterKey] >>> 0; } } else if (valuePreservingInstruction) { var valuePreservingRegister = valuePreservingInstruction[1] || valuePreservingInstruction[2], valuePreservingRegisterKey = getNativeRegisterKey(valuePreservingRegister); if (constantEpKnownRegisterValues[valuePreservingRegisterKey] !== undefined && epRegister32BitPattern.test(valuePreservingRegister)) { constantEpKnownRegisterValues[valuePreservingRegisterKey] = constantEpKnownRegisterValues[valuePreservingRegisterKey] >>> 0; } } else if (flagsNeutralRegisterWrite && !isSimplePolymorphicInstruction) { delete constantEpKnownRegisterValues[getNativeRegisterKey(flagsNeutralRegisterWrite[1])]; } if (valuePreservingInstruction) { // Value-preserving arithmetic may still replace EFLAGS. Keep register // provenance but require a fresh CMP before accepting a later Jcc. constantEpComparisonOrigin = String(); constantEpNeutralInstructionCount = 0; } else if (constantEpComparisonOrigin && ++constantEpNeutralInstructionCount > epNeutralInstructionLimit) { constantEpComparisonOrigin = String(); constantEpNeutralInstructionCount = 0; } } else if (registerWrite) { // Whole-register zero producers prove a value as well as replacing the earlier flags. if (epZeroRegisterPattern.test(currentEpInstruction)) constantEpKnownRegisterValues[getNativeRegisterKey(registerWrite[1])] = 0; else delete constantEpKnownRegisterValues[getNativeRegisterKey(registerWrite[1])]; constantEpComparisonOrigin = String(); constantEpNeutralInstructionCount = 0; } else if (partialRegisterWrite) { delete constantEpKnownRegisterValues[getEpPartialRegisterKey(partialRegisterWrite[1])]; if (currentEpOpCode !== "MOV" && currentEpOpCode !== "NOT") constantEpComparisonOrigin = String(); if (constantEpComparisonOrigin && ++constantEpNeutralInstructionCount > epNeutralInstructionLimit) constantEpComparisonOrigin = String(); } else if (epScanStopOpCodes.indexOf(currentEpOpCode) !== -1) { break; } else { constantEpKnownRegisterValues = {}; constantEpComparisonOrigin = String(); constantEpNeutralInstructionCount = 0; } if (isEpLinearFallthroughUnproven(currentEpInstruction, epAsmInstructionAddresses[i], constantEpComparisonFallthrough)) break; constantEpComparisonFallthrough.StackPointerZero = constantEpNextStackPointerZero; } } if (isConstantOpaqueEpComparisonPresent) options = addOption(options, "Const Cmp/Jcc near EP"); // Check for manual module enumeration through PEB->Ldr loader lists near the entry point. var isPebModuleWalkPresent = false, pebModuleWalkPatterns = {}; if (PE_Cached.isEntryPointAnalysisAllowed && !PE_Cached.isDriver && PE_Cached.isArchX86 && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3)) { const pebModuleWalkNativeRegister = PE_Cached.isI386 ? "E(?:AX|BX|CX|DX|SI|DI|BP)" : "R(?:AX|BX|CX|DX|SI|DI|BP|8|9|1[0-5])", pebModuleWalkSegment = PE_Cached.isI386 ? "FS" : "GS", pebModuleWalkPebAddress = PE_Cached.isI386 ? "(?:0X0*30|48)" : "(?:0X0*60|96)", pebModuleWalkLdrOffset = PE_Cached.isI386 ? "(?:0X0*C|12)" : "(?:0X0*18|24)", pebModuleWalkListOffset = PE_Cached.isI386 ? "(?:0X0*(?:C|14|1C)|12|20|28)" : "(?:0X0*(?:10|20|30)|16|32|48)", pebModuleWalkPebLoadPattern = new RegExp("^MOV (" + pebModuleWalkNativeRegister + "), (?:(?:DWORD|QWORD) PTR )?" + pebModuleWalkSegment + ":\\[" + pebModuleWalkPebAddress + "\\]$", "i"), pebModuleWalkIndexedPebLoadPattern = new RegExp("^MOV (" + pebModuleWalkNativeRegister + "), (?:(?:DWORD|QWORD) PTR )?" + pebModuleWalkSegment + ":\\[(" + pebModuleWalkNativeRegister + ") ?\\+ ?" + pebModuleWalkPebAddress + "\\]$", "i"), pebModuleWalkLdrLoadPattern = new RegExp("^MOV (" + pebModuleWalkNativeRegister + "), (?:(?:DWORD|QWORD) PTR )?\\[(" + pebModuleWalkNativeRegister + ") ?\\+ ?" + pebModuleWalkLdrOffset + "\\]$", "i"), pebModuleWalkListLoadPattern = new RegExp("^MOV (" + pebModuleWalkNativeRegister + "), (?:(?:DWORD|QWORD) PTR )?\\[(" + pebModuleWalkNativeRegister + ") ?\\+ ?" + pebModuleWalkListOffset + "\\]$", "i"), pebModuleWalkListAddressPattern = new RegExp("^LEA (" + pebModuleWalkNativeRegister + "), \\[(" + pebModuleWalkNativeRegister + ") ?\\+ ?" + pebModuleWalkListOffset + "\\]$", "i"), pebModuleWalkIndirectListLoadPattern = new RegExp("^MOV (" + pebModuleWalkNativeRegister + "), (?:(?:DWORD|QWORD) PTR )?\\[(" + pebModuleWalkNativeRegister + ")\\]$", "i"), pebModuleWalkZeroRegisterPattern = new RegExp("^(?:(?:XOR|SUB) (" + epGeneralPurposeRegister + "), \\1|(?:MOV|AND) (" + epGeneralPurposeRegister + "), (?:0X0+|0+))$", "i"); pebModuleWalkPatterns = { PebLoad: pebModuleWalkPebLoadPattern, LdrLoad: pebModuleWalkLdrLoadPattern, ListLoad: pebModuleWalkListLoadPattern }; // States 0/1/2/3 prove a whole-register zero, PEB pointer, Ldr pointer, or module-list field address. // Copies preserve the proof, while writes invalidate only their physical register. // Unknown instructions discard all proofs but may begin a new chain later. var pebModuleWalkRegisterStates = {}, pebModuleWalkFallthrough = {}; // Search the bounded EP stream for the ordered PEB -> Ldr -> module-list chain. // Per-register states allow the chain to survive register substitution and copies. for (var i = 0; i < 512 && !isPebModuleWalkPresent; i++) { var currentEpInstruction = emulateAndGetAsmInstructionByIndex(i), currentEpOpCode = getAsmOpCode(currentEpInstruction); if (!currentEpOpCode) break; // Safety check // Decode all state-producing and state-destroying forms before advancing the chain. var pebPointerLoad = currentEpInstruction.match(pebModuleWalkPebLoadPattern), indexedPebPointerLoad = currentEpInstruction.match(pebModuleWalkIndexedPebLoadPattern), zeroRegister = currentEpInstruction.match(pebModuleWalkZeroRegisterPattern), ldrPointerLoad = currentEpInstruction.match(pebModuleWalkLdrLoadPattern), moduleListLoad = currentEpInstruction.match(pebModuleWalkListLoadPattern), moduleListAddress = currentEpInstruction.match(pebModuleWalkListAddressPattern), indirectModuleListLoad = currentEpInstruction.match(pebModuleWalkIndirectListLoadPattern), registerCopy = currentEpInstruction.match(epRegisterCopyPattern), simplePolymorphicInstruction = currentEpInstruction.match(epSimplePolymorphicInstructionPattern), valuePreservingInstruction = currentEpInstruction.match(epValuePreservingInstructionPattern), registerWrite = currentEpInstruction.match(epFlagsNeutralRegisterWritePattern) || currentEpInstruction.match(epTrackedRegisterWritePattern), partialRegisterWrite = currentEpInstruction.match(epPartialRegisterWritePattern), conditionalBranch = currentEpInstruction.match(epConditionalBranchPattern); if (pebPointerLoad || (indexedPebPointerLoad && pebModuleWalkRegisterStates[getNativeRegisterKey(indexedPebPointerLoad[2])] === 0)) { var pebRegister = getNativeRegisterKey((pebPointerLoad || indexedPebPointerLoad)[1]); // An indexed FS/GS read is equivalent only when its full base register is proven zero. pebModuleWalkRegisterStates[pebRegister] = 1; } else if ((moduleListLoad && pebModuleWalkRegisterStates[getNativeRegisterKey(moduleListLoad[2])] === 2) || (indirectModuleListLoad && pebModuleWalkRegisterStates[getNativeRegisterKey(indirectModuleListLoad[2])] === 3)) { // Only a native-width read through the proven Ldr field completes the chain, not LEA alone. log(logType.emulation, "PEB loader module-list walk near EP: " + currentEpInstruction); isPebModuleWalkPresent = true; } else if (moduleListAddress) { var moduleListAddressDestination = getNativeRegisterKey(moduleListAddress[1]); if (pebModuleWalkRegisterStates[getNativeRegisterKey(moduleListAddress[2])] === 2) { pebModuleWalkRegisterStates[moduleListAddressDestination] = 3; } else { delete pebModuleWalkRegisterStates[moduleListAddressDestination]; } } else if (ldrPointerLoad) { var ldrDestinationRegister = getNativeRegisterKey(ldrPointerLoad[1]), ldrSourceRegister = getNativeRegisterKey(ldrPointerLoad[2]); // Promote only a destination loaded through a proven PEB pointer to state 2. if (pebModuleWalkRegisterStates[ldrSourceRegister] === 1) { pebModuleWalkRegisterStates[ldrDestinationRegister] = 2; } else { delete pebModuleWalkRegisterStates[ldrDestinationRegister]; } } else if (moduleListLoad) { // An unproven list-shaped load overwrites its destination with unknown data. delete pebModuleWalkRegisterStates[getNativeRegisterKey(moduleListLoad[1])]; } else if (zeroRegister) { // A 32-bit destination zero-extends in PE32+, so this proves the native base too. pebModuleWalkRegisterStates[getNativeRegisterKey(zeroRegister[1] || zeroRegister[2])] = 0; } else if (registerCopy) { var pebModuleWalkCopyDestination = getNativeRegisterKey(registerCopy[1]), pebModuleWalkCopySource = getNativeRegisterKey(registerCopy[2]); // A PE32+ r32 destination destroys a 64-bit PEB/Ldr pointer by zero-extension. if (PE_Cached.is64bit && epRegister32BitPattern.test(registerCopy[1]) && pebModuleWalkRegisterStates[pebModuleWalkCopySource] !== 0) { delete pebModuleWalkRegisterStates[pebModuleWalkCopyDestination]; } else if (pebModuleWalkRegisterStates[pebModuleWalkCopySource] !== undefined) { pebModuleWalkRegisterStates[pebModuleWalkCopyDestination] = pebModuleWalkRegisterStates[pebModuleWalkCopySource]; } else { delete pebModuleWalkRegisterStates[pebModuleWalkCopyDestination]; } } else if (simplePolymorphicInstruction || valuePreservingInstruction) { var pebModuleWalkPreservingRegister = (simplePolymorphicInstruction ? (simplePolymorphicInstruction[1] || simplePolymorphicInstruction[2]) : (valuePreservingInstruction[1] || valuePreservingInstruction[2])), pebModuleWalkPreservingRegisterKey = getNativeRegisterKey(pebModuleWalkPreservingRegister); if (PE_Cached.is64bit && epRegister32BitPattern.test(pebModuleWalkPreservingRegister) && pebModuleWalkRegisterStates[pebModuleWalkPreservingRegisterKey] !== 0) { delete pebModuleWalkRegisterStates[pebModuleWalkPreservingRegisterKey]; } } else if (currentEpOpCode === "NOP" || currentEpOpCode === "FNOP" || currentEpOpCode === "PAUSE" || currentEpOpCode === "CLC" || currentEpOpCode === "STC" || currentEpOpCode === "CMC" || conditionalBranch || /^(?:CMP|TEST)$/.test(currentEpOpCode)) { // These instructions preserve every proven pointer on the fall-through path. } else if (registerWrite) { // A recognized write invalidates only the overwritten physical register. delete pebModuleWalkRegisterStates[getNativeRegisterKey(registerWrite[1])]; } else if (partialRegisterWrite) { delete pebModuleWalkRegisterStates[getEpPartialRegisterKey(partialRegisterWrite[1])]; } else { // Unmodelled semantics may alter every tracked pointer; restart the chain. pebModuleWalkRegisterStates = {}; if (isEpLinearScanStopped(currentEpOpCode, epAsmInstructionAddresses[i], epScanStopOpCodes)) break; } if (isEpLinearFallthroughUnproven(currentEpInstruction, epAsmInstructionAddresses[i], pebModuleWalkFallthrough)) break; } } if (isPebModuleWalkPresent) options = addOption(options, "PEB module walk near EP"); // Check for the x87 environment trick used by 32-bit shellcode to recover its EIP. var isFpuEpRecoveryPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && !PE_Cached.isDriver && PE_Cached.isI386 && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3)) { var isFpuEnvironmentStored = false, hasFpuInstructionPointerSource = false, fpuEpNeutralInstructionCount = 0, fpuEpCurrentAddress = PE_Cached.addressOfUnmanagedEntryPoint; // In the 28-byte x87 environment, the saved instruction pointer begins at // offset 0xC. Writing the environment at ESP-0xC therefore places EIP at // the current stack top, where a following POP can recover it. // Advance by physical instruction length because raw x87 encodings may be // rendered inconsistently by the disassembler used by different DiE builds. var fpuEpFallthrough = {}; for (var i = 0; i < 512 && !isFpuEpRecoveryPresent; i++) { var currentEpInstruction = PE.getDisasmString(fpuEpCurrentAddress), currentEpOpCode = getAsmOpCode(currentEpInstruction), currentEpInstructionLength = PE.getDisasmLength(fpuEpCurrentAddress), currentEpOffset = PE.VAToOffset(fpuEpCurrentAddress), fpuOpcodeOffset = currentEpOffset, fpuPrefixLength = 0, fpuPrefixByte = -1, fpuHasOperandSizeOverride = false, fpuHasAddressSizeOverride = false, fpuHasLockPrefix = false, fpuStackSegmentValid = true; // Operand-size override selects the 14-byte environment, whose FIP is at +6 rather than +12. // https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html (Vol. 1, x87 FPU operating environment) while (fpuPrefixLength < 14 && isVirtualAddressRangeFileBacked(fpuEpCurrentAddress, fpuPrefixLength + 1, SECTION_FLAGS_EXECUTE)) { fpuPrefixByte = PE.readByte(fpuOpcodeOffset); if ([0x26, 0x2E, 0x36, 0x3E, 0x64, 0x65, 0x66, 0x67, 0xF0, 0xF2, 0xF3].indexOf(fpuPrefixByte) === -1 && !(fpuPrefixLength === 0 && fpuPrefixByte === 0x9B)) break; if (fpuPrefixByte === 0xF0) fpuHasLockPrefix = true; if (fpuPrefixByte === 0x66) fpuHasOperandSizeOverride = true; if (fpuPrefixByte === 0x67) fpuHasAddressSizeOverride = true; if ([0x26, 0x2E, 0x3E, 0x64, 0x65].indexOf(fpuPrefixByte) !== -1) fpuStackSegmentValid = false; fpuPrefixLength++; fpuOpcodeOffset++; } // Match both FNSTENV [ESP-0xC] encodings and consume their complete prefixed instruction. var storesRawFpuEnvironmentAtStack = currentEpOffset !== -1 && !fpuHasAddressSizeOverride && (PE.compare("D9 74 24 F4", fpuOpcodeOffset) || PE.compare("D9 B4 24 F4 FF FF FF", fpuOpcodeOffset)), isRawFpuNop = currentEpOffset !== -1 && PE.compare("D9 D0", fpuOpcodeOffset), fpuOpcodeByte = fpuPrefixLength < 14 ? fpuPrefixByte : currentEpOffset !== -1 ? PE.readByte(fpuOpcodeOffset) : -1, fpuSecondByte = (fpuOpcodeByte === 0xD8 || fpuOpcodeByte === 0xD9 || fpuOpcodeByte === 0xDD) && isVirtualAddressRangeFileBacked(fpuEpCurrentAddress, fpuPrefixLength + 2, SECTION_FLAGS_EXECUTE) ? PE.readByte(fpuOpcodeOffset + 1) : -1, // These documented D9 math forms may also decode as DB; consume both bytes instead of treating the second as integer code. isRawFpuSource = (fpuOpcodeByte === 0xD9 && ((fpuSecondByte >= 0xE8 && fpuSecondByte <= 0xEE) || [0xF2, 0xF3, 0xF5].indexOf(fpuSecondByte) !== -1 || (fpuSecondByte >= 0xF8 && fpuSecondByte <= 0xFF))) || (fpuOpcodeByte === 0xD8 && fpuSecondByte >= 0xC0), isRawFpuStateSave = fpuOpcodeByte === 0xDD && fpuSecondByte >= 0 && fpuSecondByte < 0xC0 && (fpuSecondByte & 0x38) === 0x30; if (!currentEpOpCode || currentEpInstructionLength <= 0) break; // Safety check // Only the explicitly decoded raw x87 forms may cross a DB boundary; invalid integer encodings cannot reach recovery. if (currentEpOpCode === "DB" && !storesRawFpuEnvironmentAtStack && !isRawFpuNop && !isRawFpuSource && !isRawFpuStateSave) break; // LOCK faults on x87, but a valid locked integer operation must not hide a later recovery sequence. if (fpuHasLockPrefix && fpuOpcodeByte >= 0xD8 && fpuOpcodeByte <= 0xDF) break; if (isRawFpuStateSave && !fpuPrefixLength) { // Decode the complete DD /6 memory operand so displacement bytes cannot become fake x87 sources. var fpuSaveMode = fpuSecondByte >>> 6, fpuSaveRm = fpuSecondByte & 7, fpuSaveLength = 2; if (fpuSaveRm === 4) { if (!isVirtualAddressRangeFileBacked(fpuEpCurrentAddress, 3, SECTION_FLAGS_EXECUTE)) break; fpuSaveLength++; if (fpuSaveMode === 0 && (PE.readByte(currentEpOffset + 2) & 7) === 5) fpuSaveLength += 4; } else if (fpuSaveMode === 0 && fpuSaveRm === 5) fpuSaveLength += 4; if (fpuSaveMode === 1) fpuSaveLength++; else if (fpuSaveMode === 2) fpuSaveLength += 4; if (!isVirtualAddressRangeFileBacked(fpuEpCurrentAddress, fpuSaveLength, SECTION_FLAGS_EXECUTE)) break; currentEpInstructionLength = fpuSaveLength; } else if (storesRawFpuEnvironmentAtStack) { // Override the decoded length with the exact short or long raw encoding. currentEpInstructionLength = fpuPrefixLength + (PE.compare("D9 74 24 F4", fpuOpcodeOffset) ? 4 : 7); } else if (isRawFpuNop || isRawFpuSource) { // Some DiE builds render these complete x87 instructions as separate bytes. currentEpInstructionLength = fpuPrefixLength + 2; } if (currentEpInstructionLength > 15 || !isVirtualAddressRangeFileBacked(fpuEpCurrentAddress, currentEpInstructionLength, SECTION_FLAGS_EXECUTE)) break; if (isEpProvenFaultingInstruction(currentEpInstruction, currentEpOpCode, fpuEpFallthrough.StackPointerZero)) break; var storesFpuEnvironmentAtStack = !fpuHasOperandSizeOverride && fpuStackSegmentValid && /^(?:FSTENV|FNSTENV) (?:(?:BYTE|WORD|DWORD|QWORD|TBYTE|FWORD) PTR )?\[ESP ?- ?(?:0X0*C|12)\]$/i.test(currentEpInstruction), isFpuInstructionPointerSource = isRawFpuNop || isRawFpuSource || (fpuOpcodeByte >= 0xD8 && fpuOpcodeByte <= 0xDF && /^F[A-Z0-9]+$/.test(currentEpOpCode) && !/^(?:F(?:N)?(?:INIT|CLEX|STCW|STSW|STENV|SAVE)|FLDCW|FLDENV|FRSTOR)$/.test(currentEpOpCode)), popsNativeRegister = /^POP E(?:AX|BX|CX|DX|SI|DI|BP)$/i.test(currentEpInstruction), registerWrite = currentEpInstruction.match(epFlagsNeutralRegisterWritePattern) || currentEpInstruction.match(epTrackedRegisterWritePattern), isNeutralInstruction = currentEpOpCode === "NOP" || currentEpOpCode === "FNOP" || isRawFpuNop || currentEpOpCode === "PAUSE" || epSimplePolymorphicInstructionPattern.test(currentEpInstruction) || (registerWrite && currentEpOpCode !== "POP") || /^(?:CMP|TEST)$/.test(currentEpOpCode); // FSAVE/FNSAVE reinitialize x87, including FIP; a later environment cannot reuse the old source. // https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html (Vol. 2, FSAVE/FNSAVE) if (isRawFpuStateSave || /^(?:FINIT|FNINIT|FSAVE|FNSAVE|FLDENV|FRSTOR)$/.test(currentEpOpCode)) hasFpuInstructionPointerSource = false; else if (isFpuInstructionPointerSource) hasFpuInstructionPointerSource = true; if (storesFpuEnvironmentAtStack || (storesRawFpuEnvironmentAtStack && !fpuHasOperandSizeOverride && fpuStackSegmentValid)) { // FNSTENV preserves the previous x87 FIP; without a reachable x87 source, // its saved pointer cannot be attributed to this entry-point code. isFpuEnvironmentStored = hasFpuInstructionPointerSource; fpuEpNeutralInstructionCount = 0; } else if (popsNativeRegister && isFpuEnvironmentStored) { // A native-width POP consumes the saved EIP and completes the recovery idiom. log(logType.emulation, "FPU EIP recovery near EP: " + currentEpInstruction); isFpuEpRecoveryPresent = true; } else if (isNeutralInstruction && isFpuEnvironmentStored && fpuEpNeutralInstructionCount < epNeutralInstructionLimit) { // Register-only junk cannot move the stack slot containing the saved EIP. fpuEpNeutralInstructionCount++; } else { // A stack-affecting or unknown instruction invalidates the saved-slot proof. isFpuEnvironmentStored = false; fpuEpNeutralInstructionCount = 0; if (isEpLinearScanStopped(currentEpOpCode, fpuEpCurrentAddress, epScanStopOpCodes)) break; } if (isEpLinearFallthroughUnproven(currentEpInstruction, fpuEpCurrentAddress, fpuEpFallthrough)) break; // Continue the physical fall-through sweep using the validated instruction size. fpuEpCurrentAddress += currentEpInstructionLength; } } if (isFpuEpRecoveryPresent) options = addOption(options, "FPU EIP recovery"); // Check for instructions that cannot normally execute in a user-mode process. var isPrivilegedEpOpCodePresent = false; var privilegedEpOpCodeDetected = String(); if (PE_Cached.isEntryPointAnalysisAllowed && !PE_Cached.isDriver && PE_Cached.isArchX86 && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3)) { const privilegedEpOpCodes = [ "CLAC", "CLGI", "CLI", "CLTS", "HLT", "IN", "INSB", "INSD", "INSW", "INVD", "INVLPG", "INVLPGA", "LGDT", "LIDT", "LLDT", "LMSW", "LTR", "OUT", "OUTSB", "OUTSD", "OUTSW", "RDMSR", "RSM", "SKINIT", "STAC", "STGI", "STI", "SWAPGS", "VMCALL", "VMCLEAR", "VMLAUNCH", "VMLOAD", "VMPTRLD", "VMPTRST", "VMREAD", "VMRESUME", "VMRUN", "VMSAVE", "VMWRITE", "VMXOFF", "VMXON", "WBINVD", "WRMSR", "XSETBV" ]; // Sweep only the bounded user-mode startup path; the first privileged operation // is sufficient because its concrete opcode is preserved in the verdict. var privilegedEpFallthrough = {}; for (var i = 0; i < 512 && !isPrivilegedEpOpCodePresent; i++) { var currentEpInstruction = emulateAndGetAsmInstructionByIndex(i), currentEpOpCode = getAsmOpCode(currentEpInstruction); if (!currentEpOpCode) break; // Safety check // MOV requires operand inspection because ordinary register moves are legal, // while any access to CR0-CR8 is privileged in a user-mode image. var isControlRegisterAccess = currentEpOpCode === "MOV" && /\bCR[0-8]\b/i.test(currentEpInstruction); if (privilegedEpOpCodes.indexOf(currentEpOpCode) !== -1 || isControlRegisterAccess) { log(logType.emulation, "Privileged instruction near user-mode EP: " + currentEpInstruction); privilegedEpOpCodeDetected = currentEpOpCode; isPrivilegedEpOpCodePresent = true; } else if (epScanStopOpCodes.indexOf(currentEpOpCode) !== -1) { // A linear sweep cannot prove the return path of a Call or any later bytes. break; } if (isEpLinearFallthroughUnproven(currentEpInstruction, epAsmInstructionAddresses[i], privilegedEpFallthrough)) break; } } if (isPrivilegedEpOpCodePresent) options = addOption(options, "Privileged op " + privilegedEpOpCodeDetected[0] + privilegedEpOpCodeDetected.substring(1).toLowerCase() + " near EP"); // Check for descriptor-table and machine-status probes traditionally used by anti-VM code. var isSystemEpProbePresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && !PE_Cached.isDriver && PE_Cached.isArchX86 && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3)) { const systemEpProbeOpCodes = ["SGDT", "SIDT", "SLDT", "SMSW", "STR"]; // These non-privileged probes are distinctive only while they remain close to the // process entry path, so stop the bounded sweep at a terminal transfer. var systemEpProbeFallthrough = {}; for (var i = 0; i < 512 && !isSystemEpProbePresent; i++) { var currentEpInstruction = emulateAndGetAsmInstructionByIndex(i), currentEpOpCode = getAsmOpCode(currentEpInstruction); if (!currentEpOpCode) break; // Safety check if (systemEpProbeOpCodes.indexOf(currentEpOpCode) !== -1) { // One descriptor-table or machine-status read is a complete probe proof. log(logType.emulation, "System-state probe near EP: " + currentEpInstruction); isSystemEpProbePresent = true; } else if (isEpLinearScanStopped(currentEpOpCode, epAsmInstructionAddresses[i], epScanStopOpCodes)) { // An unproven call return cannot make later bytes reachable. break; } if (isEpLinearFallthroughUnproven(currentEpInstruction, epAsmInstructionAddresses[i], systemEpProbeFallthrough)) break; } } if (isSystemEpProbePresent) options = addOption(options, "System probe near EP"); // Check for a reachable loop that reads successive pages of the mapped image. var isImagePageSweepEpPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && !PE_Cached.isDriver && PE_Cached.isArchX86 && PE_Cached.is64bit && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3)) { isImagePageSweepEpPresent = isEpImagePageSweepProven(epScanStopOpCodes); if (isImagePageSweepEpPresent) log(logType.emulation, "Image page walk near EP"); } if (isImagePageSweepEpPresent) options = addOption(options, "Image page walk near EP"); // Check for a direct transition from a regular user-mode entry stub into the kernel. var isDirectEpSystemCallPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && !PE_Cached.isDriver && PE_Cached.isArchX86 && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3)) { // Scan the bounded startup stream for kernel-entry instructions emitted directly // by the image rather than reached through an imported operating-system wrapper. var directEpSystemCallFallthrough = {}; for (var i = 0; i < 512 && !isDirectEpSystemCallPresent; i++) { var currentEpInstruction = emulateAndGetAsmInstructionByIndex(i), currentEpOpCode = getAsmOpCode(currentEpInstruction); if (!currentEpOpCode) break; // Safety check if (currentEpOpCode === "SYSCALL" || currentEpOpCode === "SYSENTER" || /^INT (?:0X0*2E|46)$/i.test(currentEpInstruction)) { // Cover native x64/x86 fast calls and the legacy Windows INT 2E gateway. log(logType.emulation, "Direct system call near EP: " + currentEpInstruction); isDirectEpSystemCallPresent = true; } else if (isEpLinearScanStopped(currentEpOpCode, epAsmInstructionAddresses[i], epScanStopOpCodes)) { // A terminal transfer ends the startup stream before unrelated bytes. break; } if (isEpLinearFallthroughUnproven(currentEpInstruction, epAsmInstructionAddresses[i], directEpSystemCallFallthrough)) break; } } if (isDirectEpSystemCallPresent) options = addOption(options, "Direct syscall near EP"); // Check whether a direct branch enters the middle of another reachable instruction. var isOverlappingEpCodePresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && !PE_Cached.isDriver && PE_Cached.isArchX86 && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3)) { var overlappingEpPendingPaths = [{ Address: PE_Cached.addressOfUnmanagedEntryPoint, KnownZeroRegisters: {}, ZeroFlag: -1, CarryFlag: -1, StackPointerZero: false }], overlappingEpVisitedStates = {}, overlappingEpDecodedStarts = {}, overlappingEpDecodedInteriors = {}, overlappingEpInstructionCount = 0; // Preserve physical fall-through and follow only direct nearby branch targets. // Never decode bytes after a terminal transfer as though they were executable. for (var streamIndex = 0; streamIndex < overlappingEpPendingPaths.length && streamIndex < 16 && overlappingEpInstructionCount < 512 && !isOverlappingEpCodePresent; streamIndex++) { var overlappingEpPath = overlappingEpPendingPaths[streamIndex], overlappingEpCurrentAddress = overlappingEpPath.Address, overlappingEpKnownZeroRegisters = overlappingEpPath.KnownZeroRegisters, overlappingEpZeroFlag = overlappingEpPath.ZeroFlag, overlappingEpCarryFlag = overlappingEpPath.CarryFlag, overlappingEpStackPointerZero = overlappingEpPath.StackPointerZero; while (overlappingEpInstructionCount < 512) { var currentEpInstruction = PE.getDisasmString(overlappingEpCurrentAddress), currentEpOpCode = getAsmOpCode(currentEpInstruction), currentEpInstructionLength = PE.getDisasmLength(overlappingEpCurrentAddress); if (!currentEpOpCode || currentEpOpCode === "DB" || currentEpInstructionLength <= 0 || currentEpInstructionLength > 15 || !isVirtualAddressRangeFileBacked(overlappingEpCurrentAddress, currentEpInstructionLength, SECTION_FLAGS_EXECUTE)) break; if (PE_Cached.isI386 && /^AAM (?:0X0+|0+)$/i.test(currentEpInstruction)) break; if (isEpProvenFaultingInstruction(currentEpInstruction, currentEpOpCode, overlappingEpStackPointerZero)) break; var overlappingEpStateKey = overlappingEpCurrentAddress + ":" + overlappingEpZeroFlag + ":" + overlappingEpCarryFlag + ":" + overlappingEpStackPointerZero + ":" + Object.keys(overlappingEpKnownZeroRegisters).sort().join(","); if (overlappingEpVisitedStates[overlappingEpStateKey]) break; overlappingEpVisitedStates[overlappingEpStateKey] = true; if (overlappingEpDecodedInteriors[overlappingEpCurrentAddress.toString()]) { log(logType.emulation, "Overlapping branch target near EP at VA 0x" + overlappingEpCurrentAddress.toString(16)); isOverlappingEpCodePresent = true; break; } if (!overlappingEpDecodedStarts[overlappingEpCurrentAddress.toString()]) { for (var byteIndex = 1; byteIndex < currentEpInstructionLength; byteIndex++) { if (overlappingEpDecodedStarts[(overlappingEpCurrentAddress + byteIndex).toString()]) { log(logType.emulation, "Overlapping branch target near EP at VA 0x" + overlappingEpCurrentAddress.toString(16)); isOverlappingEpCodePresent = true; break; } overlappingEpDecodedInteriors[(overlappingEpCurrentAddress + byteIndex).toString()] = true; } if (isOverlappingEpCodePresent) break; overlappingEpDecodedStarts[overlappingEpCurrentAddress.toString()] = true; } overlappingEpInstructionCount++; overlappingEpStackPointerZero = advanceEpStackPointerZero(currentEpInstruction, currentEpOpCode, overlappingEpStackPointerZero); // A proven flat null dereference transfers control to an exception handler; // physical bytes after it are not a reachable fall-through stream. var nullMemoryOperand = currentEpInstruction.match(/(?:^| )(?:(?:CS|DS|ES|SS):)?\[([A-Z][A-Z0-9]*)\]/i), nullMemoryRegister = nullMemoryOperand && getNativeRegisterKey(nullMemoryOperand[1]); if (nullMemoryRegister && overlappingEpKnownZeroRegisters[nullMemoryRegister] && !/^(?:NOP|LEA|PREFETCH|PREFETCHNTA|PREFETCHT[012])$/.test(currentEpOpCode)) break; var zeroedRegister = currentEpInstruction.match(/^(?:XOR|SUB) ([A-Z][A-Z0-9]*), \1$/i) || currentEpInstruction.match(/^MOV ([A-Z][A-Z0-9]*), (?:0X0+|0+)$/i), isFullZeroRegister = zeroedRegister && (epRegister32BitPattern.test(zeroedRegister[1]) || /^R(?:AX|BX|CX|DX|SI|DI|BP|8|9|1[0-5])$/i.test(zeroedRegister[1])); if (isFullZeroRegister) { var zeroRegisterKey = getNativeRegisterKey(zeroedRegister[1]); overlappingEpKnownZeroRegisters[zeroRegisterKey] = true; } else if (!/^(?:NOP|FNOP|PAUSE|CMP|TEST|J[A-Z]+)$/.test(currentEpOpCode)) { overlappingEpKnownZeroRegisters = {}; } var directControlFlow = currentEpInstruction.match(/^(?:CALL|J[A-Z]+|LOOP(?:E|NE|NZ|Z)?) (0X[0-9A-F]+|\d+)$/i), branchResult = directControlFlow && currentEpOpCode !== "CALL" && currentEpOpCode !== "JMP" ? getEpKnownBranchResult(currentEpOpCode, overlappingEpZeroFlag, overlappingEpCarryFlag) : -1, nextZeroFlag = currentEpOpCode === "CALL" ? overlappingEpZeroFlag : advanceEpZeroFlag(currentEpInstruction, currentEpOpCode, overlappingEpZeroFlag), nextCarryFlag = currentEpOpCode === "CALL" ? overlappingEpCarryFlag : advanceEpCarryFlag(currentEpInstruction, currentEpOpCode, overlappingEpCarryFlag); if (directControlFlow && branchResult !== 0 && isEpNativeNearTransfer(overlappingEpCurrentAddress, currentEpInstruction, currentEpInstructionLength)) { var directControlFlowTarget = parseInt(directControlFlow[1], 0); if (Math.abs(directControlFlowTarget - PE_Cached.addressOfUnmanagedEntryPoint) <= 0x200 && isVirtualAddressRangeFileBacked(directControlFlowTarget, 1, SECTION_FLAGS_EXECUTE)) { var branchZeroRegisters = {}; for (var zeroRegister in overlappingEpKnownZeroRegisters) branchZeroRegisters[zeroRegister] = true; overlappingEpPendingPaths.push({ Address: directControlFlowTarget, KnownZeroRegisters: branchZeroRegisters, ZeroFlag: nextZeroFlag, CarryFlag: nextCarryFlag, StackPointerZero: overlappingEpStackPointerZero }); } } if (currentEpOpCode === "CALL" && !isEpDirectCallReturnProven(overlappingEpCurrentAddress)) break; if (currentEpOpCode === "CALL") { overlappingEpKnownZeroRegisters = {}; overlappingEpZeroFlag = -1; overlappingEpCarryFlag = -1; } else { overlappingEpZeroFlag = nextZeroFlag; overlappingEpCarryFlag = nextCarryFlag; } // A proven taken branch has no physical fall-through to decode. if (branchResult === 1) break; if (epScanStopOpCodes.indexOf(currentEpOpCode) !== -1 && currentEpOpCode !== "CALL") break; overlappingEpCurrentAddress += currentEpInstructionLength; } } } if (isOverlappingEpCodePresent) options = addOption(options, "Overlapping code near EP"); // Check for an image address materialized in a register and used as an indirect transfer. var isIndirectEpTransferPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && !PE_Cached.isDriver && PE_Cached.isArchX86 && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3)) { const indirectEpTransferPattern = new RegExp("^(JMP|CALL) (" + epGeneralPurposeRegister + ")$", "i"), indirectEpArithmeticPattern = new RegExp("^(ADD|SUB|XOR) (" + epGeneralPurposeRegister + "), (0X[0-9A-F]+|\\d+)$", "i"), indirectEpLeaPattern = new RegExp("^LEA (" + epGeneralPurposeRegister + "), \\[(RIP|EIP|" + epGeneralPurposeRegister + ")(?: ([+-]) (0X[0-9A-F]+|\\d+))?\\]$", "i"), indirectEpLeaAbsolutePattern = new RegExp("^LEA (" + epGeneralPurposeRegister + "), (?:(?:CS|DS|ES|SS|FS|GS):)?\\[(?:[RE]IZ\\*(?:1|2|4|8) \\+ )?(0X[0-9A-F]+|\\d+)\\]$", "i"), indirectEpRegisterWritePattern = new RegExp("^(?:MOV(?:ABS)?|MOVZX|MOVSX|MOVSXD|LEA|XOR|ADD|SUB|ADC|SBB|AND|OR|NOT|NEG|INC|DEC|POP) (" + epGeneralPurposeRegister + ")(?:,|$)", "i"); // Keep address facts per reachable path: an unrelated write on one branch must // not erase a proof on another. Calls and unknown writes still fail closed. var indirectEpPendingStates = [{ Address: PE_Cached.addressOfUnmanagedEntryPoint, KnownAddresses: {}, ZeroFlag: -1, CarryFlag: -1, StackPointerZero: false }], indirectEpVisitedStates = {}; for (var i = 0; i < indirectEpPendingStates.length && i < 512 && !isIndirectEpTransferPresent; i++) { var indirectEpState = indirectEpPendingStates[i], currentEpAddress = indirectEpState.Address, currentEpInstruction = PE.getDisasmString(currentEpAddress), currentEpOpCode = getAsmOpCode(currentEpInstruction), currentEpLength = PE.getDisasmLength(currentEpAddress), indirectEpKnownAddresses = indirectEpState.KnownAddresses; if (!currentEpOpCode || currentEpOpCode === "DB" || currentEpLength < 1 || currentEpLength > 15 || !isVirtualAddressRangeFileBacked(currentEpAddress, currentEpLength, SECTION_FLAGS_EXECUTE)) continue; if (PE_Cached.isI386 && /^AAM (?:0X0+|0+)$/i.test(currentEpInstruction)) continue; if (isEpProvenFaultingInstruction(currentEpInstruction, currentEpOpCode, indirectEpState.StackPointerZero, indirectEpKnownAddresses)) continue; var indirectEpFactKeys = Object.keys(indirectEpKnownAddresses).sort(), indirectEpStateKey = currentEpAddress + ":" + indirectEpState.ZeroFlag + ":" + indirectEpState.CarryFlag + ":" + indirectEpState.StackPointerZero, indirectEpNextStackPointerZero = advanceEpStackPointerZero(currentEpInstruction, currentEpOpCode, indirectEpState.StackPointerZero); for (var factIndex = 0; factIndex < indirectEpFactKeys.length; factIndex++) { indirectEpStateKey += ":" + indirectEpFactKeys[factIndex] + "=" + indirectEpKnownAddresses[indirectEpFactKeys[factIndex]]; } if (indirectEpVisitedStates[indirectEpStateKey]) continue; indirectEpVisitedStates[indirectEpStateKey] = true; // Classify producers, copies, consumers and invalidators before mutating provenance. var zeroedAddressRegister = currentEpInstruction.match(epZeroRegisterPattern), registerLiteralAddress = currentEpInstruction.match(epLiteralMovePattern), registerCopy = currentEpInstruction.match(epRegisterCopyPattern), registerArithmetic = currentEpInstruction.match(indirectEpArithmeticPattern), registerLea = currentEpInstruction.match(indirectEpLeaPattern), registerLeaAbsolute = currentEpInstruction.match(indirectEpLeaAbsolutePattern), indirectTransfer = currentEpInstruction.match(indirectEpTransferPattern), registerWrite = currentEpInstruction.match(indirectEpRegisterWritePattern), conditionalBranch = currentEpInstruction.match(epConditionalBranchPattern), directJump = currentEpInstruction.match(epDirectJumpPattern), simplePolymorphicInstruction = currentEpInstruction.match(epSimplePolymorphicInstructionPattern), simplePolymorphicRegister = simplePolymorphicInstruction ? (simplePolymorphicInstruction[1] || simplePolymorphicInstruction[2]) : String(), valuePreservingInstruction = currentEpInstruction.match(epValuePreservingInstructionPattern), partialRegisterWrite = currentEpInstruction.match(epPartialRegisterWritePattern), isNeutralInstruction = currentEpOpCode === "NOP" || currentEpOpCode === "FNOP" || currentEpOpCode === "PAUSE" || (simplePolymorphicInstruction && !(PE_Cached.is64bit && epRegister32BitPattern.test(simplePolymorphicRegister))); if (zeroedAddressRegister) { indirectEpKnownAddresses[getNativeRegisterKey(zeroedAddressRegister[1] || zeroedAddressRegister[2])] = 0; } else if (registerLiteralAddress) { var indirectEpTargetRegister = getNativeRegisterKey(registerLiteralAddress[1]), indirectEpTargetAddress = parseInt(registerLiteralAddress[2], 0); // Reject imprecise JavaScript integers before applying architectural truncation. if (indirectEpTargetAddress <= epMaximumSafeInteger) { if (epRegister32BitPattern.test(registerLiteralAddress[1])) indirectEpTargetAddress = indirectEpTargetAddress >>> 0; // A zero remains useful for proving a later null dereference, not as an image target. if (indirectEpTargetAddress === 0 || PE.VAToOffset(indirectEpTargetAddress) !== -1) { indirectEpKnownAddresses[indirectEpTargetRegister] = indirectEpTargetAddress; } else { delete indirectEpKnownAddresses[indirectEpTargetRegister]; } } else { delete indirectEpKnownAddresses[indirectEpTargetRegister]; } } else if (registerCopy) { // Follow one exact copy of the materialized VA. A PE32+ r32 destination // truncates and zero-extends the address, so validate the transformed VA. var indirectEpTargetRegister = getNativeRegisterKey(registerCopy[1]), indirectEpSourceRegister = getNativeRegisterKey(registerCopy[2]), indirectEpTargetAddress = indirectEpKnownAddresses[indirectEpSourceRegister]; if (indirectEpTargetAddress !== undefined) { if (epRegister32BitPattern.test(registerCopy[1])) indirectEpTargetAddress = indirectEpTargetAddress >>> 0; if (indirectEpTargetAddress === 0 || PE.VAToOffset(indirectEpTargetAddress) !== -1) { indirectEpKnownAddresses[indirectEpTargetRegister] = indirectEpTargetAddress; } else { delete indirectEpKnownAddresses[indirectEpTargetRegister]; } } else { delete indirectEpKnownAddresses[indirectEpTargetRegister]; } } else if (registerArithmetic) { var arithmeticRegister = registerArithmetic[2], arithmeticRegisterKey = getNativeRegisterKey(arithmeticRegister), arithmeticAddress = indirectEpKnownAddresses[arithmeticRegisterKey], arithmeticImmediate = parseInt(registerArithmetic[3], 0); var isDwordArithmetic = epRegister32BitPattern.test(arithmeticRegister); if (arithmeticAddress !== undefined && arithmeticImmediate <= (PE_Cached.is64bit ? 0x7FFFFFFF : 0xFFFFFFFF)) { if (registerArithmetic[1] === "ADD") arithmeticAddress += arithmeticImmediate; else if (registerArithmetic[1] === "SUB") arithmeticAddress -= arithmeticImmediate; else arithmeticAddress = arithmeticImmediate === 0 ? arithmeticAddress : isDwordArithmetic ? (arithmeticAddress ^ arithmeticImmediate) >>> 0 : -1; if (isDwordArithmetic) arithmeticAddress >>>= 0; if (arithmeticAddress >= 0 && arithmeticAddress <= epMaximumSafeInteger && (arithmeticAddress === 0 || isVirtualAddressRangeFileBacked(arithmeticAddress, 1, SECTION_FLAGS_EXECUTE))) { indirectEpKnownAddresses[arithmeticRegisterKey] = arithmeticAddress; } else { delete indirectEpKnownAddresses[arithmeticRegisterKey]; } } else { delete indirectEpKnownAddresses[arithmeticRegisterKey]; } } else if (registerLeaAbsolute || (registerLea && /^(?:RIP|EIP)$/.test(registerLea[2]))) { var leaDestination = (registerLeaAbsolute || registerLea)[1], leaDestinationKey = getNativeRegisterKey(leaDestination), leaTargetAddress = getEpEncodedLeaTarget(currentEpAddress, currentEpInstruction, currentEpLength); if (leaTargetAddress >= 0 && leaTargetAddress <= epMaximumSafeInteger && (leaTargetAddress === 0 || isVirtualAddressRangeFileBacked(leaTargetAddress, 1, SECTION_FLAGS_EXECUTE))) { indirectEpKnownAddresses[leaDestinationKey] = leaTargetAddress; } else { delete indirectEpKnownAddresses[leaDestinationKey]; } } else if (registerLea) { var leaDestination = registerLea[1], leaSource = registerLea[2], leaDestinationKey = getNativeRegisterKey(leaDestination), leaSourceKey = getNativeRegisterKey(leaSource), leaDisplacement = registerLea[4] ? parseInt(registerLea[4], 0) : 0, leaBaseAddress = indirectEpKnownAddresses[leaSourceKey], leaWidthsMatch = PE_Cached.is64bit ? !epRegister32BitPattern.test(leaSource) : epRegister32BitPattern.test(leaDestination) && epRegister32BitPattern.test(leaSource); if ((leaWidthsMatch || (leaBaseAddress === 0 && leaDisplacement === 0)) && leaBaseAddress !== undefined && leaDisplacement <= (PE_Cached.is64bit && registerLea[3] === "+" ? 0x7FFFFFFF : 0x80000000)) { var leaTargetAddress = leaBaseAddress + (registerLea[3] === "+" ? leaDisplacement : -leaDisplacement); if (PE_Cached.isI386 || epRegister32BitPattern.test(leaDestination)) leaTargetAddress >>>= 0; if (leaTargetAddress >= 0 && leaTargetAddress <= epMaximumSafeInteger && (leaTargetAddress === 0 || isVirtualAddressRangeFileBacked(leaTargetAddress, 1, SECTION_FLAGS_EXECUTE))) { indirectEpKnownAddresses[leaDestinationKey] = leaTargetAddress; } else { delete indirectEpKnownAddresses[leaDestinationKey]; } } else { delete indirectEpKnownAddresses[leaDestinationKey]; } } else if (indirectTransfer) { var indirectTransferRegister = getNativeRegisterKey(indirectTransfer[2]); var indirectEpTargetAddress = indirectEpKnownAddresses[indirectTransferRegister]; // A mapped target consumed through its proven register completes the proof. if (indirectEpTargetAddress !== undefined && isVirtualAddressRangeFileBacked(indirectEpTargetAddress, 1, SECTION_FLAGS_EXECUTE)) { log(logType.emulation, "Indirect " + indirectTransfer[1] + " transfer near EP to VA 0x" + indirectEpTargetAddress.toString(16)); isIndirectEpTransferPresent = true; } // Any indirect transfer ends the physical path, even when its target is unknown. continue; } else if (isNeutralInstruction || conditionalBranch || directJump || /^(?:CMP|TEST)$/.test(currentEpOpCode)) { // These instructions preserve general-purpose registers on each proven edge. } else if (simplePolymorphicInstruction) { // Only PE32+ r32 self-operations reach this branch. They zero-extend one // physical register but cannot invalidate addresses proven in other registers. var indirectEpSelfRegisterKey = getNativeRegisterKey(simplePolymorphicRegister); if (indirectEpKnownAddresses[indirectEpSelfRegisterKey] !== 0) delete indirectEpKnownAddresses[indirectEpSelfRegisterKey]; } else if (valuePreservingInstruction) { var valuePreservingRegister = valuePreservingInstruction[1] || valuePreservingInstruction[2], valuePreservingRegisterKey = getNativeRegisterKey(valuePreservingRegister), valuePreservingAddress = indirectEpKnownAddresses[valuePreservingRegisterKey]; if (valuePreservingAddress !== undefined && PE_Cached.is64bit && epRegister32BitPattern.test(valuePreservingRegister)) { // Revalidate the zero-extended low DWORD before retaining its provenance. valuePreservingAddress = valuePreservingAddress >>> 0; if (valuePreservingAddress === 0 || PE.VAToOffset(valuePreservingAddress) !== -1) { indirectEpKnownAddresses[valuePreservingRegisterKey] = valuePreservingAddress; } else { delete indirectEpKnownAddresses[valuePreservingRegisterKey]; } } } else if (partialRegisterWrite) { delete indirectEpKnownAddresses[getEpPartialRegisterKey(partialRegisterWrite[1])]; } else if (registerWrite) { // A modeled write invalidates only the destination and preserves other proofs. delete indirectEpKnownAddresses[getNativeRegisterKey(registerWrite[1])]; } else { // Unknown semantics may consume or transform any address, so restart tracking. indirectEpKnownAddresses = {}; if (isEpLinearScanStopped(currentEpOpCode, currentEpAddress, epScanStopOpCodes)) continue; } var indirectEpNextZeroFlag = advanceEpZeroFlag(currentEpInstruction, currentEpOpCode, indirectEpState.ZeroFlag), indirectEpNextCarryFlag = advanceEpCarryFlag(currentEpInstruction, currentEpOpCode, indirectEpState.CarryFlag), indirectEpNextAddress = currentEpAddress + currentEpLength, indirectEpSuccessors = []; if (conditionalBranch) { var indirectEpBranchResult = getEpKnownBranchResult(currentEpOpCode, indirectEpState.ZeroFlag, indirectEpState.CarryFlag); // A PE32 word-sized edge truncates EIP; the decoder's full VA is not its target. if (indirectEpBranchResult !== 0 && isEpNativeNearTransfer(currentEpAddress, currentEpInstruction, currentEpLength)) indirectEpSuccessors.push(parseInt(conditionalBranch[2], 0)); if (indirectEpBranchResult !== 1) indirectEpSuccessors.push(indirectEpNextAddress); } else if (directJump) { if (isEpNativeNearTransfer(currentEpAddress, currentEpInstruction, currentEpLength)) indirectEpSuccessors.push(parseInt(directJump[1], 0)); } else { indirectEpSuccessors.push(indirectEpNextAddress); } for (var successorIndex = 0; successorIndex < indirectEpSuccessors.length; successorIndex++) { var indirectEpSuccessorAddress = indirectEpSuccessors[successorIndex]; if (!isVirtualAddressRangeFileBacked(indirectEpSuccessorAddress, 1, SECTION_FLAGS_EXECUTE)) continue; var indirectEpSuccessorFacts = {}; for (var factKey in indirectEpKnownAddresses) indirectEpSuccessorFacts[factKey] = indirectEpKnownAddresses[factKey]; indirectEpPendingStates.push({ Address: indirectEpSuccessorAddress, KnownAddresses: indirectEpSuccessorFacts, ZeroFlag: indirectEpNextZeroFlag, CarryFlag: indirectEpNextCarryFlag, StackPointerZero: indirectEpNextStackPointerZero }); } } } if (isIndirectEpTransferPresent) options = addOption(options, "Indirect EP transfer"); // Check for native trap-flag inspection or deliberate single-step activation near EP. var isEpTrapFlagCheckPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && !PE_Cached.isDriver && PE_Cached.isArchX86 && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3)) { const trapFlagNativeRegister = PE_Cached.isI386 ? "E(?:AX|BX|CX|DX|SI|DI|BP)" : "R(?:AX|BX|CX|DX|SI|DI|BP|8|9|1[0-5])", trapFlagWordRegister = PE_Cached.isI386 ? "(?:AX|BX|CX|DX|SI|DI|BP)" : "(?:AX|BX|CX|DX|SI|DI|BP|R(?:8|9|1[0-5])W)", trapFlagStackPointer = PE_Cached.isI386 ? "ESP" : "RSP", trapFlagPushPattern = PE_Cached.isI386 ? /^(?:PUSHF|PUSHFD)$/ : /^(?:PUSHF|PUSHFQ)$/, trapFlagPopPattern = PE_Cached.isI386 ? /^(?:POPF|POPFD)$/ : /^(?:POPF|POPFQ)$/, trapFlagPopRegisterPattern = new RegExp("^POP (" + trapFlagNativeRegister + "|" + trapFlagWordRegister + ")$", "i"), trapFlagRegisterTestPattern = new RegExp("^(?:TEST|AND) (" + epFlagsGeneralPurposeRegister + "), (?:0X0*100|256)$|^BT (" + epFlagsGeneralPurposeRegister + "), (?:0X0*8|8)$", "i"), trapFlagStackSetPattern = new RegExp("^OR (?:(?:DWORD|QWORD) PTR )?\\[" + trapFlagStackPointer + "\\], (?:0X0*100|256)$", "i"), trapFlagBitTestBranchOpCodes = ["JAE", "JB", "JC", "JNAE", "JNB", "JNC"], trapFlagValueTestBranchOpCodes = ["JE", "JNE", "JNZ", "JZ"]; // State 1 tracks flags saved on the stack; state 2 follows a POP retaining bit 8; // state 3 waits for a condition compatible with the // TEST/AND or BT flags; state 4 tracks direct TF modification on the stack. var trapFlagState = 0, trapFlagRegisters = {}, trapFlagTestOpCode = String(), trapFlagNeutralInstructionCount = 0, trapFlagFallthrough = {}; // Walk the bounded startup path and accept only one complete flags-save sequence. for (var i = 0; i < 512 && !isEpTrapFlagCheckPresent; i++) { var currentEpInstruction = emulateAndGetAsmInstructionByIndex(i), currentEpOpCode = getAsmOpCode(currentEpInstruction); if (!currentEpOpCode) break; // Safety check // Determine which instruction advances, preserves, or invalidates the active state. var poppedFlagsRegister = currentEpInstruction.match(trapFlagPopRegisterPattern), testedFlagsRegister = currentEpInstruction.match(trapFlagRegisterTestPattern), conditionalBranch = currentEpInstruction.match(epConditionalBranchPattern), registerCopy = currentEpInstruction.match(epRegisterCopyPattern), simplePolymorphicInstruction = currentEpInstruction.match(epSimplePolymorphicInstructionPattern), valuePreservingInstruction = currentEpInstruction.match(epValuePreservingInstructionPattern), partialRegisterWrite = currentEpInstruction.match(epPartialRegisterWritePattern), flagsNeutralRegisterWrite = currentEpInstruction.match(epFlagsNeutralRegisterWritePattern), registerWrite = flagsNeutralRegisterWrite || currentEpInstruction.match(epTrackedRegisterWritePattern), registerWriteKey = registerWrite ? getNativeRegisterKey(registerWrite[1]) : String(), testedFlagsRegisterKey = testedFlagsRegister ? getNativeRegisterKey(testedFlagsRegister[1] || testedFlagsRegister[2]) : String(), isRegisterOnlyInstruction = registerWrite && currentEpOpCode !== "POP"; if (testedFlagsRegister && (testedFlagsRegisterKey.length === 2 || /W$/.test(testedFlagsRegisterKey))) testedFlagsRegisterKey = getEpPartialRegisterKey(testedFlagsRegisterKey); var isTrapFlagStatePreservingInstruction = ((trapFlagState === 1 || trapFlagState === 4) && (isRegisterOnlyInstruction || /^(?:CMP|TEST)$/.test(currentEpOpCode))) || (trapFlagState === 2 && (isRegisterOnlyInstruction || partialRegisterWrite || valuePreservingInstruction)) || (trapFlagState === 3 && flagsNeutralRegisterWrite); var isStatePreservingInstruction = currentEpOpCode === "NOP" || currentEpOpCode === "FNOP" || currentEpOpCode === "PAUSE" || simplePolymorphicInstruction || isTrapFlagStatePreservingInstruction; var isTrapFlagTestedRegister = trapFlagState === 2 && testedFlagsRegister && trapFlagRegisters[testedFlagsRegisterKey]; var isTrapFlagBranchCompatible = trapFlagState === 3 && conditionalBranch && ( (trapFlagTestOpCode === "BT" && trapFlagBitTestBranchOpCodes.indexOf(currentEpOpCode) !== -1) || (trapFlagTestOpCode !== "BT" && trapFlagValueTestBranchOpCodes.indexOf(currentEpOpCode) !== -1) ); if (trapFlagPushPattern.test(currentEpOpCode)) { // State 1: EFLAGS/RFLAGS has been copied to the current stack top. trapFlagState = 1; trapFlagRegisters = {}; trapFlagTestOpCode = String(); trapFlagNeutralInstructionCount = 0; } else if (trapFlagState === 1 && trapFlagStackSetPattern.test(currentEpInstruction)) { // State 4: OR proves the saved Trap Flag bit is set regardless of its old value. trapFlagState = 4; trapFlagNeutralInstructionCount = 0; } else if (trapFlagState === 4 && trapFlagPopPattern.test(currentEpOpCode)) { // Restoring the modified flags deliberately enables single-step execution. log(logType.emulation, "Trap flag enabled near EP: " + currentEpInstruction); isEpTrapFlagCheckPresent = true; } else if (trapFlagState === 1 && poppedFlagsRegister) { // State 2: preserve the TF bit in the popped register and its exact copies. // TF lies in the low WORD, so a word POP proves the same bit without proving upper bits. trapFlagRegisters[poppedFlagsRegister[1].length === 2 || /W$/.test(poppedFlagsRegister[1]) ? getEpPartialRegisterKey(poppedFlagsRegister[1]) : getNativeRegisterKey(poppedFlagsRegister[1])] = true; trapFlagState = 2; trapFlagNeutralInstructionCount = 0; } else if (isTrapFlagTestedRegister) { // State 3: the same saved value is tested specifically for bit 8 / 0x100. trapFlagState = 3; trapFlagTestOpCode = currentEpOpCode; trapFlagNeutralInstructionCount = 0; } else if (isTrapFlagBranchCompatible) { // Accept only branch families which consume the flags produced by BT or TEST/AND. log(logType.emulation, "Trap flag checked near EP: " + currentEpInstruction); isEpTrapFlagCheckPresent = true; } else if (isStatePreservingInstruction && trapFlagState && trapFlagNeutralInstructionCount < epNeutralInstructionLimit) { // Before the explicit TEST/BT, flag-changing junk on unrelated registers // is harmless. Afterwards only instructions that preserve EFLAGS are safe. if (trapFlagState === 2) { // A register copy keeps the source; a write invalidates only its destination. if (registerCopy) { var trapFlagCopyDestination = getNativeRegisterKey(registerCopy[1]), trapFlagCopySource = getNativeRegisterKey(registerCopy[2]); if (trapFlagRegisters[trapFlagCopySource]) trapFlagRegisters[trapFlagCopyDestination] = true; else delete trapFlagRegisters[trapFlagCopyDestination]; } else if (partialRegisterWrite) { delete trapFlagRegisters[getEpPartialRegisterKey(partialRegisterWrite[1])]; } else if (registerWrite && !simplePolymorphicInstruction && !valuePreservingInstruction) { delete trapFlagRegisters[registerWriteKey]; } } if (trapFlagState === 2 && !Object.keys(trapFlagRegisters).length) trapFlagState = 0; trapFlagNeutralInstructionCount++; } else { // Any incompatible instruction breaks the sequence but permits a later restart. trapFlagState = 0; trapFlagRegisters = {}; trapFlagTestOpCode = String(); trapFlagNeutralInstructionCount = 0; if (isEpLinearScanStopped(currentEpOpCode, epAsmInstructionAddresses[i], epScanStopOpCodes)) break; } if (isEpLinearFallthroughUnproven(currentEpInstruction, epAsmInstructionAddresses[i], trapFlagFallthrough)) break; } } if (isEpTrapFlagCheckPresent) options = addOption(options, "Trap flag check near EP"); // Check for dense instruction-level polymorphism in the reachable entry-point code. // A single bounded CFG walk keeps instruction collection independent from individual // verdicts. Conditional branches retain both paths, while a Call fall-through needs // a proven return. Unconditional jumps retain only their target. // Collect orthogonal behavioral facts during one CFG walk. Separate counters let the // verdict rules below combine only the evidence relevant to their own FP threshold. const epPolymorphismBranchRequiredCount = 10, epPolymorphismCallRequiredCount = 2, epPolymorphismPushRequiredCount = 11, epPolymorphismStackImbalanceRequiredCount = 4, epPolymorphismTransformRequiredCount = 7, epPolymorphismTransformKindRequiredCount = 5, epPolymorphismDensityRequiredCount = 30, epInvalidBranchSourceMaximumDistance = 0x200, epRedundantPrefixSourceMaximumDistance = 0x1000, epRedundantPrefixRequiredInstructionCount = 2, epRedundantPrefixImmediateCount = 4; var epNoncanonicalShiftCount = 0, epStackMutationCount = 0, epComplexStackMutationCount = 0, epRareBitOpCount = 0, epPolymorphismBranchCount = 0, epPolymorphismBackwardControlFlowCount = 0, epPolymorphismCallCount = 0, epPolymorphismPushCount = 0, epPolymorphismPopCount = 0, epPolymorphismTransformCount = 0, epPolymorphismIdentityTransformCount = 0, epPolymorphismTransformKindCount = 0, epPolymorphismDominantTransformCount = 0, epBitstreamAdcCount = 0, epBitstreamMovsbCount = 0, epResolverPathProven = false, epRedundantPrefixInstructionCount = 0, epMaximumRedundantPrefixCount = 0, epExportResolverFieldCount = 0, epApiHashMaximumTransformCount = 0, epApiHashMaximumTransformKindCount = 0, epCallPopIpRecoveryAddress = -1, epSelfModifyingCallStubAddress = -1, epInvalidBranchSourceAddress = -1, epInvalidBranchTargetAddress = -1, epFlagsTransferPresent = false, epRolPresent = false, epRorPresent = false, epAdcPresent = false, epBitstreamIndirectTransferPresent = false, epConflictingPrefixPresent = false; var epInvalidBranchTargetOpCode = String(), epStrangeOpCodeDetected = String(); // These instructions are valid CPU operations but remain unusual directly at startup. // The shared CFG may also reach legitimate CRT feature dispatch, so deep occurrences are // retained only inside writable executable code where ordinary compiler output cannot live. const epStrangeOpCodes = { "CPUID": true, "RDTSC": true, "RDTSCP": true, "RDRAND": true, "RDSEED": true }; var epExportResolverHeaderAccessPresent = false; var epExportResolverDirectoryAccessPresent = false, epApiHashByteReadPresent = false, epApiHashBackwardControlFlowPresent = false, epHashedApiResolverIndirectTransferPresent = false; // Count generic self-modifying call stubs per section; one loose match is insufficient. var epRepeatedSelfModifyingCallStubCountBySection = {}; // These service facts are retained only for a previously proven PEB loader walk. // They are converted into one verdict after the shared EP traversal is complete. var epExportResolverFieldsFound = {}, epApiHashTransformsByRegister = {}; if (PE_Cached.isEntryPointAnalysisAllowed && !PE_Cached.isDriver && PE_Cached.isArchX86 && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3)) { // Global and local limits bound the reachable graph and every secondary proof. // Patterns describe semantics rather than concrete registers or byte sequences. const epPolymorphismInstructionLimit = 512, epPolymorphismImageSize = PE.getImageOptionalHeader("SizeOfImage"), epCallPopIpRecoveryLookaheadLimit = 16, epSelfModifyingCallStubLookaheadLimit = 16, epSelfModifyingCallStubNeutralInstructionLimit = 2, epRepeatedSelfModifyingCallStubLookaheadLimit = 24, epRepeatedSelfModifyingCallStubRequiredCount = 3, epRepeatedSelfModifyingCallStubMaximumWriteDistance = 0x100, epShiftRotatePattern = /^(?:SAL|SHL|SHR|SAR|ROL|ROR|RCL|RCR) (.+), (0X[0-9A-F]+|\d+)$/i, epStackMutationPattern = /^(ADD|SUB|ADC|SBB|AND|OR|XOR|INC|DEC|NEG|NOT|SAL|SHL|SHR|SAR|ROL|ROR|RCL|RCR|XADD|XCHG) (?:(?:BYTE|WORD|DWORD|QWORD) PTR )?(?:[A-Z]+:)?\[[^\]]*\b(?:ESP|RSP)\b[^\]]*\](?:,|$)/i, // Follow the architecture-neutral manual export-resolution sequence: // DOS.e_lfanew, the PE32/PE32+ Export directory slot, then independent // IMAGE_EXPORT_DIRECTORY fields. Constants alone never produce a verdict. epExportResolverHeaderAccessPattern = /\[[^\]]*\+\s*(?:0X0*3C|60)(?:\]|\s|\+|-)/i, epExportResolverDirectoryAccessPattern = PE_Cached.isI386 ? /\[[^\]]*\+\s*(?:0X0*78|120)(?:\]|\s|\+|-)/i : /\[[^\]]*\+\s*(?:0X0*88|136)(?:\]|\s|\+|-)/i, epExportResolverFieldAccessPattern = /\[[^\]]*\+\s*(0X0*(?:18|1C|20|24)|24|28|32|36)(?:\]|\s|\+|-)/i, epApiHashByteReadPattern = /^(?:MOV(?:ZX|SX)? [^,]+, BYTE PTR (?:[A-Z]+:)?\[[^\]]+\]|LODSB(?: |$))/i, // Hash arithmetic must accumulate on one physical register. Register spelling // is normalized later so EAX/RAX and R8D/R8 cannot split the same data flow. epApiHashTransformPattern = new RegExp("^(ROR|ROL|XOR|ADD|ADC|SUB|IMUL) (" + epGeneralPurposeRegister + "),", "i"), epPopGeneralPurposeRegisterPattern = new RegExp("^POP (" + epGeneralPurposeRegister + ")$", "i"), epBitstreamGeneralPurposeRegister = "(?:[ABCD][HL]|BPL|SIL|DIL|R(?:8|9|1[0-5])B|" + epFlagsGeneralPurposeRegister + ")", epBitstreamAdcPattern = new RegExp("^ADC (" + epBitstreamGeneralPurposeRegister + "), \\1$", "i"), epBitstreamScalarReadPattern = /^MOV ([ABCD][HL]), BYTE PTR \[([A-Z][A-Z0-9]*)\]$/i, epByteRegisterPattern = /^(?:[ABCD][HL]|SPL|BPL|SIL|DIL|R(?:8|9|1[0-5])B)$/i, epWordRegisterPattern = /^(?:AX|BX|CX|DX|SP|BP|SI|DI|R(?:8|9|1[0-5])W)$/i, epDwordRegisterPattern = /^(?:E(?:AX|BX|CX|DX|SP|BP|SI|DI)|R(?:8|9|1[0-5])D)$/i, epQwordRegisterPattern = /^(?:R(?:AX|BX|CX|DX|SP|BP|SI|DI|8|9|1[0-5]))$/i, epSelfModifyingCallStubNativePointer = PE_Cached.isI386 ? "DWORD" : "QWORD", epSelfModifyingCallStubNativeAccumulator = PE_Cached.isI386 ? "EAX" : "RAX", epSelfModifyingCallStubReturnStorePattern = new RegExp("^POP " + epSelfModifyingCallStubNativePointer + " PTR (?:[A-Z]+:)?\\[[^\\]]+\\]$", "i"), epSelfModifyingCallStubAccumulatorPushPattern = new RegExp("^PUSH " + epSelfModifyingCallStubNativeAccumulator + "$", "i"), epSelfModifyingCallStubKeyLoadPattern = /^MOV EAX, (?:(?:DWORD PTR )?(?:[A-Z]+:)?\[[^\]]+\]|0X[0-9A-F]+|\d+)$/i, epSelfModifyingCallStubDecodePattern = /^(?:LEA EAX, \[EAX (?:\+|-) (?:0X[0-9A-F]+|\d+)\]|(?:ADD|SUB|XOR) EAX, (?:0X[0-9A-F]+|\d+))$/i, // FS/GS have unproven bases; CS cannot be written in x86 but its override is ignored in x64. // Intel SDM Vol. 1, 3.7.4.1: https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-1-manual.pdf epSelfModifyingCallStubFlatWriteSegment = PE_Cached.isI386 ? "(?:(?:DS|ES|SS):)?" : "(?:(?:CS|DS|ES|SS):)?", epSelfModifyingCallStubWritePattern = new RegExp("^XCHG DWORD PTR " + epSelfModifyingCallStubFlatWriteSegment + "\\[(?:RIP\\s*([+-])\\s*(0X[0-9A-F]+|\\d+)|(0X[0-9A-F]+|\\d+))\\], EAX$", "i"), epSelfModifyingCallStubAccumulatorPopPattern = new RegExp("^POP " + epSelfModifyingCallStubNativeAccumulator + "$", "i"), epRepeatedSelfModifyingCallStubNativeRegister = PE_Cached.isI386 ? "E(?:AX|BX|CX|DX|SI|DI|BP)" : "R(?:AX|BX|CX|DX|SI|DI|BP|8|9|1[0-5])", epRepeatedSelfModifyingCallStubStackPointer = PE_Cached.isI386 ? "ESP" : "RSP", epRepeatedSelfModifyingCallStubReturnReadPattern = new RegExp("^(?:POP (?:" + epSelfModifyingCallStubNativePointer + " PTR (?:[A-Z]+:)?\\[[^\\]]+\\]|" + epRepeatedSelfModifyingCallStubNativeRegister + ")|(?:MOV|LEA) " + epRepeatedSelfModifyingCallStubNativeRegister + ", (?:" + epSelfModifyingCallStubNativePointer + " PTR )?(?:[A-Z]+:)?\\[" + epRepeatedSelfModifyingCallStubStackPointer + "(?:\\s*[+-]\\s*(?:0X[0-9A-F]+|\\d+))?\\])$", "i"), epRepeatedSelfModifyingCallStubTransformPattern = new RegExp("^(?:ADD|ADC|SUB|SBB|XOR|AND|OR|NOT|NEG|ROL|ROR|RCL|RCR|LEA|MOVZX|MOVSX|MOVSXD|BSWAP) (" + epGeneralPurposeRegister + ")(?:,|$)", "i"), epRepeatedSelfModifyingCallStubWritePattern = new RegExp("^(?:MOV|XCHG|CMPXCHG) (?:BYTE|WORD|DWORD|QWORD) PTR " + epSelfModifyingCallStubFlatWriteSegment + "\\[(?:RIP\\s*([+-])\\s*(0X[0-9A-F]+|\\d+)|(0X[0-9A-F]+|\\d+))\\], (" + epGeneralPurposeRegister + ")$", "i"), epComplexStackMutationOpCodes = [ "NEG", "NOT", "SAL", "SHL", "SHR", "SAR", "ROL", "ROR", "RCL", "RCR", "XADD", "XCHG" ], epRareBitOpCodes = [ "ADC", "SBB", "ROL", "ROR", "BT", "BTC", "BTR", "BTS", "BSWAP", "XADD" ], // These uncommon transformations are safe only as one component of the compound // flow profile. Requiring repetition avoids treating ordinary low-level code as junk. epPolymorphismTransformOpCodes = [ "AAA", "AAD", "AAM", "AAS", "ADC", "BSF", "BSR", "BSWAP", "BT", "BTC", "BTR", "BTS", "CMC", "DAA", "DAS", "LAHF", "RCL", "RCR", "ROL", "ROR", "SAHF", "SAL", "SHLD", "SHRD", "STC", "XADD", "XCHG" ], // Exact identity operations are useful polymorphism evidence even when their // opcodes are ordinary. Restrict matching to the already proven semantic forms; // a normal Add, Mov or Lea must never become evidence by opcode alone. epPolymorphismIdentityTransformOpCodes = { "ADD": true, "IMUL": true, "LEA": true, "MOV": true, "OR": true, "SAR": true, "SHL": true, "SHR": true, "SUB": true, "XOR": true }; // The FIFO starts at EP. Known zero and carry flags exclude impossible // conditional edges without guessing the outcome of other branches. var epPolymorphismPendingAddresses = [PE_Cached.addressOfUnmanagedEntryPoint], epPolymorphismPendingZeroFlags = [-1], epPolymorphismPendingStackPointerZeros = [false], epPolymorphismPendingCarryFlags = [-1], epPolymorphismPendingAddressIndex = 0, epPolymorphismVisitedAddresses = {}, epPolymorphismVisitedStates = {}, epPolymorphismVisitedInstructionCount = 0, epPolymorphismTransformOpCodesFound = {}, epRareBitOpCodesFound = {}, epDecodedInstructionLengthByAddress = {}, epDirectCallTargets = {}, epPopRegisterByAddress = {}, epInvalidBranchCandidates = [], epValidInvalidBranchCandidates = [], epRedundantPrefixCandidates = [], epBitstreamBackwardEdges = [], epBitstreamScalarReadCandidates = [], epBitstreamIndirectTransferAddresses = [], epBitstreamSuccessors = {}, epBitstreamFacts = {}, epBitstreamInstructions = {}, epResolverInstructions = {}; var epBitstreamStartAddresses = [PE_Cached.addressOfUnmanagedEntryPoint]; // A statically proven synthetic Ret is an unconditional branch in disguise. Continue // from its image target so the shared CFG sees the real decoder behind the EP facade. if (isSyntheticEpTransferPresent && syntheticEpTargetAddress) { epPolymorphismPendingAddresses.push(parseInt(syntheticEpTargetAddress, 0)); epPolymorphismPendingZeroFlags.push(-1); epPolymorphismPendingStackPointerZeros.push(false); epPolymorphismPendingCarryFlags.push(-1); epBitstreamStartAddresses.push(parseInt(syntheticEpTargetAddress, 0)); } // Consume one reachable address at a time until the graph or global budget is exhausted. while (epPolymorphismPendingAddressIndex < epPolymorphismPendingAddresses.length && epPolymorphismVisitedInstructionCount < epPolymorphismInstructionLimit && epPolymorphismPendingAddressIndex < epPolymorphismInstructionLimit * 4) { var epPolymorphismCurrentAddress = epPolymorphismPendingAddresses[epPolymorphismPendingAddressIndex], epPolymorphismCurrentZeroFlag = epPolymorphismPendingZeroFlags[epPolymorphismPendingAddressIndex], epPolymorphismCurrentStackPointerZero = epPolymorphismPendingStackPointerZeros[epPolymorphismPendingAddressIndex], epPolymorphismCurrentCarryFlag = epPolymorphismPendingCarryFlags[epPolymorphismPendingAddressIndex++], epPolymorphismAddressKey = epPolymorphismCurrentAddress.toString(), epPolymorphismStateKey = epPolymorphismAddressKey + ":" + epPolymorphismCurrentZeroFlag + ":" + epPolymorphismCurrentCarryFlag + ":" + epPolymorphismCurrentStackPointerZero, epPolymorphismCurrentRva = epPolymorphismCurrentAddress - PE_Cached.imageBase, epPolymorphismSectionIndex = -1; // Skip loops and addresses which have no file-backed VA mapping. if (epPolymorphismVisitedStates[epPolymorphismStateKey] || PE.VAToOffset(epPolymorphismCurrentAddress) === -1) continue; epPolymorphismVisitedStates[epPolymorphismStateKey] = true; // VAToOffset alone does not prove that the address contains executable physical // bytes. Resolve the section explicitly to avoid decoding data or virtual padding. for (var d = 0; d < PE_Cached.numberOfSections && epPolymorphismSectionIndex === -1; d++) { var epPolymorphismSectionRva = PE.getSectionVirtualAddress(d), epPolymorphismSectionRawSize = PE.getSectionFileSize(d); if (epPolymorphismCurrentRva >= epPolymorphismSectionRva && epPolymorphismCurrentRva < epPolymorphismSectionRva + epPolymorphismSectionRawSize && (PE.section[d].Flags & SECTION_FLAGS_EXECUTE)) { epPolymorphismSectionIndex = d; } } if (epPolymorphismSectionIndex === -1) continue; var epPolymorphismInstruction = PE.getDisasmString(epPolymorphismCurrentAddress), epPolymorphismOpCode = getAsmOpCode(epPolymorphismInstruction), epPolymorphismInstructionLength = PE.getDisasmLength(epPolymorphismCurrentAddress); if (!epPolymorphismOpCode || epPolymorphismOpCode === "DB" || epPolymorphismInstructionLength < 1 || epPolymorphismInstructionLength > 15 || !isVirtualAddressRangeFileBacked(epPolymorphismCurrentAddress, epPolymorphismInstructionLength, SECTION_FLAGS_EXECUTE)) continue; if (/^AAM (?:0X0+|0+)$/i.test(epPolymorphismInstruction)) continue; // A proven null user-mode SP faults on stack access; its callee and continuation are unreachable. if (isEpProvenFaultingInstruction(epPolymorphismInstruction, epPolymorphismOpCode, epPolymorphismCurrentStackPointerZero)) continue; var epPolymorphismNextStackPointerZero = advanceEpStackPointerZero(epPolymorphismInstruction, epPolymorphismOpCode, epPolymorphismCurrentStackPointerZero); // Word-sized taken transfers cannot seed native-VA callee or branch evidence from the decoder. if ((epPolymorphismOpCode === "CALL" || epPolymorphismOpCode === "JMP" || (/^(?:J(?!MPF)[A-Z]+|LOOP(?:E|NE|NZ|Z)?)$/.test(epPolymorphismOpCode) && getEpKnownBranchResult(epPolymorphismOpCode, epPolymorphismCurrentZeroFlag, epPolymorphismCurrentCarryFlag) !== 0)) && !isEpNativeNearTransfer(epPolymorphismCurrentAddress, epPolymorphismInstruction, epPolymorphismInstructionLength)) { continue; } // Count evidence only after this path is admitted: a rejected arrival must not consume another path's first visit. var epPolymorphismIsRevisit = !!epPolymorphismVisitedAddresses[epPolymorphismAddressKey]; if (!epPolymorphismIsRevisit) { epPolymorphismVisitedAddresses[epPolymorphismAddressKey] = true; epPolymorphismVisitedInstructionCount++; } if (epPolymorphismIsRevisit) { // Revisit only the control flow. Re-counting opcode evidence would inflate // heuristics, while merging flag-dependent edges into the shared CFG could // make unrelated paths appear to be one executable path. var revisitNextAddress = epPolymorphismCurrentAddress + epPolymorphismInstructionLength, revisitZeroFlag = advanceEpZeroFlag(epPolymorphismInstruction, epPolymorphismOpCode, epPolymorphismCurrentZeroFlag), revisitCarryFlag = advanceEpCarryFlag(epPolymorphismInstruction, epPolymorphismOpCode, epPolymorphismCurrentCarryFlag), revisitBranch = epPolymorphismInstruction.match(/^(CALL|J[A-Z]+|LOOP(?:E|NE|NZ|Z)?) (0X[0-9A-F]+|\d+)$/i); if (revisitBranch) { var revisitTargetAddress = parseInt(revisitBranch[2], 0); if (epPolymorphismOpCode === "CALL") { if (isEpDirectCallReturnProven(epPolymorphismCurrentAddress)) { epPolymorphismPendingAddresses.push(revisitNextAddress); epPolymorphismPendingZeroFlags.push(-1); epPolymorphismPendingStackPointerZeros.push(epPolymorphismNextStackPointerZero); epPolymorphismPendingCarryFlags.push(-1); } epPolymorphismPendingAddresses.push(revisitTargetAddress); epPolymorphismPendingZeroFlags.push(epPolymorphismCurrentZeroFlag); epPolymorphismPendingStackPointerZeros.push(epPolymorphismNextStackPointerZero); epPolymorphismPendingCarryFlags.push(epPolymorphismCurrentCarryFlag); } else { var revisitBranchResult = getEpKnownBranchResult(epPolymorphismOpCode, revisitZeroFlag, revisitCarryFlag); if (revisitBranchResult !== 0) { epPolymorphismPendingAddresses.push(revisitTargetAddress); epPolymorphismPendingZeroFlags.push(revisitZeroFlag); epPolymorphismPendingStackPointerZeros.push(epPolymorphismNextStackPointerZero); epPolymorphismPendingCarryFlags.push(revisitCarryFlag); } if (epPolymorphismOpCode !== "JMP" && epPolymorphismOpCode !== "JMPF" && revisitBranchResult !== 1) { epPolymorphismPendingAddresses.push(revisitNextAddress); epPolymorphismPendingZeroFlags.push(revisitZeroFlag); epPolymorphismPendingStackPointerZeros.push(epPolymorphismNextStackPointerZero); epPolymorphismPendingCarryFlags.push(revisitCarryFlag); } } } else if (epScanStopOpCodes.indexOf(epPolymorphismOpCode) === -1) { epPolymorphismPendingAddresses.push(revisitNextAddress); epPolymorphismPendingZeroFlags.push(revisitZeroFlag); epPolymorphismPendingStackPointerZeros.push(epPolymorphismNextStackPointerZero); epPolymorphismPendingCarryFlags.push(revisitCarryFlag); } continue; } // Inspect prefix groups at a proven physical instruction boundary. The AMD64 // APM, volume 3, section 1.2.1 allows one legacy prefix per group; multiple // members of one group have undefined results, not guaranteed redundancy. var epPolymorphismInstructionOffset = PE.VAToOffset(epPolymorphismCurrentAddress), epPolymorphismSectionFileEnd = PE.getSectionFileOffset(epPolymorphismSectionIndex) + PE.getSectionFileSize(epPolymorphismSectionIndex), epCurrentPrefixCount = 0, epCurrentPrefixGroupOneCount = 0, epCurrentLockPrefixCount = 0, epCurrentPrefixGroupTwoCount = 0, epCurrentPrefixGroupThreeCount = 0, epCurrentPrefixGroupFourCount = 0, epCurrentRexPrefixCount = 0, epCurrentGroupOnePrefix = -1, epCurrentConflictingPrefixPresent = false, epCurrentRedundantPrefixPresent = false; var epCurrentGroupTwoPrefixesFound = {}; // Count raw prefixes only inside the proven decoded instruction. The last // byte must remain available for its opcode, even at the 15-byte limit. for (var p = 0; p < 14 && p < epPolymorphismInstructionLength - 1 && epPolymorphismInstructionOffset + p < epPolymorphismSectionFileEnd; p++) { var epCurrentPrefixByte = PE.readByte(epPolymorphismInstructionOffset + p); if (epCurrentPrefixByte === 0xF0 || epCurrentPrefixByte === 0xF2 || epCurrentPrefixByte === 0xF3) { if (epCurrentGroupOnePrefix !== -1 && epCurrentGroupOnePrefix !== epCurrentPrefixByte) { epCurrentConflictingPrefixPresent = true; } epCurrentGroupOnePrefix = epCurrentPrefixByte; epCurrentPrefixGroupOneCount++; if (epCurrentPrefixByte === 0xF0) epCurrentLockPrefixCount++; epCurrentRedundantPrefixPresent = epCurrentRedundantPrefixPresent || epCurrentPrefixGroupOneCount > 1; } else if (epCurrentPrefixByte === 0x2E || epCurrentPrefixByte === 0x36 || epCurrentPrefixByte === 0x3E || epCurrentPrefixByte === 0x26 || epCurrentPrefixByte === 0x64 || epCurrentPrefixByte === 0x65) { var epCurrentGroupTwoPrefixKey = epCurrentPrefixByte.toString(); // Different segment overrides are evidence only when several prefixes // flood one proven instruction; one override alone can be ordinary code. epCurrentPrefixGroupTwoCount++; if (epCurrentGroupTwoPrefixesFound[epCurrentGroupTwoPrefixKey]) { epCurrentRedundantPrefixPresent = true; } else { epCurrentGroupTwoPrefixesFound[epCurrentGroupTwoPrefixKey] = true; } } else if (epCurrentPrefixByte === 0x66) { epCurrentPrefixGroupThreeCount++; epCurrentRedundantPrefixPresent = epCurrentRedundantPrefixPresent || epCurrentPrefixGroupThreeCount > 1; } else if (epCurrentPrefixByte === 0x67) { epCurrentPrefixGroupFourCount++; epCurrentRedundantPrefixPresent = epCurrentRedundantPrefixPresent || epCurrentPrefixGroupFourCount > 1; } else if (PE_Cached.isAMD64 && epCurrentPrefixByte >= 0x40 && epCurrentPrefixByte <= 0x4F) { epCurrentRexPrefixCount++; epCurrentRedundantPrefixPresent = epCurrentRedundantPrefixPresent || epCurrentRexPrefixCount > 1; } else { break; } epCurrentPrefixCount++; } if (epCurrentPrefixGroupTwoCount > 1 && epCurrentPrefixCount >= 4) epCurrentRedundantPrefixPresent = true; // XACQUIRE/XRELEASE may accompany LOCK on a memory read-modify-write instruction. // Intel SDM, XACQUIRE/XRELEASE: https://cdrdv2-public.intel.com/789589/334569-sdm-vol-2d.pdf if (epCurrentPrefixGroupOneCount === 2 && epCurrentLockPrefixCount === 1 && epCurrentPrefixCount + 1 < epPolymorphismInstructionLength) { var epHleOpcodeOffset = epPolymorphismInstructionOffset + epCurrentPrefixCount, epHleOpcode = PE.readByte(epHleOpcodeOffset), epHleModRmOffset = epHleOpcodeOffset + 1, epHleTwoByteOpcode = epHleOpcode === 0x0F ? PE.readByte(epHleModRmOffset++) : -1, epHleModRm = epHleModRmOffset < epPolymorphismInstructionOffset + epPolymorphismInstructionLength ? PE.readByte(epHleModRmOffset) : -1, epHleGroup = (epHleModRm >> 3) & 7; if (epHleModRm >= 0 && (epHleModRm & 0xC0) !== 0xC0 && ((epHleOpcode <= 0x31 && (epHleOpcode & 7) <= 1) || epHleOpcode === 0x86 || epHleOpcode === 0x87 || ((epHleOpcode === 0x80 || epHleOpcode === 0x81 || epHleOpcode === 0x83) && epHleGroup < 7) || ((epHleOpcode === 0xFE || epHleOpcode === 0xFF) && epHleGroup <= 1) || ((epHleOpcode === 0xF6 || epHleOpcode === 0xF7) && (epHleGroup === 2 || epHleGroup === 3)) || (epHleOpcode === 0x0F && (epHleTwoByteOpcode === 0xB0 || epHleTwoByteOpcode === 0xB1 || epHleTwoByteOpcode === 0xC0 || epHleTwoByteOpcode === 0xC1 || epHleTwoByteOpcode === 0xAB || epHleTwoByteOpcode === 0xB3 || epHleTwoByteOpcode === 0xBB || (epHleTwoByteOpcode === 0xBA && epHleGroup >= 5) || (epHleTwoByteOpcode === 0xC7 && epHleGroup === 1))))) { epCurrentConflictingPrefixPresent = false; epCurrentRedundantPrefixPresent = epCurrentPrefixGroupTwoCount > Object.keys(epCurrentGroupTwoPrefixesFound).length || (epCurrentPrefixGroupTwoCount > 1 && epCurrentPrefixCount >= 4) || epCurrentPrefixGroupThreeCount > 1 || epCurrentPrefixGroupFourCount > 1 || epCurrentRexPrefixCount > 1; } } // Save raw candidates until every decoded boundary is known. Linkers deliberately // encode long Nop instructions with repeated 66 prefixes for alignment. if (epCurrentRedundantPrefixPresent && epPolymorphismOpCode !== "NOP" && epPolymorphismOpCode !== "FNOP" && epPolymorphismOpCode !== "PAUSE" && !/^XCHG (AX|EAX|RAX), \1$/i.test(epPolymorphismInstruction) && Math.abs(epPolymorphismCurrentAddress - PE_Cached.addressOfUnmanagedEntryPoint) <= epRedundantPrefixSourceMaximumDistance) { epRedundantPrefixCandidates.push({ Address: epPolymorphismCurrentAddress, PrefixCount: epCurrentPrefixCount, IsConflicting: epCurrentConflictingPrefixPresent }); } // Prefix evidence remains useful even when the disassembler rejects an otherwise // valid instruction because of deliberately excessive encoding. Semantic analysis // below still requires a decoded opcode and a positive instruction length. if (!epPolymorphismOpCode || epPolymorphismInstructionLength <= 0) continue; epBitstreamInstructions[epPolymorphismAddressKey] = epPolymorphismInstruction; if (isPebModuleWalkPresent) epResolverInstructions[epPolymorphismAddressKey] = epPolymorphismInstruction; epDecodedInstructionLengthByAddress[epPolymorphismAddressKey] = epPolymorphismInstructionLength; epBitstreamSuccessors[epPolymorphismAddressKey] = []; if (!epStrangeOpCodeDetected && epStrangeOpCodes[epPolymorphismOpCode] && ((PE.section[epPolymorphismSectionIndex].Flags & SECTION_FLAGS_WRITE) !== 0 || (epPolymorphismOpCode !== "CPUID" && Math.abs(epPolymorphismCurrentAddress - PE_Cached.addressOfUnmanagedEntryPoint) <= 0x40))) { epStrangeOpCodeDetected = epPolymorphismOpCode; } // Count explicit stack traffic. Calls are kept separate because their implicit // return-address push has normal compiler semantics and must not inflate Push. if (epPolymorphismOpCode === "PUSH") epPolymorphismPushCount++; if (epPolymorphismOpCode === "POP") epPolymorphismPopCount++; // Repeated uncommon transforms distinguish generated decoder/dispatcher code from // a compiler startup routine. Exact identity operations add semantic mutations // which generators commonly vary without broadening ordinary arithmetic matches. var epPolymorphismTransformKind = String(); if (epPolymorphismTransformOpCodes.indexOf(epPolymorphismOpCode) !== -1) { epPolymorphismTransformKind = epPolymorphismOpCode; } else if (epPolymorphismIdentityTransformOpCodes[epPolymorphismOpCode]) { var epPolymorphismIdentityInstruction = epPolymorphismInstruction.match(epSimplePolymorphicInstructionPattern) || epPolymorphismInstruction.match(epValuePreservingInstructionPattern), epPolymorphismIdentityRegister = epPolymorphismIdentityInstruction ? (epPolymorphismIdentityInstruction[1] || epPolymorphismIdentityInstruction[2]) : String(); // A 32-bit destination clears the upper half on AMD64 and is therefore not // an identity transformation of the complete physical register. if (epPolymorphismIdentityInstruction && !(PE_Cached.is64bit && epRegister32BitPattern.test(epPolymorphismIdentityRegister))) { epPolymorphismTransformKind = "IDENTITY_" + epPolymorphismOpCode; epPolymorphismIdentityTransformCount++; } } if (epPolymorphismTransformKind) { epPolymorphismTransformCount++; if (!epPolymorphismTransformOpCodesFound[epPolymorphismTransformKind]) { epPolymorphismTransformOpCodesFound[epPolymorphismTransformKind] = 0; epPolymorphismTransformKindCount++; } epPolymorphismTransformOpCodesFound[epPolymorphismTransformKind]++; if (epPolymorphismTransformOpCodesFound[epPolymorphismTransformKind] > epPolymorphismDominantTransformCount) { epPolymorphismDominantTransformCount = epPolymorphismTransformOpCodesFound[epPolymorphismTransformKind]; } } // Count every reachable call spelling, including indirect dispatcher calls. if (epPolymorphismOpCode === "CALL") epPolymorphismCallCount++; // Conditional, unconditional and loop branches all contribute to CFG fragmentation. if (/^(?:J[A-Z]+|LOOP(?:E|NE|NZ|Z)?)$/.test(epPolymorphismOpCode)) { epPolymorphismBranchCount++; } // Keep both sides of Call/Pop pairs because a branch may make the target appear // before its call in the worklist. A match is accepted only after both addresses // have independently passed the executable, physically-backed section check. var epPopGeneralPurposeRegister = epPolymorphismInstruction.match(epPopGeneralPurposeRegisterPattern); if (epPopGeneralPurposeRegister) { // Record the exact native register used to recover a possible return address. epPopRegisterByAddress[epPolymorphismAddressKey] = epPopGeneralPurposeRegister[1]; } // Count the two independent primitives required by the bitstream-unpacker rule. var epBitstreamIsAdc = epBitstreamAdcPattern.test(epPolymorphismInstruction), epBitstreamIsRepMovsb = /^REP MOVSB(?: |$)/i.test(epPolymorphismInstruction); if (epBitstreamIsAdc) epBitstreamAdcCount++; if (epBitstreamIsRepMovsb) epBitstreamMovsbCount++; if (epBitstreamIsAdc || epBitstreamIsRepMovsb) { epBitstreamFacts[epPolymorphismAddressKey] = { Adc: epBitstreamIsAdc, RepMovsb: epBitstreamIsRepMovsb }; } if (epPolymorphismOpCode === "MOV" && epPolymorphismInstruction.indexOf("BYTE PTR") !== -1 && epBitstreamScalarReadCandidates.length < 32) { var epBitstreamScalarRead = epPolymorphismInstruction.match(epBitstreamScalarReadPattern); if (epBitstreamScalarRead) { epBitstreamScalarReadCandidates.push({ Address: epPolymorphismCurrentAddress, Length: epPolymorphismInstructionLength, ValueRegister: epBitstreamScalarRead[1], SourceRegister: epBitstreamScalarRead[2] }); } } var epShiftRotate = epPolymorphismInstruction.match(epShiftRotatePattern); if (epShiftRotate) { var epShiftDestination = epShiftRotate[1], epShiftOperandWidth = 0; // Resolve operand width from either an explicit memory size or register class. if (/^BYTE PTR /i.test(epShiftDestination) || epByteRegisterPattern.test(epShiftDestination)) { epShiftOperandWidth = 8; } else if (/^WORD PTR /i.test(epShiftDestination) || epWordRegisterPattern.test(epShiftDestination)) { epShiftOperandWidth = 16; } else if (/^DWORD PTR /i.test(epShiftDestination) || epDwordRegisterPattern.test(epShiftDestination)) { epShiftOperandWidth = 32; } else if (/^QWORD PTR /i.test(epShiftDestination) || epQwordRegisterPattern.test(epShiftDestination)) { epShiftOperandWidth = 64; } // x86 masks immediate shift/rotate counts. Encoding a count at least as wide // as the operand is therefore a deliberately noncanonical spelling which // ordinary compilers normalize to the equivalent smaller immediate. if (epShiftOperandWidth && parseInt(epShiftRotate[2], 0) >= epShiftOperandWidth) { epNoncanonicalShiftCount++; } } var epStackMutation = epPolymorphismInstruction.match(epStackMutationPattern); if (epStackMutation) { // Count all arithmetic writes through SP and separately count uncommon transforms. epStackMutationCount++; if (epComplexStackMutationOpCodes.indexOf(epStackMutation[1]) !== -1) { epComplexStackMutationCount++; } } // Flag-transfer and bit-operation facts jointly describe a dense bit mixer. if (/^(?:PUSHF|PUSHFD|PUSHFQ|POPF|POPFD|POPFQ|LAHF|SAHF)$/.test(epPolymorphismOpCode)) { epFlagsTransferPresent = true; } epRolPresent = epRolPresent || epPolymorphismOpCode === "ROL"; epRorPresent = epRorPresent || epPolymorphismOpCode === "ROR"; epAdcPresent = epAdcPresent || epPolymorphismOpCode === "ADC"; if (epRareBitOpCodes.indexOf(epPolymorphismOpCode) !== -1 && !epRareBitOpCodesFound[epPolymorphismOpCode]) { epRareBitOpCodesFound[epPolymorphismOpCode] = true; epRareBitOpCount++; } // Accumulate resolver facts inside the shared traversal to avoid a second // Bounded EP pass. PEB->Ldr must already be proven before collection. if (isPebModuleWalkPresent) { // Observe the stages in order. A later constant or byte loop is ignored until // the preceding PE-header relationship has appeared on reachable code paths. epExportResolverHeaderAccessPresent = epExportResolverHeaderAccessPresent || epExportResolverHeaderAccessPattern.test(epPolymorphismInstruction); if (epExportResolverHeaderAccessPresent && epExportResolverDirectoryAccessPattern.test(epPolymorphismInstruction)) { epExportResolverDirectoryAccessPresent = true; } if (epExportResolverDirectoryAccessPresent) { epApiHashByteReadPresent = epApiHashByteReadPresent || epApiHashByteReadPattern.test(epPolymorphismInstruction); var epExportResolverFieldAccess = epPolymorphismInstruction.match(epExportResolverFieldAccessPattern); if (epExportResolverFieldAccess) { var epExportResolverFieldKey = parseInt(epExportResolverFieldAccess[1], 0).toString(); // Count distinct fields, not repeated reads of one field inside a loop. // This separates export parsing from unrelated structure traversal. if (!epExportResolverFieldsFound[epExportResolverFieldKey]) { epExportResolverFieldsFound[epExportResolverFieldKey] = true; epExportResolverFieldCount++; } } var epApiHashTransform = epPolymorphismInstruction.match(epApiHashTransformPattern); var epApiHashCopy = epPolymorphismInstruction.match(epRegisterCopyPattern); if (epApiHashCopy) { var hashSource = getNativeRegisterKey(epApiHashCopy[2]), hashDestination = getNativeRegisterKey(epApiHashCopy[1]); if (epApiHashTransformsByRegister[hashSource]) { var sourceHash = epApiHashTransformsByRegister[hashSource], copiedKinds = {}; for (var kind in sourceHash.Kinds) copiedKinds[kind] = true; epApiHashTransformsByRegister[hashDestination] = { Count: sourceHash.Count, KindCount: sourceHash.KindCount, Kinds: copiedKinds }; } else delete epApiHashTransformsByRegister[hashDestination]; } if (epApiHashTransform) { var epApiHashRegisterName = epApiHashTransform[2], epApiHashRegisterKey = getNativeRegisterKey(epApiHashRegisterName), epApiHashTransformOpCode = epApiHashTransform[1]; if (!epApiHashTransformsByRegister[epApiHashRegisterKey]) { // Keep the count and opcode diversity together per accumulator; // combining arithmetic from unrelated registers would inflate noise. epApiHashTransformsByRegister[epApiHashRegisterKey] = { Count: 0, KindCount: 0, Kinds: {} }; } epApiHashTransformsByRegister[epApiHashRegisterKey].Count++; if (!epApiHashTransformsByRegister[epApiHashRegisterKey].Kinds[epApiHashTransformOpCode]) { epApiHashTransformsByRegister[epApiHashRegisterKey].Kinds[epApiHashTransformOpCode] = true; epApiHashTransformsByRegister[epApiHashRegisterKey].KindCount++; } // Only the strongest single-register chain is carried to the final // predicate. This keeps the post-traversal test constant-time. epApiHashMaximumTransformCount = Math.max( epApiHashMaximumTransformCount, epApiHashTransformsByRegister[epApiHashRegisterKey].Count ); epApiHashMaximumTransformKindCount = Math.max( epApiHashMaximumTransformKindCount, epApiHashTransformsByRegister[epApiHashRegisterKey].KindCount ); } } } var epPolymorphismNextAddress = epPolymorphismCurrentAddress + epPolymorphismInstructionLength, epPolymorphismDirectControlFlow = epPolymorphismInstruction.match(/^(CALL|J[A-Z]+|LOOP(?:E|NE|NZ|Z)?) (0X[0-9A-F]+|\d+)$/i), epPolymorphismNextZeroFlag = advanceEpZeroFlag(epPolymorphismInstruction, epPolymorphismOpCode, epPolymorphismCurrentZeroFlag), epPolymorphismNextCarryFlag = advanceEpCarryFlag(epPolymorphismInstruction, epPolymorphismOpCode, epPolymorphismCurrentCarryFlag), epPolymorphismBranchTaken = getEpKnownBranchResult(epPolymorphismOpCode, epPolymorphismNextZeroFlag, epPolymorphismNextCarryFlag); // An unresolved Call/Jmp supplies the final dispatcher fact for bitstream unpacking. if (!epPolymorphismDirectControlFlow && (epPolymorphismOpCode === "CALL" || epPolymorphismOpCode === "JMP" || epPolymorphismOpCode === "JMPF")) { epBitstreamIndirectTransferPresent = true; epBitstreamIndirectTransferAddresses.push(epPolymorphismCurrentAddress); // An indirect transfer becomes resolver evidence only after export parsing // and a credible name-hash loop have already been established. if (isPebModuleWalkPresent && epExportResolverHeaderAccessPresent && epExportResolverDirectoryAccessPresent && epExportResolverFieldCount >= 2 && epApiHashByteReadPresent && epApiHashMaximumTransformCount >= 2 && epApiHashMaximumTransformKindCount >= 2) { epHashedApiResolverIndirectTransferPresent = true; } } var epBitstreamQueueLength = epPolymorphismPendingAddresses.length; if (epPolymorphismDirectControlFlow) { var epPolymorphismTargetAddress = parseInt(epPolymorphismDirectControlFlow[2], 0); // A direct branch into unmapped, virtual-only or non-executable bytes is not // compiler control flow. Calls are excluded because this rule should not make // assumptions about unusual external or loader-provided call destinations. if (epPolymorphismOpCode !== "CALL") { var epPolymorphismTargetRva = epPolymorphismTargetAddress - PE_Cached.imageBase, isEpPolymorphismTargetExecutable = false; for (var d = 0; d < PE_Cached.numberOfSections && !isEpPolymorphismTargetExecutable; d++) { var epPolymorphismTargetSectionRva = PE.getSectionVirtualAddress(d), epPolymorphismTargetSectionRawSize = PE.getSectionFileSize(d); if (epPolymorphismTargetRva >= epPolymorphismTargetSectionRva && epPolymorphismTargetRva < epPolymorphismTargetSectionRva + epPolymorphismTargetSectionRawSize && (PE.section[d].Flags & SECTION_FLAGS_EXECUTE) && PE.VAToOffset(epPolymorphismTargetAddress) !== -1) { isEpPolymorphismTargetExecutable = true; } } const isEpPolymorphismTargetWithinImage = epPolymorphismTargetRva >= 0 && epPolymorphismTargetRva < epPolymorphismImageSize, isEpPolymorphismBranchNearEntryPoint = Math.abs( epPolymorphismCurrentAddress - PE_Cached.addressOfUnmanagedEntryPoint ) <= epInvalidBranchSourceMaximumDistance; // An internal invalid target is loader-visible proof at any reachable depth. // A target beyond the image additionally requires a spatially near startup // branch, filtering distant false instruction boundaries in ordinary code. if (epPolymorphismBranchTaken !== 0 && !isEpPolymorphismTargetExecutable && (isEpPolymorphismTargetWithinImage || isEpPolymorphismBranchNearEntryPoint)) { epInvalidBranchCandidates.push({ SourceAddress: epPolymorphismCurrentAddress, TargetAddress: epPolymorphismTargetAddress, OpCode: epPolymorphismOpCode, TakenProven: epPolymorphismOpCode === "JMP" || epPolymorphismOpCode === "JMPF" || epPolymorphismBranchTaken === 1 }); } } // Only an explicit backward branch proves a cycle. An ordinary Call to a // function emitted earlier in the image is not iterative control flow. if (epPolymorphismOpCode !== "CALL" && epPolymorphismBranchTaken !== 0 && epPolymorphismTargetAddress < epPolymorphismCurrentAddress) { epPolymorphismBackwardControlFlowCount++; if (epBitstreamBackwardEdges.length < 64) { epBitstreamBackwardEdges.push({ Source: epPolymorphismCurrentAddress, Target: epPolymorphismTargetAddress }); } // A backward edge proves iteration without assuming one compiler-specific // loop spelling. It is retained only for the already formed hash chain. if (isPebModuleWalkPresent && epExportResolverHeaderAccessPresent && epExportResolverDirectoryAccessPresent && epExportResolverFieldCount >= 2 && epApiHashByteReadPresent && epApiHashMaximumTransformCount >= 2 && epApiHashMaximumTransformKindCount >= 2) { epApiHashBackwardControlFlowPresent = true; } } if (epPolymorphismOpCode === "CALL") { var epPolymorphismTargetKey = epPolymorphismTargetAddress.toString(); var isEpDirectCallReturning = isEpDirectCallReturnProven(epPolymorphismCurrentAddress); // Preserve every reachable direct call target for secondary validation. epDirectCallTargets[epPolymorphismTargetKey] = true; } if (epPolymorphismOpCode === "JMP" || epPolymorphismOpCode === "JMPF") { // An unconditional jump has no reachable physical fall-through. epPolymorphismPendingAddresses.push(epPolymorphismTargetAddress); epPolymorphismPendingZeroFlags.push(epPolymorphismNextZeroFlag); epPolymorphismPendingStackPointerZeros.push(epPolymorphismNextStackPointerZero); epPolymorphismPendingCarryFlags.push(epPolymorphismNextCarryFlag); } else if (epPolymorphismOpCode === "CALL") { // A physical fall-through exists only if every bounded callee path returns. if (isEpDirectCallReturning) { epPolymorphismPendingAddresses.push(epPolymorphismNextAddress); epPolymorphismPendingZeroFlags.push(-1); epPolymorphismPendingStackPointerZeros.push(epPolymorphismNextStackPointerZero); epPolymorphismPendingCarryFlags.push(-1); } epPolymorphismPendingAddresses.push(epPolymorphismTargetAddress); epPolymorphismPendingZeroFlags.push(epPolymorphismCurrentZeroFlag); epPolymorphismPendingStackPointerZeros.push(epPolymorphismNextStackPointerZero); epPolymorphismPendingCarryFlags.push(epPolymorphismCurrentCarryFlag); } else { // Known flags exclude only a proved-impossible conditional edge. if (epPolymorphismBranchTaken !== 0) { epPolymorphismPendingAddresses.push(epPolymorphismTargetAddress); epPolymorphismPendingZeroFlags.push(epPolymorphismNextZeroFlag); epPolymorphismPendingStackPointerZeros.push(epPolymorphismNextStackPointerZero); epPolymorphismPendingCarryFlags.push(epPolymorphismNextCarryFlag); } if (epPolymorphismBranchTaken !== 1) { epPolymorphismPendingAddresses.push(epPolymorphismNextAddress); epPolymorphismPendingZeroFlags.push(epPolymorphismNextZeroFlag); epPolymorphismPendingStackPointerZeros.push(epPolymorphismNextStackPointerZero); epPolymorphismPendingCarryFlags.push(epPolymorphismNextCarryFlag); } } } else if (epPolymorphismOpCode === "CALL") { // An unresolved indirect call cannot prove its physical fall-through. } else if (epScanStopOpCodes.indexOf(epPolymorphismOpCode) === -1) { epPolymorphismPendingAddresses.push(epPolymorphismNextAddress); epPolymorphismPendingZeroFlags.push(epPolymorphismNextZeroFlag); epPolymorphismPendingStackPointerZeros.push(epPolymorphismNextStackPointerZero); epPolymorphismPendingCarryFlags.push(epPolymorphismNextCarryFlag); } epBitstreamSuccessors[epPolymorphismAddressKey] = epPolymorphismPendingAddresses.slice(epBitstreamQueueLength); } // Every stored edge is proven; an unknown CALL fall-through never enters the graph. var epInvalidBranchProvenReachable = {}; if (epInvalidBranchCandidates.length) { var epInvalidBranchPendingAddresses = epBitstreamStartAddresses.slice(), epInvalidBranchPendingIndex = 0; while (epInvalidBranchPendingIndex < epInvalidBranchPendingAddresses.length) { var reachableAddress = epInvalidBranchPendingAddresses[epInvalidBranchPendingIndex++], reachableKey = reachableAddress.toString(); if (epInvalidBranchProvenReachable[reachableKey] || !epBitstreamSuccessors.hasOwnProperty(reachableKey)) continue; epInvalidBranchProvenReachable[reachableKey] = true; var reachableSuccessors = epBitstreamSuccessors[reachableKey]; for (var p = 0; p < reachableSuccessors.length; p++) { epInvalidBranchPendingAddresses.push(reachableSuccessors[p]); } } } // Resolve bad-target candidates after traversal so queue order cannot promote an // alternative address located inside a later decoded instruction boundary. for (var i = 0; i < epInvalidBranchCandidates.length; i++) { var epInvalidBranchCandidate = epInvalidBranchCandidates[i], isEpInvalidBranchCandidateOverlapping = false; for (var p = 1; p < 15 && !isEpInvalidBranchCandidateOverlapping; p++) { var epInvalidBranchPreviousAddress = epInvalidBranchCandidate.SourceAddress - p, epInvalidBranchPreviousInstructionLength = epDecodedInstructionLengthByAddress[epInvalidBranchPreviousAddress.toString()]; if (epInvalidBranchPreviousInstructionLength > p) { isEpInvalidBranchCandidateOverlapping = true; } } if (isEpInvalidBranchCandidateOverlapping) continue; epValidInvalidBranchCandidates.push(epInvalidBranchCandidate); var invalidTargetRva = epInvalidBranchCandidate.TargetAddress - PE_Cached.imageBase; if ((invalidTargetRva < 0 || invalidTargetRva >= epPolymorphismImageSize) && !epInvalidBranchProvenReachable[epInvalidBranchCandidate.SourceAddress.toString()]) continue; if (epInvalidBranchTargetAddress === -1 && epInvalidBranchCandidate.TakenProven) { epInvalidBranchSourceAddress = epInvalidBranchCandidate.SourceAddress; epInvalidBranchTargetAddress = epInvalidBranchCandidate.TargetAddress; epInvalidBranchTargetOpCode = epInvalidBranchCandidate.OpCode; } } // Resolve prefix candidates only after every reachable instruction boundary is known. // A candidate inside another decoded instruction comes from an alternative x86 decode // path and cannot independently prove redundant prefixes. for (var i = 0; i < epRedundantPrefixCandidates.length; i++) { var epRedundantPrefixCandidate = epRedundantPrefixCandidates[i], isEpRedundantPrefixCandidateOverlapping = false; for (var p = 1; p < 15 && !isEpRedundantPrefixCandidateOverlapping; p++) { var epRedundantPrefixPreviousAddress = epRedundantPrefixCandidate.Address - p, epRedundantPrefixPreviousInstructionLength = epDecodedInstructionLengthByAddress[epRedundantPrefixPreviousAddress.toString()]; if (epRedundantPrefixPreviousInstructionLength > p) { isEpRedundantPrefixCandidateOverlapping = true; } } if (isEpRedundantPrefixCandidateOverlapping) continue; epRedundantPrefixInstructionCount++; epConflictingPrefixPresent = epConflictingPrefixPresent || epRedundantPrefixCandidate.IsConflicting; if (epRedundantPrefixCandidate.PrefixCount > epMaximumRedundantPrefixCount) { epMaximumRedundantPrefixCount = epRedundantPrefixCandidate.PrefixCount; } } // Reuse the reachable direct-call set for two independent behavioral proofs. for (var epDirectCallTargetKey in epDirectCallTargets) { // Object keys are decimal address strings; restore the numeric VA once per target. var epDirectCallTargetAddress = parseInt(epDirectCallTargetKey, 10); if (epSelfModifyingCallStubAddress === -1) { var epSelfModifyingCallStubTargetRva = epDirectCallTargetAddress - PE_Cached.imageBase, epSelfModifyingCallStubSectionIndex = -1; // A normal compiler has no reason to call a physically-backed writable code // area which reconstructs its own nearby instructions. The section check // excludes ordinary thunks before the instruction proof begins. for (var d = 0; d < PE_Cached.numberOfSections && epSelfModifyingCallStubSectionIndex === -1; d++) { var epSelfModifyingCallStubSectionRva = PE.getSectionVirtualAddress(d), epSelfModifyingCallStubSectionRawSize = PE.getSectionFileSize(d), epSelfModifyingCallStubSectionFlags = PE.section[d].Flags; if (epSelfModifyingCallStubTargetRva >= epSelfModifyingCallStubSectionRva && epSelfModifyingCallStubTargetRva < epSelfModifyingCallStubSectionRva + epSelfModifyingCallStubSectionRawSize && (epSelfModifyingCallStubSectionFlags & (SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE)) === (SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE)) { epSelfModifyingCallStubSectionIndex = d; } } if (epSelfModifyingCallStubSectionIndex !== -1) { // The strict profile advances through seven ordered semantic stages. var epSelfModifyingCallStubCurrentAddress = epDirectCallTargetAddress, epSelfModifyingCallStubState = 0, epSelfModifyingCallStubNeutralInstructionCount = 0, epSelfModifyingCallStubWriteAddress = -1; // The concrete profile follows fixed semantics rather than exact bytes: // consume the return address, preserve the accumulator, derive a DWORD, // atomically write it into the instruction immediately following Pause, // and restore state. // Two neutral instructions per stage tolerate simple polymorphic padding // without allowing unrelated code to assemble a proof accidentally. for (var epSelfModifyingCallStubIndex = 0; epSelfModifyingCallStubIndex < epSelfModifyingCallStubLookaheadLimit && epSelfModifyingCallStubAddress === -1; epSelfModifyingCallStubIndex++) { var epSelfModifyingCallStubCurrentRva = epSelfModifyingCallStubCurrentAddress - PE_Cached.imageBase; if (epSelfModifyingCallStubCurrentRva < epSelfModifyingCallStubSectionRva || epSelfModifyingCallStubCurrentRva >= epSelfModifyingCallStubSectionRva + epSelfModifyingCallStubSectionRawSize || PE.VAToOffset(epSelfModifyingCallStubCurrentAddress) === -1) break; var epSelfModifyingCallStubInstruction = PE.getDisasmString(epSelfModifyingCallStubCurrentAddress), epSelfModifyingCallStubOpCode = getAsmOpCode(epSelfModifyingCallStubInstruction), epSelfModifyingCallStubInstructionLength = PE.getDisasmLength(epSelfModifyingCallStubCurrentAddress), epSelfModifyingCallStubStageMatched = false, epSelfModifyingCallStubWrite; if (!epSelfModifyingCallStubOpCode || epSelfModifyingCallStubInstructionLength <= 0 || !isVirtualAddressRangeFileBacked(epSelfModifyingCallStubCurrentAddress, epSelfModifyingCallStubInstructionLength, SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE)) break; switch (epSelfModifyingCallStubState) { case 0: // Stage 0 consumes the call return address into writable memory. epSelfModifyingCallStubStageMatched = epSelfModifyingCallStubReturnStorePattern.test(epSelfModifyingCallStubInstruction); break; case 1: // Stage 1 preserves the native accumulator before using EAX as a decoder. epSelfModifyingCallStubStageMatched = epSelfModifyingCallStubAccumulatorPushPattern.test(epSelfModifyingCallStubInstruction); break; case 2: // Stage 2 materializes a DWORD key or encoded instruction value. epSelfModifyingCallStubStageMatched = epSelfModifyingCallStubKeyLoadPattern.test(epSelfModifyingCallStubInstruction); break; case 3: // Stage 3 transforms that DWORD without requiring one concrete key. epSelfModifyingCallStubStageMatched = epSelfModifyingCallStubDecodePattern.test(epSelfModifyingCallStubInstruction); break; case 4: // Stage 4 atomically exchanges the decoded DWORD with executable bytes. epSelfModifyingCallStubWrite = epSelfModifyingCallStubInstruction.match(epSelfModifyingCallStubWritePattern); epSelfModifyingCallStubStageMatched = Boolean(epSelfModifyingCallStubWrite); if (epSelfModifyingCallStubWrite) { if (epSelfModifyingCallStubWrite[3]) { // The disassembler may print a direct absolute write target. epSelfModifyingCallStubWriteAddress = parseInt(epSelfModifyingCallStubWrite[3], 0); } else { // Resolve a RIP-relative displacement against the following instruction. var epSelfModifyingCallStubDisplacement = parseInt(epSelfModifyingCallStubWrite[2], 0); if (epSelfModifyingCallStubWrite[1] === "-") epSelfModifyingCallStubDisplacement = -epSelfModifyingCallStubDisplacement; epSelfModifyingCallStubWriteAddress = epSelfModifyingCallStubCurrentAddress + epSelfModifyingCallStubInstructionLength + epSelfModifyingCallStubDisplacement; } // A complete decoded write must reach writable code, not merely follow Pause beyond the image. epSelfModifyingCallStubStageMatched = isVirtualAddressRangeFileBacked(epSelfModifyingCallStubWriteAddress, 4, SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE); } break; case 5: // Stage 5 restores the accumulator saved before reconstruction. epSelfModifyingCallStubStageMatched = epSelfModifyingCallStubAccumulatorPopPattern.test(epSelfModifyingCallStubInstruction); break; case 6: // The bundled disassembler may canonicalize F3 90 to Nop even // though the prefix deliberately serializes the code rewrite. epSelfModifyingCallStubStageMatched = epSelfModifyingCallStubOpCode === "PAUSE" || PE.compare("F3 90", PE.VAToOffset(epSelfModifyingCallStubCurrentAddress)); break; } if (epSelfModifyingCallStubStageMatched) { // Advance exactly one stage and restart its local padding allowance. epSelfModifyingCallStubState++; epSelfModifyingCallStubNeutralInstructionCount = 0; if (epSelfModifyingCallStubState === 7 && epSelfModifyingCallStubWriteAddress === epSelfModifyingCallStubCurrentAddress + epSelfModifyingCallStubInstructionLength) { // The rewritten DWORD must begin immediately after the serializing Pause. epSelfModifyingCallStubAddress = epDirectCallTargetAddress; } } else if ((epSelfModifyingCallStubOpCode === "NOP" || epSelfModifyingCallStubOpCode === "FNOP" || epSimplePolymorphicInstructionPattern.test(epSelfModifyingCallStubInstruction) || epValuePreservingInstructionPattern.test(epSelfModifyingCallStubInstruction)) && epSelfModifyingCallStubNeutralInstructionCount < epSelfModifyingCallStubNeutralInstructionLimit) { // Permit a small amount of neutral polymorphism independently per stage. epSelfModifyingCallStubNeutralInstructionCount++; } else { // Any semantic mismatch rejects this strict stub candidate. break; } // Strict-profile matching is physical and linear within one section. epSelfModifyingCallStubCurrentAddress += epSelfModifyingCallStubInstructionLength; } if (epSelfModifyingCallStubAddress === -1) { // Fall back to a looser profile only when the exact seven-stage form failed. var epRepeatedSelfModifyingCallStubCurrentAddress = epDirectCallTargetAddress, epRepeatedSelfModifyingCallStubReturnReadPresent = false, epRepeatedSelfModifyingCallStubTransformPresent = false, epRepeatedSelfModifyingCallStubTransformRegister = String(), epRepeatedSelfModifyingCallStubFallthrough = {}; // The generic profile deliberately ignores exact registers and arithmetic. // Requiring three distinct called stubs in the same writable code section // compensates for that freedom. Each stub must recover its return address, // transform a register value, and write back into its own nearby code bytes. for (var epRepeatedSelfModifyingCallStubIndex = 0; epRepeatedSelfModifyingCallStubIndex < epRepeatedSelfModifyingCallStubLookaheadLimit && epSelfModifyingCallStubAddress === -1; epRepeatedSelfModifyingCallStubIndex++) { var epRepeatedSelfModifyingCallStubCurrentRva = epRepeatedSelfModifyingCallStubCurrentAddress - PE_Cached.imageBase; if (epRepeatedSelfModifyingCallStubCurrentRva < epSelfModifyingCallStubSectionRva || epRepeatedSelfModifyingCallStubCurrentRva >= epSelfModifyingCallStubSectionRva + epSelfModifyingCallStubSectionRawSize || PE.VAToOffset(epRepeatedSelfModifyingCallStubCurrentAddress) === -1) break; var epRepeatedSelfModifyingCallStubInstruction = PE.getDisasmString(epRepeatedSelfModifyingCallStubCurrentAddress), epRepeatedSelfModifyingCallStubOpCode = getAsmOpCode(epRepeatedSelfModifyingCallStubInstruction), epRepeatedSelfModifyingCallStubInstructionLength = PE.getDisasmLength(epRepeatedSelfModifyingCallStubCurrentAddress), epRepeatedSelfModifyingCallStubWrite = undefined; if (!epRepeatedSelfModifyingCallStubOpCode || epRepeatedSelfModifyingCallStubInstructionLength <= 0 || !isVirtualAddressRangeFileBacked(epRepeatedSelfModifyingCallStubCurrentAddress, epRepeatedSelfModifyingCallStubInstructionLength, SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE)) break; if (!epRepeatedSelfModifyingCallStubReturnReadPresent) { // First prove that the called stub obtains its own return address. epRepeatedSelfModifyingCallStubReturnReadPresent = epRepeatedSelfModifyingCallStubReturnReadPattern.test(epRepeatedSelfModifyingCallStubInstruction); } else if (!epRepeatedSelfModifyingCallStubTransformPresent) { // Then bind the first following arithmetic transform to its destination. var epRepeatedSelfModifyingCallStubTransform = epRepeatedSelfModifyingCallStubInstruction.match(epRepeatedSelfModifyingCallStubTransformPattern); if (epRepeatedSelfModifyingCallStubTransform) { epRepeatedSelfModifyingCallStubTransformRegister = epRepeatedSelfModifyingCallStubTransform[1]; epRepeatedSelfModifyingCallStubTransformPresent = true; } } if (epRepeatedSelfModifyingCallStubReturnReadPresent && epRepeatedSelfModifyingCallStubTransformPresent) { // A write is eligible only after both earlier semantic stages exist. epRepeatedSelfModifyingCallStubWrite = epRepeatedSelfModifyingCallStubInstruction.match(epRepeatedSelfModifyingCallStubWritePattern); if (epRepeatedSelfModifyingCallStubWrite && epRepeatedSelfModifyingCallStubWrite[4] !== epRepeatedSelfModifyingCallStubTransformRegister) { // Reject a write sourced from a value unrelated to the transform. break; } } if (epRepeatedSelfModifyingCallStubWrite) { var epRepeatedSelfModifyingCallStubWriteAddress; if (epRepeatedSelfModifyingCallStubWrite[3]) { // Preserve an absolute write target exactly as decoded. epRepeatedSelfModifyingCallStubWriteAddress = parseInt(epRepeatedSelfModifyingCallStubWrite[3], 0); } else { // Convert RIP-relative syntax into the effective write VA. var epRepeatedSelfModifyingCallStubDisplacement = parseInt(epRepeatedSelfModifyingCallStubWrite[2], 0); if (epRepeatedSelfModifyingCallStubWrite[1] === "-") { epRepeatedSelfModifyingCallStubDisplacement = -epRepeatedSelfModifyingCallStubDisplacement; } epRepeatedSelfModifyingCallStubWriteAddress = epRepeatedSelfModifyingCallStubCurrentAddress + epRepeatedSelfModifyingCallStubInstructionLength + epRepeatedSelfModifyingCallStubDisplacement; } var epRepeatedSelfModifyingCallStubSectionAddress = PE_Cached.imageBase + epSelfModifyingCallStubSectionRva, epRepeatedSelfModifyingCallStubWriteSize = epRepeatedSelfModifyingCallStubInstruction.indexOf("QWORD PTR") !== -1 ? 8 : epRepeatedSelfModifyingCallStubInstruction.indexOf("DWORD PTR") !== -1 ? 4 : epRepeatedSelfModifyingCallStubInstruction.indexOf("WORD PTR") !== -1 ? 2 : 1; // Require the write to remain inside the same executable/writable // section and close enough to reconstruct the current stub. if (epRepeatedSelfModifyingCallStubWriteAddress >= epRepeatedSelfModifyingCallStubSectionAddress && epRepeatedSelfModifyingCallStubWriteAddress + epRepeatedSelfModifyingCallStubWriteSize <= epRepeatedSelfModifyingCallStubSectionAddress + epSelfModifyingCallStubSectionRawSize && isVirtualAddressRangeFileBacked(epRepeatedSelfModifyingCallStubWriteAddress, epRepeatedSelfModifyingCallStubWriteSize, SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE) && Math.abs(epRepeatedSelfModifyingCallStubWriteAddress - epDirectCallTargetAddress) <= epRepeatedSelfModifyingCallStubMaximumWriteDistance) { var epRepeatedSelfModifyingCallStubSectionKey = epSelfModifyingCallStubSectionIndex.toString(); // Initialize and increment the section-local evidence count. if (!epRepeatedSelfModifyingCallStubCountBySection[epRepeatedSelfModifyingCallStubSectionKey]) { epRepeatedSelfModifyingCallStubCountBySection[epRepeatedSelfModifyingCallStubSectionKey] = 0; } epRepeatedSelfModifyingCallStubCountBySection[epRepeatedSelfModifyingCallStubSectionKey]++; if (epRepeatedSelfModifyingCallStubCountBySection[epRepeatedSelfModifyingCallStubSectionKey] >= epRepeatedSelfModifyingCallStubRequiredCount) { // Three independent stubs compensate for the generic profile's freedom. epSelfModifyingCallStubAddress = epDirectCallTargetAddress; } } break; } // Never join instructions across a terminal control-flow boundary. if (epScanStopOpCodes.indexOf(epRepeatedSelfModifyingCallStubOpCode) !== -1) break; // A skipped write cannot reconstruct the stub, even when its bytes follow the branch physically. if (isEpLinearFallthroughUnproven(epRepeatedSelfModifyingCallStubInstruction, epRepeatedSelfModifyingCallStubCurrentAddress, epRepeatedSelfModifyingCallStubFallthrough)) break; // Continue linearly inside the already validated writable code section. epRepeatedSelfModifyingCallStubCurrentAddress += epRepeatedSelfModifyingCallStubInstructionLength; } } } } // A Pop at a call target is not sufficient by itself: calling-convention adapters // may discard the return address without using it. Confirm that the recovered value // is read before it can be overwritten or invalidated by another control transfer. var epIpRecoveryRegister = epPopRegisterByAddress[epDirectCallTargetKey]; // Only targets beginning with Pop can carry a Call/Pop instruction-pointer proof. if (!epIpRecoveryRegister) continue; var epIpRecoveryCurrentAddress = parseInt(epDirectCallTargetKey, 10), epIpRecoveryPopLength = PE.getDisasmLength(epIpRecoveryCurrentAddress), epIpRecoveryKnownRegisters = {}, epIpRecoveryKnownValues = {}, epIpRecoveryRegisterPatterns = {}; epIpRecoveryKnownRegisters[epIpRecoveryRegister] = true; // Reject malformed POP decoding before moving to its first consumer. if (epIpRecoveryPopLength <= 0) continue; epIpRecoveryCurrentAddress += epIpRecoveryPopLength; // Inspect a short physical window after Pop; long-distance use would be too // easy to confuse with ordinary register data flow and would increase FP. for (var epIpRecoveryIndex = 0; epIpRecoveryIndex < epCallPopIpRecoveryLookaheadLimit; epIpRecoveryIndex++) { var epIpRecoveryCurrentRva = epIpRecoveryCurrentAddress - PE_Cached.imageBase, epIpRecoverySectionIndex = -1; // The proof lookahead is linear and intentionally short, but every decoded // address must still refer to executable physical bytes rather than padding. for (var d = 0; d < PE_Cached.numberOfSections && epIpRecoverySectionIndex === -1; d++) { var epIpRecoverySectionRva = PE.getSectionVirtualAddress(d), epIpRecoverySectionRawSize = PE.getSectionFileSize(d); if (epIpRecoveryCurrentRva >= epIpRecoverySectionRva && epIpRecoveryCurrentRva < epIpRecoverySectionRva + epIpRecoverySectionRawSize && (PE.section[d].Flags & SECTION_FLAGS_EXECUTE)) { epIpRecoverySectionIndex = d; } } if (epIpRecoverySectionIndex === -1 || PE.VAToOffset(epIpRecoveryCurrentAddress) === -1) break; var epIpRecoveryInstruction = PE.getDisasmString(epIpRecoveryCurrentAddress), epIpRecoveryOpCode = getAsmOpCode(epIpRecoveryInstruction), epIpRecoveryInstructionLength = PE.getDisasmLength(epIpRecoveryCurrentAddress), epIpRecoverySimplePolymorphism = epSimplePolymorphicInstructionPattern.test(epIpRecoveryInstruction), epIpRecoveryValuePreserving = epValuePreservingInstructionPattern.test(epIpRecoveryInstruction); if (!epIpRecoveryOpCode || epIpRecoveryInstructionLength <= 0 || !isVirtualAddressRangeFileBacked(epIpRecoveryCurrentAddress, epIpRecoveryInstructionLength, SECTION_FLAGS_EXECUTE)) break; // Pointer-use provenance and literal values are different facts: a zero is not a recovered IP. if (isEpProvenFaultingInstruction(epIpRecoveryInstruction, epIpRecoveryOpCode, false, epIpRecoveryKnownValues)) break; var epIpRecoveryCopy = epIpRecoveryInstruction.match(epRegisterCopyPattern), epIpRecoveryLiteral = epIpRecoveryInstruction.match(epLiteralMovePattern), epIpRecoveryZeroRegister = epIpRecoveryInstruction.match(epZeroRegisterPattern), epIpRecoveryRegisterWrite = epIpRecoveryInstruction.match(epFlagsNeutralRegisterWritePattern) || epIpRecoveryInstruction.match(epTrackedRegisterWritePattern), epIpRecoveryPartialWrite = epIpRecoveryInstruction.match(epPartialRegisterWritePattern); if (epIpRecoveryZeroRegister) { epIpRecoveryKnownValues[getNativeRegisterKey(epIpRecoveryZeroRegister[1] || epIpRecoveryZeroRegister[2])] = 0; } else if (epIpRecoveryLiteral || epIpRecoveryCopy) { var epIpRecoveryValueDestination = getNativeRegisterKey((epIpRecoveryLiteral || epIpRecoveryCopy)[1]), epIpRecoveryValue = epIpRecoveryLiteral ? parseInt(epIpRecoveryLiteral[2], 0) : epIpRecoveryKnownValues[getNativeRegisterKey(epIpRecoveryCopy[2])]; if (epIpRecoveryValue !== undefined && epIpRecoveryValue <= epMaximumSafeInteger) { epIpRecoveryKnownValues[epIpRecoveryValueDestination] = epRegister32BitPattern.test((epIpRecoveryLiteral || epIpRecoveryCopy)[1]) ? epIpRecoveryValue >>> 0 : epIpRecoveryValue; } else { delete epIpRecoveryKnownValues[epIpRecoveryValueDestination]; } } else if (epIpRecoverySimplePolymorphism || epIpRecoveryValuePreserving) { var epIpRecoveryIdentity = epIpRecoveryInstruction.match(epSimplePolymorphicInstructionPattern) || epIpRecoveryInstruction.match(epValuePreservingInstructionPattern), epIpRecoveryIdentityRegister = epIpRecoveryIdentity[1] || epIpRecoveryIdentity[2], epIpRecoveryIdentityKey = /^(?:[ABCD]X|SI|DI|BP|R(?:8|9|1[0-5])W)$/.test(epIpRecoveryIdentityRegister) ? getEpPartialRegisterKey(epIpRecoveryIdentityRegister) : getNativeRegisterKey(epIpRecoveryIdentityRegister); if (epRegister32BitPattern.test(epIpRecoveryIdentityRegister) && epIpRecoveryKnownValues[epIpRecoveryIdentityKey] !== undefined) epIpRecoveryKnownValues[epIpRecoveryIdentityKey] >>>= 0; } else if (epIpRecoveryPartialWrite) { delete epIpRecoveryKnownValues[getEpPartialRegisterKey(epIpRecoveryPartialWrite[1])]; } else if (epIpRecoveryRegisterWrite) { delete epIpRecoveryKnownValues[getNativeRegisterKey(epIpRecoveryRegisterWrite[1])]; } if (epIpRecoveryCopy) { var epIpRecoveryCopyDestination = epIpRecoveryCopy[1], epIpRecoveryCopySource = epIpRecoveryCopy[2], epIpRecoveryIsNativeCopy = (PE_Cached.isI386 ? epDwordRegisterPattern : epQwordRegisterPattern).test(epIpRecoveryCopyDestination); // MOV preserves its source. Only an exact native-width copy proves the destination. if (epIpRecoveryIsNativeCopy && epIpRecoveryKnownRegisters[epIpRecoveryCopySource]) { epIpRecoveryKnownRegisters[epIpRecoveryCopyDestination] = true; } else { delete epIpRecoveryKnownRegisters[PE_Cached.is64bit ? getNativeRegisterKey(epIpRecoveryCopyDestination) : epIpRecoveryCopyDestination]; } epIpRecoveryCurrentAddress += epIpRecoveryInstructionLength; continue; } var epIpRecoveryNeutralInstruction = epIpRecoveryOpCode === "NOP" || epIpRecoveryOpCode === "FNOP" || epIpRecoveryOpCode === "PAUSE" || epIpRecoveryOpCode === "CLC" || epIpRecoveryOpCode === "STC" || epIpRecoveryOpCode === "CMC" || epStackRoundTripPushPattern.test(epIpRecoveryInstruction) || epIpRecoverySimplePolymorphism || epIpRecoveryValuePreserving || epIpRecoveryRegisterWrite || epIpRecoveryPartialWrite || /^(?:CMP|TEST)$/.test(epIpRecoveryOpCode); for (var epIpRecoveryKnownRegister in epIpRecoveryKnownRegisters) { var epIpRecoveryPatterns = epIpRecoveryRegisterPatterns[epIpRecoveryKnownRegister]; if (!epIpRecoveryPatterns) { var epIpRecoveryRegisterSuffix = epIpRecoveryKnownRegister.substring(1), epIpRecoveryByteRegister = /^(?:AX|BX|CX|DX)$/.test(epIpRecoveryRegisterSuffix) ? epIpRecoveryRegisterSuffix[0] + "(?:L|H)" : epIpRecoveryRegisterSuffix + "L", epIpRecoveryAliases = /^R(?:8|9|1[0-5])$/.test(epIpRecoveryKnownRegister) ? epIpRecoveryKnownRegister + "(?:D|W|B)?" : "(?:" + (epIpRecoveryKnownRegister[0] === "R" ? epIpRecoveryKnownRegister + "|" : String()) + "E" + epIpRecoveryRegisterSuffix + "|" + epIpRecoveryRegisterSuffix + "|" + epIpRecoveryByteRegister + ")"; epIpRecoveryPatterns = { Mention: new RegExp("\\b" + epIpRecoveryKnownRegister + "\\b", "i"), Alias: new RegExp("\\b" + epIpRecoveryAliases + "\\b", "i"), Read: new RegExp("^(?:(?:ADD|ADC|SUB|SBB|AND|OR|XOR|IMUL|INC|DEC|NEG|NOT|SAL|SHL|SHR|SAR|ROL|ROR|RCL|RCR|XADD|XCHG|CMP|TEST|PUSH|CALL|JMP) " + epIpRecoveryKnownRegister + "(?:,|$)|[^,]+,.*\\b" + epIpRecoveryKnownRegister + "\\b|[^,]*\\[[^\\]]*\\b" + epIpRecoveryKnownRegister + "\\b)", "i"), Clear: new RegExp("^(?:(?:XOR|SUB) " + epIpRecoveryKnownRegister + ", " + epIpRecoveryKnownRegister + "|AND " + epIpRecoveryKnownRegister + ", (?:0X0+|0+)|IMUL " + epIpRecoveryKnownRegister + ", " + epIpRecoveryKnownRegister + ", (?:0X0+|0+))$", "i") }; epIpRecoveryRegisterPatterns[epIpRecoveryKnownRegister] = epIpRecoveryPatterns; } if (epIpRecoveryPatterns.Alias.test(epIpRecoveryInstruction) && !epIpRecoveryPatterns.Mention.test(epIpRecoveryInstruction)) { delete epIpRecoveryKnownRegisters[epIpRecoveryKnownRegister]; } else if (epIpRecoveryPatterns.Mention.test(epIpRecoveryInstruction) && !epIpRecoverySimplePolymorphism && !epIpRecoveryValuePreserving && !epIpRecoveryPatterns.Clear.test(epIpRecoveryInstruction) && epIpRecoveryPatterns.Read.test(epIpRecoveryInstruction)) { epCallPopIpRecoveryAddress = parseInt(epDirectCallTargetKey, 10); break; } } if (epCallPopIpRecoveryAddress !== -1) break; if (epIpRecoveryRegisterWrite) { var epIpRecoveryWrittenRegister = epIpRecoveryRegisterWrite[1]; if (PE_Cached.is64bit) epIpRecoveryWrittenRegister = getNativeRegisterKey(epIpRecoveryWrittenRegister); delete epIpRecoveryKnownRegisters[epIpRecoveryWrittenRegister]; } // Stop when the instruction cannot safely preserve a pending recovered pointer. if (!epIpRecoveryNeutralInstruction || epScanStopOpCodes.indexOf(epIpRecoveryOpCode) !== -1 || !Object.keys(epIpRecoveryKnownRegisters).length) break; // Advance only after the current instruction has preserved the pending proof. epIpRecoveryCurrentAddress += epIpRecoveryInstructionLength; } if (epCallPopIpRecoveryAddress !== -1) break; } if (isPebModuleWalkPresent && epExportResolverHeaderAccessPresent && epExportResolverDirectoryAccessPresent && epExportResolverFieldCount >= 2 && epApiHashByteReadPresent && epApiHashMaximumTransformCount >= 2 && epApiHashMaximumTransformKindCount >= 2 && epApiHashBackwardControlFlowPresent && epHashedApiResolverIndirectTransferPresent) { epResolverPathProven = isEpResolverPathProven(epBitstreamStartAddresses, epBitstreamSuccessors, epResolverInstructions, epBitstreamIndirectTransferAddresses, { Header: epExportResolverHeaderAccessPattern, Directory: epExportResolverDirectoryAccessPattern, Field: epExportResolverFieldAccessPattern, ByteRead: epApiHashByteReadPattern, Transform: epApiHashTransformPattern, PebLoad: pebModuleWalkPatterns.PebLoad, LdrLoad: pebModuleWalkPatterns.LdrLoad, ListLoad: pebModuleWalkPatterns.ListLoad, RegisterCopy: epRegisterCopyPattern, RegisterExchange: new RegExp("^XCHG (" + epGeneralPurposeRegister + "), (" + epGeneralPurposeRegister + ")$", "i"), RegisterWrite: new RegExp("^(?:MOV(?:ABS)?|MOVZX|MOVSX|MOVSXD|LEA|XOR|ADD|SUB|ADC|SBB|AND|OR|NOT|NEG|INC|DEC|POP) (" + epGeneralPurposeRegister + ")(?:,|$)", "i"), PartialWrite: epPartialRegisterWritePattern }); } // Clean up: release the control-flow worklist and its potentially dense visited set. epPolymorphismPendingAddresses = undefined; epPolymorphismPendingZeroFlags = undefined; epPolymorphismPendingStackPointerZeros = undefined; epPolymorphismPendingCarryFlags = undefined; epPolymorphismVisitedAddresses = undefined; epPolymorphismVisitedStates = undefined; epPolymorphismTransformOpCodesFound = undefined; epRareBitOpCodesFound = undefined; epDecodedInstructionLengthByAddress = undefined; epDirectCallTargets = undefined; epPopRegisterByAddress = undefined; epInvalidBranchCandidates = undefined; epRedundantPrefixCandidates = undefined; epExportResolverFieldsFound = undefined; epApiHashTransformsByRegister = undefined; epResolverInstructions = undefined; } // Convert the resolver facts collected by the shared EP traversal into a verdict. // PEB enumeration, PE export parsing, byte-wise hashing, a loop, and a final indirect // transfer are required independently; ordinary loader introspection cannot match alone. // Two distinct export fields and two transform kinds are the smallest useful cross-check; // no seed or literal hash constant is fixed, preserving polymorphic implementations. var isHashedApiResolverPresent = false; // A complete resolver remains abnormal in either process or DLL startup. Ordinary DLL // initialization may inspect loader state, but does not satisfy the full export/hash chain. if (PE_Cached.isEntryPointAnalysisAllowed && isPebModuleWalkPresent && epExportResolverHeaderAccessPresent && epExportResolverDirectoryAccessPresent && epExportResolverFieldCount >= 2 && epApiHashByteReadPresent && epApiHashMaximumTransformCount >= 2 && epApiHashMaximumTransformKindCount >= 2 && epApiHashBackwardControlFlowPresent && epHashedApiResolverIndirectTransferPresent && epResolverPathProven) { log(logType.emulation, "Manual API resolver hashes export names before an indirect transfer near EP"); isHashedApiResolverPresent = true; } if (isHashedApiResolverPresent) options = addOption(options, "Hashed API resolver near EP"); // Check for noncanonical x86 instruction-prefix groups in reachable EP code. var isRedundantEpPrefixPresent = false; if (epConflictingPrefixPresent || epRedundantPrefixInstructionCount >= epRedundantPrefixRequiredInstructionCount || epMaximumRedundantPrefixCount >= epRedundantPrefixImmediateCount) { log(logType.emulation, "Noncanonical prefixes near EP: instructions/max: " + epRedundantPrefixInstructionCount + "/" + epMaximumRedundantPrefixCount); isRedundantEpPrefixPresent = true; } if (isRedundantEpPrefixPresent) options = addOption(options, "Noncanonical prefixes near EP"); // Check for a direct call whose executable target starts by recovering its return address var isCallPopEpIpRecoveryPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && epCallPopIpRecoveryAddress !== -1) { // The shared pass already validated both the Pop target and later pointer use. log(logType.emulation, "Call/Pop IP recovery near EP"); isCallPopEpIpRecoveryPresent = true; } if (isCallPopEpIpRecoveryPresent) options = addOption(options, "Call/Pop IP recovery near EP"); // Check whether a CALL-next return address is recovered from a deeper stack slot. var isStackSlotIpRecoveryPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86) { const is64bit = PE_Cached.is64bit, register = is64bit ? "R(?:AX|BX|CX|DX|SI|DI|BP|8|9|1[0-5])" : "E(?:AX|BX|CX|DX|SI|DI|BP)", stackPointer = is64bit ? "RSP" : "ESP", pointerWidth = is64bit ? "QWORD" : "DWORD", pointerSize = is64bit ? 8 : 4, nativePush = new RegExp("^PUSH (?:" + register + "|-?(?:0X[0-9A-F]+|[0-9]+))$"), nativePop = new RegExp("^POP (" + register + ")$"), stackAdjustment = new RegExp("^(ADD|SUB) " + stackPointer + ", (0X[0-9A-F]+|[0-9]+)$"), stackLoad = new RegExp("^MOV (" + register + "), " + pointerWidth + " PTR \\[" + stackPointer + "(?: \\+ (0X[0-9A-F]+|[0-9]+))?\\]$"), registerCopy = new RegExp("^MOV (" + register + "), (" + register + ")$"), recoverOrigin = new RegExp("^SUB (" + register + "), (?:0X0*5|0*5)$"), recoverOriginAdd = new RegExp("^ADD (" + register + "), -(?:0X0*5|0*5)$"), recoverOriginLea = new RegExp("^LEA (" + register + "), \\[(" + register + ") - (?:0X0*5|0*5)\\]$"), directJump = /^JMP (0X[0-9A-F]+|[0-9]+)$/; var address = PE_Cached.addressOfUnmanagedEntryPoint, visited = {}, callAddress = -1, preambleState = {}; // Reach the CALL-next without guessing across unknown control flow. for (var i = 0; i < 16; i++) { if (visited[address] || !isVirtualAddressRangeFileBacked(address, 1, SECTION_FLAGS_EXECUTE)) break; visited[address] = true; var instruction = PE.getDisasmString(address), length = PE.getDisasmLength(address), opCode = getAsmOpCode(instruction); if (!opCode || opCode === "DB" || length < 1 || length > 15 || !isVirtualAddressRangeFileBacked(address, length, SECTION_FLAGS_EXECUTE)) break; if (isEpProvenFaultingInstruction(instruction, opCode, preambleState.StackPointerZero)) break; var jump = instruction.match(directJump); if (opCode === "CALL") { var offset = PE.VAToOffset(address); if (length === 5 && offset !== -1 && PE.compare("E8 00 00 00 00", offset)) { callAddress = address; address += length; } break; } if (/^(?:CALLF|HLT|INT(?:1|3|O)?|IRET(?:D|Q)?|JMPF?|RET(?:F|N)?|SYS(?:CALL|ENTER|RET|EXIT)|UD[012])$/.test(opCode) && !jump) break; if (jump) { if (!isEpNativeNearTransfer(address, instruction, length)) break; address = parseInt(jump[1], 0); } else { if (isEpLinearFallthroughUnproven(instruction, address, preambleState, true)) break; address += length; } } if (callAddress !== -1) { var stackDepth = 0, recovered = {}; // Follow the exact saved return-address slot through stack movement and register copies. for (var step = 0; step < 64 && address - callAddress <= 0x200; step++) { if (!isVirtualAddressRangeFileBacked(address, 1, SECTION_FLAGS_EXECUTE)) break; var current = PE.getDisasmString(address), currentLength = PE.getDisasmLength(address), opcode = getAsmOpCode(current); if (!opcode || opcode === "DB" || currentLength < 1 || currentLength > 15 || !isVirtualAddressRangeFileBacked(address, currentLength, SECTION_FLAGS_EXECUTE)) break; var pop = current.match(nativePop), adjustment = current.match(stackAdjustment), load = current.match(stackLoad), copy = current.match(registerCopy), subtraction = current.match(recoverOrigin), addition = current.match(recoverOriginAdd), lea = current.match(recoverOriginLea); if ((subtraction && recovered[subtraction[1]]) || (addition && recovered[addition[1]]) || (lea && recovered[lea[2]])) { isStackSlotIpRecoveryPresent = true; break; } if (nativePush.test(current) || /^PUSH (?:AX|BX|CX|DX|SI|DI|BP|R(?:8|9|1[0-5])W)$/.test(current) || /^(?:PUSHF|PUSHFD|PUSHFQ)$/.test(current)) { var stackSlotPushSize = getEpPushOperandSize(address, currentLength); if (!stackSlotPushSize) break; stackDepth += stackSlotPushSize; } else if (pop) { if (stackDepth < pointerSize) break; stackDepth -= pointerSize; delete recovered[pop[1]]; } else if (adjustment) { var amount = parseInt(adjustment[2], 0); if (!isFinite(amount) || amount > 0x200) break; stackDepth += adjustment[1] === "SUB" ? amount : -amount; if (stackDepth < 0) break; } else if (load) { var loadOffset = load[2] ? parseInt(load[2], 0) : 0; if (loadOffset === stackDepth) recovered[load[1]] = true; else delete recovered[load[1]]; } else if (copy) { if (recovered[copy[2]]) recovered[copy[1]] = true; else delete recovered[copy[1]]; } else if (!/^(?:NOP|FNOP|PAUSE)$/.test(opcode)) { break; } if (stackDepth > 0x200) break; address += currentLength; } } if (isStackSlotIpRecoveryPresent) log(logType.emulation, "Call-next return address recovered from a saved stack slot near EP"); } if (isStackSlotIpRecoveryPresent) options = addOption(options, "Stack-slot IP recovery near EP"); // Check whether startup saves its stack pointer before transferring control. var isEpStackSavePresent = false, isEpStackHandoffPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86) { const entryPoint = PE_Cached.addressOfUnmanagedEntryPoint, is64bit = PE_Cached.is64bit, pointerSize = is64bit ? 8 : 4, stackPointer = is64bit ? "RSP" : "ESP", pointerWidth = is64bit ? "QWORD" : "DWORD", nativeRegister = is64bit ? "R(?:AX|BX|CX|DX|SI|DI|BP|8|9|1[0-5])" : "E(?:AX|BX|CX|DX|SI|DI|BP)", nativePush = new RegExp("^PUSH (?:(?:" + nativeRegister + ")|-?(?:0X[0-9A-F]+|[0-9]+))$"), nativePop = new RegExp("^POP (?:" + nativeRegister + ")$"), stackAdjustment = new RegExp("^(ADD|SUB) " + stackPointer + ", (0X[0-9A-F]+|[0-9]+)$"), ripStackStore = new RegExp("^MOV " + pointerWidth + " PTR \\[RIP ([+-]) (0X[0-9A-F]+|[0-9]+)\\], (" + stackPointer + "|" + nativeRegister + ")$"), absoluteStackStore = new RegExp("^MOV " + pointerWidth + " PTR \\[(0X[0-9A-F]+|[0-9]+)\\], (" + stackPointer + "|" + nativeRegister + ")$"), stackRegisterCopy = new RegExp("^MOV (" + nativeRegister + "), (" + stackPointer + "|" + nativeRegister + ")$"), stackRegisterLea = new RegExp("^LEA (" + nativeRegister + "), \\[" + stackPointer + "\\]$"), stackRegisterWrite = new RegExp("^(?!(?:CMP|TEST|PUSH) )[A-Z][A-Z0-9]* (" + epGeneralPurposeRegister + ")(?:,|$)"), ripStackLoad = new RegExp("^MOV (" + stackPointer + "|" + nativeRegister + "), " + pointerWidth + " PTR \\[RIP ([+-]) (0X[0-9A-F]+|[0-9]+)\\]$"), absoluteStackLoad = new RegExp("^MOV (" + stackPointer + "|" + nativeRegister + "), " + pointerWidth + " PTR \\[(0X[0-9A-F]+|[0-9]+)\\]$"), stackRestore = new RegExp("^MOV " + stackPointer + ", (" + nativeRegister + ")$"), // A partial SP write or x64 ESP zero-extension invalidates native stack depth too. stackPointerAlias = is64bit ? "(?:RSP|ESP|SP|SPL)" : "(?:ESP|SP)", unknownStackWrite = new RegExp("^(?!(?:CMP|TEST) )\\S+ " + stackPointerAlias + "(?:,|$)|^XCHG .*\\b" + stackPointerAlias + "$"), directJump = /^JMP (0X[0-9A-F]+|[0-9]+)$/, materializedJump = new RegExp("^JMP (" + nativeRegister + ")$"), materializedMove = new RegExp("^MOV(?:ABS)? (" + nativeRegister + "), (0X[0-9A-F]+|[0-9]+)$"), materializedLea = new RegExp("^LEA (" + epGeneralPurposeRegister + "), (?:(?:CS|DS|ES|SS|FS|GS):)?\\[(?:RIP|EIP) ([+-]) (0X[0-9A-F]+|[0-9]+)\\]$"), materializedLeaAbsolute = new RegExp("^LEA (" + epGeneralPurposeRegister + "), (?:(?:CS|DS|ES|SS|FS|GS):)?\\[(?:[RE]IZ\\*(?:1|2|4|8) \\+ )?(0X[0-9A-F]+|[0-9]+)\\]$"), conditionalBranch = /^(J(?!MP)[A-Z]+) (0X[0-9A-F]+|[0-9]+)$/; var hasWritableSection = false; for (var sectionIndex = 0; sectionIndex < PE_Cached.numberOfSections && !hasWritableSection; sectionIndex++) { var section = PE.section[sectionIndex]; if (section) hasWritableSection = section.hasFlags(SECTION_FLAGS_WRITE); } if (hasWritableSection) { var pending = [{ Address: entryPoint, Depth: 0, ZeroFlag: -1, CarryFlag: -1, StackRegisters: {}, ReachabilityProven: true }], visited = {}; // Keep stack depth and branch facts separate for each reachable path. for (var index = 0; index < pending.length && index < 96 && !isEpStackHandoffPresent; index++) { var state = pending[index], address = state.Address; if (Math.abs(address - entryPoint) > 0x200 || state.Depth < 0 || state.Depth > 0x200 || !isVirtualAddressRangeFileBacked(address, 1, SECTION_FLAGS_EXECUTE)) continue; var key = address + ":" + state.Depth + ":" + state.ZeroFlag + ":" + state.CarryFlag + ":" + state.ReachabilityProven + ":" + Object.keys(state.StackRegisters).sort().join(","); if (visited[key]) continue; visited[key] = true; var instruction = PE.getDisasmString(address), length = PE.getDisasmLength(address), opCode = getAsmOpCode(instruction), nextAddress = address + length; if (!opCode || opCode === "DB" || length < 1 || length > 15 || !isVirtualAddressRangeFileBacked(address, length, SECTION_FLAGS_EXECUTE)) continue; // AAM with a zero radix faults before a later stack snapshot can execute. if (/^AAM (?:0X0+|0+)$/i.test(instruction)) continue; var ripStore = is64bit && instruction.match(ripStackStore), absoluteStore = instruction.match(absoluteStackStore), jump = instruction.match(directJump), branch = instruction.match(conditionalBranch), adjustment = instruction.match(stackAdjustment), depth = state.Depth; var storeSource = ripStore ? ripStore[3] : absoluteStore ? absoluteStore[2] : String(), isOriginalStackPointer = storeSource === stackPointer ? depth === 0 : state.StackRegisters[getNativeRegisterKey(storeSource)] === true; if (isOriginalStackPointer) { var displacement = ripStore ? parseInt(ripStore[2], 0) : 0, storageAddress = absoluteStore ? parseInt(absoluteStore[1], 0) : nextAddress + (ripStore[1] === "+" ? displacement : -displacement); if (isVirtualAddressRangeFileBacked(storageAddress, pointerSize, SECTION_FLAGS_WRITE) && isVirtualAddressRangeFileBacked(nextAddress, 1, SECTION_FLAGS_EXECUTE)) { var transferInstruction = PE.getDisasmString(nextAddress), transferLength = PE.getDisasmLength(nextAddress), transfer = transferInstruction && transferInstruction.match(directJump), target = transfer && isEpNativeNearTransfer(nextAddress, transferInstruction, transferLength) ? parseInt(transfer[1], 0) : -1, transferEnd = nextAddress + transferLength; if (!transfer && transferInstruction && transferLength > 0 && transferLength <= 15 && isVirtualAddressRangeFileBacked(nextAddress, transferLength, SECTION_FLAGS_EXECUTE)) { var moveTarget = transferInstruction.match(materializedMove), leaTarget = is64bit && transferInstruction.match(materializedLea), absoluteLeaTarget = transferInstruction.match(materializedLeaAbsolute); if ((moveTarget || leaTarget || absoluteLeaTarget) && isVirtualAddressRangeFileBacked(transferEnd, 1, SECTION_FLAGS_EXECUTE)) { var indirectInstruction = PE.getDisasmString(transferEnd), indirectLength = PE.getDisasmLength(transferEnd), indirectJump = indirectInstruction && indirectInstruction.match(materializedJump); if (indirectJump && getNativeRegisterKey(indirectJump[1]) === getNativeRegisterKey((moveTarget || leaTarget || absoluteLeaTarget)[1]) && indirectLength > 0 && indirectLength <= 15 && isEpNativeNearTransfer(transferEnd, indirectInstruction, indirectLength)) { target = moveTarget ? parseInt(moveTarget[2], 0) : getEpEncodedLeaTarget(nextAddress, transferInstruction, transferLength); transferEnd += indirectLength; } } } if (target !== -1 && transferLength > 0 && transferLength <= 15 && isVirtualAddressRangeFileBacked(nextAddress, transferLength, SECTION_FLAGS_EXECUTE) && !(storageAddress < transferEnd && storageAddress + pointerSize > nextAddress)) { if (isVirtualAddressRangeFileBacked(target, 1, SECTION_FLAGS_EXECUTE)) { // A save followed by a distant transfer is a snapshot, not proof of reuse. if (Math.abs(storageAddress - entryPoint) <= 0x400 && Math.abs(target - entryPoint) >= 0x1000 && isVirtualAddressRangeFileBacked(storageAddress, pointerSize, SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE)) { isEpStackSavePresent = true; } var traceAddress = target, traceVisited = {}, loadedRegisters = {}; for (var traceStep = 0; state.ReachabilityProven && traceStep < 24; traceStep++) { if (traceVisited[traceAddress] || !isVirtualAddressRangeFileBacked(traceAddress, 1, SECTION_FLAGS_EXECUTE)) break; traceVisited[traceAddress] = true; var traceInstruction = PE.getDisasmString(traceAddress), traceLength = PE.getDisasmLength(traceAddress), traceOpCode = getAsmOpCode(traceInstruction), traceNextAddress = traceAddress + traceLength; if (!traceOpCode || traceOpCode === "DB" || traceLength < 1 || traceLength > 15 || !isVirtualAddressRangeFileBacked(traceAddress, traceLength, SECTION_FLAGS_EXECUTE) || (traceAddress < storageAddress + pointerSize && traceNextAddress > storageAddress)) break; var ripLoad = is64bit && traceInstruction.match(ripStackLoad), absoluteLoad = traceInstruction.match(absoluteStackLoad), restore = traceInstruction.match(stackRestore), traceJump = traceInstruction.match(directJump), loadDisplacement = ripLoad ? parseInt(ripLoad[3], 0) : 0, loadAddress = absoluteLoad ? parseInt(absoluteLoad[2], 0) : ripLoad ? traceNextAddress + (ripLoad[2] === "+" ? loadDisplacement : -loadDisplacement) : -1, loadedSource = ripLoad ? ripLoad[1] : absoluteLoad ? absoluteLoad[1] : String(); if (loadAddress === storageAddress) { if (loadedSource === stackPointer) { isEpStackHandoffPresent = true; break; } loadedRegisters[loadedSource] = true; } else if (restore && loadedRegisters[restore[1]]) { isEpStackHandoffPresent = true; break; } else if (traceJump) { if (!isEpNativeNearTransfer(traceAddress, traceInstruction, traceLength)) break; traceAddress = parseInt(traceJump[1], 0); continue; } else if (!/^(?:NOP|FNOP|PAUSE|ENDBR32|ENDBR64)$/.test(traceOpCode) && traceInstruction !== "LEA " + stackPointer + ", [" + stackPointer + "]") { break; } traceAddress = traceNextAddress; } if (isEpStackHandoffPresent) break; } } } } if (adjustment) { var amount = parseInt(adjustment[2], 0); if (!isFinite(amount) || amount > 0x200) continue; depth += adjustment[1] === "SUB" ? amount : -amount; } else if (nativePush.test(instruction) || (is64bit ? opCode === "PUSHFQ" : opCode === "PUSHFD")) { var stackHandoffPushSize = getEpPushOperandSize(address, length); if (!stackHandoffPushSize) continue; depth += stackHandoffPushSize; } else if (nativePop.test(instruction) || (is64bit ? opCode === "POPFQ" : opCode === "POPFD")) { depth -= pointerSize; } else if (/^(?:ENTER|LEAVE|PUSH|POP|IRET|RET|SYS(?:CALL|ENTER|RET|EXIT)|INT|HLT|UD[012]|CALLF|LOOP)/.test(opCode) || unknownStackWrite.test(instruction)) { continue; } if (depth < 0 || depth > 0x200) continue; var isDirectCall = /^CALL (?:0X[0-9A-F]+|[0-9]+)$/.test(instruction); if (isDirectCall && !isEpDirectCallReturnProven(address)) continue; // A callee may change RSP even when it returns; do not prove a handoff beyond any CALL. var reachabilityProven = state.ReachabilityProven && opCode !== "CALL"; var stackRegisters = {}, registerCopy = instruction.match(stackRegisterCopy), registerLea = instruction.match(stackRegisterLea), registerWrite = instruction.match(stackRegisterWrite), partialWrite = instruction.match(epPartialRegisterWritePattern); for (var knownRegister in state.StackRegisters) stackRegisters[knownRegister] = true; // Implicit accumulator writes and two-destination operations invalidate saved-SP provenance. if (opCode === "CALL" || opCode === "XCHG" || opCode === "XADD" || opCode === "CMPXCHG" || /^(?:MUL|DIV|IDIV)$/.test(opCode) || (opCode === "IMUL" && instruction.indexOf(",") === -1)) { stackRegisters = {}; } else if (registerCopy) { if ((registerCopy[2] === stackPointer && depth === 0) || stackRegisters[getNativeRegisterKey(registerCopy[2])]) { stackRegisters[getNativeRegisterKey(registerCopy[1])] = true; } else { delete stackRegisters[getNativeRegisterKey(registerCopy[1])]; } } else if (registerLea) { if (depth === 0) stackRegisters[getNativeRegisterKey(registerLea[1])] = true; else delete stackRegisters[getNativeRegisterKey(registerLea[1])]; } else if (registerWrite) { delete stackRegisters[getNativeRegisterKey(registerWrite[1])]; } else if (partialWrite) { delete stackRegisters[getEpPartialRegisterKey(partialWrite[1])]; } else if (opCode !== "CMP" && opCode !== "TEST" && opCode !== "NOP" && opCode !== "FNOP" && opCode !== "PAUSE" && !adjustment && !nativePush.test(instruction) && !nativePop.test(instruction) && !jump && !branch) { // Unsupported instructions may write an alias of a saved pointer. stackRegisters = {}; } var zeroFlag = opCode === "CALL" ? -1 : advanceEpZeroFlag(instruction, opCode, state.ZeroFlag), carryFlag = opCode === "CALL" ? -1 : advanceEpCarryFlag(instruction, opCode, state.CarryFlag); if (jump) { if (!isEpNativeNearTransfer(address, instruction, length)) continue; pending.push({ Address: parseInt(jump[1], 0), Depth: depth, ZeroFlag: zeroFlag, CarryFlag: carryFlag, StackRegisters: stackRegisters, ReachabilityProven: reachabilityProven }); } else if (branch) { var branchResult = getEpKnownBranchResult(opCode, state.ZeroFlag, state.CarryFlag), branchReachabilityProven = reachabilityProven && branchResult !== -1; if (branchResult !== 0 && isEpNativeNearTransfer(address, instruction, length)) pending.push({ Address: parseInt(branch[2], 0), Depth: depth, ZeroFlag: zeroFlag, CarryFlag: carryFlag, StackRegisters: stackRegisters, ReachabilityProven: branchReachabilityProven }); if (branchResult !== 1) pending.push({ Address: nextAddress, Depth: depth, ZeroFlag: zeroFlag, CarryFlag: carryFlag, StackRegisters: stackRegisters, ReachabilityProven: branchReachabilityProven }); } else if (opCode !== "JMP" && opCode !== "CALLF" && opCode !== "JMPF") { pending.push({ Address: nextAddress, Depth: depth, ZeroFlag: zeroFlag, CarryFlag: carryFlag, StackRegisters: stackRegisters, ReachabilityProven: reachabilityProven }); } } } if (isEpStackHandoffPresent) log(logType.emulation, "Saved entry-point stack pointer restored after transfer"); else if (isEpStackSavePresent) log(logType.emulation, "Entry-point stack pointer saved in writable code before a distant jump"); } if (isEpStackHandoffPresent) options = addOption(options, "EP stack handoff"); else if (isEpStackSavePresent) options = addOption(options, "EP stack save before transfer"); // Check for called stubs which reconstruct their own nearby executable bytes var isSelfModifyingEpCallStubPresent = false; if (epSelfModifyingCallStubAddress !== -1) { // Either one strict stub or three generic section-local stubs proved reconstruction. log(logType.emulation, "Self-modifying call stub near EP at VA 0x" + epSelfModifyingCallStubAddress.toString(16)); isSelfModifyingEpCallStubPresent = true; } if (isSelfModifyingEpCallStubPresent) options = addOption(options, "Self-modifying stubs near EP"); // Check whether startup code writes executable bytes and transfers into that exact range var isWrittenCodeEpTransferPresent = false; var writtenCodeEpTransfer = traceEpWrittenCodeTransfer(); if (writtenCodeEpTransfer) { log(logType.emulation, "EP writes executable code at VA 0x" + writtenCodeEpTransfer.WriteAddress.toString(16) + " and transfers to VA 0x" + writtenCodeEpTransfer.TargetAddress.toString(16)); isWrittenCodeEpTransferPresent = true; } if (isWrittenCodeEpTransferPresent) options = addOption(options, "Transfer into rewritten code near EP"); // Check for carry-fed bitstream decoding with repeated string copies, or a // scalar byte-copy loop and transfer into generated code. var isBitstreamEpUnpackerPresent = false; var isScalarBitstreamCopyPresent = false, isScalarBitstreamVirtualWritePresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && epBitstreamAdcCount >= 8 && epPolymorphismBackwardControlFlowCount !== 0 && epBitstreamScalarReadCandidates && epBitstreamScalarReadCandidates.length) { var bitstreamVirtualTargets = [], bitstreamVirtualDestinations = []; for (var candidateIndex = 0; candidateIndex < epValidInvalidBranchCandidates.length; candidateIndex++) { var candidate = epValidInvalidBranchCandidates[candidateIndex]; if (candidate.OpCode !== "JMP") continue; var bitstreamTargetRva = candidate.TargetAddress - PE_Cached.imageBase; for (var d = 0; d < PE_Cached.numberOfSections; d++) { var bitstreamSectionRva = PE.getSectionVirtualAddress(d), bitstreamSectionRawSize = PE.getSectionFileSize(d); if (bitstreamTargetRva >= bitstreamSectionRva + bitstreamSectionRawSize && bitstreamTargetRva < bitstreamSectionRva + PE.getSectionVirtualSize(d) && (PE.section[d].Flags & (SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE)) === (SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE)) { bitstreamVirtualTargets.push({ SourceAddress: candidate.SourceAddress, Start: PE_Cached.imageBase + bitstreamSectionRva, End: PE_Cached.imageBase + bitstreamSectionRva + PE.getSectionVirtualSize(d) }); break; } } } // A decoder that provably copies bytes into a separate, zero-raw executable // section is an unpacker even when later API calls obscure its final jump. for (var d = 0; d < PE_Cached.numberOfSections; d++) { var bitstreamDestinationRva = PE.getSectionVirtualAddress(d), bitstreamDestinationSize = PE.getSectionVirtualSize(d); if (d !== PE_Cached.indexOfEntryPointSection && PE.getSectionFileSize(d) === 0 && bitstreamDestinationSize >= 0x1000 && (PE.section[d].Flags & (SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE)) === (SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE) && bitstreamDestinationRva + bitstreamDestinationSize <= epPolymorphismImageSize) { bitstreamVirtualDestinations.push({ Start: PE_Cached.imageBase + bitstreamDestinationRva, End: PE_Cached.imageBase + bitstreamDestinationRva + bitstreamDestinationSize }); } } if (bitstreamVirtualTargets.length || bitstreamVirtualDestinations.length) { const scalarWritePattern = /^MOV BYTE PTR \[([A-Z][A-Z0-9]*)\], ([ABCD][HL])$/i, scalarIncrementPattern = /^INC ([A-Z][A-Z0-9]*)$/i, scalarAdcPattern = /^ADC ([A-Z][A-Z0-9]*), \1$/i; for (var i = 0; i < epBitstreamScalarReadCandidates.length && !isScalarBitstreamCopyPresent; i++) { var scalarRead = epBitstreamScalarReadCandidates[i], scalarNextAddress = scalarRead.Address + scalarRead.Length, scalarIncrements = {}, scalarDestination = String(), scalarWriteAddress = -1, scalarSequenceValid = scalarRead.Length > 0; for (var j = 0; j < 3 && scalarSequenceValid; j++) { if (!isVirtualAddressRangeFileBacked(scalarNextAddress, 1, SECTION_FLAGS_EXECUTE)) { scalarSequenceValid = false; break; } var scalarInstruction = PE.getDisasmString(scalarNextAddress), scalarLength = PE.getDisasmLength(scalarNextAddress), scalarWrite = scalarInstruction.match(scalarWritePattern), scalarIncrement = scalarInstruction.match(scalarIncrementPattern); if (scalarLength <= 0 || !isVirtualAddressRangeFileBacked(scalarNextAddress, scalarLength, SECTION_FLAGS_EXECUTE)) { scalarSequenceValid = false; break; } if (scalarWrite && scalarWrite[2] === scalarRead.ValueRegister && !scalarDestination) { scalarDestination = scalarWrite[1]; scalarWriteAddress = scalarNextAddress; } else if (scalarIncrement) { scalarIncrements[scalarIncrement[1]] = (scalarIncrements[scalarIncrement[1]] || 0) + 1; } else { scalarSequenceValid = false; } scalarNextAddress += scalarLength; } var scalarValueParent = "E" + scalarRead.ValueRegister[0] + "X"; var isScalarCopyLoopPresent = false; for (var edgeIndex = 0; edgeIndex < epBitstreamBackwardEdges.length && !isScalarCopyLoopPresent; edgeIndex++) { var edge = epBitstreamBackwardEdges[edgeIndex]; if (edge.Target !== scalarRead.Address || edge.Source < scalarNextAddress || edge.Source > scalarNextAddress + 32) continue; var bitstreamLoopAddress = scalarNextAddress, isAdcInsideCopyLoopPresent = false; while (bitstreamLoopAddress < edge.Source) { if (!isVirtualAddressRangeFileBacked(bitstreamLoopAddress, 1, SECTION_FLAGS_EXECUTE)) break; var bitstreamLoopInstruction = PE.getDisasmString(bitstreamLoopAddress), bitstreamLoopLength = PE.getDisasmLength(bitstreamLoopAddress); if (bitstreamLoopLength <= 0 || !isVirtualAddressRangeFileBacked(bitstreamLoopAddress, bitstreamLoopLength, SECTION_FLAGS_EXECUTE)) break; if (scalarAdcPattern.test(bitstreamLoopInstruction)) isAdcInsideCopyLoopPresent = true; bitstreamLoopAddress += bitstreamLoopLength; } isScalarCopyLoopPresent = bitstreamLoopAddress === edge.Source && isAdcInsideCopyLoopPresent; } if (scalarSequenceValid && isScalarCopyLoopPresent && scalarDestination && scalarDestination !== scalarRead.SourceRegister && scalarIncrements[scalarRead.SourceRegister] === 1 && scalarIncrements[scalarDestination] === 1 && scalarRead.SourceRegister !== scalarValueParent && scalarDestination !== scalarValueParent && scalarRead.SourceRegister !== "R" + scalarValueParent.substring(1) && scalarDestination !== "R" + scalarValueParent.substring(1)) { for (var targetIndex = 0; targetIndex < bitstreamVirtualTargets.length && !isScalarBitstreamCopyPresent; targetIndex++) { var target = bitstreamVirtualTargets[targetIndex]; if (isEpBitstreamPathProven(epBitstreamStartAddresses, target.SourceAddress, scalarWriteAddress, epBitstreamSuccessors, epBitstreamFacts, { ReadAddress: scalarRead.Address, Register: scalarDestination, Start: target.Start, End: target.End, Instructions: epBitstreamInstructions })) { isScalarBitstreamCopyPresent = true; } } for (var destinationIndex = 0; destinationIndex < bitstreamVirtualDestinations.length && !isScalarBitstreamCopyPresent; destinationIndex++) { var destination = bitstreamVirtualDestinations[destinationIndex]; for (var transferIndex = 0; transferIndex < epBitstreamIndirectTransferAddresses.length && !isScalarBitstreamCopyPresent; transferIndex++) { if (isEpBitstreamPathProven(epBitstreamStartAddresses, epBitstreamIndirectTransferAddresses[transferIndex], scalarWriteAddress, epBitstreamSuccessors, epBitstreamFacts, { ReadAddress: scalarRead.Address, Register: scalarDestination, Start: destination.Start, End: destination.End, Instructions: epBitstreamInstructions })) { isScalarBitstreamCopyPresent = true; isScalarBitstreamVirtualWritePresent = true; } } } } } } } var isRepBitstreamCopyPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && epBitstreamAdcCount >= 8 && epBitstreamMovsbCount >= 2 && epBitstreamIndirectTransferPresent) { for (var i = 0; i < epBitstreamIndirectTransferAddresses.length && !isRepBitstreamCopyPresent; i++) { isRepBitstreamCopyPresent = isEpBitstreamPathProven(epBitstreamStartAddresses, epBitstreamIndirectTransferAddresses[i], -1, epBitstreamSuccessors, epBitstreamFacts); } } if (isRepBitstreamCopyPresent || isScalarBitstreamCopyPresent) { // Require decoding, copying and transfer on one reachable path. var bitstreamCopyDetail = isScalarBitstreamVirtualWritePresent ? ", scalar byte copy into virtual code and indirect transfer" : ", scalar byte copy and jump into virtual code"; log(logType.emulation, "Bitstream unpacker near EP: Adc: " + epBitstreamAdcCount + (isScalarBitstreamCopyPresent ? bitstreamCopyDetail : ", Rep Movsb: " + epBitstreamMovsbCount)); isBitstreamEpUnpackerPresent = true; } if (isBitstreamEpUnpackerPresent) options = addOption(options, "Bitstream unpacker near EP"); // Check for direct branches from reachable EP code into invalid image locations var isInvalidEpBranchTargetPresent = false; if (epInvalidBranchTargetAddress !== -1) { log(logType.emulation, "Invalid " + epInvalidBranchTargetOpCode[0] + epInvalidBranchTargetOpCode.substring(1).toLowerCase() + " target near EP: source VA 0x" + epInvalidBranchSourceAddress.toString(16) + " -> target VA 0x" + epInvalidBranchTargetAddress.toString(16)); isInvalidEpBranchTargetPresent = true; } if (isInvalidEpBranchTargetPresent) options = addOption(options, "Invalid branch target near EP"); // Check for repeated shifts or rotates whose immediate count is at least as wide as the operand var isNoncanonicalEpShiftPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && epNoncanonicalShiftCount >= 5) { // Five encodings establish a generator habit rather than one hand-written oddity. log(logType.emulation, "Noncanonical shifts near EP: " + epNoncanonicalShiftCount); isNoncanonicalEpShiftPresent = true; } if (isNoncanonicalEpShiftPresent) options = addOption(options, "Noncanonical shift near EP"); // Check for repeated arithmetic stack writes containing at least one complex bit transformation var isComplexEpStackMutationPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && epStackMutationCount >= 4 && epComplexStackMutationCount !== 0) { // Density supplies repetition; the uncommon transform supplies specificity. log(logType.emulation, "Stack mutations near EP: " + epStackMutationCount + " (complex: " + epComplexStackMutationCount + ")"); isComplexEpStackMutationPresent = true; } if (isComplexEpStackMutationPresent) options = addOption(options, "Complex stack mutation near EP"); // Check for a dense mixture of flag transfers, bidirectional rotates, carry arithmetic, // and at least five distinct rare bit-operation families. Prefer the concrete shift rule // when both match so that one polymorphic bit mixer does not produce duplicate verdicts. var isMixedEpBitOpsPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && epFlagsTransferPresent && epRolPresent && epRorPresent && epAdcPresent && epRareBitOpCount >= 5) { // Every mandatory family must be reachable; isolated arithmetic is insufficient. log(logType.emulation, "Mixed bit-operation families near EP: " + epRareBitOpCount); isMixedEpBitOpsPresent = true; } if (!isNoncanonicalEpShiftPresent && isMixedEpBitOpsPresent) options = addOption(options, "Mixed bit ops near EP"); // Check for a transform-heavy cyclic entry-point graph characteristic of generated // polymorphic dispatchers. No individual counter is trusted: stack traffic, calls, // branching, a backward edge and uncommon transforms must all be independently present. var isPolymorphicEpFlowPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && epPolymorphismBranchCount >= epPolymorphismBranchRequiredCount && epPolymorphismBackwardControlFlowCount !== 0 && epPolymorphismCallCount >= epPolymorphismCallRequiredCount && epPolymorphismPushCount >= epPolymorphismPushRequiredCount && epPolymorphismPushCount - epPolymorphismPopCount >= epPolymorphismStackImbalanceRequiredCount && epPolymorphismTransformCount >= epPolymorphismTransformRequiredCount && epPolymorphismTransformKindCount >= epPolymorphismTransformKindRequiredCount && epPolymorphismDominantTransformCount * 2 <= epPolymorphismTransformCount && epPolymorphismPushCount + epPolymorphismTransformCount >= epPolymorphismDensityRequiredCount) { // Independent minima keep every required behavior present. Unbalanced explicit stack // traffic rejects ordinary callees, transformation diversity and distribution exclude // fixed unpacking loops, and combined density rejects incidental mixtures. log(logType.emulation, "Irregular ctrl flow near EP: branches/back: " + epPolymorphismBranchCount + "/" + epPolymorphismBackwardControlFlowCount + ", Call: " + epPolymorphismCallCount + ", Push/Pop: " + epPolymorphismPushCount + "/" + epPolymorphismPopCount + ", transforms/identity/kinds/max: " + epPolymorphismTransformCount + "/" + epPolymorphismIdentityTransformCount + "/" + epPolymorphismTransformKindCount + "/" + epPolymorphismDominantTransformCount); isPolymorphicEpFlowPresent = true; } if (isPolymorphicEpFlowPresent) options = addOption(options, "Irregular ctrl flow near EP"); // Check for strange opcodes at entry point var isStrangeOpCodeNearEpPresent = false; if (epStrangeOpCodeDetected) { log(logType.emulation, epStrangeOpCodeDetected + " in reachable code near EP"); isStrangeOpCodeNearEpPresent = true; } if (isStrangeOpCodeNearEpPresent) options = addOption(options, epStrangeOpCodeDetected[0] + epStrangeOpCodeDetected.substring(1).toLowerCase() + " near EP"); // Check for an explicit x86 SEH stub that redirects the exception context. var isExceptionBasedEpTransferPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.hasUnmanagedEntryPoint && PE_Cached.isEntryPointCorrect && PE_Cached.isI386 && !PE_Cached.is64bit && !PE_Cached.isDynamicLinkLibrary && !PE_Cached.isDriver && !PE_Cached.isTlsPresent && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3) && isVirtualAddressRangeFileBacked(PE_Cached.addressOfUnmanagedEntryPoint, 19, SECTION_FLAGS_EXECUTE)) { const sehFileCharacteristics = PE.getImageFileHeader("Characteristics"), sehDllCharacteristics = PE.getImageOptionalHeader("DllCharacteristics"), sehOptionalHeaderOffset = getOptHeaderOffset(), sehOptionalHeaderSize = sehOptionalHeaderOffset >= 4 && sehOptionalHeaderOffset <= PE_Cached.fileSize ? PE.readWord(sehOptionalHeaderOffset - 4) : 0, sehOptionalHeaderEnd = sehOptionalHeaderOffset + sehOptionalHeaderSize, sehRelocationDirectoryOffset = sehOptionalHeaderOffset + 0x88, sehLoadConfigDirectoryOffset = sehOptionalHeaderOffset + 0xB0; // Absolute handler and target VAs are unambiguous only in a fixed-base image. // SafeSEH and pre-EP TLS code can alter whether this handler is usable. if ((sehFileCharacteristics & IMAGE_FILE_RELOCS_STRIPPED) && !(sehDllCharacteristics & (IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE | IMAGE_DLLCHARACTERISTICS_NO_SEH)) && sehOptionalHeaderOffset > 4 && sehOptionalHeaderSize >= 0xB8 && sehOptionalHeaderEnd <= PE_Cached.fileSize && PE.read_uint32(sehOptionalHeaderOffset + 0x5C) > 10 && PE.read_uint32(sehRelocationDirectoryOffset) === 0 && PE.read_uint32(sehRelocationDirectoryOffset + 4) === 0 && PE.read_uint32(sehLoadConfigDirectoryOffset) === 0 && PE.read_uint32(sehLoadConfigDirectoryOffset + 4) === 0) { var sehEntryPointOffset = PE.VAToOffset(PE_Cached.addressOfUnmanagedEntryPoint), sehStubLength = PE.compare("68........64FF350000000064892500000000", sehEntryPointOffset) || PE.compare("68........64A1000000005064892500000000", sehEntryPointOffset) ? 19 : 0; // MOV r32, FS:[0]; PUSH r32 also preserves the previous SEH head. Reject // ESP as the temporary register because that would change the stack itself. if (!sehStubLength && isVirtualAddressRangeFileBacked(PE_Cached.addressOfUnmanagedEntryPoint, 20, SECTION_FLAGS_EXECUTE) && PE.compare("68........648B..00000000..64892500000000", sehEntryPointOffset)) { var sehReadModRm = PE.readByte(sehEntryPointOffset + 7), sehReadRegister = (sehReadModRm >> 3) & 7; if ((sehReadModRm & 0xC7) === 0x05 && sehReadRegister !== 4 && PE.readByte(sehEntryPointOffset + 12) === 0x50 + sehReadRegister) sehStubLength = 20; } var sehTrapAddress = PE_Cached.addressOfUnmanagedEntryPoint + sehStubLength, sehTrapOffset = sehEntryPointOffset + sehStubLength, sehTrapLength = sehStubLength && PE.compare("CC", sehTrapOffset) ? 1 : sehStubLength && PE.compare("0F0B", sehTrapOffset) ? 2 : 0; if (sehTrapLength && isVirtualAddressRangeFileBacked(sehTrapAddress, sehTrapLength, SECTION_FLAGS_EXECUTE)) { var sehHandlerAddress = PE.read_uint32(sehEntryPointOffset + 1); if (isVirtualAddressRangeFileBacked(sehHandlerAddress, 19, SECTION_FLAGS_EXECUTE)) { var sehHandlerOffset = PE.VAToOffset(sehHandlerAddress), sehTargetAddress = PE.read_uint32(sehHandlerOffset + 10), sehHandlerLength = isVirtualAddressRangeFileBacked(sehHandlerAddress, 22, SECTION_FLAGS_EXECUTE) && PE.compare("8B44240CC780B8000000........B800000000C21000", sehHandlerOffset) ? 22 : PE.compare("8B44240CC780B8000000........31C0C21000", sehHandlerOffset) || PE.compare("8B44240CC780B8000000........33C0C21000", sehHandlerOffset) ? 19 : 0; // A raw SEH handler returns ExceptionContinueExecution = 0, not the // -1 used by a VEH handler or an __except filter. if (sehHandlerLength && isVirtualAddressRangeFileBacked(sehHandlerAddress, sehHandlerLength, SECTION_FLAGS_EXECUTE) && sehTargetAddress !== sehTrapAddress + sehTrapLength && isVirtualAddressRangeFileBacked(sehTargetAddress, 1, SECTION_FLAGS_EXECUTE)) { log(logType.emulation, "Static SEH transfer stub near EP: handler VA 0x" + sehHandlerAddress.toString(16) + " rewrites CONTEXT.Eip to VA 0x" + sehTargetAddress.toString(16)); isExceptionBasedEpTransferPresent = true; } } } } } if (isExceptionBasedEpTransferPresent) options = addOption(options, "Exception-based transfer near EP"); // Check whether every proven direct path from the formal EP reaches a breakpoint. var isBreakpointOnlyEpFlowPresent = false, epTlsCallbackRoots = getFileBackedTlsCallbackAddresses(); if (PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.hasUnmanagedEntryPoint && PE_Cached.isEntryPointCorrect && epTlsCallbackRoots && epTlsCallbackRoots.length === 0 && !PE_Cached.isDriver && PE_Cached.isArchX86 && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3) && isVirtualAddressRangeFileBacked(PE_Cached.addressOfUnmanagedEntryPoint, 1, SECTION_FLAGS_EXECUTE) && isEpBoundedDirectFlowProven(PE_Cached.addressOfUnmanagedEntryPoint, /^INT(?: ?3| 0X0*3)$/i)) { log(logType.emulation, "Every proven direct EP path reaches a breakpoint"); isBreakpointOnlyEpFlowPresent = true; } if (isBreakpointOnlyEpFlowPresent) options = addOption(options, "Breakpoint-only EP flow"); // Check each loader-declared TLS callback without merging unrelated execution paths. var isBreakpointOnlyTlsFlowPresent = false; if (PE_Cached.isTlsPresent && !PE_Cached.isDriver && PE_Cached.isArchX86 && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3)) { if (epTlsCallbackRoots) { for (var callbackIndex = 0; callbackIndex < epTlsCallbackRoots.length && !isBreakpointOnlyTlsFlowPresent; callbackIndex++) { if (isEpBoundedDirectFlowProven(epTlsCallbackRoots[callbackIndex], /^INT(?: ?3| 0X0*3)$/i)) { log(logType.emulation, "Every proven path from TLS callback VA 0x" + epTlsCallbackRoots[callbackIndex].toString(16) + " reaches a breakpoint"); isBreakpointOnlyTlsFlowPresent = true; } } } } if (isBreakpointOnlyTlsFlowPresent) options = addOption(options, "Breakpoint-only TLS flow"); // Check for TLS initialization paired with an unusable formal entry point. // A trap or immediate return delegates startup to TLS callbacks explicitly. An EP in // the virtual-only tail of an executable section is stronger evidence: no instruction // exists in the file, so TLS code must reconstruct it or prevent normal EP execution. var isTlsHiddenEpPresent = false; if (PE_Cached.isTlsPresent && epTlsCallbackRoots && epTlsCallbackRoots.length > 0) { const tlsEntryPointRva = PE_Cached.addressOfUnmanagedEntryPoint - PE_Cached.imageBase; var isTlsEntryPointInExecutableVirtualSection = false, isTlsEntryPointPhysicallyBacked = false, tlsEntryPointSectionIndex = -1; if (tlsEntryPointRva > 0) { for (var i = 0; i < PE_Cached.numberOfSections; i++) { const tlsEntryPointSectionRva = PE.getSectionVirtualAddress(i), tlsEntryPointSectionVirtualSize = PE.getSectionVirtualSize(i), tlsEntryPointSectionFileSize = PE.getSectionFileSize(i), tlsEntryPointSectionMappedSize = Math.max(tlsEntryPointSectionVirtualSize, tlsEntryPointSectionFileSize); if (tlsEntryPointRva >= tlsEntryPointSectionRva && tlsEntryPointRva < tlsEntryPointSectionRva + tlsEntryPointSectionMappedSize && (PE.section[i].Flags & SECTION_FLAGS_EXECUTE)) { var tlsEntryPointOffsetInSection = tlsEntryPointRva - tlsEntryPointSectionRva; isTlsEntryPointInExecutableVirtualSection = true; tlsEntryPointSectionIndex = i; // Check both the declared raw span and the actual file mapping. This // rejects zero-filled section tails as well as truncated raw data. if (tlsEntryPointOffsetInSection < tlsEntryPointSectionFileSize && PE.getSectionFileOffset(i) + tlsEntryPointOffsetInSection < PE_Cached.fileSize && PE.VAToOffset(PE_Cached.addressOfUnmanagedEntryPoint) !== -1) { isTlsEntryPointPhysicallyBacked = true; } } } } if (isTlsEntryPointInExecutableVirtualSection && !isTlsEntryPointPhysicallyBacked) { log(logType.any, "TLS virtual-only EP in section #" + tlsEntryPointSectionIndex); isTlsHiddenEpPresent = true; } else if (PE_Cached.isArchX86) { // DiE prints larger RET stack adjustments in hexadecimal, not only decimal. if (/^INT(?: )?3$/.test(PE_Cached.firstEpAsmInstruction) || // INT3 (0xCC) / INT 3 (0xCD 0x03) /^RET(?:N)?(?: (?:0X[0-9A-F]+|\d+))?$/i.test(PE_Cached.firstEpAsmInstruction)) { // Near RET/RETN log(logType.emulation, "TLS startup bypasses the formal EP: " + PE_Cached.firstEpAsmInstruction); isTlsHiddenEpPresent = true; } } else if (PE_Cached.isARM || PE_Cached.isThumb) { // ARM/Thumb: Check for breakpoint or return instructions if (/^BKPT/.test(PE_Cached.firstEpAsmInstruction) || // BKPT (breakpoint) /^BX(?: )?LR$/.test(PE_Cached.firstEpAsmInstruction)) { // BX LR (ARM32 return) log(logType.emulation, "TLS startup bypasses the formal EP: " + PE_Cached.firstEpAsmInstruction); isTlsHiddenEpPresent = true; } } else if (PE_Cached.isARM64) { if (/^BRK/.test(PE_Cached.firstEpAsmInstruction) || // BRK (breakpoint) /^RET(?: )?/.test(PE_Cached.firstEpAsmInstruction)) { // RET (return) log(logType.emulation, "TLS startup bypasses the formal EP: " + PE_Cached.firstEpAsmInstruction); isTlsHiddenEpPresent = true; } } } if (isTlsHiddenEpPresent) options = addOption(options, "TLS hidden EP"); // Check for a resolved chain of at least three direct JMPs from the entry point. var proxyJumpsAtEpPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.hasUnmanagedEntryPoint && PE_Cached.isEntryPointCorrect && PE_Cached.isArchX86 && isVirtualAddressRangeFileBacked(PE_Cached.addressOfUnmanagedEntryPoint, 1, SECTION_FLAGS_EXECUTE)) { var proxyJumpChain = traceEpDirectJumpChain(PE_Cached.addressOfUnmanagedEntryPoint, 64); if (proxyJumpChain && proxyJumpChain.Hops >= 3) { log(logType.emulation, "Resolved proxy jump chain at EP: " + proxyJumpChain.Hops + " hops"); proxyJumpsAtEpPresent = true; } } if (proxyJumpsAtEpPresent) options = addOption(options, "Proxy jmp at EP"); // Check for sections with full RWX permissions (Read, Write, Execute) var isRwxSectionPresent = false; var rwxSectionIndex = -1; for (var i = 0; i < PE_Cached.numberOfSections && !isRwxSectionPresent; i++) { if (PE.section[i].IsReadWriteExecute) { log(logType.any, "RWX permissions detected in section #" + i + " (\"" + File.cleanString(PE.getSectionName(i)) + "\")"); isRwxSectionPresent = true; rwxSectionIndex = i; } } if (isRwxSectionPresent) options = addOption(options, "Section #" + rwxSectionIndex + " (\"" + cleanSectionName(PE.getSectionName(rwxSectionIndex)) + "\") has RWX"); // Check for write-execute sections without read permission var isWxWithoutReadSectionPresent = false; var wxWithoutReadSectionIndex = -1; for (var i = 0; i < PE_Cached.numberOfSections && !isWxWithoutReadSectionPresent; i++) { var currentSectionFlags = PE.section[i].Flags; if ((currentSectionFlags & (SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE)) === (SECTION_FLAGS_EXECUTE | SECTION_FLAGS_WRITE) && !(currentSectionFlags & SECTION_FLAGS_READ)) { log(logType.any, "Write-execute section without read access detected: #" + i + " (\"" + File.cleanString(PE.getSectionName(i)) + "\")"); isWxWithoutReadSectionPresent = true; wxWithoutReadSectionIndex = i; } } if (isWxWithoutReadSectionPresent) options = addOption(options, "Section #" + wxWithoutReadSectionIndex + " (\"" + cleanSectionName(PE.getSectionName(wxWithoutReadSectionIndex)) + "\") has WX without R"); // Check for zero padding at the beginning of the entry point section var isEpSectionZeroPaddingPresent = false; var entryPointSectionIndex = -1; var isEpInNonExecutableSection = false; // Find the section that contains the entry point const entryPointRva = PE_Cached.addressOfUnmanagedEntryPoint - PE_Cached.imageBase; for (var i = 0; i < PE_Cached.numberOfSections && !isEpSectionZeroPaddingPresent; i++) { const sectionVirtualAddress = PE.getSectionVirtualAddress(i), sectionVirtualSize = PE.getSectionVirtualSize(i); // Check if the entry point is within this section if (entryPointRva >= sectionVirtualAddress && entryPointRva < sectionVirtualAddress + sectionVirtualSize) { entryPointSectionIndex = i; if (!(PE.section[i].Flags & SECTION_FLAGS_EXECUTE)) { log(logType.any, "Entry point is located in a non-executable section"); isEpInNonExecutableSection = true; } // Check for zero padding at the beginning of the section if (PE.compare("00 00 00", PE.getSectionFileOffset(entryPointSectionIndex))) { log(logType.any, "Zero padding detected at the beginning of EP section #" + entryPointSectionIndex); isEpSectionZeroPaddingPresent = true; } } } if (isEpSectionZeroPaddingPresent) options = addOption(options, "EP-section #" + entryPointSectionIndex + " (\"" + cleanSectionName(PE.getSectionName(entryPointSectionIndex)) + "\") zero padding"); if (isEpInNonExecutableSection) options = addOption(options, "EP in non-executable section"); // A physically backed entry point in an executable section should be marked as code. var isEpInNonCodeSection = false; if (PE_Cached.isEntryPointAnalysisAllowed && !PE_Cached.isDriver && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3) && entryPointSectionIndex !== -1 && PE_Cached.isEntryPointCorrect && PE.getSectionFileSize(entryPointSectionIndex) !== 0 && (PE.section[entryPointSectionIndex].Flags & SECTION_FLAGS_EXECUTE) && !(PE.section[entryPointSectionIndex].Flags & SECTION_FLAGS_CODE)) { log(logType.any, "Executable EP section is not marked as code"); isEpInNonCodeSection = true; } if (isEpInNonCodeSection) options = addOption(options, "EP in non-code section"); // A loader-visible process or DLL entry point cannot reside in discardable startup code. var isDiscardableEpSectionPresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && !PE_Cached.isDriver && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3) && entryPointSectionIndex !== -1 && (PE.section[entryPointSectionIndex].Flags & SECTION_FLAGS_DISCARDABLE)) { log(logType.any, "Entry point section is marked discardable"); isDiscardableEpSectionPresent = true; } if (isDiscardableEpSectionPresent) options = addOption(options, "Discardable EP section"); // BaseOfCode is advisory and protectors may deliberately leave it pointing at a // virtual-only startup section. Treat this as a structural anomaly, not corruption. var isBaseOfCodeMismatchPresent = false; const baseOfCodeRva = PE.getImageOptionalHeader("BaseOfCode"); if (baseOfCodeRva !== 0) { var isBaseOfCodeMapped = false; for (var i = 0; i < PE_Cached.numberOfSections && !isBaseOfCodeMapped; i++) { const baseOfCodeSectionRva = PE.getSectionVirtualAddress(i), baseOfCodeSectionRawSize = PE.getSectionFileSize(i); if (baseOfCodeRva >= baseOfCodeSectionRva && baseOfCodeRva < baseOfCodeSectionRva + baseOfCodeSectionRawSize && PE.VAToOffset(PE_Cached.imageBase + baseOfCodeRva) !== -1) { isBaseOfCodeMapped = true; } } if (!isBaseOfCodeMapped) { log(logType.any, "BaseOfCode VA 0x" + (PE_Cached.imageBase + baseOfCodeRva).toString(16) + " is not file-backed"); isBaseOfCodeMismatchPresent = true; } } // A valid physical EP in executable code contradicts a zero SizeOfCode header value. var isUnreportedCodePresent = false; const declaredCodeSize = PE.getSizeOfCode(); if (PE_Cached.isEntryPointAnalysisAllowed && !PE_Cached.isDriver && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3) && entryPointSectionIndex !== -1 && PE_Cached.isEntryPointCorrect && PE.getSectionFileSize(entryPointSectionIndex) !== 0 && (PE.section[entryPointSectionIndex].Flags & SECTION_FLAGS_EXECUTE) && declaredCodeSize === 0) { log(logType.any, "Executable code is present while SizeOfCode is zero"); isUnreportedCodePresent = true; } if (isUnreportedCodePresent) options = addOption(options, "Unreported code"); // Protectors may append physical code without updating the aggregate SizeOfCode field. // Allow two pages of slack because legacy linkers do not always account for per-section // file alignment consistently; substantially larger differences remain structural anomalies. var isUnderreportedCodePresent = false, isSeverelyUnderreportedCodePresent = false, isUnderreportedStartupCodePresent = false; if (PE_Cached.isEntryPointAnalysisAllowed && !PE_Cached.isDriver && (PE_Cached.imageSubsystem === 2 || PE_Cached.imageSubsystem === 3) && declaredCodeSize !== 0) { var physicalCodeSize = 0, physicalCodeSectionCount = 0; // Reconstruct the physical code total from non-empty sections explicitly marked as code. for (var i = 0; i < PE_Cached.numberOfSections; i++) { if ((PE.section[i].Flags & SECTION_FLAGS_CODE) && PE.getSectionFileSize(i) !== 0) { physicalCodeSize += PE.getSectionFileSize(i); physicalCodeSectionCount++; } } const codeSizeAlignmentTolerance = Math.max( 0x2000, PE.getImageOptionalHeader("FileAlignment") * physicalCodeSectionCount ); // Report only the excess beyond both the PE header value and alignment tolerance. if (physicalCodeSize > declaredCodeSize + codeSizeAlignmentTolerance) { isUnderreportedCodePresent = true; isSeverelyUnderreportedCodePresent = physicalCodeSize > declaredCodeSize * 2; log(logType.any, "Physical code size (0x" + physicalCodeSize.toString(16) + ") exceeds SizeOfCode (0x" + declaredCodeSize.toString(16) + ")"); } } if (isUnderreportedCodePresent && PE_Cached.numberOfSections > 1 && entryPointSectionIndex !== -1 && PE_Cached.isEntryPointCorrect) { // Treat the final section as a startup-code candidate only after the global // SizeOfCode mismatch has independently established underreported physical code. const protectedStartupSectionIndex = PE_Cached.numberOfSections - 1, protectedStartupSection = PE.section[protectedStartupSectionIndex], protectedStartupSectionFlags = protectedStartupSection.Flags, protectedStartupSectionRequiredFlags = SECTION_FLAGS_CODE | SECTION_FLAGS_EXECUTE | SECTION_FLAGS_READ, protectedStartupSectionFileSize = PE.getSectionFileSize(protectedStartupSectionIndex), protectedStartupSectionStartAddress = PE_Cached.imageBase + PE.getSectionVirtualAddress(protectedStartupSectionIndex), protectedStartupSectionEndAddress = protectedStartupSectionStartAddress + Math.max( PE.getSectionVirtualSize(protectedStartupSectionIndex), protectedStartupSectionFileSize ); // Require a substantial, high-entropy, readable executable code section. Excluding // writable sections avoids conflating this rule with self-modifying startup code. if (protectedStartupSectionFileSize >= 0x1000 && (protectedStartupSectionFlags & protectedStartupSectionRequiredFlags) === protectedStartupSectionRequiredFlags && !(protectedStartupSectionFlags & SECTION_FLAGS_WRITE) && PE.calculateEntropy(PE.getSectionFileOffset(protectedStartupSectionIndex), protectedStartupSectionFileSize) > 6.6) { if (entryPointSectionIndex === protectedStartupSectionIndex) { // EP residing directly in the candidate section is a complete startup proof. isUnderreportedStartupCodePresent = true; } else if (PE_Cached.isArchX86) { // Otherwise prove that reachable EP control flow enters the candidate section. var protectedStartupPendingAddresses = [PE_Cached.addressOfUnmanagedEntryPoint], protectedStartupPendingZeroFlags = [-1], protectedStartupPendingStackPointerZeros = [false], protectedStartupPendingCarryFlags = [-1], protectedStartupPendingAddressIndex = 0, protectedStartupVisitedAddresses = {}, protectedStartupVisitedStates = {}, protectedStartupVisitedInstructionCount = 0; // Explore a small reachable control-flow graph instead of sweeping physical bytes. // Conditional branches keep both paths; a direct Call keeps its fall-through // only when the callee must return. The visited set handles loops, and // the global instruction limit bounds deliberately dense polymorphic dispatchers. while (protectedStartupPendingAddressIndex < protectedStartupPendingAddresses.length && protectedStartupVisitedInstructionCount < 512 && protectedStartupPendingAddressIndex < 1024 && !isUnderreportedStartupCodePresent) { // Process one queued address and canonicalize it for loop detection. var protectedStartupCurrentAddress = protectedStartupPendingAddresses[protectedStartupPendingAddressIndex], protectedStartupCurrentZeroFlag = protectedStartupPendingZeroFlags[protectedStartupPendingAddressIndex], protectedStartupCurrentStackPointerZero = protectedStartupPendingStackPointerZeros[protectedStartupPendingAddressIndex], protectedStartupCurrentCarryFlag = protectedStartupPendingCarryFlags[protectedStartupPendingAddressIndex++], protectedStartupAddressKey = protectedStartupCurrentAddress.toString(), protectedStartupStateKey = protectedStartupAddressKey + ":" + protectedStartupCurrentZeroFlag + ":" + protectedStartupCurrentCarryFlag + ":" + protectedStartupCurrentStackPointerZero; // The same instruction may have different reachable successors when // two paths arrive with different flags. if (protectedStartupVisitedStates[protectedStartupStateKey] || PE.VAToOffset(protectedStartupCurrentAddress) === -1) continue; protectedStartupVisitedStates[protectedStartupStateKey] = true; var protectedStartupInstruction = PE.getDisasmString(protectedStartupCurrentAddress), protectedStartupOpCode = getAsmOpCode(protectedStartupInstruction), protectedStartupInstructionLength = PE.getDisasmLength(protectedStartupCurrentAddress); if (!protectedStartupOpCode || protectedStartupOpCode === "DB" || protectedStartupInstructionLength < 1 || protectedStartupInstructionLength > 15 || !isVirtualAddressRangeFileBacked(protectedStartupCurrentAddress, protectedStartupInstructionLength, SECTION_FLAGS_EXECUTE)) continue; if (/^AAM (?:0X0+|0+)$/i.test(protectedStartupInstruction)) continue; if (isEpProvenFaultingInstruction(protectedStartupInstruction, protectedStartupOpCode, protectedStartupCurrentStackPointerZero)) continue; var protectedStartupNextAddress = protectedStartupCurrentAddress + protectedStartupInstructionLength, protectedStartupNextStackPointerZero = advanceEpStackPointerZero(protectedStartupInstruction, protectedStartupOpCode, protectedStartupCurrentStackPointerZero), protectedStartupDirectControlFlow = protectedStartupInstruction.match(/^(CALL|J[A-Z]+|LOOP(?:E|NE|NZ|Z)?) (0X[0-9A-F]+|\d+)$/i), protectedStartupNextZeroFlag = advanceEpZeroFlag(protectedStartupInstruction, protectedStartupOpCode, protectedStartupCurrentZeroFlag), protectedStartupNextCarryFlag = advanceEpCarryFlag(protectedStartupInstruction, protectedStartupOpCode, protectedStartupCurrentCarryFlag), protectedStartupBranchTaken = getEpKnownBranchResult(protectedStartupOpCode, protectedStartupNextZeroFlag, protectedStartupNextCarryFlag); // A rejected path must not turn a truncated target or a stack fault into startup-section evidence. if (protectedStartupDirectControlFlow && protectedStartupBranchTaken !== 0 && !isEpNativeNearTransfer(protectedStartupCurrentAddress, protectedStartupInstruction, protectedStartupInstructionLength)) continue; // Charge the instruction budget only once for each admitted physical address. if (!protectedStartupVisitedAddresses[protectedStartupAddressKey]) { protectedStartupVisitedAddresses[protectedStartupAddressKey] = true; protectedStartupVisitedInstructionCount++; } if (protectedStartupDirectControlFlow) { // Resolve direct control flow before deciding which successors remain reachable. var protectedStartupTargetAddress = parseInt(protectedStartupDirectControlFlow[2], 0), isProtectedStartupTargetProven = protectedStartupOpCode === "CALL" || protectedStartupOpCode === "JMP" || protectedStartupOpCode === "JMPF" || protectedStartupBranchTaken === 1; if (isProtectedStartupTargetProven && protectedStartupTargetAddress >= protectedStartupSectionStartAddress && protectedStartupTargetAddress < protectedStartupSectionEndAddress && PE.VAToOffset(protectedStartupTargetAddress) !== -1) { // A physically backed target inside the candidate section completes the proof. isUnderreportedStartupCodePresent = true; log(logType.emulation, "EP control flow reaches underreported section #" + protectedStartupSectionIndex + " at VA 0x" + protectedStartupTargetAddress.toString(16)); } else if (protectedStartupOpCode === "JMP" || protectedStartupOpCode === "JMPF") { // Unconditional jumps retain only their explicit destination. protectedStartupPendingAddresses.push(protectedStartupTargetAddress); protectedStartupPendingZeroFlags.push(protectedStartupNextZeroFlag); protectedStartupPendingStackPointerZeros.push(protectedStartupNextStackPointerZero); protectedStartupPendingCarryFlags.push(protectedStartupNextCarryFlag); } else if (protectedStartupOpCode === "CALL") { // The callee is reachable, but its fall-through requires a return proof. protectedStartupPendingAddresses.push(protectedStartupTargetAddress); protectedStartupPendingZeroFlags.push(protectedStartupCurrentZeroFlag); protectedStartupPendingStackPointerZeros.push(protectedStartupNextStackPointerZero); protectedStartupPendingCarryFlags.push(protectedStartupCurrentCarryFlag); if (isEpDirectCallReturnProven(protectedStartupCurrentAddress)) { protectedStartupPendingAddresses.push(protectedStartupNextAddress); protectedStartupPendingZeroFlags.push(-1); protectedStartupPendingStackPointerZeros.push(protectedStartupNextStackPointerZero); protectedStartupPendingCarryFlags.push(-1); } } else { if (protectedStartupBranchTaken !== 0) { protectedStartupPendingAddresses.push(protectedStartupTargetAddress); protectedStartupPendingZeroFlags.push(protectedStartupNextZeroFlag); protectedStartupPendingStackPointerZeros.push(protectedStartupNextStackPointerZero); protectedStartupPendingCarryFlags.push(protectedStartupNextCarryFlag); } if (protectedStartupBranchTaken !== 1) { protectedStartupPendingAddresses.push(protectedStartupNextAddress); protectedStartupPendingZeroFlags.push(protectedStartupNextZeroFlag); protectedStartupPendingStackPointerZeros.push(protectedStartupNextStackPointerZero); protectedStartupPendingCarryFlags.push(protectedStartupNextCarryFlag); } } } else if (protectedStartupOpCode === "CALL") { // An unresolved indirect call provides no proven fall-through. } else if (epScanStopOpCodes.indexOf(protectedStartupOpCode) === -1) { // Ordinary instructions advance linearly; terminal opcodes add no successor. protectedStartupPendingAddresses.push(protectedStartupNextAddress); protectedStartupPendingZeroFlags.push(protectedStartupNextZeroFlag); protectedStartupPendingStackPointerZeros.push(protectedStartupNextStackPointerZero); protectedStartupPendingCarryFlags.push(protectedStartupNextCarryFlag); } } // Clean up: release the control-flow worklist and visited-address set protectedStartupPendingAddresses = undefined; protectedStartupPendingZeroFlags = undefined; protectedStartupPendingStackPointerZeros = undefined; protectedStartupPendingCarryFlags = undefined; protectedStartupVisitedAddresses = undefined; protectedStartupVisitedStates = undefined; } } } const isSizeOfCodeMismatchPresent = isUnderreportedStartupCodePresent || isSeverelyUnderreportedCodePresent; const baseOrSizeOfCodeMismatchDetails = (isBaseOfCodeMismatchPresent ? "Base" : String()) + (isBaseOfCodeMismatchPresent && isSizeOfCodeMismatchPresent ? "/" : String()) + (isSizeOfCodeMismatchPresent ? "Size" : String()); if (isBaseOfCodeMismatchPresent || isSizeOfCodeMismatchPresent) options = addOption(options, baseOrSizeOfCodeMismatchDetails + " of code mismatch" + (isUnderreportedStartupCodePresent && !isBaseOfCodeMismatchPresent ? " (EP)" : String())); // Check for a lot of sections (more than 20 -> this is a lot for native builds) var tooManySectionsPresent = false; if (PE_Cached.numberOfSections > 20) { log(logType.any, "Unusually high section count detected"); tooManySectionsPresent = true; } if (tooManySectionsPresent) options = addOption(options, "Too many sections"); // Check for IAT directory presence var isIatDirEmpty = false; if (!_isResultPresent("linker", "Turbo Linker")) { // Ignore Delphi // Offset of the IAT directory (12 is the number of IMAGE_DIRECTORY_ENTRY_ entries, 8 is the size of each entry) const iatDirOffset = getOptHeaderOffset() + (PE_Cached.is64bit ? 0x70 : 0x60) + 12 * 8; if (PE_Cached.indexOfImportsSection !== -1 && PE.read_int32(iatDirOffset) === 0) { log(logType.any, "IAT data directory is empty despite existing imports"); isIatDirEmpty = true; } } if (isIatDirEmpty) options = addOption(options, "Empty IAT data directory"); // Check for unused exception section var isUnusedExceptionSectionPresent = false; if (PE.isSectionNamePresentExp(/^\.[xp]data$/)) { // Offset of the exception directory (12 is the number of IMAGE_DIRECTORY_ENTRY_ entries, 8 is the size of each entry) var exceptionDirOffset = getOptHeaderOffset() + (PE_Cached.is64bit ? 0x88 : 0x78); if (PE.read_int32(exceptionDirOffset) === 0 && PE.read_int32(exceptionDirOffset + 4) === 0) { log(logType.any, "Exception section has no matching data directory"); isUnusedExceptionSectionPresent = true; } } if (isUnusedExceptionSectionPresent) options = addOption(options, "Exceptions directory empty"); const dllCharacteristics = PE.getImageOptionalHeader("DllCharacteristics"); // ASLR requires base relocations to rebase the image safely var isAslrWithStrippedRelocs = false; const coffCharacteristics = PE.readByte(getOptHeaderOffset() - 2) | (PE.readByte(getOptHeaderOffset() - 1) << 8); if ((coffCharacteristics & IMAGE_FILE_RELOCS_STRIPPED) && (dllCharacteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE)) { log(logType.any, "ASLR is enabled while relocations are marked stripped"); isAslrWithStrippedRelocs = true; } if (isAslrWithStrippedRelocs) options = addOption(options, "ASLR with stripped relocs"); // Reserved bits (0-4) in DllCharacteristics must always be zero — no linker ever sets them var isDllCharsReservedBitsSet = false; if (dllCharacteristics & 0x001F) { // 0x001F -> bits 0-4 -> 0b0000000000011111 log(logType.any, "Reserved DLL characteristic flags are set"); isDllCharsReservedBitsSet = true; } if (isDllCharsReservedBitsSet) options = addOption(options, "Invalid DLL flags"); // High entropy VA (0x0020) widens ASLR to 64-bit range — requires ASLR (0x0040) to have any effect var isHighEntropyWithoutAslr = false; if ((dllCharacteristics & IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA) && !(dllCharacteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE)) { log(logType.any, "High-entropy VA is enabled without ASLR"); isHighEntropyWithoutAslr = true; } if (isHighEntropyWithoutAslr) options = addOption(options, "High entropy VA without ASLR"); // High entropy VA (0x0020) is meaningless on 32-bit images — 4 GB address space is not "high entropy" var isHighEntropyOn32bit = false; if ((dllCharacteristics & IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA) && !PE_Cached.is64bit) { log(logType.any, "High-entropy VA is enabled on a 32-bit image"); isHighEntropyOn32bit = true; } if (isHighEntropyOn32bit) options = addOption(options, "High entropy VA on 32-bit image"); // Force integrity (0x0080) demands a code-integrity check at load time, requiring a relocatable image (ASLR) var isForceIntegrityWithoutAslr = false; if ((dllCharacteristics & IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY) && !(dllCharacteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE)) { log(logType.any, "Force integrity is enabled without ASLR"); isForceIntegrityWithoutAslr = true; } if (isForceIntegrityWithoutAslr) options = addOption(options, "Force integrity without ASLR"); // Control flow guard (0x4000) is only effective on ASLR images — MSVC always links CFG with DYNAMIC_BASE var isGuardCfWithoutAslr = false; if ((dllCharacteristics & IMAGE_DLLCHARACTERISTICS_GUARD_CF) && !(dllCharacteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE)) { log(logType.any, "CFG is enabled without ASLR"); isGuardCfWithoutAslr = true; } if (isGuardCfWithoutAslr) options = addOption(options, "CFG without ASLR"); // No-SEH flag (0x0400) is invalid on 64-bit images — they do not use table-based SEH var isNoSehOn64bit = false; if ((dllCharacteristics & IMAGE_DLLCHARACTERISTICS_NO_SEH) && PE_Cached.is64bit) { log(logType.any, "No-SEH is set on a 64-bit image"); isNoSehOn64bit = true; } if (isNoSehOn64bit) options = addOption(options, "No-SEH on 64-bit image"); // Check for strange exports (Non-ASCII names) var isStrangeExportsPresent = false; if (PE_Cached.numberOfUnmanagedExports > 0) { for (var i = 0; i < PE_Cached.numberOfUnmanagedExports && !isStrangeExportsPresent; i++) { const functionName = PE.getExportFunctionName(i); if (functionName && (/^\d/.test(functionName) || !isAsciiString(functionName))) { log(logType.any, "Unusual export name detected: \"" + File.cleanString(functionName) + "\""); isStrangeExportsPresent = true; } } } if (isStrangeExportsPresent) options = addOption(options, "Strange exports"); // Looks for sections whose names contain strange (or invalid) characters var isStrangeImportsPresent = false; const badImportChars = '=~!@#$%^&*()"№;%:?*():;,|\'`<> '; for (var i = 0; i < PE_Cached.numberOfUnmanagedImports && !isStrangeImportsPresent; i++) { const libraryName = PE.getImportLibraryName(i); if (isAsciiString(libraryName)) { for (var l = 0; l < badImportChars.length && !isStrangeImportsPresent; l++) { if (libraryName.indexOf(badImportChars[l]) !== -1) { log(logType.any, "Unusual import library name detected: \"" + File.cleanString(libraryName) + "\""); isStrangeImportsPresent = true; } } } else if (libraryName.indexOf("MZ") !== 0) { // 'MZ' -> IAT corrupted log(logType.any, "Non-ASCII import library name detected"); isStrangeImportsPresent = true; } } if (isStrangeImportsPresent) options = addOption(options, "Strange imports"); // Check for strange resources var isStrangeResourcesPresent = false; for (var i = 0; i < PE_Cached.numberOfUnmanagedResources && !isStrangeResourcesPresent; i++) { const resourceName = PE.getResourceNameByNumber(i); if (resourceName && (!isAsciiString(resourceName) || isNameObfuscated(resourceName))) { log(logType.any, "Strange resource name: \"" + File.cleanString(resourceName) + "\""); isStrangeResourcesPresent = true; } } if (isStrangeResourcesPresent) options = addOption(options, "Strange resources"); // Check for zero linker version var isZeroLinkerVersionPresent = false; if (PE.getMajorLinkerVersion() === 0 && PE.getMinorLinkerVersion() === 0) { log(logType.any, "Linker version is zero"); isZeroLinkerVersionPresent = true; } if (isZeroLinkerVersionPresent) options = addOption(options, "Zero linker version"); // Check for .NET metadata presence var isNetMetaDataInNativePresent = false; if (isNetMetaDataPresent() && PE_Cached.numberOfUnmanagedImports > 0) { log(logType.net, "Managed metadata detected in a native image"); isNetMetaDataInNativePresent = true; } if (isNetMetaDataInNativePresent) options = addOption(options, "DotNET meta"); // Check for .NET runtime attach functions var isNetRuntimeAttachFunctionPresent = false; if (PE.isFunctionPresent("_CorExeMain") || PE.isFunctionPresent("_CorDllMain")) { log(logType.net, ".NET runtime attachment import detected"); isNetRuntimeAttachFunctionPresent = true; } if (isNetRuntimeAttachFunctionPresent) options = addOption(options, "DotNET runtime attach"); // ASM Guard fake signatures if (PE.section[".asmg"] || PE.section["ASMGUARD"]) { for (var f = 0; f < 3; f++) _removeResult("packer", ["UPX", "MPRESS", "EP:MPRESS"][f]); } if (options.length !== 0) isDetected = true; if (isDetected) { _setResult("~protection", "Generic", String(), PE_Cached.isVerbose ? options : String()); } } // #endregion /** * Returns the architecture label reported by the PE parser. * * @returns {string} The first `getOperationSystemOptions()` field, or an empty string. */ function getPeArchitecture() { const operationSystemOptions = PE.getOperationSystemOptions(); return operationSystemOptions ? operationSystemOptions.split(',')[0] : String(); } /** * Returns the architecture-aware assembly language label used by fallback heuristics. * * @returns {string} A generic or architecture-specific ASM label. */ function getAsmLanguageName() { switch (PE_Cached.architecture) { case "I386": // x86 return "ASMx86"; case "AMD64": // x86_64 return "ASMx64"; case "ARM": // ARM return "ASM ARM"; case "ARMNT": // ARMv7 Thumb-2 case "THUMB": // ARM Thumb return "ASM Thumb"; case "ARM64": // AArch64 return "ASM ARM64"; default: return "ASM"; } } const _patternSplitter = "|"; /** * Generates a delimiter-bounded pattern of instructions or opcodes from the entry point. * * @param {boolean} onlyOpCodes - If true, only the opcodes of the instructions are included in the pattern. * @param {number} numberOf - The number of instructions to include in the pattern. * @returns {string} A `|`-delimited pattern, truncated if disassembly ends early. * @example * emulateAndGetEpAsmPattern(true, 2); // "|PUSH|MOV|" */ function emulateAndGetEpAsmPattern(onlyOpCodes, numberOf) { // Initialize the result with a pattern splitter var result = _patternSplitter; // Iterate through instructions up to the specified number for (var i = 0; i < numberOf; i++) { // Reuse the shared linear cache instead of decoding the same EP prefix again. const asmInstruction = emulateAndGetAsmInstructionByIndex(i); if (!asmInstruction) break; // Safety check // Append either the opcode or the full instruction to the result result += ( onlyOpCodes ? getAsmOpCode(asmInstruction) : // "MOV" asmInstruction // "MOV EAX, 4" ) + _patternSplitter; } // Return the generated assembly pattern return result; } var epAsmInstructions = [], epAsmInstructionAddresses = [], epDirectCallReturnCache = {}; /** * Reads a complete byte range through its loaded addresses, without virtual zero-fill. * * Section boundaries do not change its loaded layout; split records may use unrelated * raw offsets. Every chunk must retain exact VA translation and physical bounds. * Overlapping section mappings remain inconclusive on the split-record fallback. * * @param {number} address - Loaded address of the first byte. * @param {number} size - Required byte count. * @returns {number[]|undefined} Exact file-backed bytes, or unknown. */ function readFileBackedVirtualBytes(address, size) { var data; if (!(size > 0) || size !== Math.floor(size) || size > PE_Cached.fileSize || address < PE_Cached.imageBase || address > 0x1FFFFFFFFFFFFF - size) return undefined; if (isVirtualAddressRangeFileBacked(address, size, 0)) { data = PE.readBytes(PE.VAToOffset(address), size); } else { data = []; while (data.length < size) { var chunkAddress = address + data.length, chunkRva = chunkAddress - PE_Cached.imageBase, chunkOffset = PE.VAToOffset(chunkAddress), chunkSize = 0, chunkSectionIndex = -1; if (chunkOffset < 0) return undefined; // Headers can contain loader metadata too, but only their actual file bytes. var headerSize = Math.min(PE.getImageOptionalHeader("SizeOfHeaders"), PE_Cached.fileSize); if (chunkRva < headerSize && chunkOffset === chunkRva) { chunkSize = Math.min(size - data.length, headerSize - chunkRva); } else { for (var i = 0; i < PE_Cached.numberOfSections && chunkSize === 0; i++) { var sectionOffset = chunkRva - PE.getSectionVirtualAddress(i), sectionRawSize = PE.getSectionFileSize(i); if (sectionOffset >= 0 && sectionOffset < sectionRawSize && PE.getSectionFileOffset(i) + sectionOffset === chunkOffset) { chunkSize = Math.min(size - data.length, sectionRawSize - sectionOffset); chunkSectionIndex = i; } } } if (chunkSize <= 0 || chunkOffset + chunkSize > PE_Cached.fileSize || PE.VAToOffset(chunkAddress + chunkSize - 1) !== chunkOffset + chunkSize - 1) return undefined; for (var i = 0; i < PE_Cached.numberOfSections; i++) { if (i === chunkSectionIndex) continue; var otherSectionRva = PE.getSectionVirtualAddress(i), otherSectionSize = Math.max(PE.getSectionVirtualSize(i), PE.getSectionFileSize(i)); if (chunkRva < otherSectionRva + otherSectionSize && otherSectionRva < chunkRva + chunkSize) return undefined; } var chunkData = PE.readBytes(chunkOffset, chunkSize); if (!chunkData || chunkData.length !== chunkSize) return undefined; for (var i = 0; i < chunkData.length; i++) data.push(chunkData[i]); } } return data && data.length === size ? data : undefined; } /** * Reads the four native address fields of a complete, physically backed TLS directory. * Split sections use loaded order; inexact QWORD fields retain the existing unknown value. * * @param {number} address - Loaded address of IMAGE_TLS_DIRECTORY. * @returns {number[]|undefined} Template start/end, index and callback-array VAs, or unknown. * @see {@link https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-tls-directory} PE TLS directory layout */ function readFileBackedTlsDirectory(address) { var pointerSize = PE_Cached.is64bit ? 8 : 4, directoryData = readFileBackedVirtualBytes(address, pointerSize * 4 + 8); if (!directoryData) return undefined; var fields = []; for (var i = 0; i < 4; i++) { var fieldOffset = i * pointerSize, lowDword = readUnsignedDwordFromBuffer(directoryData, fieldOffset), highDword = PE_Cached.is64bit ? readUnsignedDwordFromBuffer(directoryData, fieldOffset + 4) : 0; fields.push(highDword > 0x1FFFFF ? -1 : lowDword + highDword * 0x100000000); } return fields; } /** * Resolves a bounded, null-terminated PE TLS callback array from mapped image data. * Unknown or malformed metadata stays inconclusive; no callback is inferred merely * from the presence of a TLS data directory. Callback code is validated separately. * * @returns {number[]|undefined} Executable callback VAs, an empty array, or unknown. */ function getFileBackedTlsCallbackAddresses() { if (!PE_Cached.isTlsPresent) return []; var optionalHeaderOffset = getOptHeaderOffset(); if (optionalHeaderOffset < 4 || optionalHeaderOffset > PE_Cached.fileSize) return undefined; var optionalHeaderSize = PE.readWord(optionalHeaderOffset - 4), optionalHeaderEnd = optionalHeaderOffset + optionalHeaderSize, directoryCountOffset = optionalHeaderOffset + (PE_Cached.is64bit ? 0x6C : 0x5C), directoryTableOffset = optionalHeaderOffset + (PE_Cached.is64bit ? 0x70 : 0x60), tlsEntryOffset = directoryTableOffset + IMAGE_DIRECTORY_ENTRY_TLS * 8, tlsDirectoryMinimumSize = PE_Cached.is64bit ? 0x28 : 0x18; if (optionalHeaderSize <= 0 || optionalHeaderEnd > PE_Cached.fileSize || directoryCountOffset + 4 > optionalHeaderEnd || tlsEntryOffset + 8 > optionalHeaderEnd || PE.read_uint32(directoryCountOffset) <= IMAGE_DIRECTORY_ENTRY_TLS) return undefined; var tlsDirectoryRva = PE.read_uint32(tlsEntryOffset), tlsDirectorySize = PE.read_uint32(tlsEntryOffset + 4), tlsDirectoryAddress = PE_Cached.imageBase + tlsDirectoryRva; if (tlsDirectoryRva === 0 || tlsDirectorySize < tlsDirectoryMinimumSize) return undefined; var tlsDirectoryFields = readFileBackedTlsDirectory(tlsDirectoryAddress); if (!tlsDirectoryFields) return undefined; var templateStartAddress = tlsDirectoryFields[0], templateEndAddress = tlsDirectoryFields[1], tlsIndexAddress = tlsDirectoryFields[2], callbackArrayAddress = tlsDirectoryFields[3], pointerSize = PE_Cached.is64bit ? 8 : 4, callbacks = []; if (templateStartAddress === -1 || templateEndAddress === -1 || tlsIndexAddress === -1 || templateStartAddress > templateEndAddress || (templateStartAddress === 0) !== (templateEndAddress === 0) || (templateStartAddress !== 0 && templateStartAddress < templateEndAddress && !isVirtualAddressRangeMapped(templateStartAddress, templateEndAddress - templateStartAddress, 0)) || tlsIndexAddress === 0 || (!isVirtualAddressRangeMapped(tlsIndexAddress, 4, 0) && readMappedUnsignedInteger(tlsIndexAddress, 4) === -1) || callbackArrayAddress === -1) return undefined; if (callbackArrayAddress === 0) return callbacks; for (var i = 0; i < 16; i++) { var pointerAddress = callbackArrayAddress + i * pointerSize; var callbackAddress = readMappedUnsignedInteger(pointerAddress, pointerSize); if (callbackAddress === 0) return callbacks; if (callbackAddress === -1 || !isVirtualAddressRangeFileBacked(callbackAddress, 1, SECTION_FLAGS_EXECUTE)) return undefined; callbacks.push(callbackAddress); } return undefined; } /** * Reads one unsigned DWORD or QWORD through contiguous loaded-image addresses. * A value may occupy headers, span unrelated raw offsets or end in a zero-filled tail. * Page padding requires proven section backing; low-alignment images retain file-identical bytes. * * @param {number} address - Virtual address of the value. * @param {number} size - Required integer width, four or eight bytes. * @returns {number} Exact value including virtual zero-fill, or -1 if unmapped, truncated or inexact. */ function readMappedUnsignedInteger(address, size) { if ((size !== 4 && size !== 8) || address < PE_Cached.imageBase || address > 0x1FFFFFFFFFFFFF - size) return -1; if (isVirtualAddressRangeFileBacked(address, size, 0)) { var valueOffset = PE.VAToOffset(address); return size === 8 ? readUnsignedQword(valueOffset) : PE.read_uint32(valueOffset); } var value = 0, fileBackedHeaderSize = Math.min(PE.getImageOptionalHeader("SizeOfHeaders"), PE_Cached.fileSize), imagePageSize = PE_Cached.architecture === "IA64" ? 0x2000 : 0x1000, imageSectionAlignment = PE.getImageOptionalHeader("SectionAlignment"); // The common value takes one native read. Only a split/virtual tail needs per-byte proof. // VirtualSize beyond SizeOfRawData is zero-filled: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-table-section-headers for (var i = 0; i < size; i++) { var byteAddress = address + i, byteValue = -1; if (isVirtualAddressRangeFileBacked(byteAddress, 1, 0)) { byteValue = PE.readByte(PE.VAToOffset(byteAddress)); } else { var byteRva = byteAddress - PE_Cached.imageBase; if (byteRva < fileBackedHeaderSize && PE.VAToOffset(byteAddress) === byteRva) { byteValue = PE.readByte(byteRva); } else { var isDeclaredSectionByte = false; for (var d = 0; d < PE_Cached.numberOfSections && byteValue === -1; d++) { var byteSectionOffset = byteRva - PE.getSectionVirtualAddress(d), byteSectionRawSize = PE.getSectionFileSize(d), byteSectionVirtualSize = PE.getSectionVirtualSize(d); if (byteSectionOffset >= 0 && byteSectionOffset < Math.max(byteSectionRawSize, byteSectionVirtualSize)) isDeclaredSectionByte = true; if (imageSectionAlignment >= imagePageSize && byteSectionOffset >= byteSectionRawSize && byteSectionOffset < byteSectionVirtualSize) byteValue = 0; } // Ordinary image pages zero-fill the tail after actual section contents, not the full SectionAlignment gap. // A low-alignment image uses file-identical layout, so its unowned file bytes must not become fabricated zeros. if (byteValue === -1 && !isDeclaredSectionByte && imageSectionAlignment >= imagePageSize && (imageSectionAlignment & (imageSectionAlignment - 1)) === 0 && byteRva < PE.getImageOptionalHeader("SizeOfImage")) { for (var d = 0; d < PE_Cached.numberOfSections && byteValue === -1; d++) { var pageSectionRva = PE.getSectionVirtualAddress(d), pageSectionRawSize = PE.getSectionFileSize(d), pageSectionMappedSize = Math.max(pageSectionRawSize, PE.getSectionVirtualSize(d)), byteOffsetInPageSection = byteRva - pageSectionRva; if (pageSectionRva % imagePageSize === 0 && pageSectionMappedSize > 0 && byteOffsetInPageSection >= pageSectionMappedSize && byteOffsetInPageSection < Math.ceil(pageSectionMappedSize / imagePageSize) * imagePageSize && (pageSectionRawSize === 0 || isVirtualAddressRangeFileBacked(PE_Cached.imageBase + pageSectionRva, pageSectionRawSize, 0))) { byteValue = 0; } } } else if (byteValue === -1 && !isDeclaredSectionByte && imageSectionAlignment > 0 && imageSectionAlignment < imagePageSize && (imageSectionAlignment & (imageSectionAlignment - 1)) === 0 && imageSectionAlignment === PE.getImageOptionalHeader("FileAlignment") && byteRva < PE.getImageOptionalHeader("SizeOfImage") && PE.getImageOptionalHeader("SizeOfHeaders") <= PE_Cached.fileSize) { var isFileIdenticalImage = true; for (var d = 0; d < PE_Cached.numberOfSections && isFileIdenticalImage; d++) { var flatSectionRva = PE.getSectionVirtualAddress(d), flatSectionRawSize = PE.getSectionFileSize(d), flatSectionRawOffset = PE.getSectionFileOffset(d); if (PE.getSectionVirtualSize(d) > flatSectionRawSize || (flatSectionRawSize > 0 && (flatSectionRawOffset !== flatSectionRva || flatSectionRawOffset < 0 || flatSectionRawOffset + flatSectionRawSize > PE_Cached.fileSize))) isFileIdenticalImage = false; } // Low-alignment images retain file bytes outside named sections; only the tail beyond EOF is zero-filled. // File/RVA identity is required: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-data if (isFileIdenticalImage) byteValue = byteRva < PE_Cached.fileSize ? PE.readByte(byteRva) : 0; } } } if (byteValue < 0) return -1; value += byteValue * Math.pow(256, i); } return value <= 0x1FFFFFFFFFFFFF ? value : -1; } /** * Proves that reachable EP code walks pages of its own mapped image. * The proof requires a statically derived image base and upper bound, a read through the * iterator, a page-sized advance, and a backward comparison branch to that read. * Direct branches fork independently; unknown instructions discard address facts. * Calls, malformed bytes and exhausted bounds remain inconclusive. * * @param {string[]} stopOpCodes - Terminal opcodes from the native EP scanner. * @returns {boolean} Whether a bounded reachable path contains the complete loop. */ function isEpImagePageSweepProven(stopOpCodes) { const register = "R(?:AX|BX|CX|DX|SI|DI|BP|8|9|1[0-5])", leaRegister = "(?:" + register + "|E(?:AX|BX|CX|DX|SI|DI|BP)|R(?:8|9|1[0-5])D)", ripAddress = new RegExp("^LEA (" + leaRegister + "), (?:(?:CS|DS|ES|SS|FS|GS):)?\\[(?:RIP|EIP) ([+-]) (0X[0-9A-F]+|\\d+)\\]$"), absoluteAddress = new RegExp("^LEA (" + leaRegister + "), (?:(?:CS|DS|ES|SS|FS|GS):)?\\[(?:[RE]IZ\\*(?:1|2|4|8) \\+ )?(0X[0-9A-F]+|\\d+)\\]$"), pageRead = new RegExp("^MOV (" + register + "), (?:BYTE|WORD|DWORD|QWORD) PTR \\[(" + register + ")\\]$"), pageAdvance = new RegExp("^ADD (" + register + "), (0X[0-9A-F]+|\\d+)$"), iteratorCompare = new RegExp("^CMP (" + register + "), (" + register + ")$"), branch = /^J[A-Z]+ (0X[0-9A-F]+|\d+)$/, directJump = /^JMP (0X[0-9A-F]+|\d+)$/, backwardLoopOpCodes = /^(?:JB|JC|JNAE|JL|JNGE)$/, entryPoint = PE_Cached.addressOfUnmanagedEntryPoint; if (entryPoint < 0 || entryPoint > 0x1FFFFFFFFFFFFF) return false; var pending = [{ Address: entryPoint, KnownAddresses: {}, ZeroFlag: -1, CarryFlag: -1, StackPointerZero: false, ReadAddress: -1, ReadRegister: String(), StepAddress: -1, StepRegister: String(), StepStart: -1, CompareAddress: -1, CompareLimit: -1, Visited: {} }]; for (var index = 0; index < pending.length && index < 128; index++) { var state = pending[index], address = state.Address; if (address < 0 || address > 0x1FFFFFFFFFFFFF || state.Visited[address] || Math.abs(address - entryPoint) > 0x200 || !isVirtualAddressRangeFileBacked(address, 1, SECTION_FLAGS_EXECUTE)) continue; var instruction = PE.getDisasmString(address), opCode = getAsmOpCode(instruction), length = PE.getDisasmLength(address); if (!opCode || opCode === "DB" || length < 1 || length > 15 || !isVirtualAddressRangeFileBacked(address, length, SECTION_FLAGS_EXECUTE)) continue; if (isEpProvenFaultingInstruction(instruction, opCode, state.StackPointerZero)) continue; state.StackPointerZero = advanceEpStackPointerZero(instruction, opCode, state.StackPointerZero); state.Visited[address] = true; var lea = instruction.match(ripAddress), absoluteLea = instruction.match(absoluteAddress), read = instruction.match(pageRead), advance = instruction.match(pageAdvance), compare = instruction.match(iteratorCompare), controlBranch = instruction.match(branch), jump = instruction.match(directJump), nextAddress = address + length; if (lea || absoluteLea) { var derivedRegister = getNativeRegisterKey((lea || absoluteLea)[1]), derivedAddress = getEpEncodedLeaTarget(address, instruction, length); if (derivedAddress >= 0 && derivedAddress <= 0x1FFFFFFFFFFFFF) { state.KnownAddresses[derivedRegister] = derivedAddress; } else { delete state.KnownAddresses[derivedRegister]; } state.ReadAddress = -1; state.StepAddress = -1; state.CompareAddress = -1; } else if (read) { delete state.KnownAddresses[read[1]]; state.ReadRegister = read[2]; state.ReadAddress = state.KnownAddresses[read[2]] !== undefined ? address : -1; state.StepAddress = -1; state.CompareAddress = -1; } else if (advance && parseInt(advance[2], 0) === 0x1000 && state.ReadAddress >= 0 && state.ReadRegister === advance[1] && state.KnownAddresses[advance[1]] !== undefined) { state.StepRegister = advance[1]; state.StepStart = state.KnownAddresses[advance[1]]; state.StepAddress = address; state.KnownAddresses[advance[1]] += 0x1000; state.CompareAddress = -1; } else if (compare && state.StepAddress >= 0 && address - state.StepAddress <= 16 && compare[1] === state.StepRegister && state.KnownAddresses[compare[2]] !== undefined) { state.CompareAddress = address; state.CompareLimit = state.KnownAddresses[compare[2]]; } else if (controlBranch) { var target = parseInt(controlBranch[1], 0); if (backwardLoopOpCodes.test(opCode) && state.CompareAddress >= 0 && address - state.CompareAddress <= 16 && target === state.ReadAddress && state.StepStart >= PE_Cached.imageBase && state.StepStart < PE_Cached.imageBase + 0x10000 && state.CompareLimit - state.StepStart >= 0x10000 && PE.VAToOffset(state.StepStart) !== -1 && isVirtualAddressRangeMapped(state.CompareLimit - 1, 1, 0)) return true; state.ReadAddress = -1; state.StepAddress = -1; state.CompareAddress = -1; } else if (!/^(?:NOP|FNOP|PAUSE)$/.test(opCode)) { state.KnownAddresses = {}; state.ReadAddress = -1; state.StepAddress = -1; state.CompareAddress = -1; } if (!jump && stopOpCodes.indexOf(opCode) !== -1) continue; var branchResult = controlBranch ? getEpKnownBranchResult(opCode, state.ZeroFlag, state.CarryFlag) : -1, successors = jump ? [parseInt(jump[1], 0)] : controlBranch ? branchResult === 1 ? [parseInt(controlBranch[1], 0)] : branchResult === 0 ? [nextAddress] : [parseInt(controlBranch[1], 0), nextAddress] : [nextAddress]; state.ZeroFlag = advanceEpZeroFlag(instruction, opCode, state.ZeroFlag); state.CarryFlag = advanceEpCarryFlag(instruction, opCode, state.CarryFlag); for (var successorIndex = 0; successorIndex < successors.length; successorIndex++) { var knownAddresses = {}, visited = {}; for (var key in state.KnownAddresses) knownAddresses[key] = state.KnownAddresses[key]; for (var seen in state.Visited) visited[seen] = true; pending.push({ Address: successors[successorIndex], KnownAddresses: knownAddresses, ZeroFlag: state.ZeroFlag, CarryFlag: state.CarryFlag, StackPointerZero: state.StackPointerZero, ReadAddress: state.ReadAddress, ReadRegister: state.ReadRegister, StepAddress: state.StepAddress, StepRegister: state.StepRegister, StepStart: state.StepStart, CompareAddress: state.CompareAddress, CompareLimit: state.CompareLimit, Visited: visited }); } } return false; } /** * Checks that a bounded near transfer uses the native-width control pointer. * In I386, an operand-size override uses a word pointer and truncates EIP; AMD64 * retains the existing near-transfer policy without inferring a legacy word size. * * @param {number} address - File-backed instruction virtual address. * @param {string} instruction - Decoded instruction text. * @param {number} length - Decoded instruction length in bytes. * @returns {boolean} Whether the near transfer supports the native-pointer proof. */ function isEpNativeNearTransfer(address, instruction, length) { if (/^L(?:CALL|JMP|RET)(?: |$)/.test(instruction)) return false; var opCode = getAsmOpCode(instruction); if (!/^(?:CALL|RET|RETN|J(?!MPF)[A-Z]+|LOOP(?:E|NE|NZ|Z)?)$/.test(opCode) || length < 1 || length > 15 || !isVirtualAddressRangeFileBacked(address, length, SECTION_FLAGS_EXECUTE)) return false; if (!PE_Cached.isI386) return true; var offset = PE.VAToOffset(address), hasWordOperand = false; // Operand width truncates EIP independently of the displacement width, including short branches. // Intel SDM, Jcc/JMP/LOOP "Operation": https://cdrdv2-public.intel.com/868140/253666-089-sdm-vol-2a.pdf for (var index = 0; index < length; index++) { var prefix = PE.readByte(offset + index); if (prefix === 0x66) { hasWordOperand = true; continue; } if (prefix === 0xF2 || prefix === 0xF3 || prefix === 0x2E || prefix === 0x36 || prefix === 0x3E || prefix === 0x26 || prefix === 0x64 || prefix === 0x65 || prefix === 0x67) continue; if (opCode === "CALL") return !hasWordOperand && (prefix === 0xE8 || prefix === 0xFF); if (opCode === "RET" || opCode === "RETN") return !hasWordOperand && (prefix === 0xC2 || prefix === 0xC3); if (prefix === 0xEB || (prefix >= 0xE0 && prefix <= 0xE3) || (prefix >= 0x70 && prefix <= 0x7F)) return !hasWordOperand; if (prefix === 0xE9 || prefix === 0xFF) return !hasWordOperand; if (prefix === 0x0F && index + 1 < length) { var branchOpcode = PE.readByte(offset + index + 1); return !hasWordOperand && branchOpcode >= 0x80 && branchOpcode <= 0x8F; } return false; } return false; } /** * Gets the stack-slot width of an already decoded x86 PUSH or PUSHF instruction. * Effective REX.W overrides 66; a later legacy prefix discards that REX. * Intel SDM Vol. 2A, 2.2.1: https://cdrdv2-public.intel.com/858446/253666-088-sdm-vol-2a.pdf * The caller validates executable mapping; this helper bounds every raw prefix read. * * @param {number} address - Virtual address of the decoded PUSH instruction. * @param {number} length - Complete decoded instruction length. * @returns {number} Two, four or eight bytes, or zero when the instruction range is unproven. */ function getEpPushOperandSize(address, length) { if (!PE_Cached.isArchX86 || !isFinite(address) || length < 1 || length > 15 || length % 1 !== 0) return 0; var offset = PE.VAToOffset(address), hasWordOperand = false, hasRexW = false; if (offset < 0 || offset + length > PE_Cached.fileSize) return 0; for (var index = 0; index < length; index++) { var prefix = PE.readByte(offset + index); if (prefix === 0x66) { hasWordOperand = true; hasRexW = false; } else if ([0xF2, 0xF3, 0x2E, 0x36, 0x3E, 0x26, 0x64, 0x65, 0x67].indexOf(prefix) !== -1) { hasRexW = false; } else if (PE_Cached.is64bit && prefix >= 0x40 && prefix <= 0x4F) { hasRexW = (prefix & 8) !== 0; } else return hasWordOperand && !hasRexW ? 2 : PE_Cached.isI386 ? 4 : 8; } return 0; } /** * Resolves a complete absolute or IP-relative LEA without a GPR base or index. * The raw address width is authoritative: legacy DiE can sign-extend an EA32 operand in its text. * * @param {number} address - File-backed executable instruction virtual address. * @param {string} instruction - Decoded LEA instruction text. * @param {number} length - Complete decoded instruction length. * @returns {number|undefined} Exact nonnegative address, or no proof for an unsupported form. */ function getEpEncodedLeaTarget(address, instruction, length) { if (!PE_Cached.isArchX86 || length < 1 || length > 15 || !isVirtualAddressRangeFileBacked(address, length, SECTION_FLAGS_EXECUTE)) return undefined; var destination = instruction.match(/^LEA ((?:E(?:AX|BX|CX|DX|SI|DI|BP|SP)|R(?:8|9|1[0-5])D)|R(?:AX|BX|CX|DX|SI|DI|BP|SP|8|9|1[0-5])), (?:(?:CS|DS|ES|SS|FS|GS):)?\[(?:(?:[RE]IZ\*(?:1|2|4|8) \+ )?(?:0X[0-9A-F]+|\d+)|(?:RIP|EIP) [+-] (?:0X[0-9A-F]+|\d+))\]$/i); if (!destination) return undefined; var offset = PE.VAToOffset(address), index = 0, hasWordOperand = false, hasAddressOverride = false, rex = 0; for (; index < length; index++) { var prefix = PE.readByte(offset + index); if ([0x66, 0x67, 0xF2, 0xF3, 0x2E, 0x36, 0x3E, 0x26, 0x64, 0x65].indexOf(prefix) !== -1) { if (prefix === 0x66) hasWordOperand = true; if (prefix === 0x67) hasAddressOverride = true; rex = 0; } else if (PE_Cached.is64bit && prefix >= 0x40 && prefix <= 0x4F) rex = prefix; else break; } // A word destination cannot prove a native pointer; I386 EA16 and LOCK are not these forms. if ((hasWordOperand && !(rex & 8)) || (PE_Cached.isI386 && hasAddressOverride) || index + 6 > length || PE.readByte(offset + index) !== 0x8D) return undefined; var modRm = PE.readByte(offset + index + 1), displacementOffset = index + 2, destinationIndex = ((modRm >>> 3) & 7) + (rex & 4 ? 8 : 0), destinationKey = destinationIndex < 8 ? ["RAX", "RCX", "RDX", "RBX", "RSP", "RBP", "RSI", "RDI"][destinationIndex] : "R" + destinationIndex; if (getNativeRegisterKey(destination[1]) !== destinationKey) return undefined; // RIZ/EIZ denotes the absent SIB index; its scale bits and every segment override have no effect on LEA. var isIpRelative = PE_Cached.is64bit && (modRm & 0xC7) === 5; if ((modRm & 0xC7) === 4 && index + 7 === length && !(rex & 2) && (PE.readByte(offset + index + 2) & 0x3F) === 0x25) displacementOffset++; else if (!((modRm & 0xC7) === 5 && index + 6 === length)) return undefined; var target = PE.read_uint32(offset + displacementOffset), isDwordDestination = !PE_Cached.is64bit || !(rex & 8); // DiE may print an already resolved IP-relative address; compute it from the signed displacement instead. if (isIpRelative) { var nextAddress = (hasAddressOverride || isDwordDestination ? address >>> 0 : address) + length, displacement = target < 0x80000000 ? target : target - 0x100000000; if (nextAddress > 0x1FFFFFFFFFFFFF) return undefined; target = nextAddress + displacement; if (hasAddressOverride || isDwordDestination) target >>>= 0; if (target < 0 || target > 0x1FFFFFFFFFFFFF || !isFinite(target)) return undefined; } else if (PE_Cached.is64bit && !hasAddressOverride && !isDwordDestination && target >= 0x80000000) { // LEA r32 zero-extends even an EA64 result; only a native EA64 destination sign-extends disp32. return undefined; } return target; } /** * Proves a bounded direct native control-flow graph reaches the same terminal on every path. * Direct conditional branches, including counter-based LOOPs, contribute both successors. Unknown calls or transfers, * undecodable bytes, cycles and exhausted budgets remain unproven rather than guessed. * * Multi-byte NOP operands are architectural no-ops and do not access memory. * * @param {number} startAddress - File-backed executable virtual address to examine. * @param {RegExp} terminalPattern - Exact terminal instruction required on every path. * @returns {boolean} Whether all bounded reachable paths end at that terminal. */ function isEpBoundedDirectFlowProven(startAddress, terminalPattern) { var pending = [startAddress], nextIndex = 0, nodes = {}, nodeCount = 0; while (nextIndex < pending.length) { var address = pending[nextIndex++], key = address.toString(); if (nodes.hasOwnProperty(key)) continue; // Duplicate successors do not consume the unique-node budget or invalidate a completed graph. if (nodeCount >= 64) break; if (!isVirtualAddressRangeFileBacked(address, 1, SECTION_FLAGS_EXECUTE)) break; var instruction = PE.getDisasmString(address), opCode = getAsmOpCode(instruction), length = PE.getDisasmLength(address), isTerminal = terminalPattern.test(instruction); if (!opCode || opCode === "DB" || length < 1 || length > 15 || !isVirtualAddressRangeFileBacked(address, length, SECTION_FLAGS_EXECUTE)) break; if (isTerminal && /^(?:RET|RETN)$/.test(opCode) && !isEpNativeNearTransfer(address, instruction, length)) break; var successors = [], nextAddress = address + length, directBranch = instruction && instruction.match(/^(J[A-Z]+|LOOP(?:E|NE|NZ|Z)?) (0X[0-9A-F]+|\d+)$/i); if (!isTerminal) { if (directBranch && opCode !== "JMPF") { if (!isEpNativeNearTransfer(address, instruction, length)) break; successors.push(parseInt(directBranch[2], 0)); if (opCode !== "JMP") successors.push(nextAddress); } else if (opCode === "NOP" || /^(?:PAUSE|ENDBR32|ENDBR64)$/.test(instruction)) { successors.push(nextAddress); } else { break; } } nodes[key] = { Terminal: isTerminal, Successors: successors }; nodeCount++; for (var i = 0; i < successors.length; i++) { if (!isVirtualAddressRangeFileBacked(successors[i], 1, SECTION_FLAGS_EXECUTE)) break; pending.push(successors[i]); } if (i !== successors.length) break; } var proven = {}; if (nextIndex === pending.length) { for (var key in nodes) if (nodes[key].Terminal) proven[key] = true; for (var iteration = 0; iteration < nodeCount; iteration++) { for (var key in nodes) { if (proven[key] || !nodes[key].Successors.length) continue; var allTerminal = true; for (var i = 0; i < nodes[key].Successors.length; i++) { if (!proven[nodes[key].Successors[i].toString()]) allTerminal = false; } if (allTerminal) proven[key] = true; } } } return proven[startAddress.toString()] === true; } /** * Resolves a bounded chain of direct native JMP instructions to its first non-JMP. * Conditional-branch proofs may also cross side-effect-free NOP instructions. * Every decoded byte must be file-backed executable code; cycles, interior targets, * indirect transfers and exhausted budgets remain unproven. * * @param {number} startAddress - Virtual address of the first chain node. * @param {number} maximumSteps - Maximum number of instructions to follow. * @param {boolean} allowNeutral - Whether to cross NOP, FNOP and PAUSE instructions. * @param {Object} [forbiddenInteriors] - Previously decoded instruction interiors to reject. * @returns {Object|undefined} Final address and hop count, or no proof. */ function traceEpDirectJumpChain(startAddress, maximumSteps, allowNeutral, forbiddenInteriors) { var address = startAddress, visited = {}, interiors = {}, hops = 0, steps = 0; while (steps <= maximumSteps && isFinite(address) && !visited[address.toString()] && !interiors[address.toString()] && isVirtualAddressRangeFileBacked(address, 1, SECTION_FLAGS_EXECUTE)) { if (forbiddenInteriors && forbiddenInteriors[address.toString()]) return undefined; var instruction = PE.getDisasmString(address), opCode = getAsmOpCode(instruction), length = PE.getDisasmLength(address), directJump = instruction && instruction.match(/^JMP (0X[0-9A-F]+|\d+)$/i); if (!opCode || opCode === "DB" || length < 1 || length > 15 || !isVirtualAddressRangeFileBacked(address, length, SECTION_FLAGS_EXECUTE)) return undefined; var isNeutral = allowNeutral && /^(?:NOP|FNOP|PAUSE)$/.test(opCode); if (directJump && !isEpNativeNearTransfer(address, instruction, length)) return undefined; if (!directJump && !isNeutral) return opCode === "JMP" ? undefined : { Target: address, Hops: hops }; if (steps === maximumSteps) return undefined; visited[address.toString()] = true; for (var byteIndex = 1; byteIndex < length; byteIndex++) interiors[(address + byteIndex).toString()] = true; address = directJump ? parseInt(directJump[1], 0) : address + length; if (directJump) hops++; steps++; } return undefined; } /** * Proves a direct x86 CALL enters an immediate stack-address mutation and RET. * The only modified return value must resolve to file-backed executable code. * * @param {number} callAddress - Virtual address of the direct CALL. * @returns {number|undefined} Proven redirected return address, or no proof. */ function getEpCallReturnMutationTarget(callAddress) { if (!PE_Cached.isI386) return undefined; var callInstruction = PE.getDisasmString(callAddress), directCall = callInstruction && callInstruction.match(/^CALL (0X[0-9A-F]+|\d+)$/i), callLength = PE.getDisasmLength(callAddress); if (!directCall || !isEpNativeNearTransfer(callAddress, callInstruction, callLength)) return undefined; var calleeAddress = parseInt(directCall[1], 0), fallThroughAddress = callAddress + callLength; if (Math.abs(calleeAddress - callAddress) > 0x200 || !isVirtualAddressRangeFileBacked(calleeAddress, 1, SECTION_FLAGS_EXECUTE)) return undefined; var mutationInstruction = PE.getDisasmString(calleeAddress), mutation = mutationInstruction && mutationInstruction.match(/^(ADD|SUB) DWORD PTR \[ESP\], -?(?:0X[0-9A-F]+|\d+)$/i), mutationLength = PE.getDisasmLength(calleeAddress); if (!mutation || mutationLength <= 0 || !isVirtualAddressRangeFileBacked(calleeAddress, mutationLength, SECTION_FLAGS_EXECUTE)) return undefined; var mutationOffset = PE.VAToOffset(calleeAddress), mutationOpcode = PE.readByte(mutationOffset), mutationModRm = PE.readByte(mutationOffset + 1), mutationDirection = mutation[1] === "ADD" ? 0x04 : 0x2C, mutationValue; if (mutationModRm !== mutationDirection || PE.readByte(mutationOffset + 2) !== 0x24) return undefined; if (mutationOpcode === 0x81 && mutationLength === 7) mutationValue = PE.read_uint32(mutationOffset + 3); else if (mutationOpcode === 0x83 && mutationLength === 4) { mutationValue = PE.readByte(mutationOffset + 3); if (mutationValue >= 0x80) mutationValue -= 0x100; } else return undefined; var returnInstructionAddress = calleeAddress + mutationLength, returnInstruction = PE.getDisasmString(returnInstructionAddress), returnLength = PE.getDisasmLength(returnInstructionAddress), returnTarget = (fallThroughAddress + (mutation[1] === "ADD" ? 1 : -1) * mutationValue) >>> 0; if (!/^RET(?:N)?$/.test(returnInstruction) || !isEpNativeNearTransfer(returnInstructionAddress, returnInstruction, returnLength) || returnTarget === fallThroughAddress || !isVirtualAddressRangeFileBacked(returnInstructionAddress, returnLength, SECTION_FLAGS_EXECUTE) || (returnTarget >= calleeAddress && returnTarget < returnInstructionAddress + returnLength) || !isVirtualAddressRangeFileBacked(returnTarget, 1, SECTION_FLAGS_EXECUTE)) return undefined; return returnTarget; } /** * Proves that every bounded path through a directly called native routine returns. * Unknown calls, indirect branches, non-returning terminals, cycles and budget exhaustion * remain unproven; their caller fall-through must not be treated as executable evidence. * * @param {number} callAddress - Virtual address of the direct CALL instruction. * @returns {boolean} Whether all observed paths from its target end in a near RET. */ function isEpDirectCallReturnProven(callAddress) { var callInstruction = PE.getDisasmString(callAddress), directCall = callInstruction && callInstruction.match(/^CALL (0X[0-9A-F]+|\d+)$/i); if (!directCall || !isEpNativeNearTransfer(callAddress, callInstruction, PE.getDisasmLength(callAddress))) return false; var target = parseInt(directCall[1], 0), targetKey = target.toString(); if (epDirectCallReturnCache.hasOwnProperty(targetKey)) return epDirectCallReturnCache[targetKey]; epDirectCallReturnCache[targetKey] = isEpBoundedDirectFlowProven(target, /^RET(?:N)?(?: (?:0X[0-9A-F]+|\d+))?$/i); return epDirectCallReturnCache[targetKey]; } /** * Finds constant arithmetic branches and computed stack/RET transfers on a bounded * entry-point graph. Direct jumps and conditional edges are decoded at their real * targets. Calls expose their continuation only through a bounded return proof; * unknown callees cannot expose later physical bytes as reachable evidence. * Register constants are discarded at calls and unknown register writes; * ROL/ROR preserve PF, SF and ZF, so their following Jcc must use the preceding * flag producer. The returned addresses are absent when a proof is unavailable. * * @returns {Object} OpaqueBranchAddress (-1 if absent) and ReturnTarget * (undefined if absent) for the same bounded EP graph. */ function getEpGuardedFlowEvidence() { var evidence = { OpaqueBranchAddress: -1, ReturnTarget: undefined }; if (!PE_Cached.isEntryPointAnalysisAllowed || !PE_Cached.isArchX86) return evidence; const register32 = "(?:E(?:AX|BX|CX|DX|SI|DI|BP)|R(?:8|9|1[0-5])D)", register64 = "R(?:AX|BX|CX|DX|SI|DI|BP|8|9|1[0-5])", addressRegister = PE_Cached.isI386 ? register32 : register64, literal = "(-?(?:0X[0-9A-F]+|[0-9]+))", immediateMove = new RegExp("^MOV (" + register32 + "|ESP), " + literal + "$", "i"), selfZero = new RegExp("^(?:SUB|XOR) (" + register32 + "), \\1$", "i"), arithmetic = new RegExp("^(ADD|SUB|AND|OR|XOR|SHR|SHL|ROL|ROR) (" + register32 + "), " + literal + "$", "i"), ripAddress = new RegExp("^LEA (" + register32 + "|" + register64 + "), \\[(?:RIP|EIP) ([+-]) (0X[0-9A-F]+|[0-9]+)\\]$", "i"), absoluteLea = new RegExp("^LEA (" + register32 + "|" + register64 + "), (?:(?:CS|DS|ES|SS|FS|GS):)?\\[(?:[RE]IZ\\*(?:1|2|4|8) \\+ )?(0X[0-9A-F]+|[0-9]+)\\]$", "i"), basedLea = new RegExp("^LEA (" + addressRegister + "), \\[(" + addressRegister + ")(?: ([+-]) (0X[0-9A-F]+|[0-9]+))?\\]$", "i"), nativeMove = new RegExp("^MOV(?:ABS)? (" + register64 + "|RSP), " + literal + "$", "i"), nativeArithmetic = new RegExp("^(ADD|SUB) (" + register64 + "), " + literal + "$", "i"), registerCopy = new RegExp("^MOV (" + register32 + "|" + register64 + "), (" + register32 + "|" + register64 + ")$", "i"), stackExchange = PE_Cached.isI386 ? /^XCHG DWORD PTR \[ESP\], (E(?:AX|BX|CX|DX|SI|DI|BP))$/i : new RegExp("^XCHG QWORD PTR \\[RSP\\], (" + register64 + ")$", "i"), stackStore = PE_Cached.isI386 ? /^MOV DWORD PTR \[ESP\], (E(?:AX|BX|CX|DX|SI|DI|BP))$/i : new RegExp("^MOV QWORD PTR \\[RSP\\], (" + register64 + ")$", "i"), pushedRegister = new RegExp("^PUSH (" + (PE_Cached.isI386 ? register32 : register64) + ")$", "i"), poppedRegister = new RegExp("^POP (" + addressRegister + ")$", "i"), stackArithmetic = PE_Cached.isI386 ? /^(ADD|SUB) DWORD PTR \[ESP\], (-?(?:0X[0-9A-F]+|[0-9]+))$/i : /^(ADD|SUB) QWORD PTR \[RSP\], (-?(?:0X[0-9A-F]+|[0-9]+))$/i, directBranch = /^(J[A-Z]+) (0X[0-9A-F]+|[0-9]+)$/i; var pending = [{ Address: PE_Cached.addressOfUnmanagedEntryPoint, Known: {}, Flags: { PF: -1, SF: -1, ZF: -1, Steps: 0, Kinds: 0 }, StackTarget: undefined, StackPointerZero: false }], visited = {}, processed = 0; while (pending.length && processed < 1024 && (evidence.OpaqueBranchAddress === -1 || evidence.ReturnTarget === undefined)) { var state = pending.shift(), address = state.Address; if (!isVirtualAddressRangeFileBacked(address, 1, SECTION_FLAGS_EXECUTE)) continue; var key = address + ":" + state.Flags.PF + ":" + state.Flags.SF + ":" + state.Flags.ZF + ":" + state.Flags.Steps + ":" + state.Flags.Kinds + ":" + state.StackTarget + ":" + state.StackPointerZero + ":" + JSON.stringify(state.Known); if (visited[key]) continue; visited[key] = true; var instruction = PE.getDisasmString(address), length = PE.getDisasmLength(address), opCode = getAsmOpCode(instruction); if (!opCode || opCode === "DB" || length < 1 || length > 15 || !isVirtualAddressRangeFileBacked(address, length, SECTION_FLAGS_EXECUTE)) continue; if (/^AAM (?:0X0+|0+)$/i.test(instruction)) continue; if (isEpProvenFaultingInstruction(instruction, opCode, state.StackPointerZero, state.Known)) continue; processed++; var next = { Address: address + length, Known: {}, Flags: { PF: state.Flags.PF, SF: state.Flags.SF, ZF: state.Flags.ZF, Steps: state.Flags.Steps, Kinds: state.Flags.Kinds }, StackTarget: state.StackTarget, StackPointerZero: advanceEpStackPointerZero(instruction, opCode, state.StackPointerZero, state.Known) }; for (var register in state.Known) next.Known[register] = state.Known[register]; var move = instruction.match(immediateMove), zero = instruction.match(selfZero), operation = instruction.match(arithmetic), rip = instruction.match(ripAddress), absolute = instruction.match(absoluteLea), based = instruction.match(basedLea), native = instruction.match(nativeMove), nativeOp = instruction.match(nativeArithmetic), copy = instruction.match(registerCopy), exchange = instruction.match(stackExchange), store = instruction.match(stackStore), push = instruction.match(pushedRegister), pop = instruction.match(poppedRegister), stackOp = instruction.match(stackArithmetic), branch = instruction.match(directBranch); if (zero || move || operation) { // Function-scoped vars must not reuse a previous instruction's resolved value. var destination = getNativeRegisterKey((zero || move || operation)[1 + (operation ? 1 : 0)]), old = next.Known[destination], value = undefined, steps = undefined, kinds = undefined; if (zero) { value = 0; steps = 1; kinds = instruction.indexOf("XOR ") === 0 ? 4 : 1; } else if (move) { value = parseInt(move[2], 0) >>> 0; steps = 0; kinds = 0; } else if (old && isFinite(old.Value)) { var operand = parseInt(operation[3], 0) >>> 0, shift = operand & 31; // A dword operation discards the native high bits before arithmetic, avoiding inexact sums. value = old.Value >>> 0; steps = old.Steps + 1; kinds = old.Kinds | (/^(?:SHR|SHL|ROL|ROR)$/.test(operation[1]) ? 2 : /^(?:AND|OR|XOR)$/.test(operation[1]) ? 4 : 1); // A masked zero shift count changes neither flags nor arithmetic evidence. if (/^(?:SHR|SHL|ROL|ROR)$/.test(operation[1]) && !shift) { steps = old.Steps; kinds = old.Kinds; } if (operation[1] === "ADD") value = (value + operand) >>> 0; else if (operation[1] === "SUB") value = (value - operand) >>> 0; else if (operation[1] === "AND") value = (value & operand) >>> 0; else if (operation[1] === "OR") value = (value | operand) >>> 0; else if (operation[1] === "XOR") value = (value ^ operand) >>> 0; else if (operation[1] === "SHR" && shift) value >>>= shift; else if (operation[1] === "SHL" && shift) value = (value << shift) >>> 0; else if (operation[1] === "ROL" && shift) value = ((value << shift) | (value >>> (32 - shift))) >>> 0; else if (operation[1] === "ROR" && shift) value = ((value >>> shift) | (value << (32 - shift))) >>> 0; } if (value === undefined || !isFinite(value)) delete next.Known[destination]; else next.Known[destination] = { Value: value, Steps: steps, Kinds: kinds }; if (destination === "RSP") next.StackTarget = undefined; if (!move && (!operation || !/^(?:ROL|ROR)$/.test(operation[1])) && !(operation && /^(?:SHR|SHL)$/.test(operation[1]) && !(parseInt(operation[3], 0) & 31))) { if (value === undefined) { next.Flags = { PF: -1, SF: -1, ZF: -1, Steps: 0, Kinds: 0 }; } else { var parityByte = value & 0xFF, parityBits = 0; for (var bit = 0; bit < 8; bit++) parityBits += (parityByte >>> bit) & 1; next.Flags = { PF: 1 - (parityBits & 1), SF: value >>> 31, ZF: value === 0 ? 1 : 0, Steps: steps, Kinds: kinds }; } } // Register-only operations cannot change the value already pushed onto the stack. } else if (rip || absolute || based || native || nativeOp || copy) { var targetRegister = getNativeRegisterKey((rip || absolute || based || native || nativeOp || copy)[1 + (nativeOp ? 1 : 0)]), target = undefined; if (rip) target = getEpEncodedLeaTarget(address, instruction, length); else if (absolute) target = getEpEncodedLeaTarget(address, instruction, length); else if (based && next.Known[getNativeRegisterKey(based[2])]) target = next.Known[getNativeRegisterKey(based[2])].Value + (based[4] ? (based[3] === "-" ? -1 : 1) * parseInt(based[4], 0) : 0); else if (native) target = parseInt(native[2], 0); else if (nativeOp && next.Known[targetRegister] && parseInt(nativeOp[3], 0) <= 0x7FFFFFFF) target = next.Known[targetRegister].Value + (nativeOp[1] === "SUB" ? -1 : 1) * parseInt(nativeOp[3], 0); else if (copy && next.Known[getNativeRegisterKey(copy[2])]) target = next.Known[getNativeRegisterKey(copy[2])].Value; if (copy && /^(?:E[A-Z]{2}|R(?:8|9|1[0-5])D)$/.test(copy[1]) && target !== undefined) target >>>= 0; if (isFinite(target) && target >= 0 && target <= 0x1FFFFFFFFFFFFF) next.Known[targetRegister] = { Value: target, Steps: 0, Kinds: 0 }; else delete next.Known[targetRegister]; if (targetRegister === "RSP") next.StackTarget = undefined; if (nativeOp) next.Flags = { PF: -1, SF: -1, ZF: -1, Steps: 0, Kinds: 0 }; } else if (exchange || store || push) { // Native PUSH decrements SP before writing; a proven slot at zero cannot become a return target. if (push && next.Known.RSP && next.Known.RSP.Value === (PE_Cached.isI386 ? 4 : 8)) continue; var exchangedRegister = getNativeRegisterKey((exchange || store || push)[1]), knownTarget = next.Known[exchangedRegister]; next.StackTarget = knownTarget ? knownTarget.Value : undefined; if (exchange) delete next.Known[exchangedRegister]; if (push) delete next.Known.RSP; } else if (pop) { var poppedKey = getNativeRegisterKey(pop[1]); if (next.StackTarget !== undefined) next.Known[poppedKey] = { Value: next.StackTarget, Steps: 0, Kinds: 0 }; else delete next.Known[poppedKey]; next.StackTarget = undefined; delete next.Known.RSP; } else if (stackOp) { var stackDelta = parseInt(stackOp[2], 0); // QWORD arithmetic sign-extends its immediate; dword stack targets wrap at 32 bits. if (PE_Cached.is64bit && (stackDelta < -0x80000000 || stackDelta > 0x7FFFFFFF)) next.StackTarget = undefined; else if (next.StackTarget !== undefined) next.StackTarget += (stackOp[1] === "ADD" ? 1 : -1) * stackDelta; if (PE_Cached.isI386 && next.StackTarget !== undefined) next.StackTarget >>>= 0; if (next.StackTarget < 0 || next.StackTarget > 0x1FFFFFFFFFFFFF || !isFinite(next.StackTarget)) next.StackTarget = undefined; next.Flags = { PF: -1, SF: -1, ZF: -1, Steps: 0, Kinds: 0 }; } else if (/^RET(?:N)?$/.test(opCode)) { if (isEpNativeNearTransfer(address, instruction, length) && next.StackTarget !== undefined && isVirtualAddressRangeFileBacked(next.StackTarget, 1, SECTION_FLAGS_EXECUTE)) { evidence.ReturnTarget = next.StackTarget; } continue; } else if (opCode === "CALL") { var indirectCall = instruction.match(/^CALL ([A-Z][A-Z0-9]*)$/i), indirectCallTarget = indirectCall && next.Known[getNativeRegisterKey(indirectCall[1])], callReturnProven = indirectCallTarget ? isEpBoundedDirectFlowProven(indirectCallTarget.Value, /^RET(?:N)?(?: (?:0X[0-9A-F]+|\d+))?$/i) : isEpDirectCallReturnProven(address); // A possible return is not evidence: unknown indirect calls must not expose unreachable fall-through. if (callReturnProven && isEpNativeNearTransfer(address, instruction, length)) { // The callee cannot return when native CALL first writes its return address to zero. if (next.Known.RSP && next.Known.RSP.Value === (PE_Cached.isI386 ? 4 : 8)) continue; next.Known = {}; next.Flags = { PF: -1, SF: -1, ZF: -1, Steps: 0, Kinds: 0 }; next.StackTarget = undefined; pending.push(next); } continue; } else if (branch) { var branchTarget = parseInt(branch[2], 0), outcome = -1; if (opCode === "JP" || opCode === "JPE") outcome = next.Flags.PF; else if (opCode === "JNP" || opCode === "JPO") outcome = next.Flags.PF === -1 ? -1 : 1 - next.Flags.PF; else if (opCode === "JS") outcome = next.Flags.SF; else if (opCode === "JNS") outcome = next.Flags.SF === -1 ? -1 : 1 - next.Flags.SF; else if (opCode === "JE" || opCode === "JZ") outcome = next.Flags.ZF; else if (opCode === "JNE" || opCode === "JNZ") outcome = next.Flags.ZF === -1 ? -1 : 1 - next.Flags.ZF; // Operand width affects only a taken transfer, not its proven untaken continuation. if (outcome !== 0 && !isEpNativeNearTransfer(address, instruction, length)) continue; // A Jcc to its continuation has identical successors; the separate degenerate-Jcc rule owns it. if (opCode !== "JMP" && branchTarget !== next.Address && outcome !== -1 && next.Flags.Steps >= 3 && (next.Flags.Kinds & 2 || next.Flags.Kinds & 4) && evidence.OpaqueBranchAddress === -1 && isVirtualAddressRangeFileBacked(branchTarget, 1, SECTION_FLAGS_EXECUTE)) { evidence.OpaqueBranchAddress = address; } if (opCode !== "JMP" && outcome !== 1) pending.push(next); if (outcome !== 0 && isVirtualAddressRangeFileBacked(branchTarget, 1, SECTION_FLAGS_EXECUTE)) { var taken = { Address: branchTarget, Known: next.Known, Flags: next.Flags, StackTarget: next.StackTarget, StackPointerZero: next.StackPointerZero }; pending.push(taken); } continue; } else if (/^(?:MOV|MOVABS|MOVSX|MOVSXD|MOVZX|LEA|CMOV[A-Z]+|POP)(?: |$)/.test(instruction)) { // These forms leave arithmetic flags untouched. A register destination // loses its old constant, while any unmodeled memory write loses the // proven top-of-stack target without inventing a replacement value. var writtenRegister = instruction.match(/^(?:MOV|MOVABS|MOVSX|MOVSXD|MOVZX|LEA|CMOV[A-Z]+|POP) ([A-Z][A-Z0-9]*)(?:,|$)/); if (writtenRegister) { var writtenRegisterName = writtenRegister[1], writtenRegisterKey = /^(?:[ABCD][HLX]|(?:SI|DI|BP|SP)L?|R(?:8|9|1[0-5])[BW])$/.test(writtenRegisterName) ? getEpPartialRegisterKey(writtenRegisterName) : getNativeRegisterKey(writtenRegisterName); delete next.Known[writtenRegisterKey]; } if (opCode === "POP" || !writtenRegister || writtenRegisterKey === "RSP") next.StackTarget = undefined; if (opCode === "POP") delete next.Known.RSP; } else if (/^(?:NOP|FNOP|PAUSE)$/.test(opCode)) { // Padding changes neither the proven pointer nor the modeled flags. } else if (/^(?:PUSH|PUSHF|PUSHFD|PUSHFQ|XCHG)$/.test(opCode)) { if (opCode === "XCHG") next.Known = {}; else delete next.Known.RSP; next.StackTarget = undefined; } else { next.Known = {}; next.Flags = { PF: -1, SF: -1, ZF: -1, Steps: 0, Kinds: 0 }; next.StackTarget = undefined; } // Unresolved jumps have no fall-through; bytes after them are not a reachable path. if (!/^(?:HLT|INT|INT1|INT3|INTO|IRET|IRETD|IRETQ|JMP|JMPF|LOOP|LOOPE|LOOPNE|LOOPNZ|LOOPZ|RETF|RETN|SYSCALL|SYSENTER|SYSRET|SYSEXIT|UD[012])$/.test(opCode)) pending.push(next); } return evidence; } /** * Stops a linear EP rule at a terminal transfer or an unproven CALL fall-through. * * @param {string} opCode - Current instruction mnemonic. * @param {number} address - Virtual address of the current instruction. * @param {string[]} stopOpCodes - Terminal mnemonics for the calling rule. * @returns {boolean} Whether subsequent physical bytes lack proven reachability. */ function isEpLinearScanStopped(opCode, address, stopOpCodes) { return stopOpCodes.indexOf(opCode) !== -1 && (opCode !== "CALL" || !isEpDirectCallReturnProven(address)); } /** * Propagates only directly proven x86 zero-flag facts through a bounded EP walk. * Self-XOR, self-SUB and self-CMP set ZF; instructions known not to alter flags * preserve the incoming state. Every other instruction makes it unknown, so an * uncertain conditional branch retains both successors instead of being guessed. * * @param {string} instruction - Current decoded instruction. * @param {string} opCode - Current instruction mnemonic. * @param {number} zeroFlag - Incoming ZF: 1 set, 0 clear, -1 unknown. * @returns {number} Outgoing ZF in the same three-valued domain. */ function advanceEpZeroFlag(instruction, opCode, zeroFlag) { if (/^(?:XOR|SUB|CMP) ([A-Z][A-Z0-9]*), \1$/i.test(instruction)) return 1; // Rotates update CF/OF, but preserve ZF; NOT and BSWAP do not change flags. if (/^(?:MOV(?:ABS|SX|SXD|ZX)?|LEA|PUSH|POP|NOP|FNOP|PAUSE|XCHG|NOT|BSWAP|ROL|ROR|RCL|RCR|ENDBR32|ENDBR64|CLC|STC|CMC|J[A-Z]+|LOOP(?:E|NE|NZ|Z)?)$/.test(opCode)) return zeroFlag; return -1; } /** * Propagates only directly proven x86 carry-flag facts through an EP path. * CLC/STC set CF explicitly, CMC inverts a known value, and self-XOR/SUB/CMP * clear it. INC/DEC and ordinary data moves preserve CF; all other effects * are treated as unknown rather than guessed. * * @param {string} instruction - Current decoded instruction. * @param {string} opCode - Current instruction mnemonic. * @param {number} carryFlag - Incoming CF: 1 set, 0 clear, -1 unknown. * @returns {number} Outgoing CF in the same three-valued domain. */ function advanceEpCarryFlag(instruction, opCode, carryFlag) { if (opCode === "CLC") return 0; if (opCode === "STC") return 1; if (opCode === "CMC") return carryFlag === -1 ? -1 : 1 - carryFlag; if (/^(?:XOR|SUB|CMP) ([A-Z][A-Z0-9]*), \1$/i.test(instruction)) return 0; if (/^(?:MOV(?:ABS|SX|SXD|ZX)?|LEA|PUSH|POP|NOP|FNOP|PAUSE|XCHG|NOT|BSWAP|ENDBR32|ENDBR64|INC|DEC|J[A-Z]+|LOOP(?:E|NE|NZ|Z)?)$/.test(opCode)) return carryFlag; return -1; } /** * Resolves only Jcc outcomes implied by known ZF/CF values. * Signed comparisons needing SF/OF and branches depending on register counts * remain unknown. A partially known JA/JBE can still be settled when either * flag alone determines the result. * * @param {string} opCode - Current branch mnemonic. * @param {number} zeroFlag - ZF: 1 set, 0 clear, -1 unknown. * @param {number} carryFlag - CF: 1 set, 0 clear, -1 unknown. * @returns {number} Branch result: 1 taken, 0 not taken, -1 unknown. */ function getEpKnownBranchResult(opCode, zeroFlag, carryFlag) { if (/^(?:JE|JZ)$/.test(opCode)) return zeroFlag; if (/^(?:JNE|JNZ)$/.test(opCode)) return zeroFlag === -1 ? -1 : 1 - zeroFlag; if (/^(?:JB|JC|JNAE)$/.test(opCode)) return carryFlag; if (/^(?:JAE|JNB|JNC)$/.test(opCode)) return carryFlag === -1 ? -1 : 1 - carryFlag; if (/^(?:JA|JNBE)$/.test(opCode)) return zeroFlag === 1 || carryFlag === 1 ? 0 : zeroFlag === 0 && carryFlag === 0 ? 1 : -1; if (/^(?:JBE|JNA)$/.test(opCode)) return zeroFlag === 1 || carryFlag === 1 ? 1 : zeroFlag === 0 && carryFlag === 0 ? 0 : -1; if (/^(?:JG|JNLE)$/.test(opCode) && zeroFlag === 1) return 0; if (/^(?:JLE|JNG)$/.test(opCode) && zeroFlag === 1) return 1; if (/^(?:LOOPE|LOOPZ)$/.test(opCode) && zeroFlag === 0) return 0; if (/^(?:LOOPNE|LOOPNZ)$/.test(opCode) && zeroFlag === 1) return 0; return -1; } /** * Rejects mandatory null-pointer accesses and integer division by a proven zero in user-mode x86 code. * Only exact register operands, flat null addresses and implicit stack operations are covered; unknown * values, indexed addresses, segment bases and hint-only instructions remain unproven. * * @param {string} instruction - Current decoded instruction. * @param {string} opCode - Current instruction mnemonic. * @param {boolean} isStackPointerZero - Whether the incoming native SP is proven zero. * @param {Object} [knownAddresses] - Optional canonical-register map of numbers or existing Value records. * @returns {boolean} Whether the instruction cannot have an ordinary fall-through. */ function isEpProvenFaultingInstruction(instruction, opCode, isStackPointerZero, knownAddresses) { if (!PE_Cached.isArchX86 || PE_Cached.isDriver) return false; if (!isStackPointerZero && !knownAddresses && !/\[(?:0X0+|0+)\]/i.test(instruction)) return false; var knownStackPointer = knownAddresses && knownAddresses.RSP; isStackPointerZero = isStackPointerZero || knownStackPointer === 0 || Boolean(knownStackPointer && knownStackPointer.Value === 0); if (isStackPointerZero && /^(?:CALL|PUSH(?:A[LD]?|F[DQ]?)?|POP(?:A[LD]?|F[DQ]?)?|RET(?:F|N)?|ENTER)$/.test(opCode)) return true; var divisor = (opCode === "DIV" || opCode === "IDIV") && instruction.match(/^(?:DIV|IDIV) ((?:R|E)?(?:AX|BX|CX|DX|SI|DI|BP|SP)|[ABCD][HL]|(?:SI|DI|BP|SP)L|R(?:8|9|1[0-5])[BDW]?)$/i); if (divisor) { var divisorRegister = divisor[1], divisorKey = /^(?:[ABCD][HLX]|(?:SI|DI|BP|SP)L?|R(?:8|9|1[0-5])[BW])$/.test(divisorRegister) ? getEpPartialRegisterKey(divisorRegister) : getNativeRegisterKey(divisorRegister), knownDivisor = knownAddresses && knownAddresses[divisorKey], divisorValue = typeof knownDivisor === "number" ? knownDivisor : knownDivisor && knownDivisor.Value; if (divisorKey === "RSP" && isStackPointerZero) return true; if (typeof divisorValue !== "number" || !isFinite(divisorValue) || divisorValue < 0 || divisorValue > 0x1FFFFFFFFFFFFF || divisorValue % 1 !== 0) return false; // DIV/IDIV always raise #DE for zero; partial operands use only their encoded alias bits. if (/^[ABCD]H$/.test(divisorRegister)) divisorValue = (divisorValue >>> 8) & 0xFF; else if (/^(?:[ABCD]L|(?:SI|DI|BP|SP)L|R(?:8|9|1[0-5])B)$/.test(divisorRegister)) divisorValue &= 0xFF; else if (/^(?:[ABCD]X|SI|DI|BP|SP|R(?:8|9|1[0-5])W)$/.test(divisorRegister)) divisorValue &= 0xFFFF; else if (/^(?:E[A-Z]+|R(?:8|9|1[0-5])D)$/.test(divisorRegister)) divisorValue >>>= 0; return divisorValue === 0; } // LOCK/HLE change synchronization, not the mandatory access through the same memory operand. var memoryOpCode = opCode; if (/^(?:LOCK|XACQUIRE|XRELEASE)$/.test(opCode)) { var memoryInstruction = instruction.match(/^(?:(?:XACQUIRE|XRELEASE) )?(?:LOCK )?([A-Z][A-Z0-9]*) /i); if (!memoryInstruction) return false; memoryOpCode = memoryInstruction[1]; } // CMPXCHG8B/16B read the memory operand even when the comparison fails; they cannot bypass a null access. // Intel SDM, CMPXCHG8B/CMPXCHG16B: https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-2a-manual.pdf#page=285 if (!/^(?:MOV(?:ABS|SX|SXD|ZX)?|XCHG|XADD|CMPXCHG(?:8B|16B)?|ADD|SUB|AND|OR|XOR|INC|DEC|NEG|NOT|CMP|TEST|CMOV[A-Z]+|SET[A-Z]+|PUSH|POP|DIV|IDIV)$/.test(memoryOpCode)) return false; // An absolute flat address of zero faults too; FS/GS bases and hint-only memory operands remain unknown. if (/\b(?:BYTE|WORD|DWORD|QWORD|XMMWORD) PTR (?:(?:CS|DS|ES|SS):)?\[(?:0X0+|0+)\]/i.test(instruction)) return true; var memoryBase = instruction.match(PE_Cached.isI386 ? /\b(?:BYTE|WORD|DWORD|QWORD|XMMWORD) PTR (?:(?:CS|DS|ES|SS):)?\[(E(?:AX|BX|CX|DX|SI|DI|BP|SP))\]/i : /\b(?:BYTE|WORD|DWORD|QWORD|XMMWORD) PTR (?:(?:CS|DS|ES|SS):)?\[((?:R|E)(?:AX|BX|CX|DX|SI|DI|BP|SP)|R(?:8|9|1[0-5])D?)\]/i); if (!memoryBase) return false; var baseRegister = getNativeRegisterKey(memoryBase[1]), knownValue = knownAddresses && knownAddresses[baseRegister], addressValue = typeof knownValue === "number" ? knownValue : knownValue && knownValue.Value; if (typeof addressValue !== "number" || !isFinite(addressValue) || addressValue < 0 || addressValue > 0x1FFFFFFFFFFFFF || addressValue % 1 !== 0) { addressValue = undefined; } else if (PE_Cached.is64bit && /^(?:E[A-Z]+|R(?:8|9|1[0-5])D)$/i.test(memoryBase[1])) { // An AMD64 address-size override uses the low dword even when the full register is nonzero. addressValue >>>= 0; } return (baseRegister === "RSP" && isStackPointerZero) || addressValue === 0; } /** * Tracks only a directly proven zero native stack pointer in user-mode x86 code. * Unrelated instructions preserve this fact; unproven replacement or partial writes * discard it. Native/dword replacements may reuse existing constants when provided. * A zero pointer alone is not a fault until a mandatory memory or stack access occurs. * * @param {string} instruction - Current decoded instruction. * @param {string} opCode - Current instruction mnemonic. * @param {boolean} isZero - Whether the incoming native stack pointer is proven zero. * @param {Object} [knownAddresses] - Optional canonical-register map of numbers or existing Value records. * @returns {boolean} Whether the outgoing native stack pointer is proven zero. */ function advanceEpStackPointerZero(instruction, opCode, isZero, knownAddresses) { if (!PE_Cached.isArchX86 || PE_Cached.isDriver) return false; if (knownAddresses) { var replacement = instruction.match(/^(?:MOV|XCHG) (ESP|RSP), ([A-Z][A-Z0-9]*)$/i), exchange = !replacement && instruction.match(/^XCHG ([A-Z][A-Z0-9]*), (ESP|RSP)$/i), sourceRegister = replacement ? replacement[2] : exchange && exchange[1], stackRegister = replacement ? replacement[1] : exchange && exchange[2]; // A dword SP write zero-extends in AMD64; only that width may discard a known upper half. if (sourceRegister && (stackRegister === "ESP" ? /^(?:E(?:AX|BX|CX|DX|SI|DI|BP|SP)|R(?:8|9|1[0-5])D)$/.test(sourceRegister) : PE_Cached.is64bit && /^R(?:AX|BX|CX|DX|SI|DI|BP|SP|8|9|1[0-5])$/.test(sourceRegister))) { var source = knownAddresses[getNativeRegisterKey(sourceRegister)], sourceValue = typeof source === "number" ? source : source && source.Value; if (sourceValue !== undefined && isFinite(sourceValue)) return (stackRegister === "ESP" ? sourceValue >>> 0 : sourceValue) === 0; } } var zero = instruction.match(/^(?:MOV(?:ABS)?|AND) (ESP|RSP), (?:0X0+|0+)$/i) || instruction.match(/^(?:XOR|SUB) (ESP|RSP), \1$/i); if (zero && (zero[1] === "ESP" || PE_Cached.is64bit)) return true; // Aliases retain a whole-pointer zero only when their operation cannot introduce any set bit. if (isZero && /^MOV(?:SX|SXD|ZX) (?:RSP|ESP|SP), (?:ESP|SP|SPL)$/i.test(instruction)) return true; if (isZero && /^(?:(?:MOV|XCHG|XOR|SUB) ((?:R|E)?SP|SPL), \1|LEA ((?:R|E)?SP), \[\2\]|NEG (?:RSP|ESP|SP|SPL)|BSWAP (?:RSP|ESP))$/i.test(instruction)) return true; if (isZero && /^(?:AND|SHL|SHR|SAR|ROL|ROR) (?:RSP|ESP|SP|SPL), .+$/i.test(instruction)) return true; if (isZero && /^(?:MOV|ADD|SUB|OR|XOR|RCL|RCR) (?:RSP|ESP|SP|SPL), (?:0X0+|0+)$/i.test(instruction)) return true; if (/^(?:CALLF?|PUSH(?:A[LD]?|F[DQ]?)?|POP(?:A[LD]?|F[DQ]?)?|RET(?:F|N)?|ENTER|LEAVE)$/.test(opCode)) return false; if (/^(?:CMP|TEST|BT|MUL|DIV|IDIV)$/.test(opCode) || (opCode === "IMUL" && instruction.indexOf(",") === -1)) return isZero; if (/^[A-Z][A-Z0-9]* (?:RSP|ESP|SP|SPL)(?:,|$)/i.test(instruction) || /^(?:XCHG|XADD) .*, (?:RSP|ESP|SP|SPL)$/i.test(instruction)) return false; return isZero; } /** * Rejects a physical EP fall-through skipped by a branch or a proven mandatory fault. * The caller owns a separate flag state for each physical instruction sweep. * The EP cache and the manual x87 sweep supply consecutive decoded instructions; * only branches need another engine length lookup. An unknown branch ends the * sweep rather than allowing facts from an unrelated path to cross it. * * @param {string} instruction - Current decoded instruction. * @param {number} address - Virtual address of the current instruction. * @param {Object} state - Caller-owned flag and zero-stack-pointer facts for one sweep. * @param {boolean} [isPathContinuation] - Preserve facts when a validated CFG edge revisits a lower address. * @returns {boolean} Whether subsequent physical bytes lack proven reachability. */ function isEpLinearFallthroughUnproven(instruction, address, state, isPathContinuation) { if (!isFinite(address)) return true; if (state.PreviousAddress === undefined || (!isPathContinuation && address <= state.PreviousAddress)) { state.ZeroFlag = -1; state.CarryFlag = -1; state.StackPointerZero = false; state.PreviousInstruction = String(); } var opCode = getAsmOpCode(instruction), branch = instruction && instruction.match(/^(J(?!MP)[A-Z]+|LOOP(?:E|NE|NZ|Z)?) (0X[0-9A-F]+|\d+)$/i); // AAM with a zero base raises #DE before any ordinary fall-through, independently of AL. // Intel SDM, AAM "Protected Mode Exceptions": https://cdrdv2-public.intel.com/858446/253666-088-sdm-vol-2a.pdf if (/^AAM (?:0X0+|0+)$/i.test(instruction)) return true; if (isEpProvenFaultingInstruction(instruction, opCode, state.StackPointerZero)) return true; state.StackPointerZero = advanceEpStackPointerZero(instruction, opCode, state.StackPointerZero); if (branch) { var length = PE.getDisasmLength(address); if (!isFinite(length) || length < 1 || length > 15 || length % 1 !== 0) return true; var isKnownUntaken = getEpKnownBranchResult(opCode, state.ZeroFlag, state.CarryFlag) === 0 || (/^(?:XOR|SUB|CMP) ([A-Z][A-Z0-9]*), \1$/i.test(state.PreviousInstruction) && /^(?:JA|JNBE|JB|JNAE|JC|JG|JNLE|JL|JNGE|JNE|JNZ|JO|JNP|JPO|JS)$/.test(opCode)); // A word-sized taken branch truncates EIP even when the decoder prints the physical next address. if (!isKnownUntaken && !isEpNativeNearTransfer(address, instruction, length)) return true; if (parseInt(branch[2], 0) !== address + length && !isKnownUntaken) return true; } state.ZeroFlag = advanceEpZeroFlag(instruction, opCode, state.ZeroFlag); state.CarryFlag = advanceEpCarryFlag(instruction, opCode, state.CarryFlag); state.PreviousInstruction = instruction; state.PreviousAddress = address; return false; } /** * Retrieves the assembly instruction at the specified zero-based index from the PE entry point. * * @param {number} index - The index of the assembly instruction to retrieve. * @returns {string|undefined} The disassembled instruction, or undefined when unavailable. * @sideEffects Caches retrieved instructions and addresses in the global EP arrays. * @example * emulateAndGetAsmInstructionByIndex(0); // "PUSH EBP" */ function emulateAndGetAsmInstructionByIndex(index) { // Resource-only and entryless DLLs do not have initialization code to emulate. The // centralized guard also protects future EP rules from decoding ImageBase by accident. if (!PE_Cached.isEntryPointAnalysisAllowed) return undefined; // Check if the instruction is already cached if (epAsmInstructions[index] !== undefined) { return epAsmInstructions[index] || undefined; } var firstUncachedIndex = epAsmInstructions.length, disasmAddress = PE_Cached.addressOfUnmanagedEntryPoint; // A failed decode terminates the physical instruction stream. Remembering that boundary // prevents every later request from retrying the same invalid address. if (firstUncachedIndex !== 0) { if (!epAsmInstructions[firstUncachedIndex - 1]) return undefined; disasmAddress = PE.getDisasmNextAddress(epAsmInstructionAddresses[firstUncachedIndex - 1]); } // Extend the cache only from its current tail. Sequential consumers therefore walk each // instruction boundary once instead of restarting from EP for every requested index. for (var i = firstUncachedIndex; i <= index; i++) { if (i !== firstUncachedIndex) disasmAddress = PE.getDisasmNextAddress(disasmAddress); // A failed or non-advancing decode must not seed later rules with the same // instruction, an unmapped VA, or a synthetic address returned by the engine. if (!isFinite(disasmAddress) || disasmAddress < PE_Cached.imageBase || (i > 0 && disasmAddress <= epAsmInstructionAddresses[i - 1]) || PE.VAToOffset(disasmAddress) === -1) { epAsmInstructionAddresses[i] = disasmAddress; epAsmInstructions[i] = String(); break; } epAsmInstructionAddresses[i] = disasmAddress; epAsmInstructions[i] = PE.getDisasmString(disasmAddress); if (!epAsmInstructions[i]) break; // Safety check } return epAsmInstructions[index] || undefined; } /** * Generates a subpattern string to search for instructions within a pattern divided by a separator. * * This function allows you to create a subpattern that can be used to search for specific instructions * within a larger pattern string that is divided by a separator (_patternSplitter). * * Examples: * - "|OPCODE1|OPCODE2|OPCODE3|".indexOf("|OPCODE2|") * - "|OPCODE1|OPCODE2|OPCODE3|".indexOf(getInstructionsAsmPattern("OPCODE2")) * - "|OPCODE1|OPCODE2|OPCODE3|".indexOf("|OPCODE2|OPCODE3|") * - "|OPCODE1|OPCODE2|OPCODE3|".indexOf(getInstructionsAsmPattern(["OPCODE2", "OPCODE3"])) * * @param {string|string[]} instruction - The instruction or array of instructions to create the subpattern for. * @returns {string} The generated subpattern string. */ function getInstructionsAsmPattern(instruction) { return _patternSplitter + ( Array.isArray(instruction) ? instruction.join(_patternSplitter) : instruction ) + _patternSplitter; } /** * Retrieves the first disassembled instruction at the PE entry point. * * @returns {string|undefined} The instruction text, or undefined if it cannot be disassembled. * @example * getFirstEpAsmInstruction(); // "JMP 0x401000" */ function getFirstEpAsmInstruction() { return emulateAndGetAsmInstructionByIndex(0); // return PE.getDisasmString(PE.OffsetToVA(PE.getEntryPointOffset())); } /** * Extracts the leading opcode token and normalizes legacy far-transfer spellings. * LCALL/LJMP/LRET must use existing CALL/JMP/RETF stop paths; the original instruction * still controls near-transfer and direct-target proofs, so no far return is assumed. * * @param {string} instruction - The assembly instruction. * @returns {string|undefined} The normalized opcode, the original one-token instruction, or a falsy input. * @example * getAsmOpCode("MOV EAX, 4"); // "MOV" */ function getAsmOpCode(instruction) { if (!instruction) return instruction; const separatorIndex = instruction.indexOf(" "), opCode = separatorIndex === -1 ? instruction : instruction.substring(0, separatorIndex); if (opCode === "LCALL") return "CALL"; if (opCode === "LJMP") return "JMP"; if (opCode === "LRET") return "RETF"; return opCode; } /** * Canonicalizes a decoded x86/x64 register for physical-register provenance maps. * Expects uppercase operands from DiE disassembly. E-prefixed 32-bit registers and * D-suffixed x64 registers share a key with their 64-bit aliases; byte/word * writes use a separate width-aware mapping and must not pass through this helper. * * @param {string} registerName - Uppercase decoded register operand. * @returns {string} Physical register key with the E prefix and D suffix normalized. * @example * getNativeRegisterKey("EAX"); // "RAX" * getNativeRegisterKey("R8D"); // "R8" */ function getNativeRegisterKey(registerName) { var register = registerName; if (register[0] === "E") register = "R" + register.substring(1); return register[register.length - 1] === "D" ? register.substring(0, register.length - 1) : register; } /** * Maps a decoded byte or word register write to the physical register it invalidates. * Unlike getNativeRegisterKey, this accepts partial aliases such as AH, SIL and R8B; * it must not be used to preserve a value across a width-changing write. * * @param {string} register - Uppercase decoded partial-register destination. * @returns {string} Physical register key whose existing proof must be discarded. * @example * getEpPartialRegisterKey("AH"); // "RAX" * getEpPartialRegisterKey("R8B"); // "R8" */ function getEpPartialRegisterKey(register) { if (/^R(?:8|9|1[0-5])[BW]$/.test(register)) return register.substring(0, register.length - 1); if (/^[ABCD][HLX]$/.test(register)) return "R" + register[0] + "X"; return "R" + register.replace(/L$/, String()); } /** * Retrieves the first assembly operation code from the entry point of the executable. * * @returns {string} The assembly operation code of the first instruction at the entry point. */ function getFirstEpAsmOpCode() { return getAsmOpCode(PE_Cached.firstEpAsmInstruction); } /** * Checks if an item pattern appears to be a mangled symbol name. * * @param {string} itemPattern - The pattern string to check for mangling * @returns {boolean} True if the pattern appears to be a mangled symbol, false otherwise */ function isItemMangled(itemPattern) { // Check if the pattern is too short to be mangled if (itemPattern.length <= 5) { return false; } return ( (itemPattern[0] === '?' && (itemPattern.indexOf("@@") !== -1 || itemPattern[1] === '?')) || // MSVC mangling: starts with '?' and contains "@@" or has double '?' (itemPattern.substring(0, 2) === "_Z") || // GNU mangling: starts with "_Z" (itemPattern[0] === '@' && itemPattern.indexOf('$') !== -1) || // Borland C++ mangling: starts with '@' and contains '$' (itemPattern.substring(0, 2).toLowerCase() === '$s') // Swift mangling: starts with '$s' or '$S' ); } /** * Returns information about the compiler and language based on a mangled symbol pattern. * * @param {string} itemPattern - The mangled symbol pattern to analyze. * @returns {{compiler: string, language: string} | undefined} An object containing the compiler and language if recognized, otherwise undefined. */ function getManglerInfoByMangledItem(itemPattern) { if (!isItemMangled(itemPattern)) { return undefined; } // Visual C++ (MSVC) if (itemPattern[0] === '?') { return { compiler: "Microsoft Visual C++", language: "C++" }; } // MinGW (GNU) if (itemPattern.substring(0, 2) === "_Z") { return { compiler: "MinGW", language: "C++" }; } // Borland C++ if (itemPattern[0] === '@' && itemPattern.indexOf('$') !== -1) { return { compiler: "Borland C++", language: "C++" }; } // Swift if (itemPattern.substring(0, 2).toLowerCase() === '$s') { return { compiler: "Swift", language: "Swift" }; } return undefined; } /** * Extracts the unmangled name from a mangled C++ symbol. * * This function handles both Microsoft Visual C++ (MSVC) and GNU C++ (GCC) name mangling schemes: * - MSVC mangled names start with '?' and include patterns for functions, constructors, destructors, operators, and templates * - GCC mangled names start with '_Z' and include patterns for STL functions, namespaced functions, and regular functions * - Swift mangled names start with '$s' or '$S' and include patterns for types, functions, properties, and protocols * * @param {string} itemPattern - The mangled symbol name to decode * @returns {string} The unmangled function/symbol name, or the original pattern if it's not mangled or cannot be decoded * * @example * // MSVC examples * getNameOfMangledItem('?myFunction@@YAXXZ') // returns 'myFunction' * getNameOfMangledItem('??0MyClass@@QAE@XZ') // returns 'MyClass' (constructor) * getNameOfMangledItem('??$myTemplate@H@@YAXXZ') // returns 'myTemplate' * * @example * // GCC examples * getNameOfMangledItem('_Z9myFunctionv') // returns 'myFunction' * getNameOfMangledItem('_ZSt4cout') // returns 'cout' * getNameOfMangledItem('_ZN3std4cout') // returns 'cout' * * @example * // Borland examples * getNameOfMangledItem('@func$qi') // returns 'func' * getNameOfMangledItem('@Class@method$qpc') // returns 'method' * getNameOfMangledItem('@Class@$bctr$qi') // returns 'Class' (constructor) * * @example * // Swift examples * getNameOfMangledItem('$s11MaskStorages4SIMDPTl') // returns 'SIMD' * getNameOfMangledItem('$ss9_IndexBoxC6_unboxqd__SgySLRd__lF') // returns '_unbox' * getNameOfMangledItem('$sSn11descriptionSSvpMV') // returns 'description' */ function getNameOfMangledItem(itemPattern) { if (!isItemMangled(itemPattern)) { return itemPattern; } // MSVCPP mangled names start with '?' if (itemPattern[0] === '?') { // Regular function: ?myFunction@@YAXXZ if (itemPattern[1] !== '?') { return itemPattern.split("?")[1].split("@")[0]; } // Handle all ?? patterns if (itemPattern[1] === '?') { var parts, nameWithPrefix; // Simple template: ??$myTemplate@H@@YAXXZ if (itemPattern[2] === '$') { return itemPattern.split("$")[1].split("@")[0]; } // Complex template: ??0?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QAE@XZ if (itemPattern.indexOf('?$') !== -1) { var complexMatch = itemPattern.match(/\?\?\d\?\$([^@]+)@/); if (complexMatch) { return complexMatch[1]; } } // Special mangler: ??_7MyClass@@6B@, ??_EMyClass@@QAEPAXI@Z if (itemPattern[2] === '_') { parts = itemPattern.split("?"); if (parts.length >= 3) { nameWithPrefix = parts[2].split("@")[0]; // For _E, _G, _R patterns (destructor, vector destructor, RTTI), strip the prefix if (nameWithPrefix.length > 1 && /[EGR]/.test(nameWithPrefix[1])) { return nameWithPrefix.substring(2); } // For _7 and other patterns, remove underscore and digit var nameAfterUnderscore = nameWithPrefix.substring(2); return nameAfterUnderscore.indexOf('_') === 0 ? nameAfterUnderscore.substring(1) : nameAfterUnderscore; } } // Constructor/destructor/operators: ??0MyClass@@QAE@XZ, ??1MyClass@@QAE@XZ, ??2@YAPAXI@Z if (/[0-9A-D]/.test(itemPattern[2])) { parts = itemPattern.split("?"); if (parts.length >= 3) { nameWithPrefix = parts[2].split("@")[0]; // Global operator (single character) if (nameWithPrefix.length === 1 && /[0-9A-D]/.test(nameWithPrefix[0])) { var operatorNames = { '2': 'operator_new', '3': 'operator_delete', '4': 'operator_assign', '5': 'operator_shift_left', '6': 'operator_shift_right', '8': 'operator_equal', '9': 'operator_not_equal', 'A': 'operator_subscript', 'B': 'operator_not', 'C': 'operator_cast', 'D': 'operator_cast_const' }; return operatorNames[nameWithPrefix[0]] || 'operator_unknown'; } // Class functions - remove first character return nameWithPrefix.substring(1); } } // Fallback parts = itemPattern.split("?"); if (parts.length >= 3) { var name = parts[2].split("@")[0]; return (name.length > 0 && /[0-9A-D]/.test(name[0])) ? name.substring(1) : name; } } } // GNUCPP mangled names start with '_Z' if (itemPattern.substring(0, 2) === "_Z") { // STL mangler: _ZSt4cout, _ZSt3minIiET_RKS0_S2_ if (itemPattern[2] === 'S' && itemPattern[3] === 't') { var stlMatch = itemPattern.match(/_ZSt(\d+)(.+)/); if (stlMatch) { return stlMatch[2].substring(0, parseInt(stlMatch[1], 10)); } } else if (itemPattern[2] === 'N') { // Namespace mangler: _ZN...E var components = [], remaining = itemPattern.substring(3); // Skip "_ZN" // Parse all components while (remaining.length > 0 && remaining[0] !== 'E' && /^\d/.test(remaining)) { var lengthMatch = remaining.match(/^(\d+)/); if (lengthMatch) { var length = parseInt(lengthMatch[1], 10), nameStart = lengthMatch[1].length; if (remaining.length >= nameStart + length) { var name = remaining.substring(nameStart, nameStart + length); components.push(name); remaining = remaining.substring(nameStart + length); } else { break; } } else { break; } } // Return the last component (function name) if (components.length > 0) { return components[components.length - 1]; } } else { // Regular mangler: _Z9myFunctionv, _Z3fooi var regularMatch = itemPattern.match(/_Z(\d+)(.+)/); if (regularMatch) { var funcNameLength = parseInt(regularMatch[1], 10); return regularMatch[2].substring(0, funcNameLength); } } } // Borland C++ mangled names start with '@' if (itemPattern[0] === '@') { var parts = itemPattern.split('@'); // Simple function: @func$qi if (parts.length === 2) { return parts[1].split('$')[0]; } // Class method: @Class@method$qpc if (parts.length >= 3) { // Strip leading '$' before splitting (e.g. "$bctr$qi" → "bctr$qi") var methodName = parts[2].replace(/^\$/, '').split('$')[0]; // Constructor: @Class@$bctr$... if (methodName === 'bctr') { return parts[1]; // Return class name } // Destructor: @Class@$bdtr$... if (methodName === 'bdtr') { return '~' + parts[1]; // Return destructor name } // Regular method return methodName; } } // Swift mangled names start with '$s' or '$S' if (itemPattern.substring(0, 2).toLowerCase() === '$s') { var pos = 2, identifiers = []; // Parse length-prefixed identifier function parseIdentifier(str, startPos) { if (startPos >= str.length || !/^\d/.test(str[startPos])) { return null; } var match = str.substring(startPos).match(/^(\d+)/); if (!match) return null; var len = parseInt(match[1], 10), digitLen = match[1].length; if (startPos + digitLen + len > str.length) { return null; } return { name: str.substring(startPos + digitLen, startPos + digitLen + len), nextPos: startPos + digitLen + len }; } // Parse ALL length-prefixed identifiers, skipping markers/types/substitutions // We want to collect ALL readable names and return the last one var loopLimit = 50; // Safety limit to prevent infinite loops while (pos < itemPattern.length && loopLimit-- > 0) { var char = itemPattern[pos]; // Try to parse length-prefixed identifier if (/^\d/.test(char)) { var result = parseIdentifier(itemPattern, pos); if (result) { identifiers.push(result.name); pos = result.nextPos; continue; } } // Skip substitution references (x, q, y, z and combinations like qd__, yz, etc.) if (/^[xqyz]/.test(char)) { pos++; // Skip additional substitution characters while (pos < itemPattern.length && /^[xqyzd_0-9]/.test(itemPattern[pos])) { pos++; } continue; } // Skip single-letter markers if followed by digit or known pattern if (/^[a-zA-Z]$/.test(char)) { var nextChar = pos + 1 < itemPattern.length ? itemPattern[pos + 1] : String(); // Check for stdlib type codes (2 letters starting with S) if (char === 'S' && /^[a-zA-Z]/.test(nextChar)) { var afterStdlib = pos + 2 < itemPattern.length ? itemPattern[pos + 2] : String(); // If after stdlib type there's a marker + digit, skip stdlib and continue if (/^[a-z]$/.test(afterStdlib)) { var afterMarker = pos + 3 < itemPattern.length ? itemPattern[pos + 3] : String(); if (/^\d/.test(afterMarker)) { pos += 3; // Skip Sx + marker continue; } } // If directly followed by digit, skip stdlib type if (/^\d/.test(afterStdlib)) { pos += 2; continue; } } // Single letter followed by digit - skip it if (/^\d/.test(nextChar)) { pos++; continue; } } // Can't parse further - stop break; } // Return the last identifier (most specific name) if (identifiers.length > 0) { return identifiers[identifiers.length - 1]; } // Fallback: check for stdlib type codes at current position if (pos < itemPattern.length && itemPattern[pos] === 'S' && pos + 1 < itemPattern.length) { var typeCode = itemPattern.substring(pos, pos + 2); var stdlibTypes = { 'Sa': 'Array', 'Sb': 'Bool', 'Sc': 'UnicodeScalar', 'Sd': 'Double', 'Sf': 'Float', 'Sg': 'Optional', 'Sh': 'Set', 'Si': 'Int', 'SL': 'Collection', 'Sl': 'Slice', 'Sm': 'Mirror', 'Sn': 'Numeric', 'So': 'ObjectiveC', 'Sp': 'UnsafePointer', 'Sq': 'Optional', 'SR': 'Sequence', 'Sr': 'UnsafeRawPointer', 'Ss': 'String', 'SS': 'String', 'St': 'Range', 'ST': 'Equatable', 'Su': 'UInt', 'Sv': 'UnsafeRawBufferPointer', 'Sw': 'UnsafeBufferPointer', 'Sx': 'Strideable', 'SY': 'RawRepresentable', 'Sy': 'RawRepresentable', 'Sz': 'BinaryInteger', 'SD': 'Dictionary', 'SE': 'Encodable', 'SH': 'Hashable', 'Sk': 'KeyPath', 'SK': 'KeyPath' }; if (stdlibTypes[typeCode]) { return stdlibTypes[typeCode]; } } } return itemPattern; } /** * Matches import-position hashes against a signature database collection. * * Each entry stores the import position at index 2 and either one hash or an * array of acceptable hashes at index 3. The first matching entry is returned. * * @param {Array} dbCollection - The database collection to validate, where each entry is an array containing at least a position and hash. * @returns {Array|null} The first entry with a matching import position hash, or null if none found. * @example * validateImportHashes([["UPX", null, 0, 0xD4FDCAB1]]); */ function validateImportHashes(dbCollection) { for (var i = 0; i < dbCollection.length; i++) { var position = dbCollection[i][2], hash = dbCollection[i][3]; if (Array.isArray(hash)) { for (var j = 0; j < hash.length; j++) { if (PE.isImportPositionHashPresent(position, hash[j])) return dbCollection[i]; } } else if (PE.isImportPositionHashPresent(position, hash)) { return dbCollection[i]; } } return null; } /** * Matches PE section names against string or regular-expression signatures in a database collection. * * An entry whose name at index 0 is null acts as an exclusion and immediately * cancels the match. Entries with more than three fields are reported as malformed. * * @param {Array} dbCollection - The database collection to validate, where each item is expected to have a section signature at index 2. * @returns {Array|null} The first matching item from the collection, or null if no match is found. * @example * validateSectionNames([["UPX", null, "UPX0"]]); */ function validateSectionNames(dbCollection) { var result = null; for (var i = 0; i < dbCollection.length; i++) { if (dbCollection[i].length > 3) { stdout(">>> Invalid section signature entry for \"" + dbCollection[i][0] + "\": expected 3 elements, but found " + dbCollection[i].length + ". Extra signatures will be ignored. Use [ and ] to define a signature collection. <<<"); } var sectionSignature = dbCollection[i][2]; if (Array.isArray(sectionSignature)) { for (var j = 0; j < sectionSignature.length; j++) { if ((typeof sectionSignature[j] === "string" && PE.isSectionNamePresent(sectionSignature[j])) || (sectionSignature[j] instanceof RegExp && PE.isSectionNamePresentExp(sectionSignature[j]))) { if (dbCollection[i][0] === null) return null; if (result === null) result = dbCollection[i]; } } } else if ((typeof sectionSignature === "string" && PE.isSectionNamePresent(sectionSignature)) || (sectionSignature instanceof RegExp && PE.isSectionNamePresentExp(sectionSignature))) { if (dbCollection[i][0] === null) return null; if (result === null) result = dbCollection[i]; } } return result; } /** * Matches PE resource names against string or regular-expression signatures in a database collection. * * An entry whose name at index 0 is null acts as an exclusion and immediately * cancels the match. Entries with more than three fields are reported as malformed. * * @param {Array} dbCollection - The database collection to validate, where each item is expected to have a resource signature at index 2. * @returns {Array|null} The first matching item from the collection, or null if no match is found. * @example * validateResourceNames([["Installer", null, /^SETUP$/i]]); */ function validateResourceNames(dbCollection) { var result = null; for (var i = 0; i < dbCollection.length; i++) { if (dbCollection[i].length > 3) { stdout(">>> Invalid resource signature entry for \"" + dbCollection[i][0] + "\": expected 3 elements, but found " + dbCollection[i].length + ". Extra signatures will be ignored. Use [ and ] to define a signature collection. <<<"); } var resourceSignature = dbCollection[i][2]; if (Array.isArray(resourceSignature)) { for (var j = 0; j < resourceSignature.length; j++) { if ((typeof resourceSignature[j] === "string" && PE.isResourceNamePresent(resourceSignature[j])) || (resourceSignature[j] instanceof RegExp && PE.isResourceNamePresentExp(resourceSignature[j]))) { if (dbCollection[i][0] === null) return null; if (result === null) result = dbCollection[i]; } } } else if ((typeof resourceSignature === "string" && PE.isResourceNamePresent(resourceSignature)) || (resourceSignature instanceof RegExp && PE.isResourceNamePresentExp(resourceSignature))) { if (dbCollection[i][0] === null) return null; if (result === null) result = dbCollection[i]; } } return result; } /** * Matches complete .NET object-name collections against a signature database collection. * * Regular expressions are intentionally unsupported: every string in the collection stored * at index 2 must identify an exact metadata object, and every object must be present. * Entries with more than three fields are reported as malformed. * * @param {Array} dbCollection - The database collection to validate, where each item is expected to have an object-name collection at index 2. * @returns {Array|null} The first matching item from the collection, or null if no match is found. * @example * validateNetObjectNames([["ConfuserEx", null, ["Decrypt", "LzmaDecoder"]]]); */ function validateNetObjectNames(dbCollection) { for (var i = 0; i < dbCollection.length; i++) { if (dbCollection[i].length > 3) { stdout(">>> Invalid .NET object signature entry for \"" + dbCollection[i][0] + "\": expected 3 elements, but found " + dbCollection[i].length + ". Extra signatures will be ignored. Use [ and ] to define an object-name collection. <<<"); } var objectNames = dbCollection[i][2]; if (Array.isArray(objectNames) && isAllNetObjectsPresent(objectNames)) return dbCollection[i]; } return null; } /** * Calculates the file offset of the PE optional header. * * The function reads the e_lfanew field at offset 0x3C to find the PE header start, * then adds 4 bytes for the PE signature and 20 bytes for the COFF header to locate * the start of the optional header. * * @returns {number} The offset in bytes, derived from the DOS `e_lfanew` field. * @example * getOptHeaderOffset(); // e_lfanew + 24 */ function getOptHeaderOffset() { return PE.read_int32(0x3C) + 4 + 20; } /** * Returns the size of a bounded, length-consistent trailing attribute-certificate table. * * The Security directory is the only PE data directory whose address is a raw file * offset. Restricting the result to an aligned table which exactly consumes the file * tail prevents malformed directory values from hiding unrelated overlay bytes. * Entry lengths must cover the whole table; this does not verify a digital signature. * * @returns {number} Certificate-table size in bytes, or zero when its bounds or lengths cannot be established. */ function getWinAuthCertificateSize() { if (PE_Cached.overlaySize < 8) return 0; const optionalHeaderOffset = getOptHeaderOffset(); if (optionalHeaderOffset < 4 || optionalHeaderOffset >= PE_Cached.fileSize) return 0; const optionalHeaderSize = PE.readWord(optionalHeaderOffset - 4), optionalHeaderEnd = Math.min(optionalHeaderOffset + optionalHeaderSize, PE_Cached.fileSize), securityDirectoryOffset = optionalHeaderOffset + (PE_Cached.is64bit ? 0x90 : 0x80); if (securityDirectoryOffset + 8 > optionalHeaderEnd) return 0; // Bytes in an undeclared directory slot are not a certificate-table entry. // Certificate Table has index 4, so NumberOfRvaAndSizes must exceed 4: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-data-directories-image-only if (PE.read_uint32(optionalHeaderOffset + (PE_Cached.is64bit ? 0x6C : 0x5C)) <= 4) return 0; const certificateOffset = PE.read_uint32(securityDirectoryOffset), certificateSize = PE.read_uint32(securityDirectoryOffset + 4), overlayEndOffset = PE_Cached.overlayOffset + PE_Cached.overlaySize; if (certificateOffset < PE_Cached.overlayOffset || certificateSize < 8 || certificateOffset % 8 !== 0 || certificateSize % 8 !== 0 || certificateOffset + certificateSize !== overlayEndOffset) return 0; // A directory range alone cannot distinguish certificate metadata from an overlay payload. // Each dwLength includes the header and advances on an eight-byte boundary: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-attribute-certificate-table-image-only var entryOffset = certificateOffset; for (var entryCount = 0; entryCount < 256 && entryOffset < overlayEndOffset; entryCount++) { if (entryOffset + 8 > overlayEndOffset) return 0; var entrySize = PE.read_uint32(entryOffset); if (entrySize < 8 || entrySize > overlayEndOffset - entryOffset) return 0; entryOffset += Math.ceil(entrySize / 8) * 8; } // On malformed lengths or budget exhaustion, retain the bytes for ordinary overlay analysis. if (entryOffset !== overlayEndOffset) return 0; return certificateSize; } /** * Resolves section/overlay entropy thresholds using rigorous bounds before a full scan. * Returns a lower bound only above 7.4 and an upper bound only at or below 7; * comparisons against the 7, 7.3 and 7.4 thresholds retain full-range verdicts. * The small preview chooses a scan strategy, but never supplies a verdict. * * @param {number} rangeOffset - File offset of the range. * @param {number} rangeSize - Size of the range in bytes. * @param {Object} [knownRange] - Receives exact Entropy and Size of a measured prefix for reuse. * @returns {number} Conclusive entropy bound, exact fallback, or zero for an invalid range. */ function calculateStructuredDataEntropy(rangeOffset, rangeSize, knownRange) { if (knownRange) { knownRange.Entropy = 0; knownRange.Size = 0; } if (!(rangeOffset >= 0) || !(rangeSize > 0) || rangeOffset + rangeSize > PE_Cached.fileSize) return 0; if (rangeSize >= 0x400000) { var previewEntropy = PE.calculateEntropy(rangeOffset, 0x10000), measuredSize = previewEntropy <= 1.8 ? Math.floor(rangeSize / 4) : previewEntropy >= 7.9 ? Math.floor(rangeSize * 15 / 16) : 0; if (measuredSize > 0) { var measuredEntropy = PE.calculateEntropy(rangeOffset, measuredSize), bounds = calculateEntropyBounds(measuredEntropy, measuredSize, rangeSize); if (bounds.minimum > 7.4 || bounds.maximum <= 7) { if (knownRange) { knownRange.Entropy = measuredEntropy; knownRange.Size = measuredSize; } return bounds.minimum > 7.4 ? bounds.minimum : bounds.maximum; } } } // A preview or a bound near either threshold cannot establish the whole-range verdict. var entropy = PE.calculateEntropy(rangeOffset, rangeSize); if (knownRange) { knownRange.Entropy = entropy; knownRange.Size = rangeSize; } return entropy; } /** * Derives rigorous entropy bounds for a complete range from one exact subrange. * * Shannon entropy is concave, so the weighted known entropy is a lower bound. Its * upper bound adds the binary mixing entropy and assumes the unknown remainder reaches * the byte alphabet maximum of eight bits. A caller may skip an exact full-range pass * only when its verdict threshold lies outside the returned interval. * * @param {number} knownEntropy - Exact entropy of a subrange. * @param {number} knownSize - Size of the subrange in bytes. * @param {number} totalSize - Size of the complete range in bytes. * @returns {Object} Minimum and maximum possible entropy of the complete range. */ function calculateEntropyBounds(knownEntropy, knownSize, totalSize) { if (!(knownEntropy >= 0 && knownEntropy <= 8) || !(knownSize > 0) || !(totalSize > 0) || knownSize > totalSize) { return { minimum: 0, maximum: 8 }; } const knownWeight = knownSize / totalSize; if (knownWeight === 1) return { minimum: knownEntropy, maximum: knownEntropy }; const unknownWeight = 1 - knownWeight, mixingEntropy = -(knownWeight * Math.log(knownWeight) + unknownWeight * Math.log(unknownWeight)) / Math.LN2; return { minimum: knownWeight * knownEntropy, maximum: Math.min(8, mixingEntropy + knownWeight * knownEntropy + unknownWeight * 8) }; } /** * Reads an unsigned little-endian DWORD from an already bounded byte buffer. * * @param {Array} dataBuffer - Byte array returned by PE.readBytes(). * @param {number} offset - Zero-based offset inside the array. * @returns {number} Unsigned 32-bit value. */ function readUnsignedDwordFromBuffer(dataBuffer, offset) { // JavaScript bitwise operators return signed 32-bit values. The final unsigned // shift preserves RVA values above 0x7FFFFFFF as positive Numbers. return (dataBuffer[offset] | (dataBuffer[offset + 1] << 8) | (dataBuffer[offset + 2] << 16) | (dataBuffer[offset + 3] << 24)) >>> 0; } /** * Reads a little-endian QWORD when JavaScript can represent it exactly. * * @param {number} offset - File offset of the eight-byte value. * @returns {number} Exact unsigned value, or -1 when it exceeds Number precision. */ function readUnsignedQword(offset) { var lowDword = PE.read_uint32(offset), highDword = PE.read_uint32(offset + 4); // Number.MAX_SAFE_INTEGER has only 53 exact integer bits. Reject a high part wider // than 21 bits instead of silently rounding an address to adjacent file data. if (highDword > 0x1FFFFF) return -1; // Arithmetic composition is intentional: bitwise operators would truncate the QWORD. return lowDword + highDword * 0x100000000; } /** * Proves that a virtual-address range belongs to one mapped image section. * * @param {number} address - Virtual address at the beginning of the range. * @param {number} size - Required byte count. * @param {number} requiredSectionFlags - Complete section-characteristic mask, or zero. * @returns {boolean} True when the loader maps the whole range with every required flag. */ function isVirtualAddressRangeMapped(address, size, requiredSectionFlags) { // Reject empty and wrapped ranges before subtracting ImageBase. Keeping all values // as Numbers avoids signed 32-bit truncation on normal PE32+ addresses. if (address < PE_Cached.imageBase || size <= 0 || address + size <= address) return false; var rangeRva = address - PE_Cached.imageBase; for (var i = 0; i < PE_Cached.numberOfSections; i++) { // The loader maps the larger virtual/raw extent. Requiring the complete range to // stay in one section prevents a valid start from hiding an unmapped tail. var sectionRva = PE.getSectionVirtualAddress(i), sectionMappedSize = Math.max(PE.getSectionVirtualSize(i), PE.getSectionFileSize(i)), rangeOffsetInSection = rangeRva - sectionRva; if (rangeOffsetInSection >= 0 && rangeOffsetInSection + size <= sectionMappedSize && PE.section[i].hasFlags(requiredSectionFlags)) return true; } return false; } /** * Proves that a virtual-address range belongs to one physically backed section. * * @param {number} address - Virtual address at the beginning of the range. * @param {number} size - Required byte count. * @param {number} requiredSectionFlags - Complete section-characteristic mask, or zero. * @returns {boolean} True only when the whole range is file-backed and has every required flag. */ function isVirtualAddressRangeFileBacked(address, size, requiredSectionFlags) { // This helper is intentionally stricter than isVirtualAddressRangeMapped(): callers // intend to read bytes from disk, so virtual padding is never an acceptable match. if (address < PE_Cached.imageBase || size <= 0 || address + size <= address) return false; var rangeRva = address - PE_Cached.imageBase; for (var i = 0; i < PE_Cached.numberOfSections; i++) { var sectionRva = PE.getSectionVirtualAddress(i), sectionRawSize = PE.getSectionFileSize(i), rangeOffsetInSection = rangeRva - sectionRva; if (rangeOffsetInSection >= 0 && rangeOffsetInSection + size <= sectionRawSize && PE.section[i].hasFlags(requiredSectionFlags)) { var rangeOffset = PE.getSectionFileOffset(i) + rangeOffsetInSection; // Validate both endpoints through the engine as well as local section math. // This rejects truncated files, overlapping mappings and translation anomalies. return rangeOffset >= 0 && rangeOffset + size <= PE_Cached.fileSize && PE.VAToOffset(address) === rangeOffset && PE.VAToOffset(address + size - 1) === rangeOffset + size - 1; } } return false; } /** * Checks whether the PE Optional Header contains a non-empty CLR Runtime Header directory. * * This low-level fallback reads `NumberOfRvaAndSizes`, then the CLR directory's * RVA and size. It supports PE32 and PE32+ and rejects header offsets outside * the file bounds. It does not validate the directory contents. * * @returns {boolean} True if the declared CLR directory has non-zero RVA and size; otherwise false. */ function isNetMetaDataPresent() { if (PE_Cached.fileSize < 0x40) return false; var optHeaderOffset = getOptHeaderOffset(); if (optHeaderOffset < 24 || optHeaderOffset >= PE_Cached.fileSize) return false; var optHeaderEnd = Math.min(optHeaderOffset + PE.readWord(optHeaderOffset - 4), PE_Cached.fileSize); var numRvaAndSizesOffset, clrDirOffset; if (!PE_Cached.is64bit) { numRvaAndSizesOffset = optHeaderOffset + 0x5C; clrDirOffset = optHeaderOffset + 0xD0; // CLR offset for PE32 } else { numRvaAndSizesOffset = optHeaderOffset + 0x6C; clrDirOffset = optHeaderOffset + 0xE0; // CLR offset for PE64 } // Section headers may follow a short Optional Header at these same physical offsets. // Only bytes inside the declared Optional Header can supply CLR directory evidence. if (clrDirOffset + 8 > optHeaderEnd || numRvaAndSizesOffset + 4 > optHeaderEnd) { return false; } // Check if the number of RVA and sizes is valid if (PE.read_uint32(numRvaAndSizesOffset) < 15) { return false; } // Read CLR directory RVA and size var clrRva = PE.read_int32(clrDirOffset), clrSize = PE.read_int32(clrDirOffset + 4); // Validate CLR directory RVA and size return clrRva !== 0 && clrSize !== 0; } /** * Checks if a compiler has been detected in the analysis results. * * @returns {boolean} Returns true if any compiler or compiler-related results are found, false otherwise. */ function isCompilerDetected() { return _getNumberOfResults("compiler") > 0 || _getNumberOfResults("~compiler") > 0; } /** * Checks if a linker has been detected in the analysis results. * * @returns {boolean} Returns true if any linker or linker-related results are found, false otherwise. */ function isLinkerDetected() { return _getNumberOfResults("linker") > 0 || _getNumberOfResults("~linker") > 0; } // #region "languages and compilers (.NET and Native)" /** Infers source languages and compilers from PE metadata and binary markers. */ function scanForLanguagesAndCompilers_NET_and_Native() { log(logType.nothing, "Scanning for programming language..."); var c_cpp = _isLangPresent("C/C++"); // Unknown; C or C++ const extdb = [ ["C++", "cpp"], // https://en.wikipedia.org/wiki/C++ ["Rust", "rs"], // https://en.wikipedia.org/wiki/Rust_(programming_language) ["Java", "jar"], // https://en.wikipedia.org/wiki/Java ["Python", "pyd"], // https://en.wikipedia.org/wiki/Python_(programming_language) ["Kotlin", "kt"] // https://en.wikipedia.org/wiki/Kotlin ]; const sourceFileExtensionMinimumCount = 3, sourceFileNameContextLength = 0x100; function isSourceFileExtensionPresent(extension) { var extensionMarker = "." + extension, areaOffset = PE_Cached.imageBodyOffset, areaEnd = areaOffset + PE_Cached.imageBodySize, currentOffset = areaOffset, remainingSize = PE_Cached.imageBodySize, sourceFileCount = 0; while (remainingSize >= extensionMarker.length + 1 && sourceFileCount < sourceFileExtensionMinimumCount) { var extensionOffset = PE.findString(currentOffset, remainingSize, extensionMarker); if (extensionOffset === -1) break; var nextOffset = extensionOffset + extensionMarker.length; currentOffset = nextOffset; remainingSize = areaEnd - currentOffset; // An extension is useful only as a cheap anchor. Validate the string // terminator and the adjacent filename locally instead of making the // signature engine scan the complete image through leading wildcards. if (nextOffset >= areaEnd || PE.readByte(nextOffset) !== 0x00) continue; var contextSize = Math.min(sourceFileNameContextLength, extensionOffset - areaOffset), contextBytes = PE.readBytes(extensionOffset - contextSize, contextSize), sourceFilePrefix = String(); for (var k = 0; k < contextBytes.length; k++) { sourceFilePrefix += String.fromCharCode(contextBytes[k]); } var stringBoundary = sourceFilePrefix.lastIndexOf("\0"); if (stringBoundary !== -1) sourceFilePrefix = sourceFilePrefix.substring(stringBoundary + 1); var pathSeparator = Math.max(sourceFilePrefix.lastIndexOf("\\"), sourceFilePrefix.lastIndexOf("/")), sourceFileName = sourceFilePrefix.substring(pathSeparator + 1); if (/^[A-Za-z0-9_$@][A-Za-z0-9_$@+ .(){}\[\]-]*$/.test(sourceFileName)) sourceFileCount++; } return sourceFileCount >= sourceFileExtensionMinimumCount; } for (var i = 0; i < extdb.length; i++) { const langName = extdb[i][0], langExtName = extdb[i][1]; if (!_isLangPresent(langName) && isSourceFileExtensionPresent(langExtName)) { log(logType.any, "Lines of ." + langExtName + " files (" + langName + ") detected"); _setLangByHeur(langName); } } if (!_isLangDetected("C++") && isSourceFileExtensionPresent("c")) { log(logType.any, "Lines of .c files (C) detected (not a C++)"); _setLangByHeur("C"); } for (var i = 0; i < PE_Cached.numberOfUnmanagedResources; i++) { var resourceOffset = PE.getResourceOffsetByNumber(i), resourceSize = PE.getResourceSizeByNumber(i); if (resourceOffset >= 0 && resourceSize > 0 && resourceOffset <= PE_Cached.fileSize - resourceSize) { // Adjacent resource bytes cannot complete a text-DFM header from this resource. var resourceSignature = PE.getString(resourceOffset, Math.min(resourceSize, 0x40)); if (resourceSignature.split(" ")[0] === "object" && resourceSignature.indexOf(": ") !== -1) { _setLangByHeur("Object Pascal"); break; } } } var isPpLibraryPresent = false, isCLibraryPresent = false; for (var i = 0; i < PE_Cached.numberOfUnmanagedImports; i++) { const libraryName = PE.getImportLibraryName(i).toLowerCase(); // Detect mangler in imported libraries for (var k = 0; k < PE.getNumberOfImportThunks(i) && !isPpLibraryPresent; k++) { const functionName = PE.getImportFunctionName(i, k); // import, thunk if (isItemMangled(functionName)) { log(logType.any, "Mangler detected -> \"" + libraryName + "\", at function \"" + getNameOfMangledItem(functionName) + "\""); var isNotCpp = false; if (!isCompilerDetected()) { var manglerInfo = getManglerInfoByMangledItem(functionName); if (manglerInfo) { _setResult("~compiler", manglerInfo.compiler, String(), String()); if (!_isLangDetected()) _setLangByHeur(manglerInfo.language); if (manglerInfo.language !== "C++") isNotCpp = true; } } isPpLibraryPresent = !isNotCpp; // if language is unknown } } if ( libraryName.indexOf("++") !== -1 || libraryName.indexOf("cpp") !== -1 || libraryName.indexOf("msvcp") !== -1 ) { log(logType.any, "C++ library present -> \"" + libraryName + "\""); isPpLibraryPresent = true; } if (libraryName.indexOf("msvcr") !== -1 || libraryName.indexOf("crtdll") !== -1) { log(logType.any, "C library present -> \"" + libraryName + "\""); isCLibraryPresent = true; } } // Detect mangler in exports for (var e = 0; e < PE_Cached.numberOfUnmanagedExports && !isPpLibraryPresent; e++) { var exportName = PE.getExportFunctionName(e); if (exportName && isItemMangled(exportName)) { log(logType.any, "Mangler detected at exported item \"" + getNameOfMangledItem(exportName) + "\""); var isNotCpp = false; if (!isCompilerDetected()) { var manglerInfo = getManglerInfoByMangledItem(exportName); if (manglerInfo) { _setResult("~compiler", manglerInfo.compiler, String(), String()); if (!_isLangDetected()) _setLangByHeur(manglerInfo.language); if (manglerInfo.language !== "C++") isNotCpp = true; } } isPpLibraryPresent = !isNotCpp; } } // Detect compilers by Rich signature if (!isCompilerDetected() && PE_Cached.isRichSignaturePresent) { for (var j = 0; j < PE_Cached.numberOfRichIDs; j++) { const richId = PE.getRichID(j), richVersion = PE.getRichVersion(j), richCount = PE.getRichCount(j); if (richId === 0x000d && richVersion === 9782 && richCount === 0x0001) { log(logType.any, "Microsoft Visual Basic 6.0 build metadata detected"); _setResult("~compiler", "Microsoft Visual Basic", "6.0", String()); _setLangByHeur("VB"); break; } else if (richId === 0x0103) { log(logType.any, "MASM build metadata detected"); _setResult("~compiler", "MASM", String(), String()); break; } } } // Detect IL2CPP by sections (thanks to AyukiDev) if (!isCompilerDetected()) { var isIl2cpp = false; if (PE.isSectionNamePresentExp(/^\.rtc\$[IT](?:AA|ZZ)$/)) { isIl2cpp = true; } else { const il2cppSections = [ "il2cpp", ".text$mn", ".rdata$zzzdbg" ]; for (var i = 0; i < il2cppSections.length && !isIl2cpp; i++) { if (PE.isSectionNamePresent(il2cppSections[i])) { isIl2cpp = true; } } } if (isIl2cpp) { log(logType.any, "IL2CPP build characteristics detected"); _setResult("~compiler", "IL2CPP Technology", String(), String()); _setLangByHeur("Native MSIL/C#"); } } var isCLikeLibsNotFound = !isCLibraryPresent && !isPpLibraryPresent; var majorLinkerVersion = PE.getMajorLinkerVersion(), minorLinkerVersion = PE.getMinorLinkerVersion(); if (!isCompilerDetected() && !isLinkerDetected() && !PE_Cached.isDotNet) { var ehFrameSection = PE.section[".eh_frame"], buildIdSection = PE.section[".build-id"]; if (!ehFrameSection && !buildIdSection && ( PE_Cached.isRichSignaturePresent || PE.compare("'MZ'90000300000004000000FFFF0000B800000000000000400000000000000000000000000000000000000000000000000000000000000000000000....00000E1FBA0E00B409CD21B8014CCD21'This program cannot be run in DOS mode.\r\r\n$'00000000") || PE.compare("'MZ'90000300000004000000FFFF0000B800000000000000400000000000000000000000000000000000000000000000000000000000000000000000....000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") || PE.isSectionNamePresent(".00cfg") )) { if (PE.isSectionNamePresent(".gfids") || PE.isSectionNamePresent(".giats") || PE.isSectionNamePresent(".gljmp") || PE.isSectionNamePresent(".voltbl")) { log(logType.any, "Microsoft Visual Studio build markers detected"); _setResult("~tool", "Microsoft Visual Studio", String(), String()) } log(logType.any, "Microsoft linker and compiler characteristics detected"); _setResult("~linker", "Microsoft Linker", (majorLinkerVersion !== 0 ? (majorLinkerVersion + "." + minorLinkerVersion) : String()), String()); _setResult("~compiler", "Microsoft Visual C/C++", String(), String()); if (!_isLangDetected() && isCLikeLibsNotFound) { if (isNetMetaDataPresent()) { _setLangByHeur("MSIL/C/C++"); } else { _setLangByHeur("C/C++"); } } } else if ( ehFrameSection || buildIdSection || ( PE.isSectionNamePresent(".CRT") && PE.isSectionNamePresent(".rdata") && PE.isSectionNamePresent(".xdata") && PE.isSectionNamePresent(".idata") && (PE.isSectionNamePresent(".tls") || PE.isSectionNamePresent(".bss")) || PE.isSectionNamePresent(".buildid") )) { log(logType.any, "MinGW toolchain characteristics detected"); _setResult("~compiler", "MinGW", String(), String()); if (!_isLangDetected() && isCLikeLibsNotFound) { _setLangByHeur("C/C++"); } } else if (PE.isSectionNamePresentExp(/\.f(?:lat|asm)$/) && !isPpLibraryPresent) { log(logType.any, "FASM toolchain characteristics detected"); _setResult("~compiler", "FASM", String(), String()); _setLangByHeur(getAsmLanguageName()); } } const rdataSection = PE.section[".rdata"]; if (rdataSection) { if (c_cpp && // if C/C++ detected by DIE PE.isSignaturePresent( rdataSection.FileOffset, rdataSection.FileSize, convertStringToUnicodeSignature("Visual C++"))) { log(logType.any, "Embedded Visual C++ Runtime detected."); isPpLibraryPresent = true; // Visual C++ Runtime library in resources } } if (isPpLibraryPresent || (c_cpp && PE.isSignaturePresent(PE_Cached.imageBodyOffset, PE_Cached.imageBodySize, "' C++ '"))) { _setLangByHeur("C++"); } else if (!_isLangPresent("C++") && isCLibraryPresent && (PE.isFunctionPresent("_iob") || PE.isFunctionPresent("printf") || PE.isFunctionPresent("malloc") || PE.isFunctionPresent("memset"))) { _setLangByHeur("C"); } else if (PE.isLibraryPresentExp(/^api-ms-win-crt*/i) || PE.isSectionNamePresent(".msvcjmc") || PE.isSectionNamePresentExp(/\.CRT(?:\$[A-Z]{3})?$/i)) { _setLangByHeur("C/C++"); } else if (!_isLangDetected() && !_getNumberOfResults("compiler") && !PE_Cached.isDotNet) { if (!_getNumberOfResults("protector") && !_getNumberOfResults("cryptor") && !_getNumberOfResults("~cryptor") && !_getNumberOfResults("packer") && !_getNumberOfResults("~packer")) { _setLangByHeur(getAsmLanguageName()); } } } // #endregion /** * Sets the language based on heuristic analysis. * * @param {string} languageName - The name of the detected language. */ function _setLangByHeur(languageName) { log(logType.any, languageName + " language detected!"); _setLang(languageName, heurLabel); } /** * Adds a new option to the existing options text, separated by " + " if optionsText is not empty. * * @param {string} optionsText - The current options text. * @param {string} newOptionText - The new option to add. * @returns {string} The updated options text with the new option appended. */ function addOption(optionsText, newOptionText) { if (optionsText) optionsText += " + "; optionsText += newOptionText; return optionsText; } /** * Logs a message with a specific type and text. * * @param {number} messageTypeId - The type of the message. * -2: Error, * -1: About, * 1: Any, * 2: .NET, * 3: Instruction emulation. * @param {string} messageText - The text of the message to log. * Must not contain newline characters. * @throws Will throw an error if the messageText contains a newline character. */ function log(messageTypeId, messageText) { if (!messageText) { return; } else if (/\r|\n|\t/.test(messageText)) { messageText = messageText.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim(); // _error("Illegal char at log( ... )"); } var prefix = String(); if (messageTypeId !== logType.warning) { prefix = heurLabel; } if (messageTypeId > logType.warning && messageTypeId !== logType.nothing) { prefix += "/"; } switch (messageTypeId) { case logType.warning: prefix = "!"; break; case logType.about: prefix += "About"; break; case logType.any: prefix += "Any"; break; case logType.net: prefix += ".NET"; break; case logType.emulation: prefix += "EMU"; break; } if (typeof _log !== "undefined") { _log("[" + prefix + "] " + messageText); } else { _error("Unable to write log message"); } } // #region "encrypted PE KPA" const encryptedPeKey0Offsets = [0, 40, 40, 42, 40, 40, 42, 42, 40, 45, 40, 44, 48, 52, 42, 45, 48, 51, 54, 57, 40], encryptedPeKey1Offsets = [0, 40, 41, 40, 41, 41, 43, 43, 41, 46, 41, 45, 49, 40, 43, 46, 49, 52, 55, 58, 41], encryptedPeLfa0Offsets = [0, 40, 40, 42, 40, 40, 42, 46, 44, 42, 40, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40], encryptedPeLfa1Offsets = [0, 40, 41, 40, 41, 41, 43, 40, 45, 43, 41, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41], encryptedPeLfa2Offsets = [0, 40, 40, 41, 42, 42, 44, 41, 46, 44, 42, 40, 50, 49, 48, 47, 46, 45, 44, 43, 42]; /** * Verifies a KPA-derived encrypted PE candidate without materializing its plaintext. * * @param {Array|Uint8Array} dataBuffer - Candidate data range. * @param {number} peStartOffset - Candidate MZ offset inside the range. * @param {number} maxValidLfaNew - Exclusive upper bound for e_lfanew. * @param {number} keyLength - Repeating key length from 1 through 20. * @param {number} mode - 0 for XOR/XNOR, 1 for ADD/SUB, 2 for reverse subtraction. * @returns {boolean} True when the decrypted header fields describe a valid PE image. */ function verifyEncryptedPeSignature(dataBuffer, peStartOffset, maxValidLfaNew, keyLength, mode) { maxValidLfaNew = Math.min(maxValidLfaNew, dataBuffer.length - peStartOffset - 0x20); var encZero = dataBuffer[peStartOffset + encryptedPeLfa2Offsets[keyLength]], cipherByte = dataBuffer[peStartOffset + 0x3E], valLfa2 = mode === 0 ? (cipherByte ^ encZero) : (mode === 1 ? ((cipherByte - encZero) & 0xFF) : ((encZero - cipherByte) & 0xFF)); if ((valLfa2 << 16) >= maxValidLfaNew) return false; encZero = dataBuffer[peStartOffset + encryptedPeLfa1Offsets[keyLength]]; cipherByte = dataBuffer[peStartOffset + 0x3D]; var valLfa1 = mode === 0 ? (cipherByte ^ encZero) : (mode === 1 ? ((cipherByte - encZero) & 0xFF) : ((encZero - cipherByte) & 0xFF)); encZero = dataBuffer[peStartOffset + encryptedPeLfa0Offsets[keyLength]]; cipherByte = dataBuffer[peStartOffset + 0x3C]; var valLfa0 = mode === 0 ? (cipherByte ^ encZero) : (mode === 1 ? ((cipherByte - encZero) & 0xFF) : ((encZero - cipherByte) & 0xFF)); const lfaNewOffset = (valLfa0 | (valLfa1 << 8) | (valLfa2 << 16)) >>> 0; if (lfaNewOffset <= 0x40 || lfaNewOffset >= maxValidLfaNew) return false; const baseZeroOffset = peStartOffset + 40, peSignatureOffset = peStartOffset + lfaNewOffset, baseRemainder = (lfaNewOffset - 40) % keyLength; encZero = dataBuffer[baseZeroOffset + (baseRemainder % keyLength)]; cipherByte = dataBuffer[peSignatureOffset]; if ((mode === 0 ? (cipherByte ^ encZero) : mode === 1 ? ((cipherByte - encZero) & 0xFF) : ((encZero - cipherByte) & 0xFF)) !== 0x50) return false; encZero = dataBuffer[baseZeroOffset + ((baseRemainder + 1) % keyLength)]; cipherByte = dataBuffer[peSignatureOffset + 1]; if ((mode === 0 ? (cipherByte ^ encZero) : mode === 1 ? ((cipherByte - encZero) & 0xFF) : ((encZero - cipherByte) & 0xFF)) !== 0x45) return false; encZero = dataBuffer[baseZeroOffset + ((baseRemainder + 2) % keyLength)]; cipherByte = dataBuffer[peSignatureOffset + 2]; if ((mode === 0 ? (cipherByte ^ encZero) : mode === 1 ? ((cipherByte - encZero) & 0xFF) : ((encZero - cipherByte) & 0xFF)) !== 0x00) return false; encZero = dataBuffer[baseZeroOffset + ((baseRemainder + 3) % keyLength)]; cipherByte = dataBuffer[peSignatureOffset + 3]; if ((mode === 0 ? (cipherByte ^ encZero) : mode === 1 ? ((cipherByte - encZero) & 0xFF) : ((encZero - cipherByte) & 0xFF)) !== 0x00) return false; encZero = dataBuffer[baseZeroOffset + ((baseRemainder + 0x18) % keyLength)]; cipherByte = dataBuffer[peSignatureOffset + 0x18]; var magicByte1 = mode === 0 ? (cipherByte ^ encZero) : mode === 1 ? ((cipherByte - encZero) & 0xFF) : ((encZero - cipherByte) & 0xFF); encZero = dataBuffer[baseZeroOffset + ((baseRemainder + 0x19) % keyLength)]; cipherByte = dataBuffer[peSignatureOffset + 0x19]; var magicByte2 = mode === 0 ? (cipherByte ^ encZero) : mode === 1 ? ((cipherByte - encZero) & 0xFF) : ((encZero - cipherByte) & 0xFF), headerMagic = magicByte1 | (magicByte2 << 8); if (headerMagic !== 0x010B && headerMagic !== 0x020B) return false; encZero = dataBuffer[baseZeroOffset + ((baseRemainder + 0x06) % keyLength)]; cipherByte = dataBuffer[peSignatureOffset + 0x06]; var numSections1 = mode === 0 ? (cipherByte ^ encZero) : mode === 1 ? ((cipherByte - encZero) & 0xFF) : ((encZero - cipherByte) & 0xFF); encZero = dataBuffer[baseZeroOffset + ((baseRemainder + 0x07) % keyLength)]; cipherByte = dataBuffer[peSignatureOffset + 0x07]; var numSections2 = mode === 0 ? (cipherByte ^ encZero) : mode === 1 ? ((cipherByte - encZero) & 0xFF) : ((encZero - cipherByte) & 0xFF), totalSections = numSections1 | (numSections2 << 8); if (totalSections === 0 || totalSections > 48) return false; encZero = dataBuffer[baseZeroOffset + ((baseRemainder + 0x16) % keyLength)]; cipherByte = dataBuffer[peSignatureOffset + 0x16]; var charByte1 = mode === 0 ? (cipherByte ^ encZero) : mode === 1 ? ((cipherByte - encZero) & 0xFF) : ((encZero - cipherByte) & 0xFF); encZero = dataBuffer[baseZeroOffset + ((baseRemainder + 0x17) % keyLength)]; cipherByte = dataBuffer[peSignatureOffset + 0x17]; var charByte2 = mode === 0 ? (cipherByte ^ encZero) : mode === 1 ? ((cipherByte - encZero) & 0xFF) : ((encZero - cipherByte) & 0xFF); return Boolean((charByte1 | (charByte2 << 8)) & 0x0002); } /** * Searches a byte range for an encrypted PE header using the original KPA proofs. * * The two key-period predicates are grouped by their ten unique offsets and encoded * as a 20-bit candidate mask. Surviving key lengths are still verified from 1 to 20, * preserving the original result and algorithm precedence exactly. * * @param {Array|Uint8Array} dataBuffer - Data range to inspect. * @param {number} bufferSize - Number of bytes available in the range. * @returns {string} Detected algorithm label, or an empty string. */ function scanBufferForEncryptedPe(dataBuffer, bufferSize) { // Truncated engine reads must not turn missing bytes into zero-valued XOR operands. bufferSize = Math.min(bufferSize, dataBuffer.length); // Check if the native function is present if (typeof PE !== "undefined" && typeof PE.scanBufferForEncryptedPe === "function") { return PE.scanBufferForEncryptedPe(dataBuffer, bufferSize); } if (typeof Binary !== "undefined" && typeof Binary.scanBufferForEncryptedPe === "function") { return Binary.scanBufferForEncryptedPe(dataBuffer, bufferSize); } // Fallback: Old implementation in JavaScript const maxSearchIndex = bufferSize - 0x100; for (var offset = 0; offset < maxSearchIndex; offset++) { const cipherByte3 = dataBuffer[offset + 3], cipherLfaMsb = dataBuffer[offset + 0x3F]; var candidateKeyLengths = 0; if (cipherByte3 === dataBuffer[offset + 40] && cipherLfaMsb === dataBuffer[offset + 40]) { candidateKeyLengths |= 0x00001; // 1 } if (cipherByte3 === dataBuffer[offset + 41]) { if (cipherLfaMsb === dataBuffer[offset + 41]) candidateKeyLengths |= 0x00002; // 2 if (cipherLfaMsb === dataBuffer[offset + 44]) candidateKeyLengths |= 0x40000; // 19 } if (cipherByte3 === dataBuffer[offset + 42]) { if (cipherLfaMsb === dataBuffer[offset + 42]) candidateKeyLengths |= 0x00004; // 3 if (cipherLfaMsb === dataBuffer[offset + 50]) candidateKeyLengths |= 0x01000; // 13 } if (cipherByte3 === dataBuffer[offset + 43]) { if (cipherLfaMsb === dataBuffer[offset + 43]) candidateKeyLengths |= 0x80218; // 4, 5, 10, 20 if (cipherLfaMsb === dataBuffer[offset + 47]) candidateKeyLengths |= 0x00080; // 8 } if (cipherByte3 === dataBuffer[offset + 45]) { if (cipherLfaMsb === dataBuffer[offset + 45]) candidateKeyLengths |= 0x00020; // 6 if (cipherLfaMsb === dataBuffer[offset + 42]) candidateKeyLengths |= 0x00040; // 7 if (cipherLfaMsb === dataBuffer[offset + 49]) candidateKeyLengths |= 0x02000; // 14 } if (cipherByte3 === dataBuffer[offset + 47] && cipherLfaMsb === dataBuffer[offset + 41]) { candidateKeyLengths |= 0x00400; // 11 } if (cipherByte3 === dataBuffer[offset + 48]) { if (cipherLfaMsb === dataBuffer[offset + 45]) candidateKeyLengths |= 0x00100; // 9 if (cipherLfaMsb === dataBuffer[offset + 48]) candidateKeyLengths |= 0x04000; // 15 } if (cipherByte3 === dataBuffer[offset + 51]) { if (cipherLfaMsb === dataBuffer[offset + 51]) candidateKeyLengths |= 0x00800; // 12 if (cipherLfaMsb === dataBuffer[offset + 47]) candidateKeyLengths |= 0x08000; // 16 } if (cipherByte3 === dataBuffer[offset + 54] && cipherLfaMsb === dataBuffer[offset + 46]) { candidateKeyLengths |= 0x10000; // 17 } if (cipherByte3 === dataBuffer[offset + 57] && cipherLfaMsb === dataBuffer[offset + 45]) { candidateKeyLengths |= 0x20000; // 18 } if (candidateKeyLengths === 0) continue; const cipherM = dataBuffer[offset], cipherZ = dataBuffer[offset + 1]; if (cipherM === 0x4D && cipherZ === 0x5A) continue; const keyXorM = cipherM ^ 0x4D, keyXorZ = cipherZ ^ 0x5A, keyAddM = (cipherM - 0x4D) & 0xFF, keyAddZ = (cipherZ - 0x5A) & 0xFF, keyRevM = (cipherM + 0x4D) & 0xFF, keyRevZ = (cipherZ + 0x5A) & 0xFF, maxValidLfaNew = bufferSize - offset - 0x20; for (var keyLength = 1, keyLengthBit = 1; candidateKeyLengths !== 0; keyLength++, keyLengthBit <<= 1) { if (!(candidateKeyLengths & keyLengthBit)) continue; candidateKeyLengths ^= keyLengthBit; const cipherByteKey0 = dataBuffer[offset + encryptedPeKey0Offsets[keyLength]]; if (cipherByteKey0 === keyXorM && dataBuffer[offset + encryptedPeKey1Offsets[keyLength]] === keyXorZ) { if (verifyEncryptedPeSignature(dataBuffer, offset, maxValidLfaNew, keyLength, 0)) return "XOR-XNOR"; } // MZ key candidates can coincide across algorithms. A failed full XOR proof // must still allow ADD/SUB and reverse subtraction to validate the header. if (cipherByteKey0 === keyAddM && dataBuffer[offset + encryptedPeKey1Offsets[keyLength]] === keyAddZ && verifyEncryptedPeSignature(dataBuffer, offset, maxValidLfaNew, keyLength, 1)) return "ADD-SUB"; if (cipherByteKey0 === keyRevM && dataBuffer[offset + encryptedPeKey1Offsets[keyLength]] === keyRevZ && verifyEncryptedPeSignature(dataBuffer, offset, maxValidLfaNew, keyLength, 2)) return "SUB-REV"; } } return String(); } // #endregion // #region "malicious code (.NET and Native)" /** Correlates suspicious PE indicators into cautious malicious-code verdicts. */ function scanForMaliciousCode_NET_and_Native() { log(logType.nothing, "Scanning for malicious code..."); var verdicts = []; const mayBeInfected = "May be infected, be careful!", numberOfPackerResults = _getNumberOfResults("packer"), numberOfCryptorResults = _getNumberOfResults("cryptor"), numberOfProtectorResults = _getNumberOfResults("protector"), numberOfProtectionResults = _getNumberOfResults("protection"); if (PE_Cached.isDotNet) { var isNjRatDetected = false, njRatVersion = String(); var njRatDataSeparator = "|'|'|", njRatDataSeparatorPattern = convertStringToUnicodeSignature(njRatDataSeparator) + "00"; // NjRAT Generic: Detect NjRAT via requests-separator or assembly name if (verdicts.length === 0) { if (PE.isNetUStringPresent("im523")) { njRatVersion = "0.7D Green Edition"; isNjRatDetected = true; } else if (PE_Cached.nameOfNetAssemblyName === "w" || PE_Cached.nameOfNetModuleName === "w.exe" || PE_Cached.nameOfNetAssemblyName === "k" || PE_Cached.nameOfNetModuleName === "k.exe" || PE.isNetObjectPresent("njLogger") || PE.isNetUStringPresent("|PWD| ") || PE.isNetUStringPresent(njRatDataSeparator) || PE.isSignatureInSectionPresent(0, njRatDataSeparatorPattern)) { isNjRatDetected = true; } } var requiredDotNetImports = [ "System.Diagnostics", "System.IO.Compression", "user32", "kernel32", "ntdll" // no ext ], foundImportsCount = 0, pattern, allPatternImportsFound; // Reference names belong to CLR metadata, not necessarily section #0 or arbitrary native string data. for (var i = 0; i < requiredDotNetImports.length; i++) { if (PE.isNetObjectPresent(requiredDotNetImports[i])) { foundImportsCount++; } } if (verdicts.length === 0 && !njRatVersion && PE.isNetObjectPresent("System.Net.Sockets") && foundImportsCount >= 2) { var maliciousImportPatterns = [ { type: "NjRAT", edition: "0.7D Green Edition", references: [ "w", "System.Drawing", "System.Windows.Forms", "System.Runtime.InteropServices", "avicap32.dll", "capGetDriverDescriptionA", "kernel32", "KERNEL32.DLL", "ntdll", "user32", "GetWindowText", "EnumChildWindows", "SendMessage", "GetWindowTextLength", "user32.dll", "winmm.dll" ], isVbNet: true }, { type: "NjRAT", edition: "0.7D Lime Edition", references: [ "Stub", "System.Drawing", "System.Windows.Forms", "System.Runtime.InteropServices", "avicap32.dll", "capGetDriverDescriptionA", "kernel32", "ntdll", "NtSetInformationProcess", "user32", "MapVirtualKey", "ToUnicodeEx", "GetKeyboardState", "user32.dll", "wintrust.dll" ], isVbNet: true }, { type: "NjRAT", edition: "0.7D", references: [ "Stub", "System.Drawing", "System.Windows.Forms", "System.Runtime.InteropServices", "avicap32.dll", "capGetDriverDescriptionA", "kernel32", "ntdll", "NtSetInformationProcess", "user32", "MapVirtualKey", "ToUnicodeEx", "GetKeyboardState", "user32.dll" ], isVbNet: true }, { type: "NjRAT", edition: "0.11G-0.12G", references: [ "k", "System.Drawing", "System.Windows.Forms", "System.Runtime.InteropServices", "avicap32.dll", "capGetDriverDescriptionA", "kernel32", "ntdll", "NtsetInformationProcess", "user32", "MapVirtualKey", "ToUnicodeEx", "GetKeyboardState", "GetForegroundWindow", "GetWindowTextLengthA", "GetAsyncKeyState", "user32.dll" ], isVbNet: true }, { type: "NjRAT", edition: "0.7D Danger Edition", references: [ "Stub", "System.Drawing", "System.Windows.Forms", "System.Runtime.InteropServices", "avicap32.dll", "capGetDriverDescriptionA", "Kernel32.dll", "KERNEL32.DLL", "user32", "GetWindowText", "EnumChildWindows", "GetForegroundWindow", "SendMessage", "GetWindowTextLength", "SetWindowPos", "user32.dll", "winmm.dll" ], isVbNet: true }, { type: "NjRAT", edition: "0.7D Golden Edition", references: [ "Stub", "System.Drawing", "System.Windows.Forms", "System.Runtime.InteropServices", "avicap32.dll", "kernel32", "Kernel32.dll", "ntdll", "NtSetInformationProcess", "user32", "user32.dll", "GetForegroundWindow", "GetWindowTextA", "winmm.dll" ], isVbNet: true }, { type: "XWorm", edition: "3.0-5.0", references: [ "System.Core", "System.Drawing", "System.Management", "System.Windows.Forms", "System.Runtime.InteropServices", "avicap32.dll", "capCreateCaptureWindowA", "kernel32.dll", "SetThreadExecutionState", "user32.dll", "GetForegroundWindow", "GetWindowText" ], isVbNet: true }, { type: "VenomRAT", edition: "6.X", references: [ "Microsoft.CSharp", "System.Core", "System.Drawing", "System.Management", "System.Windows.Forms", "System.Runtime.InteropServices", "kernel32.dll", "SetThreadExecutionState", "ntdll.dll", "RtlSetProcessIsCritical", "user32.dll", "GetWindowText", "GetForegroundWindow" ], isVbNet: true }, { type: "RevengeRAT", edition: "0.3", references: [ "System.Management", "System.Windows.Forms", "avicap32.dll", "capGetDriverDescriptionA", "kernel32", "psapi", "EmptyWorkingSet", "user32", "GetWindowText" ], isVbNet: true }, { type: "Webremote TorCT", edition: undefined, references: [ "System.Windows.Forms", "avicap32.dll", "capGetDriverDescriptionA", "capCreateCaptureWindowA", "user32", "SendMessage", "SetWindowPos", "DestroyWindow", "user32.dll", "GetForegroundWindow", "GetWindowText" ], isVbNet: true }, { // NjRAT Generic 1 type: "NjRAT", edition: undefined, references: [ "System.Drawing", "System.Windows.Forms", "GetForegroundWindow", "System.Runtime.InteropServices", "System.Security.Cryptography", "GetWindowText", "avicap32.dll", "A", "kl", "OK" ], isVbNet: true }, { // NjRAT Generic 2 (with USB-spreader) type: "NjRAT", edition: undefined, references: [ "System.Drawing", "System.Windows.Forms", "GetForegroundWindow", "System.Runtime.InteropServices", "System.Security.Cryptography", "GetWindowText", "avicap32.dll", "A", "kl", "USB" ], isVbNet: true } ]; for (var j = 0; j < maliciousImportPatterns.length; j++) { pattern = maliciousImportPatterns[j]; allPatternImportsFound = true; for (var k = 0; k < pattern.references.length && allPatternImportsFound; k++) { if (!PE.isNetObjectPresent(pattern.references[k])) { allPatternImportsFound = false; } } // If not all pattern imports were found, skip this pattern if (!allPatternImportsFound) { continue; } // Check for VB.NET standard library references if (pattern.isVbNet && !PE_Cached.isVbNetStandardLibraryPresent) { continue; } verdicts.push({ type: pattern.type, version: pattern.edition ? pattern.edition : String(), details: String() }); break; } } if (verdicts.length === 0 && isNjRatDetected) { verdicts.push({ type: "NjRAT", version: njRatVersion, details: String() }); } if (verdicts.length === 0 && ( PE.isNetObjectPresent("avfucker") || ( PE.isNetUStringPresent("SharpInvoker") && PE.isNetUStringPresent("myres") ) )) { verdicts.push({ type: "RAT Injector", version: String(), details: String() }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.asyncrat if (verdicts.length === 0 && ( PE_Cached.nameOfNetAssemblyName === "AsyncClient" || PE_Cached.nameOfNetModuleName === "AsyncClient.exe" || (PE.isNetObjectPresent("Pastebin") && PE.isNetObjectPresent("Packet")) || (PE.isNetObjectPresent("Antivirus") && PE.isNetObjectPresent("Pong")) )) { verdicts.push({ type: "AsyncRAT", version: String(), details: String() }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.revenge_rat if (verdicts.length === 0 && ( PE_Cached.nameOfNetAssemblyName === "Nuclear Explosion" || PE_Cached.nameOfNetModuleName === "Nuclear Explosion.exe" )) { verdicts.push({ type: "RevengeRAT", version: "0.3", details: String() }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.stormkitty_rat if (verdicts.length === 0 && (PE.isNetUStringPresent("\nStormKitty v") || (PE.isNetUStringPresent("---") && PE.isNetUStringPresent("btc")))) { verdicts.push({ type: "StormKitty", version: String(), details: String() }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.nanocore if (verdicts.length === 0 && (PE.isNetObjectPresent("ClientLoaderForm") || PE_Cached.nameOfNetAssemblyName === "NanoCore Client")) { verdicts.push({ type: "NanoCore RAT", version: String(), details: String() }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.orcus_rat if (verdicts.length === 0 && (PE_Cached.nameOfNetModuleName === "Orcus.exe" || PE.isNetObjectPresent("Orcus.Connections") || PE.isNetUStringPresent(".orcusInstallation"))) { verdicts.push({ type: "Orcus RAT", version: "1.3-1.9.1", details: String() }); } // https://www.broadcom.com/support/security-center/protection-bulletin/liberium-rat-malware if (verdicts.length === 0 && (PE_Cached.nameOfNetModuleName === "Client.exe" && PE.isNetObjectPresent("System.Security.Cryptography.X509Certificates"))) { verdicts.push({ type: "Liberium RAT", version: String(), details: String() }); } // https://web.archive.org/web/20260605100749/https://github.com/0x25bit/Echelon-Stealer-v5 if (verdicts.length === 0 && (PE_Cached.nameOfNetModuleName === "Echelon.exe" || PE.isNetObjectPresent("TGrabber") || PE.isNetUStringPresent("\n\ud83d\udcc2 FileGrabber: "))) { verdicts.push({ type: "Echelon Stealer", version: String(), details: String() }); } if (verdicts.length === 0 && PE.isNetObjectPresent("button_unlock_Click")) { verdicts.push({ type: "Liberium WinLocker", version: String(), details: String() }); } if (verdicts.length === 0 && PE_Cached.nameOfNetModuleName === "Kheir.exe") { verdicts.push({ type: "jnRAT", version: String(), details: String() }); } // https://blackhatrussia.com/qatar-rat-2026/ if (verdicts.length === 0 && PE_Cached.nameOfNetModuleName === "QatarC.exe") { verdicts.push({ type: "Qatar RAT", version: "1.2", details: String() }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.darkrat if (verdicts.length === 0 && (PE_Cached.nameOfNetModuleName === "Client.exe" && PE.isNetObjectPresent("fusion"))) { verdicts.push({ type: "DarkRAT", version: String(), details: String() }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.icarus (??) if (verdicts.length === 0 && PE.isNetObjectPresent("tosuck")) { verdicts.push({ type: "ICARUS Evil Worm RAT", version: String(), details: String() }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.noneuclid_rat if (verdicts.length === 0 && PE.isNetObjectPresent("isVM_by_wim_temper")) { verdicts.push({ type: "NonEuclid RAT", version: String(), details: String() }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.imminent_monitor_rat if (verdicts.length === 0 && ((PE.isNetObjectPresent("LZLoader") && PE.isNetObjectPresent("Injector")) || PE.isNetUStringPresent("nAvgBytesPerSec: "))) { verdicts.push({ type: "Imminent Monitor RAT", version: String(), details: String() }); } // https://www.sonicwall.com/blog/russian-threat-group-cryptobytes-is-still-active-in-the-wild-with-uxcryptor if (verdicts.length === 0 && ( (PE_Cached.nameOfNetModuleName === "lc.exe" || PE_Cached.nameOfNetAssemblyName === "lc") || PE.isNetObjectPresent("0xC00000FD: The memory location at the specified address returned \"null\"") || PE.isNetUStringPresent("%temp%\\$unlocker_id.ux-cryptobytes") || PE.isNetUStringPresent("\r\nMemory section at address 0x0424* is locked!") || PE.isNetUStringPresent("\r\nService UXCryptor started.") || PE.isNetUStringPresent("\r\n\r\n * Windows blocked!") || PE.isNetUStringPresent("attrib $h $s $r $i /D ") || PE.isNetUStringPresent("lc.exe error") || PE.isNetUStringPresent("ByPassWarnMsg") || PE.isNetUStringPresent("ByPassMessage") || PE.isNetUStringPresent("ux-cryptor.exe") || PE.isNetUStringPresent("YOU ARE HACKED!\\1") || PE.isNetUStringPresent("OneDrive10293") || ( PE.isNetUStringPresent("_o_program") && PE.isNetUStringPresent("border_1") ) )) { verdicts.push({ type: "UX-Locker", version: String(), details: String() }); } if (PE.isSignatureInSectionPresent(0, "00" + convertStringToUnicodeSignature(" RAT") + "00") && ( PE.isNetObjectPresent("System.Net.Sockets") || PE.isNetObjectPresent("GetWindowText") || PE.isNetObjectPresent("avicap32.dll") || PE.isNetObjectPresent("avicap32") ) || ( PE.isNetObjectPresent("AntiTaskManager") || PE.isNetObjectPresent("BlockAvSites") || PE.isNetObjectPresent("UpdaterEXE")) || ( // System.Net.Sockets + GetForegroundWindow + GetWindowsText PE.isNetObjectPresent("System.Net.Sockets") && PE.isNetObjectPresent("GetForegroundWindow") && PE.isNetObjectPresent("GetWindowText") && ( // Search for 'AntivirusProduct' or 'DisableTaskMgr' PE.isSignatureInSectionPresent(0, convertStringToUnicodeSignature("AntivirusProduct")) || PE.isSignatureInSectionPresent(0, convertStringToUnicodeSignature("DisableTaskMgr")) ) ) || ( // GetForegroundWindow + GetWindowText + NtSetInformationProcess PE.isNetObjectPresent("GetForegroundWindow") && PE.isNetObjectPresent("GetWindowText") && PE.isNetObjectPresent("NtSetInformationProcess") )) { verdicts.push({ type: "RAT", version: String(), details: "General signs" }); } var signGroupsToCheck = [{ names: ["Zcash", "ZCash", "zcash"], strings: [ "\\Wallets\\Zcash\\", "Zcash", "zcash" ] }, { names: ["TotalCommander", "Totalcommander", "totalcommander"], strings: [ "\\FTP\\Total Commander", "wcx_ftp.ini", "\\FTP\\Total Commander\\wcx_ftp.ini" ] }, { names: ["Telegram", "telegram", "tdata"], strings: [ "\\Telegram_", "\\Telegram.exe", "Telegram.exe", "\\tdata", "tdata", "Telegram Desktop" ] }, { names: ["Creds"], strings: [ "cookies.sqlite", "\\cookies.sqlite", "logins.json", "key3.db", "\\keys3.db", "key4.db", "\\keys4.db", "Amex Card", "BCGlobal", "Carte Blanche Card", "Diners Club Card", "Discover Card", "Insta Payment Card", "JCB Card", "KoreanLocalCard", "Laser Card", "Maestro Card", "Mastercard", "Solo Card", "Switch Card", "Union Pay Card", "Visa Card", "Visa Master Card", "Express Card" ] }, { names: ["Psi"], strings: [ "\\Psi+\\profiles\\default\\", "\\Jabber\\Psi+\\profiles\\default\\", "\\Psi\\profiles\\default\\", "\\Jabber\\Psi\\profiles\\default\\" ] }, { names: ["ProtonVPN", "Protonvpn", "protonvpn", "ProtonVPNRule"], strings: [ "\\ProtonVPN", "\\ProtonVPN\\ProtonVPN.exe", "\\Vpn\\ProtonVPN\\", "ProldCharotonVPN" ] }, { names: ["Outlook", "OutLook", "outlook"], strings: [ "Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows Messaging Subsystem\\Profiles\\Outlook\\9375CFF0413111d3B88A00104B2A6676", "Software\\Microsoft\\Windows Messaging Subsystem\\Profiles\\9375CFF0413111d3B88A00104B2A6676", "\\EmailClients\\Outlook", "Software\\Microsoft\\Office\\15.0\\Outlook\\Profiles\\Outlook\\9375CFF0413111d3B88A00104B2A6676", "Software\\Microsoft\\Office\\16.0\\Outlook\\Profiles\\Outlook\\9375CFF0413111d3B88A00104B2A6676", "{220D5CC1-853A-11D0-84BC-00C04FD43F8F}", "{417E2D75-84BD-11D0-84BB-00C04FD43F8F}" ] }, { names: ["OpenVPN", "OpenVpn", "openvpn"], strings: [ "OpenVPN", "\\VPN\\OpenVPN", "\\OpenVPN\\config\\conf\\", "\\VPN\\OpenVPN\\config\\conf\\", "OpenVPN Connect\\profiles", "ovpn" ] }, { names: ["PIAVPN", "PiaVPN", "PiaVpn", "Piavpn", "piavpn"], strings: [ "\\pia_manager", "pia_manager" ] }, { names: ["ExpressVPN", "ExpressVpn", "Expressvpn", "expressvpn"], strings: [ "\\ExpressVPN", "ExpressVPN" ] }, { names: ["NordVPN", "NordVpn", "Nordvpn", "nordvpn", "NordApp"], strings: [ "\\NordVPN\\", "NordVPN", "NordVpn.exe*", "\\Vpn\\NordVPN" ] }, { names: ["Proxifier", "proxifier"], strings: [ "\\Roaming\\Proxifier\\Profiles\\Default.ppx", "Default.ppx" ] }, { names: ["Monero", "monero"], strings: [ "monero-project", "monero-core", "wallet_path", "\\Wallets\\Monero\\", "Wallets\\Monero", "Wallets\\Monero\\" ] }, { names: ["Litecoin", "LiteCoin"], strings: [ "Litecoin", "\\Wallets\\LitecoinCore\\", "\\wallet.dat", "\\LitecoinCore\\wallet.dat", "LitecoinCore\\wallet.dat" ] }, { names: ["Jaxx", "jaxx"], strings: [ "\\com.liberty.jaxx\\IndexedDB\\file__0.indexeddb.leveldb\\", "\\Wallets\\Jaxx\\com.liberty.jaxx\\IndexedDB\\file__0.indexeddb.leveldb\\", "com.liberty.jaxx", "file_0.indexeddb.leveldb" ] }, { names: ["FileZilla", "Filezilla", "filezilla"], strings: [ "\\FileZilla", "\\FileZilla\\FileZilla.log", "RecentServers", "\\FileZilla\\recentservers.xml", "FileZilla\\recentservers.xml", "\\FileZilla\\sitemanager.xml", "FileZilla.log" ] }, { names: ["Exodus", "exodus", "ExodusRule"], strings: [ "\\Exodus\\exodus.wallet\\", "\\Wallets\\Exodus\\", "exodus.wallet", "\\exodus.wallet" ] }, { names: ["Ethereum", "ethereum"], strings: [ "\\Ethereum\\keystore", "Ethereum\\keystore", "\\Wallets\\Ethereum\\", "Wallets\\Ethereum\\" ] }, { names: ["FireFox", "Firefox", "firefox", "GetFirefox", "Gecko"], strings: [ "\\mozglue.dll", "mozcrt19.dll", "nss3.dll", "mozsqlite3.dll", "softokn3.dll", "\\Firefox\\Profiles\\", "signons.sqlite", "2a864886f70d0209", "2a864886f70d010c050103", "moz_places", "moz_cookies", "moz_bookmarks", "\\Mozilla Firefox\\", "Mozilla Firefox\\", "\\Mozilla\\Firefox\\Profiles", "webappsstore.sqlite", "SELECT * FROM moz_disabledHosts;", "SELECT * FROM moz_logins;", "SELECT * FROM moz_places", "moz_logins", "encryptedPassword", "NSS_Init" ] }, { names: ["Yahoo", "yahoo", "YahooMessenger"], strings: [ "\\Local Settings\\Application Data\\Yahoo Messenger\\", "\\ys.scr", "ys.scr", "Software\\Yahoo\\pager", "SOFTWARE\\Yahoo\\pager", "\\Yahoo!\\Messenger\\Profiles", "\\Archive\\Messages\\", "\\Archive\\Messages", "Archive\\Messages\\", "Archive\\Messages" ] }, { names: ["Kazaa", "GetKazaa"], strings: [ "Software\\Kazaa\\LocalContent\\ChannelsDir", "SOFTWARE\\Kazaa\\LocalContent\\ChannelsDir" ] }, { names: ["Digsby", "digsby", "GetDigsby", "DigsbyMessenger"], strings: [ "\\Digsby Logs\\", "\\Digsby Logs", "Digsby Logs\\", "Digsby Logs" ] }, { names: ["Electrum", "electrum", "ElectrumRule"], strings: [ "\\Electrum\\wallets", "Electrum\\wallets", "\\Wallets\\Electrum\\", "Wallets\\Electrum\\", "\\electrum_data", "electrum_data", "\\electrum_data\\wallets", "\\electrum_data\\wallets\\" ] }, { names: ["Edge", "edge", "msedge", "MicrosoftEdge"], strings: [ "2F1A6504-0641-44CF-8BB5-3612D865F2E5", /* —> */ "Windows Secure Note", "3CCD5499-87A8-4B10-A215-608888DD3B55", /* —> */ "Windows Web Password Credential", "154E23D0-C644-4E6F-8CE6-5069272F999F", /* —> */ "Windows Credential Picker Protector", "4BF4C442-9B8A-41A0-B380-DD4A704DDB28", /* —> */ "Web Credentials", "77BC582B-F0A6-4E15-4E80-61736B6F3B29", /* —> */ "Windows Credentials", "E69D7838-91B5-4FC9-89D5-230D4D4CC2BC", /* —> */ "Windows Domain Certificate Credential", "3E0E35BE-1B77-43E7-B873-AED901B6275B", /* —> */ "Windows Domain Password Credential", "3C886FF3-2669-4AA2-A8FB-3F6759A77548", /* —> */ "Windows Extended Credential", "pResourceElement", "pIdentityElement", /* —> */ "pAuthenticatorElement", "\\Microsoft\\Edge\\User Data\\Default\\Cookies", "\\Microsoft\\Edge\\User Data" ] }, { names: ["Discord", "discord"], strings: [ "\\Discord\\Local Storage\\leveldb\\", "\\discord\\Local Storage\\leveldb\\", "\\discordcanary\\Local Storage\\leveldb\\", "https://discordapp.com/api/v6/users/@me", "https://discordapp.com/api/v7/users/@me", "https://discordapp.com/api/v8/users/@me", "https://cdn.discordapp.com/avatars/", "\\Discord", "Discord Canary\\leveldb", "Discord PTB\\Local Storage\\leveldb", "Discord\\Local Storage\\leveldb" ] }, { names: ["DashCore", "Dashcore", "dashcore"], strings: [ "Dash-Qt", "\\Wallets\\DashCore\\", "\\Wallets\\DashCore", "Wallets\\DashCore\\", "Wallets\\DashCore", "\\DashCore\\wallet.dat" ] }, { names: ["InternetExplorer", "IE7", "IE78", "CIEPasswords", "CIE7Passwords", "CIEPasswords"], strings: [ "Software\\Microsoft\\Internet Explorer\\IntelliForms\\Storage1", "Software\\Microsoft\\Internet Explorer\\IntelliForms\\Storage2", "\\AppData\\Roaming\\Microsoft\\Windows\\Cookies", "abe2869f-9b47-4cd9-a358-c22904dba7f7\0", "{5E7E8100-9138-11D1-945A-00C04FC308FF}", "Software\\Microsoft\\FTP\\Accounts", "pstorec.dll" ] }, { names: ["MSN", "GetMSN", "getMSN75Passwords", "CMSNMessengerPasswords"], strings: [ "\\MSN Messenger\\msidcrl.dll", "msidcrl.dll", "Software\\Microsoft\\MessengerService", "Software\\Microsoft\\MSNMessenger", "82BD0E67-9FEA-4748-8672-D5EFE5B779B0\0", "msidcrl.dll", "WindowsLive:name=*", "\\My Received Files\\", "\\My Received Files" ] }, { names: ["Skype", "skype", "GetSkype"], strings: [ "\\Skype\\", "\\Skype" ] }, { names: ["IMVU", "GetIMVU", "GetImvu", "Getimvu", "getimvu", "GETIMVU"], strings: [ "HKEY_CURRENT_USER\\Software\\IMVU\\username\\", "HKEY_CURRENT_USER\\Software\\IMVU\\password\\" ] }, { names: ["Paltalk", "paltalk", "GetPaltalk"], strings: [ "Software\\Paltalk", "SOFTWARE\\Paltalk", "HKEY_CURRENT_USER\\Software\\Paltalk\\", "HKEY_CURRENT_USER\\SOFTWARE\\Paltalk\\" ] }, { names: ["Chromium", "chromium", "chrome", "GetChrome"], strings: [ "\\Google\\Chrome\\User Data\\Local State", "\\cookies.db", "\\..\\..\\Local State", "\\Google\\Chrome\\User Data\\Default\\Web Data", "\\Google\\Chrome\\User Data\\default\\Login Data", "encrypted_key", "credit_cards", "card_number_encrypted", "LocalPrefs.json", "password_value" ] }, { names: ["Opera", "opera", "GetOpera"], strings: [ "\\Opera Software\\Opera Stable\\Cookies", "\\Opera Software\\Opera GX Stable\\Cookies", "\\Cookies", "\\Opera\\Opera\\wand.dat", "\\Opera\\Opera\\profile\\wand.dat", "wand.dat" ] }, { names: ["Bytecoin", "ByteCoin", "bytecoin"], strings: [ "\\bytecoin", "\\Bytecoin", "\\Wallets\\Bytecoin\\", "\\Wallets\\Bytecoin", "Wallets\\Bytecoin\\", "Wallets\\Bytecoin", ".wallet", "\\Bytecoin\\", ] }, { names: ["BitcoinCore", "Bitcoincore", "BitCoinCore", "bitcoincore"], strings: [ "Bitcoin-Qt", "\\Wallets\\BitcoinCore\\", "\\Wallets\\BitcoinCore", "Wallets\\BitcoinCore\\", "Wallets\\BitcoinCore", "\\BitcoinCore\\wallet.dat", "\\wallet.dat", "wallet.dat", "\\Bitcoin\\wallet.dat" ] }, { names: ["AtomicWallet", "Atomicwallet", "atomicwallet", "AtomicRule"], strings: [ "\\atomic\\Local Storage\\leveldb\\", "\\Wallets\\Atomic\\Local Storage\\leveldb\\", ] }, { names: ["Guarda"], strings: [ "Guarda" ] }, { names: ["MetaMask", "Metamask", "metamask"], strings: [ "\\Wallets\\Metamask", "Metamask_Extension_" ] }, { names: ["Coinomi", "coinomi"], strings: [ "\\Coinomi", "Coinomi" ] }, { names: ["Armory", "armory", "ArmoryWallet", "Armorywallet", "armorywallet", "ArmoryRule"], strings: [ "\\Armory\\", "\\Wallets\\Armory\\" ] }, { names: ["Steam", "steam"], strings: [ "HKEY_LOCAL_MACHINE\\SOFTWARE\\Valve\\Steam", "HKEY_CURRENT_USER\\SOFTWARE\\Valve\\Steam", "SteamPath", "Steampath", "\\Steam.dll", "SteamDecryptDataForThisMachine", "Software\\Valve\\Steam", "SOFTWARE\\Valve\\Steam", "\\config\\SteamAppData.vdf", "\\ClientRegistry.blob", "\\config\\config.vdf", "Steam\\Config\\config.vdf", "\\config\\loginusers.vdf", "\\Config\\SteamAppData.vdf", "Steam\\Config\\SteamAppData.vdf", "\\Steam\\Config\\SteamAppData.vdf", "\\Steam\\config\\SteamAppData.vdf" ] }, { names: ["Trillian", "trillian", "GetTrillian", "Trillian_Decrypt"], strings: [ "\\Trillian\\users\\global\\accounts.ini" ] }, { names: ["CoreFTP", "CoreFtp", "Coreftp", "coreftp", "GetCoreFTP"], strings: [ "HKEY_CURRENT_USER\\Software\\FTPWare\\CoreFTP\\Sites\\4\\Host", "HKEY_CURRENT_USER\\Software\\FTPWare\\CoreFTP\\Sites\\4\\Port", "HKEY_CURRENT_USER\\Software\\FTPWare\\CoreFTP\\Sites\\4\\User", "HKEY_CURRENT_USER\\Software\\FTPWare\\CoreFTP\\Sites\\4\\PW", "HKEY_CURRENT_USER\\Software\\FTPWare\\COREFTP\\Sites\\", "\\CoreFTP\\sites.idx" ] }, { names: ["LastPass", "Lastpass", "lastpass", "GetLastPass"], strings: [ "HKEY_CURRENT_USER\\SOFTWARE\\LastPass", "HKEY_CURRENT_USER\\SOFTWARE\\LastPass", "LoginUsers", "wxlastpasscookie" ] }, { names: ["FlashFXP", "FlashFxp", "Flashfxp", "flashfxp"], strings: [ "\\FlashFXP\\3\\quick.dat", "FlashFXP\\3\\quick.dat", "\\FlashFXP\\4\\quick.dat", "FlashFXP\\4\\quick.dat", "C:\\ProgramData\\FlashFXP\\3\\quick.dat", "C:\\ProgramData\\FlashFXP\\4\\quick.dat" ] }, { names: ["SmartFTP", "Smartftp", "smartftp"], strings: [ "\\SmartFTP\\Client 2.0\\Favorites\\Quick Connect\\", "\\SmartFTP\\Client 2.0\\Favorites\\Quick Connect\\*.xml", ] }, { names: ["Pidgin", "pidgin", "GetPidgin"], strings: [ "\\.purple\\accounts.xml", ".purple\\accounts.xml" ] }, { names: ["NoIP", "NoIp", "GetNoIP"], strings: [ "HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Vitalwerks\\DUC", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Vitalwerks\\DUC" ] }, { names: ["RuneScape"], strings: [ "\\RSBot_Accounts.ini", "RSBot_Accounts.ini" ] }, { names: ["ICQRecover"], strings: [ "\\ICQ", "\\Owner.mdb", "StoredPassword" ] }, { names: ["jDownloader"], strings: [ "\\jDownloader\\config\\database.script", "INSERT INTO CONFIG VALUES('AccountController','" ] }, { names: ["MicrosoftOffice", "GetOffice"], strings: [ // Office 2003 (11.0) "SOFTWARE\\Microsoft\\Office\\11.0\\Registration\\{90110409-6000-11D3-8CFE-0150048383C9}", "SOFTWARE\\Microsoft\\Office\\12.0\\Registration\\{90110409-6000-11D3-8CFE-0150048383C9}", // Office 2007 (12.0) "SOFTWARE\\Microsoft\\Office\\12.0\\Registration\\{90120000-0030-0000-0000-0000000FF1CE}", "SOFTWARE\\Microsoft\\Office\\12.0\\Registration\\{90120000-0011-0000-0000-0000000FF1CE}", "SOFTWARE\\Microsoft\\Office\\12.0\\Registration\\{90120000-0012-0000-0000-0000000FF1CE}", // Office 2010 (14.0) "SOFTWARE\\Microsoft\\Office\\14.0\\Registration\\{90140000-0011-0000-0000-0000000FF1CE}", "SOFTWARE\\Microsoft\\Office\\14.0\\Registration\\{90140000-0012-0000-0000-0000000FF1CE}", "SOFTWARE\\Microsoft\\Office\\14.0\\Registration\\{90140000-003A-0000-0000-0000000FF1CE}", // Office 2013 (15.0) "SOFTWARE\\Microsoft\\Office\\15.0\\Registration\\{90150000-0011-0000-0000-0000000FF1CE}", "SOFTWARE\\Microsoft\\Office\\15.0\\Registration\\{90150000-0012-0000-0000-0000000FF1CE}", "SOFTWARE\\Microsoft\\Office\\15.0\\Registration\\{91150000-0011-0000-0000-0000000FF1CE}", // Office 2016 / 2019 / 2021 (16.0) "SOFTWARE\\Microsoft\\Office\\16.0\\Registration\\{90160000-0011-0000-0000-0000000FF1CE}", "SOFTWARE\\Microsoft\\Office\\16.0\\Registration\\{90160000-0012-0000-0000-0000000FF1CE}", "SOFTWARE\\Microsoft\\Office\\16.0\\Registration\\{91160000-0011-0000-0000-0000000FF1CE}" ] }, { names: ["FTPCommander", "FtpCommander", "Ftpcommander", "ftpcommander"], strings: [ "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\FTP Commander\\UninstallString", "FTP Commander", "Ftplist.txt" ] }, { names: ["PuTTY", "Putty", "putty"], strings: [ "Software\\SimonTatham\\PuTTY\\Sessions", "Software\\SimonTatham\\PuTTY\\Sessions\\", "Software\\SimonTatham\\PuTTY\\SshHostKeys", "SOFTWARE\\SimonTatham\\PuTTY\\Sessions", "SOFTWARE\\SimonTatham\\PuTTY\\Sessions\\", "SOFTWARE\\SimonTatham\\PuTTY\\SshHostKeys", "SshHostKeys" ] }, { names: ["GameKeys", "FuckTheKey", "StealKeys", "GrabKeys"], strings: [ "ergc", "CDKey", "SERIAL", "DiscKey_SCCT", "codkey", "CDKEY", "LicenseNumber", "DigitalProductId", "RegNumber", "SerialNo", "Unlock Code", "ProductKey", "Serial", "SerialNumber", "RegistrationCode", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\Electronic Arts\\Crysis\\ergc", "HKEY_CURRENT_USER\\Software\\Electronic Arts\\Electronic Arts\\Crysis Wars\\ergc", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA GAMES\\Battlefield 2\\ergc", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA GAMES\\Battlefield 2 Special Forces\\ergc", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA GAMES\\Battlefield 2142\\ergc", "HKEY_LOCAL_MACHINE\\SOFTWARE\\CRYTEK\\FARCRY2\\UBI.COM", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Activision\\Call of Duty WAW", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Rockstar Games\\Grand Theft Auto IV", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA GAMES\\Need For Speed Hot Pursuit 2\\ergc", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\Electronic Arts\\Need for Speed ProStreet\\ergc", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA Sports\\FIFA 09\\ergc", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\Electronic Arts\\Need for Speed Carbon", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA GAMES\\Need For Speed Hot Pursuit 2", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA GAMES\\Need for Speed Most Wanted", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\Electronic Arts\\Need for Speed ProStreet", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA GAMES\\Need For Speed Underground", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA Games\\Need for Speed Underground 2", "HKEY_LOCAL_MACHINE\\SOFTWARE\\SEGA\\Medieval II Total War", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Adobe\\Golive\\5.0\\Registration", "HKEY_LOCAL_MACHINE\\SOFTWARE\\ahead\\Installation\\BAK\\Nero 7\\Info", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Ubisoft\\Splinter Cell Pandora Tomorrow", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Activision\\Call of Duty", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Activision\\Call of Duty 2", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Activision\\Call of Duty United Offensive", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Activision\\Call of Duty 4", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Activision\\Call of Duty WAW", "HKEY_LOCAL_MACHINE\\SOFTWARE\\THQ\\Dawn of War", "HKEY_LOCAL_MACHINE\\SOFTWARE\\THQ\\Dawn of War - Dark Crusade", "HKEY_LOCAL_MACHINE\\SOFTWARE\\ACD Systems\\PicaView", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Eugen Systems\\ActOfWa", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Adobe\\Photoshop\\7.0\\Registration", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Elcom\\Advanced PDF Password Recovery\\Registration", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Elcom\\Advanced PDF Password Recovery Pro\\Registration", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Elcom\\Advanced ZIP Password Recovery\\Registration", "HKEY_LOCAL_MACHINE\\Software\\Electronic Arts\\EA GAMES\\Battlefield Vietnam", "HKEY_LOCAL_MACHINE\\Software\\Electronic Arts\\EA GAMES\\Black and White", "HKEY_LOCAL_MACHINE\\Software\\Electronic Arts\\Black and White 2", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA Games\\Boulder Dash Rocks", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA Games\\Burnout Paradise", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Sunflowers\\Anno 1701", "HKEY_LOCAL_MACHINE\\SOFTWARE\\ashampoo\\Ashampoo WinOptimizer Platinum 3", "HKEY_LOCAL_MACHINE\\SOFTWARE\\@stake\\LC5\\Registration", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA GAMES\\Battlefield 1942", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA GAMES\\Battlefield 1942 Secret Weapons of WWII", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA GAMES\\Battlefield 1942 The Road to Rome", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA Games\\Battlefield 2", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA GAMES\\Battlefield 2142", "HKEY_LOCAL_MACHINE\\SOFTWARE\\TechSmith\\Camtasia Studio\\6.0", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Techland\\Chrome", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Codec Tweak Tool", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA GAMES\\Generals", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA GAMES\\Command and Conquer Generals Zero Hour", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Westwood\\Red Alert 2", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Westwood\\Red Alert", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Westwood\\Tiberian Sun", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\Command and Conquer 3", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\Electronic Arts\\Command and Conquer 3", "HKEY_LOCAL_MACHINE\\SOFTWARE\\THQ\\Company of Heroes", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Valve\\Counter-Strike\\Settings", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\Electronic Arts\\Crysis", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Cyberlink\\PowerDVD", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Cyberlink\\PowerBar", "HKEY_LOCAL_MACHINE\\SOFTWARE\\CyberLink\\PowerProducer\\3.0\\UserReg", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Valve\\Day of Defeat\\Settings", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\Electronic Arts\\The Battle for Middle-earth II", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA Games\\The Sims 2", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA Games\\The Sims 2 University", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA Games\\The Sims 2 Nightlife", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA Games\\The Sims 2 Open For Business", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA Games\\The Sims 2 Pets", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA Games\\The Sims 2 Seasons", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA Games\\The Sims 2 Glamour Life Stuff", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA Games\\The Sims 2 Celebration Stuff", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Electronic Arts\\EA Games\\The Sims 2 H M Fashion Stuff", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Valve\\Half-Life\\Settings" ] }, { names: ["Minecraft", "minecraft", "MinecraftLauncher"], strings: [ "\\.minecraft\\launcher_profiles.json", "launcherconfig", ".lunarclient", "TlauncherProfiles.json", "meteor-client", "alts.novo", "paladium-group", "PolyMC", "Badlion Client", "intentlauncher", "cheatbreaker_accounts.json", "launcher_accounts_microsoft_store.json", "\\VimeWorld", "Software\\VimeWorld", "SOFTWARE\\VimeWorld", "\\.vimeworld" ] }, { names: ["Riot"], strings: [ "\\RiotGamesPrivateSettings.yaml", "RiotGamesPrivateSettings.yaml" ] }, { names: ["Roblox"], strings: [ ":SOFTWARE\\Roblox\\RobloxStudioBrowser\\roblox.com -Name .ROBLOSECURITY" ] }, { names: [/* Generic --> It's not subject to strict classification. It can be found in legitimate .NET programs, but the RiskScore threshold will not trigger false positives. */], strings: [ "90059c37-1320-41a4-b58d-2b75a9850d2f", "EnableLUA", "DisableCMD", "DisableTaskMgr", "DisableRegistry", "discordtokenprotector", "*.txt", "*.rdp", "virustotal.com", "avast.com", "totalav.com", "scanguard.com", "totaladblock.com", "pcprotect.com", "mcafee.com", "bitdefender.com", "us.norton.com", "avg.com", "malwarebytes.com", "pandasecurity.com", "avira.com", "norton.com", "eset.com", "zillya.com", "kaspersky.com", "usa.kaspersky.com", "sophos.com", "home.sophos.com", "adaware.com", "bullguard.com", "clamav.net", "drweb.com", "emsisoft.com", "f-secure.com", "zonealarm.com", "trendmicro.com", "ccleaner.com", "keyscrambler", "^(1|3|bc1)[a-zA-Z0-9]{25,39}$", "^(L|M|3)[a-zA-Z0-9]{26,33}$", "^0x[a-fA-F0-9]{40}$", "^4[0-9AB][1-9A-HJ-NP-Za-km-z]{93}$", "^[1-9A-HJ-NP-Za-km-z]{32,44}$", "^X[1-9A-HJ-NP-Za-km-z]{33}$", "^r[0-9a-zA-Z]{24,34}$", "^T[1-9A-HJ-NP-Za-km-z]{33}$", "^(bitcoincash:)?(q|p)[a-z0-9]{41}$", "\\(?.*?)\\", "^3[47][0-9]{13}$", "^(6541|6556)[0-9]{12}$", "^389[0-9]{11}$", "^3(?:0[0-5]|[68][0-9])[0-9]{11}$", "6(?:011|5[0-9]{2})[0-9]{12}$", "^63[7-9][0-9]{13}$", "^(?:2131|1800|35\\\\d{3})\\\\d{11}$", "^(6304|6706|6709|6771)[0-9]{12,15}$", "^(5018|5020|5038|6304|6759|6761|6763)[0-9]{8,15}$", "5[1-5][0-9]{14}$", "^(6334|6767)[0-9]{12}|(6334|6767)[0-9]{14}|(6334|6767)[0-9]{15}$", "^(62[0-9]{14,17})$", "4[0-9]{12}(?:[0-9]{3})?$", "^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14})$", "3[47][0-9]{13}$", "\r\nPassword: ", "\r\nPassword : ", "Password : ", "Password: ", "PASSWORD: ", "PASSWORD : ", "Software\\Microsoft\\Active Setup\\Installed Components\\{89850105-ECBD-11cf-8B85-00AA005B4340}", "SOFTWARE\\Microsoft\\Active Setup\\Installed Components\\{89850105-ECBD-11cf-8B85-00AA005B4340}", "{89850105-ECBD-11cf-8B85-00AA005B4340}", // Blacklisted computer/user names and GUIDs "7AB5C494-39F5-4941-9163-47F54D6D5016", "032E02B4-0499-05C3-0806-3C0700080009", "03DE0294-0480-05DE-1A06-350700080009", "11111111-2222-3333-4444-555555555555", "6F3CA5EC-BEC9-4A4D-8274-11168F640058", "ADEEEE9E-EF0A-6B84-B14B-B83A54AFC548", "4C4C4544-0050-3710-8058-CAC04F59344A", "00000000-0000-0000-0000-AC1F6BD04972", "49434D53-0200-9065-2500-65902500E439", "49434D53-0200-9036-2500-36902500F022", "5BD24D56-789F-8468-7CDC-CAA7222CC121", "777D84B3-88D1-451C-93E4-D235177420A7", "49434D53-0200-9036-2500-369025000C65", "B1112042-52E8-E25B-3655-6A4F54155DBF", "00000000-0000-0000-0000-AC1F6BD048FE", "EB16924B-FB6D-4FA1-8666-17B91F62FB37", "A15A930C-8251-9645-AF63-E45AD728C20C", "67E595EB-54AC-4FF0-B5E3-3DA7C7B547E3", "C7D23342-A5D4-68A1-59AC-CF40F735B363", "63203342-0EB0-AA1A-4DF5-3FB37DBB0670", "44B94D56-65AB-DC02-86A0-98143A7423BF", "6608003F-ECE4-494E-B07E-1C4615D1D93C", "D9142042-8F51-5EFF-D5F8-EE9AE3D1602A", "49434D53-0200-9036-2500-369025003AF0", "8B4E8278-525C-7343-B825-280AEBCD3BCB", "4D4DDC94-E06C-44F4-95FE-33A1ADA5AC27", "79AF5279-16CF-4094-9758-F88A616D81B4", "FE822042-A70C-D08B-F1D1-C207055A488F", "76122042-C286-FA81-F0A8-514CC507B250", "481E2042-A1AF-D390-CE06-A8F783B1E76A", "F3988356-32F5-4AE1-8D47-FD3B8BAFBD4C", "9961A120-E691-4FFE-B67B-F0E4115D5919", "bee7370c-8c0c-4", "desktop-nakffmt", "win-5e07cos9alr", "b30f0242-1c6a-4", "desktop-vrsqlag", "q9iatrkprh", "xc64zb", "desktop-d019gdm", "desktop-wi8clet", "server1", "lisa-pc", "john-pc", "desktop-b0t93d6", "desktop-1pykp29", "desktop-1y2433r", "wileypc", "work", "6c4e733f-c2d9-4", "ralphs-pc", "desktop-wg3myjs", "desktop-7xc6gez", "desktop-5ov9s0o", "qarzhrdbpj", "oreleepc", "archibaldpc", "julia-pc", "d1bnjkfvlh", "compname_5076", "desktop-vkeons4", "NTT-EFF-2W11WSS", "wdagutilityaccount", "abby", "peter wilson", "hmarc", "patex", "john-pc", "rdhj0cnfevzx", "keecfmwgj", "frank", "8nl0colnq5bq", "lisa", "john", "george", "pxmduopvyx", "8vizsm", "w0fjuovmccp5a", "lmvwjj9b", "pqonjhvwexss", "3u2v9m8", "julia", "heuerzl", "harry johnson", "j.seance", "a.monaldo", "tvm", "outpost" ] }, { names: [/* Generic */], strings: [ // Cryptowallets extension IDs "nkbihfbeogaeaoehlefnkodbefgpgknn", "ejbalbakoplchlghecdalmeeeajnimhm", "fhbohimaelbohpjbbldcngcnapndodjp", "hnfanknocfeofbddgcijnmhnfnkdnaad", "fnjhmkhhmkbjkkabndcnnogagogbneec", "egjidjbpglichdcondbcbdnbeeppgdph", "ojggmchlghnjlapmfbnjholfjkiidbch", "opcgpfmipidbgpenhmajoajpbobppdil", "efbglgofoippbgcjepnhiblaibcnclgk", "ibnejdfjmmkpcnlpebklmnkoeoihofec", "ejjladinnckdgjemekebdpeokbikhfci", "phkbamefinggmakgklpkljjmgibohnba", "ebfidpplhabeedpnhjnobghokpiioolj", "afbcbjpbpfadlkmhmclhkeeodmamcflc", "aeachknmefphepccionboohckonoeemg", "bhghoamapcdpbohphigoooaddinpkbai", "aholpfdialjgjfhomihkjbmgjidlcdno", "bfnaelmomeimhlpmgjnjophhpkkoljpa", "agoakfejjabomempkjlepdflaleeobhb", "mfgccjchihfkkindfppnaooecgfneiii", "lgmpcpglpngdoalbgeoldeajfclnhafa", "bhhhlbepdkbapadjdnnojkbgioiodbic", "jblndlipeogpafnldhgmapagcccfchpi", "kncchdigobghenbbaddojjnnaogfppfj", "ffnbelfdoeiohenkjibnmadjiehjhajb", "hpglfhgfnhbgpjdenjgmdgoeiappafln", "cjelfplplebdjjenllpjcblmjkfcffne", "amkmjjmmflddogmhpjloimipbofnfjih", "fhilaheimglignddkjgofkcbgekhenbh", "nlbmnnijcnlegkjjpcfjclmcfggfefdm", "nanjmdknhkinifnkgdcggcfnhdaammmj", "nkddgncdjgjfcddamfgcmfnlhccnimig", "aiifbnbfobpmeekipheeijimdpnlpgpp", "fnnegphlobjdpkhecapkijjdkgcjhkib", "cgeeodpfagjceefieflmdfphplkenlfk", "pdadjkfkgcafgbceimcpbkalnfnepbnk", "mgffkfbidihjpoaomajlbgchddlicgpn", "aodkkagnadcbobfpggfnjeongemjbjca", "kpfopkelmapcoipemfendmdcghnegimn", "hmeobnfnfcmdkdcmlblgagmfpfboieaf", "lpfcbjknijpeeillifnkikgncikgfhdo", "dngmlblcodfobpdpecaadgfbcggfjfnm", "eigblbgjknlfbajkfhopmcojidlgcehm", "oeljdldpnmdbchonielidgobddffflal", "naepdomgkenhinolocfifgehidddafch", "bmikpgodpkclnkgmnpphehdgcimmided", "jhfjfclepacoldmjmkmdlmganfaalklb", "chgfefjpcobfbnpmiokfjjaglahmnded", "igkpcodhieompeloncfnbekccinhapdb", "admmjipmmciaobhojoghlmleefbicajg", "caljgklbbfbcjjanaijlacgncafpegll", "imloifkgjagghnncjkhggdhalmcnfklk", "cphhlgmgameodnhkjdmkpanlelnlohao", "dmkamcknogkgcdfhhbddcghachkejeap", "fhmfendgdocmcbmfikdcogofphimnkno", "flpiciilemghbmfalicajoolhkkenfel", "hcflpincpppdclinealmandijcmnkbgn", "mnfifefkajgofkcjkemidiaecocnkjeh", "nlgbhdfgdhgbiamfdfmbikcdghidoadd", "ilbbpajmiplgpehdikmejfemfklpkmke", "pknlccmneadmjbkollckpblgaaabameg", "pfknkoocfefiocadajpngdknmkjgakdg", "fcglfhcjfpkgdppjbglknafgfffkelnm", "idkppnahnmmggbmfkjhiakkbkdpnmnon", "mhonjhhcgphdphdjcdoeodfdliikapmj", "bhmlbgebokamljgnceonbncdofmmkedg", "hieplnfojfccegoloniefimmbfjdgcgp", "pidhddgciaponoajdngciiemcflpnnbg", "blbpgcogcoohhngdjafgpoagcilicpjh", "gflpckpfdgcagnbdfafmibcmkadnlhpj", "doljkehcfhidippihgakcihcmnknlphh", "nbokbjkelpmlgflobbohapifnnenbjlh", "apjdnokplgcjkejimjdfjnhmjlbpgkdi", "ieedgmmkpkbiblijbbldefkomatsuahh", "jifanbgejlbcmhbbdbnfbfnlmbomjedj", "dojmlmceifkfgkgeejemfciibjehhdcl", "jpxupxjxheguvfyhfhahqvxvyqthiryh", "pfkcfdjnlfjcmkjnhcbfhfkkoflnhjln", "hbpfjlflhnmkddbjdchbbifhllgmmhnm", "ocmfilhakdbncmojmlbagpkjfbmeinbd", "dbhklojmlkgmpihhdooibnmidfpeaing", "mammpjaaoinfelloncbbpomjcihbkmmc", "khcodhlfkpmhibicdjjblnkgimdepgnd", "bfbdnbpibgndpjfhonkflpkijfapmomn", "gjffdbjndmcafeoehgdldobgjmlepcal", "eidlicjlkaiefdbgmdepmmicpbggmhoj", "bobfejfdlhnabgglompioclndjejolch", "elokfmmmjbadpgdjmgglocapdckdcpkn", "ppdjlkfkedmidmclhakfncpfdmdgmjpm", "cfoajccjibkjhbdjnpkbananbejpkkjb", "flikjlpgnpcjdienoojmgliechmmheek", "gofhklgdnbnpcdigdgkgfobhhghjmmkj", "hppmchachflomkejbhofobganapojjol", "lbfeahdfdkibininjgejjgpdafeopflb", "kgeohlebpjgcfiidfhhdlnnkhefajmca", "inljaljiffkdgmlndjkdiepghpolcpki", "njgnlkhcjgmjfnfahdmfkalpjcneebpl", "gabedfkgnbglfbnplfpjddgfnbibkmbb", "jbdaocneiiinmjbjlgalhcelgbejmnid", "ocefimbphcgjaahbclemolcmkeanoagc", "fihkakfobkmkjojpchpfgcmhfjnmnfpi", "nfinomegcaccbhchhgflladpfbajihdf", "nphplpgoakhhjchkkhmiggakijnkhfnd", "penjlddjkjgpnkllboccdgccekpkcbin", "fldfpgipfncgndfolcbkdeeknbbbnhcc", "pnccjgokhbnggghddhahcnaopgeipafg", "imlcamfeniaidioeflifonfjeeppblda", "ajkifnllfhikkjbjopkhmjoieikeihjb", "kkpllkodjeloidieedojogacfhpaihoh", "kgdijkcfiglijhaglibaidbipiejjfdp", "onhogfjeacnfoofkfgppdlbmlmnplgbn", "fijngjgcjhjmmpcmkeiomlglpeiijkld", "mlbafbjadjidk1bhgopoamemfibcpdfi", "jnlgamecbpmbajjfhmmmlhejkemejdma", "ppbibelpcjmhbdihakflkdcoccbgbkpo", "mcohilncbfahbmgdjkbpemcciiolgcge", "enabgbdfcbaehmbigakijjabdpdnimlg", "fopmedgnkfpebgllppeddmmochcookhc", "khpkpbbcccdmmclmpigdgddabeilkdpd", "lnnnmfcpbkafcpgdilckhmhbkkbpkmid", "kilnpioakcdndlodeeceffgjdpojajlo", "mdjmfdffdcmnoblignmgpommbefadffd", "aijcbedoijmgnlmjeegjaglmepbmpkpi", "ookjlbkiijinhpmnjffcofjonbfbgaoc", "jnmbobjmhlngoefaiojfljckilhhlhcj", "acmacodkjbdgmoleebolmdjonilkdbch" ] }]; var detectedRiskScoreCounter = 0; for (var i = 0; i < signGroupsToCheck.length; i++) { var groupToCheck = signGroupsToCheck[i], isElementFromGroupDetected = false; for (var j = 0; j < groupToCheck.strings.length; j++) { if (PE.isNetUStringPresent(groupToCheck.strings[j])) { isElementFromGroupDetected = true; } } if (!isElementFromGroupDetected) { for (var k = 0; k < groupToCheck.names.length; k++) { if (PE.isNetObjectPresent(groupToCheck.names[k])) { isElementFromGroupDetected = true; } } } if (isElementFromGroupDetected) detectedRiskScoreCounter++; if (detectedRiskScoreCounter >= 9) break; } if (detectedRiskScoreCounter >= 3) { const percentageOfRiskScore = Math.min(65 + Math.round((detectedRiskScoreCounter - 3) * (35 / 6)), 100); verdicts.push({ type: "Stealer", version: String(), details: "RiskScore " + percentageOfRiskScore + "%" + (percentageOfRiskScore < 70 ? " — " + mayBeInfected : String()) }); } else { var signsToCheck = [ "KeyLogger", "Keylogger", "keylogger", "ClipLogger", "Cliplogger", "cliplogger", "Stealer", "stealer", "Grabber", "grabber", "potentiallyVulnerablePasswords", "ScanGeckoBrowsersPaths", "ScannedWallets", "ScanWallets", "GrabPasswords", "Grabpasswords", "grabpasswords", "StealPasswords", "Stealpasswords", "stealpasswords", "ScanSteam", "TGrabber", "DGrabber", "GrabFiles", "Grabfiles", "grabfiles", "StealFiles", "Stealfiles", "stealfiles", "GrabCookies", "Grabcookies", "grabcookies", "grabcookies", "WalletsStealer", "Walletsstealer", "walletsstealer", "WalletsGrabber", "Walletsgrabber", "walletsgrabber" ]; for (var x = 0; x < signsToCheck.length; x++) { if (PE.isNetObjectPresent(signsToCheck[x])) { verdicts.push({ type: "Stealer", version: String(), details: "General signs" }); break; } } } // Clean up: release the dictionary signGroupsToCheck = undefined; } // https://malpedia.caad.fkie.fraunhofer.de/details/win.ramnit if (verdicts.length === 0 && ( PE.isSectionNamePresent(".rmnet") || (PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isI386 && PE.compareEP( "60" + // pushal "E8 00 00 00 00" + // call $+5 "5D" + // pop ebp "8B C5" + // mov eax, ebp "81 ED .. .. 01 20" + // sub ebp, ... "2B 85 .. .. .. .." + // sub eax, dword ptr [ebp + ...] "89 85 .. .. .. .." + // mov dword ptr [ebp + ...], eax "B0 00" // mov al, 0 )))) { verdicts.push({ type: "Ramnit", version: String(), details: "infected" }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.neshta if (verdicts.length === 0 && PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86 && PE.compareEP( "55" + // push ebp "8B EC" + // mov ebp, esp "83 C4 E0" + // add esp, -0x20 "33 C0" + // xor eax, eax "89 45 E0" + // mov dword ptr [ebp - 0x20], eax "89 45 E8" + // mov dword ptr [ebp - 0x18], eax "89 45 E4" + // mov dword ptr [ebp - 0x1c], eax "89 45 EC" + // mov dword ptr [ebp - 0x14], eax "B8 54 80 40 00" + // mov eax, 0x408054 "E8 12 BE FF FF" + // call $+5 - 0x41EE "33 C0" + // xor eax, eax "55" + // push ebp "68 20 82 40 00" + // push 0x408220 "64 FF 30" + // push dword ptr fs:[eax] "64 89 20" + // mov dword ptr fs:[eax], esp "B8 A8 91 40 00" + // mov eax, 0x4091a8 "B9 0B 00 00 00" // mov ecx, 0xb )) { verdicts.push({ type: "Neshta", version: String(), details: "infected" }); } // https://www.securityhome.eu/malware/malware.php?mal_id=17234082845c832fbf144826.79985377 if (verdicts.length === 0 && PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86 && PE.compareEP( "60" + // pushal "E8 00 00 00 00" + // call $+5 "5B 81 EB .. .. .. .." + // sub ebx, ... "83 EC 74" + // sub esp, 0x74 "8B EC" + // mov ebp, esp "8B 83 .. .. .. .." + // mov eax, dword ptr [ebx + ...] "89 45 00" // mov dword ptr [ebp], eax )) { verdicts.push({ type: "Slugin", version: String(), details: "infected" }); } // https://en.wikipedia.org/wiki/CIH_(computer_virus) if (verdicts.length === 0 && PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86 && !PE_Cached.is64bit && PE_Cached.indexOfEntryPointSection !== -1 && PE.compareEP("55")) { // push ebp const instructionsOfWin9xCih = [ "8D 44 24 F8" + // lea eax, [esp - 8] ".. DB", // xor/sub/... ebx, ebx // === "64 87 03", // xchg byte ptr fs:[ebx], eax // === "E8 00 00 00 00", // call $+5 // === "51" + // push ecx "50" // push eax ]; var offsetSearchFrom = PE.VAToOffset(PE_Cached.addressOfUnmanagedEntryPoint), entryPointSectionEndOffset = PE.getSectionFileOffset(PE_Cached.indexOfEntryPointSection) + PE.getSectionFileSize(PE_Cached.indexOfEntryPointSection); for (var i = 0; i < instructionsOfWin9xCih.length; i++) { var instructionSearchSize = Math.min(0x20, entryPointSectionEndOffset - offsetSearchFrom); var foundInstructionPatternAt = instructionSearchSize > 0 ? PE.findSignature(offsetSearchFrom, instructionSearchSize, instructionsOfWin9xCih[i]) : -1; if (foundInstructionPatternAt === -1) { break; } else { offsetSearchFrom = foundInstructionPatternAt + 1; } if (i === instructionsOfWin9xCih.length - 1) { verdicts.push({ type: "Win9X.CIH", version: String(), details: "infected" }); } } } // https://community.avast.com/t/win95-dupator/591940 if (verdicts.length === 0 && ((PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isI386 && PE.compareEP( "60" + // pushal "8B E8" + // mov ebp, eax "81 ED 00 10 40 00" + // sub ebp, 0x401000 "8B 85" // mov eax, dword ptr [ebp + ...] )) || PE.section[PE.nLastSection].Name === "DUPATOR!")) { verdicts.push({ type: "Win9X.Dupator", version: String(), details: "infected" }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.parite if (verdicts.length === 0 && PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86) { var pariteAddress = PE_Cached.addressOfUnmanagedEntryPoint, pariteLoopCount = 0, pariteState = 0, // 0 = initial, 1 = Push/Xor found, 2 = Sub found pariteVisitedAddresses = {}, pariteStackPointerZero = false; const pariteStopOpCodes = ["RET", "RETN", "RETF", "IRET", "IRETD", "IRETQ", "HLT", "UD2", "INT3", "CALL"]; for (var i = 0; i < 364; i++) { if (pariteVisitedAddresses[pariteAddress] || !isVirtualAddressRangeFileBacked(pariteAddress, 1, SECTION_FLAGS_EXECUTE)) break; var currentInstruction = PE.getDisasmString(pariteAddress), currentOpCode = getAsmOpCode(currentInstruction), nextPariteAddress = PE.getDisasmNextAddress(pariteAddress), currentInstructionSize = nextPariteAddress - pariteAddress; if (!currentInstruction || currentInstructionSize < 1 || currentInstructionSize > 15 || !isVirtualAddressRangeFileBacked(pariteAddress, currentInstructionSize, SECTION_FLAGS_EXECUTE)) break; pariteVisitedAddresses[pariteAddress] = true; // Do not classify patterns in physical bytes after a terminal transfer or mandatory fault. if (isEpLinearScanStopped(currentOpCode, pariteAddress, pariteStopOpCodes) || isEpProvenFaultingInstruction(currentInstruction, currentOpCode, pariteStackPointerZero) || /^AAM (?:0X0+|0+)$/i.test(currentInstruction) || (currentOpCode === "JNE" && !isEpNativeNearTransfer(pariteAddress, currentInstruction, currentInstructionSize))) break; pariteStackPointerZero = advanceEpStackPointerZero(currentInstruction, currentOpCode, pariteStackPointerZero); if (currentOpCode === "JMP") { var pariteJumpTarget = currentInstruction.match(/^JMP (0X[0-9A-F]+|\d+)$/i); if (!pariteJumpTarget || !isEpNativeNearTransfer(pariteAddress, currentInstruction, currentInstructionSize)) break; pariteAddress = parseInt(pariteJumpTarget[1], 0); pariteState = 0; continue; } switch (pariteState) { case 0: if (/^(?:PUSH|XOR) DWORD PTR \[E[A-Z]{2} \+ E[A-Z]{2}\]/.test(currentInstruction)) { pariteState = 1; } break; case 1: if (currentOpCode === "SUB" && currentInstruction.match(/^SUB E[A-Z]{2}, [2-4]$/)) { pariteState = 2; } else if (currentOpCode === "JNE" || currentOpCode === "JMP") { pariteState = 0; } break; case 2: if (currentOpCode === "JNE") { var jneTargetMatch = currentInstruction.match(/^JNE 0X([0-9A-F]+)$/); if (jneTargetMatch && parseInt(jneTargetMatch[1], 16) < pariteAddress) { pariteLoopCount++; } pariteState = 0; } else if (currentOpCode === "JMP") { pariteState = 0; } break; } pariteAddress = nextPariteAddress; } if (pariteLoopCount >= 0x20) { verdicts.push({ type: "Parite", version: String(), details: "infected" }); } } // http://www.microsoft.com/security/portal/threat/encyclopedia/entry.aspx?Name=Virus%3AWin32%2FPolip.A if (verdicts.length === 0 && PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86 && !PE_Cached.is64bit && PE_Cached.numberOfSections >= 2 && ( getAsmOpCode(PE_Cached.firstEpAsmInstruction) === "PUSH" || getAsmOpCode(PE_Cached.firstEpAsmInstruction) === "CALL" )) { for (var x = 1; x < PE_Cached.numberOfSections; x++) { if (PE.getSectionName(x) === String()) { var currentSection = PE.section[x]; if (currentSection.IsReadWriteExecute && PE_Cached.indexOfEntryPointSection !== x && PE_Cached.indexOfResourceSection !== x && PE_Cached.indexOfImportsSection !== x && PE_Cached.indexOfExportsSection !== x && PE_Cached.indexOfRelocsSection !== x && PE_Cached.indexOfTlsSection !== x && PE.calculateEntropy(currentSection.FileOffset, currentSection.FileSize) > 7.25 && PE.isSignatureInSectionPresent(x, "55" + // push ebp "8B EC" + // mov ebp, esp "83 EC .." + // sub esp, ... "60" // pushal )) { verdicts.push({ type: "Polip", version: String(), details: "infected" }); break; } } } } // https://web.archive.org/web/20260611044606/https://blog.sekoia.io/mars-a-red-hot-information-stealer/ if (verdicts.length === 0 && PE.isSectionNamePresent("LLCPPC")) { verdicts.push({ type: "Mars Stealer", version: String(), details: String() }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.wannacryptor if (verdicts.length === 0 && PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86 && PE.compareEP( "55" + // push ebp "8B EC" // mov ebp, esp )) { var isWannaCryDetected = false; if (PE_Cached.numberOfUnmanagedResources > 0 && ( /^[RW]$/.test(PE.getResourceNameByNumber(0)) || // "R" or "W" /^[A-Z]{3}$/.test(PE.getResourceNameByNumber(0)) // like "XIA" )) { if (PE.isSignaturePresent(PE.resource[0].FileOffset, Math.min(PE.resource[0].FileSize, 32), "'MZ' 90") && // Embedded PE PE.isSignaturePresent(PE.resource[0].FileOffset, PE.resource[0].FileSize, "%% '.wnry'")) { isWannaCryDetected = true; } else { // Embedded ZIP and other modified variants var rsrcSection = PE.section[".rsrc"], dataSection = PE.section[".data"]; const cmdSignature = convertStringToUnicodeSignature("%s\\ProgramData") + "00 00 00 00 00 'cmd.exe /c ' 22 '%s' 22"; if ((rsrcSection && PE.isSignaturePresent(rsrcSection.FileOffset, rsrcSection.FileSize, cmdSignature)) || (dataSection && PE.isSignaturePresent(dataSection.FileOffset, dataSection.FileSize, cmdSignature))) { isWannaCryDetected = true; } } if (isWannaCryDetected) { verdicts.push({ type: "WannaCry", version: String(), details: String() }); } } } // https://citec.us/forum/showthread.php/15998-ConsoleDevil if (verdicts.length === 0 && PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86 && PE.compareEP( "68 .. .. .. .." + // push [...] "68 00 00 00 00" + // push 0 "68 .. .. .. .." + // push [...] "E8 .. .. .. .." + // call [...] "83 C4 0C" + // add esp, 0xc "68 00 00 00 00" // push 0 )) { var consoleDevilVersion = String(); if (PE.isImportPositionHashPresent(0, 0xb27ceda0) && PE.isImportPositionHashPresent(1, 0xa77d975c)) { consoleDevilVersion = "1.0"; } else if (PE.isImportPositionHashPresent(0, 0x1dec6c44) && PE.isImportPositionHashPresent(1, 0xf1ed6358)) { consoleDevilVersion = "1.2"; } if (consoleDevilVersion) { verdicts.push({ type: "ConsoleDevil RAT", version: consoleDevilVersion, details: String() }); } } // https://malpedia.caad.fkie.fraunhofer.de/details/win.poison_ivy if (verdicts.length === 0 && PE_Cached.isEntryPointAnalysisAllowed && PE_Cached.isArchX86 && PE.compareEP( "B8 00 04 40 00" + // mov eax, 0x400400 "FF D0" + // call eax "6A 00" // push 0 ) && PE.isImportPositionHashPresent(0, 0xbd365a2a)) { verdicts.push({ type: "Poison Lvy RAT", version: String(), details: String() }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.ghost_rat if (verdicts.length === 0 && PE.isExportFunctionPresent("Ip")) { verdicts.push({ type: "Gh0st RAT", version: String(), details: String() }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.darkcomet if (verdicts.length === 0 && ( (PE.isImportPositionHashPresent(0, 0xf375ee03) && PE.isImportPositionHashPresent(1, 0x6043ad68) && PE.isImportPositionHashPresent(2, 0x49e8b6dd)) || // UPX packed (PE.isImportPositionHashPresent(0, 0x174efb84) && PE.isImportPositionHashPresent(1, 0x4afe3a51) && PE.isImportPositionHashPresent(2, 0x3d9a43d0)) // MPRESS packed )) { verdicts.push({ type: "DarkComet RAT", version: "5.3", details: String() }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.bit_rat if (verdicts.length === 0 && PE_Cached.numberOfUnmanagedImports === 1) { var isBitRatDetected = false, isBitRatTorStubVersion = false; if (PE.isImportPositionHashPresent(0, 0x202c6668)) { isBitRatDetected = true; } else if (PE.isImportPositionHashPresent(0, 0x249e9ed3)) { isBitRatDetected = true; isBitRatTorStubVersion = true; } if (isBitRatDetected) { verdicts.push({ type: "BitRAT", version: String(), details: isBitRatTorStubVersion ? "Tor" : String() }); } } if (verdicts.length === 0 && (PE.isImportPositionHashPresent(0, 0x991f7f9a) && PE.isImportPositionHashPresent(1, 0x3495fca1)) || (PE.isImportPositionHashPresent(0, 0x7d78c0f9) && PE.isImportPositionHashPresent(1, 0x4b76cb0f))) { verdicts.push({ type: "Furax RAT", version: String(), details: String() }); } // https://malpedia.caad.fkie.fraunhofer.de/details/ps1.octopus if (verdicts.length === 0 && (PE.isImportPositionHashPresent(0, 0x42600eef) && PE.isSignatureInSectionPresent(0, "'Octopus'"))) { verdicts.push({ type: "Octopus RAT", version: String(), details: String() }); } if (verdicts.length === 0 && (PE_Cached.numberOfUnmanagedImports === 1 && PE.isImportPositionHashPresent(0, 0x06a04d16)) || (PE_Cached.numberOfUnmanagedImports > 4 && PE.isImportPositionHashPresent(0, 0xa719918b) && PE.isImportPositionHashPresent(-1, 0x9f8ccfc6)) ) { verdicts.push({ type: "Biorante", version: "1.X", details: String() }); } if (verdicts.length === 0 && PE.isImportPositionHashPresent(0, 0xb3318086) && // UPX packed PE.isImportPositionHashPresent(1, 0x3d9a43d0) && PE.isImportPositionHashPresent(7, 0xf3b2674d)) { verdicts.push({ type: "Smart RAT", version: "1.0.X", details: String() }); } if (verdicts.length === 0 && PE_Cached.numberOfUnmanagedImports === 1 && PE.isImportPositionHashPresent(0, 0x1b62aa20)) { verdicts.push({ type: "Ace RAT", version: "1.X", details: String() }); } if (verdicts.length === 0 && PE_Cached.numberOfUnmanagedImports === 1 && PE_Cached.isRichSignaturePresent && PE.isImportPositionHashPresent(0, 0x2d6f7e88)) { verdicts.push({ type: "Aquates RAT", version: "1.0", details: String() }); } if (verdicts.length === 0 && PE.section[0].Name === "\uFFFD\uFFFD50") { verdicts.push({ type: "Exception RAT", version: "1.0", details: String() }); } // https://habr.com/ru/companies/mws/articles/350974/ if (verdicts.length === 0 && PE.section["LineRecs"]) { verdicts.push({ type: "Slingshot APT", version: String(), details: String() }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.olympic_destroyer if (verdicts.length === 0 && PE_Cached.numberOfUnmanagedResources > 0) { if (PE.compare("C1 7E 20 A2 DA F9 56 45 61 39 EB DB 51 9D A7 4F", PE.resource[0].FileOffset)) { verdicts.push({ type: "Olympic Destroyer", version: String(), details: String() }); } } // https://en.wikipedia.org/wiki/Equation_Group if (verdicts.length === 0 && PE_Cached.numberOfUnmanagedResources > 0) { for (var i = 0; i < PE_Cached.numberOfUnmanagedResources; i++) { if (PE.resource[i].Name === "BINRES" && PE.compare("4D 5A 90", PE.resource[i].FileOffset)) { verdicts.push({ type: "Equation Group", version: String(), details: String() }); break; } } } // https://malpedia.caad.fkie.fraunhofer.de/details/win.bozok if (verdicts.length === 0 && !PE_Cached.isRichSignaturePresent && PE.isImportPositionHashPresent(0, 0xbd365a2a)) { // kernel32.dll->ExitProcess var bozokRatVersion = String(); if (PE.isImportPositionHashPresent(1, 0x0ed01f83)) { bozokRatVersion = "1.0"; } else if (PE.isImportPositionHashPresent(1, 0xefd4be65)) { bozokRatVersion = "1.2-1.3"; } else if (PE.isImportPositionHashPresent(1, 0x58133a50)) { bozokRatVersion = "1.4-1.5.X"; } if (bozokRatVersion) { verdicts.push({ type: "Bozok RAT", version: bozokRatVersion, details: String() }); } } if (verdicts.length === 0 && !PE_Cached.isRichSignaturePresent) { // VB5 var aquaRatVersion = String(); if (PE.isImportPositionHashPresent(0, 0xfc2095af)) { aquaRatVersion = "0.0.1"; } else if (PE.isImportPositionHashPresent(0, 0x8bc8445e)) { aquaRatVersion = "0.2"; } if (aquaRatVersion && PE.isSignatureInSectionPresent(0, "'Aqua'")) { verdicts.push({ type: "Aqua RAT", version: aquaRatVersion, details: String() }); } } if (verdicts.length === 0 && (PE.isImportPositionHashPresent(0, 0x90648ec6) && PE.isImportPositionHashPresent(1, 0x199ccb02) && PE.isImportPositionHashPresent(2, 0x4bb25d37)) || (PE.isImportPositionHashPresent(0, 0x41b27a00) && PE.isImportPositionHashPresent(1, 0x56a9aa44) && PE.isImportPositionHashPresent(2, 0x4bb25d37)) || (PE.isImportPositionHashPresent(0, 0xc649d485) && PE.isImportPositionHashPresent(1, 0xf2e69b15) && PE.isImportPositionHashPresent(2, 0x4bb25d37)) || (PE.isImportPositionHashPresent(0, 0xe27edc0d) && PE.isImportPositionHashPresent(1, 0x1da5760e) && PE.isImportPositionHashPresent(2, 0x42779d34)) || // UPX packed (PE.isImportPositionHashPresent(0, 0x3d9a43d0) && PE.isImportPositionHashPresent(1, 0xc28f4fe3) && PE.isImportPositionHashPresent(2, 0x5d1bddcc)) || (PE.isImportPositionHashPresent(0, 0xf375ee03) && PE.isImportPositionHashPresent(1, 0x3d9a43d0) && PE.isImportPositionHashPresent(2, 0xc28f4fe3) && (PE.isImportPositionHashPresent(3, 0x5d1bddcc) || PE.isImportPositionHashPresent(3, 0xa2163984))) ) { verdicts.push({ type: "Babylon RAT", version: String(), details: String() }); } if (verdicts.length === 0 && PE.isImportPositionHashPresent(0, 0x519fb9a6) && PE.isImportPositionHashPresent(1, 0x69561fa8) && PE.isImportPositionHashPresent(2, 0xe6aa7d45)) { verdicts.push({ type: "Sinique RAT", version: "1.0", details: String() }); } // https://malpedia.caad.fkie.fraunhofer.de/details/win.netwire if (verdicts.length === 0 && PE.isImportPositionHashPresent(1, 0x5d1bddcc)) { var netWireVersion = String(); if (PE.isImportPositionHashPresent(0, 0xfa0dd025)) { netWireVersion = "1.2.X"; } else if (PE.isImportPositionHashPresent(0, 0x7845f465)) { netWireVersion = "1.4"; } if (netWireVersion) { verdicts.push({ type: "NetWire RAT", version: netWireVersion, details: String() }); } } // https://malpedia.caad.fkie.fraunhofer.de/details/win.remcos if (verdicts.length === 0 && PE.isImportPositionHashPresent(0, 0xf25ba92f) && PE.isImportPositionHashPresent(1, 0xf740ae08) && PE.isImportPositionHashPresent(2, 0xba3a1112)) { verdicts.push({ type: "Remcos RAT", version: String(), details: String() }); } // https://web.archive.org/web/20260108153947/https://indetectables.net/viewtopic.php?t=20716 if (verdicts.length === 0 && !PE_Cached.isRichSignaturePresent && PE_Cached.numberOfUnmanagedImports === 20 && PE.isImportPositionHashPresent(9, 0x5206612e) && PE.isImportPositionHashPresent(16, 0xf3b2674d) && PE.isImportPositionHashPresent(19, 0xabde73fb)) { verdicts.push({ type: "Daleth RAT", version: "1.0", details: String() }); } // https://web.archive.org/web/20090902161812/http://synsecurity.net/index.php?page=downloads if (verdicts.length === 0 && !PE_Cached.isRichSignaturePresent && PE_Cached.numberOfUnmanagedImports === 31 && PE.isImportPositionHashPresent(0, 0xcf603a7d) && PE.isImportPositionHashPresent(-1, 0x0201b2be) && PE.isImportPositionHashPresent(-1, 0x3ca208f6)) { verdicts.push({ type: "Syndrome RAT", version: "4.3.X", details: String() }); } // https://web.archive.org/web/20251224000209/https://sumhex151.github.io/ (decryptor) if (verdicts.length === 0 && !PE_Cached.isRichSignaturePresent && PE_Cached.isTlsPresent && ( (PE.isImportPositionHashPresent(18, 0xc3df3568) && PE.isSignatureInSectionPresent(0, "'lock_text'00")) || (PE.isImportPositionHashPresent(4, 0xc3df3568) && PE.isImportPositionHashPresent(5, 0x622ae906)) // UPX packed )) { verdicts.push({ type: "Amp WinLocker", version: "Sumhex Edition", details: String() }); } // https://max[.]ru/ (Malicious site; Be careful!) if (verdicts.length === 0 && ( PE.getVersionStringInfo("ProductName") === "Max" || PE.getVersionStringInfo("CompanyName") === "Communication Platform LLC" )) { verdicts.push({ type: "MAX Spyware", version: String(), details: String() }); } // Many not-so-smart virus writers use base64 to pack // or hide malicious code, but do not realize that this // is very easily detected by heuristic analysis. var isBase64Payload = false; var base64Version = String(); const base64PeHeaderAnchorDensityWindowCandidates = 64; const minBase64PeHeaderAverageAnchorDistance = 0x100; const maxBase64PeHeaderAnchorCandidates = 4096; const base64PeHeaderPostfixSigns = [ "oAAA", // ~[00 00 00 00 00] "qQAA", // ~[90 00 03] "pQAA", // ~[50 00 02] "p4AA" // ~[78 00 01] ]; const base64PeHeaderAnchorPatterns = ["TV", convertStringToUnicodeSignature("TV")], base64PeHeaderPostfixPatterns = [[], []], base64PeHeaderCompletePatterns = [[], []]; for (var i = 0; i < base64PeHeaderPostfixSigns.length; i++) { base64PeHeaderPostfixPatterns[0].push("'" + base64PeHeaderPostfixSigns[i] + "'"); base64PeHeaderPostfixPatterns[1].push(convertStringToUnicodeSignature(base64PeHeaderPostfixSigns[i])); base64PeHeaderCompletePatterns[0].push("TV" + base64PeHeaderPostfixSigns[i]); base64PeHeaderCompletePatterns[1].push(convertStringToUnicodeSignature("TV" + base64PeHeaderPostfixSigns[i])); } // Resources can alias an already scanned section. Only complete containment is reused, // so partially overlapping and malformed ranges retain their original independent scan. var scannedBase64Areas = []; function isBase64PeHeaderPresent(areaOffset, areaSize, isUnicode) { var patternType = isUnicode ? 1 : 0, anchorPattern = base64PeHeaderAnchorPatterns[patternType], postfixOffsetDelta = isUnicode ? 4 : 2, completePatternSize = isUnicode ? 12 : 6, currentOffset = areaOffset, remainingSize = areaSize, candidateCount = 0, densityWindowOffset = areaOffset; var isDensityWindowComplete = false, isAnchorDensityHigh = false; while (remainingSize >= completePatternSize) { var anchorOffset = isUnicode ? PE.findSignature(currentOffset, remainingSize, anchorPattern) : PE.findString(currentOffset, remainingSize, anchorPattern); if (anchorOffset === -1) break; if (anchorOffset + completePatternSize <= areaOffset + areaSize) { for (var i = 0; i < base64PeHeaderPostfixSigns.length; i++) { if (PE.compare(base64PeHeaderPostfixPatterns[patternType][i], anchorOffset + postfixOffsetDelta)) return true; } } var advanced = (anchorOffset - currentOffset) + postfixOffsetDelta; currentOffset += advanced; remainingSize -= advanced; candidateCount++; // Sparse anchors are cheaper to validate individually. If a local window // becomes dense, let the native search routine scan the remaining area // for the four complete, selective patterns instead. isDensityWindowComplete = candidateCount % base64PeHeaderAnchorDensityWindowCandidates === 0; isAnchorDensityHigh = isDensityWindowComplete && currentOffset - densityWindowOffset < base64PeHeaderAnchorDensityWindowCandidates * minBase64PeHeaderAverageAnchorDistance; if (isAnchorDensityHigh || candidateCount === maxBase64PeHeaderAnchorCandidates) { for (var i = 0; i < base64PeHeaderPostfixSigns.length; i++) { var completePattern = base64PeHeaderCompletePatterns[patternType][i]; if ((isUnicode ? PE.findSignature(currentOffset, remainingSize, completePattern) : PE.findString(currentOffset, remainingSize, completePattern)) !== -1) return true; } break; } if (isDensityWindowComplete) densityWindowOffset = currentOffset; } return false; } function scanArea(areaOffset, areaSize) { if (areaSize <= 0 || areaOffset <= 0 || isBase64Payload) return; var areaEnd = areaOffset + areaSize; var isTrackableArea = areaEnd > areaOffset && areaEnd <= PE_Cached.fileSize; if (isTrackableArea) { for (var i = 0; i < scannedBase64Areas.length; i++) { if (areaOffset >= scannedBase64Areas[i].Offset && areaEnd <= scannedBase64Areas[i].End) return; } scannedBase64Areas.push({ Offset: areaOffset, End: areaEnd }); } if (isBase64PeHeaderPresent(areaOffset, areaSize, false)) { base64Version = "ASCII"; isBase64Payload = true; return; } if (isBase64PeHeaderPresent(areaOffset, areaSize, true)) { base64Version = "UTF-16LE"; isBase64Payload = true; } } if (numberOfPackerResults === 0 || numberOfPackerResults > 1 || numberOfProtectorResults > 0) { for (var i = 0; i < PE_Cached.numberOfSections && !isBase64Payload; i++) { var sectionOffset = PE.getSectionFileOffset(i), sectionSize = PE.getSectionFileSize(i), sectionName = PE.getSectionName(i); // Optimizations: Skip known sections that are unlikely to contain encoded payloads if (PE_Cached.numberOfSections > 1) { if (sectionOffset === 0 || sectionSize < 0x2000) { continue; } var isBase64PrimaryNativeCodeSection = !PE_Cached.isDotNet && i === PE_Cached.indexOfEntryPointSection && sectionName === ".text" && i === 0; var isBase64DedicatedResourceSection = i === PE_Cached.indexOfResourceSection && sectionName === ".rsrc" && i !== PE_Cached.indexOfImportsSection && i !== PE_Cached.indexOfEntryPointSection && i !== PE_Cached.indexOfTlsSection && i !== PE_Cached.indexOfRelocsSection; var isBase64DedicatedImportsSection = i === PE_Cached.indexOfImportsSection && sectionName === ".idata" && i !== PE_Cached.indexOfResourceSection && i !== PE_Cached.indexOfEntryPointSection && i !== PE_Cached.indexOfTlsSection && i !== PE_Cached.indexOfRelocsSection; var isBase64DedicatedRelocsSection = i === PE_Cached.indexOfRelocsSection && sectionName === ".reloc" && i !== PE_Cached.indexOfResourceSection && i !== PE_Cached.indexOfEntryPointSection && i !== PE_Cached.indexOfTlsSection && i !== PE_Cached.indexOfImportsSection; if (isBase64PrimaryNativeCodeSection || isBase64DedicatedResourceSection || isBase64DedicatedImportsSection || isBase64DedicatedRelocsSection) { continue; } } scanArea(sectionOffset, sectionSize); } } if (!isBase64Payload && PE_Cached.numberOfUnmanagedResources > 0) { for (var r = 0; r < PE_Cached.numberOfUnmanagedResources && !isBase64Payload; r++) { var resourceOffset = PE.getResourceOffsetByNumber(r), resourceSize = PE.getResourceSizeByNumber(r); // Target actual payloads (> 4 KB) and skip bitmaps if (resourceOffset > 0 && resourceSize >= 0x1000 && !PE.compare("28 00 00 00 ?? ?? 00 00 ?? ?? 00 00 01 00 ?? 00 00 00 00 00", resourceOffset)) { scanArea(resourceOffset, resourceSize); } } } if (!isBase64Payload) { scanArea(PE_Cached.overlayOffset, PE_Cached.overlaySize); } // A single detected packer normally suppresses the section pass. Its entry-point section // may still contain a Base64 PE prefix, so inspect only that one representative range. var base64FallbackSectionIndex = PE_Cached.indexOfEntryPointSection; if (!isBase64Payload && numberOfPackerResults === 1 && numberOfProtectorResults === 0 && base64FallbackSectionIndex !== -1) { scanArea(PE.getSectionFileOffset(base64FallbackSectionIndex), PE.getSectionFileSize(base64FallbackSectionIndex)); } if (isBase64Payload) { verdicts.push({ type: "Encoded executable payload", version: "Base64".append(base64Version), details: mayBeInfected }); } // Detect encrypted PE files in resources, sections AND overlay via KPA (Known Plaintext Attack) // Supports MULTIPLE algorithms (XOR, XNOR, ADD, SUB, SUB-REV) and key lengths up to 20 bytes var isEncPePresent = false; var detectedAlgo = String(); var encryptedPeScanCache = {}; function scanEncryptedPeArea(areaOffset, areaSize) { if (areaOffset < 0 || areaOffset >= PE_Cached.fileSize || !(areaSize > 0)) return String(); // Declared section/resource sizes may outlive their physical data after truncation. areaSize = Math.min(areaSize, PE_Cached.fileSize - areaOffset); const cacheKey = areaOffset + ":" + areaSize; if (encryptedPeScanCache[cacheKey] !== undefined) return encryptedPeScanCache[cacheKey]; var algorithm = String(); // Check if native function is present if (typeof PE !== "undefined" && typeof PE.scanBufferForEncryptedPe === "function") { algorithm = PE.scanBufferForEncryptedPe(areaOffset, areaSize); } else if (typeof Binary !== "undefined" && typeof Binary.scanBufferForEncryptedPe === "function") { algorithm = Binary.scanBufferForEncryptedPe(areaOffset, areaSize); } else { // Old implementation fallback: read bytes and run JavaScript KPA algorithm = scanBufferForEncryptedPe(PE.readBytes(areaOffset, areaSize), areaSize); } encryptedPeScanCache[cacheKey] = algorithm; return algorithm; } // 1. Scan Resources for (var i = 0; i < PE_Cached.numberOfUnmanagedResources && !isEncPePresent; i++) { var resourceOffset = PE.getResourceOffsetByNumber(i), resourceSize = PE.getResourceSizeByNumber(i); if (resourceOffset > 0 && resourceSize > 0x1000 && !PE.compare("28 00 00 00 ?? ?? 00 00 ?? ?? 00 00 01 00 ?? 00 00 00 00 00", resourceOffset)) { var maxScanSize = Math.min(resourceSize, 0x1000); detectedAlgo = scanEncryptedPeArea(resourceOffset, maxScanSize); if (detectedAlgo) { isEncPePresent = true; break; } } } // 2. Scan Overlay if (!isEncPePresent) { var overlayOffset = PE_Cached.overlayOffset, overlaySize = PE_Cached.overlaySize; if (overlayOffset > 0 && overlaySize > 0x1000 && !PE.isSigned()) { var maxScanSize = Math.min(overlaySize, 0x14000); detectedAlgo = scanEncryptedPeArea(overlayOffset, maxScanSize); if (detectedAlgo) isEncPePresent = true; } } // 3. Scan sections if (!isEncPePresent) { for (var i = 0; i < PE_Cached.numberOfSections && !isEncPePresent; i++) { var sectionOffset = PE.getSectionFileOffset(i), sectionSize = PE.getSectionFileSize(i), sectionName = PE.getSectionName(i); if (PE_Cached.numberOfSections > 1) { if (sectionOffset === 0 || sectionSize < 0x2500) { continue; } var isEncryptedPePrimaryNativeCodeSection = !PE_Cached.isDotNet && i === PE_Cached.indexOfEntryPointSection && sectionName === ".text" && i === 0; var isEncryptedPeDedicatedResourceSection = i === PE_Cached.indexOfResourceSection && sectionName === ".rsrc" && i !== PE_Cached.indexOfImportsSection && i !== PE_Cached.indexOfEntryPointSection && i !== PE_Cached.indexOfTlsSection && i !== PE_Cached.indexOfRelocsSection; var isEncryptedPeDedicatedImportsSection = i === PE_Cached.indexOfImportsSection && sectionName === ".idata" && i !== PE_Cached.indexOfResourceSection && i !== PE_Cached.indexOfEntryPointSection && i !== PE_Cached.indexOfTlsSection && i !== PE_Cached.indexOfRelocsSection; var isEncryptedPeDedicatedRelocsSection = i === PE_Cached.indexOfRelocsSection && sectionName === ".reloc" && i !== PE_Cached.indexOfResourceSection && i !== PE_Cached.indexOfEntryPointSection && i !== PE_Cached.indexOfTlsSection && i !== PE_Cached.indexOfImportsSection; if (isEncryptedPePrimaryNativeCodeSection || isEncryptedPeDedicatedResourceSection || isEncryptedPeDedicatedImportsSection || isEncryptedPeDedicatedRelocsSection) { continue; } } var maxScanSize = (PE_Cached.isDotNet && i === 0) ? Math.min(sectionSize, 0x64000) : Math.min(sectionSize, sectionName.match(/^\.[rex]?data$/i) ? 0x12000 : 0x6000); detectedAlgo = scanEncryptedPeArea(sectionOffset, maxScanSize); if (detectedAlgo) { isEncPePresent = true; break; } } } if (isEncPePresent) { verdicts.push({ type: "Encrypted executable payload", version: "Algo: " + detectedAlgo, details: mayBeInfected }); } // Check for fake copyright info that may indicate a builder was used or the malware is masquerading as a legitimate software. const legalCopyrightInfo = PE.getVersionStringInfo("LegalCopyright"); if (legalCopyrightInfo && !/(?:\(C\)|©|\bcopyright\b)/i.test(legalCopyrightInfo)) { const legalCopyrightInfoLowerCase = legalCopyrightInfo.toLowerCase(); const companyNamesDict = [ // Big Tech — most commonly faked "microsoft", "google", "apple", "amazon", // Software & games "adobe", "oracle", "mozilla", "vmware", "valve", "rockstar", "blizzard", "ubisoft", "roblox", "ea digital", "cd projekt", "epic games", // Messaging & social "discord", "telegram", "whatsapp", // Security vendors (faked to appear "trusted") "kaspersky", "avast", "malwarebytes", "doctor web", "bitdefender", "norton", "mcafee", "eset", "avira", "sophos", // Hardware "intel" ]; for (var i = 0; i < companyNamesDict.length; i++) { if (new RegExp("\\b" + companyNamesDict[i] + "\\b").test(legalCopyrightInfoLowerCase) && legalCopyrightInfo.indexOf("Microsoft.") !== 0) { verdicts.push({ type: "Fake build info", version: String(), details: mayBeInfected }); break; } } } // Some malwares masquerade as critical system files to avoid detection, but they often forget // to change the internal and original file names, which can be easily checked. // Also check if such files are signed and have a rich signature, as legitimate // system files should have these features. const systemSignedFileNamesDict = [ "csrss.exe", "wininit.exe", "lsass.exe", "svchost.exe", "taskhostw.exe", "ntkrnlmp.exe" /* -> (ntoskrnl.exe) */, "RuntimeBroker.exe", "smss.exe", "SecurityHealthService.exe" ]; const systemFileNamesDict = systemSignedFileNamesDict.concat([ "dwm.exe", "winlogon.exe", "services.exe", "spoolsv.exe", "OneDrive.exe", "fontdrvhost.exe", "sihost.exe", "winresume.exe", "SystemSettings.exe", "ShellExperienceHost.exe", "StartMenuExperienceHost.exe", "conhost.exe" ]); const originalFileName = PE.getVersionStringInfo("OriginalFilename"), internalFileName = PE.getVersionStringInfo("InternalName"), originalFileNameLowerCase = originalFileName.toLowerCase(), internalFileNameLowerCase = internalFileName.toLowerCase(), netAssemblyNameLowerCase = PE_Cached.nameOfNetAssemblyName.toLowerCase(), netModuleNameLowerCase = PE_Cached.nameOfNetModuleName.toLowerCase(); var isFakeOrInfected = false; function compareOriginalAndInternalNames(inputName) { if (!inputName) return false; inputName = inputName.toLowerCase(); return ( originalFileNameLowerCase === inputName || internalFileNameLowerCase === inputName ); } for (var j = 0; j < systemSignedFileNamesDict.length && !isFakeOrInfected; j++) { if (compareOriginalAndInternalNames(systemSignedFileNamesDict[j])) { if (!PE.isSigned()) { isFakeOrInfected = true; } } } if (PE_Cached.isDotNet || !PE_Cached.isRichSignaturePresent) { if (numberOfPackerResults > 0 || numberOfProtectorResults > 0) { for (var k = 0; k < systemFileNamesDict.length && !isFakeOrInfected; k++) { if (compareOriginalAndInternalNames(systemFileNamesDict[k])) { isFakeOrInfected = true; } } } } if (isFakeOrInfected) { verdicts.push({ type: "Fake " + (!PE_Cached.isDotNet ? "or infected " : String()) + "system file", version: String(), details: mayBeInfected }); } // Check for obfuscated names and anomalous signs in .NET and native assemblies if (PE_Cached.isDotNet && ( netAssemblyNameLowerCase === "stub" || netModuleNameLowerCase.indexOf("stub.") === 0 || netAssemblyNameLowerCase.indexOf("crypted") !== -1 || netModuleNameLowerCase.indexOf("crypted") !== -1 || netAssemblyNameLowerCase.indexOf("payload") !== -1 || netModuleNameLowerCase.indexOf("payload") !== -1 || isNameObfuscated(PE_Cached.nameOfNetAssemblyName) || isNameObfuscated(PE_Cached.nameOfNetModuleName) ) || originalFileNameLowerCase.indexOf("stub.") === 0 || internalFileNameLowerCase.indexOf("stub.") === 0 || (function () { const fieldsToCheck = [ "Comments", "CompanyName", "FileDescription", "ProductName", "LegalCopyright", "LegalTrademarks", "OriginalFilename", "InternalName" ]; for (var i = 0; i < fieldsToCheck.length; i++) { if (isNameObfuscated(PE.getVersionStringInfo(fieldsToCheck[i]))) { return true; } } return false; })() ) { verdicts.push({ type: "Anomalous build info", version: String(), details: mayBeInfected }); } else if (/(?:^tmp|\.tmp$)/.test(PE_Cached.nameOfNetModuleName)) { verdicts.push({ type: "Generated temporary assembly", version: String(), details: mayBeInfected }); } // Check for multiple packers, cryptors, and protectors, which may indicate the use of a builder or attempts to hide the true nature of the file. var protectionResultsCount = numberOfPackerResults + numberOfCryptorResults + numberOfProtectorResults + numberOfProtectionResults; if (protectionResultsCount > 3) { verdicts.push({ type: "Anomalous build content", version: String(), details: mayBeInfected }); } // Check for anomalous resource names in .NET and native assemblies, which may // indicate the presence of a builder or attempts to hide the true nature of the malware. if (PE.isResourceNamePresentExp(/^(?:STUB|SERVER)(?:\.[A-Z]{3})?$/) || PE.isResourceNamePresentExp(/(?:PAYLOAD|SHELLCODE|INJECT|CRYPTED|DECRYPTOR)/) || // Why virus-makers use such resource names is beyond me, but they do. PE.isResourceNamePresentExp(/^(?:TROJAN|VIRUS|MALWARE|DROPPER)$/)) { verdicts.push({ type: "Anomalous resources", version: String(), details: mayBeInfected }); } for (var v = 0; v < verdicts.length; v++) { var verdict = verdicts[v]; log(logType.any, "Potential malware match: " + verdict.type + (verdict.version ? " " + verdict.version : String())); _setResult("~malware", verdict.type, verdict.version, verdict.details); } } // #endregion /** * Finds a PE image inside a bounded file-backed resource or overlay range. * An MZ prefix alone is insufficient: the DOS pointer, NT signature, optional * header, and complete section table must all fit inside the same range. * * @param {number} rangeOffset - File offset of the containing range. * @param {number} rangeSize - File-backed size of that range. * @param {number} searchSize - Maximum prefix length to search for MZ candidates. * @returns {number} File offset of a verified PE header, or -1. */ function findEmbeddedPeInRange(rangeOffset, rangeSize, searchSize) { if (rangeOffset < 0 || rangeSize < 0x80 || rangeOffset + rangeSize > PE_Cached.fileSize) return -1; var rangeEnd = rangeOffset + rangeSize, searchEnd = rangeOffset + Math.min(rangeSize, searchSize), nextCandidateOffset = rangeOffset; for (var candidateCount = 0; candidateCount < 32 && nextCandidateOffset + 2 <= searchEnd; candidateCount++) { var candidateOffset = PE.findSignature(nextCandidateOffset, searchEnd - nextCandidateOffset, "'MZ'"); if (candidateOffset === -1 || candidateOffset + 0x40 > rangeEnd) break; var ntHeaderRelativeOffset = PE.read_uint32(candidateOffset + 0x3C), ntHeaderOffset = candidateOffset + ntHeaderRelativeOffset; if (ntHeaderRelativeOffset >= 0x40 && ntHeaderRelativeOffset <= 0x4000 && ntHeaderOffset + 0x18 <= rangeEnd && PE.compare("'PE'0000", ntHeaderOffset)) { var numberOfSections = PE.readWord(ntHeaderOffset + 6), optionalHeaderSize = PE.readWord(ntHeaderOffset + 20), optionalHeaderOffset = ntHeaderOffset + 0x18, sectionTableEnd = optionalHeaderOffset + optionalHeaderSize + numberOfSections * 0x28; var isEmbeddedHeaderLayoutValid = numberOfSections > 0 && numberOfSections <= 96 && optionalHeaderSize >= 0x60 && sectionTableEnd <= rangeEnd; var optionalHeaderMagic = isEmbeddedHeaderLayoutValid ? PE.readWord(optionalHeaderOffset) : 0; // PE32+ has a longer fixed Optional Header; section bytes must not stand in for its missing fields. if (isEmbeddedHeaderLayoutValid && (optionalHeaderMagic === 0x10B || optionalHeaderMagic === 0x20B) && optionalHeaderSize >= (optionalHeaderMagic === 0x20B ? 0x70 : 0x60)) { var embeddedSizeOfImage = PE.read_uint32(optionalHeaderOffset + 0x38), embeddedSizeOfHeaders = PE.read_uint32(optionalHeaderOffset + 0x3C); var isEmbeddedSectionDataPresent = false, isEmbeddedSectionLayoutValid = embeddedSizeOfHeaders >= sectionTableEnd - candidateOffset && embeddedSizeOfHeaders <= rangeEnd - candidateOffset && embeddedSizeOfImage >= embeddedSizeOfHeaders; for (var s = 0; s < numberOfSections && isEmbeddedSectionLayoutValid; s++) { var sectionHeaderOffset = optionalHeaderOffset + optionalHeaderSize + s * 0x28, sectionVirtualSize = PE.read_uint32(sectionHeaderOffset + 0x08), sectionVirtualAddress = PE.read_uint32(sectionHeaderOffset + 0x0C), sectionRawSize = PE.read_uint32(sectionHeaderOffset + 0x10), sectionRawOffset = PE.read_uint32(sectionHeaderOffset + 0x14); isEmbeddedSectionLayoutValid = sectionVirtualAddress + Math.max(sectionVirtualSize, sectionRawSize) <= embeddedSizeOfImage; if (sectionRawSize === 0 || !isEmbeddedSectionLayoutValid) continue; isEmbeddedSectionLayoutValid = sectionRawOffset >= sectionTableEnd - candidateOffset && candidateOffset + sectionRawOffset + sectionRawSize <= rangeEnd; isEmbeddedSectionDataPresent = true; } if (isEmbeddedSectionDataPresent && isEmbeddedSectionLayoutValid) return candidateOffset; } } nextCandidateOffset = candidateOffset + 2; } return -1; } // #region "interesting markers (.NET and Native)" /** Reports notable PE header settings and other informational markers. */ function scanForInterestingMarkers_NET_and_Native() { log(logType.nothing, "Scanning for interesting markers..."); var options = String(); var isDetected = Boolean(); const dllCharacteristics = PE.getImageOptionalHeader("DllCharacteristics"); // https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ee417681(v=vs.85)#:~:text=Root%20Element:%20GameDefinitionFile,GameDefinition%20Attributes var isGdfSchemaPresent = false; for (var i = 0; i < PE_Cached.numberOfUnmanagedResources && !isGdfSchemaPresent; i++) { if (PE.getResourceNameByNumber(i) === "__GDF_XML") { const gdfResourceOffset = PE.getResourceOffsetByNumber(i); if (gdfResourceOffset !== -1 && PE.isSignaturePresent( gdfResourceOffset, PE.getResourceSizeByNumber(i), convertStringToUnicodeSignature("GameDefinitionFile"))) { log(logType.any, "Image contains a Windows Game Definition File"); isGdfSchemaPresent = true; } } } if (isGdfSchemaPresent) options = "GDF Schema"; // https://learn.microsoft.com/en-us/windows/win32/secauthz/appcontainer-isolation var isAppContainerImage = false; if (dllCharacteristics & IMAGE_DLLCHARACTERISTICS_APPCONTAINER) { log(logType.any, "Image requests AppContainer isolation"); isAppContainerImage = true; } if (isAppContainerImage) options = addOption(options, "AppContainer Sandboxed Image"); if (options.length !== 0) isDetected = true; if (isDetected) { _setResult("~marker", "Contains", String(), PE_Cached.isVerbose ? options : String()); } } // #endregion // #region "extensions (.NET and Native)" /** Checks the file-system extension against the detected executable format. */ function scanNameForAnomalies_FileSystem_NET_and_Native() { log(logType.nothing, "Checking file system executable extension..."); var options = String(); var isDetected = Boolean(); const executableFileSystemExtension = File.getFileSuffix().toLowerCase(), extTypes = { // Can be launched from Windows Explorer /* EXEC */ "exe": null, /* EXEC */ "com": "MS-DOS Executable", /* EXEC */ "pif": "MS-DOS Executable Link", /* EXEC */ "scr": "Windows Screensaver", /* EXEC */ "bat": "Windows Batch", /* EXEC */ "cmd": "Windows Batch", // System, host-loaded, and resource modules /* HOST */ "dll": null, /* DRVR */ "drv": "Windows Driver (Legacy)", /* DRVR */ "sys": "Windows System Component", /* FWRM */ "efi": "UEFI Component", /* META */ "winmd": "Windows Runtime Metadata", /* HOST */ "ocx": "ActiveX Component", /* HOST */ "cpl": "Control Panel Applet", /* HOST */ "acm": "Audio Compression Manager Driver", /* HOST */ "ax": "DirectShow Filter", /* HOST */ "ime": "Input Method Editor", /* HOST */ "tsp": "TAPI Service Provider", /* RSRC */ "rll": "Resource Library", /* RSRC */ "mui": "Multilingual Resource Module", /* RSRC */ "msstyles": "Windows Visual Style", /* RSRC */ "mun": "Windows Resource Module", /* RSRC */ "dat": "Resource Module", /* RSRC */ "hxs": "Microsoft Help Compiled", // Programming language and third-party modules /* META */ "netmodule": ".NET Module", /* PLUG */ "xll": "Microsoft Excel Add-In", /* PLUG */ "bpl": "Borland Package", /* PLUG */ "dpl": "Delphi Package (Legacy)", /* PLUG */ "pyd": "CPython Module", /* PLUG */ "node": "Node.js Module", /* PLUG */ "mexw32": "MATLAB x86 MEX Module", /* PLUG */ "mexw64": "MATLAB x64 MEX Module", /* PLUG */ "api": "Adobe Acrobat Plug-In", /* PLUG */ "uxpaddon": "Adobe UXP Native Add-On", /* PLUG */ "arx": "AutoCAD ObjectARX Module", /* PLUG */ "dbx": "AutoCAD ObjectDBX Module", /* PLUG */ "mll": "Autodesk Maya Plug-In", /* PLUG */ "rhp": "Rhinoceros Plug-In", /* PLUG */ "gha": "Grasshopper Assembly", /* PLUG */ "asi": "ASI Game Plug-In", /* PLUG */ "vst3": "VST3 Audio Plug-In", /* PLUG */ "clap": "CLAP Audio Plug-In", /* PLUG */ "ofx": "OpenFX Visual Effects Plug-In", /* PLUG */ "wcx": "Total Commander Packer Plug-In", /* PLUG */ "wfx": "Total Commander File System Plug-In", /* PLUG */ "wlx": "Total Commander Lister Plug-In", /* PLUG */ "wdx": "Total Commander Content Plug-In", /* PLUG */ "wcx64": "Total Commander x64 Packer Plug-In", /* PLUG */ "wfx64": "Total Commander x64 File System Plug-In", /* PLUG */ "wlx64": "Total Commander x64 Lister Plug-In", /* PLUG */ "wdx64": "Total Commander x64 Content Plug-In", /* PLUG */ "dlo": "3ds Max Procedural Object Plug-In", /* PLUG */ "dle": "3ds Max Scene Export Plug-In", /* PLUG */ "dli": "3ds Max Scene Import Plug-In", /* PLUG */ "dlm": "3ds Max Modifier Plug-In", /* PLUG */ "dlc": "3ds Max Controller Plug-In", /* PLUG */ "dlt": "3ds Max Materials and Textures Plug-In", /* PLUG */ "bmi": "3ds Max Bitmap Manager I/O Plug-In", /* PLUG */ "dlr": "3ds Max Renderer Plug-In", /* PLUG */ "dlv": "3ds Max Rendering Effects Plug-In", /* PLUG */ "dlu": "3ds Max Utility Plug-In", /* PLUG */ "gup": "3ds Max Global Utility Plug-In", /* PLUG */ "bmf": "3ds Max Bitmap Manager Filter Plug-In", /* PLUG */ "bms": "3ds Max Bitmap Manager Storage Module", /* PLUG */ "dlb": "3ds Max Shader Plug-In", /* PLUG */ "dlf": "3ds Max Font Loader", /* PLUG */ "dlh": "3ds Max Sampler Plug-In", /* PLUG */ "dlk": "3ds Max Filter Kernel", /* PLUG */ "dls": "3ds Max Object Snap Plug-In", /* PLUG */ "dlx": "3ds Max MAXScript Plug-In", /* PLUG */ "flt": "3ds Max Image Filter Plug-In", /* PLUG */ "whl": "Windhawk Tweaker Component", // Malware-analysis and quarantine suffixes /* LABS */ "infected": "Infected File", /* LABS */ "virusscan": "Infected File (VirusScan Lab.)", }, hasExecutableFileSystemExtensionType = Object.prototype.hasOwnProperty.call( extTypes, executableFileSystemExtension ), executableFileSystemExtensionType = hasExecutableFileSystemExtensionType ? extTypes[executableFileSystemExtension] : undefined; // If the file extension is present in the database and differs from vanilla (generic) .exe/.dll var isSpecializedExecutableFileSystemExtension = false; if (hasExecutableFileSystemExtensionType && executableFileSystemExtensionType !== null) { log(logType.any, "Image uses the recognized ." + File.cleanString(executableFileSystemExtension.toUpperCase()) + " extension for " + executableFileSystemExtensionType); isSpecializedExecutableFileSystemExtension = true; } if (isSpecializedExecutableFileSystemExtension) options = "Extension of " + executableFileSystemExtensionType; // Check for missing file extension var isExecutableFileSystemExtensionEmpty = false; if (executableFileSystemExtension === String()) { log(logType.any, "Executable image has no filename extension"); isExecutableFileSystemExtensionEmpty = true; } if (isExecutableFileSystemExtensionEmpty) options = addOption(options, "No extension"); // Check for a non-standard (custom) file extension var isCustomExecutableFileSystemExtension = false; if (executableFileSystemExtension.length <= 0xC && !isExecutableFileSystemExtensionEmpty && !executableFileSystemExtensionType && executableFileSystemExtensionType !== null) { log(logType.any, "Executable image uses the unrecognized ." + File.cleanString(executableFileSystemExtension.toUpperCase()) + " extension"); isCustomExecutableFileSystemExtension = true; } if (isCustomExecutableFileSystemExtension) options = addOption(options, "Extension (\"." + executableFileSystemExtension.toUpperCase() + "\")"); // Checking for DLL extension in executable PE files // Occurs in non-standard protections and SDK-style .NET application assemblies. For example: // * app.deps.json -> Dependency manifest // * app.runtimeconfig.json -> Runtime configuration // * app.exe -> Native apphost executable // * [app.dll] -> Managed application assembly without IMAGE_FILE_DLL var hasDllExtensionWithoutDllCharacteristic = false; if (executableFileSystemExtension === "dll" && !PE_Cached.isDynamicLinkLibrary) { log(logType.any, "Application image uses the .DLL extension. A host process may be expected."); hasDllExtensionWithoutDllCharacteristic = true; } if (hasDllExtensionWithoutDllCharacteristic) options = addOption(options, "Application Image with \".DLL\" extension"); // Checking for EXE extension in PE files with the DLL characteristic var hasExeExtensionWithDllCharacteristic = false; if (executableFileSystemExtension === "exe" && PE_Cached.isDynamicLinkLibrary) { log(logType.any, "DLL image uses the .EXE extension"); hasExeExtensionWithDllCharacteristic = true; } if (hasExeExtensionWithDllCharacteristic) options = addOption(options, "DLL Image with \".EXE\" extension"); if (options.length !== 0) isDetected = true; if (isDetected) { _setResult("~other", "Generic", "FS", options); } } // #endregion // Every time I start writing bad code I get hit with my head on the keybofewuihdsowefjfqodgsa79dowqhdsioefurogrwhuoguethuhofrwyioguqwehuf