保護模式篇——總結與提升

寂靜的羽夏發表於2021-10-31

寫在前面

  此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統核心的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏看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,最大值4000H0C0600000就是第一個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分頁進入作業系統,但是,結果通過PCHunterWinDbg發現,還是10-10-12分頁的分頁模式,這是為什麼呢?

? 點選檢視答案 ?
配置虛擬機器的時候,是否啟用了 PAE/NX 這個選項了嗎?

結語

  到此,保護模式篇就結束了。仔細複習一下之前學過的東西。下一步我們將要踏入下一個篇章。學保護模式的時候我們蹣跚學步,到後來就可以小跑進行了。後面的教程我會根據我的空餘時間,加快更文的速度。

下一篇

  羽夏看Win系統核心——驅動篇

相關文章