首页
社区
课程
招聘
[原创]自己写的文件分析器-PE DeCODER v1.0( 看雪学院2006金秋读书季)
发表于: 2006-10-16 22:56 13564

[原创]自己写的文件分析器-PE DeCODER v1.0( 看雪学院2006金秋读书季)

2006-10-16 22:56
13564

(源代码和程序在上传文件里)
    在《软件加密技术》这本书里看过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

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


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

上传的附件:
收藏
免费 7
支持
分享
最新回复 (17)
雪    币: 263
活跃值: (10)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
2
顶一下高手,想你学习了
2006-10-16 23:02
0
雪    币: 257
活跃值: (11)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
3
学习~~~~
2006-10-16 23:21
0
雪    币: 53
活跃值: (80)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
不错啊,可学习
2006-10-17 06:47
0
雪    币: 179
活跃值: (131)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
5
学习
2006-10-17 09:45
0
雪    币: 179
活跃值: (131)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
6
可执行文件用Release再编译一下好了
2006-10-17 09:47
0
雪    币: 405
活跃值: (10)
能力值: ( LV9,RANK:1130 )
在线值:
发帖
回帖
粉丝
7
PE结构现在我还是比较模糊。理解不够深刻,因为都不怎么研究。

学习
2006-10-17 16:00
0
雪    币: 214
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
支持开源的!!!!
2006-10-17 16:21
0
雪    币: 296
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
顶一下搂主的精神!学习
2006-10-17 19:45
0
雪    币: 175
活跃值: (2491)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
dos窗口的?
2006-10-17 20:06
0
雪    币: 538
活跃值: (460)
能力值: ( LV9,RANK:290 )
在线值:
发帖
回帖
粉丝
11
高手,顶下。。。。。。。。
2006-10-18 16:10
0
雪    币: 716
活跃值: (162)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
12
sustain!!!
2006-10-18 22:53
0
雪    币: 238
活跃值: (326)
能力值: ( LV12,RANK:450 )
在线值:
发帖
回帖
粉丝
13
	  for ( INT i=0, j=0; i<OPTIONAL_HEADER32.NumberOfRvaAndSizes; i++, j+=8 )
	  {
	    ToNumeric((LPDWORD)&OPTIONAL_HEADER32.DataDirectory[i].VirtualAddress, buf, 96+j, 4);
		ToNumeric((LPDWORD)&OPTIONAL_HEADER32.DataDirectory[i].Size, buf, 100+j, 4);
	  }

原程序第 154 行的地方(见上面)要加一个判断,如果 NumberOfRvaAndSizes大于 16 时将这个值设置为 16。
Nicolas Brulez 在他的 Anti Reverse Engineering Uncovered 文中给出了一个实例,其中NumberOfRvaAndSizes是如下设置的:
LoaderFlags: 0xABDBFFDE <--- Bogus Value
NumberOfRvaAndSizes: 0xDFFFDDDE <--- Bogus Value

用你的程序读取这个文件时将出错,读写内存错,然后立即退出。
另外你的程序读取那个全世界最小的PE文件 MiniPE.exe 时直接退出,没有任何提示,你自己去研究一下那个PE头,然后修改一下程序,我没有时间仔细看你的代码,请原谅。

