前言
回到我们之前提到的三级内存映射关系,对于我们之前查询地址的操作可以通过判断TLB中是否有对应的缓存来进行读取,当存在有对应数据时我们便从TLB进行直接获取,若不存在则是通过访问cr3进而访问对应的PDE、PTE信息,内存访问的大致流程为:
 
TLB直接记录了虚拟内存到物理内存的访问关系,对于TLB我们无法直接通过汇编语句来进行获取
实验过程
在此次实验中需要注意:
- 代码顺序执行,即不产生页面异常
- 确保测试虚拟地址已经存放到TLB中,即需要有最近的内存访问
- 刷新TLB实际上是使TLB无效,不访问内存
验证TLB的存在
我们首先编写一串代码来进行验证TLB,代码如下:
| 12
 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
| 12
 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设置如下
| 12
 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属性位后呢?代码如下:
| 12
 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来进行刷新
| 12
 3
 
 | __asm{invlpg ds:[xxx] ; xxx 为对应地址
 }
 
 | 
我们将原来的第二次TLB刷新更新为:
| 12
 3
 
 | __asm{invlpg ds:[0x0041b000]
 }
 
 | 
可以成功的看到对应g_out_2的输出又回到了 2
总结
上述的应用我们可以应用到内核的Hook,我们可以将需要Hook的地址修改其PTE加入一个可写选项,需要注意的是我们需要对其TLB进行强制刷新,因为其处于内核状态下是肯定有G位属性的,如果简单的采用cr3的刷新方式来进行刷新TLB时可能会产生异常