寫在前面
此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統核心的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看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 這個選項了嗎?
結語
到此,保護模式篇就結束了。仔細複習一下之前學過的東西。下一步我們將要踏入下一個篇章。學保護模式的時候我們蹣跚學步,到後來就可以小跑進行了。後面的教程我會根據我的空餘時間,加快更文的速度。
下一篇
羽夏看Win系統核心——驅動篇