使用windows API编写PE文件加载器(Loader)
目前支支持32位
PE文件结构
在《逆向工程核心原理》中讲的已经很详细了,这里主要面向32位的可执行程序来讲解。
DOS头和PE头统称为PE头,下面的部分称之为PE体。
DOS头
DOS头的文件结构
1 | typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header |
其中DOS头有一个很重要的部分e_lfanew
,他指向了exe的文件头,在我们编写的loader获取头的部分
1 | DOSHeader = PIMAGE_DOS_HEADER(Image); //得到DOS头 |
NT头
那么关于NT文件头
1 | typedef struct _IMAGE_NT_HEADERS { |
这个文件头很关键,Signature可以判断类型,FileHeader即文件头,可以从NumberOfSections
获得节区数目。
OPTIONAL_HEADER结构体如下
1 | typedef struct _IMAGE_OPTIONAL_HEADER { |
OPTIONAL_HEADER中记载了很多详细信息,其中有用的就是ImageBase
、SizeOfHeaders
和AddressOfEntryPoint
ImageBase
:描写在虚拟内存中(不了解操作系统的话可以理解为程序启动的基地址)的地址SizeOfHeaders
:记录了整个PE头的大小(包含DOS头),方便控制写入程序的大小AddressOfEntryPoint
:记录程序入口代码起始地址,比如ImageBase
可能为0x4000,AddressOfEntryPoint
可能为0x4100。
IAT导入表
Windows为了知道使用了那些函数,会导入这些函数的表,从导入表到动态链接中查找函数。每一个节都会有一个导入表,每一个表的信息有40字节,那么找到表的地址就是base + count*40
,其中base
为DOSHeader->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):堆栈段寄存器
- 在实模式中,CS、DS、ES、SS中的值是物理地址
- 在保护模式中,装入寄存器的是段选择子FS
其中最重要的就是 FS寄存器。在保护模式下,x86处理器使用段描述符来管理内存,将内存划分为不同的段,如代码段、数据段、堆栈段等。段选择子是一个16位的值,用于标识特定段的起始地址和访问权限。
FS寄存器主要有两个作用:
- 定位线程局部存储(Thread Local Storage,TLS):
- 在多线程程序中,每个线程通常都有自己的TLS,用于存储线程本地的数据,如线程特定变量。
- FS寄存器中存储了一个特殊的段选择子,用于定位线程的TLS。
- 线程可以通过访问FS寄存器来访问自己的TLS。
- 访问段描述符表(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
函数
新进程在调用进程的安全上下文中运行。
如果调用进程正在模拟其他用户,则新进程将令牌用于调用进程,而不是模拟令牌。 若要在模拟令牌表示的用户的安全上下文中运行新进程,请使用 CreateProcessAsUser 或 CreateProcessWithLogonW 函数。
1 | BOOL CreateProcessA( |
-
lpApplicationName
:exe的文件路径,比如c:\test.exe
-
lpCommandLine
:要执行该程序时的参数 -
bInheritHandles
:如果此参数为 TRUE,则调用进程中的每个可继承句柄都由新进程继承。 如果参数为 FALSE,则不继承句柄。 -
dwCreationFlags
:控制优先级类和进程的创建的标志。 -
lpProcessInformation
:进程信息windows中使用
PROCESS_INFORMATION
描述1
2
3
4
5
6typedef 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
13typedef 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;
最后创建好的进程就在lpProcessInformation
的hProcess
中了
编写Loader(进程镂空)
知道了加载过程,那么
- 获得DOS头,从而获得NT头
- 检查是否为正确的文件格式(PE)
- 初始化进程信息和启动时信息
- 创建当前程序进程的副本,并将副本设置为暂停
- 根据上下文信息找到导入表和PEB
- 复制导入表和PEB,将EAX设置为待加载PE文件的入口地址
DWORD(pImageBase) + NTHeader->OptionalHeader.AddressOfEntryPoint;
- 恢复暂停的副本,运行加载的PE文件
1 |
|