深耕保护模式(五)

阅读量257720

|

发布时间 : 2021-12-15 10:30:04

 

2-9-9-12分页

在10-10-12分页方式下,物理地址最多可达4GB。但随着硬件发展,4GB的物理地址范围已经无法满足要求,Intel在1996年就已经意识到这个问题了,所以设计了新的分页方式。也就是2-9-9-12分页,又称为PAE(物理地址扩展)分页.

在了解2-9-9-12分页之前,应该先明确10-10-12分页到底是怎么来的,这样更有助于理解2-9-9-12分页。

为什么是10-10-12分页

intel实际上是先确定了页的大小,为4KB。那么物理页要求的索引应该是4096,也就是2的12次方,这个12次方实际上就是最后的12。

当初的物理内存比较小,所以4个字节的PTE就够了,又因为页的尺寸是4K,所以一个页能存储1024个 PTE ,也就是2的10次方 第二个10也就确定了。

第一个10同理,四字节的PDE,而页是4096,所以能储存1024个PDE,也就是2的10次方。

为什么是2-9-9-12分页

页的大小是4K,这点是不能动的。那么最后12位依旧是12位。

如果想增大能访问的物理页,需要增大PTE,而考虑到对齐效率高的问题,直接扩展到8个字节,也就是64位。

那么一个PTE有8个字节64位,一个PTT表还是4K,所以这里2的9次方 512即可索引到每一项。第三个9就是这个2的9次方。

同理是第二个9,一个PDE有8个字节64位,一个PDT表还是4K,也是2的9次方可以索引到每一项。

第一个2则是多出来的,做成了新的一级。

2-9-9-12分页结构(PAE,物理地址扩展)

2位2进制数最多就有四个索引,称为PDPTE(Page-Directory-Point-Table Entry):页目录指针表项。每项占8个字节。

在2-9-9-12分页模式下,通过线性地址找到物理地址

在boot.ini文件中修改execute为noexecute。

执行这样一段代码:

#include "stdafx.h"
int main(int argc, char* argv[])
{
    char x = 'a';

    printf("x的地址:%x\n",&x);

    getchar();
    return 0;
}

获取到x的线性地址。并进行拆分:

可以看到进程的cr3已经是0x20这样增长,10-10-12的cr3是0x1000增长。

只是多了一层偏移,与10-10-12分页寻址大体差不多。

kd> !dq 09e40300 + 0 
kd> !dq 1a781000 + 0
kd> !dq 1a8a4000 + 978
kd> !dq 1aaa9000 + F7C

PDPTE(Page-Directory-Point-Table Entry)

36-63位是保留位,保留位并不意味着填0就行了,而是cpu要用的,但是我们不能用,并且cpu用了还不会告诉我们怎么用的。

12-35位是PDT的基址,把低12位补0,那么0-35位,共36位为PDT基址。

9-11位是拿给操作系统用的,反正cpu不用。

0-11位都是属性。

PDE结构

在2-9-9-12分页模式下,PDE的结构有两种,一种是PS位为1的情况,一种是PS位为0的情况。

PS=1

当PS=1时是大页,35-21位是大页的物理地址,这样36位的物理地址的低21位为0,这就意味着页的大小为2MB,且都是2MB对齐。

2MB哪里来的呢?2-9-9-12,后面的9和12合并成了一个大页,所以是21位,也就是2的21次方,所以是2MB。

PAT位为Page Attribute Table,页属性表,可以看到下面PS为0的时候就有没这一项,原因就是这个位是针对页的,目录当然没有。

PS=0

当PS=0时,35-12位是页表基址,低12位补0,共36位。

PTE结构

PTE中35-12是物理页基址,24位,低12位补0

物理页基址+后12位的页内偏移指向具体数据

XD标志位

AMD中称为NX位,即No Excetion。

在上面说到保留位是cpu自己在用的,并且不会跟我们说怎么用的。在保留位的最高位,也就是63位,实际上是一个标明是否可执行的位。

