PE文件总的可以理解为后缀为EXE的可以执行的文件。
但实际还有很多其他类型文件,如ROM文件,dll文件等等。
个人正在熟悉PE文件的结构与编写代码读取与修改。
汇编与PE文件与逆向绕不过去的坎,所以决定在这里记录学习过程的心得,与代码编写的源代码。
我们知道文件在磁盘中是以二进制形式保存,包括可执行程序。
这些数据以指定的规则保存与加载,学习熟悉这些规则才能够很好的修改这些文件。
很基础的电脑常识,
二进制数8个组成一个字节BYTE,
二进制数16个组成一个字WORD,
二进制数32个组成一个双字DWORD,
为了更加直观方便的操作二进制数,可以以十六进制数来表示二进制数。
4个二进制数可以用一个16进制数表示。
这样一个可执行文件通过ultraedit程序打开,就可以以十六 进制形式来显示文件数据。
(ultraedit网络都可以搜索下载)
如下图:
PE文件结构之DOS头组成及编程读取
在界面上可以看到文件以十六进制的形式,从左到右,从上到下,
以字节为单位依次展开。
第0个字节地址为00000000H,从左到右共16个字节,从上到下以16个字节递增。
这些数据以一定的格式存储,按指定的规则被系统读取使用。
格式如何?
好比一个人的组成,由头与身躯两大部分组成,但头与身躯又有细分。
头有眼睛,嘴巴等,身躯有手脚,头控制身体。
或许通过书来打比喻更形象。
打开一本书,我们关心的内容主要由头部与内容两大部分组成。
头部又会有封面,前言,简介,目录等等,从这些部分我们可以大概了解书的相关介绍,
作者是谁,书适合什么人,内容有什么,然后是目录,通过目录可以检索书的具体内容。
另一部分是书的内容,内容具体细分章节,小节,可由目录索引。
PE文件也可总分为两部分:
PE文件头+节区。
PE文件头记录此PE文件相关的信息与实现访问节区类似目录的功能。
节区类似于书的内容,保存有代码,资料,数据等文件具体内容。
PE文件头与节区具体都是可以在细分的。
如下:
PE文件结构之DOS头组成及编程读取
总的来说,PE文件头由DOS头,PE头,节表三部分组成。
DOS头又细分为DOS_MZ头(对应结构体IMAGE_DOS_HEADER), 与DOS_Stub.
PE头实际为一个结构体IAMGE_NT_HEADER,
又细分为Signature(固定为"PE00"),
标准PE头(对应结构体IMAGE_FILE_HEADER),
可选PE头(对应结构体IMAGE_OPTIONAL_HEADER).
节表由众多节表项组成,类似目录,记录PE文件节区对应节相关信息。
一个PE文件大概由上面部分组成,每个部分在文件中前后按顺序存储。
如何编写代码来访问每个部分呢?
这里编写代码来实现打开PE文件,读取与显示DOS部分数据。
PE头部分的读取放在下一帖子来实现。
理论配合实践才能更好理解PE结构。
例程界面如下:
PE文件结构之DOS头组成及编程读取
首先点击界面OPEN打开一个PE文件。
然后就可以点击DOS头,DOS Stub两按钮来显示读取的DOS头信息与DOS stub信息。
例程集成一个类CPE来处理PE文件相关功能。
打开PE文件函数为
- bool CPE::Open(CString sFullFileName)
- {
- PIMAGE_DOS_HEADER pDosHead=NULL;
- sFullFileName.TrimRight();
- CFile f;
- if(!f.Open(sFullFileName,CFile::modeRead|CFile::shareDenyNone|CFile::typeBinary))
- {
- Print(_T("PE文件打开失败。"));
- return false;
- }
- UINT lFileLen = f.GetLength();
- if(lFileLen<=0)
- {
- Print(_T("PE文件大小异常。"));
- return false;
- }
- Close();//释放事先分配的空间;
- pDosHead = (PIMAGE_DOS_HEADER)new char[lFileLen + 10];
- if(pDosHead == NULL)
- {
- Print(_T("PE文件分配空间失败。"));
- return false;
- }
- if(lFileLen != f.Read(pDosHead,lFileLen))
- {
- delete pDosHead;
- Print(_T("PE文件读取失败。"));
- return false;
- }
- if(!IsPEValid(pDosHead))
- {
- delete pDosHead;
- Print(_T("PE文件无效。"));
- return false;
- }
- //
- m_pDosHead = pDosHead;
- m_PEFileFullName = sFullFileName;
- m_lPEFileLen = lFileLen;
- //
- f.Close();
- return true;
- }
复制代码 PE文件成功打开后,头64个字节就为DOS头数据,对应结构体为IMAGE_DOS_HEADER。
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
对应下图第一行第0个字节到第4行最后一字节。
此结构体在例程中,鼠标选中,按F12就可跳转到定义位置。
如上边打开PE文件的代码,打开时就对全局变量PIMAGE_DOS_HEADER m_pDosHead进行了赋值。
这样的话,想要显示结构体数据,只要简单在界面上通过控件显示就好。
- void CDosHeadDlg::UpdateData(PIMAGE_DOS_HEADER pDosHeader,bool bSetGet)
- {
- if(pDosHeader == NULL)
- return;
- CString sText,sTemp;
- int i=0;
- if(bSetGet)//设置控件数据;
- {
- sText.Format("0x%04X",pDosHeader->e_magic);
- SetDlgItemText(IDC_EDIT1,sText);
- sText.Format("0x%04X",pDosHeader->e_cblp);
- SetDlgItemText(IDC_EDIT2,sText);
- sText.Format("0x%04X",pDosHeader->e_cp);
- SetDlgItemText(IDC_EDIT3,sText);
- sText.Format("0x%04X",pDosHeader->e_crlc);
- SetDlgItemText(IDC_EDIT4,sText);
- sText.Format("0x%04X",pDosHeader->e_cparhdr);
- SetDlgItemText(IDC_EDIT5,sText);
- sText.Format("0x%04X",pDosHeader->e_minalloc);
- SetDlgItemText(IDC_EDIT6,sText);
- sText.Format("0x%04X",pDosHeader->e_maxalloc);
- SetDlgItemText(IDC_EDIT7,sText);
- sText.Format("0x%04X",pDosHeader->e_ss);
- SetDlgItemText(IDC_EDIT8,sText);
- sText.Format("0x%04X",pDosHeader->e_sp);
- SetDlgItemText(IDC_EDIT9,sText);
- sText.Format("0x%04X",pDosHeader->e_csum);
- SetDlgItemText(IDC_EDIT10,sText);
- sText.Format("0x%04X",pDosHeader->e_ip);
- SetDlgItemText(IDC_EDIT11,sText);
- sText.Format("0x%04X",pDosHeader->e_cs);
- SetDlgItemText(IDC_EDIT12,sText);
- sText.Format("0x%04X",pDosHeader->e_lfarlc);
- SetDlgItemText(IDC_EDIT13,sText);
- sText.Format("0x%04X",pDosHeader->e_ovno);
- SetDlgItemText(IDC_EDIT14,sText);
- sText.Empty();
- for(i=0;i<4;i++)
- {
- sTemp.Format("%04X",pDosHeader->e_res[i]);
- sText+=sTemp;
- }
- SetDlgItemText(IDC_EDIT15,sText);
- sText.Format("0x%04X",pDosHeader->e_oemid);
- SetDlgItemText(IDC_EDIT16,sText);
- sText.Format("0x%04X",pDosHeader->e_oeminfo);
- SetDlgItemText(IDC_EDIT17,sText);
- sText.Empty();
- for(i=0;i<10;i++)
- {
- sTemp.Format("%04X",pDosHeader->e_res2[i]);
- sText+=sTemp;
- }
- SetDlgItemText(IDC_EDIT18,sText);
- sText.Format("0x%08X",pDosHeader->e_lfanew);
- SetDlgItemText(IDC_EDIT19,sText);
- }
- else//从控件获取数据;
- {
- }
- }
复制代码 代码都是例程的内容,可以在后边下载代码来参考使用。
我们知道不PE文件的每个部分都是连续存储的,我们知道了IMAGE_DOS_HEADER地址,
自然就可以定位到紧跟其后的DOS_Stub数据。
DOS_Stub部分是一连串长度不固定的字节块,没有对应的结构体。
由链接器任意填充,我们定位到它的地址,如何丰富它的长度?
这里我们就要借助IMAGE_DOS_HEADER结构体的成员变量e_lfanew。
我们知道IMAGE_DOS_HEADER跟着DOS_Stub,再跟着PE头,
e_lfanew表示PE头地址,这样通过已选条件就知道DOS_Stub长度。
具体参考例程代码:
- void CDosStubDlg::UpdateData(PIMAGE_DOS_HEADER pDosHeader,bool bSetGet)
- {
- if(pDosHeader == NULL)
- return;
- int nOffset=sizeof(IMAGE_DOS_HEADER);
- int nEnd = pDosHeader->e_lfanew-nOffset,i=0;
- byte* p = (byte*)pDosHeader+nOffset;
- CString sText,sTemp;
- if(bSetGet)
- {
-
- for(i=0;i<nEnd;i++)
- {
- if(i%16 == 0 && i>0)
- sText += "\r\n";
- sTemp.Format("%02X ",*p);
- sText+=sTemp;
- p++;
- }
- SetDlgItemText(IDC_EDIT1,sText);
- }
- else
- {
- }
- }
复制代码 这样,例程就实现了打开PE文件,显示DOS_MZ,与DOS_Stub数据信息了。
DOS_MZ紧跟着DOS_Stub,再跟着PE头,PE头我们放在下一帖子实现读取。
DOS_MZ加DOS_Stub组成DOS头,在DOS系统会有实际意义,剩余无意义。
但在Windows NT系统却相反,DOS头基本没作用。
但全用到DOS_MZ的两个成员:
e_lfanew:表示PE头开始地址。
e_magic:可执行文件标志,固定为"MZ",据说是一开发人员名字缩写。
虽说DOS头没多大作用,IMAGE_DOS_HEADER结构体也只用到两个成员。
但了解一下每个成员含义也无妨。
IMAGE_DOS_HEADER STRUCT {
+0h WORD e_magic // EXE标志,固定 MZ(4Dh 5Ah)
+2h WORD e_cblp // 最后(部分)百中的字节数
+4h WORD e_cp // 文件中的全部和部分页数
+6h WORD e_crlc // 重定位表中的指针数
+8h WORD e_cparhdr // 头部尺寸,以段落为单位
+0ah WORD e_minalloc // 所需最小附加段
+0ch WORD e_maxalloc // 所需最大附加段
+0eh WORD e_ss // DOS代码的初始化堆栈SS
+10h WORD e_sp // DOS代码的初始化堆栈指针SP
+12h WORD e_csum // 补码校验值
+14h WORD e_ip // DOS代码的初始化指令入口[指针IP]
+16h WORD e_cs // DOS代码的初始堆栈入口
+18h WORD e_lfarlc // 重定位表字节偏移量
+1ah WORD e_ovno // 覆盖号
+1ch WORD e_res[4] // 系统保留字
+24h WORD e_oemid // OEM标识符(相对e_oeminfo)
+26h WORD e_oeminfo // OEM 信息
+29h WORD e_res2[10] // 系统保留字
+3ch DWORD e_lfanew // PE头相对文件的偏移地址。
} IMAGE_DOS_HEADER ENDS
PE文件中DOS头还是比较简单的,只要关心两个变量。
DOS头紧跟着的是PE头,PE头又由三部分组成,这些部分又如何读取与显示呢?
我们会在下一帖子,介绍其中标准PE头的读取与显示。
例程下载地址:
|