📄
字号:
所以第三阶段就是创建其初始线程。
[code][CreateProcessW()]
/*
* Create the thread for the kernel
*/
. . . . . .
hThread = KlCreateFirstThread(hProcess, lpThreadAttributes, &Sii,
(PVOID)((ULONG_PTR)ImageBaseAddress + Sii.EntryPoint),
dwCreationFlags, &lpProcessInformation->dwThreadId);
. . . . . .
lpProcessInformation->hProcess = hProcess;
lpProcessInformation->hThread = hThread;
return TRUE;
}[/code]
KlCreateFirstThread()是kernel32.dll中的一个内部(未导出)函数,这实际上是个不小的操作,而且“技术含量”比较高,从某种意义上说这才是CreateProcessW()的核心。这里传下去的参数中有两个是特别值得一提的。一个是SECTION_IMAGE_INFORMATION数据结构Sii,这个数据结构中的信息来自代表着目标映像文件的共享内存区对象,是前面通过ZwQuerySection()获取的,里面有些信息实际上来自目标映像文件。例如,Sii.EntryPoint就是程序入口在映像中的位移;而Sii.Subsystem说明了目标映像属于哪一个子系统,是GUI还是CUI。读者如果用微软的Visual Studio开发过软件,就一定知道在创建一个项目(Project)时首先就要确定目标软件是面向图形界面的还是面向控制台的。另一个参数(ImageBaseAddress + Sii.EntryPoint)则是目标映像映射到用户空间以后的程序入口地址。
[code][CreateProcessW() > KlCreateFirstThread()]
HANDLE STDCALL
KlCreateFirstThread(HANDLE ProcessHandle,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
PSECTION_IMAGE_INFORMATION Sii,
LPTHREAD_START_ROUTINE lpStartAddress,
DWORD dwCreationFlags,
LPDWORD lpThreadId)
{
OBJECT_ATTRIBUTES oaThreadAttribs;
CLIENT_ID cidClientId;
PVOID pTrueStartAddress;
NTSTATUS nErrCode;
HANDLE hThread;
/* convert the thread attributes */
RtlRosR32AttribsToNativeAttribs(&oaThreadAttribs, lpThreadAttributes);
/* native image */
if(Sii->Subsystem != IMAGE_SUBSYSTEM_NATIVE)
pTrueStartAddress = (PVOID)BaseProcessStart; /* Win32 image */
else
pTrueStartAddress = (PVOID)RtlBaseProcessStartRoutine;
. . . . . .
/* create the first thread */
nErrCode = RtlRosCreateUserThreadVa(ProcessHandle, &oaThreadAttribs,
dwCreationFlags & CREATE_SUSPENDED, 0,
&(Sii->StackReserve), &(Sii->StackCommit),
pTrueStartAddress, &hThread, &cidClientId,
2, (ULONG_PTR)lpStartAddress, (ULONG_PTR)PEB_BASE);
. . . . . .
if(lpThreadId) *lpThreadId = (DWORD)cidClientId.UniqueThread;
return hThread;
}[/code]
显然,实际的线程创建是由RtlRosCreateUserThreadVa()完成的。这里特别要注意的是指针pTrueStartAddress的设置。对于GUI或CUI的Windows应用,这个指针设置成指向BaseProcessStart()。这是Kernel32.dll中的一个内部函数。再注意调用RtlRosCreateUserThreadVa()时的参数中有两个指针:一个是pTrueStartAddress;另一个是lpStartAddress,那就是作为参数传下来的目标映像程序入口。后面读者就会看到,当新建的线程受调度运行而“返回”到用户空间时,首先进入的是pTrueStartAddress所指的BaseProcessStart(),然后再由BaseProcessStart()把lpStartAddress作为函数指针加以调用。之所以如此,是为了把目标映像的执行置于“结构化出错保护”即SHE的保护之下。按理说,目标映像的程序入口才是“真正”的起始地址;但是说pTrueStartAddress是“真正”的起始地址也没有错,因为这确实是新建线程进入用户空间时的起始地址。或许应该说,前者是逻辑意义上的起始地址,而后者则是物理意义上的起始地址。
看了漫谈十中关于BaseProcessStart()的叙述(来自“Internals”一书)、以及与此有关的代码后,我们兼容内核研发团队的胡晨展先生提出了质疑:把pTrueStartAddress设置成指向BaseProcessStart()的是当前线程,所以是当前进程用户空间的BaseProcessStart(),在当前进程用户空间的kernel32.dll映像中。可是,这里实际需要的却是新建线程所在用户空间中的BaseProcessStart(),这应该在新建线程所在用户空间的kernel32.dll映像中,然而此刻新建线程的kernel32.dll映像尚未映射。所以,这样的安排只有在kernel32.dll的装入地址不变、即不管在哪一个进程中都是装入到相同的地址的条件下才能成立。为此,他又在Windows上做了实验,故意把kernel32.dll期望装入的地址先占了,结果是Windows显示出错信息说不能装入kernel32.dll、所以不能运行目标映像。他的实验结果说明这样的安排在实际效果上还是可行的。但是,如果Windows的代码中也是这样处理的话(按“Internals”书中所说确实就是这样),这说明了Windows的程序设计至少在这个问题上存在着逻辑混乱。
言归正传,我们往下看RtlRosCreateUserThreadVa()的代码。
[code][CreateProcessW() > KlCreateFirstThread() > RtlRosCreateUserThreadVa()]
NTSTATUS CDECL
RtlRosCreateUserThreadVa(IN HANDLE ProcessHandle,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN BOOLEAN CreateSuspended,
IN LONG StackZeroBits,
IN OUT PULONG StackReserve OPTIONAL,
IN OUT PULONG StackCommit OPTIONAL,
IN PVOID StartAddress,
OUT PHANDLE ThreadHandle OPTIONAL,
OUT PCLIENT_ID ClientId OPTIONAL,
IN ULONG ParameterCount,
...)
{
va_list vaArgs;
NTSTATUS nErrCode;
va_start(vaArgs, ParameterCount);
/* FIXME: this code makes several non-portable assumptions:
all parameters are passed on the stack, the stack is a contiguous array of cells as
large as an ULONG_PTR, the stack grows downwards. This happens to work on
the Intel x86, but is likely to bomb horribly on most other platforms */
nErrCode = RtlRosCreateUserThread(ProcessHandle, ObjectAttributes, CreateSuspended,
StackZeroBits, StackReserve, StackCommit,
StartAddress, ThreadHandle, ClientId,
ParameterCount, (ULONG_PTR *)vaArgs);
va_end(vaArgs);
return nErrCode;
}[/code]
这个函数的调用参数表是可变长度的,参数ParameterCount表明后面还有几个参数。前后比对一下,就可以知道后面还用两个参数,一个是lpStartAddress,另一个是常数PEB_BASE,即新建进程的PEB指针。
注意这里的参数StartAddress其实是前面代码中的pTrueStartAddress,指向BaseProcessStart()。而前面的lpStartAddress、即目标映像的程序入口,则在ParameterCount后面,在代码中不能直接看到了。
所以,这个函数的作用只是把可变长度的参数表转换成固定长度的参数表。不过RtlRosCreateUserThreadVa()的参数表长度虽然是固定的,但是最后的指针数组vaArgs却是大小可变的,所以实质上仍是一样。我们继续往下看:
[code]
[CreateProcessW() > KlCreateFirstThread() > RtlRosCreateUserThreadVa()
> RtlRosCreateUserThread()]
NTSTATUS STDCALL
RtlRosCreateUserThread(IN HANDLE ProcessHandle,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN BOOLEAN CreateSuspended,
IN LONG StackZeroBits,
IN OUT PULONG StackReserve OPTIONAL,
IN OUT PULONG StackCommit OPTIONAL,
IN PVOID StartAddress,
OUT PHANDLE ThreadHandle OPTIONAL,
OUT PCLIENT_ID ClientId OPTIONAL,
IN ULONG ParameterCount,
IN ULONG_PTR * Parameters)
{
. . . . . .
/* allocate the stack for the thread */
nErrCode = RtlRosCreateStack(ProcessHandle, &usUserInitialTeb,
StackZeroBits, StackReserve, StackCommit);
. . . . . .
/* initialize the registers and stack for the thread */
nErrCode = RtlRosInitializeContext(ProcessHandle, &ctxInitialContext, StartAddress,
&usUserInitialTeb, ParameterCount, Parameters);
. . . . . .
/* create the thread object */
nErrCode = NtCreateThread(ThreadHandle, THREAD_ALL_ACCESS, ObjectAttributes,
ProcessHandle, ClientId, &ctxInitialContext,
&usUserInitialTeb, CreateSuspended);
. . . . . .
return STATUS_SUCCESS;
}[/code]
这里要做的是三件大事,前两件都是为最后对NtCreateThread()的调用作准备。
首先通过RtlRosCreateStack()建立新线程的用户空间堆栈。在所用到的几个参数中,StackZeroBits表示堆栈起始地址中前导0的位数,所以这是选择堆栈位置的一个准则。另一个参数StackReserve表示堆栈大小的上限,而StackCommit则是此刻需要加以映射的区间大小。二者相等就是固定大小的堆栈,否则就是可变大小的堆栈。
下面的RtlRosInitializeContext()可以说是许多奥妙所在,正是这个函数伪造了新线程的“上下文(Context)”,从而决定了当新建线程被调度运行并进入其用户空间运行时所走的路线。可以理解,一个逻辑意义上的、完整的“上下文”、应该由两部分构成,一部分在新建线程的系统空间堆栈上,另一部分在它的用户空间堆栈上。前者先建立在这里作为缓冲区使用的数据结构ctxInitialContext上,然后将其作为参数传递给NtCreateThread(),使其被复制到新建线程的系统空间堆栈上。至于后者,则取决于刚才建立的用户空间堆栈在什么地方。上下文的这两个部分都是由RtlRosInitializeContext()创建的。
[code][CreateProcessW() > KlCreateFirstThread() > RtlRosCreateUserThreadVa()
> RtlRosCreateUserThread() > RtlRosInitializeContext()]
NTSTATUS NTAPI
RtlRosInitializeContext(IN HANDLE ProcessHandle, OUT PCONTEXT Context,
IN PVOID StartAddress, IN PINITIAL_TEB InitialTeb,
IN ULONG ParameterCount, IN ULONG_PTR * Parameters)
{
static PVOID s_pRetAddr = (PVOID)0xDEADBEEF;
. . . . . .
/* Intel x86: linear top-down stack, all parameters passed on the stack */
/* get the stack base and limit */
nErrCode = RtlpRosGetStackLimits(InitialTeb, &pStackBase, &pStackLimit);
. . . . . .
/* initialize the context */
Context->ContextFlags = CONTEXT_FULL;
Context->FloatSave.ControlWord = FLOAT_SAVE_CONTROL;
Context->FloatSave.StatusWord = FLOAT_SAVE_STATUS;
Context->FloatSave.TagWord = FLOAT_SAVE_TAG;
Context->FloatSave.DataSelector = FLOAT_SAVE_DATA;
Context->Eip = (ULONG_PTR)StartAddress;
Context->SegGs = USER_DS;
Context->SegFs = TEB_SELECTOR;
Context->SegEs = USER_DS;
Context->SegDs = USER_DS;
Context->SegCs = USER_CS;
Context->SegSs = USER_DS;
Context->Esp = (ULONG_PTR)pStackBase - (nParamsSize + sizeof(ULONG_PTR));
Context->EFlags = ((ULONG_PTR)1 << 1) | ((ULONG_PTR)1 << 9);
/* write the parameters */
nErrCode = NtWriteVirtualMemory(ProcessHandle, ((PUCHAR)pStackBase) - nParamsSize,
Parameters, nParamsSize, &nDummy);
. . . . . .
/* write the return address */
return NtWriteVirtualMemory(ProcessHandle,
((PUCHAR)pStackBase) - (nParamsSize + sizeof(ULONG_PTR)),
&s_pRetAddr, sizeof(s_pRetAddr), &nDummy);
}[/code]
在CONTEXT数据结构中准备的是系统空间的那一部分上下文。注意这里的参数StartAddress就是前面代码中的pTrueStartAddress,即指向用户空间的BaseProcessStart()。而上下文中的Esp则指向用户空间堆栈的地址顶点往下减去参数数组的大小(这里是2,实际是8个字节)外加一个指针的地方。目标映像的实际入口地址lpStartAddress就在这些参数中,而且是其中地址较低的那一个。这个参数数组被写入用户空间堆栈(地址区间)的顶部,在这些参数下面则是一个伪造的“返回地址”s_pRetAddr。
这样,当目标线程从系统空间“返回”用户空间时,寄存器Eip将指向BaseProcessStart()的起点,而Esp将指向其用户空间堆栈上那个伪造的“返回地址”s_pRetAddr,在“返回地址”上方则是参数中的lpStartAddress。如此的安排制造出一个假象,似乎是:
? l 用户空间的某个函数调用了BaseProcessStart(),调用点的返回地址为s_pRetAddr。
? l 但是在执行BaseProcessStart()的第一条指令时就发生了异常,从而进入了内核。
? l 经过异常处理以后又回来了,回来就要重新执行BaseProcessStart()的第一条指令。
? l 堆栈上的参数就是调用BaseProcessStart()时的参数,其中参数1就是lpStartAddress。
后面读者就会看到,BaseProcessStart()其实是不返回的,所以返回地址s_pRetAddr是虚设的,只是占个位置而已。从上面的代码中可以看到,这个地址设置为0xDEADBEEF。显然,这个地址属于系统空间(大于0x80000000),从用户空间访问这个地址会引起异常,所以万一真的从BaseProcessStart()返回也因此而受到保护。
所以,新建的主线程在其投入运行的过程中将要涉及四个起始地址:
1. 当新建线程受调度运行时,一开始是在内核中运行,所以有个内核中的起始地址,这个地址我们尚未见到。
2. 新建线程需要返回到用户空间运行,但是用户空间的DLL动态连接尚未完成,所以先要在用户空间执行一个APC函数LdrInitializeThunk()。
3. 完成了DLL的动态连接以后,新建线程的执行又回到内核中,继续其奔向用户空间目标映像的征程。当新建线程通过reti指令回到用户空间的时候,当然要有个开始执行的起始地址,这就是它的上下文中Eip所指的地址,实际上是BaseProcessStart()。这个地址作为“断点”安插在新建线程的系统空间堆栈上。
4. 之所以需要BaseProcessStart(),是因为要把整个目标映像的执行置于“结构化出错处理”即SEH的保护之下,而真正意义上的起始地址在目标映像中的位移是由映像头部的EntryPoint字段提供的,映射到用户空间后的地址为(ImageBaseAddress + Sii.EntryPoint)。这个地址已经被安排在新建线程的用户空间堆栈上,使其正好成为BaseProcessStart()的调用参数。
万事俱备,只欠东风。现在只剩下主线程的创建了。
[code][CreateProcessW() > KlCreateFirstThread() > RtlRosCreateUserThreadVa()
> RtlRosCreateUserThread() > NtCreateThread()]
NTSTATUS STDCALL
NtCreateThread(OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle,
OUT PCLIENT_ID ClientId,
IN PCONTEXT ThreadContext,
IN PINITIAL_TEB InitialTeb,
IN BOOLEAN CreateSuspended)
{
. . . . . .
if(PreviousMode != KernelMode)
{
_SEH_TRY _SEH_END;
. . . . . .
}
Status = ObReferenceObjectByHandle(ProcessHandle, PROCESS_CREATE_THREAD,
PsProcessType, PreviousMode, (PVOID*)&Process, NULL);
. . . . . .
Status = PsInitializeThread(Process, &Thread, ObjectAttributes, PreviousMode, FALSE);
. . . . . .
/* create a client id handle */
Status = PsCreateCidHandle(Thread, PsThreadType, &Thread->Cid.UniqueThread);
. . . . . .
Status = KiArchInitThreadWithContext(&Thread->Tcb, ThreadContext);
. . . . . .
Status = PsCreateTeb(ProcessHandle, &TebBase, Thread, InitialTeb);
. . . . . .
Thread->Tcb.Teb = TebBase;
Thread->StartAddress = NULL;
/* Maybe send a message to the process's debugger */
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -