CATEGORIES

10 Years of DLL Hijacking, and What We Can Do to Prevent 10 More

September 25, 2024

Introduction

DLL Hijacking — a technique for forcing legitimate applications to run malicious code — has been in use for about a decade at least. In this write-up we give a short introduction to the technique of DLL Hijacking, followed by a digest of several dozen documented uses of that technique over the past decade as documented by MITRE. Highlights include the specific executables abused, statistics regarding the specific way the hijack was implemented, and peeks into the internal structure of some of the involved malicious DLLs. We then discuss the tools available to application developers to prevent malicious actors from abusing their legitimate applications in this way, and give a proof-of-concept for one such tool that harnesses some of the power of digital signatures without needing to deal with a certificate authority.

What is DLL Hijacking?

Various sources define “DLL Hijacking”, as well as the related term “DLL Sideloading”, differently. The different definitions for the two terms partially overlap, which may cause some confusion. For instance, MITRE suggests that Sideloading “takes advantage of the DLL search order used by the loader by positioning both the victim application and malicious payload(s) alongside each other”, whereas infosec firm Mandiant, at least in one report, defines DLL Sideloading as only the abuse of WinSxS specifically:

In this write-up, we define “DLL Hijacking” as any execution flow hijacking technique that abuses a benign executable file’s dynamic library dependencies, whether these are stated in some kind of executable manifest or loaded at runtime. This technique has been documented since at least 2013; Mandiant’s report, mentioned above, identifies a 2013 spear-phishing attack that targeted Chinese political rights activists and exploited a vulnerability in Windows ActiveX controls (CVE-2012-0158) to drop a benign executable from an Office 2003 Service Pack 2 update, which was then made to load a malicious DLL. Since then, attack chains featuring DLL hijacking have kept coming at a steady pace — primarily used by state sponsored actors such as Lazarus Group and Tropic Trooper, and occasionally by the cybercrime industry, in conjunction with e.g. the QBot infostealer and Dridex banking Trojan.

What is the purpose of DLL Hijacking?

The three main use cases for DLL Hijacking are evasion, persistence and privilege escalation.

Evasion may result from the fact that a sideloaded DLL will run as part of a process image originally derived from a benign executable. At first sight, the process will appear less suspicious; in some pathological cases, it might even be on some sort of allow-list exempting it from scrutiny. A security filter that judges processes on their reputation rather than behavior may misclassify the hijacked process as benign when that is no longer the case. This is an example of a general principle: when you trust something (or someone), you need to worry not only about it intentionally turning on you but also about it being malleable and confused.

Persistence may result if the benign executable is routinely executed during the victim system’s normal operation. The natural thought as an attacker would be to use something that will launch automatically on startup, but a moment’s thought will show that targeting the default web browser on the victim machine, or some other frequently used software, can also work well.

Privilege Escalation can be achieved if the benign executable has permissions that a vanilla process does not. The first example that comes to mind is administrator privileges: as stated by Microsoft, “Same-desktop Elevation in UAC isn’t a security boundary”; DLL hijacking is one way an attacker can abuse this fact. In other cases, some software may implement ad-hoc security boundaries around files, drivers and other objects that only specific processes are allowed to read or modify. Hijacking those specific processes will allow bypassing that restriction.

Landscape Review

To understand the landscape of DLL Hijacking, we reviewed several dozen uses of this technique by different campaigns, as catalogued by MITRE under the title “DLL Sideloading” including the specific hijacking technique used, such as where the malicious DLL was placed, how it was loaded, what benign executable was abused, and the inner bits and bytes of how the malicious DLL was constructed.

By far, the most common tactic documented in these campaigns was bundling together a known benign application and a malicious DLL, then dropping both in the same folder and executing the benign application. Just over half the surveyed campaigns used this technique. We provide a table below of these hijacking instances and the benign executable that was abused in each.

PublisherApplicationFilenameReference
NVIDIASmart Maximize Helper HostnvSmartEx.exeAPT41, A Dual Espionage and Cyber Crime Operation
MicrosoftActiveSync Ink Form MAPI Notes Serverform.exeAPT41, A Dual Espionage and Cyber Crime Operation
OracleJava Runtime Launcherjava-rmi.exeMonsoon – Analysis Of An APT Campaign
CitrixSingle Sign On Serverssonsvr.exeBBSRAT Attacks Targeting Russian Organizations Linked to Roaming Tiger
MicrosoftOneDrive UpdaterOneDriveUpdater.exeWhen Pentest Tools Go Brutal: Red-Teaming Tool Being Abused by Malicious Actors
LogitechBluetooth WizardLBTWizGi.exeDissecting a Chinese APT Targeting South Eastern Asian Government Institutions
OracleJava Platform SE 8 Policy Toolpolicytool.exeAPT10: sophisticated multi-layered loader Ecipekac discovered in A41APT campaign
SamsungSamsung InstallerRunHelp.exeOperation Soft Cell: A Worldwide Campaign Against Telecommunications Providers
Qihoo 360Total Security Shell Pro360ShellPro.exeCOVID-19 and New Year greetings: an investigation into the tools and methods used by the Higaisa group
KasperskyKaspersky Antivirusnot specifiedThreat Group 3390 Cyberespionage
Open SourcecURLcurl.exeEmissary Panda Attacks Middle East Government SharePoint Servers
Sublime HQSublime Text Plugin Hostplugin_host.exeEmissary Panda Attacks Middle East Government SharePoint Servers
Hex-RaysIDA Pronot specified#ESET research discovered a trojanized IDA Pro installer (..)
Quest SoftwareToad for OracleFmtOptions.exeLuminousMoth APT: Sweeping attacks for the chosen few
Avast SoftwareMemory Dump UtilityAvDump32.exe (renamed randomly, e.g. to jesus.exe)The Avast Abuser: Metamorfo Banking Malware Hides By Abusing Avast Executable
ESETHTTP Server ServiceEHttpSrv.exe (renamed to 3.exe)China-Based APT Mustang Panda Targets Minority Groups, Public and Private Sector Organizations
MicrosoftOutlooknot specifiedNaikon APT: Cyber Espionage Reloaded
Avast SoftwareAvast Proxynot specifiedNaikon APT: Cyber Espionage Reloaded
ESETDESLock+dlpumgr32.exeIron Tiger APT Updates Toolkit With Evolved SysUpdate Malware
MicrosoftCredential Backup & Restore Wizardcredwiz.exeSideCopy APT: Connecting lures to victims, payloads to infrastructure
MicrosoftRFS Rekey Wizardrekeywiz.exeA Global Perspective of the SideWinder APT
MicrosoftMalware Protection EngineMsMpEng.exe (renamed to utilman.exe)[..] China-Backed APT Pirate Panda May Be Seeking Access to Vietnam Government Data Center
NormanSafeground AS AntivirusZlh.exeOops, they did it again: APT Targets Russia and Belarus with ZeroT and PlugX
IntelGraphics System Tray Helperigfxtray.exeT9000: Advanced Modular Backdoor Uses Complex Anti-Analysis Techniques
Figure 1. Sample documented cases of DLL hijacking by ‘simple bundle’ of executable and malicious DLL

The first feature of this table that jumps out is the attacker’s fascination with “credible-sounding” applications: Google, Microsoft, Adobe. After all, attackers don’t have a precise threat model of how defenders will act, but maybe they believe they can get an advantage if they abuse applications by these well-known vendors. When dealing with a popular application that has a wide install base, defenders will naturally worry more about false positives (according to legend, in the distant past it was common for “trusted” applications and protocols to be exempted from inspection outright). Basically, the more defenders weigh executable reputation, the more this kind of technique becomes worthwhile.

If we put aside the constant abuse of well-esteemed and popular applications, there are some small trends in the remaining data. First is the repeated abuse of AV products, but another curiosity is the abuse of applications without much regard for their state or origin, which leads to scenarios such as:

Another trend is the bundling of executables that are part of Windows OS or just very commonly found in victim machine files. This can be seen in campaigns where the attack chain abused dropped copies of the Windows Credential Backup and Restore Wizard, or alternately, the RFS Rekey Wizard.

Red Canary’s Dridex Report seems to argue that focusing too much on the specific abused benign executable can be detrimental:

