Windows安全 之 內存映射文件(Section對像)MmCreateSection與MmMapViewOfSection 流程分析



MmCreateSection

內存區(Section)內核對像有兩種,但都是基於分頁內存的:
一種是基於頁面文件的
一種是基於其他文件,以文件空間作為基礎的 如EXE在硬盤上的空間

MmCreateSection主要目的:
1. 填充CONTROL_AREA對像
2. 填充SEGMENT對像
3. 調用ObCreateObject創建Section內核對像
4. 返回Section內核對像


SEGMENT是真正描述映射區域的對像 , 內核函數MmCreateSection中有三種創建方式:
1. MiCreatePagingFileMap //以PageFile(頁面文件)為基礎的文件共享
2. MiCreateImageFileMap //以可執行映像作文件共享
3. MiCreateDataFileMap //以普通數據文件作為基
p.s. 後兩者雖表面內核文件對像, 從函數內部邏輯可發現, 如果映射同一個文件時, Section對像會共用同一個Segment對像

由於Section對像的創建完全是建基於SEGMENT對像, 因此探討SEGMENT對像才是正確方向 了解共享實現

SEGMENT(段對像,分配在分頁內存):
1. 分配的共享空間頁面總數量大小的MMPTE陣列緊隨著SEGMENT對像(用於建立映射視圖時)
2. 指向CONTROL_AREA指針(CONTROL_AREA, 分配在非分頁內存,即物理內存, 它亦會指向SEGMENT對像, 建立互指的關係)
3. 記錄對應節中的所有頁面的數量
4. 全部MMPTE最終指向節的起始地址

建數內部會再次創建真正的:

CONTROL_AREA(控制區對象,分配在分頁內存)
1. 指向文件對像指針
2. 尾隨著多個SUBSECTION對像 (指PE中各個節的起始地址,使用Segment的MMPTE進行初始化->再讓VAD中會使用SubSection)
3. SUBSECTION形成鏈表,其中一個指針會指回CONTROL_DATA

SubSection結構如下:
typedef struct _SUBSECTION
{
     PCONTROL_AREA ControlArea;
     PMMPTE SubsectionBase;
     PSUBSECTION NextSubsection;
     ULONG PtesInSubsection;
     union
     {
          ULONG UnusedPtes;
          PMM_AVL_TABLE GlobalPerSessionHead;
     };
     ULONG u;
     ULONG StartingSector;
     ULONG NumberOfFullSectors;
} SUBSECTION, *PSUBSECTION;
文件內核對像
1. 其中一個域指向SECTION_OBJECT_POINTERS結構,包含兩個成員DataSectionObject和ImageSectionObject
2. 他們同時指向CONTROL_AREA對像


以上的數據結構是為映射到內存而初始化,及創建

對於不同類型的Section對象,Windows使用不同方法進行映射供上層程式使用

MmCreateSection的三類共享文件的初始化:

如頁面文件類別(MiCreatePagingFile):
1.判斷如果MmCreateSection的fileobject與filehandle為NULL
2.使用MiCreatePagingFile創建SEGMENT對像及PTE隊列(分配在分頁內存)
3.MiCreatePagingFile內部CONTROL_AREA及一個SUBSECTION對象 (分配在非分頁內存,即物理內存)
4.MiCreatePagingFile內部初始化所有剛分配好的對象(注: 原型PTE隊列的分配類別如為SEC_LARGE_PAGES 則直接分配大頁面物理內存)
5.並返回後由MmCreateSection填充到Section


由於以上這類別完全不涉及內核文件對像 整體操作比較簡單

如映像文件類別(MiCreateImageFileMap): 如果AllocationAttributes 為SEC_IMAGE
1. 如果文件對像本身沒有CONTROL_AREA對像和SEGMENT對像
2. MmCreateSection會首先調用MiInsertImageSectionObject添加新創建的CONTROL_AREA對像與SEGMENT對像
3. 進入MiInsertImageSectionObject後他會把新創建的CONTROL_DATA->Section指針指向FileObject->ImageSectionObject(這個只是暫時占位用途)
4. 調用MiCreateImageFileMap(假如第一次被映射 以上步驟才會執行)
5. 同樣地把SEGMENT->ControlArea 指向 FileObject->ImageSectionObject

