郑重声明:文中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,如果您不同意请关闭该页面!任何人不得将其用于非法用途以及盈利等目的,否则后果自行承担!

前言

之前为了学习PE结构知识恶补了一下相关文章,然后使用010 Editor这个软件也可以完整的读取出PE的结构和字段,但是这都是别人写好的,自己没有玩过一遍的东西都不叫学过,那只能叫见过所以就有了这篇文章,各位看官别急需要几天时间才能搞定,毕竟还不熟悉如果有哪里写错了,忘各位斧正!(PS:当前代码输出不够美观后期会修改)

使用环境

  • Visual Studio 2019 (宇宙最强编译器
  • windows 10

前置知识

恶意程序研究之PE结构梳理

获取文件映象

首先介绍下内存映射文件技术作用

  • 使用内存映射文件来访问磁盘上的数据文件。这使你可以不必对文件执行I/O操作,并且可以不必对文件内容进行缓存.
  • 可以使用内存映射文件,使同一台计算机上运行的多个进程能够相互之间共享数据。windows确实提供了其他一些方法,以便在进程之间进行数据通信,但是这些方法都是使用内存映射文件来实现的,诸如使用SendMessage或者PostMessage,都在内部使用了内存映射文件.这使得内存映射文件成为单个计算机上的多个进程互相进行通信的最有效的方法。

其实我们需要获取文件的内存映射的话需要完成三步

  • 获取这个文件的句柄(CreateFile)
  • 获取文件映射对象的句柄(CreateFileMapping)
  • 在调用进程的地址空间映射一个文件视图(MapViewOfFile)

具体的代码如下,返回一个映射视图的起始地址,这个地址会贯穿全文

HANDLE OpenPeByFileName(LPTSTR FileName)
{
LPTSTR PortableExecutableFileName = FileName;//获取PE文件名字
HANDLE hFile, hMapFile, hMapAddress = NULL;
DWORD dwFileSize = 0;

hFile = CreateFile(PortableExecutableFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);//获取程序句柄
dwFileSize = GetFileSize(hFile, NULL);
std::cout<<"文件大小: " << dwFileSize<<std::endl;
hMapFile = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, dwFileSize, NULL);//创建文件映象
hMapAddress = MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, dwFileSize);//在调用进程的地址空间映射一个文件视图,返回一个映射视图的起始地址
//把B进程的文件映象存放到A中,这样好通信
if (hMapAddress != NULL)
return hMapAddress;
}
int main()
{
HANDLE hMapAddress = NULL;
lpMapAddress = OpenPeByFileName(L"D:\\BitComet\\BitComet.exe");
return 0;
}

获取PE指纹(DOS头)

通过把文件映象转换成PIMAGE_DOS_HEADER表,然后读取就可以

void DisplayDOSHeadInfo(HANDLE ImageBase)
{
PIMAGE_DOS_HEADER pDosHead = NULL;
pDosHead = (PIMAGE_DOS_HEADER)ImageBase;

printf("DOS签名(MZSignature): %x\n", pDosHead->e_magic);
printf("文件最后页的字节数(UsedBytesInTheLastPage): %x\n", pDosHead->e_cblp);
printf("文件页数(FileSizeInPages): %x\n", pDosHead->e_cp);
printf("重定义元素个数(NumberOfRelocationItems): %x\n", pDosHead->e_crlc);
printf("头部尺寸(HeaderSizeInParagraphs): %x\n", pDosHead->e_cparhdr);
printf("所需的最小附加段(MinimumExtraParagraphs): %x\n", pDosHead->e_minalloc);
printf("所需的最大附加段(MaximumExtraParagraphs): %x\n", pDosHead->e_maxalloc);
printf("初始的SS值(InitialRelativeSS): %x\n", pDosHead->e_ss);
printf("初始的SP值(InitialSP): %x\n", pDosHead->e_sp);
printf("校验和(Checksum): %x\n", pDosHead->e_csum);
printf("初始的IP值(InitialIP): %x\n", pDosHead->e_ip);
printf("初始的CS值(InitialRelativeCS): %x\n", pDosHead->e_cs);
printf("重分配表文件地址(AddressOfRelocationTable): %x\n", pDosHead->e_lfarlc);
printf("覆盖号(OverlayNumber): %x\n", pDosHead->e_ovno);
printf("保留字[4](Reserved):\n");
printf("Reserved[0]: %x\n", pDosHead->e_res[0]);
printf("Reserved[1]: %x\n", pDosHead->e_res[1]);
printf("Reserved[2]: %x\n", pDosHead->e_res[2]);
printf("Reserved[3]: %x\n", pDosHead->e_res[3]);
printf("OEM标识符(OEMid): %x\n", pDosHead->e_oemid);
printf("OEM信息(OEMinfo): %x\n", pDosHead->e_oeminfo);
printf("保留字[10](Reserved2):\n");
printf("Reserved2[0]: %x\n", pDosHead->e_res2[0]);
printf("Reserved2[1]: %x\n", pDosHead->e_res2[1]);
printf("Reserved2[2]: %x\n", pDosHead->e_res2[2]);
printf("Reserved2[3]: %x\n", pDosHead->e_res2[3]);
printf("Reserved2[4]: %x\n", pDosHead->e_res2[4]);
printf("Reserved2[5]: %x\n", pDosHead->e_res2[5]);
printf("Reserved2[6]: %x\n", pDosHead->e_res2[6]);
printf("Reserved2[7]: %x\n", pDosHead->e_res2[7]);
printf("Reserved2[8]: %x\n", pDosHead->e_res2[8]);
printf("Reserved2[9]: %x\n", pDosHead->e_res2[9]);
printf("指示NT头的偏移(AddressOfNewExeHeader): %x\n", pDosHead->e_lfanew);
}

可以看到我们在使用010 EditorVS 2019中读取的内容一致

image-20200705192958733

输出内容

image-20200705193058120

获取NT头

获取NT表的句柄,因为后面都需要用到,所以这边直接封装成函数就好

PIMAGE_NT_HEADERS GetNtHead(HANDLE ImageBase)
{
PIMAGE_DOS_HEADER pDosHead = NULL;
PIMAGE_NT_HEADERS pNtHead = NULL;
pDosHead = (PIMAGE_DOS_HEADER)ImageBase;
pNtHead = (PIMAGE_NT_HEADERS)((DWORD)pDosHead + pDosHead->e_lfanew);
return pNtHead;
}
Signatur

这个参数和IMAGE_FILE_HEADER表一起获取即可

IMAGE_FILE_HEADER

这个表示在NT表下面所以直接调用就好

void DisplayFileHeaderInfo(HANDLE ImageBase)
{
PIMAGE_NT_HEADERS pNtHead = NULL;
pNtHead = GetNtHead(ImageBase);
printf("签名(Signatur): %x\n", pNtHead->Signature);//这个是Signatur值
printf("运行平台(Machine): %x\n", pNtHead->FileHeader.Machine);
printf("节区数目(NumberOfSections): %x\n", pNtHead->FileHeader.NumberOfSections);
printf("创建时间(TimeDateStamp): %x\n", pNtHead->FileHeader.TimeDateStamp);
printf("COFF文件符号表偏移(PointerToSymbolTable): %x\n", pNtHead->FileHeader.PointerToSymbolTable);
printf("符号表的数量(NumberOfSymbols): %x\n", pNtHead->FileHeader.NumberOfSymbols);
printf("可选头大小(SizeOfOptionalHeader): %x\n", pNtHead->FileHeader.SizeOfOptionalHeader);
printf("文件特性(Characteristics): %x\n", pNtHead->FileHeader.Characteristics);
}

可以看到输出内容完全一致

image-20200705194630967

输出内容

image-20200705194438923

IMAGE_OPTIONAL_HEADER

直接贴代码,因为该表中IMAGE_DATA_DIRECTORY表没啥用这边就展示不写了,后面有时间补上

