深耕保护模式(四)

阅读量330380

|

发布时间 : 2021-11-30 16:30:17

 

X86模式下存在10-10-12分页和2-9-9-12分页。

 

10-10-12分页

在x86系统下,总说一个进程有4GB空间,那么按照这个说法来说,在windows上起一个进程就要占用4GB空间,两个进程就要占用8GB空间,但是实际上是我们电脑的物理内存往往只有8GB,16GB多一点的可能有32GB,我们却启动了几十个进程,这显然是矛盾的。

实际上,我们所说的进程有4GB内存空间,这个概念是虚拟的。cpu会经过一定算法从虚拟内存地址找到物理内存地址。

这里还有几个概念:线性地址、有效地址、物理地址

如下指令:

MOV eax,dword ptr ds:[0x12345678]

其中,0x12345678 是有效地址

ds.Base + 0x12345678 是线性地址

物理地址就是真正在内存条上的地址,不是虚拟出来的。

每个进程都有一个CR3,(准确的说是都一个CR3的值,CR3本身是个寄存器,一个核,只有一套寄存器)

CR3指向一个物理页,一共4096字节,从CR3到物理页的过程如图:

下面在10-10-12分页模式下从线性地址找到物理地址。要想当前xp系统是10-10-12分页,需要修改boot.ini文件。

将noexecute 改成 execute。

写入一句话到记事本,并通过CE找到他的线性地址。

采用10-10-12分页方式拆解这个线性地址。(十位,十位和十二位)

拆完以后CPU首先去找CR3寄存器,CR3寄存器是一个唯一存储物理地址的寄存器,CR3中存了一个值,这个值指向一个物理页,这个也有4096个字节,也就是他的第一级,第一部分分的高十位就是确定这个地址在第一级的哪个位置,第二个十位就是确定在第二级的哪个位置,最后12位就是确定在4096个字节的物理页的v哪个地址,4096 = 2 ^ 12;第一级中每个成员是4个字节,4096个字节可以存放1024 = 2 ^ 10个地址,同样第二级也是一样。

通过windbg获取notepad的cr3。

这里还得计算几个偏移。前面两个都是目录,由于一个是4个字节所以需要乘以4。

在!dd表示查看物理地址。这里第一层是0

kd> !dd 15d45000+0

第一级找到了,要去掉最后三位,这三位是属性。

kd> !dd 15bad000+2A8

第三级一样的。使用!db一个字节一个字节的查看。

kd> !dd 15ba8000+A40
kd> !db 15ba8a40

物理地址就已经找到了。

在白皮书描述中整个过程如下(线性地址到物理地址):

 

PDE和PTE

Cr3寄存器起到了不可或缺的作用。那Cr3寄存器中存储的究竟是什么呢?

Cr3寄存器不同于其他寄存器,在所有的寄存器中,只有Cr3寄存器存储的地址是 物理地址,其他寄存器存储的都是 线性地址

Cr3寄存器所存储的物理地址指向了一个页目录表(Page-Directory Table,PDT),也就是我们前面所说的查找时的第一级。在Windows中,一个页的大小通常为4KB(有4MB的),即一个页(页目录表)可以存储1024个页目录表项(PDE)。

而第二级为页表(PTT), 每个页表的大小为4KB,即一个页表可以存储1024个页表项(PTE)。

这种设计方式正是10-10-12分页的由来,由于前面两级是四个字节一组,那么索引为2的10次方就可以获取到每一项(整个是4096字节),也就是10位;而最后一级物理页,一个字节一组,所以需要4096组,索引也要指到4096,也就是2的12次方,正好12位。

上面说到10-10-12分页还有一个大页(4MB),实际上是没有页表(PTT)这一级,也就是PDE直接去索引物理页,那么就是2^10*2^12,正好是4MB。

页表项(PTE)具有以下特征:

  1. PTE可以指向一个物理页,也可以不指向物理页
  2. 多个PTE可以指向同一个物理页
  3. 一个PTE只能指向一个物理页

 

实验

我们都知道地址0是绝对不能写入的,如果写入回报0xC0000005错误,那么是什么原因不能写入呢?他的本质实际上就是0地址没有对应的物理页,也就是上面所说的“PTE可以指向一个物理页,也可以不指向物理页”,0地址实际上就没有对应的物理页。

那么我们可以自己将线性地址0的PTE挂载到物理页上,这样就可以读写了。运行这样一段代码:

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

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

    *(int*)0 = 123;
    printf("0地址数据:%d\n",*(int*)0);
    return 0;
}

我们要做的就是将线性地址0的物理页挂载到局部变量x的物理页,让两个PTE指向的是同一个物理页。

还是先找到当前进程的cr3。

获取x的线性地址:0x0012ff7c,并对其经行分解。

然后找到其对应的物理地址

kd> !dd 1a9e9000 + 0
kd> !dd 1a9b4000 + 4BC
kd> !db 1a790000 + f7c

让线性地址0的PTE指向同一块物理地址。

如果线性地址为0,那么他就没有PTE,所以这里要写一个PTE。

kd> !dd 1a9b4000

由于二级偏移也是0,那么这里就把二级偏移直接写成物理页的首地址。也就是1a790067。

kd> !ed 1a9b4000 1a790067

回到程序重新执行,在线性地址0的位置已经写入了123。

此时用图形化表示为:

 

PDE&PTE属性

PDE和PTE的低12位实际上是表明属性,这个在之前的练习中已经了解过了。

物理页的属性

物理页的属性 = PDE属性 & PTE属性

P位和段的P位是一样的,表示当前PDE或者PTE是否有效,所以PDE与PTE的P位 P=1 才是有效的物理页。

R/W属性

R/W位表示是否是可读可写的。R/W = 0 只读,R/W = 1 可读可写,只有当PDE和PTE的R/W位都为1的时候,该物理页才是可读可写的。

观察下面一段代码:

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

int main(int argc, char* argv[])
{
    char* str = "Hello World";
    printf("线性地址:%x\n",str);
    getchar();

    DWORD dwVal = (DWORD)str;
    *(char*)dwVal = 'M';

    printf("%s",str);
    return 0;
}

直接执行是会报错的,因为str指向的是常量区中的一个字符串,这是不可以写的,但是如果我们更改物理页对应的PDE和PTE的R/W属性,则可以成功改写。

直接执行Access Violation。

拆分线性地址:

通过Cr3找到PTE,发现最后12位属性中R/W位为0。(属性为025)

那么这里就需要让R/W位为1,属性变为027。

!ed 30b4088 19c48027

代码能够顺利执行,字符串成功被修改。

U/S属性

  • U/S = 0 特权用户
  • U/S = 1 普通用户

特权用户也就意味着只有高权限才能访问,普通用户普通权限即可访问。

观察这样一段代码,直接访问肯定是失败。

int main(int argc, char* argv[])
{
    PDWORD p = (PDWORD)0x8003F00C;

    getchar();

    printf("高2G地址:%x\n",*p);
    return 0;
}

我们三环程序是无法直接访问高两G内存空间的,这里可以用之前的调用门提权访问,也可以通过修改页属性来访问。

这里具体细节和上面修改R/W差不多。

可以发现这个地址的PDE和PTE的U/S位都是0。

kd> !ed 1d50a800 0003b167
kd> !ed 3b0fc 0003f167

这里一不小心把程序放过去了,直接结束了没截图,并没有报错,也就不重新做这个实验了。

P/S位

只对PDE有意义,PS == PageSize的意思 当PS==1的时候 PDE直接指向

物理页 无PTE,低22位是页内偏移。

线性地址只能拆成2段:大小为4MB 俗称“大页”

A 位

是否被访问(读或者写)过 访问过置1 即使只访问一个字节也会导致PDE PTE置1

D 位

脏位 是否被写过 0没有被写过 1被写过

页目录表(PDE)基址

如果系统要保证某个线性地址是有效的,那么必须为其填充正确的PDE与PTE,如果我们想填充PDE与PTE那么必须能够访问PDT与PTT。那么存在2个问题:

1、一定已经有“人”为我们访问PDT与PTT挂好了PDE与PTE,我们只有找到这个线性地址就可以了。

2、这个为我们挂好PDE与PTE的“人”是谁?

