摘要:磁盘中的PE文件如果想要在操作系统上运行就必须要将其加载到内存中。同样,如果我们要对一个磁盘PE文件进行读写操作,也需要将其加载到内存。接下来我们将通过实验介绍两种不同的方法,将一个磁盘PE文件读进内存。我们先介绍第一种方法,使用ReadFile函数将PE文件
磁盘中的PE文件如果想要在操作系统上运行就必须要将其加载到内存中。同样,如果我们要对一个磁盘PE文件进行读写操作,也需要将其加载到内存。接下来我们将通过实验介绍两种不同的方法,将一个磁盘PE文件读进内存。我们先介绍第一种方法,使用ReadFile函数将PE文件读入一个由VirtualAlloc函数分配的虚拟内存空间。
实验十九:ReadFile将PE文件读进内存(C语言)
以32位和64位记事本程序为例,使用ReadFile函数将记事本程序以内存格式读进内存:
●源码
/*
FileName:ReadPe1.c
实验19:ReadFile将PE文件读进内存
(c) bcdaren, 2024
*/
#include
#include
#defineX64
intmain(intargc, char* argv)
{
HANDLEhFile;
LPVOIDlpvResult, lpvResult2;
charbuffer[16] = {0};
DWORDdwPageSize;
DWORDdwBytesRead = 0;
BOOLbReadFile;
LPCSTRszFileName = TEXT("c:\\notepad64.exe");
PIMAGE_DOS_HEADERpsImageDOSHeader;
#ifdefX64
PIMAGE_NT_HEADERS64psImageNTHeader;
#else
PIMAGE_NT_HEADERS32psImageNTHeader;
#endif
PIMAGE_SECTION_HEADERsImageSecctionHeader[3]; //不一定为3个节表项
hFile = CreateFile(szFileName,
GENERIC_READ, // 只读打开
FILE_SHARE_READ, // 允许其他进程以读取方式打开文件
NULL, // 默认安全属性
OPEN_EXISTING, // 打开已存在的文件
FILE_ATTRIBUTE_NORMAL, // 普通文件
NULL);
dwPageSize = GetFileSize(hFile, 0);//获得文件大小可通过结构体获取
lpvResult = VirtualAlloc(// 给文件分配内存空间
NULL, // 系统自动选择起始地址
dwPageSize, // 指定要分配的内存大小,以字节为单位
MEM_COMMIT, // 将申请到的虚拟内存提交到物理内存
PAGE_READWRITE); // read/write 属性
//将文件读至分配的内存
bReadFile = ReadFile(hFile, lpvResult, dwPageSize, &dwBytesRead, 0);
//实现将磁盘文件地址偏移至内存地址
#ifdefX64//64位记事本有6个节区,只打印前3个节区
psImageDOSHeader = (PIMAGE_DOS_HEADER)lpvResult;
psImageNTHeader = (PIMAGE_NT_HEADERS64)
((BYTE*)lpvResult + psImageDOSHeader->e_lfanew);
sImageSecctionHeader[0] = (PIMAGE_SECTION_HEADER)
((BYTE*)psImageNTHeader + sizeof(IMAGE_NT_HEADERS64));
sImageSecctionHeader[1] = (PIMAGE_SECTION_HEADER)
((BYTE*)sImageSecctionHeader[0] + sizeof(IMAGE_SECTION_HEADER));
sImageSecctionHeader[2] = (PIMAGE_SECTION_HEADER)
((BYTE*)sImageSecctionHeader[1] + sizeof(IMAGE_SECTION_HEADER));
#else
psImageNTHeader = (PIMAGE_NT_HEADERS32)
((BYTE*)psImageNTHeader + sizeof(IMAGE_NT_HEADERS32));
#endif
//已4KB为单位分配内存空间
dwPageSize = sImageSecctionHeader[2]->Misc.VirtualSize +
sImageSecctionHeader[2]->VirtualAddress;
dwPageSize = (dwPageSize / 0x1000 + 1) * 0x1000;//已4KB为单位对齐
lpvResult2 = VirtualAlloc(
NULL
dwPageSize, // 内存大小, 以字节为单位
//拷贝PE文件头
#ifdefX64
CopyMemory(lpvResult2, lpvResult, psImageDOSHeader->e_lfanew +
sizeof(IMAGE_NT_HEADERS64) + 3 * sizeof(IMAGE_SECTION_HEADER));
#else
sizeof(IMAGE_NT_HEADERS32) + 3 * sizeof(IMAGE_SECTION_HEADER));
#endif
//拷贝第0个节区数据
CopyMemory((BYTE*)lpvResult2 + sImageSecctionHeader[0]->VirtualAddress
(BYTE*)lpvResult + sImageSecctionHeader[0]->PointerToRawData
sImageSecctionHeader[0]->Misc.VirtualSize);
//拷贝第1个节区数据
CopyMemory(
(BYTE*)lpvResult2 + sImageSecctionHeader[1]->VirtualAddress
(BYTE*)lpvResult + sImageSecctionHeader[1]->PointerToRawData
sImageSecctionHeader[1]->Misc.VirtualSize);
//拷贝第2个节区数据
CopyMemory(
(BYTE*)lpvResult2 + sImageSecctionHeader[2]->VirtualAddress
(BYTE*)lpvResult + sImageSecctionHeader[2]->PointerToRawData
sImageSecctionHeader[2]->Misc.VirtualSize);
//打印输出
printf(" Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F\n");
printf("
\n");
for (DWORDi = 0; i
{
if (i % 8 == 0) printf(" ");
if (i % 16 == 0 )
{
if (i != 0)
CopyMemory(buffer, (PBYTE)lpvResult2 + i - 16,16);
for (intj = 0;j
{
if(*((PBYTE)buffer + j)>=32 && *((PBYTE)buffer + j)
printf("%c", *((PBYTE)buffer + j));
else
printf(".");
}
memset(buffer,0,16);
printf("\n %08X ",i);
}
printf("% 02X ", *((PBYTE)lpvResult2 + i));
CloseHandle(hFile);
printf("\n");
system("pause");
return 0;
注意
1.此例为控制台程序,在控制台窗口输出读进内存中PE文件二进制数据。
2.读进内存中的PE文件按内存格式存储,以4KB(1000H)为单位对齐,其中PE文件头占据一页内存空间。虚拟内存空间由VirtualAlloc函数按PE文件在内存中以4KB(页)为单位对其后的大小分配。
3.为了在控制台窗口右侧输出ASCII码字符,采用ANSI多字节字符集。
4.程序采用条件编译形式,支持读取32位PE文件和64位PE文件。
5.程序只读取了64位notepad64.exe前3个节区,由于输出内容超过一页,控制台窗口无法打印全部内容,可以采用命令行定向输出的方式,将打印内容输出到文本文件中。打开命令行窗口,命令行命令如下所示:
Microsoft Windows [版本 10.0.16299.2166]
(c) 2017 Microsoft Corporation。保留所有权利。
C:\Users\16400>d:
D:\code\winpe\Debug>ReadPe.exe > output.txt
D:\code\winpe\Debug>
运行:
图3-19 打印输出内存中的PE文件一
总结
示例代码中,我们分析一下实现的流程:
第一步:首先调用CreateFile函数打开一个磁盘PE文件。
第二步:调用GetFileSize函数获取文件大小。
第三步:调用VirtualAlloc函数分配与PE文件相同大小的缓冲区。
第四步:调用ReadFile函数将PE文件读入缓冲区。
第五步:计算并调用VirtualAlloc函数分配PE映像文件所需内存空间的大小。计算方法为取最后一个节区在虚拟内存中的起始地址+节区的实际大小,再向下取整。
//以4KB为单位分配内存空间
dwPageSize = sImageSecctionHeader[2]->Misc.VirtualSize +
sImageSecctionHeader[2]->VirtualAddress;
dwPageSize = (dwPageSize / 0x1000 + 1) * 0x1000;//已4KB为单位对齐
第六步:拷贝PE文件头和节区到已分配的虚拟内空间。
//从文件拷贝到内存:
CopyMemory(pNFileBuffer,pImageInMem,pImageOptionalHeader->SizeOfHeaders);
for (i = 0 ; i NumberOfSections ; i++)
{
CopyMemory(pNFileBuffer+pImageSectionHeader->PointerToRawData ,
pImageInMem + pImageSectionHeader->VirtualAddress ,
pImageSectionHeader->SizeOfRawData);
pImageSectionHeader++;
}
第七步:在控制台窗口打印PE内存映像,或者以文件的形式将PE映像文件写入磁盘。
【流程图】
实验二十:CreateFileMapping将PE文件读进内存(C语言)
以32位和64位记事本程序为例,使用CreatFileMapping函数将记事本程序以内存格式读进内存:
●源码
/*
FileName:ReadPe2.c
实验20:使用文件映射将PE文件读进内存
(c) bcdaren, 2024
*/
#include
#include
//#define X64
{
HANDLEhFile;
HANDLEhMapFile = NULL;
PBYTElpMemory = NULL; //PE文件内存映射文件地址
charbuffer[16] = {0};
DWORDdwFileSize;
DWORDdwBytesRead = 0;
LPCSTRszFileName = TEXT("c:\\notepad32.exe");
PIMAGE_DOS_HEADERlpstDOS;
#ifdef X64
PIMAGE_NT_HEADERS64lpstNT;
#else
PIMAGE_NT_HEADERS32lpstNT;
#endif
//PIMAGE_SECTION_HEADER sImageSecctionHeader[3]; //不一定为3个节表项
NULL);
if (hFile == INVALID_HANDLE_VALUE)
printf("打开文件失败!\n");
else
{
dwFileSize = GetFileSize(hFile, 0);//获得文件大小可通过结构体获取
//创建内存映射文件
if (dwFileSize)
{
if (hMapFile = CreateFileMapping(hFile,NULL, PAGE_READONLY, 0, 0, NULL))
{
//获得文件在内存的映象起始位置
lpMemory = MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, 0);
if (!lpMemory)
printf("获取映像起始地址失败!\n");
//检查PE文件是否有效
lpstDOS = (IMAGE_DOS_HEADER *)lpMemory;
if (lpstDOS->e_magic != IMAGE_DOS_SIGNATURE)
printf("非PE格式文件!\n");
#ifdef X64
lpstNT = (PIMAGE_NT_HEADERS64)(lpMemory + lpstDOS->e_lfanew);
#else
lpstNT = (PIMAGE_NT_HEADERS32)(lpMemory + lpstDOS->e_lfanew);
#endif
if (lpstNT->Signature != IMAGE_NT_SIGNATURE)
}
}
}
//打印输出
printf(" Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F\n");
printf("\n");
for (DWORDi = 0; i
{
if (i % 16 == 0 )
{
if (i != 0)
CopyMemory(buffer, (PBYTE)lpMemory + i - 16,16);
{
else
printf(".");
}
}
printf("% 02X ", *((PBYTE)lpMemory + i));
}
return 0;
}
运行:
图3-20 使用映射方法读取PE文件
总结
1.示例代码实现流程:
实验二十使用文件映射的方法将PE文件读取到内存映射区。
第一步调用CreateFile函数打开磁盘PE文件。
第二步调用GetFileSize函数获取PE文件大小。
第三步调用CreateFileMapping函数创建文件映射对象。
第四步调用MapViewOfFile函数将文件映射的视图映射到调用进程的地址空间中。返回文件中映射区内的起始地址。
上述将PE文件读进内存的实现方法简单便捷,比使用ReadFile函数读取PE文件的效率高的多。
【注意】 PE有两个状态 一种是文件状态(FileBuffer) 一种是内存状态(ImageBuffer )。
2.拷贝节表
现在理解PE被分为多个区块,我们称之为节区。节区是错开的,其实PE错开的可能非常大,只是我们见到的节区并没错开。磁盘PE文件节表是可以交叉,可以重叠,可以乱序。而内存中的PE映像文件不可以交叉乱序重叠。如图3-21所示:
来源:小羊科技论