Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Exported function forwarded to itself?

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.

like image 727
Jason Larke Avatar asked Nov 12 '22 11:11

Jason Larke


1 Answers

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);
}
like image 189
Jason Larke Avatar answered Nov 15 '22 05:11

Jason Larke