:eek:本软件只是对LordPE进行简单的仿写,功能并不齐全,只是通过本软件来加深对PE文件格式的理解,对PE文件进行分析能更好的了解PE的结构,了解了PE才能对安全方向的一些知识有更好的理解。所以PE文件的解析对新人是很重要的知识,PE文件网上有大量的资料供新人们学习。
附件 84859
1.需求分析:
此软件功能需要,对PE文件进行分析,将PE文件的一些基本信息解析出来,文件的PE头基本信息,文件的区段个数和信息。PE文件的目录信息,期中重要的信息,文件导出表,导入表,资源。
对文件的导出表进行解析,获取到该文件导出表的起始地址,导出函数的数量函数名称数量,导出函数名称、序号、RVA、偏移等信息。对文件的导入表进行解析,获取到导入表中导入的DLL和DLL中相关的函数信息。资源,解析出位图,图标,图标ID等文件中用到的资源。可以对这些信息进行修改,并且保存文件。
编写PE解析软件需要对PE文件有一定了解,知道PE结构,可以将PE文件中的数据很好的提取并解析出来。
2.开发过程以及函数:
软件功能可以像lordpe可以对PE文件进行分析,并且有修改保存PE文件功能。写这个软件目前实现功能,将解析文件的功能写了一个简单的类PeHear,其中实现了PE文件解析,解析文件头的基本信息函数,解析PE区段信息函数,解析PE导入表信息函数,解析PE文件导出表信息函数。资源解析函数未完成。
该软件开发使用了类的继承,PeHear类是父类,其他函数通过继承PeHear读取文件中的信息,调用类中的函数将其中信息存到PeHear类中变量和自定义结构体中,创建出相应窗口将PE信息显示出来。
函数信息:
资源函数未完成
void cller();此函数为外界接口,调用此函数,会将文件加载分析,保存到相关变量和自定义结构体中。
首先,编写一个解析PE的类,这个类为基类,其他界面上的功能都继承于此类中。
解析PE文件信息,我们需要将PE文件加载到一段虚拟内存中,当磁盘文件一旦被加载到内存中,磁盘上的数据结构布局和内存中的数据结构布局是一致的。只是数据之间的相对位置可能会有些变化,有些偏移地址可能区别与原始的偏移位置,但是文件在磁盘和在内存中的偏移都是可以计算转换的。
附件 84862
使用CreateFile() 打开所要解析的PE文件,GetFileSize() 获取到PE文件的大小,通过大小
使用VirtualAlloc()开辟一块虚拟内存,使用ReadFile() 将PE文件读取到这块内存中。
加载到内存中的首地址就是PE的DOS头的位置,通过DOS头的结构体找到PE所在的位置。到此我们的准备工作已经做好了,后面就要通过windows自带的结构体对PE文件进行解析了。
DOS头指向PE文件。PE文件包含PE映像文件头和PE映像的扩展头。
将PE加载完成之后,对其加载首地址强制类型转换为PIMAGE_DOS_HEADER 类型的指针,获取到DOS头的结构信息。我们需要注意两个字段,e_mageic DOS可执行文件的表示为,表示这是一个MS-DOS下的可执行文件,e_lfanew新的EXE文件头偏移地址。
PE文件头
PE文件头的结构体类型为IMAGE_NT_HEADERS ,通过DOS头的内存中加载位置与DOS头中的e_lfanew字段相加计算出PE文件头的地址指针。
附件 84860
使用GetFileHeader()获取PE映像文件头
附件 84861
我们需要的字段为
NUmberOfSections 区块的数据量,用来获取到程序区段的数量。
SizeOfOptionalHeader 扩展头大小,在IMAGE_FILE_HEADER结构后面的扩展头大小。一般情况下,这个结构的大小在32为的系统中是0x00E0,而在64位系统中是0x00F0。
使用GetOptionalHeader()获取PE映像的扩展头
IMAGE_OPTIONAL_HEADER32
此结构比较大,就不粘此结构体了,只列出需要用到的字段。
在获取区段或者其他的一些PE信息时,需要将RVA地址转换为Foa,自定义函数DWORD RVA2Offset(DWORD dwRVA); dwRVA参数是要转换的RVA的值。
实现函数如下:
区段信息:
用来保存PE区段信息的结构体:
附件 84863
通过自定义GetSectionHeader();函数获取到PE区段信息,使用IMAGE_FIRST_SECTION 获取到区段表中第一个区段,通过区段个数遍历循环获取出区段表中的信息。将区段名称,实际呗使用的区段大小,此区段载入内存后的RVA,和此区段在文件中大小等信息。保存到结构体类型的向量中。
输出表信息:
保存输出表信息自定义结构体:
附件 84864
GetExportDirectory() 获取输出表信息,一般只是DLL文件中有输出表信息,所以需要判断该程序是否有输出表,如果没有则将设置弹出框标志位,弹出提示没有导出表结构,返回。
如果有输出表,则获取到PE输出表的信息输出表偏移函数数量,函数名数量、函数地址等信息,基址,名称,函数名称地址,名称字符串,函数名序号地址。
然后使用EXPORTA 类型的vector来保存导出函数序号,导出函数的RVA,偏移地址和函数名称。
通过以下函数和上面的结构体获取到导出表相应的信息。
输入表信息:
输入目录是一个多IMAGE_IMPORT_DESCRIPTOR(输入描述结构)的数组,每个被使用的DLL文件都有一个。(它们的)列表由一个全部用0填充的IMAGE_IMPORT_DESCRIPTOR(输入地址表目录项)结构作为结束。
保存输入表信息结构体:
附件 84865
相应的导入表结构体:
获取导入表信息的函数:
程序中使用到的变量:
由于本程序是通过继承实现的,要使不同的模块访问同一个基类的时候,基类中保存的信息不会改变,所以要使用静态变量来保存PE结构中的需要用到的信息。
变量信息:
总结:
此类的编写实现了PE文件的解析,可以将PE文件中内容显示到界面,但是对于修改文件的PE信息,没有用指针容易实现,没有用到指针不会出现内存泄露等指针问题,但是没有使用指针的节省空间和指针的操作速度,指针对文件修改的方便。有些函数完成的不是很好,需要进一步改善,通过这个项目更好的了解PE结构,对PE的分析有了更好的理解。但是目前软件尚未完成,通过以后的时间慢慢完善。:eek:
附件 84859
1.需求分析:
此软件功能需要,对PE文件进行分析,将PE文件的一些基本信息解析出来,文件的PE头基本信息,文件的区段个数和信息。PE文件的目录信息,期中重要的信息,文件导出表,导入表,资源。
对文件的导出表进行解析,获取到该文件导出表的起始地址,导出函数的数量函数名称数量,导出函数名称、序号、RVA、偏移等信息。对文件的导入表进行解析,获取到导入表中导入的DLL和DLL中相关的函数信息。资源,解析出位图,图标,图标ID等文件中用到的资源。可以对这些信息进行修改,并且保存文件。
编写PE解析软件需要对PE文件有一定了解,知道PE结构,可以将PE文件中的数据很好的提取并解析出来。
2.开发过程以及函数:
软件功能可以像lordpe可以对PE文件进行分析,并且有修改保存PE文件功能。写这个软件目前实现功能,将解析文件的功能写了一个简单的类PeHear,其中实现了PE文件解析,解析文件头的基本信息函数,解析PE区段信息函数,解析PE导入表信息函数,解析PE文件导出表信息函数。资源解析函数未完成。
该软件开发使用了类的继承,PeHear类是父类,其他函数通过继承PeHear读取文件中的信息,调用类中的函数将其中信息存到PeHear类中变量和自定义结构体中,创建出相应窗口将PE信息显示出来。
函数信息:
代码:
//计算值将Rva转换成Foa
DWORD RVA2Offset(DWORD dwRVA);
//加载PE
BOOL LoadPe();
//判断是否为PE文件
BOOL IsPE_File();
//获取PE映像文件头
void GetFileHeader();
//获取PE映像扩展头
void GetOptionalHeader();
//获取PE区段信息
void GetSectionHeader();
//导出表
void GetExportDirectory();
//导入表
void GetImportDirectory();
void cller();此函数为外界接口,调用此函数,会将文件加载分析,保存到相关变量和自定义结构体中。
首先,编写一个解析PE的类,这个类为基类,其他界面上的功能都继承于此类中。
解析PE文件信息,我们需要将PE文件加载到一段虚拟内存中,当磁盘文件一旦被加载到内存中,磁盘上的数据结构布局和内存中的数据结构布局是一致的。只是数据之间的相对位置可能会有些变化,有些偏移地址可能区别与原始的偏移位置,但是文件在磁盘和在内存中的偏移都是可以计算转换的。
附件 84862
使用CreateFile() 打开所要解析的PE文件,GetFileSize() 获取到PE文件的大小,通过大小
使用VirtualAlloc()开辟一块虚拟内存,使用ReadFile() 将PE文件读取到这块内存中。
加载到内存中的首地址就是PE的DOS头的位置,通过DOS头的结构体找到PE所在的位置。到此我们的准备工作已经做好了,后面就要通过windows自带的结构体对PE文件进行解析了。
DOS头指向PE文件。PE文件包含PE映像文件头和PE映像的扩展头。
将PE加载完成之后,对其加载首地址强制类型转换为PIMAGE_DOS_HEADER 类型的指针,获取到DOS头的结构信息。我们需要注意两个字段,e_mageic DOS可执行文件的表示为,表示这是一个MS-DOS下的可执行文件,e_lfanew新的EXE文件头偏移地址。
PE文件头
PE文件头的结构体类型为IMAGE_NT_HEADERS ,通过DOS头的内存中加载位置与DOS头中的e_lfanew字段相加计算出PE文件头的地址指针。
附件 84860
- Signature是PE文件头的标识,其值始终为0x00004550,ASCII码为“PE/0/0”,win32SDK使用#define IMAGE_NT_SIGNATURE定义了这个值,我们可以通过DOS和PE这两个标识来判断该文件是否为PE文件。
- FileHeader是标准PE头
- OptionalHeader是扩展PE头,重要的信息都存放在IMAGE_OPTIONAL_HEADER这个结构体中。
使用GetFileHeader()获取PE映像文件头
代码:
//解析PE映像文件头
void GetFileHeader()
{
m_PeHead = m_pNT->FileHeader;
m_pImageHeaderNum = m_PeHead.NumberOfSections;
}
我们需要的字段为
NUmberOfSections 区块的数据量,用来获取到程序区段的数量。
SizeOfOptionalHeader 扩展头大小,在IMAGE_FILE_HEADER结构后面的扩展头大小。一般情况下,这个结构的大小在32为的系统中是0x00E0,而在64位系统中是0x00F0。
使用GetOptionalHeader()获取PE映像的扩展头
IMAGE_OPTIONAL_HEADER32
此结构比较大,就不粘此结构体了,只列出需要用到的字段。
- SizeOfCode 所有代码区段的总大小。
- AddressOfEntryPoint 程序执行入口RVA。也就是我们所说的OEP。
- ImageBase 文件在内存中的首选装入地址,默认加载基址,为PE文件的优先装载地址。比如,如果该值是400000h,PE装载器将尝试把文件装到虚拟地址空间的400000h处。字眼"优先"表示若该地址区域已被其他模块占用,那PE装载器会选用其他空闲地址。
- SectionAlignment:映像文件在内存中的区段对齐大小。
- FileAlignment:映像文件在磁盘中的区段对齐大小。
- SizeOfImage:内存中整个PE映像体的尺寸。它是所有头和节经过节对齐处理后的大小。
- SizeOfHeaders:为MS-DOS头、PE头、区块表的尺寸之和。
- NumberOfRvaAndSizes:数据目录表的成员数量,一般为16个。
- IMAGE_DATA_DIRECTORY DAtaDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]为数据目录数组,这些目录中的每一个目录都描述了一个特定的、位于目录项后面的某一节中的信息的位置。
代码:
//获取PE映像扩展头
void GetOptionalHeader()
{
m_Optinal = m_pNT->OptionalHeader;
m_pDataDir =(PIMAGE_DATA_DIRECTORY)m_pNT->OptionalHeader.DataDirectory;
}
实现函数如下:
代码:
DWORD CPeHear::RVA2Offset( DWORD dwRVA )
{
for ( DWORD i=0; i<m_pNT->FileHeader.NumberOfSections; i++ )
{
if ( dwRVA>=m_pSection[i].VirtualAddress && dwRVA<m_pSection[i].VirtualAddress+m_pSection[i].Misc.VirtualSize )
{
DWORD dwR_Offset = dwRVA - m_pSection[i].VirtualAddress;
DWORD dwOffset = (DWORD)m_lpFileImage + dwR_Offset + m_pSection[i].PointerToRawData;
return dwOffset;
}
}
return 0;
}
用来保存PE区段信息的结构体:
代码:
typedef struct _SECTIONHEADER
{
CString NAME;
DWORD PointerToRawData; //区段在文件中的偏移
DWORD SizeOfRawData; //文件中区段对齐大小
DWORD MiscVirtualSize; //偏移大小
DWORD VirtualAddress; //区段的RVA地址
}SECTION,*PSECTION
- VirtualSize:实际使用的区段大小。
- VirtualAddress:此区段载入内存后的RVA。
- PointerToRawData:此区段在文件中的偏移。
- SizeOfRawData:此区段在磁盘中的体积,这个地址是按照文件页对齐的。
- Characteristics:区段属性,用以描述此区段的读写情况、状态等属性。
通过自定义GetSectionHeader();函数获取到PE区段信息,使用IMAGE_FIRST_SECTION 获取到区段表中第一个区段,通过区段个数遍历循环获取出区段表中的信息。将区段名称,实际呗使用的区段大小,此区段载入内存后的RVA,和此区段在文件中大小等信息。保存到结构体类型的向量中。
输出表信息:
保存输出表信息自定义结构体:
代码:
typedef struct _EXPORT
{
DWORD ID; //导出函数的序号
DWORD EAT; //导出函数的地址RVA
DWORD EX_RVA; //导出函数的地址偏移
CString NAME; //导出的函数名称
}EXPORTA,*PEXPORT;
- Name指向模块名的ASCII字符的RVA
- Base 导出表用于输出API索引值的基数
- NumberOfName:ENT中的条目数量,ENT的条目数量小于等于EAT中条目的数量。
- NumberOfFunctions:EAT中的条目数量。
- AddressOfNameOrdinal:EAT的RVA(导出函数地址的相对虚拟地址),EAT中的每一个非0的项都对应一个被导出的函数名称或序号。
- AddressOfNames:ENT的RVA(导出函数名的相对虚拟地址),ENT和EAT的每一个非0的项都对应一个被导出的函数地址或序号。
- AddressOfNameOrdinals:导出序号表的RVA。这个表是一个WORD类型的数组。它将ENT中的索引映射到导出地址表中相应的元素上。
GetExportDirectory() 获取输出表信息,一般只是DLL文件中有输出表信息,所以需要判断该程序是否有输出表,如果没有则将设置弹出框标志位,弹出提示没有导出表结构,返回。
如果有输出表,则获取到PE输出表的信息输出表偏移函数数量,函数名数量、函数地址等信息,基址,名称,函数名称地址,名称字符串,函数名序号地址。
然后使用EXPORTA 类型的vector来保存导出函数序号,导出函数的RVA,偏移地址和函数名称。
通过以下函数和上面的结构体获取到导出表相应的信息。
代码:
void GetExportDirectory()
{
PIMAGE_DATA_DIRECTORY pExportDir = &m_pDataDir[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (pExportDir->Size == 0)
return;
DWORD dwExportOfffset = RVA2Offset(pExportDir->VirtualAddress);
m_pExport = (PIMAGE_EXPORT_DIRECTORY)dwExportOfffset;
m_ExportOffset = RVA2Offset(m_Optinal.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress) - (DWORD)m_lpFileImage;
PDWORD pEAT = (PDWORD)RVA2Offset(m_pExport->AddressOfFunctions);
PDWORD pENT = (PDWORD)RVA2Offset(m_pExport->AddressOfNames);
PWORD pEIT = (PWORD) RVA2Offset(m_pExport->AddressOfNameOrdinals);
//m_ExportOffset =
for ( DWORD dwOrdinal=0; dwOrdinal<m_pExport->NumberOfFunctions; dwOrdinal++ )
{
if ( !pEAT[dwOrdinal] )
continue;
for ( DWORD dwIndex=0; dwIndex<m_pExport->NumberOfFunctions; dwIndex++ )
{
EXPORTA saveImport;
if ( pEIT[dwIndex] == dwOrdinal )
{
PCHAR pszFunName = (PCHAR)RVA2Offset(pENT[dwIndex]);
saveImport.NAME.Format(_T("%S"),pszFunName);
saveImport.EX_RVA = m_pExport->AddressOfFunctions;
saveImport.ID = m_pExport->Base+dwOrdinal;
saveImport.EAT = pEAT[dwOrdinal];
m_vExport.push_back(saveImport);
break;
}
else if ( dwIndex == m_pExport->NumberOfFunctions-1 )
{
saveImport.NAME.Format(_T("(Null)"));
saveImport.ID = m_pExport->Base+dwOrdinal;
saveImport.EX_RVA = m_pExport->AddressOfFunctions;
saveImport.EAT = pEAT[dwOrdinal];
m_vExport.push_back(saveImport);
break;
}
}
}
}
输入目录是一个多IMAGE_IMPORT_DESCRIPTOR(输入描述结构)的数组,每个被使用的DLL文件都有一个。(它们的)列表由一个全部用0填充的IMAGE_IMPORT_DESCRIPTOR(输入地址表目录项)结构作为结束。
保存输入表信息结构体:
附件 84865
相应的导入表结构体:
代码:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // 指向输入名称表(IAT)的RVA
} DUMMYUNIONNAME;
DWORD TimeDateStamp; //时间标识
DWORD ForwarderChain; // 转发链如果不转发此值为0
DWORD Name; //指向导入映像文件的名字
DWORD FirstThunk; // 指向导入地址表(IAT)的RVA
} IMAGE_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // 转发字符的RVA
DWORD Function; // 被导入函数的地址
DWORD Ordinal; //被导入函数的序号
DWORD AddressOfData; // 指向输入名称
} u1;
} IMAGE_THUNK_DATA32;
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //需导入函数序号
CHAR Name[1]; //导入函数名称
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
代码:
void CPeHear::GetImportDirectory()
{
PIMAGE_DATA_DIRECTORY pDir = (PIMAGE_DATA_DIRECTORY)m_pNT->OptionalHeader.DataDirectory;
PIMAGE_DATA_DIRECTORY pDataDir = pDir+IMAGE_DIRECTORY_ENTRY_IMPORT;
PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)RVA2Offset(pDataDir->VirtualAddress);
while ( pImport->Name )
{
IMPORT saveImport;
saveImport.pszDllName = (PCHAR)RVA2Offset(pImport->Name);
PIMAGE_THUNK_DATA32 pINT = (PIMAGE_THUNK_DATA32)RVA2Offset(pImport->OriginalFirstThunk);
while ( pINT->u1.Ordinal )
{
if ( !IMAGE_SNAP_BY_ORDINAL32(pINT->u1.Ordinal) )
{
PIMAGE_IMPORT_BY_NAME pByName = (PIMAGE_IMPORT_BY_NAME)RVA2Offset(pINT->u1.AddressOfData);
saveImport.NAME.Format(_T("%s"),pByName->Name);
saveImport.ID = pByName->Hint;
saveImport.IM_RVA = pImport->FirstThunk;
saveImport.Offset = RVA2Offset(pImport->FirstThunk);
m_vImport.push_back(saveImport);
pINT++;
continue;
}
// 2.4.2 如果最高位为1,则直接打印Ordinal部分
saveImport.NAME.Format(_T("Null"));
saveImport.ID = pINT->u1.Ordinal&0x0000FFFF;
saveImport.IM_RVA = pImport->FirstThunk;
saveImport.Offset = RVA2Offset(pImport->FirstThunk);
m_vImport.push_back(saveImport);
pINT++;
}
m_vpImport.push_back(pImport);
pImport++;
}
}
由于本程序是通过继承实现的,要使不同的模块访问同一个基类的时候,基类中保存的信息不会改变,所以要使用静态变量来保存PE结构中的需要用到的信息。
变量信息:
代码:
static PVOID m_lpFileImage; //文件内存中起始加载位置
static CString m_strInPath; //文件名
static IMAGE_FILE_HEADER m_PeHead; //PE映像文件头
static IMAGE_OPTIONAL_HEADER m_Optinal; //PE映像扩展头
static PIMAGE_SECTION_HEADER m_pSection; //获取PE区段信息
static PIMAGE_DOS_HEADER m_pDos; //DOS头地址
static PIMAGE_NT_HEADERS m_pNT; //PE头地址
static PIMAGE_DATA_DIRECTORY m_pDataDir; //PE数据目录
static PIMAGE_EXPORT_DIRECTORY m_pExport; //输出表
//static PIMAGE_IMPORT_DESCRIPTOR m_pImport; //输入表
static CString m_pszDllName;
static DWORD m_pImageHeaderNum; //区段数量
static DWORD m_NumberOfRvaAndSizes; //
static DWORD m_ExportOffset; //导出表的偏移
static vector<SECTION> m_vSection; //保存区段中的信息
static vector<EXPORTA> m_vExport; //保存导出表的信息
static vector<IMPORT> m_vImport; //保存导入表的信息
static vector<PIMAGE_IMPORT_DESCRIPTOR> m_vpImport;
总结:
此类的编写实现了PE文件的解析,可以将PE文件中内容显示到界面,但是对于修改文件的PE信息,没有用指针容易实现,没有用到指针不会出现内存泄露等指针问题,但是没有使用指针的节省空间和指针的操作速度,指针对文件修改的方便。有些函数完成的不是很好,需要进一步改善,通过这个项目更好的了解PE结构,对PE的分析有了更好的理解。但是目前软件尚未完成,通过以后的时间慢慢完善。:eek: