Running static PE files as dynamic processes
At this point, you have a general idea of how a minimal program is generated, compiled, and packaged into an executable file by the compiler in the static phase. So, the next question is, What does the OS do to get a static program running?
Figure 1.6 shows the process structure of how an EXE program is transformed from a static to a dynamic process under the Windows system:
Figure 1.6 – Dynamic operation of the process hatching flow
Note that this is different from the process hatching process in the latest version of Windows. For the sake of explanation, we'll ignore the processes of privilege escalation, the patching mechanism, and kernel generation, and only talk about how a static program is correctly parsed and run from a single execution.
On Windows systems, all processes must be hatched by the parent process by interrupting the system function to jump to the kernel level. For example, a parent process is currently trying to run the cmd.exe /c whoami
command, which is an attempt to hatch the cmd.exe
static file into a dynamic process and assign its parameters to /
c whoami
.
So, what happens in the whole process? As shown in Figure 1.6, these are the steps:
- The parent process makes a request to the kernel with
CreateProcess
, specifying to generate a new process (child process). - Next, the kernel will produce a new process container and fill the execution code into the container with file mapping. The kernel will create a thread to assign to this child process, which is commonly known as the main thread or GUI thread. At the same time, the kernel will also arrange a block of memory in Userland’s dynamic memory to store two structural blocks: a process environment block (PEB) for recording the current process environment information and a thread environment block (TEB) for recording the first thread environment information. The details of these two structures will be fully introduced in Chapter 2, Process Memory – File Mapping, PE Parser, tinyLinker, and Hollowing, and in Chapter 3, Dynamic API Calling – Thread, Process, and Environment Information.
- The
NtDLL
export function,RtlUserThreadStart
, is the main routing function for all threads and is responsible for the necessary initialization of each new thread, such as the generation of structured exception handling (SEH). The first thread of each process, that is, the main thread, will executeNtDLL!LdrInitializeThunk
at the user level and enter theNtDLL!LdrpInitializeProcess
function after the first execution. It is the executable program loader, responsible for the necessary correction of the PE module loaded into the memory. - After the execution loader completes its correction, it jumps back to the current execution entry (
AddressOfEntryPoint
), which is the developer’smain
function.
Important note
From a code perspective, a thread can be thought of as a person responsible for executing code, and a process can be thought of as a container for loading code.
The kernel layer is responsible for file mapping, which is the process of placing the program content based on the preferred address during the compiling period. For example, if the image base address is 0x400000
and the .text
offset is 0x1000
, then the file mapping process is essentially a simple matter of requesting a block of memory at the 0x400000
address in the dynamic memory and writing the actual contents of .text
to 0x401000
.
In fact, the loader function (NtDLL! LdrpInitializeProcess
) does not directly call AddressOfEntryPoint
after execution; instead, the tasks corrected by the loader and the entry point are treated as two separate threads (in practice, two thread contexts will be opened). NtDLL!NtContinue
will be called after the correction and will hand over the task to the entry to continue execution as a thread task schedule.
The entry point of the execution is recorded in NtHeaders→OptionalHeader.AddressOfEntryPoint
of the PE structure, but it is not directly equivalent to the main function of the developer. This is for your understanding only. Generally speaking, AddressOfEntryPoint
points to the CRTStartup
function (C++ Runtime Startup), which is responsible for a series of C/C++ necessity initialization preparations (e.g., cutting arguments into developer-friendly argc
and argv
inputs, etc.) before calling the developer’s main function.
In this section, we learned how EXE files are incubated from static to dynamically running processes on the Windows system. With the process and thread, and the necessary initialization actions, the program is ready to run.