MiCreateImageFileMap內部:
1. 首先讀取PE頭,計算多少個頁面需要保留
2. 創建真正CONTROL_AREA對像(非分頁內存池) 以及 SubSection對像
3. 創建SEGMENT對像(分頁內存池) 以及 MMPTE陣列(大小為頁面總和,如10頁就有10個MMPTE元素)
4. CONTROL_AREA對像附帶多個SUBSECTION對像 (等如PE文件中有多少個段 就有多少個SUBSECTION對像)
5. SEGMENT對像則附帶PTE陣列,用於描述整個映像的大小(大小為頁面總和,如10頁就有10個MMPTE元素)
6. 利用PE文件填充CONTROL_AREA對像及SEGMENT對像

P.S PTE陣列中存放著映像中所有頁面的信息 分配如下

SizeOfSegment = sizeof(SEGMENT) + (sizeof(MMPTE) * ((ULONG)NumberOfPtes - 1)) +  
                   sizeof(SECTION_IMAGE_INFORMATION);  
  
   //注意這裡是分頁內存 即虛擬內存啊
   NewSegment = ExAllocatePoolWithTag (PagedPool | POOL_MM_ALLOCATION,  
                                       SizeOfSegment,  
                                       MMSECT);  
   *Segment = NewSegment;  
  
   RtlZeroMemory (NewSegment, sizeof(SEGMENT));  
//MMPTE結構如下, 記錄頁面在文件中偏移
 typedef struct _MMPTE
 {
     ULONG u;
 } MMPTE, *PMMPTE;
即使以上對於內存的分配及初始化已經完成, 但進程仍然不能訪問內存, 因為即使分配了,進程沒有方法透過Section對像訪問內存
因此需要透過Section對像,進行映射,與進程中某虛擬空間進行映射



----------------------------------------------------------------------
NtMapViewOfSection->MmMapViewOfSection
它按Section類型調用下一層->
1. MiMapViewOfDataSection/
2. MiMapViewOfPhysicalSection/
3. MiMapViewOfImageSection

可以看得出也是三種映射方式 跟segment類別一致

三類映射函數的實現:
MiMapViewOfDataSection:
1. 函數首先確定內存區對像在目標進程中的地址範圍
2. 調用MiFindEmptyAddressRangeDown / MiFindEmptyAddressRange 在進程空間中尋找一段能包含大小的地址範圍
3. 申請VAD對像,初始化內存中所有開銷, 初始化成員指向傳入的Section->ControlArea 最後把VAD插入到進程的VAR AVL樹
4. 此時進程的VAD樹便多了一個內存描述

MiMapViewOfImageSection:
1. 函數首先確定基地址(由參數CapturedBase/ Section->Segment->BasedAddress )
2. 若基地址已被占用, 則調用MiFindEmptyAddressRangeDown / MiFindEmptyAddressRange 找到空閒地址, 大小為整個映像文件的大小
3. 申請VAD對像,初始化內存中所有開銷, 初始化成員指向傳入的Section->ControlArea 最後把VAD插入到進程的VAR AVL樹
4. 此時進程的VAD樹便多了一個內存描述

//我們首先回顧一下這個片段:
   //我們了解一下MiCreateImageFileMap的函數, 可以得出以下代碼..
   //直到看到這段代碼, 就明白到PTE陣列 與 磁盤之間的關係, 
   //陣列中每一個PTE都對應文件中的每一頁, 好了可以往下再看看MmMapViewOfSection的代碼
   //
   //獲取第一個節

   //MiCreateImageFileMap的代碼

        Subsection = (PSUBSECTION)(ControlArea + 1);
  //獲取第一個節
        NewSubsection = (PSUBSECTION)(LargeControlArea + 1);
 
  //遍歷所有節
        for (i = 0; i < SubsectionsAllocated; i += 1) {
   //把節複製
            RtlCopyMemory (NewSubsection, Subsection, sizeof(SUBSECTION));
   
   //設置ControlArea
            NewSubsection->ControlArea = (PCONTROL_AREA) LargeControlArea;
   //下一個節
            NewSubsection->NextSubsection = (NewSubsection + 1);

   //獲取指向文件中的第一個頁面
            PointerPte = NewSegment->PrototypePte;

   //
            TempPte.u.Long = MiGetSubsectionAddressForPte (NewSubsection);
            TempPte.u.Soft.Prototype = 1;

   
   //遍歷所有Ptes
            for (j = 0; j < NewSegment->TotalNumberOfPtes; j += 1) {

                ASSERT (PointerPte->u.Hard.Valid == 0);

                if ((PointerPte->u.Soft.Prototype == 1) &&
                    (MiGetSubsectionAddress (PointerPte) == Subsection)) {

                    OriginalProtection = MI_GET_PROTECTION_FROM_SOFT_PTE (PointerPte);

                    TempPte.u.Soft.Protection = OriginalProtection;
     //讓CONTROL上的所有原型PET都變成指向文件的原型PTE
                    MI_WRITE_INVALID_PTE (PointerPte, TempPte);
                }

                PointerPte += 1;
            }

            Subsection += 1;
            NewSubsection += 1;
        }

