Undocumented DLL loading functions. LoadLibrary call stack

I successfully completed half of the system programming course during my studies at the institute, and now, after some time, I finally decided to figure out how DLLs are loaded in Windows OS from LoadLibrary to mapping the library in memory.

After researching open sources, I came across a project by Github user paskalian WID_LoadLibraryin which the author recreated the source codes of almost all stages of loading the library. However, at present, the project does not start with a half-kick, since the function signatures have long changed and in general the grass was greener before.

So, based on this project, I decided to create my own tool that allows loading the library using the API at any level of the call stack.

Let's look at the call stack that implements library loading. Let's write a simple program:

 #include "Windows.h"

int main()
{
    LoadLibraryA("user32.dll");
}

Let's compile, load into IDA and run through the debugger

In the modules tab, load debug symbols for kernel32, ntdll and kernelbase

Let's connect to ProcessHacker and track the moment the library appears in the list of modules

So, the insides of LoadLibraryA in kernel32:

This is just a call to LoadLibraryA in kernelbase.

Kernelbase_LoadLibraryA:

Here we see a check of the passed name for compliance with twain_32.dll. If “twain_32.dll” is passed, “C:\Windows\” is added to the string and kernelbase_LoadLibraryA is called again.

Otherwise, the call is passed to kernelbase_LoadLibraryExA with additional parameters 0, 0 (reserved field and empty flags)

kernelbase_LoadLibraryExA:

Here we see that some unnamed function is called and the call is passed to LoadLibraryExW.

In fact, what happens here is that the passed string is converted to a UNICODE_STRING structure and LLExW is passed UNICODE_STRING.Buffer, 0, dwFlags.

kernelbase_LoadLibraryExW:

First, the passed arguments are checked

if (!lpLibFileName || hFile || ((dwFlags & 0xFFFF0000) != 0) || (DatafileFlags == LLEXW_ASDATAFILE))

And if something is wrong, the error 0xC000000D is returned.

Next, another UNICODE_STRING is initialized from the previously transferred buffer and checked for correctness:

Length is not 0, otherwise it is an error

If spaces are passed at the end, they are removed.

If you delete it again until you reach an empty line, then the error occurs again.

Next, if the library loading flag is set as DataFile, then the LdrGetDllPath function is called, which receives the path to the library and, using the BasepLoadLibraryAsDataFileInternal function, the library is loaded as data.

Otherwise, the flags are checked for DONT_RESOLVE_DLL_REFERENCES, LOAD_PACKAGED_LIBRARY, LOAD_LIBRARY_REQUIRE_SIGNED_TARGET, LOAD_LIBRARY_OS_INTEGRITY_CONTINUITY

And the loading continues on the LdrLoadDll function. It is worth saying here that many monitoring tools hook this function and rarely go deeper.

Function prototype:

typedef NTSTATUS(WINAPI* pLdrLoadDll)(PWCHAR PathToFile, ULONG Flags, PUNICODE_STRING ModuleFileName, PHANDLE ModuleHandle);

Let's move on to LdrLoadDll:

Here's the flag check again

And then a call to the LdrpInitializeDllPath function, which initializes an undocumented structure that paskalian described as follows:

struct LDR_UNKSTRUCT
{
	PWSTR pInitNameMaybe;
	__declspec(align(16)) PWSTR Buffer;
	int Flags;
	PWSTR pDllName;
	char Pad1[84];
	BOOLEAN IsInitedMaybe;
	char Pad2[3];
};

Function prototype:

typedef NTSTATUS(__fastcall* pLdrpInitializeDllPath)(PWSTR DllName, PWSTR DllPath, LDR_UNKSTRUCT* DllPathInited);

Then the LdrpLoadDll function is called.

This function, like all the following ones, is already non-exportable and is extremely rare on the Internet.

Function prototype:

typedef NTSTATUS(__fastcall* pLdrpLoadDll)(PUNICODE_STRING DllName, LDR_UNKSTRUCT* DllPathInited, ULONG Flags, LDR_DATA_TABLE_ENTRY** DllEntry);