A natural question that arises is how to hunt for executables amenable to dynamic library hijacking. One well-known reliable trick is running an executable through a process monitoring tool and specifically monitoring for events of failed lookups for a DLL file that is absent from some location. This indicates that someone could insert their own malicious version there for the application to find. This vetting procedure can be done using e.g. ProcMon, with the filters Path ends with .dll and Result is NAME NOT FOUND, or some equivalent. Unfortunately, this method does not scale well, even if some automations for it exist, such as the Spartacus project. Specific cases might be tractable to hunt using EDR telemetry — for example filtering for cases where a process loads a DLL lacking a known correct signature from the same directory, as suggested here. Not all dynamic library hijacks satisfy these conditions, but many do. Finally, another option is to make use of the excellent resources available at the hijacklibs repository. These catalogue hijackable DLLs, including their version information, expected signature information, and so on. This information can be used for hunting, for example by querying for files that declare a certain publisher or version information, but have a suspiciously modified hash or missing signature.

Technical Highlights of Malicious DLL Structure

We researched the internal assembly of some of the maliciously crafted DLLs used in hijacking, and identified technical themes and patterns. For example, there is less out-of-the-box support for the use of obfuscation tools (’packers’, ‘crypters’) on DLLs. As is often the case, when a threat actor feels that some code or data is not as obfuscated as it should be, they reach for the XOR loop.

Figure 2. Deobfuscation routine used in maliciously crafted DLL.

The Specific DLL pictured above is a maliciously crafted version of dbgeng.dll and had relatively few exports. If you look closely, you can see that under the hood, two of them (DebugConnect and DebugCreate) actually point to the same function.

Figure 3. DebugConnect and DebugCreate exports point at the same address.
Figure 4. The function pointed at by both exports in the DLL is a short stub that calls malicious logic.

If that seems peculiar, then there’s a maliciously crafted version of jli.dll, where it’s not just 2 functions — almost every function points at the malicious code:

Figure 5. All the JLI_ exports point at the same stub that calls attacker-crafted logic.

A maliciously crafted version of lbtserv.dll also had many exports pointing at the same target. Instead of pointing to malicious code, they all pointed to a null function stub. You can decide for yourself which seems more suspicious:

Figure 6. All the LGBT_ exports point at the same function stub.
Figure 7. This is a null stub that performs no action.

Finally, a malicious crafted version.dll contained a subtle invocation of vresion rather than version:

Figure 8. exported function names in the maliciously crafted version.dll file. This kind of ‘slip of the finger’ is popular for DLL Proxying.

Developer Tools for Preventing DLL Hijacking

In this section, we dive into preventative tools and approaches available to application developers to prevent malicious actors from successfully abusing their applications with this technique.

In mainstream operating systems, the idiomatic way for an application to declare dynamic library dependencies is via some sort of statically compiled data in its header, such as the import table included in the PE format historically used by Windows OS. These formats are simple and only allow the developer to name the library they would like to load, with little additional validation. From there the operating system takes care of everything and dictates the behavior that allows hijacking — the standard search order and the loading of the first library that has the correct name, without any further verification.

To be clear, the technology needed to tackle this kind of issue exists. It’s tempting to start thinking of pie-in-the-sky systemic reform: if only everyone digitally signed their DLLs, if only every executable understood whose DLL it was trying to load, and verified the signature… Of course, nothing feels better than pie-in-the-sky systemic reform actually happening and making an entire category of security issue disappear, but until such time that this happens, we have to deal with the issue in the current unreformed world using the modest tools that we do have.

Since declaring dependencies via executable header immediately allows hijacking, dealing with this issue at the developer level seems to require loading all possible libraries at runtime. Of course, if you call LoadLibrary("some.dll"), this simply invokes the usual search order again. A quick and dirty work-around for Windows, documented here, is to have the application first call SetDllDirectory(""). This removes the current working directory from the DLL search path, so if any DLLs do need to be loaded from the current directory, you will have to obtain its fully qualified path and supply it to LoadLibrary explicitly. A related hack is calling SetSearchPathMode (BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE | BASE_SEARCH_PATH_PERMANENT). This moves the current directory to the bottom of the search order. If you are paranoid enough to worry about maliciously crafted DLLs somewhere in the search order outside the current directory, you might want to use fully qualified paths in all calls to LoadLibrary, or use LoadLibraryEx which allows you to specify what libraries the DLL can be loaded from.

Unfortunately, even if you control where the DLL is loaded from, there is plenty of room left for hijacking. The fundamental reason for this is that the location of a loaded library in the file system is not an ironclad guarantee of anything. While it’s true that in all probability no one is going to directly tamper with C:\Windows\System32\ws2_32.dll, it’s a different story if the loaded library is something custom-made for the application, and is normally loaded from the current directory to begin with. In that case, path-based verification will not catch that typical malicious “bundle” of benign app and malicious library side-by-side in the same directory. After all, the malicious library is exactly where the application expects it to be.

To get around this issue requires dealing with digital signatures, or an equivalent solution. Obviously there are some OS-provided amenities to do this (e.g. LoadLibraryEx has a flag that will require the target DLL to be signed — LOAD_LIBRARY_REQUIRE_SIGNED_TARGET), but as a developer, this may still appear to be a significant barrier that takes up time- and resource-consuming registration with a certificate authority. Happily, a workaround is possible. When creating a DLL, you can sign it using a private key from a self-signed certificate, then publish the certificate alongside the DLL. Anyone loading that DLL from an executable, including you, can first verify that the certificate chain checks out internally.

Note that an attacker can still craft their own malicious version of the executable (this is probably one of the reasons that in their exposition of a similar feature available for .NET, “Strong-Named Assemblies”, Microsoft says “do not rely on strong names for security. They provide a unique identity only”); But the forged executable will have an unknown hash and will not enjoy the original’s good reputation. Also note that replacing the certificate will break compatibility with previously compiled executables and DLLs.

To demonstrate how this can work, we include below a proof-of-concept program that can sign a simple DLL sample.dll using a very simplified homebrew counterpart of Authenticode — it uses OpenSSL to compute a signature on the entire file contents, then adds the signature as an overlay. A separate executable (frontloaded.exe) then performs a secure load of the signed DLL (sample.dll), first extracting the DLL signature and verifying it, and only then loading the DLL properly. If the signature verification fails, the executable panics (throws an exception).

Figure 9. The benign executable has an embedded self-signed certificate, the private key associated with which had been used to sign the original DLL, with the resulting signature added as an overlay (green). The function secure_load in the executable verifies the signature before loading the DLL (blue). A crafted malicious DLL will fail this verification and will be rejected by secure_load (Red).

The Rust code used for the signing program is below.

use openssl::x509::X509;
use openssl::sign::Verifier;
use openssl::pkey::PKey;
use openssl::sign::Signer;
use openssl::rsa::Rsa;
use openssl::hash::MessageDigest;
use std::fs;
use libloading::{Library, Symbol};
use clap::{Command, Arg};
use anyhow::{Result,anyhow};

const SIG_LEN : usize = 512;

pub fn secure_load_library(cert: &str, dll_name: &str) -> Result<Library> {
    match verify_file(cert,dll_name)? {
        true =>  unsafe { Ok(Library::new(dll_name)?) },
        false => Err(anyhow!("Signature verification failed"))
    }
}

pub fn sign(pem_key: &str, data: &[u8]) -> Result<Vec<u8>> {
    let pkey = 
        Rsa::private_key_from_pem(pem_key.as_bytes())
        .and_then(|x| PKey::from_rsa(x))?;
    let signature = Signer::new(MessageDigest::sha256(), &pkey)
        .and_then(|mut x| {x.update(data)?; Ok(x)})
        .and_then(|x| {x.sign_to_vec()})?;
    Ok(signature)
}

pub fn verify(cert: X509, data: &[u8], sig: &[u8]) -> Result<bool> {
    let public_key = cert.public_key()?;
    let verification_status = Verifier::new(MessageDigest::sha256(), &public_key) 
        .and_then(|mut x| {x.update(data)?; Ok(x)})
        .and_then(|x| x.verify(sig))?;
    Ok(verification_status)
}

pub fn sign_file(key_path: &str, fname: &str, fname_new: &str) -> Result<()> {
    let data = fs::read(fname)?;
    let sig = sign( &fs::read_to_string(key_path)?, &data)?;
    let signed_data : Vec<u8> = 
        data.into_iter().chain(sig.into_iter()).collect();
    fs::write(fname_new, signed_data)?;
    Ok(())
}

pub fn verify_file(cert: &str, fname: &str) -> Result<bool> {
    let cert = X509::from_pem(cert.as_bytes())?;
    let mut data = fs::read(fname)?;
    let sig = data.split_off(data.len()-SIG_LEN);
    verify(cert, &data, &sig)
}

The CLI:

