📄 漫谈兼容内核之二十一windows进程的用户空间.txt
字号:
> RtlRosCreateUserThread() > RtlRosCreateStack()]
NTSTATUS NTAPI RtlRosCreateStack(IN HANDLE ProcessHandle,
OUT PINITIAL_TEB InitialTeb, IN LONG StackZeroBits,
IN OUT PULONG StackReserve OPTIONAL,
IN OUT PULONG StackCommit OPTIONAL)
{
/* FIXME: read the defaults from the executable image */
ULONG_PTR nStackReserve = 0x100000;
/* FIXME: when we finally have exception handling, make this PAGE_SIZE */
ULONG_PTR nStackCommit = 0x100000;
NTSTATUS nErrCode;
if(StackReserve == NULL) StackReserve = &nStackReserve;
else *StackReserve = ROUNDUP(*StackReserve, PAGE_SIZE);
if(StackCommit == NULL) StackCommit = &nStackCommit;
else *StackCommit = ROUNDUP(*StackCommit, PAGE_SIZE);
/* FIXME: no SEH, no guard pages */
*StackCommit = *StackReserve;
/* FIXME: this code assumes a stack growing downwards */
/* fixed stack */
if(*StackCommit == *StackReserve)
{
InitialTeb->StackCommit = NULL;
InitialTeb->StackCommitMax = NULL;
InitialTeb->StackReserved = NULL;
InitialTeb->StackLimit = NULL;
/* allocate the stack */
nErrCode = NtAllocateVirtualMemory(ProcessHandle, &(InitialTeb->StackLimit),
StackZeroBits, StackReserve, MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE);
/* store the highest (first) address of the stack */
InitialTeb->StackBase = (PUCHAR)(InitialTeb->StackLimit) + *StackReserve;
*StackCommit = *StackReserve;
}
/* expandable stack */
else
{
ULONG_PTR nGuardSize = PAGE_SIZE;
PVOID pGuardBase;
InitialTeb->StackBase = NULL;
InitialTeb->StackLimit = NULL;
InitialTeb->StackReserved = NULL;
/* reserve the stack */
nErrCode = NtAllocateVirtualMemory(ProcessHandle,
&(InitialTeb->StackReserved), StackZeroBits,
StackReserve, MEM_RESERVE, PAGE_READWRITE);
/* expandable stack base - the highest address of the stack */
InitialTeb->StackCommit = (PUCHAR)(InitialTeb->StackReserved) + *StackReserve;
/* expandable stack limit - the lowest committed address of the stack */
InitialTeb->StackCommitMax = (PUCHAR)(InitialTeb->StackCommit) - *StackCommit;
/* commit as much stack as requested */
nErrCode = NtAllocateVirtualMemory(ProcessHandle, &(InitialTeb->StackCommitMax),
0, StackCommit, MEM_COMMIT, PAGE_READWRITE);
pGuardBase = (PUCHAR)(InitialTeb->StackCommitMax) - PAGE_SIZE;
/* set up the guard page */
nErrCode = NtAllocateVirtualMemory(ProcessHandle, &pGuardBase, 0,
&nGuardSize, MEM_COMMIT, PAGE_READWRITE | PAGE_GUARD);
}
/* success */
return STATUS_SUCCESS;
}
代码的主体是一个if语句,判定的条件是指针StackCommit和StackReserve所指向的数值是否相等。这两个指针是通过调用参数传下来的,它们所指向的数值决定了堆栈的大小,如果是空指针就采用默认值0x100000,就是1MB。如果两个指针所指的值相同就表示要建立大小固定的堆栈;否则就表示要建立可以随时扩充的堆栈,此时需要为限制堆栈的增长设置一个隔离页面。
我们看它else部分的代码,因为这相对而言要复杂一点。
先看对NtAllocateVirtualMemory()的第一次调用,其目的是要保留用于堆栈的区间。注意调用参数InitialTeb->StackReserved的值已预先设置为NULL;StackZeroBits的值是从上面一路传下来的,实际上也是0。所以,这个堆栈的位置也是自由分配的。根据前面分配使用用户空间的历史,我们可以得出结论:堆栈的默认位置是0x30000至0x12ffff,共1MB。实际分配的起始地址和大小则通过InitialTeb->StackReserved和参数StackReserve返回。
然后,对NtAllocateVirtualMemory()的第二次调用则要求“交割”目前所要求的大小,即StackCommit所指向的数值。注意内存管理分配的空间是由下往上伸展的,而堆栈是由上向下伸展的,因此首先交割的范围是在这区间的上部,所以这里要进行一些计算。注意这里InitialTeb->StackCommitMax所指向的倒反而是地址较低的地方。
第三次调用NtAllocateVirtualMemory()的目的在于在堆栈区间的下部(堆栈的顶部)设置一个保护页面。这里的nGuardSize是个局部量,预先设置成PAGE_SIZE,所以隔离区的大小就是一个页面。当然,这个页面也在前面保留的1MB区间之中。
这个过程中所涉及或改变的一些数值都是应该记录在目标线程的TEB中的,但是此时目标线程的TEB尚未建立,所以上层传下一个指针InitialTeb,指向一个“初始TEB”,可以先用起来,以后再把它的内容复制到目标线程的TEB中。初始TEB的数据结构与实际的TEB数据结构略有不同。
回到RtlRosCreateUserThread()的代码,下一步的操作是RtlRosInitializeContext(),这是要为新建的线程虚构出一个上下文。
[CreateProcessW() > KlCreateFirstThread() > RtlRosCreateUserThreadVa()
> 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);
/* validate the stack */
nErrCode = RtlpRosValidateTopDownUserStack(pStackBase, pStackLimit);
memset(Context, 0, sizeof(CONTEXT));
/* initialize the context */
. . . . . .
Context->Eip = (ULONG_PTR)StartAddress;
Context->SegGs = USER_DS;
Context->SegFs = TEB_SELECTOR;
. . . . . .
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);
}
这里涉及对新建进程用户空间的两次写操作。一次是把上面传下来的指针数组Parameters复制到用户空间的堆栈上,另一次是把s_pRetAddr的值0xDEADBEEF也复制到堆栈上,这样就在用户空间的堆栈上虚构出一个函数调用框架。这个框架的返回地址是0xDEADBEEF,但是实际上不会被用到,只是摆个样子、充个数。而调用参数则是由上层传下来的,下面读者就会看到一共有两个参数,其一是函数指针,其二则是PEB所在的地址。
这里指针pStackBase的值来自前面的InitialTeb->StackCommit,注意这个地址比InitialTeb->StackCommitMax更高,因为堆栈是由上向下伸展的。
注意这里所说的参数是为新建线程在用户空间的程序入口提供的,不要把它们跟前面所说的进程参数块相混淆。实际上,进程的第一个线程正式进入用户空间以后首先执行的是RtlBaseProcessStart(),这里所说的参数就是对于这个函数的调用参数。为了搞清这两个参数到底是什么,我们往上回溯到KlCreateFirstThread()对RtlRosCreateUserThreadVa()的调用:
HANDLE STDCALL KlCreateFirstThread(…)
{
……
nErrCode = RtlRosCreateUserThreadVa(ProcessHandle, &oaThreadAttribs,
dwCreationFlags & CREATE_SUSPENDED, 0,
&(Sii->StackReserve), &(Sii->StackCommit),
pTrueStartAddress, &hThread, &cidClientId,
2, (ULONG_PTR)lpStartAddress, (ULONG_PTR)PEB_BASE);
……
}
显然,这里往下传的是2个参数。一个是lpStartAddress,这是目标线程的入口地址;另一个是PEB_BASE,就是进程环境块所在的地址。注意这里的lpStartAddress和pTrueStartAddress是不同的两个地址。读者在以前的漫谈中看到过,pTrueStartAddress的值来自RtlBaseProcessStartRoutine,所指向的是RtlBaseProcessStart()。而lpStartAddress所指向的才是来自目标映像的程序入口。
又回到RtlRosCreateUserThread(),下一步是系统调用NtCreateThread()。这当然又是个比较大的操作,但是涉及用户空间格局的只是PsCreateTeb()、即TEB的建立(此外还涉及内核空间堆栈的建立,但那与用户空间无关)。我们分段阅读PsCreateTeb()的代码:
[CreateProcessW() > KlCreateFirstThread() > RtlRosCreateUserThreadVa()
> NtCreateThread() > PsCreateTeb()]
static NTSTATUS PsCreateTeb(HANDLE ProcessHandle, PTEB *TebPtr,
PETHREAD Thread, PINITIAL_TEB InitialTeb)
{
. . . . . .
TebSize = PAGE_SIZE;
if (NULL == Thread->ThreadsProcess)
{
/* We'll be allocating a 64k block here and only use 4k of it, but this
path should almost never be taken. Actually, I never saw it was taken,
so maybe we should just ASSERT(NULL != Thread->ThreadsProcess) and
move on */
TebBase = NULL;
Status = ZwAllocateVirtualMemory(ProcessHandle, &TebBase, 0, &TebSize,
MEM_RESERVE | MEM_COMMIT | MEM_TOP_DOWN,
PAGE_READWRITE);
. . . . . .
}
else
{
Process = Thread->ThreadsProcess;
ExAcquireFastMutex(&Process->TebLock);
if (NULL == Process->TebBlock || Process->TebBlock == Process->TebLastAllocated)
{
Process->TebBlock = NULL;
RegionSize = MM_VIRTMEM_GRANULARITY;
Status = ZwAllocateVirtualMemory(ProcessHandle,
&Process->TebBlock, 0, &RegionSize,
MEM_RESERVE | MEM_TOP_DOWN,
PAGE_READWRITE);
. . . . . .
Process->TebLastAllocated = (PVOID) ((char *) Process->TebBlock + RegionSize);
}
TebBase = (PVOID) ((char *) Process->TebLastAllocated - PAGE_SIZE);
Status = ZwAllocateVirtualMemory(ProcessHandle, &TebBase, 0, &TebSize,
MEM_COMMIT, PAGE_READWRITE);
. . . . . .
Process->TebLastAllocated = TebBase;
ExReleaseFastMutex(&Process->TebLock);
}
. . . . . .
这部分代码基本上就是一个if语句。判定的条件是Thread->ThreadsProcess为0,表示目标线程并不属于任何一个进程,但是这在正常的情况下实际上是不该发生的,所以代码的作者在注释中也说“也许我们只要放上ASSERT(NULL != Thread->ThreadsProcess)”就行。在正常的情况下,Thread->ThreadsProcess指向所属进程的EPROCESS数据结构,此时又分两种情况。一种是Process->TebBlock为0或者与Process->TebLastAllocated相同,前者说明尚未为TEB保留空间,后者说明为TEB保留的空间已经用完。实际上,Process->TebBlock是在PsCreatePeb()中设置的(见前面的代码),应该指向PEB所在64KB区间的下部边界,所以不应该是0,而Process->TebLastAllocated则随着TEB的创建而逐次下移,每次下移一个页面,当它指向这个区间的下部边界时,就说明这个区间已经耗尽了。这时候就在原有区间的下面再分配一个区间(但是这段代码好像有点问题)。
在绝大多数的情况下,TEB区间已经保留,其顶部的一个页面用于PEB,并且Process->TebLastAllocated也已作了相应调整,现在则在它下方再获取一个页面用作目标线程的TEB,并对Process->TebLastAllocated作相应的调整。
为目标线程的TEB分配了一个页面以后,下面就要把有关的数据写入TEB了。具体的数据主要来自前面的“初始TEB”,参数InitialTeb就是指向初始TEB结构的指针。我们继续往下看:
[CreateProcessW() > KlCreateFirstThread() > RtlRosCreateUserThreadVa()
> NtCreateThread() > PsCreateTeb()]
RtlZeroMemory(&Teb, sizeof(TEB));
/* set all pointers to and from the TEB */
Teb.Tib.Self = TebBase;
if (Thread->ThreadsProcess)
{
Teb.Peb = Thread->ThreadsProcess->Peb; /* No PEB yet!! */
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -