I ran into an extremely weird issue today when messing around with the parsing the Windows Portable Executable file structure today. Specifically in the Export table.
I found myself getting a Stack Overflow (so this seemed like the most appropriate QA board) when trying to resolve the function address of an Exported function in a DLL.
I've written my own version of GetProcAddress
which does the parsing manually rather than calling the existing GetProcAddress
method. Please don't just tell me to use the existing GetProcAddress
method, it's not suitable for my current situation and I want to learn something from this.
For most of the situations I encounter, my version has worked admirably and hasn't hit any issues. However the function was tested against a DLL named API-MS-Win-Core-ProcessThreads-L1-1-0.dll
(as part of a recursive parse of Kernel32.dll
) and this is when the StackOverflow occurred.
I've narrowed it down to the following function exported from API-MS-Win-Core-ProcessThreads-L1-1-0.dll
:
CreateRemoteThreadEx
Now, this exported function is actually a forwarded export. Usually this is no worries; I've written my function so that it should handle forwarded exports. However this function is forwarded to
api-ms-win-core-processthreads-l1-1-0.CreateRemoteThreadEx
Anyone seeing the problem here? Stepping through the code, my GetProcAddress
function then calls LoadLibrary
on api-ms-win-core-processthreads-l1-1-0
and then attempts to recursively lookup CreateRemoteThreadEx
. On the very next iteration, however, the CreateRemoteThreadEx
function is again forwarded... to
api-ms-win-core-processthreads-l1-1-0.CreateRemoteThreadEx
And so begins the StackOverflow. After a bit more investigation I found that the result of calling
LoadLibraryA("api-ms-win-core-processthreads-l1-1-0");
Returns the same result as
LoadLibraryA("kernel32.dll");
I'm stumped.
Here's my current code:
#include <Windows.h>
#define MKPTR(p1,p2) ((DWORD_PTR)(p1) + (DWORD_PTR)(p2))
INT LookupExport(IMAGE_DOS_HEADER* pDosHd, DWORD* pNames, DWORD nNames, LPCSTR lpProcName)
{
// Do a binary search on the name pointer table
INT start = 0,
index = -1,
middle = -1,
end = nNames - 1,
cmp = 0;
CHAR *pName;
while (start <= end && index == -1)
{
middle = (start + end) >> 1;
pName = (CHAR*)MKPTR(pDosHd, pNames[middle]);
if ((cmp = strcmp(pName, lpProcName)) == 0)
index = middle; // found
else if (cmp < 0)
start = middle + 1;
else
end = middle;
}
return index;
}
FARPROC InternalGetProcAddress(HMODULE hModule, LPCSTR lpProcName)
{
BOOL ordinalSearch = HIWORD(lpProcName) == 0;
WORD ordinal = 0;
IMAGE_DOS_HEADER *pDosHd = (IMAGE_DOS_HEADER*)hModule;
if (pDosHd->e_magic != IMAGE_DOS_SIGNATURE)
return NULL;
IMAGE_NT_HEADERS *pNtHd = (IMAGE_NT_HEADERS*)MKPTR(pDosHd, pDosHd->e_lfanew);
if (pNtHd->Signature != IMAGE_NT_SIGNATURE)
return NULL;
IMAGE_DATA_DIRECTORY directory = pNtHd->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (directory.Size == 0 || directory.VirtualAddress == 0)
return NULL;
IMAGE_EXPORT_DIRECTORY *pExports = (IMAGE_EXPORT_DIRECTORY*)MKPTR(pDosHd, directory.VirtualAddress);
if (!ordinalSearch)
{
INT index = LookupExport(pDosHd, (DWORD*)MKPTR(pDosHd, pExports->AddressOfNames), pExports->NumberOfNames, lpProcName);
if (index == -1)
return NULL;
ordinal = ((WORD*)MKPTR(pDosHd, pExports->AddressOfNameOrdinals))[index];
}
else
{
ordinal = LOWORD(lpProcName);
}
INT delta = pExports->Base - 1;
DWORD dwAddress = ((DWORD*)MKPTR(pDosHd, pExports->AddressOfFunctions))[ordinal - delta];
// Check whether forwarded:
if (dwAddress >= directory.VirtualAddress && dwAddress < (directory.VirtualAddress + directory.Size))
{
CHAR pForward[256];
strcpy(pForward, (CHAR*)MKPTR(pDosHd, dwAddress));
CHAR *pFunction = strchr(pForward, '.');
if (pFunction == NULL)
return NULL;
// break into seperate parts and recurse
*pFunction++ = 0;
return InternalGetProcAddress(LoadLibraryA(pForward), pFunction);
}
return (FARPROC)MKPTR(hModule, dwAddress);
}
Any insight would be greatly appreciated.
Okay after following @sergmat's advice I took a look at the API Set documentation (found here for anyone interested). I've now modified my GetProcAddress code to do a naive lookup of the Api Set table.
#include <Windows.h>
#define MKPTR(p1,p2) ((DWORD_PTR)(p1) + (DWORD_PTR)(p2))
typedef struct _stripped_peb32 {
BYTE unused1[0x038];
PVOID ApiSet;
BYTE unused2[0x1AC];
} PEB32;
typedef struct _stripped_peb64 {
BYTE unused1[0x068];
PVOID ApiSet;
BYTE unused2[0x23C];
} PEB64;
typedef struct _PROCESS_BASIC_INFORMATION {
PVOID Reserved1;
LPVOID PebBaseAddress;
PVOID Reserved2[2];
ULONG_PTR UniqueProcessId;
PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;
typedef struct _api_set_host {
DWORD ImportModuleName;
WORD ImportModuleNameLength;
DWORD HostModuleName;
WORD HostModuleNameLength;
} API_SET_HOST;
typedef struct _api_set_host_descriptor {
DWORD NumberOfHosts;
API_SET_HOST Hosts[1];
} API_SET_HOST_DESCRIPTOR;
typedef struct _api_set_entry {
DWORD Name;
WORD NameLength;
DWORD HostDescriptor;
} API_SET_ENTRY;
typedef struct _api_set_header {
DWORD unknown1;
DWORD NumberOfEntries;
API_SET_ENTRY Entries[1];
} API_SET_HEADER;
typedef NTSTATUS (__stdcall *fnNtQueryInformationProcess)(HANDLE ProcessHandle, DWORD ProcessInformationClass, PVOID ProcessInformation, ULONG ProcessInformationLength, PULONG ReturnLength);
API_SET_HEADER *GetApiSetHeader()
{
fnNtQueryInformationProcess NtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(LoadLibraryW(L"ntdll.dll"), "NtQueryInformationProcess");
if (!NtQueryInformationProcess)
return NULL;
PROCESS_BASIC_INFORMATION info;
if (NtQueryInformationProcess(GetCurrentProcess(), 0, &info, sizeof(info), NULL) != S_OK)
return NULL;
#if defined(_WIN32)
return (API_SET_HEADER*)(((PEB32*)info.PebBaseAddress)->ApiSet);
#elif defined(_WIN64)
return (API_SET_HEADER*)(((PEB64*)info.PebBaseAddress)->ApiSet);
#else
return NULL; // unsupported architecture
#endif
}
HMODULE ResolveImportMap(LPCSTR lpModuleName)
{
API_SET_HEADER *pHeader = GetApiSetHeader();
if (pHeader == NULL)
return NULL;
API_SET_ENTRY *pEntry = pHeader->Entries;
API_SET_HOST_DESCRIPTOR* pDescriptor;
wchar_t module[128];
// First, normalize the LPCSTR, the API Set table doesn't have the API- prefix
if (strnicmp("api-", lpModuleName, 4) == 0)
lpModuleName += 4;
// Next convert the LPCSTR to a unicode string for comparison and remove the extension (if found)
mbstowcs(module, lpModuleName, sizeof(module) / sizeof(wchar_t));
wchar_t *dot = wcsrchr(module, L'.');
if (dot) *dot = L'\0';
// Begin the lookup:
// todo: implement a case-insensitive binary search, not much to be gained for the effort IMO as there's
// only 35 entries in the current version of Windows 7, but the option is there for performance nuts.
for(unsigned long i = 0; i < pHeader->NumberOfEntries; ++i, ++pEntry)
{
// Check the top-level host map
if (wcsnicmp(module, (const wchar_t*)MKPTR(pHeader, pEntry->Name), pEntry->NameLength) == 0)
{
pDescriptor = (API_SET_HOST_DESCRIPTOR*)MKPTR(pHeader, pEntry->HostDescriptor);
// iterate backwards through the hosts to find the most important one (I think this is naive)
for(unsigned long j = pDescriptor->NumberOfHosts; j > 0; --j)
{
if (pDescriptor->Hosts[j - 1].HostModuleNameLength)
{
memcpy(module, (const void*)MKPTR(pHeader, pDescriptor->Hosts[j - 1].HostModuleName), pDescriptor->Hosts[j - 1].HostModuleNameLength);
module[pDescriptor->Hosts[j - 1].HostModuleNameLength / sizeof(wchar_t)] = L'\0';
return GetModuleHandleW(module); // All the modules should already be loaded, so use GetModuleHandle rather than LoadLibrary
}
}
}
}
return NULL;
}
INT LookupExport(IMAGE_DOS_HEADER* pDosHd, DWORD* pNames, DWORD nNames, LPCSTR lpProcName)
{
// Do a binary search on the name pointer table
INT start = 0,
index = -1,
middle = -1,
end = nNames - 1,
cmp = 0;
CHAR *pName;
while (start <= end && index == -1)
{
middle = (start + end) >> 1;
pName = (CHAR*)MKPTR(pDosHd, pNames[middle]);
if ((cmp = strcmp(pName, lpProcName)) == 0)
index = middle;
else if (cmp < 0)
start = middle + 1;
else
end = middle;
}
return index;
}
FARPROC InternalGetProcAddress(HMODULE hModule, LPCSTR lpProcName)
{
if (hModule == NULL)
return NULL;
BOOL ordinalSearch = HIWORD(lpProcName) == 0;
WORD ordinal = 0;
IMAGE_DOS_HEADER *pDosHd = (IMAGE_DOS_HEADER*)hModule;
if (pDosHd->e_magic != IMAGE_DOS_SIGNATURE)
return NULL;
IMAGE_NT_HEADERS *pNtHd = (IMAGE_NT_HEADERS*)MKPTR(pDosHd, pDosHd->e_lfanew);
if (pNtHd->Signature != IMAGE_NT_SIGNATURE)
return NULL;
IMAGE_DATA_DIRECTORY directory = pNtHd->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (directory.Size == 0 || directory.VirtualAddress == 0)
return NULL;
IMAGE_EXPORT_DIRECTORY *pExports = (IMAGE_EXPORT_DIRECTORY*)MKPTR(pDosHd, directory.VirtualAddress);
if (!ordinalSearch)
{
INT index = LookupExport(pDosHd, (DWORD*)MKPTR(pDosHd, pExports->AddressOfNames), pExports->NumberOfNames, lpProcName);
if (index == -1)
return NULL;
ordinal = ((WORD*)MKPTR(pDosHd, pExports->AddressOfNameOrdinals))[index];
}
else
{
ordinal = LOWORD(lpProcName);
}
INT ordbase = pExports->Base - 1;
DWORD dwAddress = ((DWORD*)MKPTR(pDosHd, pExports->AddressOfFunctions))[ordinal - ordbase];
// Check whether forwarded:
if (dwAddress >= directory.VirtualAddress && dwAddress < (directory.VirtualAddress + directory.Size))
{
CHAR pForward[256];
strcpy(pForward, (CHAR*)MKPTR(pDosHd, dwAddress));
CHAR *pFunction = strchr(pForward, '.');
if (pFunction == NULL)
return NULL;
// break into seperate parts and recurse
*pFunction++ = 0;
// check if ordinal-forwarded
if (*pFunction == '#')
pFunction = (PSTR)(unsigned short)(atoi(++pFunction));
HMODULE hDestination = LoadLibraryA(pForward);
// detect an infinite loop, the forward declaration requests the same module handle with
// the same function lookup, this could be an Api Set (Windows7+)
if (hDestination == hModule && (ordinalSearch ? LOWORD(pFunction) == LOWORD(lpProcName) : strcmp(pFunction, lpProcName) == 0))
hDestination = ResolveImportMap(pForward); // ResolveImportMap will return NULL if not an API Set and so avoid infinite recursion
return InternalGetProcAddress(hDestination, pFunction);
}
return (FARPROC)MKPTR(hModule, dwAddress);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With