用16进制编辑器写Dll文件---学习PE必看的资料之一

 

原作者   :newjueqi

/*该文章的原来出处找不到了,Witch再次表示歉意。*/


                  PE文件格式

   

*****************************************************************************

    PE文件格式是Windows下可执行文件的格式,熟悉PE文件将对操作系统和编译器工作原理的深刻理解。

    本人之所以选择手写DLL而不是手写EXE主要原因是DLL文件比起EXE文件多了输出表和重定位表的处理,手写DLL可以对PE的理解更深刻,有关手写EXE的可参考dncwbc大大写的文章:http://bbs.pediy.com/showthread.php?t=48590

  本次手写DLL是输出一个函数显示一个对话框,对话框标题是“hello”,内容是“hello,pediy!!!”。由以上可知道,DLL文件涉及到的区块如下

                                        表1

   本文的重点是上图所示的几个区块的构建,其它部分如果比较重要的会加说明,如果不重要的就略过,需要详细了解的请参考相关资料。

   那么,就从DOS部首开始分析,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;

MS_DOS头部我们关心的只有两个字段:e_magic 和 e_lfanewe_magic需要被设置成5A4Dh,这个值有个宏是IMAGE_DOS_SIGNATURE,翻译成相应的ASCⅡ码就是“MZ”,是某位大牛名字的缩写(至于是哪位大牛,请翻看《加密与解密3P266),另外一个关键的字段是e_lfanew,这个字段指出了真正PE文件头的偏移量,当PE加载器加载文件后,用基址加上e_lfanew的偏移量,就可得到PE文件的指针。

 接着是DOS stud部分,在windows下用不到,预留80字节的空间。

 现在来计算一下,MS_DOS头部共19个字段值,占64个字节,80+64=144转化为16进制就是90,所以PE文件头在整个文件里的偏移量为90,e_lfanew设置为90 00 00 00( 因为Intel CPU 字符存储是低位在前,高位在后,要把00 00 00 90 写成90 00 00 00 )。

 在DOS头部分,需要设置 e_magic 4D5A, e_lfanew 90 00 00 00

完成后内容如下:

00000000   4D 5A 00 00 00 00 00 00  00 00 00 00 00 00 00 00   MZ..............

00000010   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000020   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000030   00 00 00 00 00 00 00 00  00 00 00 00 90 00 00 00   ............?..

00000040   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000050   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000060   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000070   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000080   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

紧接着DOS文件头是PE文件头(PE Header ),是PE相关结构NT映像头(IMAGE_NT_HEADERS)的简称,内容如下

typedef struct _IMAGE_NT_HEADERS {

    DWORD Signature;

    IMAGE_FILE_HEADER FileHeader;

    IMAGE_OPTIONAL_HEADER32 OptionalHeader;

} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

IMAGE_NT_HEADERS结构中第一个字段Signature被设置成00004550h,转化成ASCⅡ码就是“PE00”,在#define IMAGE_NT_SIGNATURE中定义,这个字段标志着PE文件头的开始。

IMAGE_FILE_HEADER( 映像文件头)中包含了PE文件的一些基本信息,结构如下:

typedef struct _IMAGE_FILE_HEADER {

    WORD    Machine;

    WORD    NumberOfSections;

    DWORD   TimeDateStamp;

    DWORD   PointerToSymbolTable;

    DWORD   NumberOfSymbols;

    WORD    SizeOfOptionalHeader;

    WORD    Characteristics;

} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

1)  Machine:两个字节,可执行文件的目标CPU类型,几种常见的CPU类型如下表:

  

                        表2

     对于我们常用的Intel平台,我们选择设置为014Ch,在16进制编辑器里输入“4C 01”

2)  NumberOfSections:两个字节,区块的数目,紧接着IMAGE_NT_HEADERS后面,从表1可看到我们选择创建了5个区块,所以应该设置成“00 05”, 在16进制编辑器里输入“05 00”

3)  TimeDateStamp:四个字节,文件的创建的时间,设置为“00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

4)  PointerToSymbolTable:四个字节,COFF符号表的文件偏移位置,设置为“00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

5)  NumberOfSymbols:四个字节,如果有COFF符号表,就表示符号表的数目,设置为“00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

6)  SizeOfOptionalHeader:两个字节,紧接着IMAGE_FILE_HEADER后面的IMAGE_OPTIONAL_HEADER32的数据大小,通常对于32位系统,通常设置为00 E0h,对于64位的PE32+文件,通常是00 F0h,设置为“00 E0”, 在16进制编辑器里输入“E0 00”

7)  Characteristics:两个字节,表示文件的属性,各个位的含有如下:

Bit 0:没有重定位信息的话就置1

Bit 1:是可执行文件就置1,表示不是一个目标文件或库文件。这里说明一下,所谓的可执行文件不单单指EXE文件,更包括了编译成功的文件。

Bit 2:行号信息被移去就置1

Bit 3:符号信息被移去就置1

Bit 4:应用程序可处理超过2G的地址时就置1

Bit 7:处理器的低字节是相反的就置1

Bit 8:目标平台是32位机器就置1

Bit 9:如果没有调试信息在内就置1

Bit 10:如果程序不能从移动磁盘或多媒体光盘执行就置1

Bit 11:如果程序不能够从网络中运行就置1

Bit 12:是系统文件就置1

Bit 13:是DLL文件就置1

Bit 14:如果文件只能在单处理器上运行就置1

Bit 15:处理器的高字节是相反的就置1

 在这里,本人设置属性值为“A1 8E”, 分别置Bit 1Bit 2Bit 3Bit 7Bit 8Bit 13Bit 151,在16进制编辑器里输入“8E A1”

经网友cnhnyu指点,本人设置Characteristics的属性值是有问题,PECOFF里建议把第7和第15位设置为0,特此提出来

 

 好,现在的PE文件头部分的Signature IMAGE_FILE_HEADER部分的16进制源码如下:

 

00000090   50 45 00 00 4C 01 05 00  00 00 00 00 00 00 00 00   PE..L...........

000000A0   00 00 00 00 E0 00 8E A1                            ....?

紧接着IMAGE_FILE_HEADER就是IMAGE_OPTIONAL_HEADER32结构,注意,虽然名字是可选,但实际上是必须的。这个结构的定义如下:

typedef struct _IMAGE_OPTIONAL_HEADER {

    //

    // Standard fields.

    //

    WORD    Magic;

    BYTE    MajorLinkerVersion;

    BYTE    MinorLinkerVersion;

    DWORD   SizeOfCode;

    DWORD   SizeOfInitializedData;

    DWORD   SizeOfUninitializedData;

    DWORD   AddressOfEntryPoint;

    DWORD   BaseOfCode;

    DWORD   BaseOfData;

    //

    // NT additional fields.

    //

    DWORD   ImageBase;

    DWORD   SectionAlignment;

    DWORD   FileAlignment;

    WORD    MajorOperatingSystemVersion;

    WORD    MinorOperatingSystemVersion;

    WORD    MajorImageVersion;

    WORD    MinorImageVersion;

    WORD    MajorSubsystemVersion;

    WORD    MinorSubsystemVersion;

    DWORD   Win32VersionValue;

    DWORD   SizeOfImage;

    DWORD   SizeOfHeaders;

    DWORD   CheckSum;

    WORD    Subsystem;

    WORD    DllCharacteristics;

    DWORD   SizeOfStackReserve;

    DWORD   SizeOfStackCommit;

    DWORD   SizeOfHeapReserve;

    DWORD   SizeOfHeapCommit;

    DWORD   LoaderFlags;

    DWORD   NumberOfRvaAndSizes;

    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

一共是31个属性值,由此可见,这个结构的内容是多么的丰富!

1)  Magic:两个字节,是一个标志字,如果是ROM文件(0107h),可执行文件(010Bh),或者是PE32+文件(020Bh)。设置为“01 0B”, 在16进制编辑器里输入“0B 01”

2)  MajorLinkerVersion:一个字节,链接程序的主版本号。不影响程序的执行,设置为“00”, 在16进制编辑器里输入“00”

3)  MinorLinkerVersion:一个字节,链接程序的主版本号。不影响程序的执行,设置为“00”, 在16进制编辑器里输入“00”

4)  SizeOfCode:四个字节,可执行代码的长度,不影响程序的执行,设置为“00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

5)  SizeOfInitializedData:四个字节,初始化数据的大小,不影响程序的执行,设置为“00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

6)  SizeOfUnInitializedData:四个字节,未初始化数据的大小,不影响程序的执行,设置为“00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

7)  AddressOfEntryPoint:四个字节,程序执行入口的RVA。对于DLL文件来说,这个入口点是在进程初始化和关闭时以及进程创建/违灭时被调用。在DLL中这个域能被设置为0,前面提到的通知消息都不接收。我们知道一般的PE文件都有一个代码区块,里面存放的是所有的可执行代码,这里程序执行入口也就是代码区块的首地址RVA,本人设置为1000,这里不知道这个值怎么来没关系,在讲到区块表时就会了解。设置为“00 00 10 00”, 在16进制编辑器里输入“00 10 00 00”

8)  BaseOfCode:四个字节,代码段起始RVA。这个值在这里和AddressOfEntryPoint

一样,设置为“00 00 10 00”, 在16进制编辑器里输入“00 10 00 00”

9)  BaseOfData:四个字节,数据段的起始RVA。在区块表会有详细说明,现在设置为“00 00 20 00”, 在16进制编辑器里输入“00 20 00 00”

10)  ImageBase:四个字节,文件在内存中的首选装入地址。这个值对于DLL来说没有多大意义,因为DLL文件的载入地址是由系统决定的,设置为“00 40 00 00”, 在16进制编辑器里输入“00 00 40 00”

11)  SectionAlignment:四个字节,载入内存时区块的内存对齐大小,每个区块的载入地址必须是本数值的整数倍。默认值是CPU的页尺寸,也就是1000h(4KB), 设置为“00 00 10 00”, 在16进制编辑器里输入“00 10 00 00”

12)  FileAlignment:四个字节,磁盘上的PE文件的区块对齐大小,每个区块的原始数据必须是本字段的整数倍开始。对与X86来说,一般是200h1000h,这是为了保证块总是从磁盘的扇区开始。这个值最小是200h,我们选择设置为 “00 00 02 00”, 在16进制编辑器里输入“00 02 00 00”

13)  MajorOperatingSystemVersion:两个字节,要求操作系统的最低版本号的主版本号。设置为 “ 00 00”, 在16进制编辑器里输入“00 00”

14)  MinorOperatingSystemVersion:两个字节,要求操作系统的最低版本号的次版本号。设置为 “ 00 00”, 在16进制编辑器里输入“00 00”

15)  MajorImageVersion:两个字节,该可执行文件的主版本号,由程序员定义。设置为 “ 00 00”, 在16进制编辑器里输入“00 00”

16)  MinorImageVersion:两个字节,该可执行文件的次版本号,由程序员定义。设置为 “ 00 00”, 在16进制编辑器里输入“00 00”

17)  MajorSubSystemVersion:两个字节。要求最低子系统的主版本号,通常与下个字段一起使用。PE文件是专门为Win32系统设计的,所以该子系统版本号必须是4.0。设置为 “ 00 04”, 在16进制编辑器里输入“04 00”

18)  MinorSubSystemVersion:两个字节。要求最低子系统的次版本号,设置为 “ 00 00”, 在16进制编辑器里输入“00 00”

19)  Win32VersionValue:四个字节。保留值为0,设置为 “00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

20)  SizeOfImage:四个字节。映像装入内存后的总大小。DLL文件一共有5个区块,再加上DOS文件头和PE文件头,1000+1000*5=6000h,在讲到区块表时有详细论述。设置为 “00 00 60 00”, 在16进制编辑器里输入“00 60 00 00”

21)  SizeOfHeader: 四个字节。是DOS头,PE文件头和区块表的组合尺寸。现在我们来计算一下,DOS文件头是80hPE文件头大小是固定大小为4+20+224=248byte=F8h

5个区块表共5*40=200byte=C8h,所以一共是80+F8+C8=240h,但考虑值到必须与FileAlignment对齐,所以扩充为400h,设置为 “00 00 04 00”, 在16进制编辑器里输入“00 04 00 00”

22)  CheckSum:四个字节,映像的校验和。一般的EXE文件可以使用为0,但一些内核模式的驱动程序和DLL文件必须要有一个校验和。设置为 “00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

23)  SubSystem:两个字节,一个标明可执行文件所期望的子系统的枚举值。这个值对EXE来说是比较重要的,具体值见下表:

                

                                                  表3

      一般来说,选择值为2,设置为 “00 02”, 在16进制编辑器里输入“02 00”

24)  DllCharacteristics:两个字节,DllMain函数何时被调用,一般设置为0。设置为 “00 00”, 在16进制编辑器里输入“00 00”

25)  SizeOfStackReserve:四个字节,为线程保留的堆栈大小。设置为 “00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

26)  SizeOfStackCommit:四个字节,委派给堆栈的内存大小。设置为 “00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

27)  SizeOfHeapReserve:四个字节,为线程保留的堆大小。设置为 “00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

28)  SizeOfStackCommit:四个字节,委派给堆的内存大小。设置为 “00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

29)  LoaderFlags:四个字节,与调试相关,设置为 “00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

30)  NumberOfRvaAndSize:四个字节,数据目录表的数目。这个字段自从NT发布以来就是16。设置为 “00 00 00 10”, 在16进制编辑器里输入“10 00 00 00”

31)  DataDirectory[16]:数据目录表,有数个相同的IMAGE_DATA_DIRECTORY结构组成,指向输入表,输出表,重定位表等数据。IMAGE_DATA_DIRECTORY结构如下:

 

       typedef struct _IMAGE_DATA_DIRECTORY {

        DWORD   VirtualAddress;

        DWORD   Size;

} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

数据目录表的成员的结构如表4所示:

                                                                      表4

