Windows内核实验 ——— 零地址读写

前言

我们都知道在C语言中0地址是无法进行读写的,会造成读写异常。本质上来说是因为0地址是一个不存在的物理页面,但是我们可以尝试进行对其修改,将其PTE进行修改,进而我们可以对其进行读写操作

实验过程

我们可以先通过!pte 0来测试零地址是不可读写的

可以观察到下面的not valid,证明其不可读写的特性。但是我们之前提到了对PTE修改的计算公式,我们可以利用该计算方式来修改零地址处的PTE值,进而使其可读写

1PTE -> ((addr >> 12) << 3) + 0xC0000000

我们此处采用代码来对其进行修改,将零地址设置为可读写

12345678910111213141516171819202122232425262728293031323334353637383940414243#include#include#define PTE(addr) ((DWORD *)(((addr >> 12) << 3) + 0xC0000000)) // 0x004197B0 signFlag addrvolatile DWORD g_var; // 加 volatile 防止编译器优化DWORD signFlag = 0x12345678;void __declspec(naked) IdtEntry() { // 修改零地址的页为signFlag的页地址 PTE(0)[0] = PTE(0x004197B0)[0]; PTE(0)[1] = PTE(0x004197B0)[1]; g_var = *(DWORD *)0x18; // 0 地址指向 0x18 地址 *(DWORD *)0x18 = 0xAABBCCDD; // 修改 0x18 地址处的数据 __asm { iretd }}void crash() { __asm { int 0x20 }}int main() { if ((DWORD)IdtEntry != 0x00401040) { printf("Your addr is: %p\n", IdtEntry); return 0; } crash(); printf("signFlag addr : %p\n", &signFlag); printf("g_var addr : %p\n", &g_var); system("pause"); return 0;}

上述代码我们构造了一个可读写的全局变量signFlag,我们将其PTE的值赋给零地址, 让其获得可读写的属性

此时访问零地址和访问signFLag处地址是一样的,因为我们已经将零地址的PTE挂在了signFlag上,其指向的物理地址是一样的

我们执行完上述代码后我们再次使用!pte 0来进行查看对应零地址的页属性

可以看到其不再是not valid,而是可写可读,我们可以使用Windbg进一步的访问零地址,观察0x18地址处的数据是否被修改为了0xAABBCCDD

同时我们访问signFlag对应的页地址0x18处的数据,我们可以看到其内容也是一样的

需要注意的是signFlag最后的三位(16进制下)需要替换为 0,才是对应的页地址的起始位置

总结

对于上述的代码是不完全安全的,存在有一定的概率导致蓝屏。因为有TLB表的存在

TLB表作为页表缓冲存放着一些一些页表文件。当CPU要在主内存寻址时,不是直接在内存的物理地址里查找的,而是通过一组虚拟地址转换到主内存的物理地址,TLB就是负责将虚拟内存地址翻译成实际的物理内存地址,而CPU寻址时会优先在TLB中进行寻址

在上述代码中我们修改了映射关系将零地址的PTE内存映射到了我们自己定义的一共可读可写的页上而未对TLB表进行刷新,当CPU寻址时仍在未刷新的部分进行寻找,进而引发读写异常进而导致蓝屏

对于上述操作我们可以利用一些TLB的性质来对其进行刷新操作,x86_32仅提供了两种硬件接口来刷新TLB表项:

向cr3寄存器写入值时,会导致处理器自动刷新非全局页的TLB表项

在Pentium Pro以后,invlpg汇编指令用来无效指定线性地址的单个TLB表项无效。

我们使用第一个最简单的来进行刷新即可

1234__asm{ mov eax, cr3 mov cr3, eax}

我们可以通过手动写入cr3来完成对应的刷新工作,进而规避出现读写异常的情况发生

Copyright © 2022 历届世界杯_世界杯篮球 - cnfznx.com All Rights Reserved.