Inside this function, an additional check of the path to the Dll is performed using the function

LdrpPreprocessDllName, in which the name is resolved if name redirection is enabled, and DOS paths are also resolved.

Prototype:

typedef NTSTATUS(__fastcall* pLdrpPreprocessDllName)(PUNICODE_STRING DllName, PUNICODE_STRING ResName, PULONG pZero, PULONG pFlags);

The path obtained as a result of the function is passed as the first argument to the LdrpLoadDllInternal function.

Function prototype:

typedef NTSTATUS(__fastcall* pLdrpLoadDllInternal)(PUNICODE_STRING FullPath, LDR_UNKSTRUCT* DllPathInited, ULONG Flags, ULONG LdrFlags, PLDR_DATA_TABLE_ENTRY LdrEntry, PLDR_DATA_TABLE_ENTRY LdrEntry2, PLDR_DATA_TABLE_ENTRY* DllEntry, NTSTATUS* pStatus);

LdrFlags and LdrEntry are always 0

Inside this function, all the important stuff happens:

Checks if the dll was previously loaded into the process using LdrpFastpthReloadedDll.

Globally, the function is not important to us, so there will be no prototype 🙂

Next, the LdrpFindOrPrepareLoadingModule function is called, which determines whether the dll is already loaded somewhere, whether it belongs to KnownDll, or returns a placeholder for the dll.

Here the dll may already appear in the process as fully loaded (as in our case with user32.dll), or it may simply appear in the list of modules.

If the DLL was not found as previously loaded, the LdrpProcessWork function is called, in which the library is mapped into memory.

Function prototype:

typedef NTSTATUS(__fastcall* pLdrpProcessWork)(PLDRP_LOAD_CONTEXT LoadContext, BOOLEAN IsLoadOwner);

Next, right here, in LdrpLoadDllInternal, the Dll is initialized, added to all module lists, and so on.

Let's move on to LdrpProcessWork:

Here the library memory mapping functions are called. Inside these functions the file reading, section initialization and so on are called. This is a separate topic, we will write about it someday. In general, there are quite a lot of articles about manual library memory mapping. In short, here:

So, having dealt with the call stack, we have access to such functions, which allow loading the library quite simply:

  • LoadLibrary(A,W)

  • LoadLibraryEx(A,W)

  • LdrLoadDll

  • LdrpLoadDll

  • LdrpLoadDllInternal

  • LdrpProcessWork

If everything is quite clear with LoadLibrary and LoadLibraryEx, then LdrLoadDll is called by a standard set of functions:

HMODULE hNtdll = LoadLibraryA("ntdll.dll");
pLdrLoadDll fnLdrLoadDll = (pLdrLoadDll)GetProcAddress(hNtdll, "LdrLoadDll");
UNICODE_STRING ModuleFileName;
RtlInitUnicodeStringEx(&ModuleFileName,L"user32.dll");
HANDLE hModule = NULL;
NTSTATUS status = fnLdrLoadDll((PWSTR)(0x7F08 | 1), 0, &ModuleFileName, &hModule);

What happens next is more complicated.

Let's try to call the non-exported function LdrpLoadDll.

The easiest way looks like this:

We get the signature in the form of bytes of the beginning of the function

Let's open IDA, find the function address in ntdll and open this address in Hex View:

From here we extract an array of sufficient length to uniquely identify the function pattern:

BYTE ldrpLoadDllStart[] = { 0x40, 0x55, 0x53, 0x56, 0x57, 0x41, 0x56, 0x41, 0x57, 0x48, 0x8D, 0x6C, 0x24, 0x88, 0x48, 0x81, 0xEC, 0x78, 0x01, 0x00, 0x00, 0x48, 0x8B, 0x05, 0xB8, 0xD1, 0x16, 0x00,  0x48, 0x33, 0xC4, 0x48, 0x89, 0x45, 0x60, 0x48 };

And we look for this array in the loaded module:

