標籤:
在注入到某進程中對Ntdll下EAT鉤子的時候範圍僅僅只是當前進程,可是明明所有進程的Ntdll模組全是映射的同一個啊。原來Windows支援一種機制,允許兩個或兩個以上的進程共用同一Block Storage器。不過作業系統會給共用的儲存頁指定寫時複製屬性,當有個進程想修改一個共用頁面時,作業系統會從記憶體中找到一個閑置頁面並將修改的頁面內容複寫到這個閑置頁面上,再將虛擬位址空間和這個新的頁面映射上。那麼只需在下鉤前先想辦法消掉頁面的寫時複製屬性,再hook ntdll的時候就能在Ring3下實作類別似Ring0鉤子的效果。
本文先來分析下X86下的實現。
首先我們瞭解下X86下虛擬位址到物理地址的轉換,關於這個網上有很多分析資料以及代碼實現。
祭出一張經典老圖。
,X86下記憶體管理採用的是二級頁表結構,頁表是連續的,可以看成一維數組,數組每一項的大小是幾位元組了?回答是不一定,在32位是4位元組,64位是8位元組,32位中還有一種也是8位元組,就是所謂的PAE(實體位址延伸)。對於開啟PAE其實都是大同小異。我們先討論先討論普通4位元組。對於一個32位的虛擬位址,其中高20位對應一個物理頁地址,低12位是頁內位移,那麼意味著頁大小是12位,那麼一頁就是2^12=4096=1000h(頁大小也可能變化,萬變不離其中),而20位物理頁地址(簡稱PFN,Page frame number),加上12位頁大小,剛好是32位,那麼總共能訪問的記憶體就是2^32=4G,注意,這就是32位的極限了,而頁表將完整映射對應的4G空間,顯示需要4M的頁表(2^20 *4)。可是4M的連續儲存空間太過於奢侈,所以採取了二級頁表的方式,最高10位表示頁目錄索引,接下來10位表示頁表索引,低12位則用作頁內位移。頁目錄共有2^10=1024項,每個頁目錄下對應的頁表也有2^10=1024項。一項4個位元組,那麼頁表頁目錄總共佔有的空間:1024*4+1024*1024*4=4KB+4MB。
位址解析三部走:
0X01:PDE = PDBR[Directory];
0x02 : PTE = PDE + Table * 4;
0X03 : PhysicalAddress = PTE + Offset;
在頁目錄和頁表中, 只儲存了頁表或者頁物理地址的高20位. 原因很簡單, 頁表或者頁的物理地址, 都要求必須是4KB對齊的, 以便於放在一個頁內, 故其低12位全是0. 在這種情況下, 可以只關心其高20位, 低12位安排其他用途。
的頁表地址結構低12位是一些標誌資訊,那麼這些標誌位都代表什麼呢?
P--位0是存在(Present)標誌,用於指明表項對地址轉換是否有效。P=1表示有效;P=0表示無效。在頁轉換過程中,如果說涉及的頁目錄或頁表的表項無效,則會導致一個異常。如果P=0,那麼除表示表項無效外,其餘位可供程式自由使用,4-18b所示。例如,作業系統可以使用這些位來儲存已儲存在磁碟上的頁面的序號。
R/W--位1是讀/寫(Read/Write)標誌。如果等於1,表示頁面可以被讀、寫或執行。如果為0,表示頁面唯讀或可執行。當處理器運行在超級使用者特權級(層級0、1或2)時,則R/W位不起作用。頁目錄項中的R/W位對其所映射的所有頁面起作用。
U/S--位2是使用者/超級使用者(User/Supervisor)標誌。如果為1,那麼運行在任何特權級上的程式都可以訪問該頁面。如果為0,那麼頁面只能被運行在超級使用者特權級(0、1或2)上的程式訪問。頁目錄項中的U/S位對其所映射的所有頁面起作用。
A--位5是已訪問(Accessed)標誌。當處理器訪問頁表項映射的頁面時,頁表表項的這個標誌就會被置為1。當處理器訪問頁目錄表項映射的任何頁面時,頁目錄表項的這個標誌就會被置為1。處理器只負責設定該標誌,作業系統可通過定期地複位該標誌來統計頁面的使用方式。
D--位6是頁面已被修改(Dirty)標誌。當處理器對一個頁面執行寫操作時,就會設定對應頁表表項的D標誌。處理器並不會修改頁目錄項中的D標誌。
AVL--該欄位保留專供程式使用。處理器不會修改這幾位,以後的升級處理器也不會。
可以看到頁表的第2位標識著當前物理頁面可讀可寫,所以只需要再鉤之前把函數地址傳到驅動解析地址,函數所在的頁表第二位的標誌位改成1就行了。
PAE=ExIsProcessorFeaturePresent(PF_PAE_ENABLED); if(PAE==TRUE) { DbgPrint("PAE page mode.\n"); // 按照PAE page mode嘗試計算PDE和PTE,並查看虛擬位址是否在同一頁面 //要修改的地址起始處 ulPDEB = ( (((ULONG)pMem)>>18) & 0x3FF8 ) + 0xC0600000; ulPTEB = ( (((ULONG)pMem)>>9) & 0x7FFFF8 ) + 0xC0000000; bPDEB = MmIsAddressValid((PVOID)ulPDEB); bPTEB = MmIsAddressValid((PVOID)ulPTEB); //要修改的地址後邊界 ulPDE = ( ((((ULONG)pMem+5))>>18) & 0x3FF8 ) + 0xC0600000; ulPTE = ( ((((ULONG)pMem+5))>>9) & 0x7FFFF8 ) + 0xC0000000; bPDE = MmIsAddressValid((PVOID)ulPDE); bPTE = MmIsAddressValid((PVOID)ulPTE); if ((bPDEB && bPTEB && bPTE)) { DbgPrint("PDE(%d) : 0X%08X -> 0X%08X\n", bPDEB, ulPDEB, *(PULONG)ulPDEB); DbgPrint("PTE(%d) : 0X%08X -> 0X%08X\n", bPTEB, ulPTEB,ulPTE ); } else return STATUS_UNSUCCESSFUL; } else { DbgPrint("Non PAE page mode.\n"); // 按照Non PAE page mode計算PDE和PTE ulPDEB = ( (((ULONG)pMem)>>20) & 0xFFC ) + 0xC0300000; //cr3寄存器起始地址 ulPTEB = ( (((ULONG)pMem)>>10) & 0x3FFFFC ) + 0xC0000000; bPDEB = MmIsAddressValid((PVOID)ulPDEB); bPTEB = MmIsAddressValid((PVOID)ulPTEB); ulPDE = ( (((ULONG)pMem+5)>>20) & 0xFFC ) + 0xC0300000; ulPTE = ( (((ULONG)pMem+5)>>10) & 0x3FFFFC ) + 0xC0000000; bPDE = MmIsAddressValid((PVOID)ulPDE); bPTE = MmIsAddressValid((PVOID)ulPTE); if ((bPDEB && bPTEB && bPTE)) { DbgPrint("PDE(%d) : 0X%08X -> 0X%08X\n", bPDEB, ulPDEB, *(PULONG)ulPDEB); DbgPrint("PTE(%d) : 0X%08X -> 0X%08X\n", bPTEB, ulPTEB, *(PULONG)ulPTEB); } else return STATUS_UNSUCCESSFUL; }
判斷是否開啟PAE然後翻譯地址
if (bPTE==bPTEB)//物理頁面是否存在有效 { *(PULONG)ulPTEB |=0x00000002; //修改PTE使指定頁Copy on write機制失效 DbgPrint("The copy-on-write attrib in address 0X%08X has been forbidden!\n", pMem); status = STATUS_SUCCESS; } else { *(PULONG)ulPTEB |=0x00000002; *(PULONG)ulPTE |=0x00000002; DbgPrint("The copy-on-write attrib has been forbidden!\n"); status = STATUS_SUCCESS; } break;
然後把PTE標誌位置1就行了。修改完後在Ring3下Ntdll鉤子就能作用全域了,不要忘記鉤完後記得恢複頁面屬性。
Ring3下繞過Windows寫時複製機制實現全域EAT鉤子