PE文件从磁盘中向内存中加载映射的过程。总体思路就是将PE文件按照各种方式映射到内存中即可,通过学习PE文件向内存中加载的过程可以推导出shellcode直接加载内存执行的思路。
基本的载入流程可以概况一下步骤:
- 根据文件的大小申请具有合适权限的内存空间
- 用0进行初始化内存空间
- 将PE文件利用ReadFile映射进去
- 修改重定位表
- 根据PE文件的导入表,加载需要的dll。
- 获取导入函数的地址,写进导入表
- 修改PE的加载baseaddr
- 跳转到入口点开始执行
定位 - 利用数据目录表
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
数据目录表(IDD)是可选头的最后一个项目。长度是16,可以这个值只有od对他是关注的,可以修改该值来反od的载入调试。里面就含有两项,一个是虚拟地址和大小。
顺序是
- 导出表
- 导入表
- 资源表
- 异常信息表
- 安全证书表
- 重定位表
- 调试信息表
- 版权所以表
- 全局指针表
- TLS表
- 加载配置表
- 绑定导入表
- IAT表
- 延迟导入表
- COM信息表
- 保留
上述表中和程序加载相关就只有导入导出、重定向、IAT表这些
导出表
定位
EXE文件一般没有这个,显示的就是0
DLL程序的这个结构体很大
IDD中的该目录指向了数据目录表,这是RVA值,相对虚拟地址,在本地查看需要转换成FOA地址,文件偏移地址。
转换需要减去一个对齐,这里就是0x1000.
导出表的位置一般位于所有节区的后面,紧跟着就是导出表。
结构
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp; //时间戳
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; // 指向该导出表文件名字符串
DWORD Base; // 导出函数起始序号
DWORD NumberOfFunctions; // 所有导出函数的个数
DWORD NumberOfNames; // 以函数名字导出的函数个数
DWORD AddressOfFunctions; // 导出函数地址表RVA
DWORD AddressOfNames; // 导出函数名称表RVA
DWORD AddressOfNameOrdinals; // 导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, * PIMAGE_EXPORT_DIRECTORY;
导入表
一般是IDD表中的第二个结构体内容,指向了导入表所在的位置。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //RVA 指向IMAGE_THUNK_DATA结构数组
};
DWORD TimeDateStamp; //时间戳
DWORD ForwarderChain;
DWORD Name; //RVA,指向dll名字,该名字已0结尾
DWORD FirstThunk; //RVA,指向IMAGE_THUNK_DATA结构数组
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
导入表在文件中的位置一般在导出表的后面,就是最后节区的后第二个。最后一个导出表结构体全是0
从010可以看到清楚的结构
在结构体中的联合体中,可以看到
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //RVA 指向IMAGE_THUNK_DATA结构数组
};
【联合体内容共享内存】,这两个值保存的是同一个值,这个值可以用来遍历INT(image_name_table)导入名称表,引用博客中的一个图片
该结构体中还存在另一个指针,FirstThunk
,他可以用来遍历IAT
重定位
exe一般不需要进行重定位,因为他优先加载到4G的虚拟内存中,但是dll需要。
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED *PIMAGE_BASE_RELOCATION;
#define IMAGE_SIZEOF_BASE_RELOCATION 8
这个重定位表,最后一个结构体的标记是VirtualAddress与SizeOfBlock都为0,其他的都有意义值。该结构位于程序的最后面,
注:如果一个程序不会被别的程序加载,那么这个程序的重定位表是用不上的,因为除了内核重载的情况,主程序的2G虚拟内存空间不会被占用。
参考:
模拟载入
- 根据文件的大小申请具有合适权限的内存空间
- 用0进行初始化内存空间
- 将PE文件利用ReadFile映射进去
- 修改重定位表(EXE不用重定位,直接忽略掉)
- 根据PE文件的导入表,加载需要的dll
- 获取导入函数的地址,写进导入表
- 修改PE的加载baseaddr
- 跳转到入口点开始执行
初始化空间
首先根据文件的映像大小申请空间并且用0初始化,因为PE文件都是用0进行对齐的。VirtualAlloc
开辟地址,转换类型char
,方便后续往里面写字节。
char path[] = "C:\\Users\\rootkit\\Desktop\\loadPE\\64test.exe";
// 根据PE文件结构得到映像大小
DWORD SizeOfImage = GetSizeOfPE(path, 1);
printf("[++] size of image is [0x%x]\n", SizeOfImage);
// 开辟可利用空间在内存中
char* chBaseAddress = (char*)VirtualAlloc(NULL, SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (NULL == chBaseAddress)
{
printf("申请进程空间失败 %d\n", GetLastError());
return NULL;
}
// 用 0 进行初始化处理
memset(chBaseAddress, 0, SizeOfImage);
printf("[////] addr is %p\n", chBaseAddress);
里面的主要调用函数还是使用winnt.h里的PE结构的一些结构体,然后使用fread的方式来将内容读到结构体里
DWORD GetSizeOfPE(char* peFile, int options)
{
DWORD size = 0;
FILE* fp = fopen(peFile, "rb");
// read PE e_lfnew
fseek(fp, 0x3c, SEEK_SET);
// get nt offset
unsigned char nt_offset = 0;
fread(&nt_offset, 1, 1, fp);
//printf("->%x<-", nt_offset);
// get option head offset, 24byte from PE\x00\x00
IMAGE_NT_HEADERS nt_header;
IMAGE_OPTIONAL_HEADER opt_header;
//IMAGE_FILE_HEADER file_header;
fseek(fp, nt_offset, SEEK_SET);
fread(&nt_header, 1, 0xf8, fp);
opt_header = nt_header.OptionalHeader;
switch (options)
{
// 1 是计算映像大小
case 1:
size = opt_header.SizeOfImage;
fclose(fp);
return size;
break;
// 2 是计算所有头的大小
case 2:
size = nt_header.OptionalHeader.SizeOfHeaders;
fclose(fp);
return size;
break;
// 3 是获取区段数量
case 3:
size = nt_header.FileHeader.NumberOfSections;
fclose(fp);
return size;
break;
// 4 定位节头表的位置
case 4:
// PE签名大小+nt头的开始位置+文件头大小+可选头大小
size = sizeof(IMAGE_FILE_HEADER) + nt_header.FileHeader.SizeOfOptionalHeader + 4 + nt_offset;
fclose(fp);
return size;
break;
default:
printf("case error\n");
return 0;
}
}
将头迁移过去
头部大小不需要进行一些对齐之类的操作,所以直接复制过去就可以。大小可以直接从 nt_header.OptionalHeader.SizeOfHeaders
这个值来获得。
// 计算所有头的大小
DWORD sizeofheader_all = GetSizeOfPE(path, 2);
// 获取区段数量
DWORD numofsection_all = GetSizeOfPE(path, 3);
// 获得程序内容
FILE* fp = fopen(path, "rb");
// get size of file in disk
fseek(fp, 0, SEEK_END);
unsigned long sizeofstaticfile = ftell(fp);
// get static file context
char* pe_buffer = (char*)malloc(sizeof(char) * sizeofstaticfile);
fseek(fp, 0, SEEK_SET);
fread(pe_buffer, 1, sizeofstaticfile, fp);
// 先把头部拷过去
memcpy(chBaseAddress, pe_buffer, sizeofheader_all);
将节区迁移过去
映射节区的方式需要使用节区头来进行。复制到内存中的地址就是开辟空间的baseaddress+节区头中的对应节区的VirtualAddress
的值来进行。
// 根据节区头来将节区复制到内容中去
PIMAGE_SECTION_HEADER psection_header = (PIMAGE_SECTION_HEADER)malloc(0x28);
// 定位到节头表的位置
int num_section_header = GetSizeOfPE(path, 4);
// 遍历所有节区,根据节区头从磁盘映射到内存中
for (i = 0; i < numofsection_all; i++)
{
fseek(fp, num_section_header+(0x28*i), SEEK_SET);
fread(psection_header, 1, sizeofheader_all - num_section_header, fp);
// 判读是否是最后一个节区
if (psection_header->VirtualAddress == 0 || psection_header->SizeOfRawData == 0)
{
psection_header++;
continue;
}
DWORD srcaddr = psection_header->PointerToRawData;
DWORD sizeofdata = psection_header->SizeOfRawData;
char* section_context = (char*)malloc(sizeofdata);
fseek(fp, srcaddr, SEEK_SET);
fread(section_context, 1, sizeofdata, fp);
// 计算对应的位置
char* destaddr = (char*)(psection_header->VirtualAddress + chBaseAddress);
memcpy(destaddr, section_context, sizeofdata);
printf("[**] %s section copy success\n", psection_header->Name);
free(section_context);
section_context = NULL;
psection_header = NULL;
}
printf("section copy finish\n");
// 重写重定位
// 重定位后的地址 = 需要重定位的地址 - 默认加载基址 + 当前加载基址
// 重定位个屁,exe不用重定位
// 直接修复导入表
需要注意的是,上述代码在malloc的时候会时不时的出现一些问题,这些bug尚未找到解决方式,gpt给出的代码也有这个错误。
修复导入表
修复导入表就是根据PE结构中的IID结构体数组来执行。思路就是先通过IID结构体中拿到导入模块的Name的RVA,然后检查是否已经载入内存中,没有的话就loadlibrary
去载入。
然后再通过OriginalFirstThunk
和FirstThunk
获取导入函数的名称和地址。
然后根据获得的导出名称或者是导出号,通过GetProcAddress
去载入。
char* Pdllname = NULL;
HMODULE hDll = NULL;
PIMAGE_THUNK_DATA lpImportNameArray = NULL;
PIMAGE_IMPORT_BY_NAME lpImportByName = NULL;
PIMAGE_THUNK_DATA lpImportFuncAddrArray = NULL;
FARPROC lpFuncAddress = NULL;
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)chBaseAddress;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(chBaseAddress + pDos->e_lfanew);
unsigned long v_size = pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
PIMAGE_IMPORT_DESCRIPTOR piid = (PIMAGE_IMPORT_DESCRIPTOR)((char*)pDos + v_size);
while (1)
{
// 判断是不是最后一个
if (piid->OriginalFirstThunk==0)
{
break;
}
// 动态载入需要的dll
Pdllname = (char*)((char*)pDos + piid->Name);
// 拿到模块的句柄,没有就loadlibrary载入
hDll = GetModuleHandleA(Pdllname);
if (hDll == NULL)
{
hDll = LoadLibraryA(Pdllname);
if (hDll == NULL)
{
piid++;
continue;
}
}
printf("[>>] get IID success\n");
i = 0;
// 获取OriginalFirstThunk以及对应的 导入函数名称表 首地址
lpImportNameArray = (PIMAGE_THUNK_DATA)((char*)pDos + piid->OriginalFirstThunk);
// 获取FirstThunk以及对应的 导入函数地址表 首地址
lpImportFuncAddrArray = (PIMAGE_THUNK_DATA)((char*)pDos + piid->FirstThunk);
while (TRUE)
{
if (lpImportNameArray[i].u1.AddressOfData == 0)
{
break;
}
// 获取IMAGE_IMPORT_BY_NAME结构
lpImportByName = (PIMAGE_IMPORT_BY_NAME)((char*)pDos + lpImportNameArray[i].u1.AddressOfData);
// 判断导出函数是 序号导出 or 名称导出
if (0x80000000 & lpImportNameArray[i].u1.Ordinal)
{
// 序号导出
lpFuncAddress = GetProcAddress(hDll, (LPCSTR)(lpImportNameArray[i].u1.Ordinal & 0x0000FFFF));
}
else
{
// 名称导出
lpFuncAddress = GetProcAddress(hDll, (LPCSTR)lpImportByName->Name);
}
lpImportFuncAddrArray[i].u1.Function = lpFuncAddress;
i++;
}
piid++;
}
printf("[%%] reparie IAT success\n");
// 修改PE文件的加载基地址,设置入口点
这里的代码没有什么问题,可以正常执行。
修改EP执行
修改PE文件的入口点,然后跳到入口点去执行
// 修改PE文件的加载基地址,设置入口点
pNt->OptionalHeader.ImageBase = (DWORD)(uintptr_t)chBaseAddress;
char* EntryPoint = (DWORD)(uintptr_t)((char*)chBaseAddress + pNt->OptionalHeader.AddressOfEntryPoint);
printf("[##] reset OEP success\n");
free(pe_buffer);
CreateRemoteThread(GetCurrentProcess(), 0, SizeOfImage, (LPTHREAD_START_ROUTINE)EntryPoint, 0, 0, NULL);
// https://blog.csdn.net/v_wus/article/details/122112081
//__asm
//{
// mov eax, EntryPoint
// jmp eax
//}
所有代码
#include<Windows.h>
#include<stdio.h>
#include<winnt.h>
#pragma warning(disable : 4996)
// use to calc the size in PE
DWORD GetSizeOfPE(char* peFile, int options)
{
DWORD size = 0;
FILE* fp = fopen(peFile, "rb");
// read PE e_lfnew
fseek(fp, 0x3c, SEEK_SET);
// get nt offset
unsigned char nt_offset = 0;
fread(&nt_offset, 1, 1, fp);
//printf("->%x<-", nt_offset);
// get option head offset, 24byte from PE\x00\x00
IMAGE_NT_HEADERS nt_header;
IMAGE_OPTIONAL_HEADER opt_header;
//IMAGE_FILE_HEADER file_header;
fseek(fp, nt_offset, SEEK_SET);
fread(&nt_header, 1, 0xf8, fp);
opt_header = nt_header.OptionalHeader;
switch (options)
{
// 1 是计算映像大小
case 1:
size = opt_header.SizeOfImage;
fclose(fp);
return size;
break;
// 2 是计算所有头的大小
case 2:
size = nt_header.OptionalHeader.SizeOfHeaders;
fclose(fp);
return size;
break;
// 3 是获取区段数量
case 3:
size = nt_header.FileHeader.NumberOfSections;
fclose(fp);
return size;
break;
// 4 定位节头表的位置
case 4:
// PE签名大小+nt头的开始位置+文件头大小+可选头大小
size = sizeof(IMAGE_FILE_HEADER) + nt_header.FileHeader.SizeOfOptionalHeader + 4 + nt_offset;
fclose(fp);
return size;
break;
default:
printf("case error\n");
return 0;
}
}
int main()
{
unsigned int i;
char path[] = "C:\\Users\\rootkit\\Desktop\\loadPE\\64test.exe";
// 根据PE文件结构得到映像大小
DWORD SizeOfImage = GetSizeOfPE(path, 1);
printf("[++] size of image is [0x%x]\n", SizeOfImage);
// 开辟可利用空间在内存中
char* chBaseAddress = (char*)VirtualAlloc(NULL, SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (NULL == chBaseAddress)
{
printf("申请进程空间失败 %d\n", GetLastError());
return NULL;
}
// 用 0 进行初始化处理
memset(chBaseAddress, 0, SizeOfImage);
printf("[////] addr is %p\n", chBaseAddress);
// 从磁盘到上述空间中进行映射
// 计算所有头的大小
DWORD sizeofheader_all = GetSizeOfPE(path, 2);
// 获取区段数量
DWORD numofsection_all = GetSizeOfPE(path, 3);
// 获得程序内容
FILE* fp = fopen(path, "rb");
// get size of file in disk
fseek(fp, 0, SEEK_END);
unsigned long sizeofstaticfile = ftell(fp);
// get static file context
char* pe_buffer = (char*)malloc(sizeof(char) * sizeofstaticfile);
fseek(fp, 0, SEEK_SET);
fread(pe_buffer, 1, sizeofstaticfile, fp);
// 先把头部拷过去
memcpy(chBaseAddress, pe_buffer, sizeofheader_all);
// 根据节区头来将节区复制到内容中去
PIMAGE_SECTION_HEADER psection_header = (PIMAGE_SECTION_HEADER)malloc(0x28);
// 定位到节头表的位置
int num_section_header = GetSizeOfPE(path, 4);
// 遍历所有节区,根据节区头从磁盘映射到内存中
for (i = 0; i < numofsection_all; i++)
{
fseek(fp, num_section_header+(0x28*i), SEEK_SET);
fread(psection_header, 1, sizeofheader_all - num_section_header, fp);
// 判读是否是最后一个节区
if (psection_header->VirtualAddress == 0 || psection_header->SizeOfRawData == 0)
{
psection_header++;
continue;
}
DWORD srcaddr = psection_header->PointerToRawData;
DWORD sizeofdata = psection_header->SizeOfRawData;
char* section_context = (char*)malloc(sizeofdata);
fseek(fp, srcaddr, SEEK_SET);
fread(section_context, 1, sizeofdata, fp);
char* destaddr = (char*)(psection_header->VirtualAddress + chBaseAddress);
memcpy(destaddr, section_context, sizeofdata);
printf("[**] %s section copy success\n", psection_header->Name);
free(section_context);
section_context = NULL;
psection_header = NULL;
}
printf("section copy finish\n");
// 重写重定位
// 重定位后的地址 = 需要重定位的地址 - 默认加载基址 + 当前加载基址
// 重定位个屁,exe不用重定位
// 直接修复导入表
char* Pdllname = NULL;
HMODULE hDll = NULL;
PIMAGE_THUNK_DATA lpImportNameArray = NULL;
PIMAGE_IMPORT_BY_NAME lpImportByName = NULL;
PIMAGE_THUNK_DATA lpImportFuncAddrArray = NULL;
FARPROC lpFuncAddress = NULL;
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)chBaseAddress;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(chBaseAddress + pDos->e_lfanew);
unsigned long v_size = pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
PIMAGE_IMPORT_DESCRIPTOR piid = (PIMAGE_IMPORT_DESCRIPTOR)((char*)pDos + v_size);
while (1)
{
// 判断是不是最后一个
if (piid->OriginalFirstThunk==0)
{
break;
}
// 动态载入需要的dll
Pdllname = (char*)((char*)pDos + piid->Name);
// 拿到模块的句柄,没有就loadlibrary载入
hDll = GetModuleHandleA(Pdllname);
if (hDll == NULL)
{
hDll = LoadLibraryA(Pdllname);
if (hDll == NULL)
{
piid++;
continue;
}
}
printf("[>>] get IID success\n");
i = 0;
// 获取OriginalFirstThunk以及对应的 导入函数名称表 首地址
lpImportNameArray = (PIMAGE_THUNK_DATA)((char*)pDos + piid->OriginalFirstThunk);
// 获取FirstThunk以及对应的 导入函数地址表 首地址
lpImportFuncAddrArray = (PIMAGE_THUNK_DATA)((char*)pDos + piid->FirstThunk);
while (TRUE)
{
if (lpImportNameArray[i].u1.AddressOfData == 0)
{
break;
}
// 获取IMAGE_IMPORT_BY_NAME结构
lpImportByName = (PIMAGE_IMPORT_BY_NAME)((char*)pDos + lpImportNameArray[i].u1.AddressOfData);
// 判断导出函数是 序号导出 or 名称导出
if (0x80000000 & lpImportNameArray[i].u1.Ordinal)
{
// 序号导出
lpFuncAddress = GetProcAddress(hDll, (LPCSTR)(lpImportNameArray[i].u1.Ordinal & 0x0000FFFF));
}
else
{
// 名称导出
lpFuncAddress = GetProcAddress(hDll, (LPCSTR)lpImportByName->Name);
}
lpImportFuncAddrArray[i].u1.Function = lpFuncAddress;
i++;
}
piid++;
}
printf("[%%] reparie IAT success\n");
// 修改PE文件的加载基地址,设置入口点
pNt->OptionalHeader.ImageBase = (DWORD)(uintptr_t)chBaseAddress;
char* EntryPoint = (DWORD)(uintptr_t)((char*)chBaseAddress + pNt->OptionalHeader.AddressOfEntryPoint);
printf("[##] reset OEP success\n");
free(pe_buffer);
CreateRemoteThread(GetCurrentProcess(), 0, SizeOfImage, (LPTHREAD_START_ROUTINE)EntryPoint, 0, 0, NULL);
// https://blog.csdn.net/v_wus/article/details/122112081
//__asm
//{
// mov eax, EntryPoint
// jmp eax
//}
return 0;
}
一些问题
vs在64bit的时候,无法使用上述语法内敛汇编,所以使用CreateRemoteThread
函数来执行。
上述代码执行并不稳定,在malloc的时候会出现不能稳定复现的问题,如果malloc不报错,在GetModuleHandleA
的时候也会出现问题,目前还没有搞清楚为啥。
参考:
原创]PE加载器的简单实现-编程技术-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)