/******************** nt5.0 ******************/
#define BeingDeleted 0x1
#define BeingCreated 0x2
#define BeingPurged 0x4
#define NoModifiedWriting 0x8
#define FailAllIo 0x10
#define Image 0x20
#define Based 0x40
#define File 0x80
#define Networked 0x100
#define NoCache 0x200
#define PhysicalMemory 0x400
#define CopyOnWrite 0x800
#define Reserve 0x1000
#define Commit 0x2000
#define FloppyMedia 0x4000
#define WasPurged 0x8000
#define UserReference 0x10000
#define GlobalMemory 0x20000
#define DeleteOnClose 0x40000
#define FilePointerNull 0x80000
#define DebugSymbolsLoaded 0x100000
#define SetMappedFileIoComplete 0x200000
#define CollidedFlush 0x400000
#define NoChange 0x800000
#define HadUserReference 0x1000000
#define ImageMappedInSystemSpace 0x2000000
紧随CONTROL_AREA之后的是Subsection的数目Subsections。每一个Subsection都描述了关于具体的文件映射section的信息。例如,read-only, read-write, copy-on-write等等的section。NT5.0的SUBSECTION结构体:
typedef struct _SUBSECTION { // size=0x20 nt5.0
// +0x10 if GlobalOnlyPerSession
PCONTROL_AREA ControlArea; //38, 00
DWORD Flags; //3c, 04
DWORD StartingSector;//40, 08
DWORD NumberOfSectors; //44, 0c
PVOID BasePte; //48, 10 pointer to start pte
DWORD UnusedPtes; //4c, 14
DWORD PtesInSubsect; //50, 18
PSUBSECTION pNext; //54, 1c
}SUBSECTION, *PSUBSECTION;
在subsection中有指向CONTROL_AREA的指针,标志,指向base Proto PTE的指针,Proto PTE的数目。StartingSector是4K block的编号,文件中的section起始于此。在标志中还有额外的信息:
#define SS_PROTECTION_MASK 0x1f0
#define SS_SECTOR_OFFSET_MASK 0xfff00000 // (low 12 bits)
#define SS_STARTING_SECTOR_HIGH_MASK 0x000ffc00 // (nt5 only) (in pages)
//other 5 bit(s)
#define ReadOnly 1
#define ReadWrite 2
#define CopyOnWrite 4
#define GlobalMemory 8
#define LargePages 0x200
我们来看剩下的最后一个结构体SEGMENT,它描述了所有的映射和用于映射section的Proto PTE。SEGMENT的内存是从paged pool中分配的。我给出SEGMENT结构体(NT 5.0)
typedef struct _SEGMENT {
PCONTROL_AREA ControlArea; //00
DWORD BaseAddr; //04
DWORD TotalPtes; //08
DWORD NonExtendedPtes;//0c
LARGE_INTEGER SizeOfsegemnt; //10
DWORD ImageCommit; //18
DWORD ImageInfo; //1c
DWORD ImageBase; //20
DWORD Commited; //24
PTE PteTemplate; //28 or 64 bits if pae enabled
DWORD BasedAddr; //2c
DWORD BaseAddrPae; //30 if PAE enabled
DWORD ProtoPtes; //34
DWORD ProtoPtesPae; //38 if PAE enabled
}SEGMENT,*PSEGMENT;
正如我所料,结构体包含着对CONTROL_AREA的引用,指向Proto PTE的pool的指针和所有section的信息。有个东西需要解释一下。结构体的样子依赖于是否支持PAE。PAE就是Physical Address Extenion。从第5版开始,Windows NT包含了支持PAE的内核Ntkrnlpa.exe。总的来讲,支持PAE就意味着在NT里可以使用的虚拟地址不是4GB而是64GB。在使用PAE时的地址转换又多了一级——所有的虚地址空间被分为4部分。在打开PAE时PTE和PDE的大小不是4B而是8B,这我们可以从SEGMENT结构体中看出。现在还不需要进一步详细的讲PAE,毕竟很少用到,所以我们就此打住。
描述section的所有结构体都介绍过了,而section对象结构体本身还没有提到。从直观上可以想到,它应该会引用到SEGMENT或是CONTROL_AREA,因为有了这两个结构体后就可以得到保存的所有信息。通过反汇编得到的section对象的body为以下形式:
typedef struct _SECTION_OBJECT { // size 0x28
VAD_HEADER VadHeader; // 0
PSEGMENT pSegment; //0x14 Segment
LARGE_INTEGER SectionSize; //0x18
DWORD ControlFlags; //0x20
DWORD PgProtection; //0x24
} SECTION_OBJECT, *SECTION_OBJECT;
#define PageFile 0x10000
#define MappingFile 0x8000000
#define Based 0x40
#define Unknown 0x800000 // not sure, in fact it's AllocAttrib&0x400000
我们看到,所得的结构体完全符合现有的高层信息的描述。唯一可能有疑问的就是VAD_HEADER。它描述了base section在地址空间中的位置。VAD_HEADER位于顶点为_MmSectionBasedRoot的VAD树中。我们再次体会到,要理解操作系统的工作原理,就要理解其内部的结构。为了有一个总体上的把握,下面给出了描述section的结构体间互相联系的一个图。
SECTION_OBJECT->SEGMENT<->CONTROL_AREA->FILE_OBJECT->SECTION_OBJECT_POINTERS+
^ |
+--------------------------------------------+
08.从内存管理器角度看进程的创建
====================================================
前面我们从Win32角度介绍过进程的创建,也讲过内存管理器和对象管理器的工作原理,以及section对象结构体。现在最有意思的当然就是在进程创建中将内存管理器也考虑进来。
进程是用未公开的系统调用NtCreateProcess()创建的。下面给出其伪代码:
/*****************************************************************/
/* -- Here it is, just wrapper -- */
NtCreateProcess(
OUT Handle,
IN ACCESS_MASK Access,
IN POBJECT_ATTRIBUTES ObjectAttrib,
IN HANDLE Parent,
IN BOOLEAN InheritHandles,
IN HANDLE SectionHandle,
IN HANDLE DebugPort,
IN HANDLE ExceptionPort
)
{
if(Parent)
{
ret=PspCreateProcess(Handle,
Access,
ObjectAttrib,
Parent,
InheritHandles,
SectionHandle,
DebugPort,
ExceptionPort);
}
else ret=STATUS_INVALID_PARAMETER;
return ret;
}
我们看到,NtCreateProcess是对另一个内部函数PspCreateProcess的封装。NtCreateProcess进行的唯一工作就是检查Parent(父进程句柄)。但是接下来我们看到,对于NT来说这并没有什么意义,因为总的来说,进程的继承性本身没有特别的意义。现在我们来看PspCreateProcess()。
PspCreateProcess(
OUT PHANDLE Handle,
IN ACCESS_MASK Access,
IN POBJECT_ATTRIBUTES ObjectAttrib,
IN HANDLE Parent,
IN BOOLEAN InheritHandles,
IN HANDLE SectionHandle,
IN HANDLE DebugPort,
IN HANDLE ExceptionPort
);
我很快注意到,函数中的Parent参数可以接受值0,这就表明在NtCreateProcess中检验此参数是为了限制用户模式。函数的参数中有对section、debug port和exception port、父进程的引用。通过调用ObReferenceObjectByHandle,可以得到指向这些对象的指针。实际上父进程句柄通常传递的是-1,这表示是当前进程。如果Parent等于0,则进程的affinity就不从父进程处取得,而是从系统变量中取得。
if(Parent)
{ //Get pointer to father's body
ObReferenceObjectByHandle(Parent,0x80,PsProcessType,PrevMode,&pFather,0);
AffinityMask=pFather->Affinity; // on witch processors will be executed
Prior=8;
}
else {
pFather=0;
AffinityMask=KeActiveProcessors;
Prior=8;
}
优先级总是为8。随后,创建进程对象。NT4.0下其大小为504字节。
// size of process body - 504 bytes
// creating process object... (type object PsProcessType)
ObCreateObject(PrevMode,PsProcessType,ObjectAttrib,PrevMode,0,504,&pProcess);
// clear body
memset(pProcess,0,504);
初始化某些域和Quota Block(见对象管理器的相关介绍)。
pProcess->CreateProcessReported=0;
pProcess->DebugPort=pDebugPort;
pProcess->ExceptPort=pExceptPort;
// Inherit Quota Block, if pFather==NULL, PspDefaultQuotaBlock
PspInheritQuota(pProcess,pFather);
if(pFather){
pProcess->DefaultHardErrorMode=pFather->DefaultHardErrorMode;
pProcess->InheritedFromUniqueProcessId=pFather->UniqueProcessId;
}
else {
pProcess->InheritedFromUniqueProcessId=0;
pProcess->DefaultHardErrorMode=1;
}
之后,调用MmCreateProcessAddressSpace,创建地址上下文。参数是函数得到的指向进程的指针、工作集的大小和指向结果结构体的指针。这个结构体形式如下:
struct PROCESS_ADDRESS_SPACE_RESULT{
dword Dt; // dict. table phys. addr.
dword HypSpace; // hyp space page phys. addr.
dword WorkingSet; // working set page phys. addr.
}CASResult;
MmCreateProcessAddressSpace(PsMinimumWorkingSet,pProcess,&CASResult);
我们看到,函数向我们返回的是页表的物理地址描述符(用于新地址空间的CR3的内容),Hyper Space的页地址和工作集的页地址。在此之后是初始化进程对象的某些域:
pProcess->MinimumWorkingSet=MinWorkingSet;
pProcess->MaximumWorkingSet=MaximumWorkingSet;
KeInitializeProcess(pProcess,Prior,AffinityMask,&CASResult,pProcess->
DefaultHardErrorProcessing&0x4);
pProcess->ForegroundQuantum=PspForegroundQuantum;
如果有父进程且设置了标志参数,则会继承父进程的句柄表:
if(pFather) // if there is father and inherithandle, so, inherit handle db
{
pFather2=0;
if(bInheritHandle)pFather2=pFather;
ObInitProcess(pFather2,pProcess); // see info about ObjectManager
}
下面的东西比较有意思,证明了NT执行系统的灵活性,从表面上是看不出来的。如果在参数中有指定的section,则使用这个section来初始化进程的地址空间,否则其工作就会像*UNIX中的fork()。
if(pSection)
{
MmInitializeProcessAddressSpace(pProcess,0,pSection);
ObDereferenceObject(pSection);
res=ObInitProcess2(pProcess); //work with unknown byte +0x22 in process
if(res>=0)PspMapSystemDll(pProcess,0);
Flag=1; //Created addr space
}
else { // if there is futher, but no section, so, do operation like fork()
if(pFatherProcess){
if(PsInitialSystemProcess==pFather){
MmRes=MmInitializeProcessAddressSpace(pProcess,0,0);
}
else {
pProcess->SectionBaseAddress=pFather->SectionBaseAddress;
MmRes=MmInitializeProcessAddressSpace(pProcess,pFather,0);
Flag=1; //created addr space
}
}
}
接下来是使用PsActiveProcessHead将进程插入Active Process链表,创建Peb和做其它辅助性的工作。我们不再赘述。最后,当所有的工作都做完后,进行安全子系统方面的工作。我们过去曾研究过安全子系统(见对象管理器部分),所以这里只简单的给出其伪代码。只是我注意到,如果父进程是system(句柄值等于PspInitialSystemProcessHandle),则不对其安全性进行检验。
// finally, security operations
if(pFather&&PspInitialSystemProcessHandle!=Father)
{
ObGetObjectSecurity(pProcess,&SecurityDescriptor,&MemoryAllocated);
pToken=PsReferencePrimaryToken(pProcess);
AccessRes=SeAccessCheck(SecurityDescriptor,&SecurityContext,
0,0x2000000,
0,0,&PsProcessToken->GenericMapping,
PrevMode,pProcess->GrantedAccess,
&AccessStatus);
ObDereferenceObject(pToken);
ObReleaseObjectSecuryty(SecurityDescriptor,MemoryAllocated);
if(!AccessRes)pProcess->GrantedAccess=0;
pProcess->GrantedAccess|=0x6fb;
}
else{
pProcess->GrantedAccess=0x1f0fff;
}
if(SeDetailedAuditing)SeAuditProcessCreation(pProcess,pFather);
最有意思的是函数KeInitializeProcess和MmCreateProcessAddressSpace。前一个函数除了初始化进程对象的其它成员之外,还要初始化TSS中的IO位图的偏移。
pProcess->IopmOffset=0x20ad; // IOMAP BASE!!!
// You can patch kernel here and
// got i/o port control ;)
偏移的选取是这样的,它指向I/O位图,这样就能阻止进程直接使用I/O端口。
在函数MmCreateProcessAddressSpace中进行的是进程地址空间的创建。我就不给出所有的伪代码了,只简要的写写主要的操作。它为Hyper Space, Working Set和Page Directory选择页。反汇编后的代码证实了,它们是从zero frame链表中选出或是由MiZeroPhysicalPage函数来清零的。之后初始化新创建的Page Directory。
pProcess->WorkingSetPage=Frame3; // WorkingSetPage
(MmPfnDatabase+0x18*Frame)->Pte=0xc0300000;
ValidPde_U=ValidPdePde&0xeff^Frame2; // HyperSpace
/**************IMPORTANT!!!!!!!!!!!!!!************************/
/* 重要! 这里初始化PD */
/*************************************************************/
Va=MiMapPageInHyperSpace(Frame,&LastIrql);
// no we got Va of our new Page Directory
// Fill some fields
*(Va+0xc04)=ValidPde_U; // HyperSpace
ValidPde_U=ValidPde_U&0xfff^PhysAddr; // DT
*(Va+0xc00)=ValidPde_U; // self-pde
// copy from current process, kernel address mapping
memcpy(
(MmVirtualBias+0x80000000)>>0x14+Va, // it's like that we found,
// what MmVirtualBias is it ;)
(MmVirtualBias+0x80000000)>>0x14+0xc0300000,
0x80 // 32 pdes -> 4Mb*32=128Mb
);
memcpy( // copy pdes, corresponding to NonPagedArea
MmNonPagedSystemStart>>0x14+Va,
MmNonPagedSystemStart>>0x14+0xc0300000,
(0xc0300ffc-MmNonPagedSystemStart>>0x14+0xc0300000)&0xfffffffc+4);
memcpy(Va+0xc0c, // cache, forgot about it now, it's another story ;)
0xc0300c0c,
(MmSystemCacheEnd>>0x14)-0xc0c+4
);
也就是将PDE拷贝到内核地址空间中去(其对所有的进程不变,Hyper Space除外),而且是拷贝到不可换出的区域。同时这个空间是属于系统cache的。
09.上下文切换
==========================
知道了ETHREAD、EPROCESS结构体和内存管理器的工作原理,就不难猜到上下文切换时会发生什么。Windows NT的设计者使用线程,不关心共享的是谁的地址空间,也就是说有两种可能:线程属于当前进程——必需要切换到另一个线程(更新堆栈并更换GDT描述符),而线程属于另一个进程,必需切换到那个进程(重新加载CR3)。对此,为了证实我的推测,我反汇编了KeAttachProcess函数。这个函数是未公开的,但所有已知的函数都用其来切换到另一进程的地址空间。通过KeDetachProcess可以返回到当前进程。KeAttachProcess使用下述内部函数:
KiAttachProcess - KeAttachProcess仅仅是对这个函数的封装
KiSwapProcess - 更换地址空间。(本质上就是重新加载CR3)
SwapContext - 更换上下文。一般不管地址空间的切换,只调整线程上下文。
KiSwapThred - 切换到链表中的下一个线程(SwapContext)调用
下面给出这些内部函数的伪代码。
-----------------------------------------------------------------------------
/************************ KeAttachProcess ***************************/
// just wrapper
//
KeAttachProcess(EPROCESS *Process)
{
KiAttachProcess(Process,KeRaiseIrqlToSynchLevel);
}
/************************ KiAttachProcess ***************************/
KiAttachProcess(EPROCESS *Process,Irql){
//CurThread=fs:124h
//CurProcess=CurThread->ApcState.Process;
if(CurProcess!=Process){
if(CurProcess->ApcStateIndex || KPCR->DpcRoutineActive)KeBugCheckEx...
}
//if we already in process's context
if(CurProcess==Process){KiUnlockDispatcherDatabase(Irql);return;}
Process->StackCount++;
KiMoveApcState(&CurThread->ApcState,&CurThread->SavedApcState);
// init lists
CurThread->ApcState.ApcListHead[0].Blink=&CurThread->ApcState.ApcListHead[0];
CurThread->ApcState.ApcListHead[0].Flink=&CurThread->ApcState.ApcListHead[0];
CurThread->ApcState.ApcListHead[1].Blink=&CurThread->ApcState.ApcListHead[1];
CurThread->ApcState.ApcListHead[1].Flink=&CurThread->ApcState.ApcListHead[1];;
//fill curtheads's fields
CurThread->ApcState.Process=Process;
CurThread->ApcState.KernelApcInProgress=0;
CurThread->ApcState.KernelApcPending=0;
CurThread->ApcState.UserApcPending=0;
CurThread->ApcState.ApcStatePointer.SavedApcState=&CurThread->SavedApcState;
CurThread->ApcState.ApcStatePointer.ApcState=&CurThread->ApcState;
CurThread->ApcStateIndex=1;
//if process ready, just swap it...
if(!Process->State)//state==0, ready
{
KiSwapProcess(Process,CurThread->SavedApcState.Process);
KiUnlockDispatcherDatabase(Irql);
return;
}
CurThread->State=1; //ready?
CurThread->ProcessReadyQueue=1;
//put Process in Thread's waitlist
CurThread->WaitListEntry.Flink=&Process->ReadyListHead.Flink;
CurThread->WaitListEntry.Blink=Process->ReadyListHead.Blink;
Process->ReadyListHead.Flink->Flink=&CurThread->WaitListEntry.Flink;
Process->ReadyListHead.Blink=&CurThread->WaitListEntry.Flink;
// else, move process to swap list and wait
if(Process->State==1){//idle?
Process->State=2; //trans
Process->SwapListEntry.Flink=&KiProcessInSwapListHead.Flink;
Process->SwapListEntry.Blink=KiProcessInSwapListHead.Blink;
KiProcessInSwapListHead.Blink=&Process->SwapListEntry.Flink;
KiSwapEvent.Header.SignalState=1;
if(KiSwapEvent.Header.WaitListHead.Flink!=&KiSwapEvent.Header.WaitListHead.
Flink)
KiWaitTest(&KiSwapEvent,0xa); //fastcall
}
CurThread->WaitIrql=Irql;
KiSwapThread();
return;
}
从这个函数可以得到以下结论。进程可以处于以下状态——0(准备),1(Idle),2(Trans——切换)。这证实了高层次的信息。KiAttachProcess使用了另外两个函数KiSwapProcess和KiSwapThread。
/************************* KiSwapProcess ****************************/
KiSwapProcess(EPROCESS* NewProcess, EPROCESS* OldProcess)
{
// just reload cr3 and small work with TSS
// TSS=KPCR->TSS;
// xor eax,eax
// mov gs,ax
TSS->CR3=NewProcess->DirectoryTableBase;//0x1c
// mov cr3,NewProcess->DirectoryTableBase
TSS->IopmOffset=NewProcess->IopmOffset;//0x66
if(WORD(NewProcess->LdtDescriptor)==0){lldt 0x00; return;//}
//GDT=KPCR->GDT;
(QWORD)GDT->0x48=(QWORD)NewProcess->LdtDescriptor;
(QWORD)GDT->0x108=(QWORD)NewProcess->Int21Descriptor;
lldt 0x48;
return;
}








