KPROCESS IDT PEB Ldr 《寒江独钓》内核学习笔记(3)
继续上一篇(2)未完成的研究,我们接下来学习 KPROCESS这个数据结构。
1. 相关阅读材料
《深入理解计算机系统(原书第2版)》
二. KPROCESS
KPROCESS,也叫内核进程块。我们在开始学习它的数据机构之前,首先要思考的一个问题是,它和EPROCESS名字感觉差不多,那它们之间是什么关系呢?它们在内核区域中都位于那一层呢?
我们先来看一张图:
windows内核中的执行体负责各种与管理和策略相关的的功能(在学习笔记(2)有相关的介绍)。而内核层(或微内核)实现了操作系统的"核心机制"。进程和线程在这"两"层上都有对应的数据结构。甚至EPROCESS和KPROCESS中的某些成员域(例如TheradLstHead)是相同的东西存了两份(EPROCESS中的TheradLstHead域的链表包含了各个子线程的ETHREAD结构中的TheradLstEntry节点,而KPROCESS中的TheradLstHead域的链表包含了各个子线程的KTHREAD结构中的TheradLstEntry节点)。
清除地区分内核中的确有这两种不同层次的数据结构很重要。
那EPROCESS和KROCESS都保存在哪里呢?我们知道,我们可以通过
DWORD EProcess;
EProcess = (DWORD)PsGetCurrentProcess();
的方法来获得EPROCESS在内核中的基址,然后回想我们之前学习的EPROCESS的结构,EPROCESS结构的第一个成员域: KPROCESS Pcb 就是我们今天要学习的KPROCESS。
ntdll!_EPROCESS +0x000 Pcb : _KPROCESS +0x000 Header : _DISPATCHER_HEADER +0x010 ProfileListHead : _LIST_ENTRY [ 0x82529db0 - 0x82529db0 ] +0x018 DirectoryTableBase : [2] 0xce401a0 +0x020 LdtDescriptor : _KGDTENTRY +0x028 Int21Descriptor : _KIDTENTRY +0x030 IopmOffset : 0x20ac +0x032 Iopl : 0 '' +0x033 Unused : 0 '' +0x034 ActiveProcessors : 0 +0x038 KernelTime : 6 +0x03c UserTime : 1 +0x040 ReadyListHead : _LIST_ENTRY [ 0x82529de0 - 0x82529de0 ] +0x048 SwapListEntry : _SINGLE_LIST_ENTRY +0x04c VdmTrapcHandler : (null) +0x050 ThreadListHead : _LIST_ENTRY [ 0x826cdd58 - 0x826cdd58 ] +0x058 ProcessLock : 0 +0x05c Affinity : 1 +0x060 StackCount : 1 +0x062 BasePriority : 8 '' +0x063 ThreadQuantum : 18 '' +0x064 AutoAlignment : 0 '' +0x065 State : 0 '' +0x066 ThreadSeed : 0 '' +0x067 DisableBoost : 0 '' +0x068 PowerState : 0 '' +0x069 DisableQuantum : 0 '' +0x06a IdealNode : 0 '' +0x06b Flags : _KEXECUTE_OPTIONS +0x06b ExecuteOptions : 0 ''
换句话说,这个Pcb(或者叫内核进程控制块,或者叫KPROCESS,都是指同一个东西)是EPROCESS中的第一个"内嵌结构体"。结合结构体的内存中存放的知识,我们还知道一个进程的KPROCESS对象的地址和EPROCESS对象的地址是相同的。
每个KPROCESS对象都代表一个进程,反之每一个进程都有一个对应的KPROCESS。
首先给出KPROCESS的结构体定义,然后我们来一条一条地学习。 base\ntos\inc\ke.h (在(2)中有说明,这是windows的"开源"研究项目 WRK,从中我们可以找到很多头文件文件供我们学习之用)
typedef struct _KPROCESS { DISPATCHER_HEADER Header; LIST_ENTRY ProfileListHead; ULONG_PTR DirectoryTableBase[2]; KGDTENTRY LdtDescriptor; KIDTENTRY Int21Descriptor; USHORT IopmOffset; UCHAR Iopl; BOOLEAN Unused; volatile KAFFINITY ActiveProcessors; ULONG KernelTime; ULONG UserTime; LIST_ENTRY ReadyListHead; SINGLE_LIST_ENTRY SwapListEntry; PVOID VdmTrapcHandler; LIST_ENTRY ThreadListHead; KSPIN_LOCK ProcessLock; KAFFINITY Affinity; union { struct { LONG AutoAlignment : 1; LONG DisableBoost : 1; LONG DisableQuantum : 1; LONG ReservedFlags : 29; }; LONG ProcessFlags; }; SCHAR BasePriority; SCHAR QuantumReset; UCHAR State; UCHAR ThreadSeed; UCHAR PowerState; UCHAR IdealNode; BOOLEAN Visited; union { KEXECUTE_OPTIONS Flags; UCHAR ExecuteOptions; }; ULONG_PTR StackCount; LIST_ENTRY ProcessListEntry; } KPROCESS, *PKPROCESS, *PRKPROCESS;
1. DISPATCHER_HEADER Header
Header域表名KPROCESS对象也是一个分发器对象(dispatcher object),我们之前说过windows内核中和进程相关的数据结构有2套,EPROCESS更侧重于管理方面的信息,而KPROCESS更侧重于CPU调度方面的信息。
而这个"分发器对象"Header就涉及到: 基于线程调度的同步机制 相关的知识。
我们知道,当一个线程的控制流到达一个等待函数时(例如KeWaitForSingleObject),若等待的条件不满足,则线程调度器会将处理器的执行权交给其他处于就绪状态的线程。此后,当这个处于等待状态的线程等待条件满足时,系统会通过KiUnwaitThread与KiReadyThread函数使该线程变成"延迟的就绪状态(加入调度队列,并不能保证立刻获得CPU调度)",从而使该线程可以继续执行。
在此过程中,线程对象的一个等待块链表,即KTHREAD的WaitBlockList域(我们在之后的学习笔记中会涉及到),记录了该线程正在等待的那些对象。随着这些对象的状态发生变化,该线程的执行条件将有可能"被满足"。所以,其他的线程可以通过改变这些对象的状态来控制等待线程的执行与否(这里所谓的被等待的对象我们最常见的就是: Mutex互斥体、Event事件、SPINLOCK自旋锁)。要理解这些线程间同步的核心思想: 就是要获得到一个"令牌"后才能"行动"。
这些被等待的对象可以用来协调线程之间的行为,它们被称为"同步对象"或者"分发器对象"。在windows内核中,凡是头部以DISPATCH_HEADER开始的对象都是"分发器对象"。
关于DISPATCHER_HEADER结构的定义见 base\ntos\inc\ntosdef.h
typedef struct _DISPATCHER_HEADER { union { struct { UCHAR Type; union { UCHAR Absolute; UCHAR NpxIrql; }; union { UCHAR Size; UCHAR Hand; }; union { UCHAR Inserted; BOOLEAN DebugActive; }; }; volatile LONG Lock; }; LONG SignalState; LIST_ENTRY WaitListHead; } DISPATCHER_HEADER;
我们将在学习KTHREAD的时候会详细学习这个数据结构以及线程间同步的相关知识。我们目前只有记住几点:
1. 这个DISPATCHER_HEADER 是在KPROCESS中的,即是一个进程拥有的对象 2. 这个DISPATCHER_HEADER 是被别的线程等待的,也就是别的线程会阻塞等待在这个进程上 3. 进程对象本身是可以"被等待"的,即进程对象是一个"可等待对象"。
2. LIST_ENTRY ProfileListHead
ProfileListHead域用于当该进程参与性能分析(profiling)时,作为一个节点加入到全局的性能分析进程列表(内核全局变量KiProfileListHead)中。
+0x010 ProfileListHead : _LIST_ENTRY [ 0x82529db0 - 0x82529db0 ]
3. ULONG_PTR DirectoryTableBase[2]
DirectoryTableBase是一个只有2项的数组:
DirectoryTableBase[0]: 指向该进程的页目录表地址
DirectoryTableBase[1]: 指向该进程的超空间(Hyper Space)的页目录表地址
关于内存的页式存储、页面的相关内容,在学习笔记(2)中已经给出相关介绍
4. 针对x86处理器的系统中断调用
KGDTENTRY LdtDescriptor;
KIDTENTRY Int21Descriptor;
USHORT IopmOffset;
UCHAR Iopl;
LdtDescriptor: 指向该进程的LDT(局部描述符表)的描述符(参考学习笔记(2))
Int21Descriptor: 为了兼容DOS程序,允许它们通过int 21h指令来调用DOS系统功能
IopmOffset: 指定了IOPM(I/O权限表、I/O Prilege Map)的位置,内核通过IOPM可以控制进程的用户模式I/O访问权限。
Iopl: 定义了进程的I/O优先级(I/O Privilege Level)
+0x020 LdtDescriptor : _KGDTENTRY +0x028 Int21Descriptor : _KIDTENTRY +0x030 IopmOffset : 0x20ac +0x032 Iopl : 0 ''
关于Int21Descriptor所指向的IDT(中断描述符表),这里拓展一下。
首先,我们要明白几点,windows的技术是在不断发展的,所以我们现在再去学习windows的内核有时候会感到迷惑,同一个实现或同一个功能往往存在多种具体的实现技术,这是真实存在但却正常的现象,一方面windows要做到"向下兼容",另一方面windows要不断的封装出更好方便好用的新技术来"取代"原来的老的技术,对于我们来说,我们最好是都学,但心里一定要区分这些技术的出现"先后顺序(从历史的角度来看)",要明白当前windows中主要使用的是哪些技术。
1) 中断时一种机制,用来处理硬件需要向CPU输入信息的情况。 比如鼠标,键盘等。触发的本质是: 使CPU的执行暂时暂停并跳到"中断处理函数"中,终端处理函数已经在内存中了 2) 这些中断处理函数保存在一个叫做IDT(中断描述符)的表中,每个"中断号"在这个表中都有一项 3) IDT是在DOS时代使用的"新"技术,在更早的时候我们知道是在内存的0~1024中存在"中断处理例程"来完成中断的操作。 4) 中断可以由硬件产生(称为外部中断),也可以由软件产生(称为内部中断),在程序中写入int n指令可以产生n号中断和异常(n从0-ffh) 5) "中断"是一种CPU的硬件机制,由CPU来提供(回想CPU的EFLAG寄存器中有一个T标志位标识是否单步中断)。而中断所需要的数据结构由操作系统来维护,并保存在内存中 6) 早期的操作系统甚至是通过中断来进行内核调用的。int指令是一种c从ring3 进入ring 0的方法。比如windows在xp版本之前使用的int 2e。在x86 CPU提供了sysenter指令后,
这种方式才被放弃 7) 每一种中断对应一个中断号。CPU执行中断指令时,会去IDT表中查找对应的中断服务程序(interrupt service routine,ISR)。ISR(为了表述方便后面用ISR n表示n号中断的处理程序)
,x86CPU最大可以支持256种中断 8) 中断是CPU的机制,不管运行的是什么操作系统,只有是运行于x86架构,IDT结构式必然存在的。IDT表中的ISRs应该有操作系统提供(这也体现一个基本的道理,不管是win还是linux,
可能它们使用的技术名称不同,但是追其根本,核心的思想永远是不变的,只不过在不同的操作系统上有不同的表现形式罢了)
那既然说是IDT(中断描述符表),我们知道表一定是一个类似数组的结构,这个数组的基地址保存在IDTR上(回想一下学习笔记(2)中的GDTR是不是也是类似的思路)。IDTR是一个寄存器,它的结构如下:
这个寄存器中含有IDT表32位的基地址和16位的长度(限长)值。IDT表基地址应该对齐在8字节边界上以提高处理器的访问效率。结合GDTR的思想就很好理解了。建议拿起笔和纸画一画,再结合一些别的资料看一看,不要着急,一定要把这些基本的知识点领会了,这样学些才有效果。
typedef struct P2C_IDTR_ { P2C_U16 limit; // 范围 P2C_U32 base; // 基地址(就是IDT标的开始地址) } P2C_IDTR, *PP2C_IDTR; P2C_IDTR idtr; // 一句汇编读取到IDT的位置。 _asm sidt idtr return (void *)idtr.base;
结合上面的图片理解这段代码,我们可以获取到IDT(中断描述符表)的基地址(注意,是这个表的基地址)。那接下来我们就可以利用和我们平时索引数组的方法来索引这个结构体数组了,我们称这个IDT表(数组)中单独的每一项为"中断门(本质就是一个数组项)"。所以接下来最后一个问题,这个数组的每一项的结构是什么样的呢?
我们知道,IDT是一个最大为256项的表,每个表项为8字节。称为中断门,中断门对应的数据结构为 P2C_IDTENTRY
根据中断号对应的异常类型不同(Faults/Traps/Aborts)8个字节的意义也不同。
关于异常类型,请阅读"《深入理解计算机系统(原书第2版)》" 8.1.2 异常的类型,可以获得原理级的解释。
(图中每一个红框都代表一个"中断门"),它的数据结构定义如下:
typedef struct P2C_IDT_ENTRY_ { P2C_U16 offset_low; P2C_U16 selector; P2C_U8 reserved; P2C_U8 type:4; P2C_U8 always:1; P2C_U8 dpl:2; P2C_U8 present:1; P2C_U16 offset_high; } P2C_IDTENTRY, *PP2C_IDTENTRY;
(其中offset_low和offset_high组合起来就是一个完整的这个中断例程的入口地址,我们在编程中可以使用一些宏来进行 Bit Operation来进行拼接和拆分)
回到主线上来来,KPROCESS的Int21Descriptor中保存的就是IDTR中保存的一样,即IDT的基址,我们可以利用这个成员域来对IDT进行寻址。
5. volatile KAFFINITY ActiveProcessors
ActiveProcessors域记录了当前进程正在哪些处理器上运行。这和进程创建时的CPU处理器的亲和性有关。我们将在学习进程、线程创建过程的学习专题中再次学习它。
+0x034 ActiveProcessors : 0
6. 运行用时
ULONG KernelTime;
ULONG UserTime;
KernelTime: 记录了一个进程对象在内核模式下所花的时间
UserTime: 记录了一个进程在用户模式下所花的时间
进程的KernelTime和UserTime时间值等于其所属的线程的对应的KernelTime和UserTime值的和。
即: 进程(KernelTime + UserTime) = 线程1(KernelTime + UserTime)+ .. + 线程n(KernelTime + UserTime
但是要注意,由于仅当一个线程结束时才会更新其进程的这两个时间值,所以,若一个进程中还没有任何一个线程结束,则KRPCESS中的这两个域为0,即进程和线程的运行时间并不是实时同步的。
+0x038 KernelTime : 6 +0x03c UserTime : 1
7. LIST_ENTRY ReadyListHead
ReadyListHead域是一个双向链表的表头,该链表记录了这个进程中处于"就绪状态"但尚未被加入"全局就绪链表"的"线程",这个域的意义在于,当一个进程被换出内存以后,他所属的线程一旦就绪,则被挂到此链表中,并要求换入该进程。以后,当该进程被换入内存,ReadyListHead中的所有线程被加入到系统全局的"就绪线程链表"中。
注意,ReadyListHead所指向的链表中的每一项都是一个指向KTHREAD对象的WaitListEntry域的地址,所以,
从该链表中的每一项都可以定位到对应的线程对象(处于"就绪状态"但尚未被加入"全局就绪链表"的"线程")。从某种程序上来说,我们可以利用这个链表来进行线程枚举。
+0x040 ReadyListHead : _LIST_ENTRY [ 0x82529de0 - 0x82529de0 ]
8. SINGLE_LIST_ENTRY SwapListEntry
SwapListEntry域是一个"单链表项"(注意是一个项),当一个进程要被换出内存时,它通过此域寻址到"KiProcessOutSwapListHead为链头的单链表",并把当前进程加入到以KiProcessOutSwapListHead为链头的单链表中。
+0x048 SwapListEntry : _SINGLE_LIST_ENTRY
9. LIST_ENTRY ThreadListHead
ThreadListHead域指向一个链表头,此链表包含了该进程的所有当前线程。这个域非常重要,当一个线程被初始创建的时候,被加入到此链表中,在线程被终止的时候从链表中移除。由此我们可以看出进程和线程之间的从属关系。
+0x050 ThreadListHead : _LIST_ENTRY [ 0x826cdd58 - 0x826cdd58 ]
10. KSPIN_LOCK ProcessLock
ProcessLock域是一个自旋锁(spin lock)对象,它的用途是保护此进程中的数据成员。在内核代码中关于进程和线程各种操作的很多地方都可以看到对ProcessLock锁的操作,这可以确保对进程数据结构的修改和访问总是一致的。既不会产生"脏读","幻读"现象。
11. KAFFINITY Affinity
Affinity域指定了该进程可以在哪些处理器上运行,其类型为KAFFINITY。它采用了bit位图表示法,二进制表示的每一位分别对应于当前机器上的一个处理器(核)。
typedef ULONG_PTR KAFFINITY;
+0x05c Affinity : 1
13. 进程中的线程调度
LONG ProcessFlags
SCHAR BasePriority;
SCHAR QuantumReset;
1) ProcessFlags域包括了进程中的几个标志: AutoAlignment、DisableBoost、DisableQuantum。
AutoAlignment: 用于该进程中的内存访问对齐设置,此标志也会被传递到线程的数据结构中,当一个线程的对齐检查开关打开时,
该线程中的未对齐数据访问将会导致对齐错误(alignment fault)
DisableBoost、DisableQuantum: 与线程调度过程中的优先级提升和时限(Quantum)有关。
2) BasePriority域用于指定一个进程中的线程的"基本优先级",所有的线程在启动时都会"继承"进程的BasePriority值
3) QuantumReset域用于指定一个进程中线程的"基本时限重置值",在现代windows版本中此值被设置为6.
请参考《windows 内核原理与实现》 3.5 windows中的线程调度
13. UCHAR State
State域用来说明一个进程是否在内存中,共有六种可能的状态:
ProcessInMemory、ProcessOutOfMemory、ProcessInTransition、ProcessOutTransition、ProcessInSwap、ProcessOutSwap
所谓一个进程在内存中,后者已被换出,或者正在转移当中,是指该进程的虚拟地址空间需要占据足够的物理内存,或者虚拟空间中的内容已被换出物理内存,或者正在换入或换出的过程中。即是否产生了虚拟内存到物理地址的映射。
14. UCHAR ThreadSeed
ThreadSeed域用于为该进程的线程选择适当的"理想处理器(Ideal Processor)",在每个线程被初始化的时候,都指定此进程的ThreadSeed值作为这个新线程的"理想处理器",然后ThreadSeed域又被设置一个新的值,以便该进程的下一个线程使用。
这里的理想处理器是指在多处理器的环境下,每个线程都有一个优先选择的处理器。
15. UCHAR PowerState
PowerState域用于记录电源状态,这涉及到电源状态管理的知识。请参考《windows 内核原理与实现》 6.4节
(我们会发现,学习内核数据结构是一个很好的学习途径,在学习的过程中,它可以整个windows系统的所有知识都串起来,让你有一个整体的联动感)
16. UCHAR IdealNode
IdealNode域用于为一个"进程"选择优先的处理器节点,这是在进程初始化时设定的。这里所谓的节点是NUMA(非一致内存访问)结构中的概念
17. BOOLEAN Visited
Visited域作用不详,在WRK中也没有使用到
18. UCHAR ExecuteOptions
ExecuteOptions域用于设置一个进程的内存执行选项,这是为了支持NX(No-Execute 内存不可执行)而引入到windows XP/Server 2003中的一个域。和我们在溢出中经常谈到的DEP有很大的关系。
NX位(全名"No eXecute bit",即"禁止执行位"),是应用在CPU中的一种安全技术。 支持NX技术的系统会把内存中的区域分类为只供存储处理器指令集与只供存储数据使用的两种。任何标记了NX位的区块代表仅供存储数据使用而不是存储处理器的指令集,处理器将不会将此处的数
据作为代码执行,以此这种技术可防止大多数的缓存溢出式攻击(即一些恶意程序把自身的恶意指令集通过特殊手段放在其他程序的存储区并被执行,从而攻击甚至控制整台电脑系统)。
19. ULONG_PTR StackCount
StackCount域记录了当前"进程"中有多少"线程"的栈(线程栈)位于内存中
20. LIST_ENTRY ProcessListEntry
ProcessListEntry域用于将当前系统中的所有具有活动线程的进程串成一个链表,链表头为KiProcessListHead。看到这里,我们回想EPROCESS中也有一个类似的域成员ActiveProcessLinks。那这两个域成员有什么区别呢?
要注意:这一链表(KiProcessListHead)仅用于AMD64系统
至此,KPROCESS的数据结构也全部介绍完了。简单总结一下:
1) KPROCESS对象中记录的信息主要包括两类: 1.1) 一类和进程的内存环境相关,比如页目录表、交换状态等 1.2) 另一类是与线程相关的一些属性,比如线程列表以及线程所需要的优先级、时限设置等 2) 系统中的KROCESS通过KiProcessListHead链表串起来,但这一链表仅用于AMD64系统。我们知道,系统的总进程链表是在执行体层(EPROCESS)管理的
二. PEB(Process Environment Block 进程环境块)
我们接下来学习系统中和进程相关的最后一个数据结构: PEB(进程环境块)。
在学习PEB的数据结构之前,我们先要搞清几个问题:
1. 和EPROCESS和KPROCESS不一样,PEB位于用户模式地址空间中,PEB可以放在用户模式空间的任何地方。只要我们能告知操作系统它所在的内存位置即可(GDT也是放在用户空间任意地方)。
它包含了映像加载器、堆管理器和其他的windows系统DLL所需要的信息,因为它们需要在用户模式下修改PEB中的信息 2. 有三种方式可以访问(寻址)到PEB,它们分别对应于不同的应用场景。 2.1 在内核中我们可以先PsGetCurrentProcess得到当前进程的EPROCESS,然后通过它的成员域PEB访问到当前进程 的PEB(注意,PEB是针对每个进程而言的,每个进程都有一个PEB) 2.2 在内核中,我们可以通过KPRCB获得当前线程的ETHREAD,然后通过它的成员域来访问当TEB,然后再通过TEB的成员域来访问PEB 2.3 在用户模式中,我们可以通过FS寄存器来得到TEB,然后通过TEB的成员域访问到当前进程的PEB。
下面看看PEB的数据结构定义:
typedef struct _PEB { BOOLEAN InheritedAddressSpace; BOOLEAN ReadImageFileExecOptions; BOOLEAN BeingDebugged; BOOLEAN Spare; HANDLE Mutant; PVOID ImageBaseAddress; PPEB_LDR_DATA LoaderData; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; PVOID SubSystemData; PVOID ProcessHeap; PVOID FastPebLock; PPEBLOCKROUTINE FastPebLockRoutine; PPEBLOCKROUTINE FastPebUnlockRoutine; ULONG EnvironmentUpdateCount; PPVOID KernelCallbackTable; PVOID EventLogSection; PVOID EventLog; PPEB_FREE_BLOCK FreeList; ULONG TlsExpansionCounter; PVOID TlsBitmap; ULONG TlsBitmapBits[0x2]; PVOID ReadOnlySharedMemoryBase; PVOID ReadOnlySharedMemoryHeap; PPVOID ReadOnlyStaticServerData; PVOID AnsiCodePageData; PVOID OemCodePageData; PVOID UnicodeCaseTableData; ULONG NumberOfProcessors; ULONG NtGlobalFlag; BYTE Spare2[0x4]; LARGE_INTEGER CriticalSectionTimeout; ULONG HeapSegmentReserve; ULONG HeapSegmentCommit; ULONG HeapDeCommitTotalFreeThreshold; ULONG HeapDeCommitFreeBlockThreshold; ULONG NumberOfHeaps; ULONG MaximumNumberOfHeaps; PPVOID *ProcessHeaps; PVOID GdiSharedHandleTable; PVOID ProcessStarterHelper; PVOID GdiDCAttributeList; PVOID LoaderLock; ULONG OSMajorVersion; ULONG OSMinorVersion; ULONG OSBuildNumber; ULONG OSPlatformId; ULONG ImageSubSystem; ULONG ImageSubSystemMajorVersion; ULONG ImageSubSystemMinorVersion; ULONG GdiHandleBuffer[0x22]; ULONG PostProcessInitRoutine; ULONG TlsExpansionBitmap; BYTE TlsExpansionBitmapBits[0x80]; ULONG SessionId; } PEB, *PPEB;
可能是我水平的原因,我翻遍了google和baidu都没有找到一篇详细讲PEB结构的文章。希望有知道的朋友能不吝赐教,分享一些这方面的好的资料。我就我手上有的资源尽我的能力来分析一下这个PEB结构吧。
http://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/NT%20Objects/Process/PEB.html http://blog.csdn.net/imquestion/article/details/16421 《windows内核情景分析》P247页,PEB结构
1. BOOLEAN InheritedAddressSpace
不详..
2. BOOLEAN ReadImageFileExecOptions
不详..
3. BOOLEAN BeingDebugged
BeingDebugged域指示了当前这个进程是否正在被调试。有一种反调试技术就是在进程加载的时候检测当前PEB的BeingDebugged域成员,以此来判断当前进程是否处于"被调试"状态。
//这个函数也可以被用来检测当前进程是否处在调试状态 BOOL WINAPI CheckRemoteDebuggerPresent( _In_ HANDLE hProcess, _Inout_ PBOOL pbDebuggerPresent );
4. BOOLEAN Spare
不详...
5. HANDLE Mutant
不详..
6. PVOID ImageBaseAddress
ImageBaseAddress域保存的是进程映像基址,就是PE中的IMAGE_OPTIONAL_HEADER->ImageBase对应的值。对于EXE来说,默认的ImageBase为0x400000;对于DLL来说,它是0x10000000
7. PPEB_LDR_DATA LoaderData(也有叫Ldr的)
LoaderData域是PEB中一个很重要的成员域,它是一个指向PEB_LDR_DATA结构体的指针。它由PE Loader(加载器)填充,也就说,在这个指针指向的结构中,可以找到很多在PE中包含的信息。另外,我们在做Buffer OverFlow的时候经常会遇到这个数据结构,枚举用户进程加载的模块就和它密切相关。我们扩展出去,详细学习一下这个结构。
typedef struct _PEB_LDR_DATA { ULONG Length; BOOLEAN Initialized; PVOID SsHandle; LIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; } PEB_LDR_DATA, *PPEB_LDR_DATA;
7.1) ULONG Length
//结构体大小 Size of structure, used by ntdll.dll as structure version ID.
7.2) BOOLEAN Initialized
//指示是当前进程时候已经被初始化过了 If set, loader data section for current process is initialized.
7.3) 加载模块链表,这三个双链表的内容都一样,但是顺序不一样。
LIST_ENTRY nLoadOrderModuleList //Doubly linked list containing pointers to LDR_MODULE structure for previous and next module in load order. LIST_ENTRY InMemoryOrderModuleList //As above, but in memory placement order. LIST_ENTRY InInitializationOrderModuleList //As InLoadOrderModuleList, but in initialization order.
nLoadOrderModuleList、InMemoryOrderModuleList、InInitializationOrderModuleList这三个域都是指向它们各自的双链表中的下一个LDR_MODULE的LIST_ENTRY
typedef struct _LDR_MODULE { LIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; PVOID BaseAddress; PVOID EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName; ULONG Flags; SHORT LoadCount; SHORT TlsIndex; LIST_ENTRY HashTableEntry; ULONG TimeDateStamp; } LDR_MODULE, *PLDR_MODULE;
(感谢原作者的劳动,这两张图很清晰的表名了动态寻址程序加载模块的线路图)
LoaderData 是指向 PEB_LDR_DATA 的指针,通过 PEB_LDR_DATA ,我们可以找到进程载入的所有模块(kernel32.dll、ntdll.dll、user.dll....)
例如我们可以利用类似的代码来进行对kernel32.dll的动态寻址:
find_kernel32: push esi xor ecx, ecx mov esi, [fs:ecx+0x30] //PEB = FS:0x30 mov esi, [esi + 0x0c] //LDR = PEB:0x0c mov esi, [esi + 0x1c] //FLink = LDR:0x1c next_module: mov eax, [esi + 0x8] //函数返回时eax保存模块基址 mov edi,[esi+0x20] //指向BaseDllName mov esi ,[esi] //这里是转移到链表的下一个 cmp [edi+12*2],cx //kernel32.dll 12*2个字节最后一位正好是00 jne next_module pop esi Ret
这里还要注意一个问题,就是既然这三个链表都是一样的,那我们在枚举进程加载模块的时候随便选一条就可以了吗? 并不是这样的,在XP和win7下InitializationOrderModuleList中节点的顺序是不同的。
看雪上的这篇文章很好的说明了这点: http://bbs.pediy.com/showthread.php?t=149527
InLoadOrderModuleList在所有系统中,按照顺序:第一个是EXE模块本身的ImageBase,第二个是NTDLL.DLL,第三个是KERNEL32.DLL
好,继续回到我们的主线PEB的数据结构的学习上来。
8. PRTL_USER_PROCESS_PARAMETERS ProcessParameters
ProcessParameters域是指向 RTL_USER_PROCESS_PARAMETERS 的指针,RTL_USER_PROCESS_PARAMETERS 中是一些进程的参数。
typedef struct _RTL_USER_PROCESS_PARAMETERS { ULONG MaximumLength; ULONG Length; ULONG Flags; ULONG DebugFlags; PVOID ConsoleHandle; ULONG ConsoleFlags; HANDLE StdInputHandle; HANDLE StdOutputHandle; HANDLE StdErrorHandle; UNICODE_STRING CurrentDirectoryPath; HANDLE CurrentDirectoryHandle; UNICODE_STRING DllPath; UNICODE_STRING ImagePathName; UNICODE_STRING CommandLine; PVOID Environment; ULONG StartingPositionLeft; ULONG StartingPositionTop; ULONG Width; ULONG Height; ULONG CharWidth; ULONG CharHeight; ULONG ConsoleTextAttributes; ULONG WindowFlags; ULONG ShowWindowFlags; UNICODE_STRING WindowTitle; UNICODE_STRING DesktopName; UNICODE_STRING ShellInfo; UNICODE_STRING RuntimeData; RTL_DRIVE_LETTER_CURDIR DLCurrentDirectory[0x20]; } RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
这个数据结构包含了当前进程的路径,包含的DLL路径,命令行,当前文件目录(在windows编程中,我们经常会需要用到当前相对路径,当前绝对路径)等很多信息。详情请参考:
http://undocumented.ntinternals.net/UserMode/Structures/RTL_USER_PROCESS_PARAMETERS.html
9. PVOID ProcessHeap
ProcessHeap 域指向的是进程堆(默认的那个)的首地址,每个进程在新建的时都会由系统自动创建一个默认堆以供使用。这也就是为什么我们在程序中可以直接使用malloc来动态申请堆内存的时候不需要指定使用的是哪个堆。
int*p; p=(int*)malloc(sizeof(int));
同时,除了进程的默认堆之外,我们也可以在进程运行中创建新的堆,并从我们创建的堆中来分配内存。
HANDLE hp; HLOCAL h1; //创建一个动态堆 hp = HeapCreate(0, 0, 0); //从我们创建的堆中申请一个byte的堆空间 h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 16);
10. PVOID FastPebLock
FastPebLock域存放的是PEBLOCKROUTINE这个例程函数需要用到的参数。
11. PEB加锁/解锁回调例程
PPEBLOCKROUTINE FastPebLockRoutine;
PPEBLOCKROUTINE FastPebUnlockRoutine;
typedef void (*PPEBLOCKROUTINE) ( PVOID PebLock );
PEB中的这两个域指向的是两个回调函数的入口地址,从名字上看是上锁和解锁的回调函数。我不太清楚它们存在的意义。
12. ULONG EnvironmentUpdateCount
//进程的环境变量更改的次数 Counter of process environment updates.
13. PPVOID KernelCallbackTable
KernelCallbackTable域用于从内核"回调"用户空间的函数。
14. 其他
PVOID EventLogSection;
PVOID EventLog;
PPEB_FREE_BLOCK FreeList;
ULONG TlsExpansionCounter;
不详..
15. PVOID TlsBitmap
TlsBitmap域代表TLS位图
16. 其他
ULONG TlsBitmapBits[0x2]; PVOID ReadOnlySharedMemoryBase; PVOID ReadOnlySharedMemoryHeap; PPVOID ReadOnlyStaticServerData; PVOID AnsiCodePageData; PVOID OemCodePageData; PVOID UnicodeCaseTableData; ULONG NumberOfProcessors; ULONG NtGlobalFlag; BYTE Spare2[0x4]; LARGE_INTEGER CriticalSectionTimeout; ULONG HeapSegmentReserve; ULONG HeapSegmentCommit; ULONG HeapDeCommitTotalFreeThreshold; ULONG HeapDeCommitFreeBlockThreshold; ULONG NumberOfHeaps; ULONG MaximumNumberOfHeaps; PPVOID *ProcessHeaps; PVOID GdiSharedHandleTable; PVOID ProcessStarterHelper; PVOID GdiDCAttributeList; PVOID LoaderLock; ULONG OSMajorVersion; ULONG OSMinorVersion; ULONG OSBuildNumber; ULONG OSPlatformId; ULONG ImageSubSystem; ULONG ImageSubSystemMajorVersion; ULONG ImageSubSystemMinorVersion; ULONG GdiHandleBuffer[0x22]; ULONG PostProcessInitRoutine; ULONG TlsExpansionBitmap; BYTE TlsExpansionBitmapBits[0x80]; ULONG SessionId;
不详..
(原谅我全部这样写上不详,我确实没在网上和书上找到有关PEB的相关数据结构的资料,不过Ldr这块的内容倒是挺多了,利用PEB来枚举进程DLL模块也是个很成熟的技术了,在shellcode的编写中,再次希望看到这篇文章的牛牛如果有关于这方面的补充资料的,望分享之,不胜感激)
至此,EPROCESS/KPROCESS/PEB的数据结构学习就结束了。我们可以发现,通过学习这些基础的数据结构,我们可以积累大量的操作系统的相关知识,我甚至觉得这些结构才是windows的集大成者,通过这些结构的学习,我们可以辐射出去扩展到windows的很多领域的知识,以后应多多从这方面去思考一些技术。