void DisplayOptionalHeaderInfo(HANDLE ImageBase)
{
PIMAGE_NT_HEADERS pNtHead = NULL;
pNtHead = GetNtHead(ImageBase);
printf("魔数(Magic): %x\n", pNtHead->OptionalHeader.Magic);
printf("链接器的主版本号(MajorLinkerVersion): %x\n", pNtHead->OptionalHeader.MajorLinkerVersion);
printf("链接器的次版本号(MinorLinkerVersion): %x\n", pNtHead->OptionalHeader.MinorLinkerVersion);
printf("代码节大小(SizeOfCode): %x\n", pNtHead->OptionalHeader.SizeOfCode);
printf("已初始化数大小(SizeOfInitializedData): %x\n", pNtHead->OptionalHeader.SizeOfInitializedData);
printf("未初始化数大小(SizeOfUninitializedData): %x\n", pNtHead->OptionalHeader.SizeOfUninitializedData);
printf("最先执行代码起始地址(AddressOfEntryPoint): %x\n", pNtHead->OptionalHeader.AddressOfEntryPoint);
printf("代码基址(BaseOfCode): %x\n", pNtHead->OptionalHeader.BaseOfCode);
printf("数据基址(BaseOfData): %x\n", pNtHead->OptionalHeader.BaseOfData);
printf("镜像首地址(ImageBase): %x\n", pNtHead->OptionalHeader.ImageBase);
printf("节段在内存中的最小单位(SectionAlignment): %x\n", pNtHead->OptionalHeader.SectionAlignment);
printf("节段在磁盘文件中的最小单位(FileAlignment): %x\n", pNtHead->OptionalHeader.FileAlignment);
printf("主系统的主版本号(MajorOperatingSystemVersion): %x\n", pNtHead->OptionalHeader.MajorOperatingSystemVersion);
printf("主系统的次版本号(MinorOperatingSystemVersion): %x\n", pNtHead->OptionalHeader.MinorOperatingSystemVersion);
printf("镜像的主版本号(MajorImageVersion): %x\n", pNtHead->OptionalHeader.MajorImageVersion);
printf("镜像的次版本号(MinorImageVersion): %x\n", pNtHead->OptionalHeader.MinorImageVersion);
printf("子系统的主版本号(MajorSubsystemVersion): %x\n", pNtHead->OptionalHeader.MajorSubsystemVersion);
printf("子系统的次版本号(MinorSubsystemVersion): %x\n", pNtHead->OptionalHeader.MinorSubsystemVersion);
printf("保留字段(Win32VersionValue): %x\n", pNtHead->OptionalHeader.Win32VersionValue);
printf("镜像被加载进内存时的大小(SizeOfImage): %x\n", pNtHead->OptionalHeader.SizeOfImage);
printf("所有头的总大小(SizeOfHeaders): %x\n", pNtHead->OptionalHeader.SizeOfHeaders);
printf("镜像文件的校验和(CheckSum): %x\n", pNtHead->OptionalHeader.CheckSum);
printf("运行此镜像所需的子系统(Subsystem): %x\n", pNtHead->OptionalHeader.Subsystem);
printf("DLL标识(DllCharacteristics): %x\n", pNtHead->OptionalHeader.DllCharacteristics);
printf("最大栈大小(SizeOfStackReserve): %x\n", pNtHead->OptionalHeader.SizeOfStackReserve);
printf("初始提交的堆栈大小(SizeOfStackCommit): %x\n", pNtHead->OptionalHeader.SizeOfStackCommit);
printf("最大堆大小(SizeOfHeapReserve): %x\n", pNtHead->OptionalHeader.SizeOfHeapReserve);
printf("初始提交的局部堆空间大小(SizeOfHeapCommit): %x\n", pNtHead->OptionalHeader.SizeOfHeapCommit);
printf("保留字段(LoaderFlags): %x\n", pNtHead->OptionalHeader.LoaderFlags);
printf("DataDirectory的数组个数(NumberOfRvaAndSizes): %x\n", pNtHead->OptionalHeader.NumberOfRvaAndSizes);
}

对比两者的值

image-20200705194853197

输出内容

image-20200705194956218

获取导入表

有些文件是没有办法获取到导入表的,但是只要是正常的PE文件的话还是可以获取的

直接贴代码

void DisplayImportTable(HANDLE ImageBase)
{
PIMAGE_DOS_HEADER pDosHead = NULL;
PIMAGE_NT_HEADERS pNtHead = NULL;
PIMAGE_IMPORT_DESCRIPTOR pInput = NULL;
PIMAGE_THUNK_DATA _pThunk = NULL;
DWORD dwThunk = NULL;
DWORD dwOriginalFirstThunk = NULL;
USHORT Hint;
int iSystemBits = 0;

pDosHead = (PIMAGE_DOS_HEADER)ImageBase;
pNtHead = GetNtHead(ImageBase);
//判断版本是32位还是64位
if (pNtHead->FileHeader.Machine== IMAGE_FILE_MACHINE_I386)
{
iSystemBits = 32;
}
if (pNtHead->FileHeader.Machine == (IMAGE_FILE_MACHINE_IA64 || IMAGE_FILE_MACHINE_AMD64))
{
iSystemBits = 64;
}
if (pNtHead->OptionalHeader.DataDirectory[1].VirtualAddress == 0) { return; } // 读取导入表RVA
pInput = (PIMAGE_IMPORT_DESCRIPTOR)ImageRvaToVa((PIMAGE_NT_HEADERS)pNtHead, pDosHead, pNtHead->OptionalHeader.DataDirectory[1].VirtualAddress, NULL);
for (; pInput->Name != NULL;)
{
char* szFunctionModule = (PSTR)ImageRvaToVa((PIMAGE_NT_HEADERS)pNtHead, pDosHead, (ULONG)pInput->Name, NULL); // 遍历出模块名称
dwThunk = pInput->FirstThunk;
dwOriginalFirstThunk= pInput->OriginalFirstThunk;
_pThunk = (PIMAGE_THUNK_DATA)ImageRvaToVa((PIMAGE_NT_HEADERS)pNtHead, pDosHead, (ULONG)pInput->FirstThunk, NULL);

for (; _pThunk->u1.AddressOfData != NULL;)
{

char* szFunction = (PSTR)ImageRvaToVa((PIMAGE_NT_HEADERS)pNtHead, pDosHead, (ULONG)(((PIMAGE_IMPORT_BY_NAME)_pThunk->u1.AddressOfData)->Name), 0);
if (szFunction != NULL)
{
memcpy(&Hint, szFunction - 2, 2);
}
else
{
Hint = -1;
}
if (Hint== NULL || dwThunk == NULL || dwOriginalFirstThunk == NULL || szFunctionModule == NULL || szFunction == NULL)
{

}
else
{
std::cout << "序号:" << Hint << std::endl;
std::cout << "Thunk RVA:" << std::hex << dwThunk << std::endl;
std::cout << "OriginalFirstThunk:" << std::hex << dwOriginalFirstThunk << std::endl;
std::cout << "DLL名称:" << szFunctionModule << std::endl;
std::cout << "API名称:" << szFunction << std::endl;
std::cout << "--------------------------------------------------" << std::endl;
}

//32位 +4 64位+8
if (iSystemBits == 32)
{
dwThunk += 4;
}
else if (iSystemBits == 64)
{
dwThunk += 8;
}
_pThunk++;
}
pInput++;
}
}