注:今天回头一看,居然将出错位置写错,很对不起,现在已经纠正。
2006-10-19 01:45
0
雪    币: 222
活跃值: (10)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
14
Thanks,i will fix it !
2006-10-20 13:51
0
雪    币: 9754
活跃值: (2141)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
佩服楼主!学习中……
2006-10-20 14:07
0
雪    币: 222
活跃值: (10)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
16
(上传文件在附件里)
        说来惭愧,前几天自己写的一个文件分析器放在论坛上,有一位高手找到了我的分析器的BUG,并回了我的帖子。我仔细一想,确实存在一些不完善的地方,当晚我修改了程序代码,改进了不足的地方,并做了比较充分地测试。现在我把我的文件分析器的PE DeCODER v1.1放在论坛上,一是向使用过1.0版本且有运行错误的朋友表示一下歉意,是自己的疏忽没有做到尽量的完美,二也想给我找出程序BUG的这位朋友表示感谢,Thanks a million!

        说到这个版本相对于1.0版本有那些改进,主要是体现在程序运行的稳定性得到了提高。在一些朋友的使用过程中,会出现内存错误的情况。针对如此,我发现异常情况都是出现在读取输入,输出表信息和前后两次读取不同exe文件的时候,所以我主要在程序里做了如下的几个方面的改进:

        1)在DataDump对象里增加了Reset( void )成员函数。
        2)改进了DataDump对象里输出输出表(Export_Table)的成员函数BOOL Show_EXPORT_TABLE( ifstream&, ofstream& ) const。
        3)改进了DataDump对象里输出输出表(Import_Table)的成员函数BOOL Show_IMPORT_TABLE( ifstream&, ofstream& ) const。
        4)在一些小细节上作了改进。

        大家可能会问,难道文件头、可选文件头和节表的读取就没有改进吗?这是因为这三部分在一个exe文件中处于相对固定的位置。我们读取信息的关键是在于文件信息的定位,读取这一部分相对固定的信息,技术含量相对较低,出错的概率也非常小,故暂时没有对这一部分作出处理。下面我会逐个分析作出改进的原因和改进方案。

        1)在DataDump对象里增加了Reset()成员函数。
          在使用过程中,比如第一次输入文件名“c:\foo.exe”,分析完毕后再输入“C:\sanTC.exe”,进行下一个分析任务。那么第二次分析的结果可能会出错。具体表现是在输出表信息部分,会出现乱码。后来发现其实前一个是有输出表的,而后一个文件则没有输出表。所以在第二次分析时,本不该有输出表信息,但却显示乱码。这是由于前一次输出信息完毕以后,没有将节表中保存输出表信息结构回到原来的初始状态。第二次因为该指针未回到初始状态,让程序误以为存在输出表,就会显示出乱码。改进方法就是加一个Reset()成员函数。代码如下:
        VOID DataDump::Reset( VOID )
        {
          if ( SECTION_HEADER )
                delete SECTION_HEADER;
          SECTION_HEADER = NULL;
          ExVRk = ImVRk = 0x00000000;
          OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress = OPTIONAL_HEADER32.DataDirectory[0].Size = 0x00000000;        //Export table
                OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress = OPTIONAL_HEADER32.DataDirectory[1].Size = 0x00000000;        //Import table
        }

