從DbgView探究Windows內存管理筆記

0x00 前言

  講內存管理單純的理論比較空洞,所以本文從探究DbgVeiw的內存分佈開始,來探究windows系統的內存管理,討論malloc和VirtualAlloc的差別,和缺頁異常。

參考文章

https://blog.csdn.net/weixin_42052102/article/details/83757538

https://blog.csdn.net/weixin_42052102/article/details/83722047

https://blog.csdn.net/weixin_42052102/article/details/83751896

0x01 線性地址的管理

首先在虛擬機中打開一個.exe的文件,這裏選擇了DbgView,之後到Windbg裏查看對應的進程

 

看一下_KPROCESS結構體

在0x11c的位置的VadRoot處是一個搜索二叉樹的入口點,每一個節點都記錄被佔用的線性地址空間,每一個節點的結構,都是_MMVAD結構體

接下來,先手動找一下這個搜索二叉樹的節點  

觀察一下1)Parent:因爲是根節點所以沒有父節點了

               2)LeftChild / RightChild:左子樹/右子樹,一個是往左邊拓展的線,一個是往右邊拓展

重複這一過程就可以找到所有的節點選擇二叉樹而並非鏈表是因爲搜索二叉樹查找的效率比鏈表高很多

               3)StartingVpn && EndingVpn: 這兩個值是以頁(4kb)爲單位的,所以各把這兩個數後面添上3個0,就是當前節點的起始位置與結束位置,也此兩者之間就是已經被佔用的位置,所以想找到那些已被佔用和未被佔用的線性地址,遍歷這個二叉樹就可以了。

             4)ControlArea:看一下結構體

     其中的FilePointer如果其中的值是NULL則線性地址對應的是真正的物理頁

 

    而如果指向一個_FILE_OBJECT(如上圖),那線性地址對應的是某文件影射的內存,我們繼續跟下去

         5)_MMVAD+0x14成員u,我們不妨參考一下 ReactOS 裏的說明


union {
   ULONG_PTR   LongFlags
 
   MMVAD_FLAGS   VadFlags
 
} u

主要使用的是MMVAD_FLAGS這個結構,跟進這個結構(和實際windows還是有一定差別的,但是主要成員沒問題,其中Protection的偏移爲0x14)

typedef struct _MMVAD_FLAGS
{
    ULONG_PTR CommitCharge:19
    ULONG_PTR NoChange:1
    ULONG_PTR VadType:3
    ULONG_PTR MemCommit:1
    ULONG_PTR Protection:5//規定文件屬性
                          // 1 READONLY  2  EXECUTE  3  EXECUTE _READ  4 READWITER  
	                  // 5 WRITECOPY  6  EXECUTE _READWITER   7 EXECUTE_WRITECOPY  

    ULONG_PTR Spare:2
    ULONG_PTR PrivateMemory:1//是實際物理頁(1:private)還是文件映射(0:mapped)
} MMVAD_FLAGS,*PMMVAD_FLAGS;

 

遍歷所有節點,在windbg裏也存在這樣的指令! vad 0x89473968(這個是根節點的線性地址)

一下列舉4種不同的情況

1. 其中的Level就是二叉樹中的層級,

2. start和end 後面再加3個0(因爲單位爲4kb)就是每個節點線性地址的起始與終點位置

3.Private是指線性地址對應真實物理頁(由),Mapped是指線性地址對應的是某文件影射的內存

4.文件的屬性就是由上訴u裏的MMVAD_FLAGS->Protection確定的,其中EXECUTE_WRITECOPY(寫拷貝)這一情況下文會單獨說明

如果要把DLL模塊隱藏的話,不止要斷TEB,PEB的各種的鏈,vad這塊也得想辦法繞過去,如果直接刪除的話一定會造成系統的不穩定,所以可以VAD樹隱藏一種是使 _MMVAD.StartingVpn=_MMVAD.EndingVpn,達到隱藏的效果,第二種可以將兩個VAD結點融合達到隱藏效果,即p1.EndingVpn = p2.EndingVpn

0x01 Private Memory

  Private Memory,由上文可知線性地址對應的是實際物理頁。具有Private Memory性質的線性地址是系統或者用戶通過VirtualAlloc這個函數申請到的,那爲什麼叫Private?大概是因爲VirtualAlloc申請的大小,是以物理頁爲單位的,當前線性地址獨享整個物理頁。

   在我們寫 C或C++時申請內存時常用的是 malloc(C)或new(C++),那與上段的 VirtualAlloc有是什麼分別?

(因爲new的底層實現就是malloc,所以不討論new)以下分別說明:

VirtualAlloc:

首先看一下原型:

