A portable executable (PE) file can describe a number of different types of object, including EXE files, DLLs, screensavers, and drivers. All PE files have a set of headers, followed by a set of sections.
The headers and tables describe the type of executable, the functionality it supports, the APIs it calls, etc. The sections tell the PE loader how to map file data into memory.
There are three main headers and two main tables:
- DOS Header - Contains the magic number (MZ), the DOS stub, and some very low-level information about the executable. This is largely legacy now, and is only included for historical purposes.
- Common Object File Format (COFF) Header - Contains machine type (e.g. i386 or Intel64) and some basic flags about the type of executable that this file is.
- Portable Executable (PE) Header - Contains information about the runtime of the process, e.g. entry point address, code and data sizes, base addresses, the Windows subsystem to run in (e.g. GUI, console, driver, EFI, etc.), the characteristics of the module (e.g. is it NX / ASLR compatible?), the initial stack and heap sizes, etc.
- Data Directory Table - A set of pointers (RVAs) and sizes for sixteen "special" segments of the file data, e.g. the import / export directories, the debug directory, .NET metadata, security directory, relocation table, TLS directories, etc
- Section Table - A table that represents various sections within the executable. Sections contain the data that constitutes the actual program, e.g. the instructions.
When the OS loads a PE file, it creates blocks of virtual memory for each section, with the addresses, sizes, and flags specified in the section table, and copies the data from the file into these memory sections. There are two special sections: code and data. The code and data sections are identified by the
BaseOfData fields in the PE header, and are assigned to the
DS segment registers respectively. The
eip register (or
rip in 64-bit) is then set to the value as specified in the
AddressOfEntryPoint field in the PE header, and the program begins.
The PE format was not designed to be resistant against modification. There are many vectors to code injection at the file level:
- Add a new section to the section table, mark it as code, then change
AddressOfEntryPoint to point to your new section. You can now execute code and return back to the original entry point (OEP). This is easy for AVs to detect because the EP points to outside the code section.
- Inject code into the "slack" space at the end of the original code section, then modify the entry point and jump back to OEP at the end of your injected code. This slack space is usually limited in size, so it's not always this simple. It may also be possible to use dead code as slack space.
- Expand the original code section, shift the data down, inject your code at OEP, re-map the entire section table, re-map any fixups, and rebuild the imports and exports directories. This is difficult, and essentially involved re-building the entire file, but is difficult for AVs to detect.
- Add a new data directory that contains instructions (often encrypted), then inject a small stub into the program (through any of the above methods) that allocates a new executable memory block and copies the data into it. If encryption is used, the stub decrypts the instructions. This new code block can then be jumped to directly.
- Add TLS callback entries that include an initialisation callback to some code you have injected into the file (e.g. via a new section).
- Modify known functions to do something different entirely.
- Modify call tables to point to alternative (patched) code.
- Many more...
As you can see, if an adversary has write access to a PE file, they can inject their code pretty easily. The best protection against this is digital signatures, which are managed inside the security directory of the PE file. A digital signature is essentially a certificate that is signed by a trusted provider, or by a provider whose certificate authority you trust. This certificate is then embedded in the PE file, and matches the data exactly. If you modify the PE file, or modify the signature, the PE loader will reject the file. Of course, it is also possible to simply strip the signature from the file, so that it runs without any certificate. This can be prevented via local policy, in which you can set a flag that requires all executables to be signed.
Unfortunately, this isn't the only way to inject code. The alternative is to inject memory into a process at runtime, then start a remote thread inside that process. This is relatively trivial in Windows:
OpenProcess on the target process, with
VirtualAllocEx to allocate a block of executable memory in the remote process.
WriteProcessMemory to write code into that memory.
CreateRemoteThread to start a new thread in the process, using the base of the allocated memory as the thread entry point.
This causes code to run in the context of the target process. However, this is only possible when step 1 succeeds, and this is reliant on the malicious process having access to the other process. A low-privilege user cannot open such a handle to a high-privilege (e.g. administrator) user's process. However, if the malicious process is running with high privileges, all bets are off.
There is very little that can be done to protect you from these attacks once a malicious application is running with write access to processes and executable files.