细心的同学已经发现了,在PDE和PTE结构中,无论是10-10-12分页还是2-9-9-12分页都只有R/W位,也就是可读可写,但是并没有可执行的概念,之前在段的学习中,是有可读可写可执行的概念的,这里显然是不科学的。所以在2-9-9-12分页模式中的最高位标明该物理页的可执行属性。

Intel就做了硬件保护,做了一个不可执行位,XD=1时,表明该物理页是不可执行的,实际上是为了保护数据段,数据就是数据,是不能被当作代码执行的。

这里做一个呼应,在上面的2-9-9-12实验中可以看到,当找到PTE时,他的最高位已经变成了8。

这表明我们存储的’a‘就是a,而不能被执行。有一定的程度预防缓冲区溢出漏洞。

在PAE分页模式下,PDE与PTE的最高位为XD/NX位。

线性地址0xc0600000

在学习10-10-12分页的时候我们知道:PTE的基址是0xc0000000,PDE的基址是0xc0300000。在这两个线性地址能直接找到PTE和PDE,无需依靠Cr3。

但是在2-9-9-12分页模式下,没有指向Cr3的线性地址。但是仍然有一个特殊的线性地址:0xc0600000。

这个线性地址可以通过反编译ntkrnlpa.exe中的MmIsAddressValid函数找到。这个函数的作用就是判断线性地址可不可用。

他这里是sub,其实等同于add 0xC0600000

虽然没有线性地址指向cr3,那么有什么对应关系呢?

获取PDPTE指针。

kd> !dq 0ac40340
# ac40340 00000000`18733001 00000000`18674001
# ac40350 00000000`18675001 00000000`185b2001

获取0xc0600000线性地址的PDE

kd> !dq 0ac40340  + 3*8
kd> !dq 185b2000 + 3*8
kd> !dq 185b2000
#185b2000 00000000`18733063 00000000`18674063
#185b2010 00000000`18675063 00000000`185b2063

可以发现c0600000对应的PDE重新指回了PDPTT四个指针。

第三个PDPTE指向了一个PDT表,此表的前四项 指向了PDPTE的每一个元素。

 

TLB

假设我们有如下代码:

mov eax,dword ptr ds:[12345678]

这行代码会通过线性地址12345678和当前进程的cr3一起去找到对应的物理地址,而整个过程,实际上要访问PDE和PTE甚至是PDPTE,显然不只是四个字节。在2-9-9-12会读24个字节,如果跨页可能更多。

再看下面一段代码:

mov eax,dword ptr ds:[12345ffe]

在0x12345ffe位置下读取4字节的数据,那么实际上会读取到下一个页的地址,那么这里又会牵扯到PDE和PTE的访问和读取数据,这显然是非常损耗效率的。

为了提高效率,只能用一块缓存来做记录。

于是CPU内部做了一个表,来记录这些东西,这个表格是CPU内部的,和寄存器一样快,这个表格叫:TLB(Translation Lookaside Buffer)。

TLB结构

LA存储的是线性地址,PA是对应的物理地址,ATTR是属性,如果是10-10-12分页,那么是PDE和PTE的属性AND起来

如果是2-9-9-12分页,属性是PDPTE PDE PTE三个属性AND起来的

实际上有些属性是or起来的,比如XD位,G位。

LRU是用来统计这个线性地址的读写情况的,这是因为TLB这个表在CPU内部,那么他就不会很大,当线性地址存储满了的时候,他就会看LRU的统计情况,把读写的次数比较少的项删除,然后再把新的线性地址项添上。

不同的CPU 这个表的大小不一样。

只要Cr3变了,TLB立马刷新,一核一套TLB。这个也比较好理解,Cr3切换代表进程的切换,进程变了自然线性地址与物理地址的对应关系也失去了意义。

但有这样一个需求:在4GB虚拟空间内,低2G是用户自己的数据,高2G是系统的数据,高2G的内容对于每个进程来说,几乎都是相同的,那么当进程切换的时候,也意味着Cr3变化了,我们不希望TLB中所有的数据都被删除,重建的话耗费很多时间并影响效率,希望有一些高2G空间的线性地址所指引的物理地址项被保留,如何做到这一点呢?

