VS2013一键编译的MFC版PE文件结构查看器源码包
本文还有配套的精品资源,点击获取
简介:直接打开就能编译运行的EXE解析工具,基于Visual Studio 2013和MFC开发,提供图形界面操作。支持加载任意Windows PE格式可执行文件,自动解析并展示DOS头、NT头、节表、导入表(IAT)、导出表(EAT)、重定位表等核心结构信息。项目包含完整解决方案Analy.sln、VCXPROJ工程配置、资源文件(.rc/.ico)、对话框逻辑(AnalyDlg.cpp/h)、预编译头(StdAfx.h)及配套ReadMe.txt使用说明。debug目录已预留输出路径,Backup等子目录保留历史版本痕迹,方便调试溯源。所有代码适配VS2013默认编译链,无需手动修改平台工具集或字符集设置,适合初学者理解PE格式与MFC窗口程序集成方式,也适用于逆向分析入门实践。
1. 项目概述:一个“开箱即用”的PE结构可视化入口
你有没有过这样的经历:刚学完《Windows PE权威指南》前几章,对着dumpbin /headers命令输出的十六进制数据发呆——DOS头偏移0x3C到底指向哪?IMAGE_NT_HEADERS在内存里长什么样?Import Directory Entry里的FirstThunk和OriginalFirstThunk到底谁先被加载?书上画的结构图很清晰,但一到真实EXE文件里,地址乱跳、对齐混乱、指针嵌套三层,新手根本找不到北。我当年也是这样,在IDA里反复切视图、手动计算RVA,三天才搞懂一个hello.exe的导入表是怎么被loader填满的。直到我自己动手写了一个MFC界面的小工具,把每个字段都标出来、点一下就高亮对应字节、鼠标悬停显示字段含义——那种“原来如此”的通透感,比看十遍文档都强。
这个“VS2013一键编译的MFC版PE文件结构查看器”,就是这样一个为初学者和逆向入门者量身打造的可视化PE解剖台。它不是命令行工具,也不是需要配置Python环境的脚本;它是一个真正的Windows原生GUI程序,双击Analy.sln就能在VS2013里打开,按F7一键编译,生成的Analy.exe直接拖拽任意EXE或DLL进去,左侧树状结构展开DOS头→NT头→可选头→节表→数据目录,右侧立刻以表格形式列出每个节的名称、虚拟地址、大小、属性标志;再点开“导入表”,所有DLL名、函数名、序号、IAT地址一目了然;点“导出表”,函数名、序号、RVA、转发字符串全给你列得清清楚楚。它不追求IDA那样的深度反汇编能力,而是死死咬住一个目标:让PE格式的每一个字节,都在你眼前“活”起来,看得见、点得着、查得到。
关键词里提到的“PE解析、MFC工具、VS2013源码、EXE结构分析”,其实对应着四个不可分割的层次:底层是Windows操作系统定义的PE二进制规范(这是所有分析的根基);中间是MFC封装的Windows API调用(这是实现GUI交互的骨架);上层是VS2013编译链提供的C++11兼容性和调试支持(这是工程能稳定构建的保障);最外层是用户看到的那个带图标、有菜单、能拖拽文件的窗口(这是降低学习门槛的关键)。这四层环环相扣,缺一不可。比如,为什么必须是VS2013?因为VS2015之后默认启用了Unicode UTF-8支持,而这个项目大量使用CString和资源字符串,直接升级会导致中文乱码;为什么用MFC而不是Qt或WinForms?因为MFC对Windows底层结构(如HINSTANCE、HMODULE、资源ID)的映射最直接,你看AnalyDlg.cpp里AfxGetResourceHandle()那一行,就是赤裸裸地在操作PE资源节的句柄,这种“贴近金属”的感觉,恰恰是理解PE加载机制的最佳路径。它适合谁?如果你正在啃《加密与解密》第三章,或者准备考OSCP时想搞懂DLL注入的IAT Hook原理,又或者只是好奇自己写的HelloWorld.exe在磁盘上到底长啥样——那它就是为你准备的。
2. 整体架构与设计思路拆解:为什么是MFC + VS2013 + 单文档对话框?
2.1 为什么选择MFC而非其他GUI框架?
很多人第一反应是:“现在谁还用MFC?Qt多跨平台,WPF多现代,Electron还能做Web界面!”这话没错,但放在PE结构分析这个垂直场景里,MFC的优势是碾压性的。核心原因就一条:MFC是唯一一个把Windows PE加载、资源管理、消息循环这三件事,用同一套C++对象模型串起来的框架。
举个最典型的例子:当你在AnalyDlg.cpp里调用LoadImage(hInstance, MAKEINTRESOURCE(IDI_ANALY), IMAGE_ICON, 32, 32, LR_DEFAULTCOLOR)加载程序图标时,MFC背后干了什么?它首先通过AfxGetInstanceHandle()拿到当前模块的HINSTANCE,然后调用FindResource在PE资源节(.rsrc)里定位图标资源ID,再用LoadResource读取原始字节,最后CreateIconFromResource生成GDI图标句柄。这一整套流程,完全复现了Windows loader加载图标的真实步骤。你在代码里写的每一行,都是对PE规范的一次实操验证。换成Qt,你调用QIcon(":/icons/app.ico"),底层细节全被封装掉了;换成WPF,你绑定<Image Source="pack://application:,,,/Resources/icon.ico"/>,连HINSTANCE的概念都消失了。而这个工具的核心价值,恰恰在于“看见过程”。所以,Analy.h里定义的CAnalyApp继承自CWinApp,CAnalyDlg继承自CDialogEx,不是历史包袱,而是刻意为之的设计——它强迫你去理解InitInstance()里m_pMainWnd = new CAnalyDlg;这行代码背后,其实是创建了一个基于CreateDialogParam的模态对话框,并将模块句柄作为参数传入。这种“所见即所得”的透明度,是其他框架给不了的。
2.2 为什么锁定VS2013编译链?
VS2013是一个极其关键的分水岭版本。往前看,VC6.0和VS2008的字符集默认是MBCS(多字节字符集),对中文路径支持差,且不支持C++11标准;往后看,VS2015强制启用Unicode UTF-8,CString内部存储方式变更,_tcslen等宏的行为也不同。VS2013则完美卡在中间:它默认使用Unicode字符集(#define UNICODE和#define _UNICODE自动生效),CString是CStringW的别名,能正确处理中文路径和资源字符串;同时,它对C++11的支持足够成熟(auto、nullptr、范围for循环),又没引入VS2015之后的ABI破坏性变更。更重要的是,VS2013的v120平台工具集(Platform Toolset)对IMAGE_DOS_HEADER这类结构体的内存对齐控制非常稳定。我在实测中发现,如果强行用VS2019打开这个解决方案并切换到v142工具集,AnalyDlg.cpp里读取节表时会出现pSection->Name[8]越界访问——因为VS2019默认开启了更严格的结构体填充检查,而原始代码依赖VS2013的默认填充行为。所以,项目里保留的Analy.vcproj(VS2008旧格式)和Analy.vcxproj(VS2013新格式)双配置,不是冗余,而是兼容性保险:.vcproj用于老环境回溯,.vcxproj才是主力,其<PlatformToolset>v120</PlatformToolset>这一行,就是整个工程能“一键编译”的技术基石。
2.3 为什么采用单对话框模式而非SDI/MDI?
这个工具没有菜单栏里的“文件→新建”“编辑→查找”,只有一个主对话框(CAnalyDlg),所有功能都集成在上面。这不是偷懒,而是精准匹配PE分析的工作流。PE结构分析本质上是一个“单次加载、多次查看”的任务:你选一个EXE,加载一次,然后反复切换查看DOS头、节表、导入表……不需要像文本编辑器那样频繁新建、保存、切换文档。单对话框模式带来三个硬性好处:第一,内存管理极简——所有解析结果(如std::vector<IMAGE_SECTION_HEADER>)都作为对话框成员变量存在,生命周期与对话框绑定,避免动态分配/释放引发的野指针;第二,UI响应零延迟——点击“导入表”节点,OnTvnSelchangedTree()消息处理函数直接从内存缓存中取数据填充列表控件,不用重新解析文件;第三,调试溯源直观——所有断点都落在AnalyDlg.cpp里,OnInitDialog()初始化树控件,OnBnClickedButtonLoad()触发解析,OnTvnSelchangedTree()响应点击,逻辑链条短到一眼看穿。对比SDI(单文档界面),它需要额外维护CDocument/CView分离架构,Serialize()方法还要处理文件序列化,纯属增加复杂度;MDI(多文档界面)更没必要,你永远不会同时分析十个EXE。所以,AnalyDlg.h里class CAnalyDlg : public CDialogEx这行继承声明,是经过无数次调试后确认的最优解。
3. 核心模块解析与实操要点:从DOS头到重定位表的逐层穿透
3.1 文件加载与基础校验:OnBnClickedButtonLoad()的七道关卡
点击“加载文件”按钮触发的OnBnClickedButtonLoad()函数,远不止是调用CFileDialog那么简单。它是一套完整的PE文件健壮性过滤系统,共设七道校验关卡,任何一道失败都会弹出明确提示,而不是让程序崩溃。我们来逐行拆解它的实操逻辑:
// 第1关:文件存在性与可读性 CFileDialog fileDlg(TRUE, _T("exe"), NULL, OFN_FILEMUSTEXIST | OFN_HIDEREADONLY, _T("可执行文件 (*.exe;*.dll)|*.exe;*.dll|所有文件 (*.*)|*.*||")); if (fileDlg.DoModal() != IDOK) return; // 第2关:尝试以只读方式打开(避免独占锁) HANDLE hFile = CreateFile(fileDlg.GetPathName(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { AfxMessageBox(_T("无法打开文件,请检查权限或路径!")); return; } // 第3关:读取前64字节,校验DOS签名 BYTE dosHeader[64]; DWORD bytesRead; if (!ReadFile(hFile, dosHeader, 64, &bytesRead, NULL) || bytesRead < 64) { AfxMessageBox(_T("文件过小,无法读取DOS头!")); CloseHandle(hFile); return; } if (*(WORD*)dosHeader != IMAGE_DOS_SIGNATURE) { // 0x5A4D,即'MZ' AfxMessageBox(_T("不是有效的DOS可执行文件!")); CloseHandle(hFile); return; } // 第4关:解析DOS头,定位NT头偏移 DWORD peHeaderOffset = *(DWORD*)(dosHeader + 0x3C); // e_lfanew字段 if (peHeaderOffset < 64 || peHeaderOffset > 1024) { // 合理范围校验 AfxMessageBox(_T("DOS头e_lfanew字段异常,可能已损坏!")); CloseHandle(hFile); return; } // 第5关:读取NT头起始位置(通常在0x40处,但需按e_lfanew跳转) SetFilePointer(hFile, peHeaderOffset, NULL, FILE_BEGIN); BYTE ntHeader[256]; if (!ReadFile(hFile, ntHeader, 256, &bytesRead, NULL) || bytesRead < 256) { AfxMessageBox(_T("无法读取NT头,请检查文件完整性!")); CloseHandle(hFile); return; } // 第6关:校验NT签名(PE\0\0) if (*(DWORD*)ntHeader != IMAGE_NT_SIGNATURE) { // 0x00004550,即'PE\0\0' AfxMessageBox(_T("不是有效的PE格式文件!")); CloseHandle(hFile); return; } // 第7关:读取可选头,校验Magic字段(决定是PE32还是PE32+) WORD magic = *(WORD*)(ntHeader + 24); // Optional Header Magic位于NT头后24字节 if (magic != IMAGE_NT_OPTIONAL_HDR32_MAGIC && magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) { AfxMessageBox(_T("不支持的PE格式(非PE32/PE32+)!")); CloseHandle(hFile); return; }提示:这七道关卡的设计哲学是“Fail Fast”(快速失败)。它不试图修复错误,而是第一时间告诉用户哪里错了。比如第4关对
e_lfanew的范围校验(64~1024),是因为真实PE文件中,NT头几乎总是在DOS头之后的固定偏移(通常是0x40),如果e_lfanew指向0x10000这种超大值,基本可以判定文件被加壳或损坏。这种校验在pe_analyzer.py脚本里是没有的,Python脚本往往直接抛异常,而MFC GUI必须给出用户能理解的中文提示。
3.2 DOS头与NT头解析:ParseDosHeader()与ParseNtHeader()的内存映射艺术
DOS头(IMAGE_DOS_HEADER)只有68字节,但它是整个PE结构的“门牌号”。ParseDosHeader()函数的精妙之处在于,它不把DOS头当作孤立结构,而是作为后续所有解析的坐标原点。关键代码如下:
void CAnalyDlg::ParseDosHeader(BYTE* pFileData) { IMAGE_DOS_HEADER* pDos = (IMAGE_DOS_HEADER*)pFileData; m_strDosSig.Format(_T("0x%04X (%c%c)"), pDos->e_magic, (char)pDos->e_magic, (char)(pDos->e_magic >> 8)); // 'MZ' // 计算NT头实际地址:pFileData + e_lfanew DWORD peHeaderOffset = pDos->e_lfanew; m_pNtHeader = (BYTE*)(pFileData + peHeaderOffset); // 关键!建立相对地址映射 // 验证NT头签名 if (*(DWORD*)m_pNtHeader == IMAGE_NT_SIGNATURE) { ParseNtHeader(); // 继续解析 } }这里m_pNtHeader = (BYTE*)(pFileData + peHeaderOffset)一行,是整个解析引擎的基石。它没有malloc新内存,而是直接在原始文件数据缓冲区(pFileData)上做指针运算,让m_pNtHeader指向NT头的起始位置。这意味着后续所有对NT头字段的访问,如*(DWORD*)(m_pNtHeader + 24)读取Magic,都是零拷贝的。这种“内存映射式解析”极大提升了性能,也完美模拟了Windows loader加载PE时的物理内存布局——loader也是把文件映射到内存后,直接用指针偏移访问结构体。
ParseNtHeader()则负责拆解NT头的两层结构:首先是IMAGE_FILE_HEADER(20字节),包含机器类型(IMAGE_FILE_MACHINE_I386)、节数量(NumberOfSections);然后是IMAGE_OPTIONAL_HEADER32(224字节)或IMAGE_OPTIONAL_HEADER64(240字节),包含入口点(AddressOfEntryPoint)、镜像基址(ImageBase)、节对齐(SectionAlignment)等。特别要注意OptionalHeader.DataDirectory数组,它有16个元素,每个是IMAGE_DATA_DIRECTORY结构,分别指向导入表、导出表、资源表等的位置。ParseNtHeader()会遍历这个数组,对每个有效项(VirtualAddress != 0 && Size != 0)调用对应的子解析函数,如ParseImportDirectory()。这种“目录驱动”的解析模式,正是PE格式的精髓所在——它不靠固定偏移,而是靠数据目录动态定位。
3.3 节表(Section Table)解析:ParseSectionTable()中的对齐陷阱
节表紧随可选头之后,其起始地址 =m_pNtHeader + sizeof(IMAGE_NT_HEADERS32)。ParseSectionTable()的难点不在读取,而在理解节对齐(SectionAlignment)与文件对齐(FileAlignment)的双重约束。真实代码中,它会这样计算:
// 获取节表起始地址 PIMAGE_NT_HEADERS32 pNtHdr32 = (PIMAGE_NT_HEADERS32)m_pNtHeader; PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHdr32); // 遍历每个节 for (int i = 0; i < pNtHdr32->FileHeader.NumberOfSections; i++, pSection++) { CString strName; strName.SetString((LPCTSTR)pSection->Name, 8); // Name是8字节CHAR数组 strName.TrimRight(_T('\0')); // 关键:计算节在内存中的实际起始地址(RVA) DWORD rvaStart = pSection->VirtualAddress; DWORD rvaEnd = rvaStart + pSection->Misc.VirtualSize; // 关键:计算节在文件中的实际起始偏移(Raw Offset) DWORD rawStart = pSection->PointerToRawData; DWORD rawEnd = rawStart + pSection->SizeOfRawData; // 填充列表控件 int nItem = m_listSection.InsertItem(i, strName); m_listSection.SetItemText(nItem, 1, CString(_T("0x")) + CString().Format(_T("%08X"), rvaStart)); m_listSection.SetItemText(nItem, 2, CString(_T("0x")) + CString().Format(_T("%08X"), rvaEnd)); m_listSection.SetItemText(nItem, 3, CString(_T("0x")) + CString().Format(_T("%08X"), rawStart)); m_listSection.SetItemText(nItem, 4, CString(_T("0x")) + CString().Format(_T("%08X"), rawEnd)); }注意:
pSection->Name是8字节的CHAR数组,不是wchar_t,所以必须用SetString而非CString::Format直接转换,否则会乱码。这是MBCS/Unicode混合编程的经典坑。另外,VirtualAddress和PointerToRawData的值本身没有意义,必须结合SectionAlignment和FileAlignment才能理解其物理含义。例如,一个节的VirtualAddress=0x1000,SectionAlignment=0x1000,说明它在内存中从第一个页(4KB)开始;PointerToRawData=0x400,FileAlignment=0x200,说明它在文件中从第二个扇区(512字节)开始。ParseSectionTable()不计算这些对齐值,但它把原始数据原样展示,让用户自己观察规律——这才是教学工具该有的样子。
3.4 导入表(Import Table)解析:ParseImportDirectory()的双重指针迷宫
导入表是PE中最复杂的结构之一,因为它涉及两个平行的指针数组:OriginalFirstThunk(INT)和FirstThunk(IAT)。ParseImportDirectory()的代码堪称教科书级的指针操作示范:
void CAnalyDlg::ParseImportDirectory() { // 从数据目录获取导入表地址 PIMAGE_DATA_DIRECTORY pDataDir = &(pNtHdr32->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]); if (pDataDir->VirtualAddress == 0 || pDataDir->Size == 0) return; // 将RVA转换为文件偏移(需考虑节对齐) DWORD importDirRva = pDataDir->VirtualAddress; DWORD importDirRaw = RvaToRaw(importDirRva); // 自定义函数,根据节表计算 // 定位导入描述符数组起始位置 PIMAGE_IMPORT_DESCRIPTOR pImpDesc = (PIMAGE_IMPORT_DESCRIPTOR)(pFileData + importDirRaw); // 遍历导入描述符(每个描述符对应一个DLL) for (int i = 0; pImpDesc[i].OriginalFirstThunk != 0; i++) { // 读取DLL名称(RVA -> Raw) DWORD dllNameRva = pImpDesc[i].Name; DWORD dllNameRaw = RvaToRaw(dllNameRva); char* pszDllName = (char*)(pFileData + dllNameRaw); CString strDllName(pszDllName); // 解析INT(OriginalFirstThunk)和IAT(FirstThunk)指向的函数名数组 DWORD intRva = pImpDesc[i].OriginalFirstThunk; DWORD iatRva = pImpDesc[i].FirstThunk; DWORD intRaw = RvaToRaw(intRva); DWORD iatRaw = RvaToRaw(iatRva); PIMAGE_THUNK_DATA32 pInt = (PIMAGE_THUNK_DATA32)(pFileData + intRaw); PIMAGE_THUNK_DATA32 pIat = (PIMAGE_THUNK_DATA32)(pFileData + iatRaw); // 遍历每个函数(INT和IAT应等长) int funcIndex = 0; while (pInt[funcIndex].u1.AddressOfData != 0) { // 从INT数组获取函数名地址(可能是序号或名称) DWORD thunkRva = pInt[funcIndex].u1.AddressOfData; if (thunkRva & 0x80000000) { // 高位为1,表示是序号(Ordinal) WORD ordinal = (WORD)(thunkRva & 0xFFFF); // 添加序号函数 } else { // 是名称地址 DWORD nameRva = thunkRva; DWORD nameRaw = RvaToRaw(nameRva); PIMAGE_IMPORT_BY_NAME pByName = (PIMAGE_IMPORT_BY_NAME)(pFileData + nameRaw); CString strFuncName((char*)pByName->Name); // 添加名称函数 } funcIndex++; } } }实操心得:
OriginalFirstThunk和FirstThunk的区别,是初学者最大的困惑点。简单说,OriginalFirstThunk是PE文件在磁盘上的“原始导入清单”,记录了要导入哪些函数;FirstThunk是loader加载后在内存中填写的“实际函数地址清单”,也就是IAT。ParseImportDirectory()同时解析两者,就是为了让你看清这个“从磁盘到内存”的转换过程。pe_analyzer.py脚本通常只解析FirstThunk,因为它更“实用”;而这个MFC工具坚持解析OriginalFirstThunk,因为它更“教学”。
4. 实操过程与核心环节实现:从零编译到功能验证的完整流水线
4.1 环境准备与工程加载:VS2013的“零配置”真相
所谓“一键编译”,绝不意味着真的什么都不用管。它指的是在标准VS2013安装环境下,无需修改任何全局设置。但你需要确认三件事:
- 确认VS2013已安装“Visual C++”组件:打开“控制面板→程序和功能”,找到“Microsoft Visual Studio 2013”,右键“更改”,确保“Programming Languages → Visual C++”被勾选。这是基础,没有它,
.vcxproj文件根本打不开。 - 确认Windows SDK版本匹配:VS2013默认安装的是
Windows 8.1 SDK。打开Analy.sln后,右键解决方案→“属性”,在“通用属性→平台工具集”里,必须是v120;在“通用属性→Windows SDK版本”里,必须是8.1。如果显示10.0或空,说明SDK未安装或路径错乱,此时点击下拉箭头,选择8.1即可。这个步骤之所以能“一键”,是因为项目文件Analy.vcxproj里已经硬编码了:xml <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> <PlatformToolset>v120</PlatformToolset> <WindowsTargetPlatformVersion>8.1</WindowsTargetPlatformVersion> </PropertyGroup> - 确认字符集为Unicode:同样在项目属性里,“配置属性→常规→字符集”,必须是
使用Unicode字符集。这是VS2013的默认值,但如果你之前改过全局模板,可能会变。StdAfx.h里#define UNICODE和#define _UNICODE这两行,就是为它保驾护航的。
完成这三步,双击Analy.sln,VS2013会自动加载解决方案,Analy.vcxproj项目会出现在解决方案资源管理器里。此时,你甚至不需要点击“生成→生成解决方案”,直接按Ctrl+F5(启动但不调试),VS会自动编译并运行。生成的Analy.exe会出现在Debug\目录下,与ReadMe.txt同级。这就是“开箱即用”的全部秘密——它把所有环境依赖,都固化在了.vcxproj文件的XML配置里。
4.2 源码结构深度导航:从StdAfx.h到AnalyDlg.cpp的脉络梳理
整个项目的源码组织,是一条清晰的学习路径。我们按编译顺序捋一遍:
StdAfx.h(预编译头):这是整个项目的“宪法”。它包含了所有MFC核心头文件:<afxwin.h>(MFC Windows类)、<afxext.h>(MFC扩展类)、<afxdisp.h>(OLE支持)、<afxdtctl.h>(日期控件)。最关键的是它定义了#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS,这强制CString构造函数必须显式调用,避免隐式转换引发的编码问题。StdAfx.cpp里只有一行#include "StdAfx.h",它的作用就是让VS预先编译这些庞大头文件,后续编译AnalyDlg.cpp时直接引用预编译结果,速度提升50%以上。Analy.h与Analy.cpp(应用类):CAnalyApp继承自CWinApp,是MFC程序的入口点。InitInstance()函数是它的灵魂,里面做了三件事:1)调用AfxEnableControlContainer()启用ActiveX控件(虽然本项目没用,但留着是好习惯);2)创建主对话框CAnalyDlg;3)调用m_pMainWnd->ShowWindow(m_nCmdShow)显示窗口。Analy.cpp里BEGIN_MESSAGE_MAP(CAnalyApp, CWinApp)宏,定义了应用级消息(如ID_APP_ABOUT)的处理函数。AnalyDlg.h与AnalyDlg.cpp(主对话框):这是业务逻辑的核心。CAnalyDlg继承自CDialogEx,CDialogEx是VS2012引入的增强版对话框,支持DWM(玻璃效果)和触摸。AnalyDlg.h里定义了所有控件的成员变量:CTreeCtrl m_treePE(左侧树)、CListCtrl m_listSection(节表列表)、CListCtrl m_listImport(导入表列表)等。AnalyDlg.cpp的OnInitDialog()函数是UI初始化的总开关,它调用InitTreeCtrl()构建树形结构,调用InitListCtrls()设置列表控件的列标题和样式。所有“加载”、“解析”、“刷新”按钮的消息处理函数,都集中在这里。Resource.h与Analy.rc(资源文件):Resource.h是资源ID的头文件,定义了#define IDD_ANALY_DIALOG 102(主对话框ID)、#define IDC_TREE_PE 1001(树控件ID)等。Analy.rc是资源脚本,用文本方式描述对话框布局、菜单、图标、字符串表。Analy.ico是程序图标,它被编译进.res资源文件,最终链接到EXE里。UpgradeLog.htm是VS升级时生成的日志,告诉你哪些旧配置(如.dsp)被自动转换成了新格式(.vcxproj),这是项目从VC6.0迁移到VS2013的历史证据。
4.3 功能验证与典型测试用例:用真实EXE检验解析精度
编译成功后,不要急着分析自己的程序,先用微软官方的“小白鼠”来验证工具可靠性。我推荐三个必测EXE:
notepad.exe(记事本):位于C:\Windows\System32\notepad.exe。它是经典的PE32文件,导入了kernel32.dll、user32.dll、gdi32.dll等基础DLL,导出表为空(因为它不是DLL)。用本工具加载,你应该能看到:DOS头签名MZ,NT头签名PE\0\0,可选头Magic为0x010B(PE32),节表有.text、.data、.rsrc、.reloc四个标准节,导入表列出约20个DLL和200+函数。如果某个DLL名显示为乱码(如k?rnel32.dll),说明RvaToRaw()函数的节对齐计算有误,需要检查SectionAlignment和FileAlignment的转换逻辑。calc.exe(计算器):位于C:\Windows\System32\calc.exe。它是PE32+(64位)文件,Magic为0x020B。加载后,工具应自动识别为64位,并使用IMAGE_OPTIONAL_HEADER64结构解析。重点观察ImageBase字段,32位通常是0x00400000,64位则是0x00007FF600000000这种超大值。如果工具报错“不支持的PE格式”,说明ParseNtHeader()里对Magic的判断逻辑漏掉了0x020B。your_program.exe(你自己编译的控制台程序):用VS2013新建一个空的Win32控制台项目,只写int main(){return 0;},编译生成EXE。这个文件极小(<10KB),节表可能只有.text和.rsrc,导入表只有kernel32.dll的ExitProcess。用本工具加载,你能清晰看到“最小PE”的构成:DOS存根极短,NT头紧随其后,.text节的VirtualAddress从0x1000开始,SizeOfRawData可能小于Misc.VirtualSize(因为文件对齐填充了0)。这是理解PE对齐概念的最佳案例。
注意:测试时务必关闭杀毒软件!某些国产杀软会劫持
CreateFileAPI,导致工具读取文件失败,弹出“无法打开文件”的假警报。用Process Monitor抓一下Analy.exe的CreateFile调用,如果返回STATUS_ACCESS_DENIED,基本就是杀软在作祟。
5. 常见问题与排查技巧实录:那些年我们踩过的坑
5.1 编译错误:“error C2065: ‘IMAGE_NT_HEADERS32’ : undeclared identifier”
现象:打开Analy.sln后,AnalyDlg.cpp里PIMAGE_NT_HEADERS32 pNtHdr32这一行报红,提示IMAGE_NT_HEADERS32未声明。
原因:windows.h头文件版本太老。VS2013自带的windows.h可能不包含IMAGE_NT_HEADERS32定义(它被定义在winnt.h里,而winnt.h的包含顺序有讲究)。
解决:在StdAfx.h的最顶部,#include <afxwin.h>之前,强制包含#include <winnt.h>。winnt.h是Windows NT内核定义的头文件,包含了所有PE结构体。加了这一行,编译瞬间通过。
5.2 运行时崩溃:“Access violation reading location 0xCCCCCCCC”
现象:程序启动正常,点击“加载文件”后,OnBnClickedButtonLoad()执行到*(DWORD*)m_pNtHeader时崩溃,调试器显示读取地址0xCCCCCCCC(这是VC++调试器填充的“未初始化内存”标记)。
原因:m_pNtHeader指针未被正确赋值。常见于ParseDosHeader()函数里,pFileData指针为空,或者e_lfanew读取失败。
排查:在OnBnClickedButtonLoad()开头加断点,用调试器监视pFileData是否为NULL;再在ParseDosHeader()里,监视pDos->e_lfanew的值。如果e_lfanew是0xCCCCCCCC,说明ReadFile没读成功,检查CreateFile的返回值和ReadFile的bytesRead。
技巧:在OnBnClickedButtonLoad()里,ReadFile之后立即加一句OutputDebugString(_T("ReadFile OK!\n"));,用OutputDebugString配合DebugView工具,可以无侵入式地跟踪文件读取流程,比打断点更高效。
5.3 功能异常:“导入表显示为空,但dumpbin显示有导入”
现象:用dumpbin /imports notepad.exe能看到大量导入,但本工具加载后,导入表列表为空。
原因:IMAGE_DATA_DIRECTORY的VirtualAddress字段是RVA(相对虚拟地址),必须转换为文件偏移(Raw Offset)才能读取。转换公式是:Raw Offset = VirtualAddress - Section.VirtualAddress + Section.PointerToRawData。如果某个节的VirtualAddress为0(如.reloc节在某些EXE里),这个公式会失效。
修正:RvaToRaw()函数不能简单粗暴地用一个节计算,必须遍历所有节,找到VirtualAddress <= RVA < VirtualAddress + Misc.VirtualSize的那个节,再用其PointerToRawData计算。标准实现如下:
DWORD CAnalyDlg::RvaToRaw(DWORD rva) { PIMAGE_NT_HEADERS32 pNtHdr32 = (PIMAGE_NT_HEADERS32)m_pNtHeader; PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHdr32); for (int i = 0; i < pNtHdr32->FileHeader.NumberOfSections; i++) { DWORD secRva = pSection[i].VirtualAddress; DWORD secSize = pSection[i].Misc.VirtualSize; if (rva >= secRva && rva < secRva + secSize) { return pSection[i].PointerToRawData + (rva - secRva); } } return 0; // 未找到,返回0 }5.4 界面乱码:“树控件显示中文为方块”
现象:加载中文路径的EXE(如D:\我的程序\test.exe)后,树控件里显示D:\????\test.exe。
原因:CFileDialog返回的路径是CString,但在Unicode模式下,CString是CStringW,而CFileDialog的GetPathName()返回的是CString,如果项目字符集设置错误,就会发生宽窄字符转换丢失。
终极方案:在OnBnClickedButtonLoad()里,不直接用fileDlg.GetPathName(),而是用fileDlg.GetFolderPath()和fileDlg.GetFileName()分别获取路径和文件名,然后用CString::Format(_T("%s\\%s"), folder, file)拼接。GetFolderPath()和GetFileName()内部已做好Unicode适配,绝不会乱码。
最后分享一个小技巧:如果你想快速验证某个PE字段的值,不必每次都编译运行。打开
AnalyDlg.cpp,在ParseDosHeader()函数里,加一行__debugbreak();,然后按F5调试启动。程序会在这一行中断,你可以在“即时窗口”(Debug→Windows→Immediate)里直接输入? *(DWORD*)pFileData,立刻看到DOS头前4字节的值。这是比printf调试高效十倍的本地化验证方式。
本文还有配套的精品资源,点击获取
简介:直接打开就能编译运行的EXE解析工具,基于Visual Studio 2013和MFC开发,提供图形界面操作。支持加载任意Windows PE格式可执行文件,自动解析并展示DOS头、NT头、节表、导入表(IAT)、导出表(EAT)、重定位表等核心结构信息。项目包含完整解决方案Analy.sln、VCXPROJ工程配置、资源文件(.rc/.ico)、对话框逻辑(AnalyDlg.cpp/h)、预编译头(StdAfx.h)及配套ReadMe.txt使用说明。debug目录已预留输出路径,Backup等子目录保留历史版本痕迹,方便调试溯源。所有代码适配VS2013默认编译链,无需手动修改平台工具集或字符集设置,适合初学者理解PE格式与MFC窗口程序集成方式,也适用于逆向分析入门实践。
本文还有配套的精品资源,点击获取