LPVOID VirtualAlloc
{
	LPVOID lpAddress, 	// 要分配的內存區域的地址
	DWORD dwSize, 		// 分配的大小以物理頁爲單位
	DWORD flAllocationType, // 分配的類型
	DWORD flProtect 	// 該內存的初始保護屬性
};

1. lpAddress:不能再二叉樹節點的StartingVpn && EndingVpn之間申請,要在節與節之間申請,如果沒有特殊的需要通常填空

2. dwSize:以物理頁(4KB)爲單位

3.flAllocationType:1.MEM_RESERVE :保留指定線性地址空間,不分配物理內存

                                2.MEM_COMMIT :爲指定線性地址地址空間提交物理內存

這裏多說以下,即使是commit時,剛剛申請下來,後也不會馬上分物理頁,而是等到真正使用到它的時候再使用物理頁,下文缺頁異常處會詳細說明)

 

下面我們自己用VirtualAlloc申請一下內存:

#include <stdio.h>
#include <windows.h>

LPVOID lpAddress;

int main(int argc, char* argv[])
{
	printf("還沒有申請");
	getchar();

	lpAddress = VirtualAlloc(NULL, 0X1000, MEM_COMMIT,PAGE_READWRITE);//申請一個物理頁 COMMIT 可讀可寫

	printf("address:%x \n", lpAddress);
        getchar();
	return 0;
}

在還沒申請的時候看一下對應進程的vad

繼續運行:再看一下分佈:

 

可以看到在3a0的地方多了我們申請的節點,一個物理頁3a0,commit是1,如果是reserve的話commit就是0。

malloc 

malloc是在堆裏申請內存,實際上,是系統提前利用VirtualAlloc申請了一塊很大的內存,malloc是在已經分配好的內存裏,拿出一部分來用,VirtualAlloc就像學校到出版社批發一大堆教材,批發就必須有最低單位(物理頁4kb),而malloc就像學生從學校批發來的教材中零散的選擇自己想要的教材,沒有最小的限制。

同樣來驗證一下:

int main(int argc, char* argv[])
{
        printf("未開始分配");
        getchar();

	int x = 0x12345678;
        printf("棧:%x \n",&x);
        getchar();

	int* y = (int*)malloc(sizeof(int)*128);
	printf("堆:%x \n",y);
        getchar();

	return 0;
}

(可以逆一下malloc這個函數,會發現這個函數沒有進入0環,也可以看出,malloc實際沒有分配真正的內存)

運行,首先是棧:

運行,堆:

結果都如下圖:

沒有任何變化

堆在380~38f之間在節內

棧是從大到小的,理論上在30~12f之內。

驗證完畢。

0x02 Mapped Memory

上文介紹了線性地址的其中一種(Private),這裏來介紹線性地址的第二種mapped,Mapped性質的線性地址對應的是某文件影射的內存,這種線性地址不像Private(堆,棧...)獨享物理頁,而是同一物理頁同時映射在不同的進程中,系統通過CreateFileMapping來申請

mapped性質的線性地址細分爲兩種:

一種是多個進程共享物理頁

另一種是多個進程共享文件(物理頁只有一份,物理上的文件映射到多個進程)

圖解(嫖的):

看一下函數原型CreateFileMapping

HANDLE WINAPI CreateFileMapping(
_In_HANDLE hFile,//第一個參數如果不添則準備物理頁,
                 //如果準備文件句柄,則不止準備物理頁,還把文件與物理頁關聯上
_In_opt_LPSECURITY_ATTRIBUTES lpAttributes,
_In_DWORD flProtect,
_In_DWORD dwMaximumSizeHigh,
_In_DWORD dwMaximumSizeLow,
_In_opt_LPCTSTR lpName);

下面咱們自己實現以下:

以下是text1.exe(第一個進程)

#include <stdio.h>
#include <windows.h>

#define FileName "The_Shared_File"

int main(int argc, char* argv[])
{
	//創建文件或物理頁	 我這裏創建的是物理頁
	HANDLE hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,BUFSIZ,FileName);

	//將物理頁與線性地址進行映射			
	LPTSTR lpBuff = (LPTSTR)MapViewOfFile(hMapFile,FILE_MAP_ALL_ACCESS,0,0,BUFSIZ);	
	
	*(PDWORD)lpBuff = 0x12345678;
				
	printf("%p: %x",lpBuff, *(PDWORD)lpBuff);

	getchar();
	return 0;
}

運行:

到windbg裏看一下

 

我們在另起一個進程來讀取這個進程的內容

以下是text2.exe(第二個進程)

#include <stdio.h>
#include <windows.h>

#define FileName "The_Shared_File"

