【技术分享】深耕保护模式(四)

admin 2022年7月20日09:42:41评论12 views字数 5531阅读18分26秒阅读模式

【技术分享】深耕保护模式(四)

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+A40kd> !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的地址:%xn",&x);

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

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

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

【技术分享】深耕保护模式(四)

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

【技术分享】深耕保护模式(四)

【技术分享】深耕保护模式(四)

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

kd> !dd 1a9e9000 + 0kd> !dd 1a9b4000 + 4BCkd> !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("线性地址:%xn",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地址:%xn",*p); return 0;
}

【技术分享】深耕保护模式(四)

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

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

【技术分享】深耕保护模式(四)

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

【技术分享】深耕保护模式(四)

kd> !ed 1d50a800 0003b167kd> !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  + c00kd> !dd 131fb000 + c00kd> !dd 131fb000

【技术分享】深耕保护模式(四)

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

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

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

页表(PTT)基址

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

获取debugview的线性地址。

【技术分享】深耕保护模式(四)

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

kd> !dd 18ae9000kd> !dd 0bc7a000

【技术分享】深耕保护模式(四)

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

kd> !dd 18ae9000kd> !dd 194b9000

【技术分享】深耕保护模式(四)

然后我们拆分c0000000地址。

kd> !dd 18ae9000 + c00kd> !dd 18ae9000kd> !dd 0bc7a000

【技术分享】深耕保护模式(四)

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

再拆分c0001000地址。

kd> !dd 18ae9000 + c00kd> !dd 18ae9000 + 4kd> !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:%xn",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分页。

【技术分享】深耕保护模式(四)

- 结尾 -
精彩推荐
【技术分享】RedGuard - Excellent C2 Front Flow Control tool
【技术分享】后门防御-Neural Cleanse分析及复现
【技术分享】从Java反序列化漏洞题看CodeQL数据流

【技术分享】深耕保护模式(四)
戳“阅读原文”查看更多内容

原文始发于微信公众号(安全客):【技术分享】深耕保护模式(四)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年7月20日09:42:41
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【技术分享】深耕保护模式(四)https://cn-sec.com/archives/1186479.html

发表评论

匿名网友 填写信息