这实际上与PDE和PTE属性中G位相关,G位表明该物理页为全局的,当Cr3切换时,TLB不会清空ATTR(属性)中G位为1的项,这也是为什么很多高线性地址属性的G位都为1的原因。

TLB种类

TLB在X86体系的CPU里的实际应用最早是从Intel的486CPU开始的,在X86体系的CPU里边,一般都设有如下4组TLB(也就是上面说的一套TLB):

第一组:缓存一般页表(4K字节页面)的指令页表缓存(Instruction-TLB)。

第二组:缓存一般页表(4K字节页面)的数据页表缓存(Data-TLB)。

第三组:缓存大尺寸页表(2M/4M字节页面)的指令页表缓存(Instruction-TLB)。

第四组:缓存大尺寸页表(2M/4M字节页面)的数据页表缓存(Data-TLB)。

TLB有多大这个没说,不同cpuTLB的大小不同。

TLB是确实存在的

TLB看不见摸不着,我们怎么知道他是否真实存在呢?

看如下一段代码:

#include "stdafx.h"
#include <Windows.h>

unsigned int g_value=0;

__declspec(naked) void test()
{

    __asm
    {
        push 0x30;
        pop fs;
        pushad;
        pushfd;

        //0地址的PDE一般是存在的,这里就直接挂PTE
        mov eax,0x600000;
        mov ebx,0xc0000000;
        //在PAE模式下获取pte的算法,可以逆向MmIsAddressValid找到。
        shr eax,0x9;
        and eax,0x7ffff8;

        //find pte
        mov edx,eax;
        add edx,ebx;
        mov edx,dword ptr ds:[edx];

        mov dword ptr ds:[ebx],edx;


        mov dword ptr ds:[0],0x12345678;

        mov eax,0x700000;
        mov ebx,0xc0000000;
        shr eax,0x9;
        and eax,0x7ffff8;

        //find pte
        mov edx,eax;
        add edx,ebx;
        mov edx,dword ptr ds:[edx];
        mov dword ptr ds:[ebx],edx;

        mov eax,dword ptr ds:[0];
        mov g_value,eax;


        popfd;
        popad;

        retf;
    }
}


int main(int agrc,char * agrv[])
{
    char buf[]={0,0,0,0,0x48,0};

    void * p1 = VirtualAlloc((void*)0x600000,0x1000,MEM_COMMIT | MEM_RESERVE ,PAGE_EXECUTE_READWRITE);
    void * p2 = VirtualAlloc((void*)0x700000,0x1000,MEM_COMMIT | MEM_RESERVE ,PAGE_EXECUTE_READWRITE);

    if(p1 == NULL)
    {
        if(p2 != NULL) VirtualFree(p2,0x1000,MEM_COMMIT | MEM_RESERVE);
        printf("virtual failed p1\n");
        return 0;
    }

    if(p2 == NULL)
    {
        VirtualFree(p1,0x1000,MEM_COMMIT | MEM_RESERVE);
        printf("virtual failed p2\n");
        return 0;
    }
    //这里必须要赋值,不然会0xC0000005,VirtualAlloc分配好了空间,但是当你使用的时候才挂物理页。
    *((unsigned int *)p1)=0x100;
    *((unsigned int *)p2)=0x200;
    printf("%X\n",test);
    //eq 8003f048 0040ec00`00081005
    __asm
    {
        call fword ptr buf;
        push 0x3b;
        pop fs;
    };

    printf("%X\n",g_value);

    VirtualFree(p1,0x1000,MEM_COMMIT | MEM_RESERVE);
    VirtualFree(p2,0x1000,MEM_COMMIT | MEM_RESERVE);

    return 0;
}

我们将0x600000线性地址对应的物理页挂在0地址上,并且将值改为0x12345678(mov dword ptr ds:[0],0x12345678),然后再将0地址挂上0x700000的物理页,这时候由于0x700000位置上的值是200(((unsigned int )p2)=0x200),那么如果没有TLB,下次cpu寻址的时候0位置上的值应该为200。但运行结果却为:

说明确实有缓存帮我们存储了地址,当第一次寻址的时候已经将linear和对应的物理地址储存到TLB中。

那么如何取到最新的值呢,也就是我们想取到0地址挂上0x700000对应物理页后的值,这里最好的办法就是刷新Cr3,前面我们也提到了Cr3的切换会将TLB清空(G位为1除外)。

mov eax,cr3;
mov cr3,eax;

TLB刷新,这样可以成功读取到最新的数据,而不是老的缓存。

全局页体验

那如果我们此时将PTE属性设置为全局,更新Cr3还能刷新我们这行缓存吗?

or edx,0x100;

即便刷新了缓存,但是由于已经变成全局物理页,所以TLB不会删除地址0这一项。

INVLPG指令

这个指令可以直接删除TLB的某一项缓存,所以我们现在可以通过该指令强行删除全局页缓存。

invlpg dword ptr ds:[0];

 

中断与异常

中断

中断通常是由CPU外部的输入输出设备(硬件)所触发的,供外部设备通知,CPU“有事情需要处理”,因此又叫中断请求(Interrupt Request)。

中断请求的目的是希望CPU暂时停止执行当前正在执行的程序,转去执行中断请求所对应的中断处理例程(中断处理程序在哪有IDT表决定)

80×86有两条中断请求线:

  • 非屏蔽中断线,称为NMI(NonMaskable Interrupt)
  • 可屏蔽中断线,称为INTR(Interrupt Require)

非可屏蔽中断

当非可屏蔽中断产生时,CPU在执行完当前指令后会里面进入中断处理程序。

非可屏蔽中断不受EFLAG寄存器中IF位的影响,一旦发生,CPU必须处理。

非可屏蔽中断处理程序位于IDT表中的2号位置。

找一下2号中断执行代码,这里忘了就回去看TSS,任务门。

kd> uf 8053f3fc 
nt!KiTrap02:
8053f3fc fa              cli
8053f3fd ff3540f0dfff    push    dword ptr ds:[0FFDFF040h]
8053f403 a13cf0dfff      mov     eax,dword ptr ds:[FFDFF03Ch]
8053f408 8a685f          mov     ch,byte ptr [eax+5Fh]
8053f40b 8a485c          mov     cl,byte ptr [eax+5Ch]
8053f40e c1e110          shl     ecx,10h
8053f411 668b485a        mov     cx,word ptr [eax+5Ah]
8053f415 890d40f0dfff    mov     dword ptr ds:[0FFDFF040h],ecx
8053f41b 9c              pushfd
8053f41c 812424ffbfffff  and     dword ptr [esp],0FFFFBFFFh
8053f423 9d              popfd
8053f424 8b0d3cf0dfff    mov     ecx,dword ptr ds:[0FFDFF03Ch]
8053f42a 8d4158          lea     eax,[ecx+58h]
8053f42d c6400589        mov     byte ptr [eax+5],89h
8053f431 8b0424          mov     eax,dword ptr [esp]
8053f434 6a00            push    0
8053f436 6a00            push    0
8053f438 6a00            push    0
8053f43a 6a00            push    0
8053f43c ff7050          push    dword ptr [eax+50h]
8053f43f ff7038          push    dword ptr [eax+38h]
8053f442 ff7024          push    dword ptr [eax+24h]
8053f445 ff704c          push    dword ptr [eax+4Ch]
8053f448 ff7020          push    dword ptr [eax+20h]
8053f44b 6a00            push    0
8053f44d ff703c          push    dword ptr [eax+3Ch]
8053f450 ff7034          push    dword ptr [eax+34h]
8053f453 ff7040          push    dword ptr [eax+40h]
8053f456 ff7044          push    dword ptr [eax+44h]
8053f459 ff7058          push    dword ptr [eax+58h]
8053f45c ff3500f0dfff    push    dword ptr ds:[0FFDFF000h]
8053f462 6aff            push    0FFFFFFFFh
8053f464 ff7028          push    dword ptr [eax+28h]
8053f467 ff702c          push    dword ptr [eax+2Ch]
8053f46a ff7030          push    dword ptr [eax+30h]
8053f46d ff7054          push    dword ptr [eax+54h]
8053f470 ff7048          push    dword ptr [eax+48h]
8053f473 ff705c          push    dword ptr [eax+5Ch]
8053f476 6a00            push    0
8053f478 6a00            push    0
8053f47a 6a00            push    0
8053f47c 6a00            push    0
8053f47e 6a00            push    0
8053f480 6a00            push    0
8053f482 6a00            push    0
8053f484 6a00            push    0
8053f486 6a00            push    0
8053f488 6a00            push    0
8053f48a ff7020          push    dword ptr [eax+20h]
8053f48d ff703c          push    dword ptr [eax+3Ch]
8053f490 8bec            mov     ebp,esp
8053f492 833ddcaf548000  cmp     dword ptr [nt!KiAbiosPresent+0x8 (8054afdc)],0
8053f499 7502            jne     nt!KiTrap02+0xa1 (8053f49d)

