漫谈兼容内核之二十一windows进程的用户空间.txt
来自「漫谈系统内核内幕 收集得很辛苦 呵呵 大家快下在吧」· 文本 代码 · 共 761 行 · 第 1/4 页
TXT
761 行
ObDereferenceObject(SectionObject);
. . . . . .
}
. . . . . .
Status = PsCreatePeb(hProcess, Process, ImageBase);
. . . . . .
return Status;
}
首先是LdrpMapSystemDll(),读者想必已经知道,这是把ntdll.dll的映像“装入”、即映射到用户空间。其位置和大小取决于映像文件头部的有关信息,事实上ntdll.dll(版本之一) 的装入地址是0x77f50000,大小为0x0000A700。
接着是目标EXE映像的映射。在调用NtCreateProcess()之前,调用者已经为目标映像创建了一个Section、即共享内存区,现在要做的是通过MmMapViewOfSection()把这个共享区映射到新建进程的用户空间。映射的具体位置和大小基本上也取决于映像文件头部的有关信息,但可以有一定的弹性,不像ntdll.dll和其它几个系统DLL那么刚性。以notepad.exe为例,其文件头部提供的装入地址为0x01000000。至于映像大小就更是各不相同了。
注意这里装入的都只是静态的映像。目标映像在实际运行时所消耗的内存可能远不止于此,不过那是以后动态分配内存的事了。
最后是PsCreatePeb(),显然这是要在用户空间建立“进程环境块”PEB。
[NtCreateProcess() > PspCreateProcess() > PsCreatePeb()]
static NTSTATUS
PsCreatePeb(HANDLE ProcessHandle, PEPROCESS Process, PVOID ImageBase)
{
. . . . . .
/* Allocate the Process Environment Block (PEB) */
Process->TebBlock =
(PVOID) MM_ROUND_DOWN(PEB_BASE, MM_VIRTMEM_GRANULARITY);
AllocSize = MM_VIRTMEM_GRANULARITY;
Status = NtAllocateVirtualMemory(ProcessHandle, &Process->TebBlock, 0,
&AllocSize, MEM_RESERVE, PAGE_READWRITE);
. . . . . .
Peb = (PPEB)PEB_BASE;
PebSize = PAGE_SIZE;
Status = NtAllocateVirtualMemory(ProcessHandle, (PVOID*)&Peb, 0,
&PebSize, MEM_COMMIT, PAGE_READWRITE);
. . . . . .
Process->TebLastAllocated = (PVOID) Peb;
ViewSize = 0;
SectionOffset.QuadPart = (ULONGLONG)0;
TableBase = NULL;
Status = MmMapViewOfSection(NlsSectionObject, Process, &TableBase, 0, 0,
&SectionOffset, &ViewSize,
ViewShare, MEM_TOP_DOWN, PAGE_READONLY);
. . . . . .
KeAttachProcess(&Process->Pcb);
/* Initialize the PEB */
RtlZeroMemory(Peb, sizeof(PEB));
Peb->ImageBaseAddress = ImageBase;
Peb->OSMajorVersion = 4;
Peb->OSMinorVersion = 0;
Peb->OSBuildNumber = 1381;
Peb->OSPlatformId = 2; //VER_PLATFORM_WIN32_NT;
Peb->OSCSDVersion = 6 << 8;
Peb->AnsiCodePageData = (char*)TableBase + NlsAnsiTableOffset;
Peb->OemCodePageData = (char*)TableBase + NlsOemTableOffset;
Peb->UnicodeCaseTableData = (char*)TableBase + NlsUnicodeTableOffset;
Process->Peb = Peb;
KeDetachProcess();
return(STATUS_SUCCESS);
}
这里涉及的两个常数定义为:
#define PEB_BASE (0x7FFDF000)
#define MM_VIRTMEM_GRANULARITY (64 * 1024)
就是说,PEB在用户空间的位置是0x7FFDF000。而内存管理机制在保留和分配用户空间地址区间时的“粒度”是64KB。就是说,至少是64KB,并且以64KB为一个单元。而MM_ROUND_DOWN(PEB_BASE, MM_VIRTMEM_GRANULARITY)是计算PEB所在的那个64KB区间的下部边界:
#define MM_ROUND_DOWN(x,s) ((PVOID)(((ULONG_PTR)(x)) & ~((ULONG_PTR)(s)-1)))
计算一下就可以知道结果是0x7FFD0000。注意页面的大小PAGE_SIZE是0x1000即4KB,所以PEB_BASE与页面边界是对齐的,只是没有与64KB边界对齐。
代码中调用了两次NtAllocateVirtualMemory(),但是注意第一次是要求“保留(MEM_RESERVE)”从0x7FFD0000开始的64KB区间,而第二次则是要求仅仅“交割(MEM_COMMIT)”PEB所实际占用的区间。
下面还有一次MmMapViewOfSection(),这是为了把与NLS即“本国语言支持”有关的数据和代码映射到用户空间。像别的可执行映像一样,也要先为其创建一个Section,然后把这个Section映射到需要使用它的用户空间。NLS的Section对象NlsSectionObject是在系统初始化的时侯创建的。
注意经过PsCreatePeb()的处理以后EPROCESS结构中的指针TebBlock指向0x7FFD0000,指针Peb则指向PEB的起点0x7FFDF000,而TebLastAllocated此刻同样指向PEB的起点,但是后面我们将看到这个指针是随着TEB的分配和建立而变的。事实上,“线程环境块”TEB都在PEB的下方,并且通常不止一个。而上面所保留的64KB区间、即16个页面,则除顶端的一个页面用于PEB外其余15个页面都是为TEB准备的。
这里没有涉及堆栈,这是因为进程本身并不受调度运行,进程里面的线程才受调度运行,所以堆栈是与线程相连系、而不是与进程相连系的。
在新建进程的用户空间为其建立了PEB之后,创建者还要通过KlInitPeb()对此PEB加以进一步的初始化,在此之前创建者已经准备好了一个“进程参数块”Ppb作为调用参数之一传下来。进程参数块是一个RTL_USER_PROCESS_PARAMETERS数据结构,里面带下来的参数中有一个指针Environment,指向一个“宽字符”串,这就是各环境变量的定义。注意KlInitPeb()是在创建者进程的用户空间执行的,但是其操作目的和对象则在于新建进程的用户空间。
[CreateProcessW() > KlInitPeb()]
static NTSTATUS KlInitPeb(HANDLE ProcessHandle,
PRTL_USER_PROCESS_PARAMETERS Ppb,
PVOID *ImageBaseAddress, ULONG ImageSubSystem)
{
. . . . . .
PVOID EnvPtr = NULL;
. . . . . .
/* create the Environment */
if (Ppb->Environment != NULL)
{
ParentEnv = Ppb->Environment;
ptr = ParentEnv;
while (*ptr)
{
while(*ptr++);
}
ptr++;
EnvSize = (PVOID)ptr - ParentEnv;
}
else if (NtCurrentPeb()->ProcessParameters->Environment != NULL)
{
MEMORY_BASIC_INFORMATION MemInfo;
ParentEnv = NtCurrentPeb()->ProcessParameters->Environment;
Status = NtQueryVirtualMemory (NtCurrentProcess (), ParentEnv,
MemoryBasicInformation, &MemInfo,
sizeof(MEMORY_BASIC_INFORMATION), NULL);
EnvSize = MemInfo.RegionSize;
}
DPRINT("EnvironmentSize %ld\n", EnvSize);
/* allocate and initialize new environment block */
if (EnvSize != 0)
{
EnvSize1 = EnvSize;
Status = NtAllocateVirtualMemory(ProcessHandle, &EnvPtr, 0, &EnvSize1,
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
. . . . . .
NtWriteVirtualMemory(ProcessHandle, EnvPtr, ParentEnv, EnvSize, &BytesWritten);
}
/* create the PPB */
PpbBase = NULL;
PpbSize = Ppb->AllocationSize;
Status = NtAllocateVirtualMemory(ProcessHandle, &PpbBase, 0, &PpbSize,
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
. . . . . .
//DPRINT("Ppb->MaximumLength %x\n", Ppb->MaximumLength);
NtWriteVirtualMemory(ProcessHandle, PpbBase, Ppb,
Ppb->AllocationSize, &BytesWritten);
/* write pointer to environment */
Offset = FIELD_OFFSET(RTL_USER_PROCESS_PARAMETERS, Environment);
NtWriteVirtualMemory(ProcessHandle, (PVOID)(PpbBase + Offset),
&EnvPtr, sizeof(EnvPtr), &BytesWritten);
/* write pointer to process parameter block */
Offset = FIELD_OFFSET(PEB, ProcessParameters);
NtWriteVirtualMemory(ProcessHandle, (PVOID)(PEB_BASE + Offset),
&PpbBase, sizeof(PpbBase), &BytesWritten);
/* Write image subsystem */
Offset = FIELD_OFFSET(PEB, ImageSubSystem);
NtWriteVirtualMemory(ProcessHandle, (PVOID)(PEB_BASE + Offset),
&ImageSubSystem, sizeof(ImageSubSystem), &BytesWritten);
/* Read image base address. */
Offset = FIELD_OFFSET(PEB, ImageBaseAddress);
NtReadVirtualMemory(ProcessHandle, (PVOID)(PEB_BASE + Offset),
ImageBaseAddress, sizeof(PVOID), &BytesWritten);
return(STATUS_SUCCESS);
}
参数Ppb实际涉及两块数据。一块是进程参数块本身,这是有固定大小的;另一块的内容是一些环境变量字符串,这些字符串游离在进程参数块外面,并且也没有固定的长度。这里的目的就是要把这两块数据都复制到新建进程的用户空间去,并相应地设置好相关数据结构中的指针。为此,程序中首先要确定这些环境变量字符串所占的长度EnvSize。
知道了这些环境变量字符串所占的长度EnvSize以后,如果非0,就要在新建进程的用户空间分配相应的区间。注意这里在调用NtAllocateVirtualMemory()时的第2个参数、即指针EnvPtr的值已预先设置为NULL,表示对起始地址没有特定的要求;并且第3个参数、即要求在所分配起始地址中前导0的个数也是0,因此可以分配在任意的部位(前导0的个数实际上大致上给定了一个部位)。对于这样的分配要求,内存管理会在目标空间从低到高扫描,以找到第一个符合大小要求的区间。由于用户空间的起点是0x10000,这又是第一次要求由内存管理自由分配,所以实际分配的位置一定在0x10000处。另一方面,由于区间分配的粒度是64KB,所以实际分配的一般总是64KB,因为很难设想EnvSize会大于64KB。实际分配的位置和大小则通过参数EnvPtr和EnvSize1返回。然后通过NtWriteVirtualMemory()将来自参数Ppb、或当前进程的环境变量字符串复制到新建进程用户空间的这个区间中。
接着就是针对进程参数块PPB的本身来故伎重演了。注意这里参数PpbBase的值也是预先设置成NULL,并且调用NtAllocateVirtualMemory()时的第3个参数也是0,所以也是由内存管理自由分配,而实际分配的位置则总是在环境变量块的上方,一般应该是从0x20000到0x2ffff的64KB,实际的位置和大小则通过参数PpbBase和PpbSize返回。然后就是把进程参数块复制过去。
此后还有3次对NtWriteVirtualMemory()的调用,旨在对新建进程用户空间PPB和PEB中的几个成分作出修正。第一次是修改PPB中的指针Environment,使其指向实际的环境变量字符串(实际上总是在0x10000处)。第二次是设置PEB中的指针ProcessParameters,使其指向实际的进程参数块。第3次则是把作为参数传下来的ImageSubSystem写入新建进程PEB中的同名字段,这实际上是个作为编码的32位无符号整数,可能的取值有IMAGE_SUBSYSTEM_WINDOWS_GUI和IMAGE_SUBSYSTEM_WINDOWS_CUI等等,表示新建进程的可执行映像是视窗应用或控制台应用,具体的数值来自目标映像文件的头部。
最后的NtReadVirtualMemory()从PEB读取其ImageBaseAddress字段,并通过参数ImageBaseAddress返回目标映像装入用户空间后的起始地址。
下面就是创建新进程的第一个线程了,这里涉及的是堆栈和TEB的建立。我们先看函数RtlRosCreateUserThread()的代码,这是由Win32 API函数CreateProcessW()辗转调用下来的,也是由创建者进程在其用户空间执行:
[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;
}
这里所调用的三个函数都与用户空间的区间分配有关。先看RtlRosCreateStack():
[CreateProcessW() > KlCreateFirstThread() > RtlRosCreateUserThreadVa()
⌨️ 快捷键说明
复制代码Ctrl + C
搜索代码Ctrl + F
全屏模式F11
增大字号Ctrl + =
减小字号Ctrl + -
显示快捷键?