Disclaimer: The implementation of these APIs is likely to change between versions of Windows. I will be referencing 32-bit Windows XP SP3 in my answer. Your results may vary.
How thread creation works
There are three structures that must be initialized before calling NtCreateThread
:
INITIAL_TEB
: Contains pointers to the stack region
CONTEXT
: Contains the register state
OBJECT_ATTRIBUTES
: Contains security attributes for the thread
In my implementation, there are dedicated functions that handle each of these tasks: BaseCreateStack
, BaseInitializeContext
, and BaseFormatObjectAttributes
, respectively.
The BaseInitializeContext
function is the one you're interested in, however, since the new thread will begin at CONTEXT.Eip
.
Interestingly, BaseInitializeContext
instead puts the thread's start address (i.e. the entry point of the new process) in CONTEXT.Eax
. And CONTEXT.Eip
is set to the address of BaseProcessStartThunk
. (Since kernel32
is mapped at the same address in every process, we know this will also be the address of BaseProcessStartThunk
in the other process)
So when we call NtCreateThread
, we start a new thread in the other process at BaseProcessStartThunk
with eax
equal to the entry point.
BaseProcessStartThunk
saves the start address from register eax
. It sets the start address internally by calling NtSetInformationThread
with a ThreadInformationClass
of ThreadQuerySetWin32StartAddress
(see ntddk.h
). It then calls the start address. Finally, when the thread returns, it calls ExitThread
.
How the executable image is mapped into the new process
If you want to know the process was created in the first place, we have to go back a few steps.
First, a handle to the new process executable is opened via NtOpenFile
.
The file handle is used to create a section object via NtCreateSection
.
A call to NtQuerySection
with InformationClass
set to SectionImageInformation
is made. This parses the the section object and fills out a SECTION_IMAGE_INFORMATION
structure, which most notably includes fields the EntryPoint
field. This is how the entry point of the new process is determined.
Eventually, NtCreateProcessEx
is called, given the section handle from NtCreateSection
as a parameter. This is what actually creates the new process and maps the executable image into the new process' address space, among many other things. NtCreateProcessEx
also provides the process handle that we pass to NtCreateThread
to create the new thread.