这些表里对我们的DLL文件有影响的就有输出表(Export Table,输入表( Input Table ,重定位表(Base relocation Table)这三个表。由于下面的区块表也涉及到现在这个问题,我们必须要考虑一下我们在DLL文件中要用到的所有表的在磁盘文件上的布局和在内存中的布局,要注意内存中的区块地址必须与SectionAlignment这个值对齐,磁盘中的区块地址必须与FileAlignment这个值对齐,另外考虑到计算的方便性,我们都假定每个区块在内存中占1000h的空间,于是得出表5,下表中的英文缩写与LordPe时查看区块信息时的英文缩写意义一致

                                               表5

    由于输入表,输出表,重定位表的大小还没知道,所以先不用填。

    其它表全部填充为“00 00 00 00 00 00 00 00”

现在的PE文件头部分的IMAGE_OPTINAL_HEADER部分的16进制源码如下:

000000A0                         0B 01 00 00 00 00 00 00           ........

000000B0   00 00 00 00 00 00 00 00  00 10 00 00 00 10 00 00   ................

000000C0   00 20 00 00 00 00 40 00  00 10 00 00 00 02 00 00   . ....@.........

000000D0   00 00 00 00 00 00 00 00  04 00 00 00 00 00 00 00   ................

000000E0   00 60 00 00 00 04 00 00  00 00 00 00 02 00 00 00   .`..............

000000F0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000100   00 00 00 00 10 00 00 00  00 40 00 00 00 10 00 00   .........@......

00000110   00 30 00 00 00 10 00 00  00 00 00 00 00 00 00 00   .0..............

00000120   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000130   00 50 00 00 00 10 00 00  00 00 00 00 00 00 00 00   .P..............

00000140   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000150   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000160   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000170   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000180   00 00 00 00 00 00 00 00                            ........

  在PE文件头和原始数据之间存在一个区块表,区块表包含每个块在映像中的信息,分别指向不同的区块实体。

区块表是一个IMAGE_SECTION_HEADERS的结构数组,每个IMAGE_SECTION_HEADER结构包含了它所关联了区块的信息,如位置,长度,属性;改数组的数目有IMAGE_NT_HEADERS.FileHeader.NumberOfSection指出。

IMAGE_SECTION_HEADER结构如下:

typedef struct _IMAGE_SECTION_HEADER {

    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];

    union {

            DWORD   PhysicalAddress;

            DWORD   VirtualSize;

    } Misc;

    DWORD   VirtualAddress;

    DWORD   SizeOfRawData;

    DWORD   PointerToRawData;

    DWORD   PointerToRelocations;

    DWORD   PointerToLinenumbers;

    WORD    NumberOfRelocations;

    WORD    NumberOfLinenumbers;

    DWORD   Characteristics;

} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

1)  Name8个字节,块名。块名是个8位的ASCⅡ码。

2)  VirtualSize:四个字节,在内存中区块所占的大小。对应于表5中的Vsize

3)  VirtualAddress:四个字节,该块装载到内存中的RVA。对应于表5中的VOffset

4)  SizeOfRawData:四个字节,该块在磁盘文件中所占的大小。对应于表5中的Rsize

5)  PointerToRawData:四个字节,该块在磁盘文件中的偏移。对应于表5中的ROffset

6)  PointerToReclocations:四个字节,在OBJ文件中,表示本块的重定位信息偏移值,但在DLL文件中也没多大用餐,因为有重定位表。设置为 “00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

7)  PointerToLinenumbers:四个字节,符合表在文件中的偏移值,在DLL里用不到,设置为 “00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

8)  NumberOfRelocations:两个字节,本快在重定位表中重定位的数目。在DLL里用不到,设置为 “00 00”, 在16进制编辑器里输入“00 00”

9)  NumberOfLinenumbers:两个字节,该块在行号表中的行号数目。设置为 “00 00”, 在16进制编辑器里输入“00 00”

10)  Characteristics:四个字节,块属性,各位的含义如下:

bit 5   IMAGE_SCN_CNT_CODE,置1,节内包含可执行代码。    

bit 6   IMAGE_SCN_CNT_INITIALIZED_DATA,置1,节内包含的数据在执行前是确定的。    

bit 7   IMAGE_SCN_CNT_UNINITIALIZED_DATA,置1,本节包含未初始化的数据,执行前即将被初始化为0。一般是BSS.

bit 9   IMAGE_SCN_LNK_INFO,置1,节内不包含映象数据除了注释,描述或者其他文档外,是一个目标文件的一部分,可能是针对链接器的信息。比如哪个库被需要。

bit 11   IMAGE_SCN_LNK_REMOVE,置1,在可执行文件链接后,作为文件一部分的数据被清除。

bit 12   IMAGE_SCN_LNK_COMDAT,置1,节包含公共块数据,是某个顺序的打包的函数。

bit 15   IMAGE_SCN_MEM_FARDATA,置1,保留。

bit 17   IMAGE_SCN_MEM_PURGEABLE,置1,节的数据是可清除的。

bit 18   IMAGE_SCN_MEM_LOCKED,置1,节不可以在内存内移动。

bit 19   IMAGE_SCN_MEM_PRELOAD,置1,节必须在执行开始前调入。

bits 20 to 23指定对齐。一般是库文件的对象对齐。

bit 24   IMAGE_SCN_LNK_NRELOC_OVFL,置1,节包含扩展的重定位。

bit 25   IMAGE_SCN_MEM_DISCARDABLE,置1,进程开始后节的数据不再需要。

bit 26   IMAGE_SCN_MEM_NOT_CACHED,置1,节的数据没有缓存。

bit 27   IMAGE_SCN_MEM_NOT_PAGED,置1,节的数据不得交换出去。

bit 28   IMAGE_SCN_MEM_SHARED,置1,节的数据在所有映象例程内共享,如DLL的初始化数据。

bit 29   IMAGE_SCN_MEM_EXECUTE,置1,该块可以执行。

bit 30   IMAGE_SCN_MEM_READ,置1,该块可读。

bit 31   IMAGE_SCN_MEM_WRITE1,该块可写。

       

    对于.text区块,节内包含可执行代码,bit 5bit 29都置1;对于可执行文件,bit 30总是置1。设置为 “60 00 00 20”, 在16进制编辑器里输入“20 00 00 60”

    对于.data区块,节内包含的数据在执行前是确定的,bit 61;因为是数据区,所以bit 30bit 31都置1。设置为 “C0 00 00 40”, 在16进制编辑器里输入“40 00 00 C0”

    对于.idata区块,和.data区块的设置一样。设置为 “C0 00 00 40”, 在16进制编辑器里输入“40 00 00 C0”

    对于.edata区块,节内包含的数据在执行前是确定的,bit 61;但输出表区块是不用写数据的,所以只有bit 301。设置为 “40 00 00 40”, 在16进制编辑器里输入“40 00 00 40”

      对于.reloc区块,和.edata区块设置一样。设置为 “40 00 00 40”, 在16进制编辑器里输入“40 00 00 40”

所以区块表的16进制源码如下:

00000180                         2E 74 65 78 74 00 00 00       .text...

00000190   00 10 00 00 00 10 00 00  00 02 00 00 00 06 00 00   ................

000001A0   00 00 00 00 00 00 00 00  00 00 00 00 20 00 00 60   ............ ..`

000001B0   2E 64 61 74 61 00 00 00  00 10 00 00 00 20 00 00   .data........ ..

000001C0   00 02 00 00 00 08 00 00  00 00 00 00 00 00 00 00   ................

000001D0   00 00 00 00 40 00 00 C0  2E 69 64 61 74 61 00 00   ....@..?idata..

000001E0   00 10 00 00 00 30 00 00  00 02 00 00 00 0A 00 00   .....0..........

000001F0   00 00 00 00 00 00 00 00  00 00 00 00 40 00 00 C0   ............@..?

00000200   2E 65 64 61 74 61 00 00  00 10 00 00 00 40 00 00   .edata.......@..

00000210   00 02 00 00 00 0C 00 00  00 00 00 00 00 00 00 00   ................

00000220   00 00 00 00 40 00 00 40  2E 72 65 6C 6F 63 00 00   ....@..@.reloc..

00000230   00 10 00 00 00 50 00 00  00 02 00 00 00 0E 00 00   .....P..........

00000240   00 00 00 00 00 00 00 00  00 00 00 00 40 00 00 40   ............@..@

    下面是开始编写区块部分,但手写区块的顺序是很讲究的,因为我们的目标是编写一个可以弹出对话框的函数,所以在写代码前必须要有对话框标题和内容等基本数据,而且还需要MessageBoxA的地址,因为是DLL文件,写完代码后还要处理输出表和重定位表。

       先来个最简单的,对话框标题是“hello”,内容是“hello,pediy!!!”,我们在磁盘文件偏移800处开始写入数据,完成后内容如下:

00000800   68 65 6C 6C 6F 00 00 00  00 00 00 00 00 00 00 00   hello...........

00000810   68 65 6C 6C 6F 2C 70 65  64 69 79 21 21 21 00 00   hello,pediy!!!..

     下面是输入表部分,也就是.idata区块。

     输入表是以IMAGE_IMPORT_DESCRIPTOR( IID )数组开始的,每个被调用的DLL文件都对应一个IID,做后一个全0数组结束。IMAGE_IMPORT_DESCRIPTOR的结构如下所示:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {

    union {

        DWORD   Characteristics;            // 0 for terminating null import descriptor

        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)

    };

    DWORD   TimeDateStamp;                  IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders

    DWORD   Name;

    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has 、、//actual addresses)

} IMAGE_IMPORT_DESCRIPTOR;

1)  OriginalFirstThunk:四个字节,包含指向输入名称表(INT)的RVAINT是一个IMAGE_THUNK_DATA结构的数组,数组的每个元素指向IMAGE_IMPORT_BY_NAME结构,数组最后以0结束。

