1.windows内存寻址
1.0 windows寻址过程简介
每一个32位进程都有一个虚拟的4GB内存空间,显然这个内存空间并不是真实存在的。以如下指令来讲述windows是如何进行内存寻址的:
MOV eax,dword ptr ds:[0x12345678]
首先0x12345678
这个地址我们称为有效地址
,cpu会自动将这个有效地址
通过ds.base+0x12345678
的方法转换成线性地址
(大多数时候有效地址和线性地址相同是因为段寄存器的base是0)。然后CPU会将这个线性地址
转换成物理地址
。从线性地址转换成物理地址的过程主要有如下两种方式10-10-12
和2-9-9-12
。
1.1 10-10-12
分页方式
1.1.1 10-10-12寻址方式简介
windowXP系统默认使用的分页方式是2-9-9-12
。如果需要把系统的分页方式改成10-10-12
模式,只需要修改C:\boot.ini
文件,将noexecute
中的no
删掉即可。
每一个进程都维护了一个CR3的值(CR3是一个寄存器,每一个CPU的核只有一套寄存器),这个值是一个页目录表的物理地址。然后再用32位线性地址拆分出的10-10-12
进行业内寻址,整体结构如下图所示(此图是为了便于理解,实际结构见后文):
1.1.2 10-10-12寻址实验
1.打开一个记事本在当中输入一段内容,然后用CE查找到这段内容的线性地址,此处我的文字内容线性地址位000AAA50
。
2.将地址转换成10-10-12
模式
十六进制:000AAA50 二进制: 0000 0000 0000 1010 1010 1010 0101 0000 分割为10-10-12 0000 0000 00 //10位,对应16进制为:0x0 00 0000 1010 //10位,对应16进制为:0xAA 1010 1010 0101 0000 //12位,对应16进制为:0xA50
3.查找记事本进程的CR3,此处CR3的值为00830000
!process 0 0
4.使用!dd
命令按级别查询物理内存
kd> !dd 00830000+0 //查看PDE,0=4*0 # 830000 08afe067 03bfe067 0894e067 00000000 # 830010 00870067 00000000 00000000 00000000 # 830020 00000000 00000000 00000000 00000000 # 830030 00000000 00000000 00000000 00000000 # 830040 00000000 00000000 00000000 00000000 # 830050 00000000 00000000 00000000 00000000 # 830060 00000000 00000000 00000000 00000000 # 830070 00000000 00000000 00000000 00000000 kd> !dd 08afe000+2A8 //查看PTE,2A8=4*AA # 8afe2a8 08a94067 00837047 03c8a047 0084c047 # 8afe2b8 03d4d047 0de0e047 06ccf047 08390047 # 8afe2c8 07ad1047 03d53047 0b692047 00000000 # 8afe2d8 00000000 00000000 00000000 00000000 # 8afe2e8 00000000 00000000 00000000 00000000 # 8afe2f8 00000000 00000000 00000000 00000000 # 8afe308 00000000 00000000 00000000 00000000 # 8afe318 00000000 00000000 00000000 00000000 kd> !db 08a94000+A50 //在物理页中查看数据,此处不需要乘4 # 8a94a50 6a 00 75 00 73 00 34 00-66 00 75 00 6e 00 61 00 j.u.s.4.f.u.n.a. # 8a94a60 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ # 8a94a70 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ # 8a94a80 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ # 8a94a90 00 00 00 00 0c 00 89 00-03 00 0a 00 c0 23 16 00 .............#.. # 8a94aa0 00 00 0a 00 78 01 0a 00-00 00 00 00 14 00 89 00 ....x........... # 8a94ab0 05 00 03 00 c5 01 08 00-00 00 00 00 60 5e 0a 00 ............`^.. # 8a94ac0 68 b1 99 7c 98 a6 0a 00-00 00 00 00 00 00 00 00 h..|............
注意:上面页目录项中的067
其实是描述属性的。因此寻址的时候把这三位改成0
1.1.3 10-10-12分页模式下的PDE与PTE
内存寻址的过程中,第一级的页表叫做页目录表
,也就是所谓的PDT(Page Directory Table)
。这张表的大小为4096Byte,表中的每一个条目称为页目录项(PDE Page Directory Entry)
,每一个页目录项大小为4Byte。第二级表称为页表
,也就是所谓的PTT
,大小和页目录表都一样的。页表中的条目称为,页表项(PTE)
。
1.1.3.1 PDE与PTE的属性
PDE和PTE中的低地址的12位表示属性,PDE及PTE的属性如下图所示。 注意:物理页的属性=PDE属性&PTE属性
各个位置意义如下:
P位
:P为代表是否有效,PDE和PTE的P位都为1,才是有效的物理页。R/W位
:R/W==0表示只读,R/W==1表示可读可写U/S位
:当U/S=0表示只能特权用户访问,当U/S==1表示普通用户也可以访问PS位
:只对PDE有效,PS表示PageSize。当PS==1时,PDE直接跨过PTE指向物理页。那么线性地址的低22位就直接是物理页的页内偏移,大小为4M,也就是所说的大页
。A位
:是否被访问过,访问(读和和写都算)过置1。D位
:脏位,表示是否被写过,写过置1PWT位
:Page Write Through,PWT为1的时候,写cache的时候也要将内容写入内存中PCD位
:Page Cache Disable,PCD为1的时候,禁止某个也写入缓存,直接写入内存
1.1.4 10-10-12分页模式下的PDT基址
当我们当前想要在程序中访问PDT或者PTT时,我们的程序显然没办法直接用CR3中保存的物理地址去访问这两张表,我们的程序只能使用线性地址。而事实上,也真的有这么一个神奇的线性地址指向了PDT表。事实上,我们前面所画的寻址结构图是有一点点问题的!因为本质来讲PDT表是PPT表当中的一个。真正的寻址结构图如下:
从图中我们可以看到,CR3寄存器中其实直接存储的就是PTT768(0x300)
的地址。而PTT768
当中的PTE768(0x300)
指向了PTT768本身。因此,我们很容易得到如下结论:
- PDT的基础地址表示为10-10-12分页模式就是:
300-300-0
也就是0xc0300000
。 - PTT表是从线性地址的0xC0000000到0xC03FFFFF,一共1024个,占用4M大小的空间
- 使用线性地址访问任意一个PDE:0xC0300000 + PDI(页目录索引)*4
- 使用线性地址访问任意一个PTE:0xC0000000 + PDI*0x1000 + PTI(页表索引)*4
1.2 2-9-9-12
分页模式
2-9-9-12分页
又称为PAE(物理地址拓展)分页
。前面所学的10-10-12分页
有一个非常严重的缺点就是可以访问的内存最大为4G,因为PTE里面物理地址的长度就只有32位,所以只能支持4G以下的内存。因此1996年,intel在1996年设计了新的分页方式,也就是2-9-9-12分页
。2-9-9-12
分页中,物理地址的长度改为了36位,这样的话,可以支持的内存就变成了64G了。对于2-9-9-12
中的每一个数字有如下解释:
12
:设计之初页的大小已经确定了是4kb,为了能够寻址到页中每一个字节的内存,就需要12位进行页内寻址,因此12不动。倒数第二位的9
:之前的PTE每一个都是4字节,也就是32位。其中高字节的20位用来寻址物理页,也就是说能够表示的物理页数量只有0xfffff 个。为了扩展寻址空间,因此工程师将PTE修改为了8字节。这样一页当中就只能存储512个PTE。因此只需要9位的PTI作为编号即可。倒数第三位的9
:intel同样也将PDE从4个字节改成了8个字节。因此PDI也只需要9位即可。2
:新扩展了一个PDPI表(页目录指针表
)中的索引
其寻址过程如下图所示:
1.2.1 页目录指针表项(PDPTE)
PDPTE的结构如下图所示,其中灰色部分为保留位
:
其余个位解释如下:
0位
:有效位,为1才有效Avail位
:这3位是留给软件(操作系统)使用的12到35位
:存储的是PDT表的地址,从12到35位共24位。然后在低12位补0,共36位。
1.2.2 PAE模式下的页目录项(PDE)结构
PAE分页模式下PDE结构有两种如下图所示:
当PDE的第七位PS位
为1时,当前页为大页
。35-21位是大页的物理地址,这样36位的物理地址的低21位为0,这就意味着页的大小为2MB,且都是2MB对齐。其余的跟前面10-10-12分页模式下的基本一样。
当PDE的第七位PS位
为0时,35-12位是页表基址,低12位补0,共36位。
1.2.3 PAE模式下的页表项(PTE)结构
PAE分页模式下的PTE结构如下图所示: PTE中12-35位为物理页基址,共24位,低的12位补0。
1.2.4 根据线性地址读取2-9-9-12物理地址实验
1、获取线性地址
2、2-9-9-12拆分000AAA28地址为:
000AAA28 2:00=0 9:00 0000 000=0 9:0 1010 1010=AA 12:1010 0010 1000=A28
3、使用windby获取物理内存
kd> !process 0 0 //获取CR3的值,也就是PDPTT首地址 **** NT ACTIVE PROCESS DUMP **** PROCESS 819bd830 SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000 DirBase: 0032b000 ObjectTable: e1000cf8 HandleCount: 261. Image: System PROCESS 818af020 SessionId: none Cid: 0174 Peb: 7ffdf000 ParentCid: 0004 DirBase: 09580020 ObjectTable: e14bcb98 HandleCount: 19. Image: smss.exe ......略 PROCESS 818d6530 SessionId: 0 Cid: 010c Peb: 7ffdd000 ParentCid: 06b4 DirBase: 09580340 ObjectTable: e25b91b0 HandleCount: 46. Image: notepad.exe kd> !dq 09580340+0*4 //读取PDPTE # 9580340 00000000`0a18c001 00000000`0a4cd001 # 9580350 00000000`0244e001 00000000`03a0b001 # 9580360 00000000`0d8e5001 00000000`0d6a6001 # 9580370 00000000`0d967001 00000000`0d324001 # 9580380 00000000`0da32001 00000000`0d673001 # 9580390 00000000`0d834001 00000000`0d371001 # 95803a0 00000000`0dc0d001 00000000`0d54e001 # 95803b0 00000000`0dbcf001 00000000`0da4c001 kd> !dq 00a18c000+0*8 //读取PDE # a18c000 00000000`0a267067 00000000`0a2cd067 # a18c010 00000000`023a1067 00000000`00000000 # a18c020 00000000`022b4067 00000000`0239e067 # a18c030 00000000`00000000 00000000`00000000 # a18c040 00000000`023da067 00000000`00000000 # a18c050 00000000`00000000 00000000`00000000 # a18c060 00000000`00000000 00000000`00000000 # a18c070 00000000`00000000 00000000`00000000 kd> !dq 00a267000+AA*8 //读取PTE # a267550 80000000`0237a067 80000000`023dd047 # a267560 80000000`021f1047 80000000`021b3067 # a267570 80000000`02234047 80000000`02335047 # a267580 80000000`021f6047 80000000`02237047 # a267590 80000000`02238047 80000000`02279047 # a2675a0 80000000`022ba047 80000000`0ffcd047 # a2675b0 00000000`00000000 00000000`00000000 # a2675c0 00000000`00000000 00000000`00000000 kd> !db 00237a000+A28 //读取对应页的偏移 # 237aa28 6a 00 75 00 73 00 34 00-66 00 75 00 6e 00 61 00 j.u.s.4.f.u.n.a. # 237aa38 63 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 c............... # 237aa48 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ # 237aa58 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ # 237aa68 00 00 00 00 0c 00 89 00-03 00 0a 00 c8 23 16 00 .............#.. # 237aa78 00 00 0a 00 78 01 0a 00-00 00 00 00 14 00 89 00 ....x........... # 237aa88 05 00 03 00 d7 01 08 00-00 00 00 00 38 5e 0a 00 ............8^.. # 237aa98 68 b1 99 7c 70 a6 0a 00-00 00 00 00 00 00 00 00 h..|p...........
1.2.5 PTE当中的特殊位——XD位
PTE的最高位就是XD位,AMD当中XD位称为NX位(No Execute的缩写),当这一位为1时,这个PTE所指向的物理页中的数据都不能被当成机器码被 CPU 执行。
1.2.6 PAE中PDE和PTE线性地址计算
PAE分页模式下线性地址可以根据2-9-9-12
分为四段,即PDPTI-PDI-PTI-OFFSET
。PDE和PTE的寻址可以使用如下公式进行计算。
pPDE = (int*)(0xc0600000 + (PDPTI<<12) + (PDI<<3)); pPTE = (int*)(0xc0000000 + (PDPTI<<21) + (PDI<<12) + (PTI<<3));
1.2.7 TLB
1.2.7.1 TLB概述
当我们每读取一个字节的数据时,以10-10-12分页模式为例,CPU需要先读4个字节的PDE,然后再读四个字节的PTE,然后在读我们所指定数据所在的物理页。这样无疑效率是非常低的,为了缓解这个问题,CPU在其内部实现了一张表来记录地址和物理页的关系,也就是所说的TLB(Translation Lookaside Buffer)
,中文名为快表
。TLB
的结构如下图所示:
说明:
ATTR(页属性)
:这个值等于PDPTE,PDE,PTE三个属性与运算出来(2-9-9-12分页模式下),或者PDE和PTE与运算的的结果(10-10-12分页模式下)。LRU(统计)
:记录了每一个地址的读写次数。如果TLB表满了,有新的要加进来,那么CPU就会看这个表里哪一个条目的LRU值最小。- 只要CR3变了,那么TLB会立刻刷新。每一个核只有一套TLB。
- 不同的CPU这个表的大小不同
PTE和PDE中的G位
:操作系统当中,每一个进程拥有独立的4GB虚拟内存空间,在低2G中,不同进程对应的物理页是不同的,但是高2G中,不同进程对应的物理页几乎是相同的。而伴随着进程的切换,CR3寄存器也会进行切换,CR3的切换会导致 TLB 条目失效,对于几乎完全共享的高 2GB 地址空间来说,这种失效将造成浪费。因此PTE(当为大页时G位生效)和PDE中有一个G位
。当G位为1时,TLB刷新时将不会刷新掉G位为1的条目。- 刷新TLB中条目的指令:
invlpg [线性地址]
TLB种类:
- 缓存一般页表(4K字节页面)的指令页表缓存(Instruction-TLB);
- 缓存一般页表(4K字节页面)的数据页表缓存(Data-TLB);
- 缓存大尺寸页表(2M/4M字节页面)的指令页表缓存(Instruction-TLB);
- 缓存大尺寸页表(2M/4M字节页面)的数据页表缓存(Data-TLB)
传说中的shadowwalker就是利用的TLB,可以看出来上述表中,有的是用来存读取的数据、有的是用来存执行的数据,对于全代码校验,可以将读取部分的保留,然后把执行部分的表给他替换了。
1.2.7.2 TLB实验
设计一个中断门进行提权,然后验证TLB是否存在。
1、根据函数地址构造中断门
函数地址为0040D740,故构造如下提权中断门
kd> eq 8003f500 0040ee00`0008D740 kd> dq 8003f500 L 4 8003f500 0040ee00`0008d740 00000000`0008d740 8003f510 00000000`00080000 00000000`00080000
2、测试时分别测试进行进程切换,G位作用测试,强制刷新即可
// testTLB.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include "stdio.h" int zero=0; void _declspec(naked)proc(){ __asm{ pushad pushfd mov dword ptr ds:[0xc0000000],0x01234867//给0地址挂物理页,G位为0 //mov dword ptr ds:[0xc0000000],0x01234967//给0地址挂物理页,G位为1 mov dword ptr ds:[0],0x12345678//给0地址赋值 mov dword ptr ds:[0xc0000000],0x02345867//随意修改0地址物理页 //invlpg dword ptr ds:[0]//强制刷新0地址 //mov eax,cr3//切换进程 //mov cr3,eax//切换进程 mov eax,dword ptr ds:[0] mov zero,eax popfd popad iretd } } int main(int argc, char* argv[]) { __asm{ INT 0x20 } printf("%x",zero); int x=0; }
2.中断与异常
2.1中断
中断
通常都是由CPU外部的输入输出设备(硬件)所触发的。中断可以帮助外部设备通知CPU有事情需要CPU处理,所以也叫中断请求(Interrupt Request)
。发出中断请求的目的是希望CPU暂时停止当前正在执行的程序。转去执行中断请求所对应的(查IDT表)中断处理例程。
2.1.1 可屏蔽中断
可屏蔽中断称为NMI(NonMaskable Interrupt)
。可屏蔽中断受EFLAG寄存器
中的IF位
影响。在硬件级,可屏蔽中断由一块名为中断控制器
的芯片管理。为了便于标识不同的中断,中断控制器通常使用IRQ(Interrupt Request)后面加个数字来表示不同的中断请求。在IDT表中位置如下图:
说明:
- 可屏蔽中断的控制
-
- 屏蔽可屏蔽中断可以使用
CLI
指令清空EFLAG寄存器的IF位
- 屏蔽可屏蔽中断可以使用
-
- 设置不屏蔽可以使用
STI
指令设置EFLAG寄存器的IF位
- 设置不屏蔽可以使用
- 硬件中断与IDT表的对应关系并非是固定的
2.1.2 不可屏蔽中断
不可屏蔽中断称为INTR(Interrupt Require)
。当不可屏蔽中断发出时,CPU必须要处理,CPU会执行IDT表中标号为0x2的门。
2.2 异常
异常通常是CPU执行过程中检测到了某些错误,比如除0,访问无效页面。INT N
又称为软件中断,但其实他的本质是异常,因此EFLAG当中的IF位对INT N
没有影响。常见异常如下表所示:
3.控制寄存器
控制寄存器用于控制和确定CPU的操作模式,一共有五个控制寄存器:
CR0
CR1
:保留CR2
CR3
:页目录表基址CR4
3.1 CR0寄存器
CR0寄存器结构如下图所示(手册P2267): 对于CR0的说明如下:
PE位:
PE位以为Protect Enable,当PE位为1时CPU是保护模式,当PE位为0时,CPU为实模式。PG位
:当设置该位时即开启了分页机制,在开启这个标志之前必须先开启PE标志。-
- PG=0且PE=0处理器工作在实模式下
-
- PG=0且PE=1处理器工作在没有开启分页机制的保护模式下
-
- PG=1且PE=0在PE没有开启的情况下无法开启PG
-
- PG=1且PE=1处理器工作在开启分页机制的保护模式下
WP位
:作用进制R0的程序向R3的只读页中写入数据
3.2 CR2寄存器
CR2寄存器如下图所示: 当CPU访问某个无效的页时,会产生缺页异常,此时CPU会把异常的线性地址存到CR2寄存器里。
3.3 CR4寄存器
CR4寄存器如下图所示:
对于CR4重点说明PAE位
和PSE位
如下:
PAE位
:- PAE==1,CPU采用2-9-9-12分页
-
PAE==0,CPU采用10-10-12分页
-
PSE位
相当于PS位的开关,当PSE==1时PS位才起作用:
PSE | 分页模式 | PS | 页尺寸 |
---|---|---|---|
1 | 10-10-12 | 1 | 4M |
1 | 10-10-12 | 0 | 4K |
1 | 2-9-9-12 | 1 | 2M |
1 | 2-9-9-12 | 0 | 2K |
0 | 10-10-12 | 1 | 4K |
0 | 10-10-12 | 0 | 4K |
0 | 2-9-9-12 | 1 | 4K |
0 | 2-9-9-12 | 0 | 4K |