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這位博主來的,記錄了一些我在學習時的情況,實踐比較多,如果有什麼問題,望路過的大佬斧正。