2)  TimeDataStamp:四个字节,32位的时间标志,设置为 “00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

3)  ForwardChain:四个字节,第一个被转向的API索引。设置为 “00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

4)  Name:四个字节,DLL文件的名称。是个以00结束的ASCⅡ码的RVA

5)  FirstThunk:包含指向输入地址表( IAT )的RVAIAT是个IMAGE_THUNK_DATA结构的数组。

现在我们编写的DLL文件只是弹出一个对话框,所以只是需要用到一个函数MessageBoxA,USER32.dll动态链接库里。观察输入表区块如下:

00000A00   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000A10   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000A20   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000A30   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000A40   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000A50   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000A60   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000A70   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000A80   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000A90   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000AA0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000AB0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

必须要先构造一个IID 元素指向USER32.dll的信息,然后以一个全0IID 元素结束。这里就占用了2*4*5=40字节,就是占用了A00A27之间的空间。

由表5可知,.idata区块的虚拟偏移是3000h,物理偏移是A00h,所以两者之间的差值为3000-A00=2600h,这个数值以后算RVA时会经常用到。

现在先处理IIDName的数值,由于A00A27的空间应经被用了,所以我们在A30开始存放USER32.dll文件名的ASCⅡ码,以00结束。A30转化为RVA的方法是加上前面计算出来的2600h,所以Name的数值为2600+A30=3030h,在16进制编辑器里输入“30 30 00 00”

接下来在A40h存放INT,由于只有一个函数MessageBoxA,所以整个INT只有两项(以一个全0结束),在A50h存放MessageBoxAASCⅡ码,以00结束。

所以OriginalFirstThunk存放的是INTRVA,为2600+A40=3040h,在16进制编辑器里输入“40 30 00 00”A40h存放的是MessageBoxAASCⅡ码的RVA,为2600+A50=3050h,在16进制编辑器里输入“50 30 00 00”。 A50h存放的是MessageBoxAASCⅡ码,在16进制编辑器里输入“50 30 00 00”。但输入MessageBoxAASCⅡ码时就要注意,ASCⅡ字符串前有两个字节的空缺,是作为函数名的引用,可以为0,所以在16进制编辑器里输入“50 30 00 00”

 最后要处理的是输入地址表( IAT ),我们在A60处存放IAT,由于只有一个函数MessageBoxA,所以整个IAT只有两项(以一个全0结束),IAT的第一项也是指向MessageBoxAASCⅡ码的RVA,在A60处输入“50 30 00 00”。而FirstThunk字段填充A60RVA,2600+A60=3060h,在16进制编辑器里输入“60 30 00 00”

整个.idata区块完成后如下所示:

00000A00   40 30 00 00 00 00 00 00  00 00 00 00 30 30 00 00   @0..........00..

00000A10   60 30 00 00 00 00 00 00  00 00 00 00 00 00 00 00   `0..............

00000A20   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000A30   55 53 45 52 33 32 2E 64  6C 6C 00 00 00 00 00 00   USER32.dll......

00000A40   50 30 00 00 00 00 00 00  00 00 00 00 00 00 00 00   P0..............

00000A50   00 00 4D 65 73 73 61 67  65 42 6F 78 41 00 00 00   ..MessageBoxA...

00000A60   50 30 00 00 00 00 00 00  00 00 00 00 00 00 00 00   P0..............

00000A70   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000A80   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000A90   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

    现在我们来处理代码区块(.text)的编写。

  首先,DLL文件都有一个入口点,放在600处,内容如下:

    mov     eax, 1      B8 01000000

    retn     0C         C2 0C00    ;由于DLL入口函数压了3DWORD型参数大小为4*3=12=0Ch,所以retn  0c

   紧接着编写弹出对话框的函数,但在编写代码前先要弄清楚对话框的标题“hello”和内容“hello,pediy!!!”的内存地址。由于对话框的标题和内容数据都处于.data区块中,由表5可知,该区块在内存中与基址的偏移为2000h,与磁盘文件头的偏移为800h,两者的差值为2000-800=1800h

   现在计算对话框的标题“hello”的内存地址。“hello”在磁盘文件的800h处,加上1800800+1800=2000h,再加上ImageBase的值400000h,则标题“hello”的内存地址为400000+2000=402000h。内容“hello,pediy!!!”的内存地址如上,最后得到内存地址为401010h

   另外还需要MessageBoxA的函数地址,由输入表部分可知,文件偏移A60处存放的是MessageBoxA的函数地址,转化成内存地址为400000+((3000-A00+A60=403060h

    编写完成的代码如下:

55                 push   ebp                  

8BEC              mov    ebp, esp

6A 00              push    0

68 00204000        push    00402000             title “hello”

68 10204000        push    00402010             ;  text “hello,pediy!!!”

6A 00              push    0

FF15 60304000      call     dword ptr [403060]      ;调用MessageBoxA函数

8BE5              mov     esp, ebp

5D                pop      ebp

C3                retn

所以,代码区块(.text)的16源码如下:

00000600   B8 01 00 00 00 C2 0C 00  55 8B EC 6A 00 68 00 20   ?...?.Uj.h. 

00000610   40 00 68 10 20 40 00 6A  00 FF 15 60 30 40 00 8B   @.h. @.j..`0@.?

00000620   E5 5D C3 00 00 00 00 00  00 00 00 00 00 00 00 00   ?............

     由代码区块可知,代码中存在3个重定位数据,分别是title “hello”( 00402000h ), text “hello,pediy!!!”( 00402010 ), MessageBoxA函数地址( 00403060h )。这就需要重定为表的帮忙。重定位表是为与.reloc区块上,数据的组织方式是由许多重定位块串接而成。每个块是必须以4字节对齐,重定位块的结构如下:

typedef struct _IMAGE_BASE_RELOCATION {

    DWORD   VirtualAddress;

    DWORD   SizeOfBlock;

//  WORD    TypeOffset[1];

} IMAGE_BASE_RELOCATION;

1)  VirtualAddress:这组重定位数据的RVA地址。每个重定位数据加上这个值才是重定位项的RVA

2)  SizeOfBlack:四个字节,当前重定位结构的大小。用这个项减去8,就是TypeOffset的大小。

3)  TypeOffset:是一个数组。数组每项大小是两个字节,其中高4位是重定位的类型,低12位是重定位地址,它与VirtualAddress相加就是PE映像需要修改的地址数据的指针。

常见的重定位类型如下表6

                                                                           表6

title “hello”( 00402000h )在文件中的偏移为60Eh,转化成RVA60E+(1000-600)=100Eh,减去这组重定位数据开始的RVA地址为100E-1000=00Eh,于是得到TypeOffset12位地址00EhTypeOffset的高4位类型为3,所以TypeOffset的值为300Eh,在16进制编辑器里输入“0E 30”

text “hello,pediy!!!”( 00402010 )按title “hello”的方法计算,得到TypeOffset的值为3013h,在16进制编辑器里输入“13 30”

MessageBoxA函数地址( 00403060h )按title “hello”的方法计算,得到TypeOffset的值为301Bh,在16进制编辑器里输入“1B 30”

最后,我们得到VirtualAddress00001000h,在16进制编辑器里输入“00 10 00 00”

SizeOfBlock00000010h(有四个重定位项,其中一个是为了数据对齐,(10-8)/2=4h),在16进制编辑器里输入“10 0 00 00”

构造完成的重定位表区块16进制源码如下:

00000E00   00 10 00 00 10 00 00 00  0E 30 13 30 1B 30 00 00   .........0.0.0..

好,革命快要成功了,现在就差输出表区块(.edata)整个DLL文件就完成了。

输出表是个指向IMAGE_EXPORT_DIRECTORY( IED )结构,IED结构如下所示:

typedef struct _IMAGE_EXPORT_DIRECTORY {

    DWORD   Characteristics;

    DWORD   TimeDateStamp;

    WORD    MajorVersion;

    WORD    MinorVersion;

    DWORD   Name;

    DWORD   Base;

    DWORD   NumberOfFunctions;

    DWORD   NumberOfNames;

    DWORD   AddressOfFunctions;     // RVA from base of image

    DWORD   AddressOfNames;         // RVA from base of image

    DWORD   AddressOfNameOrdinals;  // RVA from base of image

} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

1)  Characteristics:四个字节,表示输出表的属性,保留。设置为 “00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

2)  TimeDateStamp:四个字节,表示输出表创建时间,设置为 “00 00 00 00”, 在16进制编辑器里输入“00 00 00 00”

3)  MajorVersion:两个字节,输出表的版本号,设置为 “00 00”, 在16进制编辑器里输入“00 00”

4)  MinorVersion:两个字节,输出表的次版本号,设置为 “00 00”, 在16进制编辑器里输入“00 00”

5)  Name:四个字节,指向一个ASCⅡ码的RVA。这个字符串是本输出函数所指向的DLL名。这个值后面才论述。

6)  Base:四个字节,包含这个文件输出表的起始序数值。一般来说,这个值为1。设置为 “00 00 00 01”, 在16进制编辑器里输入“01 00 00 00”

7)  NumberOfFunction:四个字节,输出地址表(EAT)中的条目数量。因为只有一个输出函数ShowMesBox,所以这个值为1,设置为 “00 00 00 01”, 在16进制编辑器里输入“01 00 00 00”

8)  NumberOfName:四个字节,输出名称表(ENT)的条目数量。因为只有一个输出函数ShowMesBox,所以这个值为1,设置为 “00 00 00 01”, 在16进制编辑器里输入“01 00 00 00”

9)  AddressOfFunction:四个字节,EATRVA。数组中的每一个非零的RVA都对应于一个被输出的符号。

10)  AddressOfName:四个字节,ENTRVAENT是一个指向ASCⅡ码字符串的RVA数组,每个ASCⅡ字符串对应于一个被输出的符号。

11)  AddressOfNameOrdinals:四个字节,输出序数表的RVA。这个表将ENT中的数组索引映射到相应的输出地址表条目。

现在我们来看,这个IMAGE_EXPORT_DIRECTORY结构共用了40个字节,也就是C00C27 

那么,我们在C30构造Name属性的所指向的DLL名字的ASCⅡ码的。“BinaryDll.dll”转化成的ASCⅡ码是“42696E617279446C6C2E646C6C”,在16进制编辑器里输入“42 69 6E 61 72 79 44 6C 6C 2E 64 6C 6C”

C30转化成RVA就是C30+(4000-C00)=4030h,所以把属性Name设置为“00 00 40 30”, 在16进制编辑器里输入“30 40 00 00”

C40构造EAT表,C40转化为RVAC40+(4000-C00)=4040h,所以把属性AddressOfFunction设置为“00 00 40 40”, 在16进制编辑器里输入“40 40 00 00”

C50构造ENT表,C50转化为RVAC50+(4000-C00)=4050h,所以把属性AddressOfName设置为“00 00 40 50”, 在16进制编辑器里输入“50 40 00 00”

C60构造输出序数表,C60转化为RVAC60+(4000-C00)=4060h,所以把属性AddressOfNameOrdinals设置为“00 00 40 60”, 在16进制编辑器里输入“60 40 00 00”

C40处输入输出函数” ShowMesBox”RVA,有代码区可得DLL的入口函数用了8个字节的空间,则ShowMesBox的地址为1000+8=1008h,在16进制编辑器里输入“08 10 00 00”

C70处输入输出函数” ShowMesBox”ASCⅡ码,在16进制编辑器里输入“53 68 6F 77 4D 65 73 42 6F 78”

C50处输入C70RVA,为C70+(4000-C00)=4070,在16进制编辑器里输入“70 40 00 00”

C60处构造输出序数表,由于只有一个输出函数,所以设置为“00 00”, 在16进制编辑器里输入“00 00”

完成后输出表16进制源码如下图所示:

00000C00   00 00 00 00 00 00 00 00  00 00 00 00 30 40 00 00   ............0@..

00000C10   01 00 00 00 01 00 00 00  01 00 00 00 40 40 00 00   ............@@..

00000C20   50 40 00 00 60 40 00 00  00 00 00 00 00 00 00 00   P@..`@..........

00000C30   42 69 6E 61 72 79 44 6C  6C 2E 64 6C 6C 00 00 00   BinaryDll.dll...

00000C40   08 10 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000C50   70 40 00 00 00 00 00 00  00 00 00 00 00 00 00 00   p@..............

00000C60   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000C70   53 68 6F 77 4D 65 73 42  6F 78 00 00 00 00 00 00   ShowMesBox......

 最后,我们来填写数据目录表的内容。

数据目录表中的输出表RVA4000,大小是80h,我们在16进制编辑器输入“00 40 00 00 80 00 00 00”

     数据目录表中的输入表RVA3000,大小是70h,我们在16进制编辑器输入“00 30 00 00 70 00 00 00”

     数据目录表中的重定位表RVA5000,大小是10h,我们在16进制编辑器输入“00 50 00 00 10 00 00 00”

最后写了个小测试程序,代码如下

#include <iostream>

#include <windows.h>

using namespace std;

int main()

{

  typedef void(*pShowMsg)(void); 

  HINSTANCE hDLL; 

  pShowMsg ShowMeg; 

  hDLL=LoadLibrary("BinaryDll.dll"); 

  ShowMeg=(pShowMsg)GetProcAddress(hDLL,"ShowMesBox"); 

  ShowMeg(); 

  ::FreeLibrary(hDLL); 

  return 0;

}

运行后出现下图:

 

 

文章附件下载地址

http://bbs.pediy.com/showthread.php?t=75210

发表于 @ 20090315日 17:50:00 | 评论( 0 ) | 编辑举报收藏 

旧一篇:软件DIY----用汇编语言给XP记事本添加自动保存功能 新一篇:浅谈快速阅读法

查看最新精华文章 请访问博客首页相关文章 发表评论 表 情:           评论内容:  用 户 名: 登录 注册 匿名评论 匿名用户验 证 码:   重新获得验证码     Copyright © newjueqi 

Powered by CSDN Blog   

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/newjueqi/archive/2009/03/15/3992408.aspx

### 回答1: 二进制文件是一种以二进制形式存储数据的文件,与文本文件不同,它们不包含任何格式化的文本,而是包含计算机可以直接理解的二进制数据。在Python中,我们可以使用内置的open()函数来读二进制文件。 要打开一个二进制文件,我们需要将文件模式设置为“rb”(读取二进制文件)或“wb”(入二进制文件)。例如,要打开一个名为“example.bin”的二进制文件进行读取,我们可以使用以下代码: ``` with open("example.bin", "rb") as f: data = f.read() ``` 这将打开“example.bin”文件并将其内容读取到变量“data”中。我们可以使用相同的方式来入二进制文件: ``` with open("example.bin", "wb") as f: f.write(data) ``` 这将把变量“data”中的内容入到“example.bin”文件中。请注意,入二进制文件时,我们需要确保我们入的数据是二进制数据,而不是字符串或其他格式的数据。 ### 回答2: Python中的文件操作是非常重要的,许多常见的编程任务都涉及文件的读。其中,二进制文件的读更是需要特别注意。 二进制文件是指保存在计算机上的文件,其内容以二进制形式存储。相对于文本文件,二进制文件通常更小,更快,并且可以直接访问文件中的任意位置。常见的二进制文件有图片、视频、音频等。 Python提供了许多函数来读取和入二进制文件。 开始讲解二进制文件的读取。打开二进制文件的过程与打开文本文件相同,只需要在打开文件时指定打开方式为“rb”(读取二进制文件)。读取文件内容时采用read()函数进行读取,它将读取整个文件的内容并将其作为一个字符串返回。read()函数还接受一个参数,表示要读取的最大字节数。如果不传入该参数,则读取整个文件。读取二进制文件的示例代码如下: ```python with open("binary_file", "rb") as f: contents = f.read() # 读取整个文件 print(contents) ``` 接下来是二进制文件入。与读取类似,打开文件时需要指定打开方式为“wb”(入二进制文件)。文件的内容可以使用write()函数进行入,它接受一个二进制字符串作为参数,并将其文件入二进制文件的示例代码如下: ```python with open("binary_file", "wb") as f: f.write(b"Hello, world!") # 入字符串 ``` 需要注意的是,入二进制文件时需要使用二进制字符串(即在字符串前面加b),否则Python会将字符串转换为ASCII编码并文件,这样就无法正确地入二进制数据了。 除了读取和入整个文件,我们还可以使用seek()函数在二进制文件中定位到指定位置,并从该位置开始读取或入数据。seek()函数接受两个参数,第一个参数表示要跳过的字节数,第二个参数表示跳跃的起点。如果第二个参数为0,则以文件起始位置为起点;如果为1,则以当前位置为起点;如果为2,则以文件末尾为起点。示例代码如下: ```python with open("binary_file", "rb") as f: f.seek(5) # 跳过前5个字节 contents = f.read(10) # 读取10个字节 print(contents) with open("binary_file", "wb") as f: f.seek(5, 0) # 跳到第5个字节 f.write(b"World") # 向文件入"World" ``` 通过上述介绍,我们知道了Python中读二进制文件的方法及注意事项,希望这篇文章对初学者有所帮助。 ### 回答3: Python中文件的读是程序中非常基础和常用的一项功能。文件可以分为文本文件和二进制文件。二进制文件和文本文件不同的是,二进制文件并不是由一些可读的字符组成,而是由计算机能够识别的比特流组成,因此在读二进制文件时需要使用二进制模式的文件操作。 在Python中,我们可以使用open()函数打开一个文件,而如果要打开二进制文件,需要在文件名的第二个参数中添加'b'。比如,我们可以使用以下代码读取一个二进制的图片文件: ```python with open('image.png', 'rb') as f: img_data = f.read() ``` 上述代码中,以二进制模式打开了一个名为image.png的文件,将文件中的内容读取到了一个变量img_data中。在读取完毕之后,使用with语句自动关闭了文件。 当我们要入二进制文件时,我们也需要使用二进制模式的文件操作。比如,我们可以使用以下代码将一个字节串入一个二进制文件中: ```python with open('binary.bin', 'wb') as f: f.write(b'\x00\x01\x02\x03') ``` 上述代码中,使用with语句打开了一个名为binary.bin的文件,并使用二进制模式打开,将一个字节串b'\x00\x01\x02\x03'入了文件中。在入完毕之后,使用with语句自动关闭了文件。 需要注意的是,二进制文件中包含的数据需要按照特定的格式进行解析才能正确读取。比如,在读取一个位图文件时,需要先读取文件头(header)中的各项属性,然后根据这些属性去解析文件中的像素数据。这就需要我们使用Python中的结构体(struct)模块,按照特定的格式读取二进制数据。例如: ```python import struct with open('bitmap.bmp', 'rb') as f: # 读取文件header = f.read(14) # 解析文件头 _, _, _, _, _, size, _, _, _, offset = struct.unpack('<2sIHHI', header) # 读取像素数据 f.seek(offset) data = f.read(size - offset) ``` 上述代码中,使用struct.unpack()解析了一个长度为14的文件头(header),然后根据header中的信息读取了像素数据。其中'<2sIHHI'用于表示不同属性的长度和类型,具体可以参考Python官方文档。 在处理二进制文件时还需要注意一些常见的问题,比如在读取或文件时,需要注意字节序(endian)的问题,需要进行正确的错误处理等。 总之,学习如何读二进制文件是Python编程中很重要的一环,可以帮助我们更好地处理计算机中的数据和文件
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值