------------------------------------------------------------------------------------------------

 //MmMapViewOfSection的代碼節錄:

   if (Process->VmTopDown == 1) {

       if (ZeroBits != 0) {
        HighestUserAddress = (PVOID)((ULONG_PTR)MM_USER_ADDRESS_RANGE_LIMIT >> ZeroBits);
            if (HighestUserAddress > MM_HIGHEST_VAD_ADDRESS) {
                  HighestUserAddress = MM_HIGHEST_VAD_ADDRESS;
                }
            }
           else {
                HighestUserAddress = MM_HIGHEST_VAD_ADDRESS;
            }

          Status = MiFindEmptyAddressRangeDown (&Process->VadRoot,  // VAD根節點 , 遍歷所有已分配內存
                                                 NeededViewSize,    // 需要空間
                                                 HighestUserAddress,// 用戶空間
                                                 X64K,          
                                                 &StartingAddress   // 返回空間地址
                                               );
       }
       else {
            Status = MiFindEmptyAddressRange (NeededViewSize,      //大致同上
                                              X64K,
                                              (ULONG)ZeroBits,
                                              &StartingAddress);
       }
    LargeStartingAddress = StartingAddress;
    LargeEndingAddress = EndingAddress;

AllocateVad:

    //....省略
    Vad = ExAllocatePoolWithTag (NonPagedPool, sizeof(MMVAD), MMVADKEY);  
    //....省略
    RtlZeroMemory (Vad, sizeof(MMVAD));  

    //設置VAD成員為空閒塊空間
    Vad->StartingVpn = MI_VA_TO_VPN (LargeStartingAddress);  
    Vad->EndingVpn = MI_VA_TO_VPN (LargeEndingAddress);  
   
    SectionOffset->LowPart = SectionOffset->LowPart & ~(X64K - 1);
    
    //大整數低32位 SectionOffset為映射虛擬地址 , 虛擬地址右移12位 得到PFN 調試中用!search指令可觀察變化
    PteOffset = (ULONG)(SectionOffset->QuadPart >> PAGE_SHIFT);

    //這裡就把PTE 這裡的PTE是指向文件中每一個節的偏移, 然後設置到VAD
    Vad->FirstPrototypePte = &Subsection->SubsectionBase[PteOffset];  
    Vad->LastContiguousPte = MM_ALLOCATION_FILLS_VAD;  

   



   //來到這裡應該明白到在MiCreateImageFileMap中經過一大輪工作後 出來的陣列是有什麼用了吧??
   //把PTE陣列的元素添加至VAD 最大原因是因為缺頁異常發生時, 內核的內存管理器會首先遍歷VAD,如果發現可以換頁 則換入分頁頁面到物理內存 解決問題
   //這樣一來已經完全解決了所有問題, 進程只需要知道哪個範圍內的領空屬於這個文件, 那進程就可以隨時訪問他, 讓他產生缺頁異常, 由系統自動修補
   //這樣的話,內存就不需要加載文件的所有頁面, 而且所有進程指向的都是文件頁面, 因而達致共享效果

 

P.S. 缺頁異常指: 訪問的虛擬地址所映射的空間不在物理內存, 因此發生中斷

以下提供一張全景分析圖:

留言

張貼留言

這個網誌中的熱門文章

How does Nested-Virtualization works?

Processor micro-architecture internals (branch prediction, branch predictor and indirect branch)

Window安全之PE結構(一)詳解DOS頭