fn main() -> Result<()> {
    let matches = Command::new("Signing Tool")
        .version("1.0")
        .about("Simple homebrew implementation of file signing using an overlay.")
        .arg(
            Arg::new("source_path")
                .short('s')
                .long("source")
                .value_name("SOURCE_PATH")
                .help("Path to the unsigned file")
                .required(true)
        )
        .arg(
            Arg::new("target_path")
                .short('t')
                .long("target")
                .value_name("TARGET_PATH")
                .help("Signed file will be written to this path.")
                .required(true)
        )
        .arg(
            Arg::new("key_path")
                .short('k')
                .long("key")
                .value_name("KEY_PATH")
                .help("Path to the signing key (PEM format)")
                .required(true)
        )
        .get_matches();

    let unsigned_path = matches.get_one::<String>("source_path").unwrap();
    let signed_path = matches.get_one::<String>("target_path").unwrap();
    let key_path  = matches.get_one::<String>("key_path").unwrap();

    sign_file(&key_path, &unsigned_path, &signed_path)
}

The (rather simple) DLL:

#[no_mangle]
pub fn add(x: i32, y: i32) -> i32 {
    x+y
}

And the executable that performs the load:

use crate::minisign::{secure_load_library,FunctionRetrieve};
use anyhow::Result;

const CERT : &str = include_str!("cert.pem");

mod minisign;

fn main() -> Result<()> {
    let lib = secure_load_library(CERT, "sample.dll")?;
    let add = lib.get_proc_addr("add")?;
    println!("{}", add(5,6));
    Ok(())
} 

That clean call to get_proc_addr actually requires a kludge, which we include below for those overly curious to know how the sausage is made.

pub trait FunctionRetrieve {
    fn get_proc_addr<'a>(&'a self, func_name: &str) -> Result<Symbol<'a, Symbol<'a, extern "C" fn(i32, i32) -> i32>>>;
}

impl FunctionRetrieve for Library {
    fn get_proc_addr<'a>(&'a self, func_name: &str) -> Result<Symbol<'a, Symbol<'a, extern "C" fn(i32, i32) -> i32>>> {
        unsafe {
            Ok(self
                .get::<Symbol<extern "C" fn(i32,i32) -> i32>>(func_name.as_bytes())?
            )
        }
    }
}

Enforcing digital signatures, either backed by a cert authority or as above, is a powerful solution — but only if you can apply it, which may not be the case if you are dynamically loading an unsigned library by some third party or by Microsoft (a surprising number of these exist). In the latter case, the mitigation technique of giving fully qualified names to LoadLibrary mentioned earlier is often, though not always, effective with respect to the resulting gap, as it forces attackers to load maliciously crafted dynamic libraries from their original locations. In the case of MS DLLs, malicious actors are typically not inclined to directly tamper with these.

Conclusion

As long as threat actors believe that they can gain an advantage privilege-wise and evasion-wise from running their code in a process derived from a “trusted” executable, DLL hijacking as a technique is likely here to stay. Though there are many possible variations, according to MITRE’s 10-year data, the simplest variant — a “benign executable and malicious library in the same folder” bundle — is the most popular.

There are practical and airtight methods to deal with this malicious technique, but the practical methods are not airtight, and the airtight methods are not practical. Developers can require that libraries be loaded from a specific hardcoded path, and even enforce an internal chain of trust for their own executable-library ecosystem as we demonstrated. This partially narrows down the space of possible DLL hijacking variants, but not completely. On the other hand, if everyone signs their binaries the usual way verified by a proper cert authority, and verifies signatures upon DLL load, there will be very little left of the problem. But solutions that start with “if everyone” are typically practical only when enforced by a government or a monopoly, and often not even then.

In the current computing environment, process boundaries are not generally considered security boundaries. Given this and the resulting popularity of DLL hijacking and other similar techniques, defenders should be vigilant not to over-emphasize executable reputation when judging process behavior. We can’t control threat actors’ fascination with running code inside quote-unquote “trusted processes”, but we can meaningfully control the degree to which this fascination is a waste of time.

POPULAR POSTS

BLOGS AND PUBLICATIONS

  • Check Point Research Publications
  • Global Cyber Attack Reports
  • Threat Research
February 17, 2020

“The Turkish Rat” Evolved Adwind in a Massive Ongoing Phishing Campaign

  • Check Point Research Publications
August 11, 2017

“The Next WannaCry” Vulnerability is Here

  • Check Point Research Publications
January 11, 2018

‘RubyMiner’ Cryptominer Affects 30% of WW Networks