int main(int argc, char* argv[])
{
	//打開相同的對象
	HANDLE hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, FileName);

	//將物理頁與線性地址進行映射			
	LPTSTR lpBuff = (LPTSTR)MapViewOfFile(hMapFile,FILE_MAP_ALL_ACCESS,0,0,BUFSIZ);	
				
	printf("進程2讀取到的值: %x",*(PDWORD)lpBuff);

	getchar();
	return 0;
}

運行,成功得到共享物理頁的值

上面說的是共享物理頁,共享文件和共享物理頁相似,下面把共享文件的腳本放一下:

進程1:

#include <stdio.h>
#include <windows.h>

int main(int argc, char* argv[])
{
	HANDLE hFile = CreateFile("C:\\NOTEPAD.EXE",GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);

	HANDLE hMapFile = CreateFileMapping(hFile,NULL,PAGE_READWRITE,0, BUFSIZ,NULL);
	
	LPTSTR lpBuff = (LPTSTR)MapViewOfFile(hMapFile,FILE_MAP_ALL_ACCESS,0,0,BUFSIZ);	
	
	printf("共享文件的線形地址:%p",lpBuff);

	getchar();
	return 0;
}

vad裏的情況:

但是,可以發現vad裏還有如下這種形式

寫拷貝

在此詳細講解

寫拷貝的類型是由LoadLibrary來映射的,其目的是怕影響到其他進程,以上圖爲例,如果我們在ntdll裏掛鉤子,去修改的話,如果不是寫拷貝,就會使所有ntdll映射的進程中ntdll改變,造成系統對不穩定,而寫拷貝的話,會把ntdll映射到一份新的物理頁,即使修改也是修改新物理頁的ntdll(副本),和原來物理頁的ntdll無關。

腳本:

#include <stdio.h>
#include <windows.h>

int main(int argc, char* argv[])
{
	HMODULE hModule = LoadLibrary("C:\\NOTEPAD.EXE");
    
	getchar();
	return 0;
}

vad的情況就是寫拷貝。

線性地址的管理的討論到此爲止,下面是物理內存的管理(要區分開)。

0x03 物理內存管理

物理內存的總物理頁數可以在windbg裏用MmNumberOfPhysicalPages這個全局變量查看

這個全局變量的單位是物理頁(4KB)所以,總共的大小MmNumberOfPhysicalPages* 4

其中每個物理頁都對應一個結構體來描述,_MMPFN,它的大小不同系統不一定大,筆者這個的大小是0x18(0x14+4)。

而這些結構體都裝在一個大的結構體數組中,此數組的起始位置由MmPfnDatabase查看:

正因爲是結構體數組,所以在_MMPFN裏沒有必要描述物理頁的指針,假如要查看第4個物理頁的指針,

80C43000(MmPfnDatabase)  + 18h(sizeof(_MMPFN)) * 3,就可以找到。

物理頁的狀態

既然是物理頁的信息,肯定要從_MMPFN結構體入手,這個結構體非常喪心病狂,我們只看和物理頁狀態相關的。

​​​​union {
   PFN_NUMBER   Flink
 
   ULONG   WsIndex
 
   PKEVENT   Event
 
   NTSTATUS   ReadStatus
 
   SINGLE_LIST_ENTRY   NextStackPfn
 
   SWAPENTRY   SwapEntry
 
} u1 
PMMPTE PteAddress
 
union {
   PFN_NUMBER   Blink
 
   ULONG_PTR   ShareCount
 
} u2
 
union {
   struct {
      USHORT   ReferenceCount
 
      MMPFNENTRY   e1
 
   } 

 
   struct {
      USHORT   ReferenceCount
 
      USHORT   ShortFlags
 
   }   e2
 
} u3
 
union {
   MMPTE   OriginalPte
 
   LONG   AweReferenceCount
 
   PMM_RMAP_ENTRY   RmapListHead
 
}; 

 
union {
   ULONG_PTR   EntireFrame
 
   struct {
      ULONG_PTR   PteFrame:25
 
      ULONG_PTR   InPageError:1
 
      ULONG_PTR   VerifierAllocation:1
 
      ULONG_PTR   AweAllocation:1
 
      ULONG_PTR   Priority:3
 
      ULONG_PTR   MustBeCached:1
 
   } 

 
} u4 
MMWSLE Wsle

看以下 u3的e1的結構MMPFNENTRY:

typedef struct _MMPFNENTRY
{
     USHORT Modified:1
     USHORT ReadInProgress:1
     USHORT WriteInProgress:1
     USHORT PrototypePte:1
     USHORT PageColor:4
     USHORT PageLocation:3//物理頁的狀態
                          //0:MmZeroedPageListHead
                          //1:MmFreePageListHead
                          //2:MmStandbyPageListHead
                          //3:MmModifiedPageListHead
                          //4:MmModifiedNoWritePageListHead
                          //5:MmBadPageListHead
     USHORT RemovalRequested:1
     USHORT CacheAttribute:2
     USHORT Rom:1
     USHORT ParityError:1
}