image-20200713172823169

获取导出表

这个表一般都在DLL文件中所以,这边换了个DLL文件来获取,直接贴代码

VOID DisplayExportTable(HANDLE ImageBase)
{

PIMAGE_EXPORT_DIRECTORY pExport;
PIMAGE_DOS_HEADER pDosHead = NULL;
PIMAGE_NT_HEADERS pNtHead = NULL;
pDosHead = (PIMAGE_DOS_HEADER)ImageBase;
pNtHead = (PIMAGE_NT_HEADERS)((DWORD)pDosHead + pDosHead->e_lfanew);
if (pNtHead->Signature != 0x00004550) { return; } // 无效PE文件

pExport = (PIMAGE_EXPORT_DIRECTORY)ImageRvaToVa((PIMAGE_NT_HEADERS)pNtHead, pDosHead, pNtHead->OptionalHeader.DataDirectory[0].VirtualAddress, NULL);
DWORD NumberOfNames = pExport->NumberOfNames;
ULONGLONG** ppdwNames = (ULONGLONG**)pExport->AddressOfNames;
ppdwNames = (PULONGLONG*)ImageRvaToVa((PIMAGE_NT_HEADERS)pNtHead, pDosHead, (ULONG)ppdwNames, NULL);
ULONGLONG** ppdwAddr = (ULONGLONG**)pExport->AddressOfFunctions;
ppdwAddr = (PULONGLONG*)ImageRvaToVa((PIMAGE_NT_HEADERS)pNtHead, pDosHead, (DWORD)ppdwAddr, NULL);
ULONGLONG* ppdwOrdin = (ULONGLONG*)ImageRvaToVa((PIMAGE_NT_HEADERS)pNtHead, pDosHead, (DWORD)pExport->AddressOfNameOrdinals, NULL);

char* szFunction = (PSTR)ImageRvaToVa((PIMAGE_NT_HEADERS)pNtHead, pDosHead, (ULONG)*ppdwNames, NULL);
std::cout <<"特征值:" << pExport->Characteristics << std::endl;
std::cout<<"时间日期标志:" <<std::hex<< pExport->TimeDateStamp << std::endl;
//std::cout <<"大版本号"<< pExport->MajorVersion << std::endl;
//std::cout << "小版本号" << pExport->MinorVersion << std::endl;
std::cout << "版本号:" << std::hex<<pExport->MajorVersion <<"."<<std::hex<<pExport->MinorVersion << std::endl;
std::cout << "名称:"<<pExport->Name << std::endl;
std::cout << "基址:"<<pExport->Base << std::endl;
std::cout <<"函数个数:"<< pExport->NumberOfFunctions << std::endl;
std::cout << "名称个数:"<< pExport->NumberOfNames << std::endl;
std::cout << "函数地址:" << pExport->AddressOfFunctions << std::endl;
std::cout << "名称地址:" << pExport->AddressOfNames << std::endl;
std::cout << "名称序数地址:" << pExport->AddressOfNameOrdinals << std::endl;
for (DWORD i = 0; i < NumberOfNames; i++)
{
std::cout << "序号:" << i << std::endl;
std::cout <<"RVA"<< *ppdwAddr << std::endl;
std::cout <<"名称:"<< szFunction << std::endl;
std::cout <<"-----------------------------------"<< std::endl;
szFunction = szFunction + strlen(szFunction) + 1;
ppdwAddr++;
}
}

image-20200713220649317

获取重定位表

这个好像用处不大,到时候有时间写,没时间就算了

文章用到的源码

https://github.com/Ascotbe/virus/tree/master/PortableExecutableParser

参考文章

http://www.cppblog.com/API/archive/2011/03/09/141423.html
https://www.cnblogs.com/LyShark/p/11748296.html