Injecting code into other process memory is not only limited to shellcodes or DLLs.
PE Injection technique enables to inject and run a complete executable module inside another process memory.
What is PE injection?
This technique is similar to reflective DLL injection, since they do not drop any files to the disk: reflective DLL injection works by creating a DLL that maps itself into memory when executed, instead of relying on the Window’s loader.
Like reflective DLL injection, this technique does not rely on LoadLibrary function, but copy its malicious code into an existing open process and cause it to execute (using a shellcode, or by calling CreateRemoteThread function).
The malware allocates memory in a host process, and instead of writing a “DLL path” it writes its malicious code by calling WriteProcessMemory.
What is PE (Portable Executable) format?
The Portable Executable format is the standard file format for executables, object code and Dynamic Link Libraries (DLLs) used in 32- and 64-bit versions of Windows operating systems.
The PE format is a data structure that encapsulates the information necessary for the Windows loader to manage the wrapped executable code.
This includes DLL references for linking, API export and import tables, resource management data and thread-local storage data.
Just one critical point: memory addresses
The Import Address Table is used to allow PE loader to set addresses of DLL functions without having to modify the code of the application: in fact, when a DLL is loaded into memory it is not guaranteed to be loaded at the same address every time.
When running in memory portable executables make use of 2 structures: IAT (Import Address Table), and Base Relocation Table.
As with DLLs, it is also possible that the application itself is not loaded at the same address every time: the Base Relocation Table is a table of pointers to every absolute address used in the code.
During process initialization, if the process is not being loaded at its base address, the PE loader will modify all the absolute addresses to work with the new base address.
The Import Address Table and Reloc Table remain in memory once the process initialization is finished: with the ability to be loaded at any base address and use DLLs at any address, the process can get its current base address and image size from the PE header, and copy itself to any region of memory in almost any process.
Here the injection process drill-down:
- Get the current image base address and size from the PE header.
- Allocate enough memory for the image inside the processes own address space using VirtualAlloc.
- Have the process copy its own image into the locally allocated memory using memcpy function.
- Call VirtualAllocEx in order to allocate memory large enough to fit the image in the target process.
- Calculate the offset of the reloc table for the image that was copied into the local memory.
- Iterate the reloc table of the local image and modify all absolute addresses to work at the address returned by VirtualAllocEx.
- Copy the local image into the memory region allocated in the target process using WriteProcessMemory function.
- Calculate the remote address of the function to be executed in the remote process by subtracting the address of the function in the current process by the base address of the current process, then adding it to the address of the allocated memory in the target process.
- Finally create a new thread with the start address set to the remote address of the function, using CreateRemoteThread.
When a malware injects its PE into another process it will have a new base address which is unpredictable, requiring it to dynamically recompute the fixed addresses of its PE.
To overcome this, the malicious code needs to find its relocation table address in the host process, and resolve the absolute addresses of the copied image by looping through its relocation descriptors.
How to identify PE injection?
When analyzing a PE injection, because of these research operations in the relocation table, it is very common to see loops (usually two “for” loops, one nested in the other), before a call to CreateRemoteThread.
Further artifacts can be the following:
- A processes allocating memory inside another process, relying on NtAllocateVirtualMemory, VirtualAllocEx, ZwMapViewOfSection functions.
- A process that uses NtProtectVirtualMemory and VirtualProtectEx functions to set the PAGE_EXECUTE flag of a memory region in another process.
- A process that call CreateRemoteThread for creating a thread in another process, especially if the thread points to code within a memory region allocated by the same process.
- A process appending code to shared sections.