函数里释放了SECTION_HEADER指向的资源,因为每个程序结表的数目可能不同,让保存输出表信息结构回到初始状态,而指向输出表信息的指针所指的结构一旦分配了资源,就不必急于回收,因为每个文件的输出表的该结构大小都一样,新的值直接覆盖原来的值就行,最后交给对象的析构函数回收就可以了。既避免了内存泄漏,又能解决上述的问题。

        2)成员函数BOOL Show_EXPORT_TABLE( ifstream&, ofstream& ) const是负责输出表的显示的,但是也负责读取和显示输出表成员所指向的函数和函数地址等一类的东东。在DataDump里还有一个成员函数是BOOL Set_EXPORT_TABLE( CHAR [],  INT ),它是负责对EXPORT_DIRECTORY所指向的结构的读取,因为这一部分是固定的,可以预先读取,当然是在存在输出表的情况下。BOOL Show_EXPORT_TABLE( ifstream&, ofstream& ) const中的关键代码如下:
        if ( EXPORT_DIRECTORY->NumberOfFunctions )
        {
          OrdalOffset = EXPORT_DIRECTORY->AddressOfNameOrdinals-ExVRk;               // Get The Raw Offset Of NameOrdinals
          
          FunOffset = EXPORT_DIRECTORY->AddressOfFunctions-ExVRk;                        // Get The Raw Offset Of Functions
                  
          NameOffset = EXPORT_DIRECTORY->AddressOfNames-ExVRk;               // Get The Raw Offset Of Name_RVA_Arrays
          buf[31] = 0;

          INT* OrdalArray = new INT [EXPORT_DIRECTORY->NumberOfFunctions];

          for ( INT i=0; i<EXPORT_DIRECTORY->NumberOfFunctions; i++ )
          {
            OrdalArray[i] = -1;
          }

          for ( i=0; i<EXPORT_DIRECTORY->NumberOfNames; i++ )
          {
            PE_file.seekg(OrdalOffset+2*i);                                         // Get The Item Of The Serial Table                             
            PE_file.read(buf, 2);
            Offset = 0x00000000;
            ToNumeric((LPDWORD)&Offset, buf, 0, 2);                      // Store The Index In The Array "OrdalArray"
            OrdalArray[Offset] = i;
          }

          for ( i=0; i<EXPORT_DIRECTORY->NumberOfFunctions; i++ )
          {                    
                fout<<setfill('0')<<"    "<<setw(8)<<EXPORT_DIRECTORY->Base+i;               // Get The Serial Of The Function
            if ( PE_file.eof() )
              break;
            PE_file.seekg(FunOffset+4*i);
            PE_file.read(buf, 4);
            ToNumeric((LPDWORD)&Offset, buf, 0, 4);                                      // Get The Item Of EAT into offset
            fout<<setfill('0')<<"            "<<setw(8)<<Offset;

                 if ( OrdalArray[i]!=-1 )     // If the OrdalArray[i]!=-1, which represents index i is injected into an Item ENT
            {
              PE_file.seekg(NameOffset+4*OrdalArray[i]);
                                         // OrdalArray[i] is the Index of the ENT
              PE_file.read(buf, 4);
                  ToNumeric((LPDWORD)&Offset, buf, 0, 4);
              PE_file.seekg(Offset-ExVRk);
              PE_file.read(buf, 31);
              buf[31] = 0;
              fout<<setfill(' ')<<setw(34)<<buf<<endl;
               }
            else
            {
              fout<<"                   ---------------"<<endl;
            }
          }
          delete [] OrdalArray;
        }
        else
        {
          fout<<" ->No Exact Information !"<<endl;
        }               
      }

        在输出表存在的情况下,首先判断包含的输出函数数目,不为零则继续。分别读取输出序数表、输出函数地址表(EAT)和输出函数名称表(ENT)的物理首地址。然后分配一个数组OrdalArray[EXPORT_DIRECTORY->NumberOfFunctions],保存输出序数表项目的值。输出序数表和输出函数名称表(ENT)存在一种对应关系,输出序数表将ENT的数组索引映射到相应的输出地址表(EAT)条目。事实上,最关键的部分就在于此,这个数组直接关系到输出表信息是否能正确定位。然后程序按顺序读取EAT中的条目。程序根据相应项的值是否为非负数,判断这是不是一个ENT的索引值。我们知道输出函数可以以序数和名字两种方式输出,满足对应关系的,就是按名字输出的,可以到文件中读取名字字符串。而第i项的名字字符串的首地址,应该是ENT+4*OrdalArray[i]。有的朋友会问,这里为什么要用一个大小为NumberOfFunctions的数组,按名字输出的函数只有NumberOfNames个,会很费空间。这是和输出序数表的结构有关的。假如直接读入输出序数表的值,确实只要NumberOfNames个。但是假如在此情况下有OrdalArray[3]=2,它表示的是第2个输出函数的名称的首地址存放在ENT的第4项,即ENT[3];而在我们的程序中,OrdalArray[3]=2表示的是第3个输出函数的索引值是2,即第3个输出函数的名称的首地址存放在ENT[2]里。有朋友又会问,难道这些序数不是顺序存放的的吗?一般情况下是这样,但也有特例,Misrosoft Office2003中的Execel.exe的输出函数序数就不是按顺序存放的。在这里也不能人为的把数组整理成有序,关键的是当前项在数组OrdalArray里的索引值不能被改动。用较小的数组,在后面就要用到查找算法,不管怎么说,这都增加了程序的复杂度。两种方法各有利弊,前者则牺牲了空间换回了时间,后者是牺牲时间节省了空间。下面有一个粗略的比较:

     OrdalArray数组大小             时间复杂度             空间复杂度           附加代码量
     NumberOfFunctions=n1           n1+n2                    n1                    小
     NumberOfNames    =n2           n2+n2*(n2-1)/2              n2                    大
                        
     其中n2<=n1

一般情况下,输出函数中按名字输出的占到大部分,所以相比较而言,我选用了前面一种算法。后面一种也有可取之处,在按序号输出的情况占大多数的情况下效率更高。只有通过刚才所述的那种映射,才能找到正确的函数名称。有的时候,在分析报告里,可能会看到有的输出函数的RVA为0x00000000,名称显示不出来。这并不是程序错误。EAT中允许存在为0的条目,表示这个序数值没有代码或数据被输出。

        3)成员函数BOOL Show_IMPORT_TABLE( ifstream&, ofstream& ) const是负责输入表的读取和显示的。和输出表不同,程序里没有BOOL Set_IMPORT_TABLE( CHAR [],  INT )函数,因为输入表的不确定性更大。包括输入表描述符IMPORT_DESCRIPTOR的个数,以及每个Dll文件输入的函数的数目,所以这个函数里要负责输入表的读取和显示。程序的关键代码如下:
          
                  if ( IMPORT_DESCRIPTOR.FirstThunk )
                  {
                    Thunk = IMPORT_DESCRIPTOR.FirstThunk;
                  }
                  else
                  {
                    Thunk = IMPORT_DESCRIPTOR.OriginalFirstThunk;
                  }            

                  DWORD OFOriginalFirstThunk = IMPORT_DESCRIPTOR.OriginalFirstThunk;

                  if ( Thunk )
                  {
                    for ( INT j=0; ; j++, OFOriginalFirstThunk += 4, Thunk+=4 )
                    {
                      if ( PE_file.eof() )
                        break;
                          
                      PE_file.seekg(Thunk-ImVRk);
                      PE_file.read(buf, 4);
                          
                      ToNumeric((LPDWORD)&ThunkValue, buf, 0, 4);
                  
                      if ( !ThunkValue )
                      {
                        break;
                      }

                      fout<<"             "<<setfill('0')<<setw(8)<<OFOriginalFirstThunk<<"        "<<setw(8)<<ThunkValue;
                      if ( !(ThunkValue & 0x80000000) )  // MSB Setted '0' Represents It Is A Pointer To IMAGE_IMPORT_BY_NAME
                      {
                        PE_file.seekg(ThunkValue-ImVRk);
                        PE_file.read(buf, 2);
                        ToNumeric((LPDWORD)&hint, buf, 0, 2);
                       
                        PE_file.read(buf, 28);
                        fout<<"        "<<setfill('0')<<setw(4)<<hint;
                        ToString(buf, buf, 0, 28);
                        fout<<setfill(' ')<<setw(30)<<buf;                       
                      }
                      else
                      {
                        fout<<"        ----               ---------------"<<endl;
                      }

                      fout<<endl;
                    }
                  }
        先要从IMPORT_DESCRIPTOR.FirstThunk中读取IMAGE_THUNK_DATA的首地址,如果不行再到IMPORT_DESCRIPTOR.OriginalFirstThunk中读取,这个顺序是不能颠倒的。虽然OriginalFirstThunk和FirstThunk在一个PE文件没有加载到内存中的时候是一样的,都是指向一个IMAGE_THUNK_DATA结构数组。既然OriginalFirstThunk和FirstThunk是相同的,那么为什么Windows要占用两个字段呢?其实在PE文件被PE加载器加载到内存中的时候这个加载器会自动把FirstThunk的值替换为API函数的真正入口,而OriginalFirstThunk只不过是用来反向查找函数名而已。那就是说OriginalFirstThunk有的时候可能为0x00000000,这样就不能对应到函数名称,而FirstThunk一定是存在有效值的,否则加载器找不到API函数的真正入口。在这里为了保证首次就得到有效的值,读取FirstThunk无疑是更好的选择。IMAGE_THUNK_DATA这个结构很有意思,因为在不同的时候这个结构代表着不同的含义。当这个双字的最高位为1时,表示函数是以序号的方式导入的;当最高位为0时,表示函数是以名称方式导入的,这时这个双字是一个RVA,指向一个IMAGE_IMPORT_BY_NAME结构,这个结构用来指定导入函数名称。所以程序里有那么一段是进行位运算,目的正在于此。

        4)在一些小细节上作了改进。

          在输出文件头时,其中有一个属性是TimeDateStamp,表示的是文件创建的时间,是自1970年1月1日以来的格林威治时间(GMT)计算的秒数。输出一串和大的数字,也不太明白他的意思,以该报它转化成我们日常生活中使用的日期时间格式。所以在中添加了以下代码:
        struct tm Time = *gmtime((CONST LONG *)&FILE_HEADER.TimeDateStamp);   
        fout<<"----> 3. Created Time : "<<asctime(&Time);

          在可选文件头里有一项属性是NumberOfRvaAndSizes,他的值一般为16。但是在一些特殊的PE文件,如miniPE里(稍后讨论),这个值会是1。而且,这个值在某些情况下也可能会比16大。所以,在NumberOfRvaAndSizes>16时,把它的值限定在16的范围内是有必要地,主要是考虑到后面计算输入输出表RVA和磁盘物理偏移的差值VRk的正确性。所以在BOOL DataDump::Set_OPTIONAL_HEADER32( CHAR buf[], INT size )中加入了下列语句:
        if ( OPTIONAL_HEADER32.NumberOfRvaAndSizes>16 )
                OPTIONAL_HEADER32.NumberOfRvaAndSizes = 16;

          输入表和输出表信息都安排在文件靠后的位置,为了防止读取信息时,超出文件末尾,在输出和输入表读取的函数里加入了检查是否到文件结束标志EOF的代码,可以比较好的处理体积比较小的PE文件,特别是对miniPE文件。关于miniPE的问题,一般都是经过人工修改的PE文件,省去了一些不需要的节表项目。缩小了输入表的体积,略去了输出表。下面是一个典型的miniPE文件,只有192字节。16进制编码如下,具体的可以参考上传文件里的一篇详细介绍的文章。。在PEDeCODER v1.1中加入了文件末尾判断就可以知道文件结束,避免运行时出错。但是还是得提醒一句,这样的PE文件一般是手工作品,并没有按照PE格式定义的存放顺序来保存数据,在用分析器读取信息是不一定能得到正确地结果,一些比较好的文件分析器亦然!

Offset      0  1  2  3  4  5  6  7   8  9  A  B  C  D  E  F

00000000   4D 5A A1 7C 65 F4 77 BE  BB 1C F0 77 33 C9 EB 08   MZ¡|eôw¾».ðw3Éë.
00000010   50 45 00 00 4C 01 01 00  51 41 80 3C 08 00 75 F9   PE..L...QA?..uù
00000020   54 51 EB 06 70 00 02 01  0B 01 50 6A F5 FF D6 50   TQë.p.....PjõÿÖP
00000030   B8 54 D3 F0 77 FF D0 C3  02 00 00 00 10 00 00 00   ¸TÓðwÿÐÃ........
00000040   FF FF FF FF 00 00 10 00  02 00 00 00 02 00 00 00   ÿÿÿÿ............
00000050   FF FF FF FF FF FF FF FF  04 00 00 00 FF FF FF FF   ÿÿÿÿÿÿÿÿ....ÿÿÿÿ
00000060   C0 00 00 00 FF FF FF FF  FF FF FF FF 03 00 FF FF   À...ÿÿÿÿÿÿÿÿ..ÿÿ
00000070   00 00 10 00 00 10 00 00  00 00 10 00 00 10 00 00   ................
00000080   FF FF FF FF 02 00 00 00  00 00 00 00 00 00 00 00   ÿÿÿÿ............
00000090   A8 00 00 00 28 00 00 00  67 64 69 33 32 2E 64 6C   ¨...(...gdi32.dl
000000A0   6C 00 00 00 00 00 00 00  C0 00 00 00 00 00 00 00   l.......À.......
000000B0   41 72 63 00 98 00 00 00  BC 00 00 00 AE 00 00 00   Arc.˜...¼...®...

          这次还在输出报告的格式上作了些改变,目的是让报告看起来个有序和规范一点。
       
        以上就是这次我租出改进的地方,谢谢大家,希望这次没有让大家失望。^_^
        我的邮箱是 : fahrenheit871116@163.com
上传的附件:
2006-10-25 19:38
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
顶一下搂主的精神!学习
2006-11-13 15:50
0
雪    币: 5340
活跃值: (598)
能力值: (RANK:170 )
在线值:
发帖
回帖
粉丝
18
对齐的问题,没有考虑进来哟
2006-11-21 08:31
0
游客
登录 | 注册 方可回帖
返回
//