⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 readme.txt

📁 在《软件加密技术》这本书里看过PE文件各部分的详细解释之后
💻 TXT
字号:
    
    在《软件加密技术》这本书里看过PE文件各部分的详细解释之后,我也有了一个自己写PE文件分析器的的想法。虽然好的分析器不在少数,但对于一堆十六进制数,有些朋友可能不明白它代表什么意思。如果在程序里就可以将这些01序列转换成可以直接看懂得信息,那至少用户可以省去以后去查表的麻烦。怀着这样的想法,我仔细的研究了书中分析器PEInfo的源代码,我发现它没有提供信息转换的功能。

    通过研究发现,PEInfo是通过PE文件在内存中的映象来获取文件信息的,我在想是否还有别的方法可以绕过将文件映象到内存这一步,直接读取文件信息。这样的方法只有直接读取磁盘上的PE文件,在磁盘上寻找所需要的文件信息。

    在这里暂且不说这样的做法和内存映象法有什么优劣,我在此仅仅只是想找寻另一条解决问题的道路,并实现之。看完我的分析和源程序,大家自然知道孰优孰劣。

    为了避免引起混淆,程序中采用了与PE标准种类似变量名来定义关键的数据,如文件头,可选文件头,节表,导入表和导出表,具体名称定义细节可以在winnt.h里查到。整个程序是以面向过程的方式写的,适当结合了面向对象的特征。我将读取的PE文件信息封装在一个对象DataDump里,这样是为了方便数据的管理和最后输出分析报告。而对文件的分析则分别有一系列的子程序来完成。现将子程序说明如下:

//-------------------------------------------------------------------------------------------------------------------

	BOOL Is_EXE_file( ifstream& PE_file )                       //判断是否是合法的PE文件,是则返回true,否则返回false

	BOOL OutReady( CHAR filename[], ofstream& fout )            //输出准备,包括输出流和输出文件,是则返回true,否则返回false

	VOID WriterInfo( ofstream& fout )                           //输出程序版本信息

	BOOL Load_EXE_Info( ifstream& PE_file )                     //读取PE文件信息,成功返回true,否则返回false

	VOID Decode_EXE_Info(CHAR filename[], BOOL IsEXE, ifstream& PE_file, ofstream& fout)  //分析PE文件信息

	VOID ToNumeric( LPDWORD ptr, CHAR buf[], INT start, INT size )   //将字符数组从start位开始,转换size位为数值,放入ptr指向的DWORD类型变量中
	
	VOID ToString( LPSTR ptr, CHAR buf[], INT start, INT size)       //从字符数组从start位开始,取出其后的size位,放入一个ptr指向的的字符数组中

//-------------------------------------------------------------------------------------------------------------------

        class DataDump 
	{
	  private :
		IMAGE_FILE_HEADER FILE_HEADER;                                  // IMAGE_FILE_HEADER 
		IMAGE_OPTIONAL_HEADER32 OPTIONAL_HEADER32;                      // IMAGE_OPTIONAL_HEADER32
		PIMAGE_SECTION_HEADER SECTION_HEADER;                           // PIMAGE_SECTION_HEADER
		IMAGE_IMPORT_DESCRIPTOR IMPORT_DESCRIPTOR;                      // IMAGE_IMPORT_DESCRIPTOR
		PIMAGE_EXPORT_DIRECTORY EXPORT_DIRECTORY;                       // PIMAGE_EXPORT_DIRECTORY

		DWORD ExVRk, ImVRk;                                             // 输出表和输入表在磁盘文件的偏移和RVA的差值

	  public :                                                          // You can get the functions of these member functions below by their names.
		DataDump();
		~DataDump();

		BOOL Set_FILE_HEADER( CHAR [], INT );
		BOOL Set_OPTIONAL_HEADER32( CHAR [], INT );
		BOOL Set_SECTION_HEADER32( CHAR [], INT );
		BOOL Set_EXPORT_TABLE( CHAR [],  INT );
		
                VOID GetReady( CHAR [] );
		DWORD Get_OPTIONAL_HEADER_SIZE( VOID ) const; 
		DWORD Get_SECTION_NUMBER( VOID ) const;
		DWORD Get_EXPORT_TABLE_RAW( VOID ) const;
		DWORD Get_IMPORT_TABLE_RAW( VOID ) const;

		VOID Set_Export_VRk( VOID );
		VOID Set_Import_VRk( VOID );
		BOOL Export_Table_Existed( VOID ) const;
		BOOL Import_Table_Existed( VOID ) const;
		
		BOOL Show_FILE_HEADER( ofstream& ) const;
		BOOL Show_OPTIONAL_HEADER32( ofstream& ) const;
		BOOL Show_SECTION_HEADER32( ofstream& ) const;
		BOOL Show_EXPORT_TABLE( ifstream&, ofstream& ) const;
		BOOL Show_IMPORT_TABLE( ifstream&, ofstream& ) const;

	};   
	DataDump pool; 
    
//-------------------------------------------------------------------------------------------------------------------



   DataDump类的实例是全局对象,这样做是方便子程序对该对象的访问。程序的基本思路是,在磁盘上打开PE文件,判断其是否为合法的PE文件,否则输出错误信息,退出;是则进行分析,包括读取文件头,可选文件头,节表,导入表和导出表,将信息储存在DataDump类中,最后以txt文件的形式输出一份文件的分析报告。

   程序的关键在于文件信息的“定位读取”上。文件头,可选文件头和节表在磁盘上是顺序存放的,跳过开始的PE标志段,就可以轻松找到上述几段,而且每一部分的确切大小都在它们的相关属性里描述了,在程序运行时可以知道的,读取信息的工作很容易就可以完成。而输入表和输出表的大小是不确定的,有的时候会存在没有输入表或没有输出表的情况,再加上输入输出表的出现位置也不固定,这会给读取输入输出表的工作带来一些困难。

   我们知道,一般在PE文件里某一项给的都是相对虚拟地址RVA,并不能直接和磁盘文件的物理地址相对应。在以内存映象为基础的方法中,只需要取得RVA,和ImageBase作简单的运算以后就可以定位到某一项数据在内存中的保存地址。而在以直接读取磁盘文件的方法里,必然要涉及到RVA到真实物理地址RAW的转换。所以对输入输出表的读取的关键转换到对输入输出表在磁盘文件上的定位了。

以下是一个通用的转换方法及示例:

+---------+---------+---------+---------+---------+---------+
|  段名称   虚拟地址  虚拟大小  物理地址  物理大小   标志      |
+---------+---------+---------+---------+---------+---------+
|  Name     VOffset    VSize    ROffset    RSize      Flags |
+---------+---------+---------+---------+---------+---------+
|  .text   00001000   00000092  00000400  00000200  60000020|
|  .rdata  00002000   000000F6  00000600  00000200  40000040|
|  .data   00003000   0000018E  00000800  00000200  C0000040|
|  .rsrc   00004000   000003A0  00000A00  00000400  C0000040|
+---------+---------+---------+---------+---------+---------+
文件虚拟偏移地址和文件物理偏移地址的转换公式如下:
FileOffset = VA - ImageBase - VRk (VRk是文件虚拟地址和文件物理址之间的差值)
           = RVA - VRk

>>>>>>>VaToFileOffset(虚拟地址转文件偏移地址)
如VA = 00401000 (虚拟地址)
ImageBase = 00400000 (基地址)
VRk = VOffset - ROffset = 00001000 - 00000400 = C00 (得出文件虚拟地址和文件物理址之间的VRk值)
FileOffset = VA - ImageBase - VRk = 00401000 - 00400000 - C00 = 400(文件物理地址的偏移地址)

    这样看来,关键就在于如何求这个VRk上。其实很简单,就用节表数据项里面的VirtualAddress减去PointerToRawData,就可以得到。VirtualAddress从字面上看,似乎是虚拟地址,但其实它也是个RVA,是相对于内存映象后的首地址的偏移,而PointerToRawData,严格的说,是相对与磁盘文件开始处的物理偏移,那对输出表的VRk来说,计算公式应该是这样:
    ExVRk = SECTION_HEADER[i].VirtualAddress - SECTION_HEADER[i].PointerToRawData;
而此时,内存和磁盘文件有相同的基址,即ImBase = RawBase。输入表的VRk也是相同的计算方法。
    ImVRk = SECTION_HEADER[i].VirtualAddress - SECTION_HEADER[i].PointerToRawData;

    我们首先要找到输入输出表所处的区段。虽然一般以.idata和.edata命名的就是输入输出表数据区段,但一旦更改了区段名称,就无从查起了。在这应该把IMAGE_OPTIONAL_HEADER32中DataDirectory数组里输入表和输出表的VirtualAddress定位到节表中VirtualAddress划分出来的区间里,就可以找到输入输出表所处的区段。代码如下:

	  for ( INT i=1; i<FILE_HEADER.NumberOfSections; i++ )
	  {
		if ( SECTION_HEADER[i].VirtualAddress>OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress  )
		{
		  ExVRk = SECTION_HEADER[i-1].VirtualAddress - SECTION_HEADER[i-1].PointerToRawData;
		  break;
		}
	  }

    接下来对于和输入表相关的数据,只需要用对应项减去ImVRk就是这一项在磁盘文件里的偏移。如输入表的OriginalFirstThunk的RVA是00318140,只要用这个值减去ImVRk,就可以得到OriginalFirstThunk在磁盘文件的偏移。So is Export Table!

    其他就只剩下怎么处理读取的数据了。我用的是C++的文件输入流fstream,以二进制的形式读进一批数据,通常都是以相应块的大小读入数据,如以sizeof(IMAGE_FILE_HEADER),然后通过 ToNumeric( LPDWORD ptr, CHAR buf[], INT start, INT size )函数将字符形式的变量转换为数值型,有时有需要一些字符型的数据,如函数名,就要用ToString( LPSTR ptr, CHAR buf[], INT start, INT size)取出特定的某几位字符,这些在源代码里都可以看到。我在写的过程中,发现C++的输入流不是太稳定,有的时候会读不进数据。我在每一个涉及到读入数据的地方都加了输入流的clear()函数,它重置了流的状态,让流始终处于稳定的状态下。输出分析报告到txt文件,我用的是C++的输出流,为了保证输出的稳定性,我也调用了输出流的clear()函数。最后的报告会保存在和用户输入的可执行文件同名的文本文件里。

    最后有一点申明,这个程序是在Visual C++ 6.0环境下编译的,在其他的C++环境下好像不能编译通过,因为winnt.h的版本问题,不同编译环境,所带的winnt.h内容不尽相同,在这些环境下编译会出错。而且这个程序可以在32位和64位环境下运行,但还不能分析64位的应用程序。可能是64位的PE32+格式和32位的PE格式不同引起的,因为我用PEid0.94和stud_PE也不能分析64位应用程序。

    读到这里,如果你看完源代码,应该可以得到自己的结论了。哪种方法更好,一目了然的,但仔细斟酌,每种方法都有他自己的优点和弊病。但这不是我所关心的事情,关键是我在这过程中,更加深入理解了PE的结构,锻炼了自己的编程能力。欢迎大家发表意见,关于程序的,关于PE的,or something else。程序写得仓促,在代码可读性上敬请原谅。有什么好的建议,欢迎大家和我联系。
    
    E-mail : fahrenheit871116@163.com

    写完之后,就像高考结束在等待成绩到来的那一段时间,放松,悠闲,别人怎么评价已不重要,尽力就好!^_^

    HQ(Fahrenheit)  04CS,   NJU

    


    

   
   

    
    
    

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -