Threat Command

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:

  1. The directory the application was loaded from.

  2. The system directory (System32).

  3. The 16-bit system directory.

  4. The Windows directory.

  5. The current working directory.

  6. 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.

The legitimate vb7to8 binary imports a number of Windows DLLs, one of which we can see is mscoree.dll:

Figure 2: Viewing the imports of vb7to8.exe.

As per eSentire, this specific campaign used mscoree.dll, however we believe that the malware author has designed this such that the buyer of this malware-as-a-service payload can hijack a vast number of applications (both legitimate Windows ones and third-party ones that are vulnerable to search-order hIjacking), as they have exported a vast number of functions, which match common Windows DLL exports:

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.

So now we can use this information to trace through the binary backward to where it’s called from. If we build a cross-refs graph, we find a node at the beginning of the chain, which we’ve labeled 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.

So how do we get here? Walking the cross-refs backward from 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.

So the payload was likely written in .NET 7.0, which gives us some information about how this was probably constructed, and is the final clue to rebuilding this attack. Not only do we know that it’s written in .NET, but we also know that it’s a NativeAOT [7] binary, thanks to the eSentire blog, meaning that its managed code was compiled ahead of time. We can also see the telltale [8] .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 <= 900

Rules 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

  1. Hijack Execution Flow: DLL, Sub-technique T1574.001 - Enterprise | MITRE ATT&CK®

  2. LoadLibraryA function (libloaderapi.h) - Win32 apps | Microsoft Learn

  3. Dynamic-link library search order - Win32 apps | Microsoft Learn

  4. Malware-as-a-Service Cocktail: ErrTraffic and Cruciferra - Killing Your EDR Since 2025

  5. Ten process injection techniques: A technical survey of common and trending process injection techniques.

  6. CreateProcessW function (processthreadsapi.h).

  7. Native AOT deployment.

  8. An introduction to reverse engineering .NET AOT applications.

  9. ModuleInitializerAttribute Class.

  10. ModuleInitializer attribute.

  11. C# dllmain with nativeaot #66546.

  12. Building native libraries.

  13. Native exports.

  14. Standard search order for unpackaged apps.

  15. Search order using LOAD_LIBRARY_SEARCH flags.

Related Content

Exploring the Hugging Face Breach: mapping AI agent tactics to Elastic Defend

Elastic Security Labs

Detecting Tycoon 2FA AiTM attacks across Entra ID and Google Workspace

Samir Bousseaden

CI/CD pipeline abuse: the problem no one is watching

Mika Ayenson

Beyond Behaviors: AI-Augmented Detection Engineering with ES|QL COMPLETION

Mika Ayenson

Misbehaving Modalities: Detecting Tools, Not Techniques

John Uhlmann