From 88 lines to 1: Detecting DLL hijacking with Elastic Defend
The ClickFix campaign that sideloads a malicious mscoree.dll also ships a driver to kill Elastic Endpoint. We rebuilt that DLL as a NativeAOT library, dropped it beside a signed Microsoft binary, and Elastic Defend 9.5.0 flagged the load.
Elastic Defend 9.5.0 detects dynamic link library (DLL) search-order hijacking [1] in a single field. Writing that rule before 9.5.0 took about 88 lines, covering approximately 2,600 named libraries, 10 excluded Windows system paths, signature checks, and a drop-to-load time window. It now takes one: dll.Ext.defense_evasions: "DLL Hijack: Masquerading".
DLL search-order hijacking, which Defend labels Masquerading, runs attacker code inside a legitimate process by abusing the order that Windows uses to search for DLLs.
We rebuilt a real attack to test the new field. In August 2026, eSentire documented a ClickFix campaign that sideloads a malicious mscoree.dll into a signed Microsoft binary and then hollows a second Microsoft process to run an infostealer. The campaign also ships a vulnerable driver built to disable endpoint detection and response (EDR), including Elastic Endpoint. So we reverse engineered that DLL, rebuilt it as a NativeAOT .NET library, and dropped it beside vb7to8.exe, and Defend flagged the load.
DLL search order: How Windows finds a library
A DLL is a Windows binary that contains code other processes can call. When a program needs a function from a DLL, it can import it and have the loader resolve it; for example, through the LoadLibrary [2] API. If the caller doesn’t provide a full path to the library, Windows searches for it by name using a predefined order [3].
For most processes running under the default safe DLL search mode, that order is:
The directory the application was loaded from.
The system directory (System32).
The 16-bit system directory.
The Windows directory.
The current working directory.
The directories listed in the PATH environment variable.
The first entry is the adversary's opportunity. If an attacker can place a DLL in the folder that the program is loaded from, using the same name as a legitimate library that normally resolves from Windows\System32, for example, the loader finds the planted copy first and maps it into the process. The attacker's code then runs inside a host process, inheriting its identity and trust. This is why the technique is popular for initial access, persistence, defense evasion, and privilege escalation.
DLL sideloading: Reconstructing a real attack
In August 2026, eSentire published a blog post, ″Malware-as-a-Service Cocktail: ErrTraffic and Cruciferra - Killing Your EDR Since 2025″ [4], outlining a ClickFix campaign which ultimately installs an infostealer and uses a Bring Your Own Vulnerable Driver (BYOVD) to disable endpoint detection software, including Elastic Endpoint.
As the eSentire post is timed well with our 9.5.0 release, and the fact it targets our own EDR, we thought this would be an excellent use case for reconstructing the attack from a red team adversary emulation perspective, as well as showcasing our new detections in 9.5.0.
Before we get into reconstructing the attack, we must analyze precisely how and where it happens. The eSentire blog outlines that after luring a victim with the ClickFix technique, the malicious PowerShell code stages a legitimate Microsoft signed binary, vb7to8.exe, along with the malicious DLL which gets sideloaded, mscoree.dll. This is our starting point for the investigation.
mscoree.dll: Analyzing the sideloaded DLL
This DLL gets sideloaded into vb7to8.exe, which is a legitimate signed Windows 64-bit binary, with the description Visual Basic 8 Keyword Upgrade Tool.
Figure 1: vb7to8.exe binary properties.
Figure 2: Viewing the imports of vb7to8.exe.
Figure 3: Large number of exports in the early stage malware.
In this case, we can search through the list of exported functions for those that match the ones belonging to mscoree.dll imported by vb7to8, and we find them present. For example:
Figure 4: Finding exports required by vb7to8.exe.
Digging into the sample a little further, thanks to the great research by eSentire, we know that this early stage payload uses a technique called process hollowing [5] to inject the main infostealer into another legitimate Microsoft binary, ServiceModelReg.exe.
Process hollowing typically will call CreateProcessW [6], passing the flag CREATE_SUSPENDED, which creates a suspended process. In this frozen state, the malware can then hollow out the original code of the process, substituting it for a malicious payload. In this case, as documented by eSentire, it’s the infostealer payload, Remus.
Doing a quick search in the binary for CreateProcessW, we discover one interesting hit. This early stage payload calls CreateProcessW, but, curiously, it doesn’t pass the CREATE_SUSPENDED flag.
Figure 5: Finding a CreateProcessW call site.
Looking just after this in another branch of the code (Figure 6), we can see a very suspicious indirect function call via a dynamically resolved function pointer. This is very typical of malware, as it helps hide the true API call from the analyst performing static analysis.
The arguments to this obfuscated function call (Figure 6) include the flags CREATE_NO_WINDOW | CREATE_SUSPENDED, represented by the hexadecimal value 0x8000004. In fact, all of the arguments to this function line up perfectly with those required for the real CreateProcessW call. What's more, just above the call site, you can see two checks where the code is looking to match the bytes MZ (4D 5A) and PE (50 45). (They appear back to front in the code, accounting for endianness.) These are the magic bytes for the start of a Windows executable file (for example, an .exe or .dll). I’ve annotated Figure 6 to show the arguments required by CreateProcessW, which helps confirm that this is a likely candidate for the function call.
Figure 6: View of the indirect function call and magic byte check.
With this information in hand, we can assume it’s highly likely that this is part of the malware’s main code. The direct CreateProcessW call belongs to an alternative execution path; it nevertheless provides useful context about the loader’s broader behavior. We suspect that this may have been a mistake on the malware author’s part, which, incidentally, made static analysis easier.
We confirmed that the execution path up to this point is correct as per the above hypothesis at runtime through some dynamic analysis. Figure 7 shows the indirect call to CreateProcessW, with the first argument (in the RCX CPU register) being the program to be hollowed, the Microsoft ServiceModelReg.exe.
Figure 7: Dynamic analysis confirmation of code execution path.
malwareStart.Figure 8: Cross-refs graph back from CreateProcessW call site.
But who calls malwareStart? Given our objective of reconstructing this attack as part of adversary emulation, we need to know how to invoke it. The graph shows us several candidates, but looking directly at the cross-refs to malwareStart, we only get one candidate, which we’ve labeled j_malwareStart. If we look at this function, we can see that it’s a single instruction, jmp malwareStart.
Figure 9: Disassembly showing a jump instruction to the start routine.
j_malwareStart, we find ourselves at a dead end in the .data and .rdata section of the binary; the remaining two links in the chain (still walking backward) are:Figure 10: The last cross-refs ending in the .data and .rdata section.
IDA cannot automatically connect the indirect table walk.
Luckily, we’ve seen a few clues in the code about what language this was written in, and if we search the strings for .net, we come across:
Figure 11: .NET 7.0 appearing in the malware’s strings.
.managed section (Figure 9), which helps us identify it as being NativeAOT. This makes the DLL bigger, but, in turn, allows it to run on devices which don’t have the runtime available.Building the sideloaded DLL in .NET
.NET provides ModuleInitializerAttribute [9], written as [ModuleInitializer] [10], which marks a static method to run once when the .NET runtime initializes its containing module. In an ordinary executable, this occurs before normal application code, such as Main. In a NativeAOT shared library, this is a safer [11] way to run an initializer, away from an explicit DLLMain, which comes with numerous safety concerns, such as deadlocking the thread.
This behavior can be combined with DLL sideloading. An attacker places a malicious DLL where Windows will select it instead of the legitimate dependency and reproduces the exports expected by the host. When the host calls one of those exports, its compiler-generated NativeAOT wrapper initializes the runtime. The module initializer then executes before the export’s C# body.
Native exports can be created with [UnmanagedCallersOnly(EntryPoint = "...")] [12, 13]. In this sample, the export bodies do little more than return zero because their primary purpose is to trigger runtime initialization and the malicious module initializer.
So, taking just two examples, we can export any number of functions as follows:
public static class Exports
{
[UnmanagedCallersOnly(EntryPoint = "LoadLibraryShim")]
public static nint LoadLibraryShim()
{
return 0;
}
[UnmanagedCallersOnly(EntryPoint = "LoadStringRCEx")]
public static nint LoadStringRCEx()
{
return 0;
}
}Once all exported functions are added, you’ll see something similar to:
Figure 12: Viewing the exports we created in our proof of concept (POC) binary.
Now, these could themselves be used to house weaponized code, but in this particular case, the actor has chosen not to do that. So, to add the code that will run under ModuleInitializerAttribute, we can add the following (this is a benign proof of concept [POC] which simply displays a message box):
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
internal static class Bootstrap
{
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int MessageBoxW(nint window, string text, string caption, uint type);
[ModuleInitializer]
internal static void Start()
{
MessageBoxW(0, "Process hijacked!", "Process hijacked!", 0);
}
}The final piece of the puzzle is to ensure that we compile with .NET 7.0, and to make it a self-contained NativeAOT binary, we edit the project settings to look roughly like:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<NativeLib>Shared</NativeLib>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
</Project>Now we can publish it and rename the resulting DLL: mscoree.dll.
Finally, we can compare our resulting payload (left) with the original (right) to make sure, from an adversary emulation perspective, that we have implemented this accurately:
Checking the exports:
Figure 13: Validating our (left) exports versus the malware (right).
And validating the way in which the malicious code is loaded:
Figure 14: Validating our (left) code entry point versus the malware (right).
Triggering the DLL sideload
Placing the rebuilt mscoree.dll directly next to the executable vulnerable to search-order hijacking (vb7to8.exe) and running it (passing some argument into the executable; it’s a requirement in this case), we get code execution. Elastic Defend now identifies this event as DLL Hijack: Masquerading.
Figure 15: The message box showing code execution inside vb7to8.exe.
Detecting DLL search-order hijacking in Elastic Defend 9.5.0
What the DLL Hijack: Masquerading enrichment checks
Elastic Defend adds the DLL Hijack: Masquerading enrichment when a loaded DLL isn’t Microsoft-signed and its name matches a DLL found in a cached inventory of Windows and system-library DLLs:
dll.Ext.defense_evasions: ["DLL Hijack: Masquerading"]The field lets detection rules query that condition directly.
Writing a DLL hijacking rule before 9.5.0
Before 9.5.0, detecting DLL search-order hijacking in a rule was possible but laborious. A representative approach had to name the library, exclude legitimate system directories, require an untrusted or absent signature, and constrain the drop-to-load window:
library where host.os.type == "windows" and event.action == "load" and
/* A DLL whose name collides with a genuine System32 module... */
dll.name : "mscoree.dll" and
/* ...but is loaded from outside the Windows system directories... */
not dll.path : ("?:\\Windows\\System32\\*",
"?:\\Windows\\SysWOW64\\*",
"?:\\Windows\\WinSxS\\*") and
/* ...is unsigned or not Microsoft-signed... */
(dll.code_signature.trusted == false or
dll.code_signature.exists == false or
not dll.code_signature.subject_name : ("Microsoft Windows",
"Microsoft Corporation")) and
/* ...and was written to disk shortly before it was loaded. */
dll.Ext.relative_file_creation_time <= 900Rules like this work, but they carry maintenance costs. They tend to enumerate abusable DLL names or generalize with directory-comparison logic that matches the loaded DLL's folder against the loading process's folder, and they need careful signature and timing conditions to stay precise. Every newly discovered abusable library is another line to maintain.
Two such examples of how complex and expensive these queries are can be found in this out-of-the-box detection rule and in this Threat Hunt, requiring a custom ENRICH policy and maintaining a long list of DLL names.
Writing the same rule in 9.5.0
With the DLL Hijack: Masquerading enrichment in place, the core detection becomes a single expression:
library where host.os.type == "windows" and event.action == "load" and
dll.Ext.defense_evasions : "DLL Hijack: Masquerading"
| Before 9.5.0 | 9.5.0 onward |
DLL name | Named explicitly in the rule, ~2,600 | Not required |
System path exclusions | Ten paths excluded manually | Handled in the sensor |
Signature check | Six-branch condition | Handled in the sensor |
Drop-to-load window | Specified in the rule | Optional, as supporting evidence |
Lines of rule logic | ~88 | 1 |
New abusable library | Rule update required | No change |
In production, we pair that field with a few guards to keep it precise.
What the DLL hijacking alert looks like
When our emulation runs, Defend emits a library load event for vb7to8.exe carrying the new enrichment. A trimmed view of the relevant fields looks like this:
{
"event": { "category": "library", "action": "load" },
"process": {
"name": "vb7to8.exe",
"executable": "C:\\Users\\ian\\Desktop\\vb7to8.exe"
},
"dll": {
"name": "mscoree.dll",
"path": "C:\\Users\\ian\\Desktop\\mscoree.dll",
"code_signature": { "exists": false, "trusted": false },
"Ext": {
"relative_file_creation_time": 6,
"defense_evasions": ["DLL Hijack: Masquerading"]
}
}
}The rule fires, and the alert presents the essentials at a glance: a Microsoft utility loading mscoree.dll from a user’s desktop, with no trusted signature, flagged as DLL Hijack: Masquerading.
Field | Value |
process.name | vb7to8.exe |
dll.name | mscoree.dll |
dll.path | C:\Users\ian\Desktop\mscoree.dll |
dll.code_signature.trusted | false |
dll.Ext.defense_evasions | DLL Hijack: Masquerading |
Figure 16: Elastic Security alert for the masquerading DLL load.
Tuning DLL hijacking detection and false positives
No behavioral signal is free of edge cases. Legitimate software occasionally ships private copies of libraries whose names overlap with system modules, or loads components from application directories by design. To keep the detection high fidelity:
Combine signature and provenance. An absent or untrusted signature increases suspicion but isn’t conclusive. A trusted signature lowers concern only when the publisher, product, load path, and loading process are consistent with expected software.
Judge the path in context. A DLL loaded from an unexpected or user-writable location is more suspicious, particularly when the process would normally obtain that module from a protected system or installation directory. AppData, ProgramData, and Temp are also used legitimately by installers and updaters, so location alone often is insufficient.
Treat drop-to-load timing as supporting evidence. A recently created DLL can indicate staging, but the same pattern is common during installation, updating, and first-run extraction. Confidence increases when an unexpected process writes the DLL shortly before another process loads it, especially outside a recognized deployment or update workflow.
Scope exclusions to known behavior. Allowlist-confirmed combinations of trusted publisher, narrow installation path, and expected loading process rather than suppressing a DLL name or the enrichment broadly.
The enrichment removes much of the rule-side complexity, but local prevalence should still be validated. Where tuning is required, prefer narrow exclusions for confirmed software installation and update workflows.
How to prevent DLL search-order hijacking
The underlying weakness is usually in how an application resolves its dependencies, so it cannot be eliminated through detection tuning alone. Endpoint administrators can nevertheless reduce the opportunity for exploitation:
Protect managed application directories. Standard users shouldn’t be able to write DLLs beside privileged, service-hosted, or automatically launched executables.
Control execution from user-writable locations. Where operationally feasible, use application-control policy to restrict unapproved executables and libraries launched from Downloads, Temp, Desktop, and other user-controlled directories.
Ensure safe DLL search mode. This is enabled [14] by default in modern versions of Windows, but it can be modified through the registry value: HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode. This reduces exposure to current-directory planting by moving the current directory later in the search order. It doesn’t prevent application-directory sideloading, because the executable’s directory remains ahead of System32.
Application developers can address the root cause by using fully qualified library paths or restricted search flags, such as LOAD_LIBRARY_SEARCH_SYSTEM32 [15]. These are application changes rather than Elastic Defend configuration.
Conclusion
DLL search-order hijacking endures because it abuses normal, documented loader behavior. By reconstructing a faithful example, including reverse engineering mscoree.dll and loading a masquerading library, we can see both why the technique works and how it looks under the hood. Elastic Defend 9.5.0's DLL Hijack: Masquerading behavior turns this into a reliable field, letting defenders write clear detection content instead of maintaining brittle, library-by-library rules.
MITRE ATT&CK mapping
Tactic | Technique | ID |
Persistence, privilege escalation, defense evasion | Hijack Execution Flow: DLL | T1574.001 |
Defense evasion | Masquerading | T1036 |
References
Hijack Execution Flow: DLL, Sub-technique T1574.001 - Enterprise | MITRE ATT&CK®.
LoadLibraryA function (libloaderapi.h) - Win32 apps | Microsoft Learn.
Dynamic-link library search order - Win32 apps | Microsoft Learn.
Malware-as-a-Service Cocktail: ErrTraffic and Cruciferra - Killing Your EDR Since 2025.
An introduction to reverse engineering .NET AOT applications.




