时间:2021-07-01 10:21:17 帮助过:12人阅读
在学习笔记(1)中,我们学习了IRP的数据结构的相关知识,接下来我们继续来学习内核中很重要的另一批数据结构: EPROCESS/KPROCESS/PEB。把它们放到一起是因为这三个数据结构及其外延和windows中进程的表示关系密切,我们在做进程隐藏和进程枚举的时候一定会涉
在学习笔记(1)中,我们学习了IRP的数据结构的相关知识,接下来我们继续来学习内核中很重要的另一批数据结构: EPROCESS/KPROCESS/PEB。把它们放到一起是因为这三个数据结构及其外延和windows中进程的表示关系密切,我们在做进程隐藏和进程枚举的时候一定会涉及到这3类数据结构,所以这里有必要及时做一下总结,做个学习笔记,理清思路。
《windows 内核原理与实现》 --- 潘爱民
《深入解析windows操作系统(第4版,中文版)》 --- 潘爱民
《Windows核心编程(第四版)》中文版
首先我们来看一张windows的内核分布图,我们之后要讲的执行体和内核层都在上面。
每个windows进程都是由一个执行体进程块(EPROCESS)块来表示的。"EPROCESS块中除了包含许多与进程有关的属性之外,还包含和指向许多其他的相关数据结构"(这也是为什么能利用EPROCESS进行进程枚举的原因)。例如,每个进程都有一个或多个线程,这些线程由执行体线程块(ETHREAD)来表示。EPROCESS块和相关的数据结构位于系统空间中,不过,进程环境块(PEB)是个例外。它位于进程地址空间中(因为它包含一些需要由用户模式代码来修改的信息)。
除了EPROCESS块以外,windows子系统进程(Csrss.exe)为每个windows进程维护了一个类似的结构。而且,windows子系统的内核部分(Win32k.sys)有一个针对每个进程的数据结构(KPROCESS),当一个线程第一次调用windows的USER或GDI函数(它们在内核模式中实现的)时,此数据结构就会被创建。在学习的一开始,我们对这些数据结构在windows系统的层级位置和大致的作用有一个了解很关键。接下来,我们来详细了解这些数据结构。
一 . EPROCESS
EPEOCESS(执行体进程块,E是Execute的意思,注意和KPROCESS区分开来)位于内核层之上(KPROCESS就内核层,我们之后会学习到KPROCESS),它侧重于提供各种管理策略,同时为上层应用程序提供基本的功能接口。所以,在执行体层的进程和线程数据结构中,有些成员直接对应于上层应用程序中所看到的功能实体。
我们使用winDbg来查看windows XP下的EPROCESS数据结构(winDbg双机调试的方法在《寒江独钓》第一章以及网上有很多成熟的使用说明)。
我们接下来的例子都以windows XP下的notepad.exe作为实验材料。首先,启动winDbg后,找到notepad.exe这个进程(你在虚拟机里要先启动notepad.exe哦)
!process 0 0 //查看当前进程 .... PROCESS 823e5490 SessionId: 0 Cid: 0af0 Peb: 7ffd5000 ParentCid: 02b8 DirBase: 0f200340 ObjectTable: e283dc30 HandleCount: 106. Image: alg.exe PROCESS 824a7020 SessionId: 0 Cid: 00a0 Peb: 7ffda000 ParentCid: 0668 DirBase: 0f2001a0 ObjectTable: e292f898 HandleCount: 44. Image: notepad.exe ...
可以看到 PROCESS 824a7020 ,824a7020 就是notepad.exe的_EPROCESS结构地址(这个地址在notepad的生命周期中就不会变了)
dt _eprocess 824a7020 //查看notepad.exe的_EPROCESS kd> dt _eprocess 824a7020 ntdll!_EPROCESS +0x000 Pcb : _KPROCESS +0x06c ProcessLock : _EX_PUSH_LOCK +0x070 CreateTime : _LARGE_INTEGER 0x1ceee34`5d8ce8a2 +0x078 ExitTime : _LARGE_INTEGER 0x0 +0x080 RundownProtect : _EX_RUNDOWN_REF +0x084 UniqueProcessId : 0x000000a0 +0x088 ActiveProcessLinks : _LIST_ENTRY [ 0x8055b158 - 0x823e5518 ] +0x090 QuotaUsage : [3] 0xa50 +0x09c QuotaPeak : [3] 0xc48 +0x0a8 CommitCharge : 0x184 +0x0ac PeakVirtualSize : 0x244d000 +0x0b0 VirtualSize : 0x1f87000 +0x0b4 SessionProcessLinks : _LIST_ENTRY [ 0xf8b6a014 - 0x823e5544 ] +0x0bc DebugPort : (null) +0x0c0 ExceptionPort : 0xe1982848 +0x0c4 ObjectTable : 0xe292f898 _HANDLE_TABLE +0x0c8 Token : _EX_FAST_REF +0x0cc WorkingSetLock : _FAST_MUTEX +0x0ec WorkingSetPage : 0xc6af +0x0f0 AddressCreationLock : _FAST_MUTEX +0x110 HyperSpaceLock : 0 +0x114 ForkInProgress : (null) +0x118 HardwareTrigger : 0 +0x11c VadRoot : 0x82401480 +0x120 VadHint : 0x8256a530 +0x124 CloneRoot : (null) +0x128 NumberOfPrivatePages : 0xc0 +0x12c NumberOfLockedPages : 0 +0x130 Win32Process : 0xe109b210 +0x134 Job : (null) +0x138 divObject : 0xe19d2c18 +0x13c divBaseAddress : 0x01000000 +0x140 QuotaBlock : 0x82cf5db8 _EPROCESS_QUOTA_BLOCK +0x144 WorkingSetWatch : (null) +0x148 Win32WindowStation : 0x0000003c +0x14c InheritedFromUniqueProcessId : 0x00000668 +0x150 LdtInformation : (null) +0x154 VadFreeHint : (null) +0x158 VdmObjects : (null) +0x15c DeviceMap : 0xe21f5258 +0x160 PhysicalVadList : _LIST_ENTRY [ 0x824a7180 - 0x824a7180 ] +0x168 PageDirectoryPte : _HARDWARE_PTE_X86 +0x168 Filler : 0 +0x170 Session : 0xf8b6a000 +0x174 ImageFileName : [16] "notepad.exe" +0x184 JobLinks : _LIST_ENTRY [ 0x0 - 0x0 ] +0x18c LockedPagesList : (null) +0x190 ThreadListHead : _LIST_ENTRY [ 0x823c224c - 0x823c224c ] +0x198 SecurityPort : (null) +0x19c PaeTop : 0xf8cba1a0 +0x1a0 ActiveThreads : 1 +0x1a4 GrantedAccess : 0x1f0fff +0x1a8 DefaultHardErrorProcessing : 1 +0x1ac LastThreadExitStatus : 0 +0x1b0 Peb : 0x7ffda000 _PEB +0x1b4 PrefetchTrace : _EX_FAST_REF +0x1b8 ReadOperationCount : _LARGE_INTEGER 0x1 +0x1c0 WriteOperationCount : _LARGE_INTEGER 0x0 +0x1c8 OtherOperationCount : _LARGE_INTEGER 0x248 +0x1d0 ReadTransferCount : _LARGE_INTEGER 0x57ce +0x1d8 WriteTransferCount : _LARGE_INTEGER 0x0 +0x1e0 OtherTransferCount : _LARGE_INTEGER 0x18846 +0x1e8 CommitChargeLimit : 0 +0x1ec CommitChargePeak : 0x184 +0x1f0 AweInfo : (null) +0x1f4 SeAuditProcessCreationInfo : _SE_AUDIT_PROCESS_CREATION_INFO +0x1f8 Vm : _MMSUPPORT +0x238 LastFaultCount : 0 +0x23c ModifiedPageCount : 7 +0x240 NumberOfVads : 0x42 +0x244 JobStatus : 0 +0x248 Flags : 0xd0800 +0x248 CreateReported : 0y0 +0x248 NoDebugInherit : 0y0 +0x248 ProcessExiting : 0y0 +0x248 ProcessDelete : 0y0 +0x248 Wow64SplitPages : 0y0 +0x248 VmDeleted : 0y0 +0x248 OutswapEnabled : 0y0 +0x248 Outswapped : 0y0 +0x248 ForkFailed : 0y0 +0x248 HasPhysicalVad : 0y0 +0x248 AddressSpaceInitialized : 0y10 +0x248 SetTimerResolution : 0y0 +0x248 BreakOnTermination : 0y0 +0x248 SessionCreationUnderway : 0y0 +0x248 WriteWatch : 0y0 +0x248 ProcessInSession : 0y1 +0x248 OverrideAddressSpace : 0y0 +0x248 HasAddressSpace : 0y1 +0x248 LaunchPrefetched : 0y1 +0x248 InjectInpageErrors : 0y0 +0x248 VmTopDown : 0y0 +0x248 Unused3 : 0y0 +0x248 Unused4 : 0y0 +0x248 VdmAllowed : 0y0 +0x248 Unused : 0y00000 (0) +0x248 Unused1 : 0y0 +0x248 Unused2 : 0y0 +0x24c ExitStatus : 259 +0x250 NextPageColor : 0x8d96 +0x252 SubSystemMinorVersion : 0 '' +0x253 SubSystemMajorVersion : 0x4 '' +0x252 SubSystemVersion : 0x400 +0x254 PriorityClass : 0x2 '' +0x255 WorkingSetAcquiredUnsafe : 0 '' +0x258 Cookie : 0x5c77be6c
并结合windows的"开源"学习版本的源代码: WRK。 在\base\ntos\inc\ps.h中,可以找到相关的数据结构的定义说明。
typedef struct _EPROCESS { KPROCESS Pcb; EX_PUSH_LOCK ProcessLock; LARGE_INTEGER CreateTime; LARGE_INTEGER ExitTime; EX_RUNDOWN_REF RundownProtect; HANDLE UniqueProcessId; LIST_ENTRY ActiveProcessLinks; SIZE_T QuotaUsage[PsQuotaTypes]; SIZE_T QuotaPeak[PsQuotaTypes]; SIZE_T CommitCharge; SIZE_T PeakVirtualSize; SIZE_T VirtualSize; LIST_ENTRY SessionProcessLinks; PVOID DebugPort; PVOID ExceptionPort; PHANDLE_TABLE ObjectTable; EX_FAST_REF Token; PFN_NUMBER WorkingSetPage; KGUARDED_MUTEX AddressCreationLock; KSPIN_LOCK HyperSpaceLock; struct _ETHREAD *ForkInProgress; ULONG_PTR HardwareTrigger; PMM_AVL_TABLE PhysicalVadRoot; PVOID CloneRoot; PFN_NUMBER NumberOfPrivatePages; PFN_NUMBER NumberOfLockedPages; PVOID Win32Process; struct _EJOB *Job; PVOID divObject; PVOID divBaseAddress; PEPROCESS_QUOTA_BLOCK QuotaBlock; PPAGEFAULT_HISTORY WorkingSetWatch; HANDLE Win32WindowStation; HANDLE InheritedFromUniqueProcessId; PVOID LdtInformation; PVOID VadFreeHint; PVOID VdmObjects; PVOID DeviceMap; PVOID Spare0[3]; union { HARDWARE_PTE PageDirectoryPte; ULONGLONG Filler; }; PVOID Session; UCHAR ImageFileName[ 16 ]; LIST_ENTRY JobLinks; PVOID LockedPagesList; LIST_ENTRY ThreadListHead; PVOID SecurityPort; PVOID PaeTop; ULONG ActiveThreads; ACCESS_MASK GrantedAccess; ULONG DefaultHardErrorProcessing; NTSTATUS LastThreadExitStatus; PPEB Peb; EX_FAST_REF PrefetchTrace; LARGE_INTEGER ReadOperationCount; LARGE_INTEGER WriteOperationCount; LARGE_INTEGER OtherOperationCount; LARGE_INTEGER ReadTransferCount; LARGE_INTEGER WriteTransferCount; LARGE_INTEGER OtherTransferCount; SIZE_T CommitChargeLimit; SIZE_T CommitChargePeak; PVOID AweInfo; SE_AUDIT_PROCESS_CREATION_INFO SeAuditProcessCreationInfo; MMSUPPORT Vm; LIST_ENTRY MmProcessLinks; ULONG ModifiedPageCount; ULONG JobStatus; union { ULONG Flags; struct { ULONG CreateReported : 1; ULONG NoDebugInherit : 1; ULONG ProcessExiting : 1; ULONG ProcessDelete : 1; ULONG Wow64SplitPages : 1; ULONG VmDeleted : 1; ULONG OutswapEnabled : 1; ULONG Outswapped : 1; ULONG ForkFailed : 1; ULONG Wow64VaSpace4Gb : 1; ULONG AddressSpaceInitialized : 2; ULONG SetTimerResolution : 1; ULONG BreakOnTermination : 1; ULONG SessionCreationUnderway : 1; ULONG WriteWatch : 1; ULONG ProcessInSession : 1; ULONG OverrideAddressSpace : 1; ULONG HasAddressSpace : 1; ULONG LaunchPrefetched : 1; ULONG InjectInpageErrors : 1; ULONG VmTopDown : 1; ULONG ImageNotifyDone : 1; ULONG PdeUpdateNeeded : 1; // NT32 only ULONG VdmAllowed : 1; ULONG SmapAllowed : 1; ULONG CreateFailed : 1; ULONG DefaultIoPriority : 3; ULONG Spare1 : 1; ULONG Spare2 : 1; }; }; NTSTATUS ExitStatus; USHORT NextPageColor; union { struct { UCHAR SubSystemMinorVersion; UCHAR SubSystemMajorVersion; }; USHORT SubSystemVersion; }; UCHAR PriorityClass; MM_AVL_TABLE VadRoot; ULONG Cookie; } EPROCESS, *PEPROCESS;
不好意思一下子复制了一大堆的代码出来,也说明EPOCESS在windows内核中的地位,所以很复杂,我们现在来一条一条的学习这个EPROCESS的数据结构成员,和其他内核中数据结构的联系以及相关的应用场景和外延。
1. KPROCESS Pcb
Pcb域即KPROCESS结构体,它们是同一种东西,只是两种叫法而已,我们现在只要知道几点,KRPOCESS的详细细节我们放到后面讲:
1) KPROCESS位于比EPROCESS更底层的内核层中 2) KPROCESS被内核用来进行线程调度使用
这里还要注意的是,因为Pcb域是EPROCESS结构的第一个成员,所以在系统内部,一个进程的KPROCESS对象的地址和EPROCESS对象的地址是相同的。这种情况和"TIB就是TEB结构的第一个成员,而EXCEPTION_REGISTRATION_RECORD又是TIB的第一个成员,又因为FS:[0x18] 总是指向当前线程的 TEB 。 所以导致用 FS:[0x18] 就直接可以寻址到SEH的链表了"。windows中的这种结构体的嵌套思想,应该予以领会。
2. EX_PUSH_LOCK ProcessLock
ProcessLock域是一个推锁(push lock)对象,用于保护EPROCESS中的数据成员(回想IRP中也同样有一种类似的锁机制)。用来对可能产生的并行事件强制串行化。
typedef struct _EX_PUSH_LOCK { union { ULONG Locked: 1; ULONG Waiting: 1; ULONG Waking: 1; ULONG MultipleShared: 1; ULONG Shared: 28; ULONG Value; PVOID Ptr; }; } EX_PUSH_LOCK, *PEX_PUSH_LOCK;
3. LARGE_INTEGER CreateTime / LARGE_INTEGER ExitTime
这两个域分别代表了进程的创建时间和退出时间,windows在内核中会记录大量和进程周边的相关信息,例如创建事件,运行时间,负载,使用内存数等,一方面对于上层应用来说,我们可以很方便地调用一些API去获取到实时的系统运行状态,另一方面操作系统本身也可以依据这些信息对进程进行合理的调度。
typedef union _LARGE_INTEGER { struct { DWORD LowPart; LONG HighPart; }; struct { DWORD LowPart; LONG HighPart; } u; LONGLONG QuadPart; } LARGE_INTEGER, *PLARGE_INTEGER;
+0x070 CreateTime : _LARGE_INTEGER 0x1ceee34`5d8ce8a2
4. EX_RUNDOWN_REF RundownProtect
RundownProtect域是进程的停止保护锁,当一个进程到最后被销毁时,它要等到所有其他进程和线程已经释放了此锁,才可以继续进行,否则就会产生孤儿线程。加锁机制也是windows中进程间或线程间同步的一个很经典的机制,进程只要设置阻塞等待在一个锁对象上,等待拥有那个锁的其他进程或线程释放锁对象,操作系统会发出signal信号,重新激活之前阻塞等待在那个锁上的进程,这样就完成了进程间,线程间的同步。
struct EX_RUNDOWN_REF typedef struct _EX_RUNDOWN_REF { union { ULONG Count; PVOID Ptr; }; } EX_RUNDOWN_REF, *PEX_RUNDOWN_REF;
5. HANDLE UniqueProcessId
UniqueProcessId域是进程的唯一编号,在进程创建时就设定好了,我们在"任务管理器"中看到的PID就是从这个域中获取的值。
+0x084 UniqueProcessId : 0x000000a0
6. LIST_ENTRY ActiveProcessLinks
ActiveProcessLinks域是一个双链表节点(注意是双链表中的一个节点),在windows系统中,所有的活动进程都连接在一起,构成了一个链表。
表头是全局变量PsActiveProcessHead。内部变量PsActiveProcessHead是一个LIST_ENTRY结构,它是一个双链表的表头,在windows的进程双链表中指定了系统进 程列表的第一个成员(回想数据结构中双链表需要一个和链表项相同的表头结构来当作入口点)。 这里注意一下: PID=4的System的ActiveProcessLinks其实也就是PsActiveProcessHead。即系 统进程System的LIST_ENTRY结构充当这个进程双链表的表头。
当一个进程被创建时,其ActiveProcessLinks域将被作为"节点"加入到内核中的进程双链表中,当进程被删除时,则从链表中移除。如果我们需要枚举所有的进程,直接操纵此链表即可。思路其实很清晰的,利用PsGetCurrentProcess()获得当前进程的EPROCESS结构,在根据指定偏移找到Flink和Blink,后面的事情就是数据结构中的遍历双链表了。
typedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; struct _LIST_ENTRY *Blink; } LIST_ENTRY, *PLIST_ENTRY;
+0x088 ActiveProcessLinks : _LIST_ENTRY [ 0x8055b158 - 0x823e5518 ]
我们暂且不表,将进程枚举的知识放到后面的学习笔记中,我们集中精力学习EROCESS的数据结构。
7. 进程内存配额使用
ULONG QuotaUsage[3] ULONG QuotaPeak[3]
QuotaUsage和QuotaPeak域是指一个进程的内存使用量和尖峰使用量(作用我们之前说过,即为了实时地报告出当前进程运行的性能数据)。这两个域是数组,其中的元素:
[0]: 非换页内存池
[1]: 换页内存池
[2]: 交换文件中的内存使用情况。
QuotaUsage[3]和QuotaPeak[3]这两个数组是在PspChargeQuota函数中计算的。在WRK中 \base\ntos\ps\psquota.c 有具体的函数实现。
NTSTATUS FORCEINLINE PspChargeQuota (
IN PEPROCESS_QUOTA_BLOCK QuotaBlock,
IN PEPROCESS Process,
IN PS_QUOTA_TYPE QuotaType,
IN SIZE_T Amount)
{
..
}
+0x090 QuotaUsage : [3] 0xa50 +0x09c QuotaPeak : [3] 0xc48
8. 进程虚拟内存使用
ULONG CommitCharge
ULONG PeakVirtualSize
ULONG VirtualSize
CommitCharge域中存储了一个进程的虚拟内存已提交的"页面数量"。PeakVirtualSize域是指虚拟内存大小的尖峰值。VirtualSize域是指一个进程的虚拟内存大小。
+0x0a8 CommitCharge : 0x184 +0x0ac PeakVirtualSize : 0x244d000 +0x0b0 VirtualSize : 0x1f87000
(我们在任务管理器中看到的这些性能参数就是从这些域中计算总和得到的)
9. LIST_ENTRY SessionProcessLinks
SessionProcessLinks域是一个双链表节点,当进程加入到一个系统会话中时,这个进程的SessionProcessLinks域将作为一个节点(LIST_ENTRY在内核中很常见)加入到该会话的进程链表中。
+0x0b4 SessionProcessLinks : _LIST_ENTRY [ 0xf8b6a014 - 0x823e5544 ]
10. 端口
PVOID DebugPort;
PVOID ExceptionPort;
DebugPort和ExceptionPort域是两个句柄(指针),分别指向当前进程对应的调试端口和异常端口。当该进程的线程发生用户模式的异常时(软件中断)时,内核的异常处理例程在处理异常过程中,将向
该进程的"异常端口"或"调试端口"发送消息,从而使这些端口的接收方(调试器或windows子系统)能够处理该异常。这涉及到windows的异常分发过程。请参考《windows内核原理与实现》
5.2.7节: 异常分发。这里总结一下windows异常发生时的处理顺序。
发生异常时系统的处理顺序: 1.系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送,并且目标程序正在被调试,则系统 挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息.注意,这也是用来探测调试器的存在的技术之一 2.如果你的程序没有被调试或者调试器未能处理异常,系统就会继续查找你是否安装了线程相关的异常处理例程,如果 你安装了线程相关的异常处理例程,系统就把异常发送给你的程序seh处理例程,交由其处理. 3.每个线程相关的异常处理例程可以处理或者不处理这个异常,如果他不处理并且安装了多个线程相关的异常处理例程, 可交由链起来的其他例程处理. 4.如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知debugger. 5.如果程序未处于被调试状态或者debugger没有能够处理,并且你调用SetUnhandledExceptionFilter安装了最后异 常处理例程的话,系统转向对它的调用. 6.如果你没有安装最后异常处理例程或者他没有处理这个异常,系统会调用默认的系统处理程序,通常显示一个对话框, 你可以选择关闭或者最后将其附加到调试器上的调试按钮.如果没有调试器能被附加于其上或者调试器也处理不了,系统 就调用ExitProcess终结程序. 7.不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程异常处理例程最后清理的机会.
+0x0bc DebugPort : (null) +0x0c0 ExceptionPort : 0xe1982848
11. PHANDLE_TABLE ObjectTable
ObjectTable域是当前进程的句柄表。句柄是一个抽象概念,代表了进程已打开的一个对象。句柄表包含了所有已经被该进程打开的那些"对象"的引用(这里对象的概念很大了,所有的内核对象都算对象)。
windows进程的句柄表是一个层次结构。
windows执行体实现了一套对象机制来管理各种资源或实体。每种对象都有一个"类型对象",类型对象定义了该类对象的一些特性和方法。对象管理器中的对象是执行体对象(在内核空间中),
它们位于系统空间中,在进程空间不能通过地址来引用它们。windows使用句柄(handle)来管理进程中的对象引用(类似我们进行文件操作时返回的文件对象句柄)。
当一个进程利用名称来创建或打开一个对象时,将获得一个句柄(这个句柄本质上一个指针,保存在当前进程的EPROCESS的句柄表中)。
该句柄指向所创建或打开的对象(同时,被创建或打开的内核对象的引用计数+1)。以后,该进程无须再次使用名称来引用该对象,使用此句柄访问即可。windows这样做可以显著提高引用对象的效率。
在windows中,句柄是进程范围内的对象引用,换句话说,句柄仅在一个进程范围内有效。一个进程中的句柄传递给另一个进程后(例如父进程创建了子进程时会自动复制父进程拥有的句柄表),
但是句柄值已经不一样了,不同进程空间中可能引用的是同一个对象,但是它们的句柄值不同(联想进程的空间独立性)。
实际上,windows支持的句柄是一个"索引",指向该句柄所在进程的句柄表(handle table)中的一个表项。
base\ntos\inc\ex.h
typedef struct _HANDLE_TABLE { //指向句柄表的结构 ULONG TableCode; //句柄表的内存资源记录在此进程中 PEPROCESS QuotaProcess; //创建进程的ID,用于回调函数 PVOID UniqueProcessId; //句柄表锁,仅在句柄表扩展时使用 EX_PUSH_LOCK HandleLock; //所有的句柄表形成一个链表(这个成员域用来指向下一个句柄表节点的),链表头为全局变量HandleTableListHead LIST_ENTRY HandleTableList; //若在访问句柄时发生竞争,则在此推锁上阻塞等待 EX_PUSH_LOCK HandleContentionEvent; //调试信息,仅当调试句柄时才有意义 PHANDLE_TRACE_DEBUG_INFO DebugInfo; //审计信息所占用的页面数量 LONG ExtraInfoPages; //空闲链表表头的句柄索引 LONG FirstFreeHandle; //最近被释放的句柄索引,用于FIFO类型空闲链表 PHANDLE_TABLE_ENTRY LastFreeHandleEntry; //下一次句柄表扩展的起始句柄索引 ULONG NextHandleNeedingPool; //正在使用的句柄表项的数量 LONG HandleCount; union { //标志域 ULONG Flags; //是否使用FIFO风格的重用,即先释放还是先重用 BOOLEAN StrictFIFO : 1; } } HANDLE_TABLE, *PHANDLE_TABLE;
HANDLE_TABLE结构中的第一个域成员TableCode域是一个指针,指向句柄表的最高层表项页面(前面说过句柄表是一个多层的结构),它的低2bit的值代表了当前句柄表的层数。
1) 如果TableCode的最低2位为0(..00)。说明句柄表只有一层(从0开始计数),这种情况下该进程最多只能容纳512个句柄(宏定义LOWLEVEL_THRESHOLD)。 2) 如果TableCode的最低2位为1(..01)。说明句柄表有两层,这种情况下该进程可容纳的句柄数是512*1024(宏定义MIDLEVEL_THRSHOLD)。 3) 如果TableCode的最低2位为2(..10)。说明句柄表有三层,这种情况下该进程可容纳的句柄数是512*1024*1024(宏定义HIGHLEVEL_THRSHOLD)。
但windows执行体限定每个进程的句柄数不得超过2^24=16777216(宏定义MAX_HANDLES)。
看到这里,我们要理清一下思路:
1. 在EPROCESS中有一个 PHANDLE_TABLE ObjectTable 成员域指向的是一个HENDLE_TABLE数据结构,这个结构并不是真正保存句柄表真实数据的地址,我们可以把这个
结构理解为一个句柄表的meta元数据结构。在HANDLE_TABLE数据结构的第一个域成员TableCode才是指向"这个句柄表"对应的真实句柄数组数据。
2. TableCode指向的这个句柄数据的数据结构为: HANDLE_TABLE_ENTRY。即图中的每个小格子为一个HANDLE_TABLE_ENTRY结构
typedef struct _HANDLE_TABLE_ENTRY { union { PVOID Object; ULONG ObAttributes; PHANDLE_TABLE_ENTRY_INFO InfoTable; ULONG_PTR Value; }; union { union { ACCESS_MASK GrantedAccess; struct { USHORT GrantedAccessIndex; USHORT CreatorBackTraceIndex; }; }; LONG NextFreeTableEntry; }; } HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;
3. 每个HANDLE_TABLE_ENTRY结构体,即每个句柄的大小的为4byte,而windows执行体在分配句柄表内存时按页面(32位系统下4KB大小)来申请内存。因此,才有了我们
之前看到了每层的句柄数1024。
4. 那我们在图中看到的不同的句柄表(注意是表,一个表中有很多句柄项)之间的的链接是怎么回事呢?这是通过TABLE_HANDLE中的成员域HandleTableList来进行链接的,这个一个LIST_ENTRY结构,也即和进程双链表一样,这是一个句柄表双链表。
总结一下:EPROCESS中的PHANDLE_TABLE ObjectTable 成员域指向的是一个HENDLE_TABLE数据结构,这个域的第一个成员的低2位很重要,它决定了这个进程中总 共有多少个这样的PHANDLE_TABLE ObjectTable结构,例如(..10),那就有3个PHANDLE_TABLE ObjectTable结构,它们依靠PHANDLE_TABLE ObjectTable中 的HandleTableList来形成一个双链表,依次形成层次关系,而PHANDLE_TABLE ObjectTable中的TableCode指向的才是指向真实的句柄数组,每个句柄以 HANDLE_TABLE_ENTRY节诶狗来标识,大小为4byte。所有的HANDLE_TABLE_ENTRY形成一个句柄数组组成一层这样的"句柄数据组",总共有3排 (因为TableCode的低2位为..10)。 这样,windows中一个进程的句柄表就被表现出来了。
+0x0c4 ObjectTable : 0xe292f898 _HANDLE_TABLE +0x000 TableCode : 0xe10fd000 +0x004 QuotaProcess : 0x824a7020 _EPROCESS +0x008 UniqueProcessId : 0x000000a0 +0x00c HandleTableLock : [4] _EX_PUSH_LOCK +0x01c HandleTableList : _LIST_ENTRY [ 0xe1090f64 - 0xe283dc4c ] +0x024 HandleContentionEvent : _EX_PUSH_LOCK +0x028 DebugInfo : (null) +0x02c ExtraInfoPages : 0 +0x030 FirstFree : 0xac +0x034 LastFree : 0 +0x038 NextHandleNeedingPool : 0x800 +0x03c HandleCount : 48 +0x040 Flags : 0 +0x040 StrictFIFO : 0y0
更多的关于句柄表的数据结构和句柄表随着进程创建和运行过程中的扩展和收缩请参考《windows 内核原理与实现》3.4.1: windows进程的句柄表。我们这里就不继续展开了。
12. EX_FAST_REF Token
Token域是一个快速引用,指向该进程的访问令牌,用于该进程的安全访问检查。这是进程访问令牌相关的知识,之前做西电线上赛的时候就遇到一题是要提升进程访问令牌权限的要求。
BOOL WINAPI OpenProcessToken(
_In_ HANDLE ProcessHandle,
_In_ DWORD DesiredAccess,
_Out_ PHANDLE TokenHandle
);
+0x0c8 Token : _EX_FAST_REF +0x000 Object : 0xe10ea4bb +0x000 RefCnt : 0y011 +0x000 Value : 0xe10ea4bb
http://msdn.microsoft.com/library/aa379295(VS.85).aspx
相关的内容请参考《Windows核心编程(第四版)》中文版》4.5章,有关UAC的部分。这里提供一个提升进程Access Token权限的链接,小聂博客看到的,当时用的就是这个思路
http://xiaonieblog.com/?post=81
13. PFN_NUMBER WorkingSetPage
WorkingSetPage域是指包含进程工作集的页面
工作集是指一个进程当前正在使用的物理页面的集合。在windows中,有3种工作工作集。 1) 进程工作集 2) 系统工作集 系统工作集是指System进程(由全局变量PsInitialSystemProcess表示)的工作集,其中包括系统模块的映像区(比如ntoskrnl.exe和设备驱动程序)、换页内存池和系统缓存(cache) 3) 会话工作集 会话工作集是指一个会话所属的代码和数据区,包括windows子系统用到的与会话有关的数据结构、会话换页内存池、会话的映射视图,以及会话空间中的设备驱动程序。
每个进程都有一个专门的页面用来存放它的"工作集链表",这是在创建进程地址空间时已经映射好的一个页面,其地址位于全局变量MmWorkingSetList中,所以,在任何一个进程中,通过变量MmWokingSetList就可以访问到它的"工作集链表"。windows通过MmWokingSetList来跟踪一个进程所使用的物理页面。 这个MmWokingSetList的类型为"MMWSL":
typedef struct _MMWSL { WSLE_NUMBER FirstFree; WSLE_NUMBER FirstDynamic; WSLE_NUMBER LastEntry; WSLE_NUMBER NextSlot; // The next slot to trim PMMWSLE Wsle; WSLE_NUMBER LastInitializedWsle; WSLE_NUMBER NonDirectCount; PMMWSLE_HASH HashTable; ULONG HashTableSize; ULONG NumberOfCommittedPageTables; PVOID HashTableStart; PVOID HighestPermittedHashAddress; ULONG NumberOfImageWaiters; ULONG VadBitMapHint; USHORT UsedPageTableEntries[MM_USER_PAGE_TABLE_PAGES]; ULONG CommittedPageTables[MM_USER_PAGE_TABLE_PAGES/(sizeof(ULONG)*8)]; } MMWSL, *PMMWSL;
更多的内容请学习《windows 内核原理与实现》 4.6章: 工作集管理的相关知识。因为windows内核的知识很多,要在一次的学习中把所有相关的知识点都学到,那就有点像无线递归的感觉了,一层又一层。我们把工作集的内容放到以后研究,但是有一点很重要,我们要记住的一点是: 工作集定了这个进程所拥有的所有内存页,那进程的内存分配,使用,调度算法一定和工作集有关,如果以后我们遇到有关内存调度算法的题目,我们应该要能反应到一些相关的基础知识点,然后到这些经典的书上去翻阅相关知识,这样慢慢积累下来感觉会比较好。
14. KGUARDED_MUTEX AddressCreationLock
AddressCreationLock域是一个守护互斥体锁(guard mutex),用于保护对地址空间的操作。当内核代码需要对虚拟地址空间进行操作时(回想IRP中讲到3种缓冲区时说过,UserBuffer方式,内核代码直接访问用户空间的内存数据),它必须在AddressCreationLock上执行"获取锁"操作,完成以后再"释放锁"操作。
base\ntos\mm\mi.h中的宏可以简化代码:
#define LOCK_ADDRESS_SPACE(PROCESS) KeAcquireGuardedMutex (&((PROCESS)->AddressCreationLock)); #define UNLOCK_ADDRESS_SPACE(PROCESS) KeReleaseGuardedMutex (&((PROCESS)->AddressCreationLock));
15. KSPIN_LOCK HyperSpaceLock
HyperSpaceLock是一个自旋锁,用于保护进程的超空间。
typedef struct { volatile unsigned int lock; } spinlock_t;
16. struct _ETHREAD *ForkInProgress
ForkInProgress指向正在复制地址空间的那个线程,仅当在地址空间复制过程中,此域才会被赋值,在其他情况下为NULL。
base\ntos\mm\forksup.c:
NTSTATUS
MiCloneProcessAddressSpace (
IN PEPROCESS ProcessToClone,
IN PEPROCESS ProcessToInitialize
)
17. ULONG_PTR HardwareTrigger
HardwareTrigger用于记录硬件错误性能分析次数
18.进程的复制
PMM_AVL_TABLE PhysicalVadRoot: PhysicalVadRoot域指向进程的物理VAD的根。它并不总是存在,只有当确实需要映射物理内存时才会被创建。
PVOID CloneRoot: CloneRoot指向一个平衡树的根,当进程地址空间复制时,此树被创建,创建出来后,一直到进程退出的时候才被销毁。CloneRoot域完全是为了支持fork语义而引入。
PFN_NUMBER NumberOfPrivatePages: 指进程私有页面的数量
PFN_NUMBER NumberOfLockedPages: 指进程被锁住的页面的数量
+0x11c VadRoot : 0x82401480 +0x120 VadHint : 0x824392c8 +0x124 CloneRoot : (null) +0x128 NumberOfPrivatePages : 0xd9 +0x12c NumberOfLockedPages : 0
19. PVOID Win32Process / struct _EJOB *Job
Win32Process域是一个指针,指向由windows子系统管理的进程区域,如果此值不为NULL,说明这是一个windows子系统进程(GUI进程)。对于job域,只有当一个进程属于一个job(作业)的时候,它才会指向一个_EJOB对象。
+0x130 Win32Process : 0xe109b210 +0x134 Job : (null)
20. 进程对应的内存区
PVOID divObject: divObject域也是一个指针,代表进程的内存去对象(进程的可执行映像文件的内存区对象)
PVOID divBaseAddress: divBaseAddress域为该内存区对象的基地址
+0x138 divObject : 0xe19d2c18 +0x13c divBaseAddress : 0x01000000
21. PEPROCESS_QUOTA_BLOCK QuotaBlock
QuotaBlock域指向进程的配额块,进程的配额块类型为: EPROCESS_QUOTA_BLOCK
typedef struct _EPROCESS_QUOTA_BLOCK { struct _EPROCESS_QUOTA_ENTRY QuotaEntry[3]; struct _LIST_ENTRY QuotaList; ULONG32 ReferenceCount; ULONG32 ProcessCount; }EPROCESS_QUOTA_BLOCK, *PEPROCESS_QUOTA_BLOCK;
注意到结构中有我们熟悉的LIST_ENTRY双链表结构,心里基本猜到了。windows系统中的"配额块"相互串起来构成了一个双链表,每个配额块都可以被多个进程共享,所以有一个引用计数指用来说明当前有多少个进程正在使用这一配额块。配额块中主要定义了非换页内存池、换页内存池、交换文件中的内存配额限制。
这里要注意的是,所有配额块构成的双链表的表头为PspQuotaBlockList。系统的默认配额块为PspDefaultQuotaBlock。
+0x140 QuotaBlock : 0x82cf5db8 _EPROCESS_QUOTA_BLOCK +0x000 QuotaEntry : [3] _EPROCESS_QUOTA_ENTRY +0x030 QuotaList : _LIST_ENTRY [ 0x82c4dea0 - 0x82ca7700 ] +0x038 ReferenceCount : 0x751 +0x03c ProcessCount : 7
22. PPAGEFAULT_HISTORY WorkingSetWatch
WorkingSetWatch域用于监视一个进程的页面错误,一旦启用了页面错误监视功能(由全局变量PsWatchEnabled开关来控制),则每次发生页面错误都会将该页面错误记录到WorkingSetWatch域的WatchInfo成员数组中,知道数组满为止。
相关的处理函数为: PsWatchWorkingSet()。
typedef struct _PAGEFAULT_HISTORY { ULONG CurrentIndex; ULONG MapIndex; KSPIN_LOCK SpinLock; PVOID Reserved; PROCESS_WS_WATCH_INFORMATION WatchInfo[1]; } PAGEFAULT_HISTORY, *PPAGEFAULT_HISTORY;
23. HANDLE Win32WindowStation
Win32WindowStation域是一个进程所属的窗口站的句柄。由于句柄的值是由每个进程的句柄表来决定的,所以,两个进程即使同属于一个窗口站,它们的Win32WindowStation也可能不同,但指向的窗口站对象是相同的。窗口站是由windows子系统来管理和控制的。
24. HANDLE InheritedFromUniqueProcessId
InheritedFromUniqueProcessId域说明了一个进程是从哪里继承来的,即父进程的标识符。
25. PVOID LdtInformation
LdtInformation用来维护一个进程的LDT(局部描述符表)信息。
http://blog.csdn.net/billpig/article/details/5833980
http://hi.baidu.com/zeyu203/item/74192edfb16abd392b35c7c0
这两篇文章对GDT和LDT解释的很好了,这里总结一下我对GDT和LDT的理解。
1) 全局描述符表GDT(Global Descriptor Table)
除了使用虚拟地址来实现有效和灵活的内存管理以外,在计算机发展史上,另一种重要的内存管理方式是将物理内存划分成若干个段(segment),处理器在访问一个内存单元时,通过"段基址+偏移"的方式算出实际的物理地址。每个段都可以有自己的访问属性,包括读写属性、特权级别等。例如,在Intel x86处理器中,有专门的段寄存器,允许每条指令在访问内存时指定在哪个段上进行。段的概念在Intel 8086/8088实模式中已经使用了,但当时段的用途是扩展地址范围,将系统的内存从64KB扩展到1MB(CS:IP那种模式)。
在Intel x86中,逻辑地址的段部分(即原来的CS:IP中的CS部分)称为"段选择符(segment selector)",也有称为段选择子的。
这个"段选择符"指定了段的索引以及要访问的特权级别。段寄存器CS,SS,DS,ES,FS,GS专门用于指定一个地址的段选择符。虽然只有这六个寄存器,但是软件可以灵活地使用它们来完成各种功能。其中与有三个段寄存器有特殊的用途:
1) CS: 代码段寄存器,指向一个包含指令的段,即代码段
2) SS: 栈段寄存器,指向一个包含当前调用栈的段,即栈段
3) DS: 数据段寄存器,指向一个包含全局和静态数据的段,即数据段
在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT),GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此积存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限(也就是说,我们通过GDTR中的值可以找到这张全局描述表的真实数据结构,GDTR的作用也就仅仅这样了,找到GDT就没了)。
那借来来就有一个问题了,我们通过GDTR中保存的基地址找到了一个所谓的全局描述表GDT,并通过"段选择子"索引到了GDT中的某一项(具体怎么索引我们后面会举例子),我们这里暂时称为某一项,因为我们目前还不知道GDT中的一项项是什么数据,那GDT中都保存了什么数据呢?这就要涉及到局部描述符表LDT了。
2) 局部描述符表LDT(Local Descriptor Table)
局部描述符表可以有若干张,每个任务可以有一张。我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。
我们知道,一个应用程序中有代码段/数据段/TSS段/LDT段,而每一个段都对一个一个LDT,而这些LDT可以通过GDT寻址到。我们回想一下,GDTR是用来寻址GDT的,因为GDT只有一个,所以可以用GDTR+段选择子的方法来寻址LDT,但是LDT不一样了,LDT有很多个,所以不存在基址这一说,所以LDTR中保存的是当前的LDT,这个指是可以动态改变的。
LDT即局部描述符(或者叫段描述符),他记录了这个段的一些属性: 段的起始地址、有效范围、和一些属性。
每一个LDT中都这样的数据结构,它很好地描述了这个段对应的信息。我们重点解释其中几个字段:
G: 当G位为0时,此长度单位为byte。当G为1时,此长度单位为4096byte。所以,段长度可达2^20 * 4096 = 4GB,即整个32位线性地址空间。 DPL(描述符特权级 Descriptor Privilege Level)是允许访问此段的最低特权级(结合下面学习的"段选择子"中有一个字段(RPL)是标识这个段选择子也即这个内存访问请求的 特权级),这样是不是就把对应关系建立起来了,比如DPL为0的段只有当RPL=0时才能访问,而DPL为3的段,可由任何RPL的代码访问。这样就解释了为什么ring3的内存空间 ring0的内核代码可以任意访问,而ring0的内存空间ring3不能访问了。 TYPE(类型域): 指定了段的类型,包括代码段、数据段、TSS段、LDT段。
3) 段选择子(Selector)
我们之前还有一个疑问没解决,"我们通过GDTR中保存的基地址找到了一个所谓的全局描述表GDT,并通过"段选择子"索引到了GDT中的某一项"。那段选择字是怎么来进行索引的呢?
段选择子是一个16位的寄存器(同实模式下的段寄存器相同)
段选择子包括三部分:描述符索引(index)、TI、请求特权级(RPL)。他的index(描述符索引)部分表示所需要的段的描述符(LDT)在描述符表的位置(编号),由这个位置再根据在GDTR中存储的描述符表基址(GDTR的32位基地址用来寻址GDT的基地址)就可以找到相应的描述符(LDT)。然后用描述符表中的段基址加上逻辑地址(假如给出这样的逻辑地址 SEL:OFFSET)的OFFSET就可以转换成线性地址,段选择子中的TI值只有一位0或1,0代表选择子是在GDT选择,1代表选择子是在LDT选择。请求特权级(RPL)则代表选择子的特权级,共有4个特权级(0级、1级、2级、3级)(但是只有2中状态被实际使用,0表示最高特权级,3表示最低特权级,CPU只能访问同一特权级或级别较低特权级的段)。
例如给出逻辑地址:21h:12345678h转换为线性地址 a. 选择子SEL=21h=0000000000100 0 01b 他代表的意思是:选择子的index=4即100b选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;左后的01b代表特权级RPL=1 b. OFFSET=12345678h若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h
说了这么多,来总一下LDT和GDT:
1. 首先,要把一些概念和名词弄清楚,有很多书和网上的文章中给了很多名词,我们要能理解它们。段选择符和段选择子是一个概念,它们就相当于8080下实模式中的CS。 2. 我们给出的逻辑地址由段选择子和偏移量组成: SEL + OFFSET. 3. GDTR中保存着GDT的基地址,通过GDTR我们可以寻址到GDT(你可以理解为寻址一个数组的基地址) 4. GDT这个