windows會把上述6種不同狀態的物理頁分別串成6個鏈表(具體解釋其中4個)

<1> MmBadPageListHead

壞鏈

<2> MmZeroedPageListHead

零化鏈表(是系統在空閒的時候進行零化的,不是程序自己清零的那種)

<3> MmFreePageListHead

空閒鏈表(物理頁是週轉使用的,剛被釋放的物理頁是沒有清0,系統空閒的時候有專門的線程從這個隊列摘取物理頁,加以清0後再掛入MmZeroedPageListHead)

<4> MmStandbyPageListHead

備用鏈表(當系統內存不夠的時候,操作系統會把物理內存中的數據交換到硬盤上,此時頁面不是直接掛到空閒鏈表上去,而是掛到備用鏈表上,雖然我釋放了,但裏邊的內容還是有意義的) 

 

ok,我們來跟一下其中一個雙向鏈表(筆者這裏選擇零化鏈表),來探究結構

第3,4成員是前者是前一物理頁的索引,向前遍歷,後者是是後一物理頁的索引,向後遍歷,筆者這裏往前走了(具體結構到ReactOS裏看一下,筆者寫不動了)

那如何找到具體的位置呢,用上文的公式80C43000(MmPfnDatabase)  + 18h(sizeof(_MMPFN)) * n

重複上一過程:

發現此刻鏈表的第3個成員剛好是第一個前節點的索引

總結個圖:

最後我們來找一下某進程的所有物理頁

還是用DbgView爲例先找到DbgView對應的_EPROCESS結構體

在+1f8的位置找到VM

進入_MMSUPPORT

其中遍歷VmWorkingSetList就可以找到所有物理頁了。

還有很多細節可以參考《Windows內核管理與實現》

0x04 缺頁異常

PTE的結構:

雖然有2-2-9-12分頁和10-10-12分頁,但是在屬性這塊沒有太大差別

P位判斷當前頁面是否有效,當p位爲1則有對應物理頁,爲0則沒有物理頁

當CPU訪問某地址,發現P位爲0,則發生缺頁異常

正是不斷進行的缺頁異常大大提高了內存的使用效率,windows只把當前需要使用的線性地址掛上物理頁,而暫時不使用的線性地址,windows會把對應物理頁的數據轉移到硬盤上。

虛擬內存

可以看一下虛擬機的虛擬內存

1.右擊“我的電腦”,選擇“屬性”。點擊“高級”選擇“性能”→“設置”

2、打開“性能選項”,選擇“虛擬內存”就可以改了

把上述的虛擬內存改了之後,在c盤根目錄下會生成一個pagefile.sys文件

這個文件的大小就是虛擬內存的大小。

大體上:當我們的物理頁佔的差不多的時候,windows會把不用的線性地址A的物理頁的數據放到pagefile.sys文件文件中,再把這個物理頁給別的正在使用的線性地址B使用,使用後並不會釋放,如果地址再次訪問線性地址A的時候,A此時沒有對應的物理頁,就會觸發缺頁異常。

 

 

細節:

圖a:位於頁面文件中
當你的線性地址的物理頁被存放到頁面文件(pagefile.sys)中的時候,你的PTE就會變成這樣
CPU訪問了這地址你的p:0,進入缺頁異常處理程序,缺頁異常位於idt表裏的E號中斷。
進入缺頁異常處理它會回頭查看你的這個PTE,這時地址時發現你的1-9位,12-31位都是有值的,它就會知道你這個線性地址是有效的,只不過數據放到頁面文件裏了
異常處理程序會到pagefile.sys中,按照PTE上描述的頁面文件偏移將頁的數據取出來掛到一個新的物理頁上,將12-31位改爲新的物理頁地址,再將p=1
圖b:要求0頁面
頁面尚未分配,下次訪問時請求一個0頁面
圖c:轉移
頁面在物理內存中,但已被轉移到某個物理頁面鏈表中,可以通過查詢_MMPFN數據庫獲取實際情況
圖d:
缺頁異常處理髮現你PTE全部爲0就會去查VAD,發現這個線性地址已經分配了它會幫你把物理頁掛上,如果這個線性地址沒有分配就會報0xC0000005。(具體看 “保留與提交的誤區” )

保留與提交

詳情見這位大佬的文章

使用VirtualAlloc申請時,並不會直接掛上物理頁,只有真正使用的時候纔會,上文在vad中顯示的commit只是說保留了物理頁,但比一定掛上。

0x05 總結

本文是我探究內存管理的過程,主要跟着My classment這位博主來的,記錄了一些我在學習時的情況,實踐比較多,如果有什麼問題,望路過的大佬斧正。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章