结论就是有一个特殊的地址:0xC0300000。存储的值就是PDT。

获取cr3

kd> !dd 131fb000  + c00
kd> !dd 131fb000 + c00
kd> !dd 131fb000

可以看到通过这个线性地址实际上是重新解析了cr3寄存器。

也就是说,以后不需要Cr3,只需在当前程序内,通过C0300000这个线性地址就可以得到当前程序PDT的首地址了。

那么PDT的首地址可以找到,PTT的首地址呢?

页表(PTT)基址

还是有个特殊的线性地址:0xC0000000

获取debugview的线性地址。

这个线性地址对应的就是PDT表,而PDE表中第一个地址为第一张PTT表。

kd> !dd 18ae9000
kd> !dd 0bc7a000

PDT表中第二个地址为第二张PTT表。

kd> !dd 18ae9000
kd> !dd 194b9000

然后我们拆分c0000000地址。

kd> !dd 18ae9000 + c00
kd> !dd 18ae9000
kd> !dd 0bc7a000

可以看到0xc0000000对应的物理地址就是第一张PTT表。

再拆分c0001000地址。

kd> !dd 18ae9000 + c00
kd> !dd 18ae9000 + 4
kd> !dd 0bc7a000

0xc0001000对应的物理地址就是第二张PTT表。

所以实际上的对应关系应该如下图所示:

根本就不存在什么PDT表,PDT表知识PTT表中的一个特殊的部分。

掌握了0xC0001000和0xC0300000,就掌握了一个进程所有的物理内存读写权限。

PDI和PTI分别指的是再PDT表和PTT表中的索引。

访问页目录表(PDT)的公式:

0xC0300000 + PDI*4

访问页表(PTT)公式:

0xC0000000 + PDI*4096 + PTI*4

总结:

1、页表被映射到了从0xC0000000到0xC03FFFFF的4M地址空间。

2、在这1024个表中有一张特殊的表:页目录表。

3、页目录被映射到了0xC0300000开始处的4K地址空间。

写入shellcode到0地址执行

这里直接看注释,要自己捋一下。

// CallGate0Address.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

char buf[] = {0x6a,0x00,0x6a,0,0x6a,0,0x6a,0,0xE8,0,0,0,0,0xc3};

__declspec(naked) void callGate()
{
    _asm
    {
        push 0x30;
        pop fs;
        pushad;
        pushfd;

        lea eax,buf;
        mov ebx,dword ptr ds:[0xc0300000];
        //当0xc0300000位置上的值是0时,表明地址0对应的PDE没有挂上,跳转代码为挂上buf对应的物理页。
        //不是0挂PTE就行了
        test ebx,ebx;
        je __gPDE;

        shr eax,12;
        and eax,0xfffff;
        shl eax,2;

        add eax, 0xc0000000;
        mov eax,[eax];
        mov dword ptr ds:[0xc0000000],eax;
        jmp __retR;

    __gPDE:
        //获取前10位偏移
        shr eax,22;
        and eax,0x3ff;
        //乘以4
        shl eax,2;

        //将buf对应的PDE挂到0地址
        add eax, 0xc0300000;
        mov eax,[eax];
        mov dword ptr ds:[0xc0300000],eax;

    __retR:    
            popfd;
            popad;
            retf;
    }

}
int main(int argc, char* argv[])
{
    unsigned int functionAddress = (unsigned int)MessageBox;
    //获取在物理页上的偏移,后12位。
    int offset1 = ((unsigned int)buf) & 0xfff;
    *((unsigned int*)&buf[9]) = functionAddress - (13 + offset1);

    char segmentGate[] = {0,0,0,0,0x48,0};
    printf("MessageBox:%x  callGate:%x buf:%x\n",MessageBox,callGate,buf);

    system("pause");
    _asm
    {
        call fword ptr segmentGate;
        push 0x3b;
        pop fs;
        mov eax,offset1;
        call eax;
    }

    return 0;
}

添加调用门:

kd> eq 8003f048 0040ec00`0008100a

 

后记

下一节进入2-9-9-12分页。

本文由SD原创发布

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

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

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

发表评论

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