前言

一種規避殺軟檢測的技術就是內存加密技術。由于殺軟并不是一直掃描內存,而是間隙性的掃描敏感內存,因此可以在cs的shellcode調用sleep休眠將可執行內存區域加密,在休眠結束時再將內存解密來規避殺軟內存掃描達到免殺的目的。

基于以上原理,結合我自己的想法進行了實現,成功實現動態免殺火絨、360、defender、卡巴等,其中卡巴不能使用cmd啟動,不然會立馬被殺,卡巴對cmd的審查太過嚴格了,導致shellcode沒來的及執行sleep加密內存就被殺了,使用powershell或其它方式可以正常啟動,但在運行一個小時后被殺,即使cs使用了profile配置文件的流量特征還是太過明顯了,建議試試其它更冷門的c2。

下面進行講解,包括以下四個方面:

  • 32位inline hook
  • 64位inline hook
  • 32位內存加密
  • 64位內存加密

其中32位內存加密免殺實現比較簡單,64位則更為復雜,不能通過簡單的hook進行實現,這里借鑒了ShellcodeFluctuation的代碼。

最后,在實現內存加密的過程中,也發現了其中不足并提出改進的方法。

注意:本文面向新人,因此篇幅比較長。

效果圖

下面是32位內存加密免殺的效果圖,64位和這差不多就不放了:

hook

Windows API Hook是一種實現Windows平臺下類似于中斷的機制。它允許應用程序攔截并處理Windows消息或指定事件,當指定的消息發出后,hook程序就可以在消息到達目標窗口之前將其捕獲,從而得到對消息的控制權,進而可以對該消息進行處理或修改,加入我們所需的功能。

Hook技術被廣泛應用于安全的多個領域,比如殺毒軟件的主動防御功能,涉及到對一些敏感API的監控,就需要對這些API進行hook;竊取密碼的木馬病毒,為了接收鍵盤的輸入,需要hook鍵盤消息;甚至是Windows系統及一些應用程序,在打補丁時也需要使用hook技術。

API hook大致有以下幾種方式:

  • Inline hook,一種應用層的注入hook,通過在內存中找到想要hook函數地址,并在其之前加入自己構造的跳轉指令,當被hook位置執行時,便可以跳轉到hook使用者自己編寫的執行代碼,在其執行完畢后,還原被修改的字節,接著執行正常流程。
  • IAT hook,一種導入表hook,通過修改導入表中某函數的地址到自己的補丁函數來實現。
  • SSDT hook,一種內核層的hook技術,通過修改系統服務表中某個服務函數的地址到自己的補丁函數來實現。
  • IRP hook,一種內核層的hook技術,通過修改IRP結構體中某個成員變量指向自己的補丁函數來實現。

需要注意的是,由于CS的shellcode獲取Windows API地址的方式是通過遍歷PEB結構和PE文件導出表并根據導出函數的hash值查找需要的模塊和API函數,因此IAT hook方式對cs的shellcode無效,這里主要使用inline hook。

1. 前置知識

這里用到的兩個函數ReadProcessMemory、WriteProcessMemory:

// 將指定地址范圍中的數據從指定進程的地址空間復制到當前進程的指定緩沖區。BOOL ReadProcessMemory(  [in]  HANDLE  hProcess, // 正在讀取內存的進程句柄。  [in]  LPCVOID lpBaseAddress, // 指向要從中讀取的指定進程中基址的指針。  [out] LPVOID  lpBuffer, // 指向從指定進程的地址空間接收內容的緩沖區的指針。  [in]  SIZE_T  nSize, // 要從指定進程讀取的字節數。  [out] SIZE_T  *lpNumberOfBytesRead // 指向變量的指針,該變量接收傳輸到指定進程的字節數。);// 將數據寫入指定進程中的內存區域。BOOL WriteProcessMemory(  [in]  HANDLE  hProcess, // 要修改的進程內存的句柄。  [in]  LPVOID  lpBaseAddress, // 指向指定進程中寫入數據的基址的指針。  [in]  LPCVOID lpBuffer, // 要寫入寫入的數據的緩沖區指針  [in]  SIZE_T  nSize, // 要寫入指定進程的字節數。  [out] SIZE_T  *lpNumberOfBytesWritten );

對內存的讀取和寫入需要有對應的權限,否則無法修改,使用ReadProcessMemory、WriteProcessMemory函數進行讀寫內存時會自動獲取對應的權限,因此可以不使用VirtualProtect修改權限。

修改函數代碼跳轉到我們的函數可以使用的幾種匯編跳轉方式,注釋后面是其機器碼:

// 方式一,使用jmp相對地址跳轉jmp <相對地址>   ; E9 <相對地址>
// 方式二,使用寄存器jmp絕對地址跳轉mov eax, <絕對地址>   ; B8 <絕對地址>jmp eax             ; FF E0
// 方式三,使用push ret絕對地址跳轉push <絕對地址>   ; 68 <絕對地址>ret             ; C3

其中,相對地址計算方式如下:

相對地址 = 要跳轉的函數地址 - jmp指令的地址 - jmp跳轉指令的總長度。

在32位系統中函數地址長為4字節,如果要修改MessageBox函數跳轉到HookedMessageBox函數,MessageBox函數地址位12340000h,HookedMessageBox地址為12345678h,指令長度為jmp指令1字節+函數地址4字節=5,那么相對地址=12345678h-12340000h-5=00005678h

所以jmp跳轉指令為:

jmp 00005678h ; E9 78 56 00 00

由于jmp后面只能跟不超過4字節長度的地址,因此jmp在32位中可以跳轉到任意的地址,在64位,地址長度為8字節,如果jmp指令地址長度與要跳轉的地址相差超過4字節則不能使用jmp相對地址跳轉。

2. 32位inline hook

32位的inline hook方式實現比較簡單,實現過程如下:

  1. 獲取需要掛鉤的函數地址
  2. 直接修改函數代碼跳轉到我們自己寫的新函數,即設置hook
  3. 在新函數中恢復原函數,即恢復hook
  4. 調用恢復的原函數
  5. 重新設置hook

下面將以MessageBox函數為例,使用inline hook方式掛鉤MessageBox跳轉到HookedMessageBox函數。

首先進入setHook函數,該函數用于設置掛鉤,oldAddress保存了MessageBox函數的地址:

使用ReadProcessMemory函數從內存中讀取原始MessageBox函數的前6個字節,需要在解綁定時還原。

這里使用方式三push ret絕對地址跳轉,使用memcpy_s寫入要跳轉的HookedMessageBox函數地址到掛鉤機器碼數組中。如果使用方式一進行jmp相對地址跳轉則修改為下面這樣:

