保护模式篇——总结与提升
写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。
看此教程之前,问几个问题,基础知识储备好了吗?上一节教程学会了吗?上一节课的练习做了吗?没有的话就不要继续了。
🔒 华丽的分割线 🔒
练习及参考
本次答案均为参考,可以与我的答案不一致,但必须成功通过。
在看参考答案之前先看一个东西,我们需要知道fs
在0环时存的是什么,首地址是什么。首先看一下fs
的首地址是什么:
从上图看出在0环的首地址是0xFFDFF000
,那么这个地址是存什么的呢?我们用!pcr
指令看一下:
KPCR for Processor 0 at ffdff000:
Major 1 Minor 1
NtTib.ExceptionList: 8054a4b0
NtTib.StackBase: 8054acf0
NtTib.StackLimit: 80547f00
NtTib.SubSystemTib: 00000000
NtTib.Version: 00000000
NtTib.UserPointer: 00000000
NtTib.SelfTib: 00000000
SelfPcr: ffdff000
Prcb: ffdff120
Irql: 0000001c
IRR: 00000000
IDR: ffff20f8
InterruptMode: 00000000
IDT: 8003f400
GDT: 8003f000
TSS: 80042000
CurrentThread: 80553740
NextThread: 00000000
IdleThread: 80553740
DpcQueue:
可以看出这个地址存储的是KPCR
结构体,那么KPCR
是什么呢?它是一个结构体,由于Windows
需要支持多个CPU
,因此Windows
内核中为此定义了一套以处理器控制区,即KPCR
为枢纽的数据结构, 使每个CPU
都有个KPCR
。其中KPCR
这个结构中有一个域PRCB
结构, 这个结构扩展了KPCR
。这两个结构用来保存与线程切换相关的全局信息。具体的细节将会在本系列教程的进程线程篇进行讲解。
为了更加方便的查看结构体详情,我们dt
一下:
dt _KPCR ffdff000
nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x01c SelfPcr : 0xffdff000 _KPCR
+0x020 Prcb : 0xffdff120 _KPRCB
+0x024 Irql : 0x1c ''
+0x028 IRR : 0
+0x02c IrrActive : 0
+0x030 IDR : 0xffff20f8
+0x034 KdVersionBlock : 0x80546ab8 Void
+0x038 IDT : 0x8003f400 _KIDTENTRY
+0x03c GDT : 0x8003f000 _KGDTENTRY
+0x040 TSS : 0x80042000 _KTSS
+0x044 MajorVersion : 1
+0x046 MinorVersion : 1
+0x048 SetMember : 1
+0x04c StallScaleFactor : 0x64
+0x050 DebugActive : 0 ''
+0x051 Number : 0 ''
+0x052 Spare0 : 0 ''
+0x053 SecondLevelCacheAssociativity : 0 ''
+0x054 VdmAlert : 0
+0x058 KernelReserved : [14] 0
+0x090 SecondLevelCacheSize : 0
+0x094 HalReserved : [16] 0
+0x0d4 InterruptMode : 0
+0x0d8 Spare1 : 0 ''
+0x0dc KernelReserved2 : [17] 0
+0x120 PrcbData : _KPRCB
做了这些铺垫,你可以继续看答案了。注意,要求是分析一下执行流程,并不是把每一个细节逆向明白,大体知道怎么处理即可:
1️⃣ 分析IDT
表中0x2
号中断的执行流程。
🔒 点击查看答案 🔒
.text:004085B6 _KiTrap02 proc near ; DATA XREF: KiSystemStartup(x)+143↓o
.text:004085B6 ; INIT:005DD510↓o
.text:004085B6
.text:004085B6 var_8 = dword ptr -8
.text:004085B6 var_4 = dword ptr -4
.text:004085B6
.text:004085B6 cli ; 屏蔽可屏蔽中断,别随便打扰我
.text:004085B7 push dword ptr ds:0FFDFF040h ; fs:[40h] TSS
.text:004085BD mov eax, ds:0FFDFF03Ch ; fs:[3ch] GDT
.text:004085C2 mov ch, [eax+5Fh] ; eax = GDT
.text:004085C5 mov cl, [eax+5Ch]
.text:004085C8 shl ecx, 10h
.text:004085CB mov cx, [eax+5Ah] ; 利用 ecx 获取 8003f058 段描述符的首地址,值为 8054AF68 ,它是个 TSS 段描述符
.text:004085CF mov ds:0FFDFF040h, ecx ; fs:[40h],切换 TSS
.text:004085D5 pushf
.text:004085D6 and [esp+8+var_8], 11111111111111111011111111111111b
.text:004085DD popf ; 将 NT位 置0
.text:004085DE mov ecx, ds:0FFDFF03Ch ; fs:[3ch],GDT
.text:004085E4 lea eax, [ecx+58h] ; 获取现在使用的 TSS
.text:004085E7 mov byte ptr [eax+5], 89h ; 修改目前 TSS 的属性
.text:004085EB mov eax, [esp+4+var_4]
.text:004085EE push 0
.text:004085F0 push 0
.text:004085F2 push 0
.text:004085F4 push 0
.text:004085F6 push dword ptr [eax+50h]
.text:004085F9 push dword ptr [eax+38h]
.text:004085FC push dword ptr [eax+24h]
.text:004085FF push dword ptr [eax+4Ch]
.text:00408602 push dword ptr [eax+20h]
.text:00408605 push 0
.text:00408607 push dword ptr [eax+3Ch]
.text:0040860A push dword ptr [eax+34h]
.text:0040860D push dword ptr [eax+40h]
.text:00408610 push dword ptr [eax+44h]
.text:00408613 push dword ptr [eax+58h]
.text:00408616 push dword ptr ds:0FFDFF000h
.text:0040861C push 0FFFFFFFFh
.text:0040861E push dword ptr [eax+28h]
.text:00408621 push dword ptr [eax+2Ch]
.text:00408624 push dword ptr [eax+30h]
.text:00408627 push dword ptr [eax+54h]
.text:0040862A push dword ptr [eax+48h]
.text:0040862D push dword ptr [eax+5Ch]
.text:00408630 push 0
.text:00408632 push 0
.text:00408634 push 0
.text:00408636 push 0
.text:00408638 push 0
.text:0040863A push 0
.text:0040863C push 0
.text:0040863E push 0
.text:00408640 push 0
.text:00408642 push 0
.text:00408644 push dword ptr [eax+20h]
.text:00408647 push dword ptr [eax+3Ch]
.text:0040864A mov ebp, esp
.text:0040864C cmp ds:dword_47A2DC, 0
.text:00408653 jz short loc_40867D
.text:00408655 jmp short loc_408659
.text:00408657 ; ---------------------------------------------------------------------------
.text:00408657 jmp short loc_40867D
.text:00408659 ; ---------------------------------------------------------------------------
.text:00408659
.text:00408659 loc_408659: ; CODE XREF: _KiTrap02+9F↑j
.text:00408659 cmp ds:dword_47A2DC, 8
.text:00408660 jb short loc_40867D
.text:00408662 jnz short loc_40867B
.text:00408664 cmp ds:_KdDebuggerNotPresent, 0
.text:0040866B jnz short loc_40867B
.text:0040866D cmp ds:_KdDebuggerEnabled, 0
.text:00408674 jz short loc_40867B
.text:00408676 call _KeEnterKernelDebugger@0 ; KeEnterKernelDebugger()
.text:0040867B
.text:0040867B loc_40867B: ; CODE XREF: _KiTrap02+AC↑j
.text:0040867B ; _KiTrap02+B5↑j ...
.text:0040867B jmp short loc_40867B
.text:0040867D ; ---------------------------------------------------------------------------
.text:0040867D
.text:0040867D loc_40867D: ; CODE XREF: _KiTrap02+9D↑j
.text:0040867D ; _KiTrap02+A1↑j ...
.text:0040867D inc ds:dword_47A2DC
.text:00408683 push 0
.text:00408685 call ds:__imp__HalHandleNMI@4 ; HalHandleNMI(x)
.text:0040868B dec ds:dword_47A2DC
.text:00408691 jnz short loc_4086C6
.text:00408693 mov eax, ds:0FFDFF040h ; fs:[40h]
.text:00408698 cmp word ptr [eax], 58h ; 'X'
.text:0040869C jz short loc_4086C6
.text:0040869E add esp, 8Ch
.text:004086A4 pop dword ptr ds:0FFDFF040h ; fs:[40h]
.text:004086AA mov ecx, ds:0FFDFF03Ch ; fs:[3ch]
.text:004086B0 lea eax, [ecx+28h]
.text:004086B3 mov byte ptr [eax+5], 8Bh ; 修改 TSS 属性为 Busy
.text:004086B7 pushf
.text:004086B8 or [esp+4+var_4], 4000h
.text:004086BF popf ; 将 NT位 置1
.text:004086C0 iret ; TSS 返回
2️⃣ 分析IDT
表中0x8
号中断的执行流程。
🔒 点击查看答案 🔒
.text:0040969D _KiTrap08 proc near ; DATA XREF: KiSystemStartup(x)+CB↓o
.text:0040969D ; INIT:005DD540↓o
.text:0040969D
.text:0040969D var_4 = dword ptr -4
.text:0040969D
.text:0040969D cli ; 屏蔽可屏蔽中断
.text:0040969E mov ecx, ds:0FFDFF03Ch ; 取 GDT 首地址
.text:004096A4 lea eax, [ecx+50h] ; 取 TSS 段描述符地址,eax = 8003f050
.text:004096A7 mov byte ptr [eax+5], 89h ; 设置 TSS 属性
.text:004096AB pushf
.text:004096AC and [esp+4+var_4], 0FFFFBFFFh
.text:004096B3 popf ; 清空 NT位
.text:004096B4 mov eax, ds:0FFDFF03Ch ; 取 GDT 首地址
.text:004096B9 mov ch, [eax+57h]
.text:004096BC mov cl, [eax+54h]
.text:004096BF shl ecx, 10h
.text:004096C2 mov cx, [eax+52h] ; 取 TSS 段描述符指向的首地址
.text:004096C6 mov eax, ds:0FFDFF040h ; 将 TSS 备份给 eax
.text:004096CB mov ds:0FFDFF040h, ecx ; 切换 TSS
.text:004096D1
.text:004096D1 loc_4096D1: ; CODE XREF: .text:004096E1↓j
.text:004096D1 push 0
.text:004096D3 push 0
.text:004096D5 push 0
.text:004096D7 push eax ; 出错的 TSS
.text:004096D8 push 8
.text:004096DA push 7Fh
.text:004096DC call _KeBugCheck2@24 ; KeBugCheck2(x,x,x,x,x,x)
.text:004096DC _KiTrap08 endp
段/页/门
当你一步步做好练习学习,看到这一篇文章的时候,恭喜你,你的基础已经差不多。有关其他的保护模式的细节,自己就能独立研究了。学保护模式,真的不易。
在本篇章,我们讲解了什么是段,什么是页。经历过重重蓝屏的折磨,学到这你应该有比较深入的了解。段是第一道防线,主要是对权限的检查。页是最后一道防线,是对内存的进一步保护。如果想要在保护模式下读取内存中某地址存的内容,必须经历过段和页的双重考验才能成功。比如fs
的0地址为什么能访问,大于0xFFF
的地址没法访问,是由于段的限制。为什么代码段只能读不能写,这也是段的限制。还有之前我们讲过0地址,你即使有了0环的权限,也访问不了,这是因为没有挂正确的物理页。高2G的内存低权限访问不了,是由于页的限制。
我们开始学页的时候,首先是10-10-12
分页,再到后来的2-9-9-12
分页。它们的结构基本相似,不过多了一层嵌套,只是后者的支持的物理页更大更多了。可以说,分页的发展,依靠需求来推动。
门在保护模式的地位也是不低的。门也有很多种:调用门、中断门、陷阱门、任务门等等。门的主要作用是提权。在操作系统中,所有的代码实现都是在内核实现的,包括所谓的API
等等。
看来操作系统并不是全知全能的,它需要和CPU
搞好关系,成就出如此复杂的系统,当然别的硬件也是不可或缺。
深入PAE分页
在讲10-10-12
分页的时候,我们讲解了目录表基址和页表基址,也知道它们的用途。但我并没有在2-9-9-12
分页进行介绍,但是可以通过逆向分析的手段来进行。下面我们来对操作系统如何在2-9-9-12
分页模式下来挂物理页。
我们先从WinDbg
看看,我打开一个Notepad
,通过往常的方式查看它的PDPTT
,如下图的!dq 129001a0
,先看好四个成员,因为它仅有四个。
然后我们看看它的最后一项,查看它的成员,如下图的!dq 3b303000
。你就会惊奇地发现,前四项是一模一样的,只是属性不太一样。我们根据我们的发现画一个图:
我们可以说PDPTT
的第四个成员的PDT
的前四个成员就是PDPTT
的所有成员,然后我们通过逆向,用IDA
看看它的作用:
.text:00439980 ; BOOLEAN __stdcall MmIsAddressValid(PVOID VirtualAddress)
.text:00439980 public _MmIsAddressValid@4
.text:00439980 _MmIsAddressValid@4 proc near ; CODE XREF: IopIsAddressRangeValid(x,x)+2F↑p
.text:00439980 ; IopGetMaxValidMemorySize(x,x)+29↑p ...
.text:00439980
.text:00439980 PS = dword ptr -8
.text:00439980 HPDE = 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 = VirtualAddress
.text:0043998A push esi
.text:0043998B mov eax, ecx
.text:0043998D shr eax, 18
.text:00439990 mov esi, 11111111111000b
.text:00439995 and eax, esi ; esi = 11111111111000b
.text:00439997 sub eax, -0C0600000h ; 目录表基址
.text:0043999C mov edx, [eax] ; 取PDE的低四个字节
.text:0043999E mov eax, [eax+4] ; 取PDE的高四个字节
.text:004399A1 mov [ebp+HPDE], eax ; HPDE:PDE的高四个字节
.text:004399A4 mov eax, edx ; eax = LPDE(PDE的低四个字节)
.text:004399A6 push edi
.text:004399A7 and eax, 1 ; 得到PDE的P位
.text:004399AA xor edi, edi
.text:004399AC or eax, edi ; 判断P是否为1
.text:004399AE jz short loc_439A11 ; 如果无效则跳
.text:004399B0 mov edi, 10000000b ; edi = 10000000b
.text:004399B5 and edx, edi ; 判断PS位是否为1,即是否是大页
.text:004399B7 push 0
.text:004399B9 mov [ebp+PS], edx
.text:004399BC pop eax ; eax = 0
.text:004399BD jz short loc_4399C3
.text:004399BF test eax, eax
.text:004399C1 jz short loc_439A15 ; jmp
.text:004399C3
.text:004399C3 loc_4399C3: ; CODE XREF: MmIsAddressValid(x)+3D↑j
.text:004399C3 shr ecx, 9
.text:004399C6 and ecx, 7FFFF8h
.text:004399CC mov eax, [ecx+0C0000004h] ; 取PTE的高四个字节,0xC0000000+0x4
.text:004399D2 sub ecx, -0C0000000h ; ecx += 0xC0000000
.text:004399D8 mov edx, [ecx] ; 取PTE的低四个字节
.text:004399DA mov [ebp+HPDE], eax
.text:004399DD push ebx
.text:004399DE mov eax, edx ; eax = LPTE
.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 ; 如果PTE的P位无效跳走
.text:004399EA and edx, edi ; edi = 10000000b
.text:004399EC push 0
.text:004399EE mov [ebp+PS], edx
.text:004399F1 pop eax ; eax = 0
.text:004399F2 jz short loc_439A15
.text:004399F4 test eax, eax
.text:004399F6 jnz short loc_439A15 ; jmp
.text:004399F8 and ecx, esi
.text:004399FA mov ecx, [ecx+0C0600000h] ; PDE的低四个字节
.text:00439A00 mov eax, 10000001b
.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 ; jmp
.text:00439A0F jnz short loc_439A15
.text:00439A11
.text:00439A11 loc_439A11: ; CODE XREF: MmIsAddressValid(x)+2E↑j
.text:00439A11 ; MmIsAddressValid(x)+68↑j
.text:00439A11 xor al, al
.text:00439A13 jmp short loc_439A17
.text:00439A15 ; ---------------------------------------------------------------------------
.text:00439A15
.text:00439A15 loc_439A15: ; CODE XREF: MmIsAddressValid(x)+41↑j
.text:00439A15 ; MmIsAddressValid(x)+72↑j ...
.text:00439A15 mov al, 1
.text:00439A17
.text:00439A17 loc_439A17: ; CODE XREF: MmIsAddressValid(x)+93↑j
.text:00439A17 pop edi
.text:00439A18 pop esi
.text:00439A19 leave
.text:00439A1A retn 4
.text:00439A1A _MmIsAddressValid@4 endp
既然有10-10-12
分页的逆向经验,我们不难逆出它们。通过逆向结果:VirtualAddress
>> 18 + C0600000
就是指向的PDE
。也就是说,得出的索引是2^14
,最大值4000H
。0C0600000
就是第一个PDT
表的首地址,C0601000
是第二个PDT
表的首地址,C0602000
是第三个PDT
表的首地址,C0603000
是第四个PDT
表的首地址。
可能你不清楚PDPTE
的作用,我给你举个例子你就明白为什么我会有哪个线性地址就是第几个PDT
表了:我们在找PDE
的时候,我们只是去了后面的21位乘8个字节找的。
可以从上图看出PDPTI × 1000H + PDI × 8
,而一个页就是1000H
,说明它们是接壤的。这就是PDPTE
在寻找物理地址的作用。
既然知道它的结构了,那么代码就自己写吧,在练习与思考就有对应的题目。
练习与思考
1️⃣ 在2-9-9-12
分页模式下用代码实现给0地址挂物理页,不能用Windbg
挂,并验证TLB
的存在。
🔒 点击查看答案 🔒
此题目和之前的10-10-12
分页的题目差不多,答案差不多,效果也是一样的。
首先构造一个调用门:eq 8003f098 0040EC0000081250
,注意调用门和裸函数的地址一致。
有关实验是否成功的评判标准,在之前的类似的题目是一样的,我就不再赘述了。
🔒 点击查看代码 🔒
#include "stdafx.h"
#include <iostream>
#include <windows.h>
int isinv=0;
int num1=0;
int num2=0;
void __declspec(naked) callgate()
{
_asm
{
push 0x30;
pop fs;
pushad;
pushfd;
mov edi,0xC0000000;
mov eax,0x10000;
shr eax,9;
add eax,edi;
mov edx,dword ptr ds:[eax];
mov dword ptr ds:[edi],edx;
add eax,4;
mov edx,dword ptr ds:[eax];
mov dword ptr ds:[edi+4],edx;
mov edx,dword ptr ds:[0];
mov [num1],edx;
mov eax,isinv ;
test eax,eax;
jz end;
invlpg dword ptr ds:[0];
end:
mov eax,0x20000;
shr eax,9;
add eax,edi;
mov edx,dword ptr ds:[eax];
mov dword ptr ds:[edi],edx;
add eax,4;
mov edx,dword ptr ds:[eax];
mov dword ptr ds:[edi+4],edx;
mov edx,dword ptr ds:[0];
mov [num2],edx;
popfd;
popad;
retf;
}
}
int main(int argc, char* argv[])
{
LPVOID page1 = VirtualAlloc((LPVOID)0x10000,0x1000,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
LPVOID page2 = VirtualAlloc((LPVOID)0x20000,0x1000,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
if (!page1||!page2)
{
puts("分配内存失败!!!");
VirtualFree(page1,0,MEM_FREE);
VirtualFree(page2,0,MEM_FREE);
system("pause");
return 0;
}
*(int*)page1 = 0x12345;
*(int*)page2 = 0x67890;
puts("是否清理缓存?");
scanf("%d",&isinv);
const char buffer[6]={0,0,0,0,0x9B,0};
_asm
{
push fs;
call fword ptr [buffer];
pop fs;
}
printf("第一次挂页的值:%x\n换页后的值:%x\n",num1,num2);
VirtualFree(page1,0,MEM_FREE);
VirtualFree(page2,0,MEM_FREE);
system("pause");
return 0;
}
2️⃣ 在VirtualBox
的XP虚拟机中我以2-9-9-12
分页进入操作系统,但是,结果通过PCHunter
和WinDbg
发现,还是10-10-12
分页的分页模式,这是为什么呢?
🔒 点击查看答案 🔒
配置虚拟机的时候,是否启用了 PAE/NX 这个选项了吗?
结语
到此,保护模式篇就结束了。仔细复习一下之前学过的东西。下一步我们将要踏入下一个篇章。学保护模式的时候我们蹒跚学步,到后来就可以小跑进行了。后面的教程我会根据我的空余时间,加快更文的速度。
下一篇
本文来自博客园,作者:寂静的羽夏 ,一个热爱计算机技术的菜鸟
转载请注明原文链接:https://www.cnblogs.com/wingsummer/p/15364654.html