二进制入门学习笔记-17.保护模式-页机制

1.windows内存寻址

1.0 windows寻址过程简介

每一个32位进程都有一个虚拟的4GB内存空间,显然这个内存空间并不是真实存在的。以如下指令来讲述windows是如何进行内存寻址的:

MOV eax,dword ptr ds:[0x12345678]

首先0x12345678这个地址我们称为有效地址,cpu会自动将这个有效地址通过ds.base+0x12345678的方法转换成线性地址(大多数时候有效地址和线性地址相同是因为段寄存器的base是0)。然后CPU会将这个线性地址转换成物理地址。从线性地址转换成物理地址的过程主要有如下两种方式10-10-122-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位脏位,表示是否被写过,写过置1
  • PWT位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作为编号即可。
  • 倒数第三位的9intel同样也将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