void setHook() {    SIZE_T bytesRead = 0;    // 保存原始MessageBoxA函數的前6個字節,需要在解綁定時還原    ReadProcessMemory(GetCurrentProcess(), oldAddress, messageBoxOriginalBytes, 6, &bytesRead);
    // 計算相對地址    DWORD_PTR offsetAddress = (DWORD_PTR)HookedMessageBox - (DWORD_PTR)oldAddress - 5;    char patch[6] = { 0xE9, 0, 0, 0, 0 };    memcpy_s(patch + 1, 4, &offsetAddress, 4);
    // 將掛鉤寫入MessageBoxA內存    WriteProcessMemory(GetCurrentProcess(), (LPVOID)oldAddress, patch, sizeof(patch), &bytesWritten);}

最后使用WriteProcessMemory將掛鉤機器碼數組其寫入內存。

然后看下要跳轉的HookedMessageBox函數,HookedMessageBox函數除了名字不同其它的參數、返回值、調用類型等應該與原MessageBox函數相同:

當從MessageBox跳轉到HookedMessageBox函數時就會打印函數的執行參數,然后解除掛鉤MessageBoxA,再調用原來的MessageBoxA并保存結果,然后重新設置掛鉤。

進入主函數,我們先調用原有的MessageBox函數,然后通過GetProcAddress動態獲取MessageBox函數的地址,然后調用setHook函數設置掛鉤,再顯示掛鉤后的彈窗,并在setHook處打上斷點:

執行程序,彈出彈窗:

按確定到斷點中斷執行,然后在旁邊的反匯編窗口中輸入oldAddress回車,查看MessageBoxA函數的匯編代碼,這是沒有掛鉤之前的函數代碼,注意看前6個機器碼(8B FF 55 8B EC 83):

然后單步執行,執行setHook()函數,到掛鉤后的MessageBoxA:

重新查看oldAddress函數地址,可以看到前6個機器碼已經被修改成了跳轉到我們自己設置的函數:

繼續執行,彈出被掛鉤后的彈窗:

然后可以看到控制臺中截取到的函數調用參數,說明掛鉤成功:

完整代碼如下:

#include #include using namespace std;
FARPROC oldAddress = NULL;SIZE_T bytesWritten = 0;char messageBoxOriginalBytes[6] = {};
int __stdcall HookedMessageBox(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
void setHook() {  SIZE_T bytesRead = 0;  // 保存原始MessageBoxA函數的前6個字節,需要在解綁定時還原  ReadProcessMemory(GetCurrentProcess(), oldAddress, messageBoxOriginalBytes, 6, &bytesRead);
  void* hookedAddress = HookedMessageBox;  char patch[6] = { 0x68, 0, 0, 0, 0, 0xC3 };  memcpy_s(patch + 1, 4, &hookedAddress, 4);
  // 將掛鉤寫入MessageBoxA內存  WriteProcessMemory(GetCurrentProcess(), (LPVOID)oldAddress, patch, sizeof(patch), &bytesWritten);}
int __stdcall HookedMessageBox(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {  // 打印從MessageBoxA函數截取的值  cout << "Ohai from the hooked function" << endl;  cout << "Text: " << lpText << endl;  cout << "Caption: " << lpCaption << endl;
  // 解除掛鉤MessageBoxA  WriteProcessMemory(GetCurrentProcess(), (LPVOID)oldAddress, messageBoxOriginalBytes, sizeof(messageBoxOriginalBytes), &bytesWritten);
  // 調用原來的MessageBoxA  int r = MessageBoxA(NULL, lpText, lpCaption, uType);
  // 重新設置掛鉤,以便下次監聽  setHook();  return r;}
int main(){  // 顯示掛鉤之前的messagebox  MessageBoxA(NULL, "hi", "hi", MB_OK);
  HINSTANCE library = LoadLibraryA("user32.dll");
  // 獲取在內存中MessageBox函數的地址  oldAddress = GetProcAddress(library, "MessageBoxA");
  /*  * 創建掛鉤:  * push 
  * ret  */  setHook();
  // 顯示掛鉤后消息框  MessageBoxA(NULL, "hi", "hi", MB_OK);  MessageBoxA(NULL, "hi", "hi", MB_OK);  return 0;}

3. 64位inline hook

由于jmp和push后面都只能跟4字節長度的地址,面對64位的8字節地址是遠遠不夠的,因此只能借助寄存器進行絕對地址跳轉:

// 方式二,使用寄存器jmp絕對地址跳轉mov r12, <絕對地址>   ; 49 BC <絕對地址>jmp r12             ; 41 FF E4
// 方式三,使用push ret絕對地址跳轉mov r12, <絕對地址>   ; 49 BC <絕對地址>push r12             ; 41 54 <絕對地址>ret                 ; C3

在32位系統中的eax、ebx等寄存器再64位中相應的變成rax、rbx等寄存器,且多出r8-r15等8個寄存器。

然后在setHook函數中做一下相應的修改即可:

char messageBoxOriginalBytes[13] = {};
void setHook() {    SIZE_T bytesRead = 0;    // 保存原始MessageBoxA函數的前12個字節,需要在解綁定時還原    ReadProcessMemory(GetCurrentProcess(), oldAddress, messageBoxOriginalBytes, 13, &bytesRead);
    void* hookedAddress = HookedMessageBox;    char patch[13] = { 0x49, 0xBC, 0, 0, 0, 0, 0, 0, 0, 0, 0x41, 0x54, 0xC3 };    memcpy_s(patch + 2, 8, &hookedAddress, 8);
    // 將掛鉤寫入MessageBoxA內存    WriteProcessMemory(GetCurrentProcess(), (LPVOID)oldAddress, patch, sizeof(patch), &bytesWritten);}

然后運行,在64位下能夠正常掛鉤MessageBoxA函數:

值得注意的是,在64位下并不是所有函數都能夠使用inline hook進行掛鉤,這也是為什么32位內存加密與64位內存加密實現方式略有不同的原因。

內存加密

使用開頭提到了內存加密技術——由于殺軟并不是一直掃描內存,而是間隙性的掃描敏感內存,因此可以在cs的shellcode調用sleep休眠將可執行內存區域加密,在休眠結束時再將內存解密來規避殺軟內存掃描達到免殺的目的。進行加密內存時,需要注意的一點,需要加密的并不是我們為shellcode申請的內存,而是shellcode自己使用VirtualAlloc函數申請的內存:

我們需要對shellcode自己使用VirtualAlloc函數申請的內存2進行加密,這就需要掛鉤sleep函數到我們自定義的HookSleep函數:

  1. 在進入HookSleep函數時使用自定義加密函數對內存2進行加密并使用VirtualProtect更改內存2權限為PAGE_NOACCESS,使其不可訪問。
  2. 恢復原來的Sleep函數并調用原函數進行休眠。
  3. 在退出HookSleep函數時對內存2進行解密并使用VirtualProtect更改內存2權限為可執行權限PAGE_EXECUTE。

那么問題來了,要加密內存2,如何獲取內存2的地址?

在32位中,我們可以直接掛鉤VirtualAlloc函數截取返回地址。在64位中,如果還使用32的辦法掛鉤VirtualAlloc函數是行不通的,原因上面也有提到,在64位下并不是所有函數都能夠使用inline hook進行掛鉤。

對比一下32位下的VirtualAlloc函數內存與64位下的VirtualAlloc函數內存:

可以發現64位下VirtualAlloc函數內存只有一句jmp跳轉指令,對于這種只有一句jmp跳轉指令的函數進行掛鉤時可能會出現錯誤,這種錯誤不一定會發生,當64位下掛鉤VirtualAlloc時,我們自己調用沒有問題,可以正常掛鉤,但是cs的shellcode進行調用時就會發生錯誤,因此64位下不能掛鉤VirtualAlloc函數,那么64位下如何獲取獲取內存2的地址呢?詳情請看下面的64位內存加密。

加密了內存2,內存1也要進行一些處理,可以使用VirtualFree釋放內存1,也可以像內存2一樣加密。

1. 32位內存加密

先掛鉤VirtualAlloc函數:

在HookedVirtualAlloc函數中保存申請的內存2的地址和大小,HookVirtualAlloc用于設置VirtualAlloc掛鉤。

然后掛鉤Sleep函數:

在HookedSleep函數中首先釋放了內存1,然后VirtualProtect修改上一步獲取的內存2地址為可讀寫,然后加密內存2再修改內存2地址為不可訪問。之后調用原來的Sleep函數,在Sleep函數結束后解密內存。

然后在main函數中設置Sleep和VirtualAlloc的掛鉤,然后分配內存執行shellcode:

這里并沒有用什么花銷的回調加載,僅使用最簡單的指針加載。

執行后可以看到調用了3次VirtualAlloc函數:

第一次是我們分配shellcode內存時調用的,后面兩次是shellcode自己調用的,查看shellcode第一次調用VirtualAlloc申請的內存,可以發現已經被釋放了,cs shellcode是執行完一段代碼就釋放一段內存

再看下shellcode第二次調用VirtualAlloc申請的內存,這是cs真正執行的內存:

至此,32位內存加密完成。

2. 64位內存加密

64位實現內存加密要復制一些,不能掛鉤VirtualAlloc,而是使用VirtualQueryEx函數:

// 檢索有關指定進程的虛擬地址空間中的頁面范圍的信息。SIZE_T VirtualQueryEx(  [in]           HANDLE                    hProcess, // 查詢其內存信息的進程的句柄。  [in, optional] LPCVOID                   lpAddress, // 指向要查詢的頁面區域的基址的指針。  [out]          PMEMORY_BASIC_INFORMATION lpBuffer, // 指向返回指定頁面范圍信息的 MEMORY_BASIC_INFORMATION 結構的指針。  [in]           SIZE_T                    dwLength // lpBuffer 參數指向的緩沖區的大小);

通過不斷的增加lpAddress的大小,就可以遍歷虛擬地址空間中的所有頁面范圍,通過第三個參數返回頁面范圍信息的 MEMORY_BASIC_INFORMATION 結構的指針:

typedef struct _MEMORY_BASIC_INFORMATION {  PVOID  BaseAddress; // 指向頁面區域的基址的指針  PVOID  AllocationBase;   DWORD  AllocationProtect;  WORD   PartitionId;  SIZE_T RegionSize; // 區域大小,即內存大小  DWORD  State; // 區域中頁面的狀態  DWORD  Protect; // 頁面權限  DWORD  Type; // 區域中頁面的狀態} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;

其中,如果MEMORY_BASIC_INFORMATION結構體的Type等于MEM_PRIVATE,則代表這塊內存是屬于動態申請的,那么不是我們申請的就是shellcode申請的。

在HookedSleep函數中調用內置函數_ReturnAddress()函數獲取函數的調用地址callerAddress,然后通過VirtualQueryEx遍歷所有內存頁信息,然后與前面獲取的所有內存頁范圍進行比較,如果函數的調用地址在這一塊內存頁范圍則表明這是shellcode申請的地址,即內存2,這樣就成功獲取到內存2地址:

void initVirtualAllocSet(PVOID callerAddress) {  SYSTEM_INFO si;  GetSystemInfo(&si);  DWORD PageSize = si.dwPageSize;  DWORD_PTR dwMin = (DWORD_PTR)si.lpMinimumApplicationAddress;  DWORD_PTR dwMax = (DWORD_PTR)si.lpMaximumApplicationAddress;  MEMORY_BASIC_INFORMATION mbi = { 0 };  while (dwMin < dwMax)  {    VirtualQueryEx(GetCurrentProcess(), (LPCVOID)dwMin, &mbi, sizeof(mbi));    if (mbi.Type == MEM_PRIVATE && (mbi.Protect == PAGE_EXECUTE_READWRITE || mbi.Protect == PAGE_EXECUTE_READ || mbi.Protect == PAGE_READWRITE))    {      if (callerAddress >= mbi.BaseAddress && (DWORD_PTR)callerAddress <= (DWORD_PTR)mbi.BaseAddress + mbi.RegionSize) {        currentMbi = mbi;        cout << "Address: " << mbi.BaseAddress << "\tSize: " << mbi.RegionSize << "\tstate: " << hex << mbi.State << "\ttype: " << hex << mbi.Type << endl;      }    }    dwMin += mbi.RegionSize;  }}

來到HookedSleep函數:

前面通過initVirtualAllocSet函數獲取內存2的地址,然后加密內存,這里采用內存1與內存2一起加密的方式,但是后面并沒有解密內存的代碼,這樣執行完HookedSleep函數后就會因為沒有解密內存而導致報出0xc0000005錯誤,即沒有權限訪問,這里利用VEH機制來解密。

Windows中主要的異常處理機制有VEH(向量異常處理)、SEH(結構化異常處理)、C++EH等,SEH就是__try、__finally、__try、__except,C++EH就是C++提供的異常處理方式,它們的異常處理順序流程如下:

可以看到VEH更接近底層,因此能處理更多的錯誤。

我們定義一個錯誤處理函數PvectoredExceptionHandler,使用VEH處理前面報出的0xc0000005錯誤:

通過ExceptionInfo->ContextRecord->Rip可以獲取出現錯誤的地址,然后比較該地址是否在內存2范圍中,如果在則進行解密。然后我們需要在主函數地址中注冊該函數為VEH處理函數:

AddVectoredExceptionHandler(1, &PvectoredExceptionHandler);

使用64位inline hook方式掛鉤Sleep:

其它方面與32位內存加密相同,至此64位內存加密完成,執行效果:

缺點與改進

使用該內存加密的不足:

  • 需要掛鉤Sleep函數(雖然可以利用VEH機制來解決),對于能夠檢測掛鉤的殺軟可能會失效。
  • 只適應于cs shellcode,其它不使用Sleep來休眠的shellcode不能使用。
  • 在剛啟動和不休眠時處于明文狀態,對于內存檢測更強殺軟來說,這一段時間就能查殺(比如用cmd啟動被卡巴查殺)

改進:

可以將shellcode劃分成多個片段,只有正在執行的片段處于解密狀態,其它部分處于加密片段,當執行到加密片段時再利用VEH機制解密片段,shellcode劃分得越小免殺效果越好,難點在于如何劃分shellcode使其正好劃分在一句匯編的結束位置。

源碼僅限【深情種聚集地】小密圈下載,在文章左下角“閱讀閱文”獲得。