nt!KiTrap02+0x9f:
8053f49b eb24            jmp     nt!KiTrap02+0xc5 (8053f4c1)

nt!KiTrap02+0xa1:
8053f49d 833ddcaf548008  cmp     dword ptr [nt!KiAbiosPresent+0x8 (8054afdc)],8
8053f4a4 721b            jb      nt!KiTrap02+0xc5 (8053f4c1)

nt!KiTrap02+0xaa:
8053f4a6 7517            jne     nt!KiTrap02+0xc3 (8053f4bf)

nt!KiTrap02+0xac:
8053f4a8 803dc0d4548000  cmp     byte ptr [nt!KdDebuggerNotPresent (8054d4c0)],0
8053f4af 750e            jne     nt!KiTrap02+0xc3 (8053f4bf)

nt!KiTrap02+0xb5:
8053f4b1 803dc1d4548000  cmp     byte ptr [nt!KdDebuggerEnabled (8054d4c1)],0
8053f4b8 7405            je      nt!KiTrap02+0xc3 (8053f4bf)

nt!KiTrap02+0xbe:
8053f4ba e82999fbff      call    nt!KeEnterKernelDebugger (804f8de8)

nt!KiTrap02+0xc3:
8053f4bf ebfe            jmp     nt!KiTrap02+0xc3 (8053f4bf)

nt!KiTrap02+0xc5:
8053f4c1 ff05dcaf5480    inc     dword ptr [nt!KiAbiosPresent+0x8 (8054afdc)]
8053f4c7 6a00            push    0
8053f4c9 ff1580864d80    call    dword ptr [nt!_imp__HalHandleNMI (804d8680)]
8053f4cf ff0ddcaf5480    dec     dword ptr [nt!KiAbiosPresent+0x8 (8054afdc)]
8053f4d5 7533            jne     nt!KiTrap02+0x10e (8053f50a)

nt!KiTrap02+0xdb:
8053f4d7 a140f0dfff      mov     eax,dword ptr ds:[FFDFF040h]
8053f4dc 66833858        cmp     word ptr [eax],58h
8053f4e0 7428            je      nt!KiTrap02+0x10e (8053f50a)

nt!KiTrap02+0xe6:
8053f4e2 81c48c000000    add     esp,8Ch
8053f4e8 8f0540f0dfff    pop     dword ptr ds:[0FFDFF040h]
8053f4ee 8b0d3cf0dfff    mov     ecx,dword ptr ds:[0FFDFF03Ch]
8053f4f4 8d4128          lea     eax,[ecx+28h]
8053f4f7 c640058b        mov     byte ptr [eax+5],8Bh
8053f4fb 9c              pushfd
8053f4fc 810c2400400000  or      dword ptr [esp],4000h
8053f503 9d              popfd
8053f504 cf              iretd

nt!KiTrap02+0x10e:
8053f50a b802000000      mov     eax,2
8053f50f e9dc280000      jmp     nt!KiSystemFatalException (80541df0)

nt!KiSystemFatalException:
80541df0 55              push    ebp
80541df1 6a00            push    0
80541df3 6a00            push    0
80541df5 6a00            push    0
80541df7 50              push    eax
80541df8 6a7f            push    7Fh
80541dfa e81774fbff      call    nt!KeBugCheck2 (804f9216)
80541dff c3              ret

可屏蔽中断

在硬件级,可屏蔽中断是由一块专门的芯片来管理的,通常称为中断控制器。它负责分配中断资源和管理各个中断源发出的中断请求.为了便于标识各个中断请求。中断管理器通常用IRQ(Interrupt Request)后面加上数字来表示不同的中断。比如:在Windows中 时钟中断的IRQ编号为0 也就是:IRQ0(大多数操作系统时钟中断在10-100MS之间,Windows系列为10-20MS)。

1、如果自己的程序执行时不希望CPU去处理这些中断,可以用CLI指令清空EFLAG寄存器中的IF位,用STI指令设置EFLAG寄存器中的IF位。

2、硬件中断与IDT表中的对应关系并非固定不变的,参见:APIC(高级可编程中断控制器),作用就是中断号和IDT表的映射关系。

异常

异常通常是CPU在执行指令时检测到的某些错误,比如除0、访问无效页面等。

中断与异常的区别:

1、中断来自于外部设备,是中断源(比如键盘)发起的,CPU是被动的。

2、异常来自于CPU本身,是CPU主动产生的。

3、INT N虽然被称为“软件中断”,但其本质是异常。EFLAG的IF位对INT N无效。

异常处理

无论是由硬件设备触发的中断请求还是由CPU产生的异常,处理程序都在IDT表。

常见的异常处理程序:

比如缺页异常,CPU会执行IDT表中的0xE号中断处理程序,由操作系统来接管。

缺页异常的产生:

1、当PDE/PTE的P=0时。

2、当PDE/PTE的属性为只读但程序试图写入的时。

3、当物理内存空间不足时,线性地址对应的物理页将被存储到文件中。

 

控制寄存器

控制寄存器用于控制和确定CPU的操作模式。

Cr0 Cr1 Cr2 Cr3 Cr4 。

Cr1 保留。

Cr3 页目录表基址。

Cr0 保留。

Cr0

1、PE位:CR0的位0是启用保护(Protection Enable)标志。

PE=1为保护模式 PE=0则为实地址模式,这个标志仅开启段级保护,而并没有启用分页机制

若要启用分页机制,那么PE和PG标志都要置位。

2、PG位:当设置该位时即开启了分页机制。在开启这个标志之前必须已经或者同时开启PE标志。

PG=0且PE=0 处理器工作在实地址模式下。

PG=0且PE=1 处理器工作在没有开启分页机制的保护模式下。

PG=1且PE=0 在PE没有开启的情况下 无法开启PG。

PG=1且PE=1 处理器工作在开启了分页机制的保护模式下 。

3、WP位:对于Intel 80486或以上的CPU,CR0的位16是写保护(Write Proctect)标志

当设置该位为1时,处理器会禁止超级用户程序(例如特权级0的程序)向用户级只读页面执行写操作。保护三环用户数据。

当CPL<3的时候:

如果 WP=0 可以读写任意用户级物理页,只要线性地址有效。

如果 WP=1 可以读取任意用户级物理页,但对于只读的物理页,则不能写。

Cr2

当CPU访问某个无效页面时,会产生缺页异常,此时,CPU会将引起异常的线性地址存放在CR2中。

Cr4

PAE/PSE说明:

PAE=1 是2-9-9-12分页 PAE=0 是10-10-12分页,这个值实际上就是从boot.ini文件中读出来的。

PSE:相当于时PS位的总开关,只有当PSE为1时的,PS位才起作用,否则无论PS位为何值,都没有大页的存在。

 

后记

保护模式的细节还有很多,具体可参考intel白皮书第三卷:

https://www.intel.cn/content/www/cn/zh/architecture-and-technology/64-ia-32-architectures-software-developer-system-programming-manual-325384.html

本文由SD原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/260893

安全客 - 有思想的安全新媒体

分享到:微信
+15赞
收藏
SD
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66