MODULEINFO modinfo = {};
HMODULE hNtdll = LoadLibraryA("ntdll.dll");
GetModuleInformation(GetCurrentProcess(), hNtdll, &modinfo, sizeof(modinfo));
void* addressLdrpLoadDllStart = 0;
int size = sizeof(ldrpLoadDllStart);
for (int i = 0; i < modinfo.SizeOfImage - size; i++)
{
if (memcmp((BYTE*)(modinfo.lpBaseOfDll) + i, ldrpLoadDllStart, size) == 0) {
		addressLdrpLoadDllStart = (BYTE*)(modinfo.lpBaseOfDll) + i;
		break;
		}
}

And we get the function:

pLdrpLoadDll ldrpLoadDll = (pLdrpLoadDll)addressLdrpLoadDllStart;

Also, for a correct call, we need the ldrpInitializeDllPath function, which we get in exactly the same way.

We call:

LDR_UNKSTRUCT someStruct = {};
LDR_DATA_TABLE_ENTRY* DllEntry = {};
ULONG flags = 0;
WCHAR origDllPath[] = L"user32.dll";
UNICODE_STRING uniOrigDllName;
RtlInitUnicodeStringEx(&uniOrigDllName, origDllPath);
ldrpInitializeDllPath(uniOrigDllName.Buffer, (PWSTR)(0x7F08 | 1), &someStruct);
ldrpLoadDll(&uniOrigDllName, &someStruct, NULL, &DllEntry);

Great, we got the downloaded library.

This method is simple, but will stop working with the next Windows update, since it often happens that the bytecode of functions changes.

This is why the paskalian code does not run now.

Here we remember a great tool from @MichelleVermishelle SymProcAddress

This tool allows you to get function addresses using debugging symbols.

Mikhail used it to get the addresses of exported functions without accessing the export table. We will use this idea to get the addresses of non-exported functions.

So, let's use Mikhail's idea with a little clarification. Standard symbols do not contain the functions we need. So let's see where IDA downloads symbols from:

Here we see that the symbols are downloaded via a link of the form http://msdl.microsoft.com/download/symbols/{module_name}.pdb/{some_hash}/{modulename}.pdb

This hash can be obtained like this:

GetPdbSignature
bool GetPdbSignature(const std::string& dllPath, GUID& pdbGuid, DWORD& pdbAge) {
	if (!SymInitialize(GetCurrentProcess(), NULL,TRUE)) {
		return false;
	}
	HMODULE hModule = LoadLibraryExA(dllPath.c_str(), NULL, DONT_RESOLVE_DLL_REFERENCES);
	if (!hModule) {
		SymCleanup(GetCurrentProcess());
		return false;
	}
	MODULEINFO modInfo;
	if (!GetModuleInformation(GetCurrentProcess(), hModule, &modInfo, sizeof(modInfo))) {
		FreeLibrary(hModule);
		SymCleanup(GetCurrentProcess());
		return false;
	}
	DWORD64 baseAddr = reinterpret_cast<DWORD64>(modInfo.lpBaseOfDll);
	IMAGEHLP_MODULE64 moduleInfo;
	ZeroMemory(&moduleInfo, sizeof(moduleInfo));
	moduleInfo.SizeOfStruct = sizeof(moduleInfo);

	if (!SymGetModuleInfo64(GetCurrentProcess(), (DWORD64)modInfo.lpBaseOfDll, &moduleInfo)) {
		FreeLibrary(hModule);
		SymCleanup(GetCurrentProcess());
		return false;
	}
	pdbGuid = moduleInfo.PdbSig70;
	pdbAge = moduleInfo.PdbAge;
	FreeLibrary(hModule);
	SymCleanup(GetCurrentProcess());

	return true;
}

Therefore, we will download the symbols ourselves and indicate that the symbols need to be searched for where we put them.

We get the GUID and AGE and form that very “hash”:

GUID pdbGuid;
DWORD pdbAge;
GetPdbSignature(dllPath, pdbGuid, pdbAge);
wchar_t guid_string[MAX_PATH] = {};
swprintf(
	guid_string, sizeof(guid_string) / sizeof(guid_string[0]),
	L"%08x%04x%04x%02x%02x%02x%02x%02x%02x%02x%02x%01x",
	pdbGuid.Data1, pdbGuid.Data2, pdbGuid.Data3,
	pdbGuid.Data4[0], pdbGuid.Data4[1], pdbGuid.Data4[2],
	pdbGuid.Data4[3], pdbGuid.Data4[4], pdbGuid.Data4[5],
	pdbGuid.Data4[6], pdbGuid.Data4[7], pdbAge);

We form a URL and download symbols to a file:

downloadDebugSymbols
bool downloadDebugSymbols(const std::wstring& guid, const std::wstring& filename) {
	std::wstring baseUrl = L"https://msdl.microsoft.com/download/symbols";
	std::wstring pdbUrl = baseUrl + L"/" + filename + L"/" + guid + L"/" + filename;

	HRESULT hr = URLDownloadToFileW(
		NULL,
		pdbUrl.c_str(),
		filename.c_str(),
		0,
		NULL
	);

	return SUCCEEDED(hr);
}
	bool success = downloadDebugSymbols(guid_string, L"ntdll.pdb");

Let's implement obtaining the address of the function we need:

GetAddressFromSymbols
FARPROC GetAddressFromSymbols(HANDLE hProcess, LPCSTR fullModulePath, LPCSTR pdbPath, LPCSTR lpProcName) {
	if (!SymInitialize(hProcess, NULL, TRUE)) {
		printf("SymInitialize failed: %lu\n", GetLastError());
		return 0;
	}
	if (!SymSetSearchPath(hProcess, pdbPath)) {
		printf("SymSetSearchPath failed: %lu\n", GetLastError());
		SymCleanup(hProcess);
		return 0;
	}
	DWORD64 baseOfDll = SymLoadModuleEx(hProcess, NULL, fullModulePath, NULL, 0, 0, NULL, 0);
	if (baseOfDll == 0) {
		printf("SymLoadModuleEx failed: %lu\n", GetLastError());
		SymCleanup(hProcess);
		return 0;
	}
	SYMBOL_INFO* symbol = (SYMBOL_INFO*)malloc(sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR));
	symbol->MaxNameLen = MAX_SYM_NAME;
	symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
	if (SymFromName(hProcess, lpProcName, symbol)) {
		printf("Symbol found: %s at address 0x%0llX\n", symbol->Name, symbol->Address);
		FARPROC result = (FARPROC)symbol->Address;
		free(symbol);
		SymCleanup(hProcess);
		return result;
	}
	else {
		printf("SymFromName failed: %lu\n", GetLastError());
	}
	free(symbol);
	SymCleanup(hProcess);
	return 0;
}

Let's move on to the implementation of DLL loading

Let's define all the structures and functions we need:

Lots of structures and prototypes
typedef struct _LSA_UNICODE_STRING {
	USHORT Length;
	USHORT MaximumLength;
	PWSTR  Buffer;
} LSA_UNICODE_STRING, * PLSA_UNICODE_STRING, UNICODE_STRING, * PUNICODE_STRING;

struct LDR_UNKSTRUCT
{
	PWSTR pInitNameMaybe;
	__declspec(align(16)) PWSTR Buffer;
	int Flags;
	PWSTR pDllName;
	char Pad1[84];
	BOOLEAN IsInitedMaybe;
	char Pad2[3];
};
typedef BOOLEAN(NTAPI* PLDR_INIT_ROUTINE)(
	_In_ PVOID DllHandle,
	_In_ ULONG Reason,
	_In_opt_ PVOID Context
	);
