前言
回到我们之前提到的三级内存映射关系,对于我们之前查询地址的操作可以通过判断TLB中是否有对应的缓存来进行读取,当存在有对应数据时我们便从TLB进行直接获取,若不存在则是通过访问cr3进而访问对应的PDE、PTE信息,内存访问的大致流程为:
TLB直接记录了虚拟内存到物理内存的访问关系,对于TLB我们无法直接通过汇编语句来进行获取
实验过程
在此次实验中需要注意:
- 代码顺序执行,即不产生页面异常
- 确保测试虚拟地址已经存放到TLB中,即需要有最近的内存访问
- 刷新TLB实际上是使TLB无效,不访问内存
验证TLB的存在
我们首先编写一串代码来进行验证TLB,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| #include <stdio.h> #include <Windows.h>
#define PTE(addr) ((DWORD *)(((addr >> 12) << 3) + 0xC0000000))
#pragma section("data_seg", read, write) __declspec(allocate("data_seg")) DWORD page1[1024]; __declspec(allocate("data_seg")) DWORD page2[1024];
DWORD g_oldPTE[2]; DWORD g_out;
void __declspec(naked) IdtEntry() { __asm { mov eax, ds: [0x0041c000] ; 通过访问,确保TLB中存在有该项目 } g_oldPTE[0] = PTE(0x0041c000)[0]; g_oldPTE[1] = PTE(0x0041c000)[1]; PTE(0x0041c000)[0] = PTE(0x0041b000)[0]; PTE(0x0041c000)[1] = PTE(0x0041b000)[1]; g_out = page2[0]; PTE(0x0041c000)[0] = g_oldPTE[0]; PTE(0x0041c000)[1] = g_oldPTE[1]; __asm { iretd } }
void crash() { page1[0] = 1; page2[0] = 2; __asm { int 0x20 } }
int main() { if ((DWORD)IdtEntry != 0x401040) { printf("IdtEntry : %p\n", IdtEntry); return 0; } crash(); printf("g_out = %d\n", g_out); system("pause"); return 0; }
|
执行结果如下:
上述代码中我们将page2
的页面先进行了一次读取,在此次读取时操作系统将对应物理地址保存到了TLB
中,即使后续我们修改了对应的内存映射,我们所访问的仍然是通过TLB
进行的,进而导致访问结果仍然为 2
如果我们加入刷新TLB
时便会通过cr3
来获取对应的物理地址信息,我们修改代码中的IdtEntry
如下:
我们通过TLB
机制来进行刷新:当对cr3进行赋值时便会刷新 TLB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| void __declspec(naked) IdtEntry() { __asm { mov eax, ds: [0x0041c000] ; 通过访问,确保TLB中存在有该项目 } g_oldPTE[0] = PTE(0x0041c000)[0]; g_oldPTE[1] = PTE(0x0041c000)[1];
PTE(0x0041c000)[0] = PTE(0x0041b000)[0]; PTE(0x0041c000)[1] = PTE(0x0041b000)[1];
__asm { mov eax, cr3 mov cr3, eax }
g_out = page2[0];
PTE(0x0041c000)[0] = g_oldPTE[0]; PTE(0x0041c000)[1] = g_oldPTE[1]; __asm { iretd } }
|
修改后程序执行结果如下:
至此我们成功的验证了TLB的存在
从上面我们大致可以看出,当我们对页进行了修改时,对其内容进行读写。如果对应数据出现在了TLB中,那么便会从TLB中优先获取值,当刷新TLB后便会从内存中进行读取,也就是我们修改后的页上开始读
TLB G属性
对于操作系统的高 2G 映射基本不变,如果Cr3
改了,那么TLB便会刷新,重建 2G 以上很浪费,所以相对的 PTE 中有一个G标志位,如果G位
为 1 刷新 TLB 时将不会刷新 PTE 的G位
为 1 的页,当 TLB 满了,根据统计信息将不常用的地址废弃,最近最常用的保留。
一般来说三环程序来说没有G属性
,我们可以手动添加一个int 3
在代码中,使其中断到我们的Windbg中,
可以明显的看到我们的页属性中没有对应的 G位属性
我们观察GDTR的页属性可以看到 G位属性的存在
印证我们之前提到的高 2G 内存,每次重建会比较消耗时间,所以在系统内其会设置G位属性,通过查询Windbg的帮助手册我们可以看到
其对应位为0x100
,所以我们在修改PTE时可以进行或上0x100
,来达到添加 G位属性的目的
我们先不设置 G位属性来进行测试,我们将IdtEntry
设置如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| void __declspec(naked) IdtEntry() { g_oldPTE[0] = PTE(0x0041c000)[0]; g_oldPTE[1] = PTE(0x0041c000)[1]; PTE(0x0041c000)[0] = PTE(0x0041b000)[0]; PTE(0x0041c000)[1] = PTE(0x0041b000)[1]; __asm { mov eax, cr3 mov cr3, eax } __asm { mov eax, ds: [0x0041c000] ; 通过访问,确保TLB中存在有该项目 } g_out_1 = page2[0]; PTE(0x0041c000)[0] = g_oldPTE[0]; PTE(0x0041c000)[1] = g_oldPTE[1]; __asm { mov eax, cr3 mov cr3, eax } g_out_2 = page2[0]; __asm { iretd } }
|
可以看到输出为:
我们第一次刷新TLB
后读取到的数据为我们修改后的page1
的内容,再次修改page2
的PTE
后读取的是我们恢复的,证明对应TLB
信息随cr3
的更新而清空了,如果我们设置对应的G属性位后呢?代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| void __declspec(naked) IdtEntry() { g_oldPTE[0] = PTE(0x0041c000)[0]; g_oldPTE[1] = PTE(0x0041c000)[1]; PTE(0x0041c000)[0] = PTE(0x0041b000)[0] | 0x100; PTE(0x0041c000)[1] = PTE(0x0041b000)[1]; __asm { mov eax, cr3 mov cr3, eax } __asm { mov eax, ds: [0x0041c000] ; 通过访问,确保TLB中存在有该项目 } g_out_1 = page2[0]; PTE(0x0041c000)[0] = g_oldPTE[0]; PTE(0x0041c000)[1] = g_oldPTE[1]; __asm { mov eax, cr3 mov cr3, eax } g_out_2 = page2[0]; __asm { iretd } }
|
可以看到输出如下:
可以看到TLB所保存的数据并没有清除,而是得到了对应的保存
但是TLB中的数据是一直存在的吗?显然不是,我们可以通过以下汇编指令来强制对含有G位属性的页的TLB来进行刷新
1 2 3
| __asm{ invlpg ds:[xxx] ; xxx 为对应地址 }
|
我们将原来的第二次TLB刷新更新为:
1 2 3
| __asm{ invlpg ds:[0x0041b000] }
|
可以成功的看到对应g_out_2
的输出又回到了 2
总结
上述的应用我们可以应用到内核的Hook,我们可以将需要Hook的地址修改其PTE加入一个可写选项,需要注意的是我们需要对其TLB进行强制刷新,因为其处于内核状态下是肯定有G位属性的,如果简单的采用cr3的刷新方式来进行刷新TLB时可能会产生异常