用c++解析PE 繞過AV/EDR API掛鉤
這篇文章是使用最初是由@spotless編寫的代碼來繞過AV/ EDR創建的API掛鉤。我想說明的是,spotless已經在這方面做了一些準備工作,我只是做了一些小的功能更改,并添加了許多注釋和文檔。這主要是為了提高我對這個主題的理解,因為我發現在手頭有MSDN文檔的情況下逐個函數地瀏覽代碼是了解它如何工作的好方法。它可能有點單調乏味,這就是為什么我對代碼進行過多的文檔化,以便其他人能夠從中吸取經驗。
這篇文章涵蓋了幾個主題,比如系統調用、用戶模式與內核模式,以及我在本文將要介紹的Windows體系結構。在這篇文章中,我將在本文中假定對這些主題有一定程度的了解,這篇文章的代碼可以在這里找到。
理解API掛鉤
鉤到底是什么?它是AV/EDR產品常用的一種技術,用于攔截函數調用,并將代碼執行流程重定向到AV/EDR,以檢查調用并確定是否為惡意調用。這是一項功能強大的技術,因為防御性應用程序可以一步一步查看你進行的每個函數調用,確定其是否為惡意程序并將其阻止。更糟糕的是(對于攻擊者來說),這些產品在系統庫/ DLL中掛鉤本地函數,這些DLL位于傳統使用的Win32 API之下。例如,WriteProcessMemory是一種常用的Win32 API,用于將shellcode寫入進程地址空間,實際上調用了ntdll.dll中包含的未文檔化的本機函數NtWriteVirtualMemory。 NtWriteVirtualMemory實際上是對內核模式的系統調用的包裝函數。由于AV / EDR產品能夠在用戶模式代碼可訪問的最低級別上掛接函數調用,因此無法對其進行轉義。
掛鉤發生的位置
為了理解如何繞過掛鉤,我們需要知道它們是如何以及在哪里創建的。當進程啟動時,某些庫或DLL將作為模塊加載到進程地址空間中。每個應用程序都是不同的,將加載不同的庫,但無論它們的功能如何,實際上所有的應用程序都將使用ntdll.dll,因為許多最常見的Windows函數都駐留在其中。防御性產品通過在DLL中連接函數調用來利用這一事實。通過掛鉤,我們實際上是指修改函數的匯編指令,在函數的開頭插入一個無條件跳轉到EDR的代碼中。EDR處理函數調用,如果允許,執行流將跳回原始函數調用,以便函數正常執行,而調用進程不知情。
識別掛鉤
所以我們知道在我們的進程中,ntdll.dll模塊已經被修改,我們不能相信任何使用它的函數調用。我們怎樣才能解開這些掛鉤呢?我們可以確定我們所使用的Windows的確切版本,找出實際的組裝說明應該是什么,并嘗試在運行中修補它們。但是這樣做會很乏味,容易出錯,而且不可重用。事實證明,磁盤上已經存在一個原始的,未經修改的,未經摘錄的ntdll.dll版本!
因此,正確的策略應該如下。首先,我們將ntdll.dll的副本映射到我們的進程內存中,以使用一個干凈的版本。然后,我們將在過程中確定掛鉤版本的位置。最后,我們只需用干凈的代碼重寫掛鉤的代碼,就可以了!
映射NtDLL.dll
映射ntdll.dll文件的視圖實際上非常簡單,我們獲得了ntdll.dll的句柄,獲得了它的文件映射的句柄,并將其映射到我們的進程中:
- HANDLE hNtdllFile = CreateFileA("c:\\windows\\system32\\ntdll.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);HANDLE hNtdllFileMapping = CreateFileMapping(hNtdllFile, NULL, PAGE_READONLY | SEC_IMAGE, 1, 0, NULL);LPVOID ntdllMappingAddress = MapViewOfFile(hNtdllFileMapping, FILE_MAP_READ, 0, 0, 0);
很簡單,現在我們已經將干凈的DLL映射到我們的地址空間中,現在我們來查找掛鉤副本。
要在進程內存中找到掛鉤的ntdll.dll的位置,我們需要在進程中加載的模塊列表中找到它。本例中的模塊是DLL和進程的主要可執行文件,在進程環境塊中存儲了它們的列表。PEB的具體介紹請點擊這里。要訪問這個列表,我們可以獲取流程和所需模塊的句柄,然后調用GetModuleInformation。然后,我們可以從miModuleInfo結構中檢索DLL的基地址:
- handle hCurrentProcess = GetCurrentProcess();
- HMODULE hNtdllModule = GetModuleHandleA("ntdll.dll");
- MODULEINFO miModuleInfo = {};
- GetModuleInformation(hCurrentProcess, hNtdllModule, &miModuleInfo, sizeof(miModuleInfo));
- LPVOID pHookedNtdllBaseAddress = (LPVOID)miModuleInfo.lpBaseOfDll;
好的,因此我們在進程中具有已加載的ntdll.dll模塊的基地址。但這到底是什么意思?DLL是一種與EXE一起可移植的可執行文件。這意味著它是一個可執行文件,因此包含各種不同類型的標頭文件和節,這些文件可讓操作系統知道如何加載和執行該文件。如上所示PE標頭是密集而復雜的,但是我發現看到一個實際的工作示例僅利用了其中的一部分,就很容易理解。哦,圖片也不會受傷。那里有很多細節級別各不相同的東西,但是來自Wikipedia的一個很好的示例有足夠的細節而又不至于太令人費解:
你可以在DOS標頭的PE開頭看到Windows的遺留物,它一直都在那兒,但現在已經沒有什么用處了。但是,我們將獲取其地址,作為獲取實際PE標頭的偏移量:
- PIMAGE_DOS_HEADER hookedDosHeader = (PIMAGE_DOS_HEADER)pHookedNtdllBaseAddress;
- PIMAGE_NT_HEADERS hookedNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)pHookedNtdllBaseAddress + hookedDosHeader->e_lfanew);
在這里,hookedDosHeader結構體的e_lfanew字段包含一個到模塊內存的偏移量,該偏移量標識PE標頭文件實際上從哪里開始,也就是上圖中的COFF頭文件。
現在我們位于PE標頭的開頭,我們可以開始對其進行解析以查找所需的內容。但是,讓我們退后一步,準確地確定我們在尋找什么,這樣我們就知道什么時候我們找到了它。
每個可執行文件/ PE都有許多部分,這些部分代表程序中各種類型的數據和代碼,例如實際的可執行代碼、資源、圖像、圖標等。這些類型的數據在可執行文件中分為不同的帶標簽的部分,命名為.text、.data、.rdata和.rsrc。.text節(有時也稱為.code節)是緊隨其后的,因為它包含組成ntdll.dll的匯編語言指令。
那么我們如何訪問這些部分呢?在上圖中,我們看到一個節表,其中包含一個指向每個節開始的指針的數組。非常適合遍歷和查找每個部分,這是通過使用for循環并遍歷掛鉤edNtHeader-> FileHeader.NumberOfSections字段的每個值來找到.text部分的方法:
- for (WORD i = 0; i < hookedNtHeader->FileHeader.NumberOfSections; i++)
- {
- // loop through each section offset
- }
從現在開始,別忘了我們將在循環中尋找.text部分。為了識別它,我們使用循環計數器i作為節表本身的索引,并獲得指向節頭的指針
- PIMAGE_SECTION_HEADER hookedSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(hookedNtHeader) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));
每個節的節標題包含該節的名稱,因此,我們可以查看每一個,看看它們是否與.text匹配:
- if (!strcmp((char*)hookedSectionHeader->Name, (char*)".text"))
- // process the header
無論如何它的標頭如何,我們找到了.text節!現在我們需要知道該部分中實際代碼的大小和位置。本節標頭包含了以下兩方面內容:
- LPVOID hookedVirtualAddressStart = (LPVOID)((DWORD_PTR)pHookedNtdllBaseAddress + (DWORD_PTR)hookedSectionHeader->VirtualAddress);
- SIZE_T hookedVirtualAddressSize = hookedSectionHeader->Misc.VirtualSize;
現在,我們有了所有需要的東西,我們可以用磁盤上的干凈的ntdll.dll重寫加載和鉤住的ntdll.dll模塊的.text部分:
· 要復制的源文件(磁盤上的內存映射文件ntdll.dll);要復制到的目的地(.text節的hookedSectionHeader->VirtualAddress);
· 復制的字節數(hookedSectionHeader->Misc.VirtualSize字節)。
保存的輸出
至此,我們保存了.text節的全部內容,因此我們可以對其進行檢查,并將其與干凈版本進行比較,從而知道解除鏈接成功了:
- char* hookedBytes{ new char[hookedVirtualAddressSize] {} };
- memcpy_s(hookedBytes, hookedVirtualAddressSize, hookedVirtualAddressStart, hookedVirtualAddressSize);
- saveBytes(hookedBytes, "hooked.txt", hookedVirtualAddressSize)
這僅是掛鉤.text節的一個副本,并調用saveBytes函數,該函數將字節寫入一個名為hook .txt的文本文件,稍后我們將研究這個文件。
內存管理
為了重寫.text部分的內容,我們需要保存當前的內存保護并將其更改為讀/寫/執行,完成后,我們將其改回來
- bool isProtected;
- isProtected = VirtualProtect(hookedVirtualAddressStart, hookedVirtualAddressSize, PAGE_EXECUTE_READWRITE, &oldProtection);
- // overwrite the .text section here
- isProtected = VirtualProtect(hookedVirtualAddressStart, hookedVirtualAddressSize, oldProtection, &oldProtection);
繞過過程
我們終于到了繞過過程,首先,我們從獲取內存映射的ntdll.dll的開頭地址開始,作為我們的復制源:
- LPVOID cleanVirtualAddressStart = (LPVOID)((DWORD_PTR)ntdllMappingAddress + (DWORD_PTR)hookedSectionHeader->VirtualAddress);
我們還要保存這些字節,以便稍后進行比較:
- char* cleanBytes{ new char[hookedVirtualAddressSize] {} };
- memcpy_s(cleanBytes, hookedVirtualAddressSize, cleanVirtualAddressStart, hookedVirtualAddressSize);
- saveBytes(cleanBytes, "clean.txt", hookedVirtualAddressSize);
現在我們可以用未鉤住的ntdll.dll重寫.text部分:
- memcpy_s(hookedVirtualAddressStart, hookedVirtualAddressSize, cleanVirtualAddressStart, hookedVirtualAddressSize);
怎么知道是否被繞過了?
那么我們怎么知道我們實際上刪除了掛鉤,而不是移動了一堆字節呢?讓我們檢查一下輸出文件hook .txt和clean.txt。這里我們使用VBinDiff對它們進行比較,第一個示例是在沒有安裝AV/EDR產品的測試設備上運行程序,正如預期的那樣,加載的ntdll和磁盤上的ntdll是相同的:
因此,讓我們再次在運行有掛鉤的Avast Free Antivirus的計算機上再次運行它:
現在,讓我們看看hooked.txt的開頭和clean.txt的結尾,它們之間有明顯的區別,用紅色標出。我們可以獲取這些原始字節,這些原始字節實際上代表匯編指令,然后使用在線反匯編程序將它們轉換為其匯編表示。
以下就是干凈的ntdll.dll的反匯編結果:
- mov QWORD PTR [rsp+0x20],r9
- mov QWORD PTR [rsp+0x10],rdx
以下就是掛鉤后的版本:
- jmp 0xffffffffc005b978
- int3
- int3
- int3
- int3
- int3
可以看到一個清晰的jump! ,這意味著當它被加載到我們的進程中時,ntdll.dll中的某些內容已經發生了明顯的變化。
但是我們怎么知道它實際上是在連接一個函數調用呢?讓我們看看能不能找到更多的答案。這是頂部掛鉤的DLL和底部干凈的DLL之間的另一個差異示例:
首先清理DLL:
- mov r10,rcx
- mov eax,0x37
- mov r10,rcx
- mov eax,0x3a
掛鉤的DLL:
- jmp 0xffffffffbffe5318
- int3
- int3
- int3
- jmp 0xffffffffbffe4cb8
- int3
- int3
- int3
現在,我們看到了更多的跳躍。但是這些mov eax和編號指令是什么意思?這些是系統調用號碼!如果你閱讀了我以前的文章,我將介紹如何以及為什么在匯編中準確找到這些內容。這個想法是使用syscall號直接調用底層函數,以避免掛鉤!但是,如果你想運行尚未編寫的代碼怎么辦?如何防止這些掛鉤捕獲你無法更改的代碼?如果你到目前為止已經做到了,那么你已經知道了!因此,讓我們使用Mateusz“j00ru”Jurczyk的簡化版Windows系統調用表,并將syscall編號與其相應的函數調用進行匹配。
看看,我們發現了什么?0x37是NtOpenSection, 0x3a是NtWriteVirtualMemory! ,Avast 顯然是在連接這些函數調用,而且我們知道我們已經用干凈的DLL重寫了它們。
本文翻譯自:https://www.solomonsklash.io/pe-parsing-defeating-hooking.html如若轉載,請注明原文地址: