首页
社区
课程
招聘
[原创]再写手工打造可执行程序
发表于: 2010-10-13 17:31 138694

[原创]再写手工打造可执行程序

2010-10-13 17:31
138694

三年前,我曾经写了一个手工打造可执行程序的文章,可是因为时间关系,我的那篇文章还是有很多模糊的地方,我一直惦记着什么时候再写一篇完美的,没想到一等就等了三年。因为各种原因直到三年后的今天我终于完成了它。现在把它分享给大家,希望大家批评指正。
    我们这里将不依赖任何编译器,仅仅使用一个十六进制编辑器逐个字节的手工编写一个可执行程序。以这种方式讲解PE结构,通过这个过程读者可以学习PE结构中的PE头、节表以及导入表相关方面的知识。
    为了简单而又令所有学习程序开发的人感到亲切,我们将完成一个Hello World! 程序。功能仅仅是运行后弹出一个消息框,消息框的内容是Hello World!。
首先了解一下Win32可执行程序的大体结构,就是通常所说的PE结构。
如图1所示PE结构示意图:


                             图1 标准PE结构图

    由图中可以看出PE结构分为几个部分:
   MS-DOS MZ 头部:所有PE文件必须以一个简单的DOS MZ 头开始。有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随MZ header 之后的DOS程序。以此达到对Dos系统的兼容。(通常情况DOS MZ header总共占用64byte)。
  MS-DOS 实模式残余程序:实际上是个有效的EXE,在不支持PE文件格式的操作系统中,它将简单显示一个错误提示,大多数情况下它是由汇编编译器自动生成。通常,它简单调用中断21h,服务9来显示字符串"This program cannot run in DOS mode"。(在我们写的程序中,他不是必须的,可以不予以实现,但是要保留其大小,大小为112byte,为了简洁,可以使用00来填充。)
  PE文件标志:是PE文件结构的起始标志。(长度4byte, Windows程序此值必须为0x50450000)
  PE文件头:是PE相关结构 IMAGE_NT_HEADERS 的简称,其中包含了许多PE装载器用到的重要域。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从DOS MZ header中找到PE header的起始偏移量,跳过了MS-DOS 实模式残余程序 ,直接定位到真正的文件头PE header,长度20byte。
PE文件可选头:虽然它的名字是“可选头部”,但是请确信:这个头部并非“可选”,而是“必需”的。(长度 224byte )。
  各段头部:又称节头部,一个Windows NT的应用程序典型地拥有9个预定义段(节),它们是“.text”、“.bss”、“.rdata”、“.data”、“.rsrc”、“.edata”、“.idata”、“.pdata”和“.debug”。一些应用程序不需要所有的这些段,同样还有些应用程序为了自己特殊的需要而定义了更多的段。(每个段头部占40byte,我们这里也不需要所有的段,仅需3个段。)
    通常我们是将PE整个结构分成四个部分,把MS-DOS MZ 头部和MS-DOS 实模式残余程序作为第一部分,可以称他为DOS部分,而PE文件标志、PE文件头、PE文件可选头三个部分作为第二部分,称之为PE头部分,因为这部分才是Windows下真正需要的部分,所以从PE文件标志开始才是真正的PE部分。各段头部是第三部分,称之为节表。它详细描述了PE文件中各个节的详细信息。最后就是各个节的实体部分了,称为节数据。
    以上仅仅是对PE结构各部分的大体讲解。接下来再手写这个Hello World!程序过程中,我将详细介绍每个部分的含义。
    首先准备一下工具,一个十六进制编辑器足以。我们这里使用VC++ 6.0所携带的十六进制编辑器,您也可以使用如WinHex等十六进制编辑工具。
    打开VC,选择文件,新建菜单项,然后选择一个二进制文件,单击确定。一切就绪了,下面就开始手写可执行程序,如图2所示:


                               图2 VC6.0下的十六进制编辑器

    首先来完成“DOS MZ header”部分。“DOS MZ header”的功能前面已经讲过,在这里不再重述,直接实现他。“DOS MZ header”总共64byte,他对应的结构是IMAGE_DOS_HEADER ,在WINNT.H文件中有定义。通过这个结构我们可以看到,这64字节被分成19个成员,每个成员都有特殊的含义,与其说我们是在逐字节的手写可执行程序,倒不如说我们是在逐个成员的写。因为单独的一个字节并不一定具有什么意义。我们在学习过程中,就是要按照官方的定义,将整个部分拆分成若干个成员,然后逐个成员的去学习。
(提示: 如果安装有VC开发环境,那么在其安装目录下有一个头文件WINNT.H,在这个头文件中定义了所有PE结构相关的各部分结构体。如图3所示:)  


             图3 VC安装目录下的WINNT.H头文件

使用VC开发环境打开此文件,然后按快捷键Ctrl+F输入IMAGE_DOS_HEADER进行搜索,如图4所示:

  
                               图4 VC下查找文件

单击Find Next按钮即可得到如下搜索结果,如图5所示:  
  
   
                              图5

可以看出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;
    按照它的定义,我们分别完成各个成员。
   第一个成员(e_magic)是个WORD类型,占2个字节,它被用于表示一个MS-DOS兼容的文件类型,他的值是固定的0x5A4D,所以在十六进制编辑器中输入“4D5A”。
(注意:
因为我们是在十六进制编辑器下写数据,所以所有的数据格式都是十六进制式的。但是我们在开发环境中通常在数据前添加“0x”用来表示十六进制数 ,即:0x5A4D。而在十六进制编辑器中,直接写成“4D5A”即可。后面内容都照此规定书写。有一点需要说明,为什么十六进制值0x5A4D输入到十六进制编辑器中是4D5A呢?这是因为一个内存值,无论是占两个字节的WORD类型,还是占四个字节的DWORD类型等,如同我们学习数学中的十进制数值一样,都是有高低位之分的,从右向左位越来越高。然而在十六进制编辑器中,十六进制位是自左向右依次增高。因此按照高低位对齐的原则,值0x5A4D中,低位0x4D应该应该放到左边,0x5A应该放到右边。也就得到了编辑器中的4D5A。)

     第2个成员到第18个成员总共58个字节,是对DOS程序环境的初始化等操作,对于我们这个程序来说,没什么影响,我们通通用“00”来填充。(如果您想对其进行详细了解,请查阅相关书籍。)
(提示:
我们在此不可能把PE结构所有的知识点都面面俱到,因为他十分的庞大。当然也没有必要对他作完全彻底的掌握,只需掌握关键的地方就可以了。以后我们都将把不影响程序执行的成员填充为零,这样做,一方面使程序看起来简洁,另一方面可以使您快速定位PE结构中要重点掌握的地方。)

    第19(e_lfanew)个成员非常重要,他是一个LONG类型,占4个字节,用来表示“PE文件标志”在文件中的偏移,单位是byte。而从图5-1中可以看到“PE文件标志”紧随“MS-DOS 实模式残余程序”其后。知道这一点,我们就可以计算一下,我们的“DOS MZ header”总共64 byte,后面的“MS-DOS 实模式残余程序”占112 byte, 64 + 112 = 176 byte。但是要注意,我们这里的176是十进制的,转化成十六进制是0xB0。因为是4个字节,其余三位字节应该以00补齐,所以最终的值为0x000000B0。所以在我们的十六进制编辑器中按照高低对齐的原则应该填写“B0000000”。
    接下来完成“MS-DOS 实模式残余程序”,笔者已经介绍,他是用在DOS下执行的,而我们所完成的HelloWorld程序是在win32下执行的。所以这里的内容并不影响我们程序的执行。因此这里直接用“00”来填充,注意总共112 byte。 这两部分完成之后代码如图6所示:  
  


              图6 完成PE结构中Dos部分的编写

    接下来便进入真正主题,开始写真正的PE结构部分:微软将“PE文件标志”,“PE文件头 ”,“PE文件可选头 ”这三个部分用一个结构来定义,即:IMAGE_NT_HEADERS32在WINNT.H中可以搜索其定义,定义如下:
  typedef struct _IMAGE_NT_HEADERS {
      DWORD Signature;
      IMAGE_FILE_HEADER FileHeader;
      IMAGE_OPTIONAL_HEADER32 OptionalHeader;
  } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
      可以看出这个结构含有3个成员:
    第一个成员(Signature)表示“PE文件标志”,是一个DWORD类型,占4个字节,它是PE开始的标记,对于Windows程序这个值必须为0x00004550,所以编辑器中填写“50450000”。
    第二个成员(FileHeader)表示“PE文件头 ”,他的类型是一个IMAGE_FILE_HEADER的结构。也就是说“PE文件头”的20个字节被定义为IMAGE_FILE_HEADER结构,定义如下:
  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;
这个结构具有7个成员:
        成员1(Machine),占2个字节,表示PE文件运行所要求的CPU。对于Intel平台,该值是0x014C,所以编辑器中应该填写“4C01”。
        成员2(NumberOfSections),占2个字节,表示PE文件中段(节)的总数,在我们这个程序中,计划完成3个段,(.text(代码段)、.rdata(只读数据段)、.data(全局变量数据段))。所以此处值是0x0003,因此填写“0300”。
        成员3(TimeDateStamp),占4个字节,表示文件创建日期和时间,从1970.1.1 00:00:00以来的秒数,我们这里填“0000”即可。
        成员4(PointerToSymbolTable),占4个字节,表示符号表的指针,主要用于调试,在这里填“0000”。
        成员5(NumberOfSymbols),占4个字节,表示符号的数目,主要用于调试,在这里填“0000”。
        成员6(SizeOfOptionalHeader),占2个字节,表示后面的“PE文件可选头 ”部分所占空间大小,我们已经知道“PE文件可选头 ”的大小是224 byte,转换成十六进制就是0xE0,此成员占两个字节,所以需要补齐一位00,即0x00E0。在编辑器中应该填写“E000”。
        成员7(Characteristics),占2个字节,表示关于文件信息的标记,比如文件是exe还是dll。这个值实际上是二进制位进行或运算得到的值。各二进制位表示的意义如下:
    Bit 0 :置1表示文件中没有重定向信息。每个段都有它们自己的重定向信息。这个标志在可执行文件中没有使用,在可执行文件中是用一个叫做基址重定向目录表来表示重定向信息的。
    Bit 1 :置1表示该文件是可执行文件。
    Bit 2 :置1表示没有行数信息;在可执行文件中没有使用。
    Bit 3 :置1表示没有局部符号信息;在可执行文件中没有使用。
    Bit 4 :未公开
    Bit 7 :未公开
    Bit 8 :表示希望机器为32位机。这个值永远为1。
    Bit 9 :表示没有调试信息,在可执行文件中没有使用。
    Bit 10:置1表示该程序不能运行于可移动介质中(如软驱或CD-ROM)。在这种情况下,OS必须把文件拷贝到交换文件中执行。
   Bit 11:置1表示程序不能在网上运行。在这种情况下,OS必须把文件拷贝到交换文件中执行。
   Bit 12:置1表示文件是一个系统文件例如驱动程序。在可执行文件中没有使用。
   Bit 13:置1表示文件是一个动态链接库(DLL)。
   Bit 14:表示文件被设计成不能运行于多处理器系统中。
   Bit 15:表示文件的字节顺序如果不是机器所期望的,那么在读出之前要进行交换。在可执行文件中它们是不可信的(操作系统期望按正确的字节顺序执行程序)。
对于我们的程序,因为它是可执行程序,所以Bit 1必须置为1,其他位按照需要置位即可。在我们的程序中只需将第二位置1表示是可执行程序。也就得到二进制值“0000000000000010”,将其转换为十六进制形式为0x02,而该成员占两个字节,补齐一位00由此得到成员7的值为0x0002。因此在编辑器中填写“0200”。如果是dll,那么得到的二进制值应该是“0010000000000000”,转换成十六进制为0x2000。如果填写编辑器中应该填写“0020”。
     第三个成员(OptionalHeader),表示“PE文件可选头”,他的类型是一个IMAGE_OPTIONAL_HEADER32结构。也就是说PE文件头的224个字节被定义为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),占2个字节,表示文件的格式,值为0x010B表示.EXE文件,为0x0107表示ROM映像,因为我们写的是一个可执行程序,所以此处应该填写“0B01”。
        成员2(MajorLinkerVersion),占1个字节,表示链接器的主版本号,此值不会影响程序的执行,我们这里填充零,此值为“00”。
        成员3(MinorLinkerVersion),占1个字节,表示链接器的幅版本号,此值不会影响程序的执行,我们这里填充零,此值为“00”。
        成员4(SizeOfCode),占4个字节,表示可执行代码的长度,此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
        成员5(SizeOfInitializedData),占4个字节,表示初始化数据的长度(数据段)。此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
        成员6(SizeOfUninitializedData),占4个字节,表示未初始化数据的长度(bss段)。此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
(说明:
在介绍成员7之前,有必要了解一个很重要的知识------文件映射到内存。在可执行程序运行之前,PE加载器将把PE文件加载到进程空间的内存中去,并且初始化每个段实体。那么加载到内存中的哪个地址去呢?这将由IMAGE_OPTIONAL_HEADER32结构的成员10的值指出加载的起始地址(又叫基地址)。这个值通常是“00400000”, 那么PE文件的首地址“00000”就被映射到内存地址“00400000”处,那么相对于文件偏移10个字节的地址为“00010”,被映射到内存后的偏移也应该是10个字节,映射后的地址应该为“00400010”。PE加载器就是按照此种方法将文件映射到内存中的。)

        成员7(AddressOfEntryPoint),4个字节,表示代码入口的RVA地址。
(说明:
RVA是指PE加载器将文件映射到内存后,某个物理地址距离加载基址的偏移地址。)

    所谓代码的入口是指程序从这儿开始执行。成员7实际上是PE装载器准备运行的PE文件中的第一条指令的RVA值。若您要改变整个程序执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先被执行。
知道成员7的含义后,我们又如何来填充它呢?如何得知我们的程序将使用哪个地址作为入口呢?前面已经提到,一般在PE文件中总会有个.text段,这个段通常是用来填写代码的。按照一般规律,我们也将实现这么一个段,将我们这个程序中的所有代码指令写到此段中。我们在完成此程序的代码时,是从.text段起始地址开始写起。所以.text段的起始地址就将是我们程序的入口地址。那么又出现另外一个问题,如何得到.text段的起始地址呢?在PE结构中,所有段都对应有一个段头部,而在段头部中将指定该段的起始地址。那么这个值要等待我们完成.text头部后才能够得到,所以此处首先用“aaaaaaaa”填写,待完成.text段头部后再计算填写它。
        成员8(BaseOfCode),4个字节,表示可执行代码起始位置。当然就是.text段的首地址,此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
        成员9(BaseOfData),4个字节,表示初始化数据的起始位置,此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
        成员10(ImageBase),4个字节,就是上面所讲的文件映射到内存后的基地址。PE文件的优先装载地址。通常为0x00400000。因为PE装载器默认情况下优先将尝试把文件装到虚拟地址空间的0x00400000处。字眼“优先”表示若该地址区域已被其他模块占用,那PE装载器会选用其他空闲地址。我们这里的值设为“00004000”。
        成员11(SectionAlignment),4个字节,表示段加载后在内存中的对齐方式,即内存中节对齐的粒度。例如,如果该值是4096 (1000h),那么每节的起始地址必须是4096的倍数。若第一节从401000h开始,大小是10个字节,下一个节并不是从401011开始,因为要经过节对齐,那么下一节必定从402000h开始,即使401000h和402000h之间还有很多空间没被使用。因为Windows管理内存采用分页管理的方式,而每页的大小为4k,也就是1000h。一般情况下程序的内存节对齐粒度都为0x00001000,我们这个值也填充为“00100000”。
        成员12(FileAlignment),4个字节,表示段在文件中的对齐方式。文件中节对齐的粒度。例如,如果该值是(200h),,那么每节的起始地址必须是512(十六进制为200h)的倍数。若第一节从文件偏移量200h开始且大小是10个字节,则下一节必定位于偏移量400h处。即使偏移量512和1024之间还有很多空间没被使用。一般情况下程序的文件节对齐粒度都为200h,所以我们在此将此值设为“00020000”。
        成员13(MajorOperatingSystemVersion),2个字节,表示操作系统主版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
        成员14(MinorOperatingSystemVersion),2个字节,表示操作系统副版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
        成员15(MajorImageVersion),2个字节,表示程序主版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
        成员16(MinorImageVersion),2个字节,表示程序副版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
        成员17(MajorSubsystemVersion),2个字节,表示子系统主版本号。win32子系统版本。PE文件是专门为Win32设计的,该子系统版本必定是4.0,那么此处值为“04”。
        成员18(MinorSubsystemVersion),2个字节,表示子系统副版本号,根据上面所说,此值应为“00”。
        成员19(Win32VersionValue),2个字节,此值一般为“00”。
        成员20(SizeOfImage),4个字节,表示程序载入内存后占用内存的大小(单位字节),即等于所有段的长度之和---------所有头和节经过节对齐处理后的大小。我们知道,我们文件PE结构总长小于1000h,但是内存中的对齐粒度是1000h,所以PE结构被映射后要占1000h,尽管很多空间没有使用,另外我们有3个段,每个段的长度小于1000h,但是被映射后同样要占1000h,所以总共占用内存的大小为1000h + 3 * 1000h = 4000h,因此此值为“00400000”。
        成员21(SizeOfHeaders),4个字节,表示所有文件头的长度之和(从文件开始到第一个段之间的大小)。所有头即PE头加所有节表头的大小,也就等于文件尺寸减去文件中所有节的尺寸。可以以此值作为PE文件第一节的文件偏移量。那么我们怎么得到这个值呢?我们的PE文件头总大小为:64 + 112 + 4 + 20 + 224 = 424,3个节表头的总大小 3 * 40 =120。424 + 120 = 544 byte 转化成十六进制为220h,那么此值就填写220h吗?不是的,因为我们文件中的对齐粒度是200h,那么220h经过文件对齐后实际上要占用400h的空间,所以此值为“00040000”。
        成员22(CheckSum),4个字节,表示校验和。它仅用在驱动程序中,在可执行文件中可能为0。它的计算方法Microsoft没有公开,在imagehelp.dll中的CheckSumMappedFile()函数可以计算它,此处我们设为填充零,此值为“00000000”。
        成员23(Subsystem),2个字节,表示NT子系统,可能是以下的值:
      IMAGE_SUBSYSTEM_NATIVE (1) 不需要子系统。用在驱动程序中。
      IMAGE_SUBSYSTEM_WINDOWS_GUI(2) WIN32 graphical程序(它可用AllocConsole()来打开一个控制台,但是不能在一开始自动得到)。
      IMAGE_SUBSYSTEM_WINDOWS_CUI(3) WIN32 console程序(它可以一开始自动建立)。
      IMAGE_SUBSYSTEM_OS2_CUI(5) OS/2 console程序(因为程序是OS/2格式,所以它很少用在PE)。
      IMAGE_SUBSYSTEM_POSIX_CUI(7) POSIX console程序。
Windows程序总是用WIN32子系统,所以只有2和3是合法的值。也就是说此值必须为2或3,如果是3,那么程序运行后会自动打开一个控制台,我们为了看一下效果,这里设为3,此值为“0300”。
        成员24(DllCharacteristics),2个字节,表示Dll属性,我们这里填充零,此值为“0000”。
        成员25(SizeOfStackReserve),4个字节,保留堆栈大小,我们这里填充零,此值为“00000000”。
        成员26(SizeOfStackCommit),4个字节,启动后实际申请的堆栈数,可随实际情况变大,我们这里填充零,此值为“00000000”。
        成员27(SizeOfHeapReserve),4个字节,保留堆大小,我们这里填充零,此值为“00000000”。
        成员28(SizeOfHeapCommit),4个字节,实际堆大小,我们这里填充零,此值为“00000000”。
        成员29(LoaderFlags),4个字节,装载标志,我们这里填充零,此值为“00000000”。
        成员30(NumberOfRvaAndSizes),4个字节,在讲这个成员之前,我们应该先了解        
        成员31,成员31实际上是一个IMAGE_DATA_DIRECTORY结构的数组,成员30的值就是表示该数组的大小。通常有16个元素,也就是十六进制的0x00000010,所以此值填为:“10000000”。 IMAGE_DATA_DIRECTORY结构定义如下:
typedef struct _IMAGE_DATA_DIRECTORY {
      DWORD   VirtualAddress;
      DWORD   Size;
  } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
成员31(DataDirectory),128个字节,上面说过他是一个IMAGE_DATA_DIRECTORY结构的数组,通常具有16个元素。
  IMAGE_DATA_DIRECTORY结构有两个成员,各占4个字节,也就得到成员31的总大小:2 * 4 * 16 = 128byte。16个元素中每个元素代表一个目录表,每个目录表表示的目录如下:
  IMAGE_DIRECTORY_ENTRY_EXPORT (0)         导出目录,用于DLL   
  IMAGE_DIRECTORY_ENTRY_IMPORT (1)         导入目录   
  IMAGE_DIRECTORY_ENTRY_RESOURCE (2)      资源目录   
  IMAGE_DIRECTORY_ENTRY_EXCEPTION (3)     异常目录   
  IMAGE_DIRECTORY_ENTRY_SECURITY (4)      安全目录   
  IMAGE_DIRECTORY_ENTRY_BASERELOC (5)    重定位表   
  IMAGE_DIRECTORY_ENTRY_DEBUG (6)         调试目录
  IMAGE_DIRECTORY_ENTRY_COPYRIGHT (7)      描述版权串   
  IMAGE_DIRECTORY_ENTRY_GLOBALPTR (8)      机器值   
  IMAGE_DIRECTORY_ENTRY_TLS (9)             本地线程存储目录
  IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG (10)  载入配置目录   
  IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (11) 绑定导入表目录   
  IMAGE_DIRECTORY_ENTRY_IAT (12)              输入地址表目录
  IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT(13) 延迟加载导入描述目录
  IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR(14) COM 运行时描述目录
  是不是所有的目录表都要关心呢?其实要把这些目录表都研究清楚是个很大的课题,对于我们这个程序,只需关心第2个元素,导入目录,它标识了我们的程序从其他模块导入的函数信息。因为我们要显示一个消息框,所以要导入user32.dll库中的MessageBoxA函数。程序要正常退出,又要导入kernel32.dll库中的ExitProcess函数。因此需要构造这个目录表。然而上面已说明每个目录是一个IMAGE_DATA_DIRECTORY结构,该结构具有两个成员,第一个成员表示目录表的起始RVA地址,第二个成员表示目录表的长度。我们将把这个目录表构造到.rdata段中,所以暂时先不填写。但是要留出空位来,为了记住该位置,我们先都填写为a,即:“aaaaaaaa”,“aaaaaaaa”。注意因为要文件对齐,所以其余的统统添零直到地址1a7h处。此时完成的代码如图7所示:


                    图7 完成PE结构中的PE头部分

    接下来完成各段头部,又称为节表。一个程序中用到的所有代码、资源、全局数据等信息分布在各个节中,而各个节的信息,如节加载位置,节大小,节属性等信息都由紧跟PE头之后的节表所指出。它实际上就是紧挨着 PE 头的一个结构数组,该数组成员的数目由 file header (IMAGE_FILE_HEADER) 结构中 NumberOfSections 域的域值来决定。节表结构又命名为 IMAGE_SECTION_HEADER。
我们这里有3个段,.text(代码段), .rdata(只读数据段),data(全局变量数据段)。每段是一个IMAGE_SECTION_HEADER 结构,具有10个成员。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;
首先我们来完成.text段。
        成员1(Name),8个字节,表示该节的名称,我们这里该节的名字为.text,那么此值应该是他的ASKII码值应该为“2E74657874000000”。
        成员2(VirtualSize),4个字节,表示该节数据映射到内存后所占字节数。在这里是指有效代码所占的字节数。稍后我们将把程序的执行代码指令写入到文件中,总共有多少字节的指令需要那时计算,我们也可以提前将代码准备好并计算出长度将其填写。笔者已经计算完毕,将写入26h个字节的代码。所以此时仍然将此值设为“26000000”。我们也可以填写26h经过内存对齐后的值,即“00100000”。
        成员3(VirtualAddress),4个字节,表示在.text段映射到内存中的起始地址,那么这个值如何得来呢?我们知道.text是紧跟PE结构后的,然后整个PE头结构映射到内存后占的大小为1000h(因为PE头本身结构小于1000h个字节,而经过内存对齐后便为1000h),那么此值便得到了,为0x00001000,因此此处填写“00100000”。这个时候我们已经可以完成前面遗留的一个问题,再填写IMAGE_OPTIONAL_HEADER32结构的第七个成员的时候,它实际上是程序的入口地址,当时已经讲解此入口地址实际就是.text段的起始地址,所以可以将此处的“aaaaaaaa”更改为“00100000”。
(提示
程序的入口地址并不一定就代码段.text的起始位置。读者只需知道IMAGE_OPTIONAL_HEADER32结构的第7个成员所表示的含义是程序的入口地址。它通常是由编译器生成的。因为我们没有使用编译器,而是要手工打造一个可执行程序,所以所有的成员值都要自己来安排。只要按照PE结构的要求,安排合理即可。因此我们可以把代码的起始地址随意安排什么地方,只要安排的那个地址刚好又是我们保存的程序执行代码的入口即可。我们为了方便将其放在.text段的起始地址处。)

        成员4(SizeOfRawData),4个字节,表示.text段在文件中所占的大小。因为我们的实际代码只有26h个字节,那么这个值可以填写“26000000”,也可以填写此值经过文件对齐后的值即200h,所以也可以填写此值为“00020000”。
        成员5(PointerToRawData),4个字节,表示.text段在文件中的起始地址,上面已经计算过PE文件的总长度为400h,他实际上也就是.text的起始偏移地址,此值为“00040000”。
        成员6(PointerToRelocations),7(PointerToLinenumbers),8(NumberOfRelocations),9(NumberOfLinenumbers),均占4个字节,都仅用于目标文件,我们这里用零来填充。
成员10(Characteristics),4个字节。包含标记以指示节属性,比如节是否含有可执行代码、初始化数据、未初始数据,是否可写、可读等。这个值实际上是二进制位进行或运算得到的值。各二进制位表示的意义如下:
  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_WRITE)置1,进程得到“写入”访问节内存。
  在我们这里,因为这是代码段,所以bit 5 (IMAGE_SCN_CNT_CODE)位要置1,一般代码段都含有初始化数据,那么bit 6 (IMAGE_SCN_CNT_INITIALIZED_DATA)位要置1,又因为代码段的代码可以执行的,所以bit 29 (IMAGE_SCN_MEM_EXECUTE) 位要置1,那么这3个二进制位进行或运算最终得到的二进制值为
“00100000000000000000000001100000”,将其转换为十六进制值为0x20000060,所以此处应该填写“60000020”。
到此整个.text头编写完毕,按照上面的方法,分别填写.rdata段和.data段。因为要文件对齐,所以后面的代码用零补齐,直到3ffh。
PE加载器根据节表加载程序的过程是这样的:读取 IMAGE_FILE_HEADER 的 NumberOfSections域,得到文件中节的数目。 读取IMAGE_OPTIONAL_HEADER32的SizeOfHeaders 域值,将其作为节表的文件偏移,并以此定位节表。 遍历整个结构数组检查各成员值。 对于每个结构,读取PointerToRawData域值并定位到该文件偏移量。然后再读取SizeOfRawData域值来决定映射内存的字节数。将VirtualAddress域值加上ImageBase域值等于节起始的虚拟地址。然后把节映射进内存,并根据Characteristics域值设置属性。 遍历整个数组,直至所有节都已处理完毕。
(提示
感染型病毒经常通过增加节来达到感染正常文件的目的。因为感染正常文件需要添加病毒代码,病毒最常用的方法是新建一个节,然后将病毒代码放置在新建的节中,然后修改程序入口地址使其指向病毒代码。这样程序运行以后首先运行的是病毒代码,等病毒代码运行完毕才会跳转到被感染程序的原始入口地址处执行。)

  最后的编写结果如图8所示:      

     
                               图8 完成各个节表

至此,我们已经完成了PE头结构的编写。为了让我们写的程序可以运行,我们还要完成.text(代码段), .rdata(只读数据段),data(全局变量数据段)三个段的实体部分。
首先编写.text段,他紧接着PE结构后面。前面已经说过,.text段中存放所有可执行的指令代码(机器码)。我们可以通过先编写汇编指令(调用MessageBoxA和ExitProcess两个函数),然后反汇编出机器代码抄到这里就可以了。我们的程序功能是弹出一个消息框,这需要用到MessageBoxA函数,当用户单击确定以后程序要退出,这又需要用到ExitProcess函数。这两个函数调用的汇编代码如下:
push    0          ; MessageBoxA的第四个参数,即消息框的风格,这里传入0。                     
push    ????   ;第三个参数,消息框的标题字符串所在的地址,需要计算。                  
push    ????   ;第二个参数,消息框的内容字符串所在的地址,需要计算。                     
push    0          ;第一个参数,消息框所属窗口句柄,这里填0。                     
call    ???? ;调用MessageBoxA,实际是跳转到该函数的跳转指令所在地址        
push    0       ;ExitProcess函数的参数,程序退出码,传入0.                     
call    ???? ;调用ExitProcess,实际是跳转到该函数的跳转指令所在地址
jmp    ???? ;跳转到MessageBoxA的真正地址处。
jmp    ???? ;跳转到ExitProcess的真正地址处。
首先计算MessageBoxA两个字符串参数的地址,实际上是两个字符串,“消息框”和“HelloWorld !”。这两个串需要保存到文件中,我们设计将其存放在.data(全局变量数据段)。这个段位于.rdata段之后,那么可以计算它的起始内存地址,PE头1000h,.text段只有26h字节,内存对齐后为1000h,.data(只读数据段)是准备用来完成导入表的段,它肯定也不会超过1000h,所以对齐后应为1000h,因此紧随其后的.rdata的起始内存地址应该在偏移为:
1000h+1000h+1000h=3000h处,程序的基址为400000h,故此得到.rdata的绝对内存地址为:
0x00400000+0x00003000=0x00403000。我们将“消息框”字符串放于此处,该字符串占7个字符,那么紧随其后的“Hello World !”字符串的地址应该为0x00403000+7=0x00403007。
所以修正以上汇编代码为:
push    0          ; MessageBoxA的第四个参数,即消息框的风格,这里传入0。                     
push    0x403000   ;第三个参数,消息框的标题字符串所在的地址。                  
push    0x403007   ;第二个参数,消息框的内容字符串所在的地址。                     
push    0          ;第一个参数,消息框所属窗口句柄,这里填0。                     
call    ???? ;调用MessageBoxA,实际是跳转到该函数的跳转指令所在地址。     
push    0       ;ExitProcess函数的参数,程序退出码,传入0.                        
call    ???? ;调用ExitProcess,实际是跳转到该函数的跳转指令所在地址。
jmp    ???? ;跳转到MessageBoxA的真正地址处。
jmp    ???? ;跳转到ExitProcess的真正地址处。
其次需要计算MessageBoxA和ExitProcess两个函数所在地址,这个需要完成.rdata段的导入表才可以得到。所以首先用200h个00将.text段填充,待完成.rdata段后再返过来完成它。
接下来完成.rdata段,这个段非常重要,也非常繁琐。因为我们要手工打造导入表。通常导入表是由编译器生成的,其生成规则遵循IMAGE_IMPORT_DESCRIPTOR结构。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;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
前面曾介绍这个程序我们只用完成数据目录数组的第二个元素-------导入表目录,然而此目录值我们当时没有填写,当时添充的是“aaaaaaaa”作为标记,现在我们要一并解决这个问题。前面已经说过,每个数据目录具有两个成员,第一个成员表示目录表的起始RVA地址,第二个成员表示目录表的长度。对于我们这个导入表目录来说,他指的就是导入表了,导入表实际上是一个IMAGE_IMPORT_DESCRIPTOR 结构数组,每个结构包含PE文件从某一个DLL库引入函数的相关信息。例如,我们这个程序将从2个DLL库中导入函数,那么这个数组就有2个成员,同时该数组以一个全零的成员结尾。每一个IMAGE_IMPORT_DESCRIPTOR结构具有5个成员,都是DOWRD类型,因此每个IMAGE_IMPORT_DESCRIPTOR结构的大小为4*5=20byte。因此整个导入表的大小应该为(2+1)*20=60byte。转换成十六进制也就是0x3C。这样也就得到了导入表的大小,现在可以将导入表目录的第二个成员修改过来,将“aaaaaaaa”替换为0x0000003c,也就是在编辑器中输入“3c000000”。接下来看导入目录的第一个成员如何计算,它是导入表的起始地址的RVA值,我们计划将导入表放到.rdata段,并且自.rdata段起始地址处开始。那么导入表的起始地址也就是.rdata段的起始地址。.rdata紧随.text段之后,那么它的起始地址偏移应该为PE头的大小1000h+.text的大小1000h即2000h。现在可以将导入表目录的第一个成员修改过来,将“aaaaaaaa”替换为“00200000”。
之后的工作就是手工打造一个导入表。导入表在文件中的位置也同样是.rdata段的起始地址处,文件地址应该为PE头的400h+.text段的200h即600h处。我们打造导入表同样也要遵循IMAGE_IMPORT_DESCRIPTOR结构,该结构有5个成员:
成员1,4个字节,他实际上是指向一个 IMAGE_THUNK_DATA 结构数组的RVA,而IMAGE_THUNK_DATA 结构数组记录所有从某个.dll库中导入的所有函数名称的RVA。实际上它是指从某个DLL文件中导入的所有函数名称所在地址的地址表,该地址表由一个全零DWORD值0x00000000结束。因此我们需要构造这样一个表。首先需要把导入的所有函数名称依次保存到某个位置,然后计算其RVA去构造函数名称地址表。
为了紧凑,我们可以将每个函数名和动态库名字符串放在导入表之后。前面已经计算得到导入表的长度为0x3c,那么字符串的位置应该保存到文件中的地址为:600h(导入表的起始文件偏移地址)+3ch(导入表长度)= 63ch处。在这里准备填写所有字符串的AscII码。首先输入“MessageBoxA”字符串,这里有一点要注意,一个PE程序在导入函数的时候可以按照函数名来导入,也就是我们准备采取的方式,也可以按照函数序号导入。函数序号在各个动态库的导出表中可以查询到。该序号是一个WORD类型的值。无论是否我们以序号方式导入都要保留其位置,也就是在“MessageBoxA”字符串前应该预留一个WORD值的位置。因为我们并不是按照函数序号的方式导入,所以这里可以填写任意值,我们就填写0x0000。然后紧接其后写入“MessageBoxA”字符串,注意字符串要以一个字节的0x00结尾。如果还导入了其他函数,那么依次输入那些被导入的函数名AscII值即可。最后输入导入库名的AscII值,即“user32.dll”字符串。这样我们完成了一个导入库的名称表,紧随其后以相同方式完成另一个导入库,即“ExitProcess”字符串和“kernel32.dll”字符串。在导入表后输入的内容如下:
“00004D657373616765426F7841007573657233322E646C6C0080004578697450726F63657373006B65726E656C33322E646C6C00”。
如图9所示:
     
  
               图9 导入表用到的字符串

完成函数名称表后就可以构造函数名称地址表。紧随名称表之后,函数名称表起始地址为文件偏移63ch处,长度是34h。所以其后的函数名称地址表的起始应该为:63ch+34h==670h。函数名称地址表中保存了从某一个DLL库中导入的所有函数名称所在内存地址的RVA值,并且以一个0x00000000作结束。导入了几个DLL库,那么就有几个函数名称地址表。我们这里总共导入了两个DLL库,那么就有两个函数名称地址表。我们分别完成它。首先是user32.dll库中导入了MessageBoxA,因为函数名称表已经构造完毕,所以我们可以得到它的文件偏移,是63ch,怎样由文件偏移计算得到RVA值呢?这取决于内存对齐粒度和文件对齐粒度。PE加载器将PE文件加载入内存是按照内存对其粒度进行加载的。让们看看从文件首到0x063C处内容如何加载入内存。首先PE头经过文件对齐占400h,而此部分内容经过内存对齐加载入内存后占1000h。然后是.text节经文件对齐占200h,而此节内容经过内存对齐加载如内存后占1000h,也就是说文件偏移600h对应内存RVA是2000h,因此文件地址0x63C对应的RVA应该是0x203C。所以函数名称地址表中填写0x0000203C,即“3C200000”,user32.dll库中只导入了一个函数,所以后面填写一个全零的DWORD值0x00000000,即“00000000”表示结束。接着完成kernel32.dll库的导入函数地址表。由函数名称表中可以得到被导入的ExitProcess的文件偏移为0x655。它对应的RVA值为0x2055,那么紧随前一个函数名称地址表填写“55200000”,由于也只导入了一个函数,所以后面填写全零的DWORD值表示此表的结束。这样完成了整个导入表所需的两个库函数名称地址表,如图10所示:


                图10导入名称地址表

由此可知user32.dll库函数名称地址表的起始文件偏移为0x670,对应的RVA值为0x2070。而kernel32.dll库函数的名称地址表的起始文件偏移为0x678,对应的RVA值为0x2078。此时可以完成关于user32.dll库的导入表的第一个成员,就是指由user32.dll库导入的函数名称地址表起始地址的RVA值,应该是0x2070,因为此成员占4个字节,所以编辑器中应该填写“70200000”。
成员2,成员3各4各字节,用处不大,我们用零填充。
成员4,4个字节,是指向DLL名字的RVA。由user32.dll导入函数名称表可以得知此DLL名称所在地址的文件偏移为0x64A,对应的RVA值应该为0x204A,所以此成员应该填写0x0000204A,即“4A200000”。
成员5,4个字节,指向一个 IMAGE_THUNK_DATA 结构数组的RVA,同成员1一样。但是此IMAGE_THUNK_DATA 数组结构含义确和成员1完全不同。它将保存所有导入函数的真实调用地址。换句话说实际上它也指向一个地址表,这个地址不再像成员1一样是函数名称地址表,而是函数真实调用地址表,这个表又称为导入地址表,简称为IAT。既然这个表存放的是函数调用的真实地址,在设计PE文件时还没有得到导入函数的调用地址,所以无法填写此表。该表由PE文件被装载到内存时,PE加载器获得导入函数的真实地址来填充这个表。同样这个表以一个全零的DWORD作为结束标志。同样为了紧凑,我们将函数调用地址表安排在函数名称地址表之后,函数名称地址表的起始文件偏移是0x670,总共0x10个字节,那么其后的函数调用地址表的起始文件偏移为0x670+0x10=0x680。转换为RVA应该为0x2080,所以成员5应该填写“80200000”。
(注意:
虽然函数调用地址最终由PE加载器来填充,我们只需指定该表的位置。但是由于该表以全零的DWORD值作为结束标记。所以如果我们开始也填充全零将使PE加载器填充失败,所以我们需要随便填入一个非零值,这里笔者填入0x00000011。之后再填写结束标记0x00000000。紧随其后是第二导入库的函数调用地址表,导入了几个函数就需要填写几个非零的DWORD值,然后填写结束标记0x00000000。最终完成的导入函数调用地址表如图11所示:)


                 图 11 IAT

至此,完成了导入表中的关于导入库user32.dll的部分,按照相同的方法继续完成关于导入库kernel32.dll部分。最终导入表如图12所示:


                     图 12 导入表

.rdata的其余部分用00填充,直到文件偏移0x800处。
最后是.data段,这个段非常简单,就是MessageBoxA所需的参数,消息框的标题和内容:即“消息框”、“Hello World !”两个字符串的AscII值。其余部分用00填充,直到0xA00处。如图13所示:


                     图 13 导入表

最后我们继续完成.text段的程序执行代码。代码如下:
push    0          ; MessageBoxA的第四个参数,即消息框的风格,这里传入0。                     
push    0x403000   ;第三个参数,消息框的标题字符串所在的地址。                  
push    0x403007   ;第二个参数,消息框的内容字符串所在的地址。                     
push    0          ;第一个参数,消息框所属窗口句柄,这里填0。                     
call    ???? ;调用MessageBoxA,实际是跳转到该函数的跳转指令所在地址。     
push    0       ;ExitProcess函数的参数,程序退出码,传入0.                        
call    ???? ;调用ExitProcess,实际是跳转到该函数的跳转指令所在地址。
jmp    ???? ;跳转到MessageBoxA的真正地址处。
jmp    ???? ;跳转到ExitProcess的真正地址处。
看上面的两个jmp跳转,它是跳转到函数的真正地址处,然而函数真正地址是由PE加载得到并填充的,我们如何知道呢?通过函数导入表的构造,我们指定了各个导入函数真正地址所要填充的位置,PE加载器将把函数调用的真实地址填充到此处,那么我们只需这样来设计:jmp dword ptr [被填充地址],也就是跳转到被填充地址所指向的内容处即可。通过导入表可以轻松得到MessageBoxA的填充地址为0x2080,这是RVA值。得到绝对地址还需要加上基址。即:0x400000+0x2080=0x402080。同理,ExitProcess函数的填充地址为0x402088。
更新后的执行代码如下:
push    0          ; MessageBoxA的第四个参数,即消息框的风格,这里传入0。                     
push    0x403000   ;第三个参数,消息框的标题字符串所在的地址。                  
push    0x403007   ;第二个参数,消息框的内容字符串所在的地址。                     
push    0          ;第一个参数,消息框所属窗口句柄,这里填0。                     
call    ???? ;调用MessageBoxA,实际是跳转到该函数的跳转指令所在地址。     
push    0       ;ExitProcess函数的参数,程序退出码,传入0.                        
call    ???? ;调用ExitProcess,实际是跳转到该函数的跳转指令所在地址。
jmp   dword ptr [0x402080] ;跳转到MessageBoxA的真正地址处。
jmp   dword ptr [0x402088] ;跳转到ExitProcess的真正地址处。
还剩两个call的地址没有确定,她们表示该函数的跳转指令所在地址。也就是指令 jmp   dword ptr [0x402080] 和 jmp   dword ptr [0x402088]两条指令所在的地址。如何得到他们呢?因为执行代码起始地址为.text段的起始地址,偏移为0x1000。而后面的各条指令长度分别为:
push    0 ;指令长度为2。
push    0x403000  ;指令长度为5。           
push    0x403007   ;指令长度为5。   
push    0          ;指令长度为2。
call    ???? ;指令长度为5。  
push    0       ;指令长度为2。                        
call    ???? ;指令长度为5。
总长度为2+5+5+2+5+2+5=26。转换为十六进制为1A,那么紧随其后的两条jmp指令的地址偏移应该为0x101A和0x1020。加上基址后得到其绝对地址分别为0x401041和0x401020.。更新后的执行代码如下:
push    0          ; MessageBoxA的第四个参数,即消息框的风格,这里传入0。                     
push    0x403000   ;第三个参数,消息框的标题字符串所在的地址。                  
push    0x403007   ;第二个参数,消息框的内容字符串所在的地址。                     
push    0          ;第一个参数,消息框所属窗口句柄,这里填0。                     
call    40101A ;调用MessageBoxA,实际是跳转到该函数的跳转指令所在地址。     
push    0       ;ExitProcess函数的参数,程序退出码,传入0.                        
call    401020 ;调用ExitProcess,实际是跳转到该函数的跳转指令所在地址。
jmp   dword ptr [0x402080] ;跳转到MessageBoxA的真正地址处。
jmp   dword ptr [0x402088] ;跳转到ExitProcess的真正地址处。
将这些指令翻译成机器码后如下:
6A00680030400068073040006A00E8070000006A00E806000000FF2580204000FF2588204000
最后将其填入.text段,其余部分用00填充,直到600h处。如图14所示:


                     图 14导入表

到此为止一个完整的显示Hello World!的可执行程序就完成了,按Ctrl+S键将编写完毕的内容保存成文件,如图15所示:


                     图 15保存文件

我们将其保存到桌面即可,并且命名为HelloWorld.exe。双击运行该程序,运行结果如图16所示:


                     图 16保存文件

程序成功运行。
这样,没有依赖任何编译器,按照PE结构的原理,纯手工成功打造了一个Win32可执行程序。通过这个过程,我们学习了PE头的结构和节表的结构,掌握了导入表结构。


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

上传的附件:
收藏
免费 9
支持
分享
最新回复 (103)
雪    币: 90
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
楼主的景仰有如涛涛江水,连绵不绝;有如黄河泛滥,一发不可收拾
2010-10-13 18:53
0
雪    币: 217
活跃值: (25)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
3
膜拜楼主,这两天也是在学习相关的知识,收藏
2010-10-13 18:54
0
雪    币: 216
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
楼主太强了。纯手工活啊!!!!!
2010-10-13 20:57
0
雪    币: 307
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
牛,膜拜下
2010-10-13 21:18
0
雪    币: 237
活跃值: (31)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
确实不错,好帖
2010-10-13 21:39
0
雪    币: 333
活跃值: (45)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wan
7
好文,非一般功力可以做啊
2010-10-14 00:02
0
雪    币: 90
活跃值: (91)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
真的很感动 很激动

潜力贴留名.Opera插图补丁.字数补丁..
2010-10-14 00:06
0
雪    币: 204
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
很好,很经典
2010-10-14 02:35
0
雪    币: 19
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
讲的很详细,顶下。。
2010-10-14 09:29
0
雪    币: 1149
活跃值: (888)
能力值: ( LV13,RANK:260 )
在线值:
发帖
回帖
粉丝
11
看了以后 我知道什么叫0 ,1了
2010-10-16 18:43
0
雪    币: 208
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
mark! 慢慢学习
2010-10-17 14:40
0
雪    币: 203
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
特感谢..学习了不少.
2010-10-18 09:11
0
雪    币: 324
活跃值: (26)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
14
三年以前楼主的帖子我现在还保存了一份的,当时给的震撼太强烈了,没有想到居然更新了
呵呵,对于分析PE结构相当的有帮助啊!
2010-10-18 11:07
0
雪    币: 768
活跃值: (530)
能力值: ( LV13,RANK:460 )
在线值:
发帖
回帖
粉丝
15
佩服楼主的耐心与毅力~
支持
2010-10-18 11:37
0
雪    币: 277
活跃值: (37)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
16
LZ就是那个传说中用电话机还原win98的神人
2010-10-18 13:41
0
雪    币: 0
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
       牛人啊!!!!!!
2010-10-18 16:32
0
雪    币: 1259
活跃值: (38)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
stu
18
不错啊.6个字.
2010-10-19 12:09
0
雪    币: 211
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
顶!!
2010-10-19 12:13
0
雪    币: 275
活跃值: (51)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
20
竟然可以这样~晕~楼主对pe 的熟悉程度实在是无话可说啊,download之,研究之
2010-10-19 13:30
0
雪    币: 10
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
不错,好文章,学习
2010-10-19 13:46
0
雪    币: 169
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
22
佩服的不行了。传说有技术的高手都长的不咋地。这是对我最大的安慰。哈哈
2010-10-19 15:36
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
23
一步步讲解,很细致,谢谢LZ
2010-10-22 01:16
0
雪    币: 429
活跃值: (542)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
分析的很详细,谢谢楼主
2010-10-22 12:26
0
雪    币: 31
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25
文章很好,支持一下!
2010-10-22 16:52
0
游客
登录 | 注册 方可回帖
返回
//