学习逆向工程也快一年的时间了,从开始的16位实模式下的内存寻址模型到32位下保护模式内存的模型,实模式下的较为简单,段地址*16+偏移地址就是寻址的内存,但是保护模式下就远远没有这么简单了。很简单的一个例子,windows下支持多进程,并且进程之间是相互隔离的,不允许随意的将一个进程的数据写入到另外一个进程的空间中,并且每个时刻CPU只允许运行一个程序,何以为见呢?
打开OD,分别用OD加载两个程序,而且是两个不同的进程,就看代码段的数据,假设我们查看0x0041F69A处的指令,我们发现两个进程的数据是不一样的,这里也会让我们感到奇怪,为什么两个进程处在相同地址处的数据不同呢?当然我们知道是保护机制中隔离在作怪,但是他是如何做到隔离的呢?为什么不能随意的向另外一个进程的空间中写入值呢?所以就这几天的学习情况对一下几个问题进行讨论:
1.OD或者其他调试器中的地址到底是个什么样的地址?虚拟地址,线性地址和物理地址间的联系又是什么?
2.Windows系统是如何做到每个进程地址空间的隔离的?
3.传说中的GDT,LDT表的作用?
4.虚拟地址是如何映射到物理地址的?
5.如何通过虚拟地址找到进程实际所在的物理地址?
6.如何通过实验来验证windows中某个进程中的一个虚拟地址就是物理内存中的地址呢?
7.如何通过修改映射关系来达到在保护模式下,两个不同的进程可以写入到对方进程的物理空间中呢?
首先还是得简单介绍一下保护模式下的两种保护机制:分段和分页。这个应该都比较好理解,将内存中的数据进行分段,不同类型的数据处于不同的段中,比如代码段就处于代码段的内存中,数据就处于数据段的代码中等等。对于分页呢,我的理解是,将内存按照一定的大小划分区域,比如按照4KB大小,将内存分为N块,每块就称为是一页。不知道这么理解是不是正确的。
然后一个比较不同的方面就是在保护模式下,段寄存器(CS,DS,ES,SS,FS,GS)不再作为寻址用,因为只有16位,而且每个寄存器都是32位的,足以进行寻址的操作了。当然,这里段寄存器我们也称为段选择器。这里,段选择器就不再像实模式下进行寻址了,而是为一个索引,索引一个二进制的结构,这个结构中包含了每一段的细节,比如说代码段具有哪些权限,什么权限的进程可以访问它(Ring3 Or Ring0)。当然这个结构我们称为段描述表,其中的项称为段描述符。
当然啦这样的描述符表就是全局描述符表(GDT)和局部描述符表(LDT)了。也就是传说中的GDT和LDT了,当然每个系统中只存在一个GDT,并且是必须存在的,而LDT是可选择的,并且每个任务都有一个不同的LDT,这次我们主要集中在GDT上。下面看一下段选择器的结构及GDT的结构:
上面图还是非常清晰的,我们可以看到段选择器包含了当前进程请求的特权,位0表示当前进程请求权限,即是ring0还是ring3,而且一般也只用到这两个权限,位2表示的是使用GDT还是LDT来存放段描述符的,位3到位15就是存放索引的位置。
GDT存放在哪里呢?Intel中有个48位的寄存器GDTR,存放的就是GDTR的地址,应该是从系统初始化的时候就开始存在了,并且这个结构也只有特定的权限进程进行才能进行读写操作,GDTR中,低16位表示的是GDT的大小(以字节为单位),其余32位存储了GDT的起始线性地址,即第一个字节的线性地址。然后就是段描述符,是如下的结构:
是一个64位的结构,最上面的结构是高32位,下面的结构是低32位的结构,其中主要的几个位置就DPL,表示引用段的权限,其他位的作用上图描述的非常清楚,不再多说。下面我们就来看看GDT的结构,我在xp sp3下作的实验,调试器内核模式的,否则看不到GDTR及其结构,首先在虚拟机中用OD加载一个程序,我们来看看每个段寄存器的值是多少:
换了几个不同的程序,发现在ring3层的程序,CS为0x1B,SS为0x23,ring0下就没有继续看了,我们拿CS做例子吧,将CS做分解:0000000000011 0 11
第1,2位为3(0B11),第2位为0,最高位为3,可以看出表示的是引用的段权限为最低,并且存放在GDT中,那我们就看看GDT长啥样子的:
这里使用gdtr来表示GDT手字节地址,gdtl表示的是GDT中索引的个数,下面我们就来看看GDT表中的数据,如果单单使用dd查看的话太不直观,windbg中我们可以使用dg命令来查看GDT中的结构和值,第一个参数为索引开始值,第二个参数为要查看索引结束值:
3 | Sel Base Limit Type l ze an es ng Flags |
4 | ---- -------- -------- ---------- - -- -- -- -- -------- |
5 | 0000 00000000 00000000 <Reserved> 0 Nb By Np Nl 00000000 |
6 | 0008 00000000 ffffffff Code RE 0 Bg Pg P Nl 00000c9a |
7 | 0010 00000000 ffffffff Data RW 0 Bg Pg P Nl 00000c92 |
8 | 0018 00000000 ffffffff Code RE 3 Bg Pg P Nl 00000cfa |
9 | 0020 00000000 ffffffff Data RW 3 Bg Pg P Nl 00000cf2 |
10 | 0028 80042000 000020ab TSS32 Busy 0 Nb By P Nl 0000008b |
11 | 0030 ffdff000 00001fff Data RW 0 Bg Pg P Nl 00000c92 |
12 | 0038 00000000 00000fff Data RW Ac 3 Bg By P Nl 000004f3 |
13 | 0040 00000400 0000ffff Data RW 3 Nb By P Nl 000000f2 |
14 | 0048 00000000 00000000 <Reserved> 0 Nb By Np Nl 00000000 |
15 | 0050 8054af00 00000068 TSS32 Avl 0 Nb By P Nl 00000089 |
16 | 0058 8054af68 00000068 TSS32 Avl 0 Nb By P Nl 00000089 |
17 | 0060 00022f40 0000ffff Data RW 0 Nb By P Nl 00000092 |
18 | 0068 000b8000 00003fff Data RW 0 Nb By P Nl 00000092 |
19 | 0070 ffff7000 000003ff Data RW 0 Nb By P Nl 00000092 |
20 | 0078 80400000 0000ffff Code RE 0 Nb By P Nl 0000009a |
21 | 0080 80400000 0000ffff Data RW 0 Nb By P Nl 00000092 |
22 | 0088 00000000 00000000 Data RW 0 Nb By P Nl 00000092 |
23 | 0090 00000000 00000000 <Reserved> 0 Nb By Np Nl 00000000 |
24 | 0098 00000000 00000000 <Reserved> 0 Nb By Np Nl 00000000 |
25 | 00A0 821b2350 00000068 TSS32 Avl 0 Nb By P Nl 00000089 |
26 | 00A8 00000000 00000000 <Reserved> 0 Nb By Np Nl 00000000 |
27 | 00B0 00000000 00000000 <Reserved> 0 Nb By Np Nl 00000000 |
28 | 00B8 00000000 00000000 <Reserved> 0 Nb By Np Nl 00000000 |
29 | 00C0 00000000 00000000 <Reserved> 0 Nb By Np Nl 00000000 |
30 | 00C8 00000000 00000000 <Reserved> 0 Nb By Np Nl 00000000 |
31 | 00D0 00000000 00000000 <Reserved> 0 Nb By Np Nl 00000000 |
32 | 00D8 00000000 00000000 <Reserved> 0 Nb By Np Nl 00000000 |
33 | 00E0 f871a000 0000ffff Code RE Ac 0 Nb By P Nl 0000009f |
34 | 00E8 00000000 0000ffff Data RW 0 Nb By P Nl 00000092 |
35 | 00F0 804fb688 000003b7 Code EO 0 Nb By P Nl 00000098 |
36 | 00F8 00000000 0000ffff Data RW 0 Nb By P Nl 00000092 |
37 | 0100 f8397400 0000ffff Data RW Ac 0 Bg By P Nl 00000493 |
38 | 0108 f8397400 0000ffff Data RW Ac 0 Bg By P Nl 00000493 |
39 | 0110 f8397400 0000ffff Data RW Ac 0 Bg By P Nl 00000493 |
40 | 0118 00008003 0000f120 <Reserved> 0 Nb By Np Nl 00000000 |
我们查看CS和SS所在段的信息:
3 | Sel Base Limit Type l ze an es ng Flags |
4 | ---- -------- -------- ---------- - -- -- -- -- -------- |
5 | 001B 00000000 ffffffff Code RE 3 Bg Pg P Nl 00000cfa |
8 | Sel Base Limit Type l ze an es ng Flags |
9 | ---- -------- -------- ---------- - -- -- -- -- -------- |
10 | 0023 00000000 ffffffff Data RW 3 Bg Pg P Nl 00000cf2 |
我们可以看出这里第五列为段的访问权限,和我们手工的出来的结果相同,当然这里我们可以看出来,这里我们根本没有分段,环0和环3的起始结束地址都是从(00000000-ffffffff),所有的段描述符都指向同一个段,所以windows并没有有使用Intel硬件平台为其提供的所有附属项目。所以我猜测windows下虚拟地址和线性地址的值是相同的。而且我也发现其实GDT在进程间隔离也没起到太大的作用,因为这里GDT也只是起到查看段的权限的作用,当然也有门的一些信息,我们这里不讨论门,以上也都是我个人的理解,如果有误也希望各位进行指出。
下面就是分页保护,这个就和进程地址间的隔离关系就比较大了,我们都知道,32位下每个进程分配的空间为4GB,当然这个4GB只是进程看到的,感觉到的,是虚拟地址,当然这个地址也是线性地址,并不是都能使用,比如说我们看到指令的地址都处于00400000开始位置,如果到00200000的话还能访问吗?基本上是不行的,你可以用OD尝试一下,这里的位置是不可访问的,执行到此位置就会发生异常,当然如果都能访问的话那岂不是相当恐怖的一件事情!你一个进程就全部占了4GB空间,你让其他进程怎么办?而且你的物理内存很大情况下也不会有4GB的,像我的虚拟机也就只有512MB呢。猜想00200000这个地址应该是被映射到别的进程的空间里面了,是很有可能的。当然映射绝非那么简单的映射。
启动分页后,线性地址空间被分为几个固定长度的存储块,这些块被称为页(大小可以是4KB,2MB或者4MB),当然我们实际中使用的页大小都为4KB,当然如果不启动分页的话线性地址就是物理地址,但是启动后就不一样了,这个线性地址不再是一个简单的地址了,这32位被分成了不同的部分,每个部分有不同的含义,如下:
一个线性地址被分为3个部分,第一个部分(0-11位)才是偏移,第三个部分(22-31位)指定了页目录的数组结构的项,这些项被称为页目录项(PDE),并且页目录的第一个字节地址,即其开始位置的物理地址(不是线性地址)存放在CR3寄存器中,当然如果都是线性地址的话那还怎么到真实的内存中查找值呢?由于索引一共就10位,所以一个页目录最多存储1024个PDE,每个PDE存储了页表的二级数组结构的起始物理地址(不是线性地址),即PDE存储了页表第一个字节的物理地址。第二个部分(12-21位)指定了页表中特定的项,页表中的项被顺序排列成数组,称为页表项(PTE),当然,一个页表最多能存储1024个PTE。当然,每个PTE存储的就是内存页的第一个物理地址,如果将第一个部分(0-11位)的值和PTE提供的物理基址相加,就可以得到物理内存中第一个字节的地址了。具体关系见下面的图:
要注意的是CR3,PDE和PTE中的值都是物理地址!通过CR3来查找进程页目录的物理地址,所以存在如下关系:CR3相当于我的电脑,PDE相当于根目录,PTE相当于磁盘(C,D,E),就是一个索引的关系,通过索引我们就可以找到线性地址所对应的物理地址了,下面看看PDE和PTE的结构:
如果要分析内存保护的话,重要的几项就是U/S标志和W标志。U/S定义了两个基于页的不同特权级:用户和超级用户。如果该位被清0,则PTE指向的页(或在给定PDE之下的页被分配为超级用户权限)。W标志位用于指明一个页或者一组页是只读还是可写,如果W位标志被置位,表明该页(或该页组)可写可读。
但是一般情况下,我们内存是开启PAE分页的,CR4寄存器中有个PAE标志位,如果开启电话那么线性地址就被分为4个部分而不是3个部分,具体见下面的图,PAE的效果就是可以将处理器访问的物理内存扩充到4GB以上,使可用的地址线达到52条:
PDPT索引就是页目录指针表(PDPT),共两位,一共可以有4个数组,其中的4个数组被称为PDPTE。当然此时分页的寄存器和不开启PAE情况下作用也是相同的,CR3指向PDPT的物理地址,当然此时CR3并不是存储物理地址所有52为,因为很多位都为0,所以没必要所有都存储进去,寻找物理地址的过程如下:
这里我们发现将线性地址转换为物理地址的过程和不开启PAE时候的情况是基本上相同的,并且如果开启PAE分页,那么PDPTE,PDE和PTE都是64位的,并且标志位几乎完全相同,最重要的在于其实物理地址在大小上是可变的,而且根据当前处理器可用的地址线数量进行变化:
当然重要的还是CR3寄存器,CR3寄存器存储页目录表首字节的物理地址。如果每个进程都有自己CR3副本,并且把该副本作为内核维护调度上下文的一部分,则两个进程完全会出现拥有相同的线性地址,就像我们OD调试的那样,但是最终映射到的物理地址一定是不相同的!不知道在哪看到的,进程切换的时候CR3的值也发生相应的改变,这样CPU就可以切换到不同的进程空间中了,因为每个进程都是具有自己的空间,而且相互隔离的。还有一个CR0寄存器,其中的WP(写保护)如果置为的话那么超级代码也无法写入只读的保护区域,需要将保护位置为0才行,比如ssdt表,如果我们直接在内核中修改其地址的话肯定会蓝屏的,需要将CR0的WP位变为0,下面为这些控制寄存器的结构图:
结构还是相当清晰的,还有要注意的是,如果开启PAE分页的话,CR3寄存器的位会发生相应的改变:
这里我们发现,PDPT的地址只使用了27位,那如何表示一个52位的地址呢?其实很简单,我们只需要将后面没使用的位全部填充为0就可以了。在不开启PAE分页的情况下,PTE的最高那20位表示的为页基址,比如0x12345,那么后面的位我们可以看做包含着0,即0x12345[0][0][0],如果不含隐式0,这个地址有时称作页帧号(PNF),页帧表示无力内存中的一个区域,用于存需要占用物理内存的页。
windows下每个进程都分配了一个自己专用的CR3控制寄存器的值,并且CR3控制寄存器有页目录20位的PFN,所以每个进程都有自己的页目录,相关的CR3值存储在进程KPROESS的DirectoryTableBase这个字段中,KPROCESS是EPROCESS的子结构,什么是子结构呢?这里表示KPROCESS和EPROCESS部分有重叠:
2 | **** NT ACTIVE PROCESS DUMP **** |
3 | PROCESS 821b9830 SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000 |
4 | DirBase: 00b18000 ObjectTable: e1000c98 HandleCount: 279. |
7 | PROCESS 81c57760 SessionId: none Cid: 0224 Peb: 7ffd4000 ParentCid: 0004 |
8 | DirBase: 08c40020 ObjectTable: e13f8fb8 HandleCount: 19. |
11 | PROCESS 81fe6648 SessionId: 0 Cid: 0264 Peb: 7ffd5000 ParentCid: 0224 |
12 | DirBase: 08c40040 ObjectTable: e1524858 HandleCount: 344. |
15 | PROCESS 8211d020 SessionId: 0 Cid: 027c Peb: 7ffd6000 ParentCid: 0224 |
16 | DirBase: 08c40060 ObjectTable: e1516968 HandleCount: 509. |
19 | PROCESS 820fdae0 SessionId: 0 Cid: 02ac Peb: 7ffdb000 ParentCid: 027c |
20 | DirBase: 08c40080 ObjectTable: e17ea610 HandleCount: 266. |
23 | PROCESS 820bed78 SessionId: 0 Cid: 02c8 Peb: 7ffdf000 ParentCid: 027c |
24 | DirBase: 08c400c0 ObjectTable: e17eae98 HandleCount: 331. |
27 | PROCESS 81f4d770 SessionId: 0 Cid: 0368 Peb: 7ffd8000 ParentCid: 02ac |
28 | DirBase: 08c400e0 ObjectTable: e1a81188 HandleCount: 25. |
31 | PROCESS 81cd0ca8 SessionId: 0 Cid: 0374 Peb: 7ffd9000 ParentCid: 02ac |
32 | DirBase: 08c40100 ObjectTable: e17509d8 HandleCount: 199. |
35 | PROCESS 81cb5a98 SessionId: 0 Cid: 03c4 Peb: 7ffdd000 ParentCid: 02ac |
36 | DirBase: 08c40120 ObjectTable: e1aceaa0 HandleCount: 252. |
39 | PROCESS 81c57da0 SessionId: 0 Cid: 0420 Peb: 7ffd3000 ParentCid: 02ac |
40 | DirBase: 08c40140 ObjectTable: e1ac9c08 HandleCount: 1137. |
43 | PROCESS 81c7e4d8 SessionId: 0 Cid: 0460 Peb: 7ffd8000 ParentCid: 02ac |
44 | DirBase: 08c40160 ObjectTable: e1ae2328 HandleCount: 71. |
47 | PROCESS 81cf98e0 SessionId: 0 Cid: 047c Peb: 7ffdc000 ParentCid: 02ac |
48 | DirBase: 08c40180 ObjectTable: e16ed958 HandleCount: 203. |
51 | PROCESS 821001c0 SessionId: 0 Cid: 05e8 Peb: 7ffd4000 ParentCid: 05c0 |
52 | DirBase: 08c401e0 ObjectTable: e1481e00 HandleCount: 418. |
55 | PROCESS 81fcb5e8 SessionId: 0 Cid: 0638 Peb: 7ffd9000 ParentCid: 02ac |
56 | DirBase: 08c40200 ObjectTable: e1524728 HandleCount: 138. |
59 | PROCESS 820f4da0 SessionId: 0 Cid: 0718 Peb: 7ffd6000 ParentCid: 05e8 |
60 | DirBase: 08c401a0 ObjectTable: e17cd9a0 HandleCount: 135. |
63 | PROCESS 81f60c08 SessionId: 0 Cid: 0740 Peb: 7ffd9000 ParentCid: 05e8 |
64 | DirBase: 08c40260 ObjectTable: e2046a38 HandleCount: 77. |
67 | PROCESS 81c4b650 SessionId: 0 Cid: 00b4 Peb: 7ffda000 ParentCid: 02ac |
68 | DirBase: 08c40220 ObjectTable: e1ff58c0 HandleCount: 275. |
71 | PROCESS 81e23a20 SessionId: 0 Cid: 040c Peb: 7ffd3000 ParentCid: 02ac |
72 | DirBase: 08c40300 ObjectTable: e14b3080 HandleCount: 99. |
73 | Image: TPAutoConnSvc.exe |
75 | PROCESS 8202ba80 SessionId: 0 Cid: 0694 Peb: 7ffdc000 ParentCid: 02ac |
76 | DirBase: 08c40320 ObjectTable: e1e1a2a8 HandleCount: 107. |
79 | PROCESS 81ac2da0 SessionId: 0 Cid: 04ec Peb: 7ffdc000 ParentCid: 0420 |
80 | DirBase: 08c40340 ObjectTable: e1d4f8f0 HandleCount: 39. |
83 | PROCESS 8204f410 SessionId: 0 Cid: 06ec Peb: 7ffde000 ParentCid: 040c |
84 | DirBase: 08c40360 ObjectTable: e1e93e40 HandleCount: 71. |
85 | Image: TPAutoConnect.exe |
87 | PROCESS 81ce5650 SessionId: 0 Cid: 05b4 Peb: 7ffde000 ParentCid: 0420 |
88 | DirBase: 08c402e0 ObjectTable: e14f6820 HandleCount: 142. |
91 | PROCESS 81aed818 SessionId: 0 Cid: 01b0 Peb: 7ffda000 ParentCid: 05e8 |
92 | DirBase: 08c40240 ObjectTable: e1a680e8 HandleCount: 60. |
93 | Image: ?á°????a[LCG].exe |
95 | PROCESS 820f1340 SessionId: 0 Cid: 0254 Peb: 7ffd6000 ParentCid: 05e8 |
96 | DirBase: 08c400a0 ObjectTable: e12ac518 HandleCount: 15. |
这个命令显示了系统中所有活动进程的列表,!process命令用于显示一个或者多个进程的信息,第一个参数一般为进程ID或者是分配给进程的EPROCESS块的16进制信息。我们看看打印出的结果,DirBase这个变量表示的就是存储在CR3寄存器中的物理地址,PROCESS后面的16进制数表示的是进程EPROCESS的线性地址,我们可以查看一下monitor.exe这个进程的EPROCESS和KPROCESS值:
1 | kd> dt nt!_EPROCESS 820f1340 |
3 | +0x06c ProcessLock : _EX_PUSH_LOCK |
4 | +0x070 CreateTime : _LARGE_INTEGER 0x1d0c299`b24158da |
5 | +0x078 ExitTime : _LARGE_INTEGER 0x0 |
6 | +0x080 RundownProtect : _EX_RUNDOWN_REF |
7 | +0x084 UniqueProcessId : 0x00000254 Void |
8 | +0x088 ActiveProcessLinks : _LIST_ENTRY [ 0x8055b158 - 0x81aed8a0 ] |
9 | +0x090 QuotaUsage : [3] 0x848 |
10 | +0x09c QuotaPeak : [3] 0x870 |
11 | +0x0a8 CommitCharge : 0xb5 |
12 | +0x0ac PeakVirtualSize : 0x2168000 |
13 | +0x0b0 VirtualSize : 0x1a7e000 |
14 | +0x0b4 SessionProcessLinks : _LIST_ENTRY [ 0xf8bb6014 - 0x81aed8cc ] |
15 | +0x0bc DebugPort : 0x81aee880 Void |
16 | +0x0c0 ExceptionPort : 0xe13e20f8 Void |
17 | +0x0c4 ObjectTable : 0xe12ac518 _HANDLE_TABLE |
18 | +0x0c8 Token : _EX_FAST_REF |
19 | kd> dt nt!_KPROCESS 820f1340 |
20 | +0x000 Header : _DISPATCHER_HEADER |
21 | +0x010 ProfileListHead : _LIST_ENTRY [ 0x820f1350 - 0x820f1350 ] |
22 | +0x018 DirectoryTableBase : [2] 0x8c400a0 |
23 | +0x020 LdtDescriptor : _KGDTENTRY |
24 | +0x028 Int21Descriptor : _KIDTENTRY |
25 | +0x030 IopmOffset : 0x20ac |
28 | +0x034 ActiveProcessors : 0 |
29 | +0x038 KernelTime : 0x16 |
31 | +0x040 ReadyListHead : _LIST_ENTRY [ 0x820f1380 - 0x820f1380 ] |
32 | +0x048 SwapListEntry : _SINGLE_LIST_ENTRY |
33 | +0x04c VdmTrapcHandler : (null) |
34 | +0x050 ThreadListHead : _LIST_ENTRY [ 0x82033b78 - 0x82033b78 ] |
35 | +0x058 ProcessLock : 0 |
38 | +0x062 BasePriority : 8 '' |
39 | +0x063 ThreadQuantum : 6 '' |
40 | +0x064 AutoAlignment : 0 '' |
42 | +0x066 ThreadSeed : 0 '' |
43 | +0x067 DisableBoost : 0 '' |
44 | +0x068 PowerState : 0 '' |
45 | +0x069 DisableQuantum : 0 '' |
46 | +0x06a IdealNode : 0 '' |
47 | +0x06b Flags : _KEXECUTE_OPTIONS |
48 | +0x06b ExecuteOptions : 0x32 '2' |
dt表示的就是查看某个结构,最后的参数为要查看EPROCESS或者KPROCESS的地址,之所以称为子结构就是因为两个结构是重合的!EPROCESS的第一个字段就是KPROCESS,我们可以看见其中DirectoryTableBase的值就是DirBase的值。
好的,下面我们就来做几个实验:
实验一
分别使用手工和windbg的方式将某个虚拟地址转换为物理地址,使用windbg直接查看物理内存中的内容,随后直接修改物理地址中的内容,看看虚拟地址中的数据是否发生变化。同时使用调试工具直接定位物理内存的方法。
首先我们查看一下CR4寄存器的值,使用.formats命令可以查看到其每位的内容:
6 | Binary: 00000000 00000000 00000110 11111001 |
8 | Time: Thu Jan 01 08:29:45 1970 |
9 | Float: low 2.50132e-042 high 0 |
我们可以看到,这里第5位为1,表示开启PAE分页,我们用OD附加我们经常使用的monitor.exe这个程序,加载sys时候经常用到,这里入口如下:
我们可以看到虚拟地址为0041F69A,这个也就是线性地址了,在windows下,那么我们如何得到其物理地址呢?按照上面的PDPT转换过程,首先我们找到进程页面的物理地址:
2 | PROCESS 820f1340 SessionId: 0 Cid: 0254 Peb: 7ffd6000 ParentCid: 05e8 |
3 | DirBase: 08c400a0 ObjectTable: e12ac518 HandleCount: 15. |
5 | VadRoot 81b99200 Vads 53 Clone 0 Private 114. Modified 0. Locked 0. |
8 | ........................................................................................ |
1.转换线性地址并得到其信息
这里我们可以看到,页目录指针表物理地址为08c400a0,我们来将0041F69A这个虚拟地址进行分解:(00 000000010 000011111 011010011010)
得到了线性地址的四个部分,PDPT页目录指针索引号为0,9位页目录索引为2(000000010),9位页表索引为31(000011111),12位物理页偏移量为1690(011010011010)。
2.定位页目录指针表并获取页目录表物理页地址
我们在dd命令前加上一个!表示后面的参数是物理地址,即查看物理地址中的内容:
由于我们的页目录指针索引号为0,所以页目录的地址为0d21e001,但是我们只用到了其中的20位,所以页目录表物理地址的首地址为0d21e000
3.定位页表
由于页目录索引为2,而且开启PAE后的PDE大小为64位,即8个字节:
1 | kd> ! dd 0d21e000 + 0x2 * 8 |
由此得到页表项的地址为149ed067,同理只用了后20位,所以页表项的物理地址为149ed000
4.定位物理页面
页表索引为31,所以这里为:
1 | kd> ! dd 149ed000 + 0x1f * 8 |
所以物理页面的起始地址为1fb18000,最终的值再加上物理偏移1690就得到了物理地址:
我们看到,真正的物理地址为1fb1869a,这里存放值和我们通过OD中查看的虚拟地址0041F69A中的代码数据完全相同!我们就通过这一复杂的运算关系将物理地址算出来了!实在不容易啊,终于看清庐山真面目了,每次看到虚拟地址都有一种被欺骗的感觉。。。。。。
好的,下面我们就来尝试直接修改物理内存中的数据,如果修改后,不出意外,我们回到OD会发现其中的数据被更改了,使用e指令对指定地址进行修改,后面的bd表示按照多大进行修改,b为byte,d为dword
1 | kd> !ed 1fb1869a 90909090 |
2 | kd> !ed 1fb1869e 90909090 |
3 | kd> !ed 1fb186a2 90909090 |
4 | kd> !ed 1fb186a6 90909090 |
5 | kd> !ed 1fb186aa 90909090 |
我们恢复系统运行,发现果然od中的内容变了!看来此区的物理内存真的就是由monitor.exe这个进程映射过来的。
如何直接在调试器中算出物理地址呢?这里我们可以使用!pte命令,参数为要转换的虚拟地址或者叫线性地址,当然这个还有一个前提就是我们必须把当前调试器进程切换到要调试的进程上,否则转换会失败:
2 | PROCESS 805539a0 SessionId: none Cid: 0000 Peb: 00000000 ParentCid: 0000 |
3 | DirBase: 00b18000 ObjectTable: e1000c98 HandleCount: 285. |
5 | VadRoot 00000000 Vads 0 Clone 0 Private 0. Modified 0. Locked 0. |
8 | ElapsedTime 00:00:00.000 |
10 | KernelTime 05:45:22.234 |
11 | QuotaPoolUsage[PagedPool] 0 |
12 | QuotaPoolUsage[NonPagedPool] 0 |
13 | Working Set Sizes (now,min,max) (7, 50, 450) (28KB, 200KB, 1800KB) |
18 | MemoryPriority BACKGROUND |
22 | THREAD 80553740 Cid 0000.0000 Teb: 00000000 Win32Thread: 00000000 RUNNING on processor 0 |
使用!process命令查看当前调试器附加的进程,这里我们为0号进程,我们要得到虚拟地址必须切换要调试的用户态进程:
1 | kd> .process /i 820f1340 |
2 | You need to continue execution (press 'g' <enter>) for the context |
3 | to be switched. When the debugger breaks in again, you will be in |
4 | the new process context. |
6 | Break instruction exception - code 80000003 (first chance) |
7 | nt!RtlpBreakWithStatusInstruction: |
10 | PROCESS 820f1340 SessionId: 0 Cid: 0254 Peb: 7ffd6000 ParentCid: 05e8 |
11 | DirBase: 08c400a0 ObjectTable: e12ac518 HandleCount: 15. |
13 | VadRoot 81b99200 Vads 53 Clone 0 Private 114. Modified 0. Locked 0. |
16 | ElapsedTime 04:05:48.484 |
18 | KernelTime 00:00:00.562 |
19 | QuotaPoolUsage[PagedPool] 53156 |
20 | QuotaPoolUsage[NonPagedPool] 2120 |
21 | Working Set Sizes (now,min,max) (746, 50, 345) (2984KB, 200KB, 1380KB) |
22 | PeakWorkingSetSize 746 |
26 | MemoryPriority BACKGROUND |
31 | THREAD 820339c8 Cid 0254.07e0 Teb: 7ffdf000 Win32Thread: e2076710 WAIT: (Executive) KernelMode Non-Alertable |
32 | b291b7d4 SynchronizationEvent |
先使用.process /i + EPROCESS地址 开始切换进程,这里/i后面的参数就是之前说的EPROCESS地址,然后按g运行调试器,当再次断下来的时候我们发现当前进程环境就切换到目标进程中了。
3 | PDE at C0600010 PTE at C00020F8 |
4 | contains 00000000149ED067 contains 000000001FB18025 |
5 | pfn 149ed ---DA--UWEV pfn 1fb18 ----A--UREV |
这里我们看到直接可以通过!pte命令得到物理内存中的对应页和PFN(1fb18),然后我们将contain的值结尾变成0后再加上之前算出的偏移地址就得到了物理地址。
实验二
用OD加载两个不同的进程,将第一个程序的页目录基址改为第二个程序的页目录基址,再通过OD修改第一个进程中的数据,观察第二个进程中的数据是否会发生变化。
这里我们加载两个程序,一个是DbgView.exe还有一个是Monitor.exe,两个不同的进程,先查看这两个进程:
1 | PROCESS 81e16020 SessionId: 0 Cid: 0574 Peb: 7ffd6000 ParentCid: 05e8 |
2 | DirBase: 08c40280 ObjectTable: e10b3448 HandleCount: 21. |
5 | PROCESS 8209bda0 SessionId: 0 Cid: 0578 Peb: 7ffde000 ParentCid: 05e8 |
6 | DirBase: 08c402c0 ObjectTable: e218d218 HandleCount: 15. |
如果我希望两个进程空间打通的话,直接将Dbview.exe的DirBase改成monitor.exe就可以了,我们试试看:
1 | kd> dt nt!_kPROCESS 81e16020 |
2 | +0x000 Header : _DISPATCHER_HEADER |
3 | +0x010 ProfileListHead : _LIST_ENTRY [ 0x81e16030 - 0x81e16030 ] |
4 | +0x018 DirectoryTableBase : [2] 0x8c40280 |
5 | +0x020 LdtDescriptor : _KGDTENTRY |
6 | +0x028 Int21Descriptor : _KIDTENTRY |
7 | +0x030 IopmOffset : 0x20ac |
10 | +0x034 ActiveProcessors : 0 |
11 | +0x038 KernelTime : 0x42 |
13 | +0x040 ReadyListHead : _LIST_ENTRY [ 0x81e16060 - 0x81e16060 ] |
14 | +0x048 SwapListEntry : _SINGLE_LIST_ENTRY |
15 | +0x04c VdmTrapcHandler : (null) |
16 | +0x050 ThreadListHead : _LIST_ENTRY [ 0x820a2c30 - 0x820a2c30 ] |
17 | +0x058 ProcessLock : 0 |
20 | +0x062 BasePriority : 8 '' |
21 | +0x063 ThreadQuantum : 6 '' |
22 | +0x064 AutoAlignment : 0 '' |
24 | +0x066 ThreadSeed : 0 '' |
25 | +0x067 DisableBoost : 0 '' |
26 | +0x068 PowerState : 0 '' |
27 | +0x069 DisableQuantum : 0 '' |
28 | +0x06a IdealNode : 0 '' |
29 | +0x06b Flags : _KEXECUTE_OPTIONS |
30 | +0x06b ExecuteOptions : 0x32 '2' |
31 | kd> ed 81e16020 + 0x18 08c402c0 |
32 | kd> dt nt!_kPROCESS 81e16020 |
33 | +0x000 Header : _DISPATCHER_HEADER |
34 | +0x010 ProfileListHead : _LIST_ENTRY [ 0x81e16030 - 0x81e16030 ] |
35 | +0x018 DirectoryTableBase : [2] 0x8c402c0 |
36 | +0x020 LdtDescriptor : _KGDTENTRY |
37 | +0x028 Int21Descriptor : _KIDTENTRY |
38 | +0x030 IopmOffset : 0x20ac |
41 | +0x034 ActiveProcessors : 0 |
42 | +0x038 KernelTime : 0x42 |
44 | +0x040 ReadyListHead : _LIST_ENTRY [ 0x81e16060 - 0x81e16060 ] |
45 | +0x048 SwapListEntry : _SINGLE_LIST_ENTRY |
46 | +0x04c VdmTrapcHandler : (null) |
47 | +0x050 ThreadListHead : _LIST_ENTRY [ 0x820a2c30 - 0x820a2c30 ] |
48 | +0x058 ProcessLock : 0 |
51 | +0x062 BasePriority : 8 '' |
52 | +0x063 ThreadQuantum : 6 '' |
53 | +0x064 AutoAlignment : 0 '' |
55 | +0x066 ThreadSeed : 0 '' |
56 | +0x067 DisableBoost : 0 '' |
57 | +0x068 PowerState : 0 '' |
58 | +0x069 DisableQuantum : 0 '' |
59 | +0x06a IdealNode : 0 '' |
60 | +0x06b Flags : _KEXECUTE_OPTIONS |
61 | +0x06b ExecuteOptions : 0x32 '2' |
这样我们就成功的将两个进程的DirBase设为相同的值,然后回到od,点击一下DbgView所在调试器的窗口,瞬间看到值发生了变化:
两个进程指向的空间相同了哈,而且左边的又一点粘滞的状态。。这时候我们在左边修改一下指令,下面就是见证奇迹的时刻:
两个进程占用了同一个物理空间!关闭调试器后系统立马蓝屏,wow!
通过以上的几个实验,我们已经比较详细的了解了Windows在保护机制下是如何将进程间的空间进行隔离的,相信你对系统也有了 更加深刻的理解,当然有部分是我自己的理解,如果有不对之处还请各位大牛指正!