PDPTE,PDE,PTE都占8字节。
PDPTE的12-35位存储了页目录表基址的高24位,低12位补零。物理地址共占36位,接下来介绍的PDE,PTE均是如此。
PCD, PWT等属性等学到TLB才知道是干嘛的。
从上图可以看出,PS位=1时,PDE直接指向大物理页,其中,物理页偏移由线性地址的剩余21位(32-2-9=21)构成,由此推出大物理页占2^21=2MB;PS=0时,PDE指向页表。
解释一下G位,G=1表示这是全局页,是多个进程共享的,这种页是通过 CreateFileMapping 申请的。与之对应的,G=0就是进程独享的物理页,这种页是通过 VirtualAlloc 分配的。
G=1,即为全局页,进程(CR3)切换时,TLB中的记录不会被刷新。
最后解释一下最高位(图中没有标出的保留位),称为XD位或者NX位,当最高位置1,表示这个物理页不能当成代码执行。XD位是PDE和PTE都有的,PDE或PTE的XD位只要有一个是1,这个物理页就不能执行。
属性部分和10-10-12差不多,没什么新东西,注意物理页基址是36位,最高位是XD位即可。
写一个测试程序:
// ReadWriteNULL_2-9-9-12.cpp : Defines the entry point for the console application. // #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { char data[0x1000] = {0}; int *p = NULL; printf("可用的物理页基址:%p\n", data); printf("请在windbg中给NULL挂物理页.\n"); getchar(); // windbg... // 读写NULL *p = 0x20201008; printf("*NULL = %x\n", *p); getchar(); return 0; }让程序跑起来:
上节课已经手动拆过线性地址啦,所以今天就用 !vtop 偷懒了。
查CR3
!process 0 0 CR3=134c03e0拆NULL
kd> !vtop 134c03e0 0 X86VtoP: Virt 00000000, pagedir 134c03e0 X86VtoP: PAE PDPE 134c03e0 - 0000000011046001 X86VtoP: PAE PDE 11046000 - 000000000f4e1067 X86VtoP: PAE PTE f4e1000 - 0000000000000000 X86VtoP: PAE zero PTE Virtual address 0 translation fails, error 0xD0000147.拆0x12ef60
kd> !vtop 134c03e0 12ef60 X86VtoP: Virt 0012ef60, pagedir 134c03e0 X86VtoP: PAE PDPE 134c03e0 - 0000000011046001 X86VtoP: PAE PDE 11046000 - 000000000f4e1067 X86VtoP: PAE PTE f4e1970 - 800000001d6b5067 X86VtoP: PAE Mapped phys 1d6b5f60 Virtual address 12ef60 translates to physical address 1d6b5f60.挂物理页(没有!eq指令??)
!ed f4e1000 1d6b5067 !ed f4e1004 80000000改好了,执行剩余代码:
实验成功。
编写一个程序,读写0x8003f048.
// ReadWriteH2G_2-9-9-12.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <windows.h> int _tmain(int argc, _TCHAR* argv[]) { printf("请在windbg修改8003f048的U/S位.\n"); getchar(); printf("%08x\n", *(PDWORD)0x8003f048); // 读 *(PDWORD)0x8003f048 = 0x12345678; // 写 printf("%08x\n", *(PDWORD)0x8003f048); // 读 getchar(); return 0; }让程序跑起来。
查CR3
!process 0 0 CR3 = 12100420拆8003f048
!vtop 12100420 8003f048 kd> !vtop 12100420 8003f048 X86VtoP: Virt 8003f048, pagedir 12100420 X86VtoP: PAE PDPE 12100430 - 00000000044c8001 X86VtoP: PAE PDE 44c8000 - 0000000000b5a163 X86VtoP: PAE PTE b5a1f8 - 000000000003f163 X86VtoP: PAE Mapped phys 3f048 Virtual address 8003f048 translates to physical address 3f048.改U/S位,和PTE的G位 G=1时,程序会崩,只有G=0才能成功。 简单解释一下G位: PDE中,只有PS=1(大页)时,G才有效。 G位是全局页的意思,G=1时,这个线性地址对应的页是全局页,进程切换时,对应的TLB不会改变。多个进程的高2G数据大部分都是相同的,我们不希望在切换进程时对这部分TLB做多余的刷新,所以需要设置G=1以提高效率。
回到本文,我们已经知道PDE的PS=0,所以我们不用管PDE的G。关键是PTE的G,为什么要改成0呢?因为线性地址 0x8003f048 默认是G=1,它在CPU中有TLB缓存。
我们修改了 0x8003f048 的PTE的U/S位,但是因为G=1,在CPU中有缓存,访问线性地址时优先读取TLB缓存,而缓存中的ATTR并没有改变,所以我们对U/S的修改是无效的。即使用 !ed 指令改了U/S,我们的应用层代码试图访问 0x8003f048 时,用的仍然是旧的属性,U/S仍然是0.
更多关于TLB和G位的知识,请看后续的TLB专题博客。
!ed 44c8000 00b5a167 !ed b5a1f8 0003f067执行剩余的代码:
首先给出我自己画的示意图: 根据示意图,0xC000000是第一张页表的线性地址,0xC0600000是第一张页目录表的线性地址。
结合示意图,理解下面的公式:
2-9-9-12 PDPTI-PDI-PTI-OFFSET 公式: pPDE = 0xc0600000 + (PDPTI*4KB) + (PDI*8) pPTE = 0xc0000000 + (PDPTI*2MB) + (PDI*4KB) + (PTI*8) 更高效的公式(MmIsAddressValid是这么干的) pPDE = 0xc0600000 + ((addr >> 18) & 0x3ff8) pPTE = 0xc0000000 + ((addr >> 9) & 0x7ffff8)解释: 0xc0600000 是第一张页目录表,0xc0600000 + (PDPTI * 4KB) 就是找线性地址对应的页目录表,再加上(PDI * 8)就找到了对应的PDE。
0xc0000000 是第一张页表,0xc0000000 + (PDPTI * 2MB) 就是找线性地址对应的2MB页表的基址,然后加上 (PDI4KB) 就是对应的页表,最后再加上(PTI8)就找到了PTE。
理解了2-9-9-12的映射结构,再来分析 MmIsAddressValid 函数就比较容易了。 该函数为了提高效率,移位看起来比较费脑,但只要理解了我上面画的示意图和那些公式,分析起来应该就没什么问题了。
代码中有一些莫名其妙的语句,比如:
.text:004399A1 mov [ebp+var_4], eax .text:004399B7 push 0 .text:004399B9 mov [ebp+var_8], edx .text:004399BC pop eax猜测是因为编程使用的是C语言,所以生成了一些冗余的代码。
分析前:
--------------------------------------------------------------------------- .text:0043997A align 10h .text:00439980 ; Exported entry 685. MmIsAddressValid .text:00439980 .text:00439980 ; =============== S U B R O U T I N E ======================================= .text:00439980 .text:00439980 ; Attributes: bp-based frame .text:00439980 .text:00439980 ; BOOLEAN __stdcall MmIsAddressValid(PVOID VirtualAddress) .text:00439980 public _MmIsAddressValid@4 .text:00439980 _MmIsAddressValid@4 proc near ; CODE XREF: IopIsAddressRangeValid(x,x)+2Fp .text:00439980 ; IopGetMaxValidMemorySize(x,x)+29p ... .text:00439980 .text:00439980 var_8 = dword ptr -8 .text:00439980 var_4 = dword ptr -4 .text:00439980 VirtualAddress = dword ptr 8 .text:00439980 .text:00439980 mov edi, edi .text:00439982 push ebp .text:00439983 mov ebp, esp .text:00439985 push ecx .text:00439986 push ecx .text:00439987 mov ecx, [ebp+VirtualAddress] .text:0043998A push esi .text:0043998B mov eax, ecx .text:0043998D shr eax, 12h .text:00439990 mov esi, 3FF8h .text:00439995 and eax, esi .text:00439997 sub eax, 3FA00000h .text:0043999C mov edx, [eax] .text:0043999E mov eax, [eax+4] .text:004399A1 mov [ebp+var_4], eax .text:004399A4 mov eax, edx .text:004399A6 push edi .text:004399A7 and eax, 1 .text:004399AA xor edi, edi .text:004399AC or eax, edi .text:004399AE jz short loc_439A11 .text:004399B0 mov edi, 80h .text:004399B5 and edx, edi .text:004399B7 push 0 .text:004399B9 mov [ebp+var_8], edx .text:004399BC pop eax .text:004399BD jz short loc_4399C3 .text:004399BF test eax, eax .text:004399C1 jz short loc_439A15 .text:004399C3 .text:004399C3 loc_4399C3: ; CODE XREF: MmIsAddressValid(x)+3Dj .text:004399C3 shr ecx, 9 .text:004399C6 and ecx, 7FFFF8h .text:004399CC mov eax, [ecx-3FFFFFFCh] .text:004399D2 sub ecx, 40000000h .text:004399D8 mov edx, [ecx] .text:004399DA mov [ebp+var_4], eax .text:004399DD push ebx .text:004399DE mov eax, edx .text:004399E0 xor ebx, ebx .text:004399E2 and eax, 1 .text:004399E5 or eax, ebx .text:004399E7 pop ebx .text:004399E8 jz short loc_439A11 .text:004399EA and edx, edi .text:004399EC push 0 .text:004399EE mov [ebp+var_8], edx .text:004399F1 pop eax .text:004399F2 jz short loc_439A15 .text:004399F4 test eax, eax .text:004399F6 jnz short loc_439A15 .text:004399F8 and ecx, esi .text:004399FA mov ecx, [ecx-3FA00000h] .text:00439A00 mov eax, 81h .text:00439A05 and ecx, eax .text:00439A07 xor edx, edx .text:00439A09 cmp ecx, eax .text:00439A0B jnz short loc_439A15 .text:00439A0D test edx, edx .text:00439A0F jnz short loc_439A15 .text:00439A11 .text:00439A11 loc_439A11: ; CODE XREF: MmIsAddressValid(x)+2Ej .text:00439A11 ; MmIsAddressValid(x)+68j .text:00439A11 xor al, al .text:00439A13 jmp short loc_439A17 .text:00439A15 ; --------------------------------------------------------------------------- .text:00439A15 .text:00439A15 loc_439A15: ; CODE XREF: MmIsAddressValid(x)+41j .text:00439A15 ; MmIsAddressValid(x)+72j ... .text:00439A15 mov al, 1 .text:00439A17 .text:00439A17 loc_439A17: ; CODE XREF: MmIsAddressValid(x)+93j .text:00439A17 pop edi .text:00439A18 pop esi .text:00439A19 leave .text:00439A1A retn 4 .text:00439A1A _MmIsAddressValid@4 endp分析后: 主要是判断PDE PTE的P,PS位。
--------------------------------------------------------------------------- .text:0043997A align 10h .text:00439980 ; Exported entry 685. MmIsAddressValid .text:00439980 .text:00439980 ; =============== S U B R O U T I N E ======================================= .text:00439980 .text:00439980 ; Attributes: bp-based frame .text:00439980 .text:00439980 ; BOOLEAN __stdcall MmIsAddressValid(PVOID VirtualAddress) .text:00439980 public _MmIsAddressValid@4 .text:00439980 _MmIsAddressValid@4 proc near ; CODE XREF: IopIsAddressRangeValid(x,x)+2Fp .text:00439980 ; IopGetMaxValidMemorySize(x,x)+29p ... .text:00439980 .text:00439980 var_8 = dword ptr -8 .text:00439980 var_4 = dword ptr -4 .text:00439980 VirtualAddress = dword ptr 8 .text:00439980 .text:00439980 mov edi, edi .text:00439982 push ebp .text:00439983 mov ebp, esp .text:00439985 push ecx .text:00439986 push ecx .text:00439987 mov ecx, [ebp+VirtualAddress] ; ecx = VAddr .text:0043998A push esi .text:0043998B mov eax, ecx ; eax = VAddr .text:0043998D shr eax, 12h ; VAddr >> 18 .text:00439990 mov esi, 3FF8h .text:00439995 and eax, esi ; eax = PDPTI * 4KB + PDI * 8 .text:00439997 sub eax, 3FA00000h ; eax = C0600000 + PDPTI * 4KB + PDI * 8 .text:00439997 ; eax 指向了 PDE .text:0043999C mov edx, [eax] .text:0043999E mov eax, [eax+4] ; eax,edx = PDE .text:004399A1 mov [ebp+var_4], eax .text:004399A4 mov eax, edx ; eax = PDE低4字节 .text:004399A6 push edi .text:004399A7 and eax, 1 ; 取PDE的P位 .text:004399AA xor edi, edi .text:004399AC or eax, edi ; if (P==0) 返回假 .text:004399AE jz short loc_439A11 .text:004399B0 mov edi, 80h .text:004399B5 and edx, edi ; 取PS位判断 .text:004399B7 push 0 .text:004399B9 mov [ebp+var_8], edx .text:004399BC pop eax ; eax = 0 .text:004399BD jz short loc_4399C3 ; if (PS==0) 跳转到小页处理 .text:004399BF test eax, eax .text:004399C1 jz short loc_439A15 ; 如果PS==1,即大页,就直接返回真 .text:004399C3 .text:004399C3 loc_4399C3: ; CODE XREF: MmIsAddressValid(x)+3Dj .text:004399C3 shr ecx, 9 ; VAddr >> 9 效果相当于右移12位,然后乘以8 .text:004399C6 and ecx, 7FFFF8h ; ecx = PDPTI * 2MB + PDI * 4KB + PTI * 8 .text:004399CC mov eax, [ecx-3FFFFFFCh] .text:004399D2 sub ecx, 40000000h ; ecx = C0000000 + PDPTI * 2MB + PDI * 4KB + PTI * 8 .text:004399D2 ; ecx 指向 PTE .text:004399D8 mov edx, [ecx] ; edx = PTE低4字节 .text:004399DA mov [ebp+var_4], eax .text:004399DD push ebx .text:004399DE mov eax, edx ; eax = PTE低4字节 .text:004399E0 xor ebx, ebx ; ebx = 0 .text:004399E2 and eax, 1 ; 取PTE的P位 .text:004399E5 or eax, ebx .text:004399E7 pop ebx .text:004399E8 jz short loc_439A11 ; P==0 返回假 .text:004399EA and edx, edi ; PTE低4字节 & 80h,即取PAT位 .text:004399EC push 0 .text:004399EE mov [ebp+var_8], edx .text:004399F1 pop eax .text:004399F2 jz short loc_439A15 ; PAT==0 返回真 .text:004399F4 test eax, eax .text:004399F6 jnz short loc_439A15 .text:004399F8 and ecx, esi ; 后面是判断PAT==1的情况,我就不分析了,看不懂 .text:004399FA mov ecx, [ecx-3FA00000h] .text:00439A00 mov eax, 81h .text:00439A05 and ecx, eax .text:00439A07 xor edx, edx .text:00439A09 cmp ecx, eax .text:00439A0B jnz short loc_439A15 .text:00439A0D test edx, edx .text:00439A0F jnz short loc_439A15 .text:00439A11 .text:00439A11 loc_439A11: ; CODE XREF: MmIsAddressValid(x)+2Ej .text:00439A11 ; MmIsAddressValid(x)+68j .text:00439A11 xor al, al .text:00439A13 jmp short loc_439A17 .text:00439A15 ; --------------------------------------------------------------------------- .text:00439A15 .text:00439A15 loc_439A15: ; CODE XREF: MmIsAddressValid(x)+41j .text:00439A15 ; MmIsAddressValid(x)+72j ... .text:00439A15 mov al, 1 .text:00439A17 .text:00439A17 loc_439A17: ; CODE XREF: MmIsAddressValid(x)+93j .text:00439A17 pop edi .text:00439A18 pop esi .text:00439A19 leave .text:00439A1A retn 4 .text:00439A1A _MmIsAddressValid@4 endp我写了一个程序,是用一个指针指向一块内存,然后用汇编call过去。然后可以对比XD=0和XD=1时,CALL的结果。
// TestXD.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <Windows.h> char *buff; DWORD *GetPDE(DWORD addr) { //return (DWORD *)(0xc0600000 + ((addr >> 18) & 0x3ff8)); DWORD PDPTI = addr >> 30; DWORD PDI = (addr >> 21) & 0x000001FF; DWORD PTI = (addr >> 12) & 0x000001FF; return (DWORD *)(0xC0600000 + PDPTI * 0x1000 + PDI * 8); } DWORD *GetPTE(DWORD addr) { //return (DWORD *)(0xc0000000 + ((addr >> 9) & 0x7ffff8)); DWORD PDPTI = addr >> 30; DWORD PDI = (addr >> 21) & 0x000001FF; DWORD PTI = (addr >> 12) & 0x000001FF; return (DWORD *)(0xC0000000 + PDPTI * 0x200000 + PDI * 0x1000 + PTI * 8); } void __declspec(naked) R0Function() { __asm { push ebp mov ebp,esp sub esp,0x1000 pushad pushfd } __asm push fs //__asm int 3 // 修改buff的XD位 *(GetPDE((DWORD)buff) + 1) |= 0x80000000; *(GetPTE((DWORD)buff) + 1) |= 0x80000000; //__asm int 3 __asm pop fs __asm { popfd popad add esp,0x1000 mov esp,ebp pop ebp iretd } } int _tmain(int argc, _TCHAR* argv[]) { // 申请一个内存页,写入硬编码 buff = (char *)VirtualAlloc(NULL,0x1000,MEM_COMMIT,PAGE_EXECUTE_READWRITE); printf("buff: %p\n", buff); BYTE bytecode [7] = {0x90,0xB8,0x01,0x00,0x00,0x00,0xC3}; // NOP, MOV EAX,1, RET memcpy(buff,bytecode,7); // 测试,对比XD=0和XD=1的运行结果 printf("输入XD位:"); int xd; scanf("%d",&xd); // 输入0可以调用,输入1调用失败 getchar(); if (xd == 1) { printf("在IDT表构建中断门,请在windbg中执行下面的指令:\n"); printf("eq 8003f500 %04xee00`0008%04x\n",(DWORD)R0Function>>16,(DWORD)R0Function & 0x0000FFFF); getchar(); __asm int 0x20 printf("XD位修改成功,buff不可执行.\n"); } DWORD dwEAX = 0; __asm { push eax xor eax,eax call buff mov dwEAX,eax pop eax }; if (dwEAX == 0) printf("调用失败.\n"); else if (dwEAX == 1) printf("调用成功.\n"); printf("bye!\n"); getchar(); return 0; }如果XD=0,就是默认情况了,可以正常调用函数并返回。
如果将XD修改为1,那么执行CALL会失败,程序会卡死。