typedef struct _LDR_SERVICE_TAG_RECORD
{
	struct _LDR_SERVICE_TAG_RECORD* Next;
	ULONG ServiceTag;
} LDR_SERVICE_TAG_RECORD, * PLDR_SERVICE_TAG_RECORD;
typedef struct _LDRP_CSLIST
{
	PSINGLE_LIST_ENTRY Tail;
} LDRP_CSLIST, * PLDRP_CSLIST;
typedef enum _LDR_DDAG_STATE
{
	LdrModulesMerged = -5,
	LdrModulesInitError = -4,
	LdrModulesSnapError = -3,
	LdrModulesUnloaded = -2,
	LdrModulesUnloading = -1,
	LdrModulesPlaceHolder = 0,
	LdrModulesMapping = 1,
	LdrModulesMapped = 2,
	LdrModulesWaitingForDependencies = 3,
	LdrModulesSnapping = 4,
	LdrModulesSnapped = 5,
	LdrModulesCondensed = 6,
	LdrModulesReadyToInit = 7,
	LdrModulesInitializing = 8,
	LdrModulesReadyToRun = 9
} LDR_DDAG_STATE;
typedef struct _LDR_DDAG_NODE
{
	LIST_ENTRY Modules;
	PLDR_SERVICE_TAG_RECORD ServiceTagList;
	ULONG LoadCount;
	ULONG LoadWhileUnloadingCount;
	ULONG LowestLink;
	union
	{
		LDRP_CSLIST Dependencies;
		SINGLE_LIST_ENTRY* RemovalLink;
	};
	LDRP_CSLIST IncomingDependencies;
	LDR_DDAG_STATE State;
	SINGLE_LIST_ENTRY* CondenseLink;
	ULONG PreorderNumber;
	ULONG Pad;
} LDR_DDAG_NODE, * PLDR_DDAG_NODE;
typedef struct _RTL_BALANCED_NODE
{
	union
	{
		struct _RTL_BALANCED_NODE* Children[2];                             
		struct
		{
			struct _RTL_BALANCED_NODE* Left;                                
			struct _RTL_BALANCED_NODE* Right;                               
		};
	};
	union
	{
		struct
		{
			UCHAR Red : 1;                                                    
			UCHAR Balance : 2;                                                
		};
		ULONG ParentValue;                                                  
	};
} RTL_BALANCED_NODE, * PRTL_BALANCED_NODE;
typedef enum _LDR_DLL_LOAD_REASON
{
	LoadReasonStaticDependency,
	LoadReasonStaticForwarderDependency,
	LoadReasonDynamicForwarderDependency,
	LoadReasonDelayloadDependency,
	LoadReasonDynamicLoad,
	LoadReasonAsImageLoad,
	LoadReasonAsDataLoad,
	LoadReasonEnclavePrimary, 
	LoadReasonEnclaveDependency,
	LoadReasonPatchImage, 
	LoadReasonUnknown = -1
} LDR_DLL_LOAD_REASON, * PLDR_DLL_LOAD_REASON;
typedef enum _LDR_HOT_PATCH_STATE
{
	LdrHotPatchBaseImage,
	LdrHotPatchNotApplied,
	LdrHotPatchAppliedReverse,
	LdrHotPatchAppliedForward,
	LdrHotPatchFailedToPatch,
	LdrHotPatchStateMax,
} LDR_HOT_PATCH_STATE, * PLDR_HOT_PATCH_STATE;
typedef struct _LDRP_LOAD_CONTEXT
{
	UNICODE_STRING BaseDllName;
	LDR_UNKSTRUCT* UnkStruct;
	HANDLE SectionHandle;
	DWORD Flags;
	NTSTATUS* pStatus;
	LDR_DATA_TABLE_ENTRY* Entry;
	_LIST_ENTRY WorkQueueListEntry;
	LDR_DATA_TABLE_ENTRY* ReplacedEntry;
	LDR_DATA_TABLE_ENTRY** pvImports;
	LDR_DATA_TABLE_ENTRY** IATCheck;
	PVOID pvIAT;
	ULONG SizeOfIAT;
	ULONG CurrentDll;
	PIMAGE_IMPORT_DESCRIPTOR pImageImportDescriptor;
	ULONG ImageImportDescriptorLen;
	__declspec(align(8)) ULONG OriginalIATProtect;
	PVOID GuardCFCheckFunctionPointer;
	__int64 GuardFlags;
	__int64 DllNameLenCompare;
	__int64 UnknownFunc;
	SIZE_T Size;
	__int64 UnknownPtr;
	HANDLE FileHandle;
	PIMAGE_DOS_HEADER ImageBase;
	wchar_t BaseDllNameBuffer[260];
} LDRP_LOAD_CONTEXT, * PLDRP_LOAD_CONTEXT;
typedef struct _LDR_DATA_TABLE_ENTRY
{
	LIST_ENTRY InLoadOrderLinks;
	LIST_ENTRY InMemoryOrderLinks;
	union
	{
		LIST_ENTRY InInitializationOrderLinks;
		LIST_ENTRY InProgressLinks;
	};
	PIMAGE_DOS_HEADER DllBase;
	PLDR_INIT_ROUTINE EntryPoint;
	ULONG SizeOfImage;
	UNICODE_STRING FullDllName;
	UNICODE_STRING BaseDllName;
	union
	{
		UCHAR FlagGroup[4];
		ULONG Flags;
		struct
		{
			ULONG PackagedBinary : 1;
			ULONG MarkedForRemoval : 1;
			ULONG ImageDll : 1;
			ULONG LoadNotificationsSent : 1;
			ULONG TelemetryEntryProcessed : 1;
			ULONG ProcessStaticImport : 1;
			ULONG InLegacyLists : 1;
			ULONG InIndexes : 1;
			ULONG ShimDll : 1;
			ULONG InExceptionTable : 1;
			ULONG ReservedFlags1 : 2;
			ULONG LoadInProgress : 1;
			ULONG LoadConfigProcessed : 1;
			ULONG EntryProcessed : 1;
			ULONG ProtectDelayLoad : 1;
			ULONG ReservedFlags3 : 2;
			ULONG DontCallForThreads : 1;
			ULONG ProcessAttachCalled : 1;
			ULONG ProcessAttachFailed : 1;
			ULONG CorDeferredValidate : 1;
			ULONG CorImage : 1;
			ULONG DontRelocate : 1;
			ULONG CorILOnly : 1;
			ULONG ChpeImage : 1;
			ULONG ChpeEmulatorImage : 1;
			ULONG ReservedFlags5 : 1;
			ULONG Redirected : 1;
			ULONG ReservedFlags6 : 2;
			ULONG CompatDatabaseProcessed : 1;
		};
	};
	USHORT ObsoleteLoadCount;
	USHORT TlsIndex;
	LIST_ENTRY HashLinks;
	ULONG TimeDateStamp;
	struct _ACTIVATION_CONTEXT* EntryPointActivationContext;
	PVOID Lock; 
	PLDR_DDAG_NODE DdagNode;
	LIST_ENTRY NodeModuleLink;
	struct _LDRP_LOAD_CONTEXT* LoadContext;
	PVOID ParentDllBase;
	PVOID SwitchBackContext;
	RTL_BALANCED_NODE BaseAddressIndexNode;
	RTL_BALANCED_NODE MappingInfoIndexNode;
	ULONG_PTR OriginalBase;
	LARGE_INTEGER LoadTime;
	ULONG BaseNameHashValue;
	LDR_DLL_LOAD_REASON LoadReason; 
	ULONG ImplicitPathOptions;
	ULONG ReferenceCount; 
	ULONG DependentLoadFlags;
	UCHAR SigningLevel; 
	ULONG CheckSum; 
	PVOID ActivePatchImageBase;
	LDR_HOT_PATCH_STATE HotPatchState;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;

typedef NTSTATUS(__fastcall* pRtlInitUnicodeStringEx)(PUNICODE_STRING target, PCWSTR source);
typedef NTSTATUS(__fastcall* pLdrpLoadDll)(PUNICODE_STRING DllName, LDR_UNKSTRUCT* DllPathInited, ULONG Flags, LDR_DATA_TABLE_ENTRY** DllEntry);
typedef NTSTATUS(WINAPI* pfnLdrLoadDll)(PWCHAR PathToFile, ULONG Flags, PUNICODE_STRING ModuleFileName, PHANDLE ModuleHandle);
typedef NTSTATUS(__fastcall* pLdrpLoadDllInternal)(PUNICODE_STRING FullPath, LDR_UNKSTRUCT* DllPathInited, ULONG Flags, ULONG LdrFlags, PLDR_DATA_TABLE_ENTRY LdrEntry, PLDR_DATA_TABLE_ENTRY LdrEntry2, PLDR_DATA_TABLE_ENTRY* DllEntry, NTSTATUS* pStatus);
typedef NTSTATUS(__fastcall* pLdrpInitializeDllPath)(PWSTR DllName, PWSTR DllPath, LDR_UNKSTRUCT* DllPathInited);
typedef NTSTATUS(__fastcall* pLdrpPreprocessDllName)(PUNICODE_STRING DllName, PUNICODE_STRING ResName, PULONG pZero, PULONG pFlags);
typedef NTSTATUS(__fastcall* pLdrpFindOrPrepareLoadingModule)(PUNICODE_STRING FullPath, LDR_UNKSTRUCT* DllPathInited, ULONG Flags, ULONG LdrFlags, PLDR_DATA_TABLE_ENTRY LdrEntry, PLDR_DATA_TABLE_ENTRY* pLdrEntryLoaded, NTSTATUS* pStatus);
typedef NTSTATUS(__fastcall* pLdrpProcessWork)(PLDRP_LOAD_CONTEXT LoadContext, BOOLEAN IsLoadOwner);

Let's add a couple of DEFINEs to make it convenient to choose the method for loading DLLs:

#define MODE_LOADLIBRARYA 0
#define MODE_LOADLIBRARYEXA 1
#define MODE_LDRLOADDLL 2
#define MODE_LDRPLOADDLL 3
#define MODE_LDRPLOADDLLINTERNAL 4
#define MODE_LDRPPROCESSWORK 5

We implement library loading in all possible ways:

Library loading implementation
switch (mode)
	{
	case MODE_LOADLIBRARYA: {
		hResModule = LoadLibraryA(dllName);
		break;
	}
	case MODE_LOADLIBRARYEXA: {
		hResModule = LoadLibraryExA(dllName, 0, 0);
		break;
	}
	case MODE_LDRLOADDLL: {
		pLdrLoadDll fnLdrLoadDll = (pLdrLoadDll)GetProcAddress(hNtDll, "LdrLoadDll");
		UNICODE_STRING ModuleFileName;
		RtlInitUnicodeStringEx(&ModuleFileName, GetWC(dllName));
		HANDLE hModule = NULL;
		NTSTATUS status = fnLdrLoadDll((PWSTR)(0x7F08 | 1), 0, &ModuleFileName, &hModule);
		break;
	}
	case MODE_LDRPLOADDLL: {
		LDR_UNKSTRUCT someStruct = {};
		LDR_DATA_TABLE_ENTRY* DllEntry = {};
		ULONG flags = 0;
		UNICODE_STRING uniDllName;
		RtlInitUnicodeStringEx(&uniDllName, GetWC(dllName));
		pLdrpInitializeDllPath ldrpInitializeDllPath = (pLdrpInitializeDllPath)(GetAddressFromSymbols(GetCurrentProcess(), "C:\\Windows\\System32\\ntdll.dll", "./ntdll.pdb", "LdrpInitializeDllPath"));
		ldrpInitializeDllPath(uniDllName.Buffer, (PWSTR)(0x7F08 | 1), &someStruct);
		pLdrpLoadDll ldrpLoadDll = (pLdrpLoadDll)(GetAddressFromSymbols(GetCurrentProcess(), "C:\\Windows\\System32\\ntdll.dll", "./ntdll.pdb", "LdrpLoadDll"));
		ldrpLoadDll(&uniDllName, &someStruct, NULL, &DllEntry);
		break;
	}
	case MODE_LDRPLOADDLLINTERNAL: {
		LDR_UNKSTRUCT someStruct = {};
		LDR_DATA_TABLE_ENTRY* DllEntry = {};
		ULONG flags = 0;
		UNICODE_STRING uniDllName;
		RtlInitUnicodeStringEx(&uniDllName, GetWC(dllName));
		UNICODE_STRING FullDllPath;
		WCHAR Buffer[128];
		FullDllPath.Length = 0;
		FullDllPath.MaximumLength = MAX_PATH - 4;
		FullDllPath.Buffer = Buffer;
		Buffer[0] = 0;
		pLdrpPreprocessDllName ldrpPreprocessDllName = (pLdrpPreprocessDllName)(GetAddressFromSymbols(GetCurrentProcess(), "C:\\Windows\\System32\\ntdll.dll", "./ntdll.pdb", "LdrpPreprocessDllName"));
		pLdrpLoadDllInternal ldrpLoadDllInternal = (pLdrpLoadDllInternal)(GetAddressFromSymbols(GetCurrentProcess(), "C:\\Windows\\System32\\ntdll.dll", "./ntdll.pdb", "LdrpLoadDllInternal"));

		NTSTATUS res = ldrpPreprocessDllName(&uniDllName, &FullDllPath, 0, &flags);
		ldrpLoadDllInternal(&FullDllPath, &someStruct, flags, 0x4, 0, 0, &DllEntry, &res);

		break;
	}
	case MODE_LDRPPROCESSWORK: {
		LDR_DATA_TABLE_ENTRY* pLdrEntryLoaded = 0;
		LDR_UNKSTRUCT undefStruct = {};
		UNICODE_STRING uniDllName;
		RtlInitUnicodeStringEx(&uniDllName, GetWC(dllName));
		pLdrpInitializeDllPath ldrpInitializeDllPath = (pLdrpInitializeDllPath)(GetAddressFromSymbols(GetCurrentProcess(), "C:\\Windows\\System32\\ntdll.dll", "./ntdll.pdb", "LdrpInitializeDllPath"));
		ldrpInitializeDllPath(uniDllName.Buffer, (PWSTR)(0x7F08 | 1), &undefStruct);
		ULONG flags = 0;
		UNICODE_STRING FullDllPath;
		WCHAR Buffer[128];
		FullDllPath.Length = 0;
		FullDllPath.MaximumLength = MAX_PATH - 4;
		FullDllPath.Buffer = Buffer;
		Buffer[0] = 0;
		pLdrpPreprocessDllName ldrpPreprocessDllName = (pLdrpPreprocessDllName)(GetAddressFromSymbols(GetCurrentProcess(), "C:\\Windows\\System32\\ntdll.dll", "./ntdll.pdb", "LdrpPreprocessDllName"));
		NTSTATUS res = ldrpPreprocessDllName(&uniDllName, &FullDllPath, 0, &flags);
		pLdrpFindOrPrepareLoadingModule ldrpFindOrPrepareLoadingModule = (pLdrpFindOrPrepareLoadingModule)(GetAddressFromSymbols(GetCurrentProcess(), "C:\\Windows\\System32\\ntdll.dll", "./ntdll.pdb", "LdrpFindOrPrepareLoadingModule"));
		NTSTATUS Status = ldrpFindOrPrepareLoadingModule(&FullDllPath, &undefStruct, flags, 0x4, 0, &pLdrEntryLoaded, &res);
		pLdrpProcessWork ldrpProcessWork = (pLdrpProcessWork)(GetAddressFromSymbols(GetCurrentProcess(), "C:\\Windows\\System32\\ntdll.dll", "./ntdll.pdb", "LdrpProcessWork"));

		if (Status == STATUS_DLL_NOT_FOUND)
			NTSTATUS res = ldrpProcessWork(pLdrEntryLoaded->LoadContext, TRUE);
		break;
	}

As a result, we have a fully working tool for loading a library using undocumented functions.

Thanks paskalian for the great research. It made a lot of things clearer.

Thanks to @MichelleVermishelle for the debug symbols idea.

The tool is available on my GitHub

Subscribe to our telegram channel AUTHORITY

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *