【转载文章-看雪学院】非常经典
http://www.pediy.com/kssd/index.html
其实还是很多细节的地方没弄懂,保存下来,认真分析分析。
【文章标题】: 手写可执行程序
【文章作者】: dncwbc
【作者邮箱】: [email protected]
【作者QQ号】: 182445917
【软件名称】: Hello World!
【软件大小】: 2.5K
【下载地址】: 自己搜索下载
【编写语言】: 机器码
【使用工具】: VC++ 6.0
【操作平台】: Winxp
【详细过程】
最近,学习PE结构的知识。之后深有感触,随即便萌发了不依赖任何开发环境和编译器,纯手工写一个小程序的念
头。为了简单而又令所有学习程序开发的人感到亲切,就写一个Hello World! 程序吧…
在这里,我们首先复习一下Win32可执行程序的大体结构,就是通常所说的PE结构。
PE 的意思就是Portable Executable(可移植的执行体)。
PE结构如下图:
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
MS-DOS
MZ 头部 --------------> 64 byte
 ̄  ̄  ̄  ̄  ̄  ̄
MS-DOS
实模式残余程序 --------------> 112 byte
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
PE文件标志 --------------> 4 byte
 ̄  ̄  ̄  ̄  ̄  ̄
PE文件头 --------------> 20 byte
 ̄  ̄  ̄  ̄  ̄  ̄
PE文件可选头 --------------> 224 byte
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
各段头部 --------------> n * 40 byte
 ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
DOS MZ header:所有PE文件(甚至32位的DLLs) 必须以一个简单的DOS MZ header 开始。有了它,一旦程序在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结构各部分的大体讲解,先热热身而已。接下来在手写这个Hello World!程序中,我会详细介绍
每个字节的含义。
首先准备一下工具(肯定有人要问了,不是纯手写吗?怎么还要准备工具啊?),毕竟一个十六进制编辑器是少
不了的,否则在哪写啊?我在这里就使用VC++ 6.0所携带的十六进制编辑器。好了,工具暂时就这个了,开工了。
打开VC,选择文件,新建菜单项,然后选择一个二进制文件,确定。一切就绪了,下面就开始我们的手写可执行
程序旅程吧……
首先我们来完成“DOS MZ header”,“DOS MZ header”的功能前面已经讲过,在这里就不多说了,我们直接来
看一下如何实现他。“DOS MZ header”总共64byte,他对应的结构是IMAGE_DOS_HEADER ,在WINNT.H文件中有定义。通过
这个结构我们可以看到,这64字节被分成19个成员,每个成员都有他的功能,与其说我们在一个字节一个字节的手写可执行
程序,倒不如说我们是在一个成员一个成员的写。因为单独的一个字节并不一定具有什么意义。我们在学习过程中,就是要
把整个部分拆分成几个成员,然后一个成员一个成员的去学习。(实际上并不是我们去拆分,人家已经帮我们拆分好了。我
们只需按照标准的结构去学习就可以了。所有的结构都定义在WINNT.H文件中。)
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;
第一个成员占2个字节,它被用于表示一个MS-DOS兼容的文件类型,他的值是固定的----“4D5A”
(注意:因为我们是在十六进制编辑器下写数据,所以所有的数据格式都是十六进制式的。但是我们在开发环境中默认
都是十进制的,所以必须在数据前加 0x ,即:0x4D5A。而我为了方便,就直接写成“4D5A”,也就是直接输入到编辑器
中的值,是十六进制,后面的都照此规定书写。)
第2个成员到第18个成员总共58个字节,是对DOS程序环境的初始化等操作,对于我们这个程序来说,没什么影响,
我们通通用“00”来填充。(如果读者想对其进行详细了解,请查阅相关书籍。)注意:因为我们不可能把PE结构所
有的东西都面面俱到,他十分的庞大。当然也没有必要都去记他,只需掌握关键的地方就可以了。以后我们都将把不
影响程序执行的成员填充为零,这样做,一方面使程序看起来简洁,另一方面可以使您快速定位PE结构中要重点掌握
的地方。
第19个成员非常重要,他占4个字节,用来表示“PE文件标志”在文件中的偏移,单位是byte。而从上图中可以看到
“PE文件标志”紧随“MS-DOS 实模式残余程序”其后。知道这一点,我们就可以计算一下了,我们的“DOS MZ header”总
共64 byte,后面的“MS-DOS 实模式残余程序”占112 byte, 64 + 112 = 176 byte,但是要注意,我们这里的176可是十进制
的,转化成十六进制是B0,对了,就是这个值,因为是4个字节,所以我们应该填“B0000000”。
接下来我们来完成“MS-DOS 实模式残余程序”,我们已经知道,他是用在DOS下执
行的,我们这里可以直接用“00”来填充,注意总共112 byte。 这两部分完成之后代码如下:
00000 4D 5A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 MZ..............
00010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00030 00 00 00 00 00 00 00 00 00 00 00 00 B0 00 00 00 ............?...
00040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
接下来我们便进入主题,开始写真正的PE结构部分:微软将“PE文件标志”,“PE文件头 ”,“PE文件可选头 ”
这三个部分用一个结构来定义,即:IMAGE_NT_HEADERS32(WINNT.H中有定义,后面象这样的结构均在WINNT.H中有定义),
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
这个结构含有3个成员:
第一个成员表示“PE文件标志”,可以看到他是一个DWORD类型,因此占4个字节,它是PE开始的标记,对Windows程
序这个值必须为“50450000”。
第二个成员表示“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,占2个字节,表示该文件运行所要求的CPU。对于Intel平台,该值是“4C01”。
成员2,占2个字节,表示该文件中段的总数,我们这里计划写3个段,(.text(代码段)、.rdata(只读数据段)、
.data(全局变量数据段))。所以此处值是“0300”。
成员3,占4个字节,表示文件创建日期和时间,从1970.1.1 00:00:00以来的秒数,我们这里填“0000”即可。
成员4,占4个字节,表示符号表的指针,主要用于调试,在这里填“0000”。
成员5,占4个字节,表示符号的数目,主要用于调试,在这里填“0000”。
成员6,占2个字节,表示后面的“PE文件可选头 ”部分所占空间大小,我们已经知道“PE文件可选头 ”的大小是
224 byte,转换成十六进制就是E0,所以这里的值为“E000”
成员7,占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,其他的按照需要置位即可,这里我们仅将第二位置位,
由此得到成员7的值为“0200”。
第三个成员,表示“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,占2个字节,表示文件的格式,值为0x010B表示.EXE文件,为0x0107表示ROM映像,因为我们写的是一个可执
行程序,所以此值应该为“0B01”。
成员2,占1个字节,表示链接器的主版本号,此值不会影响程序的执行,我们这里填充零,此值为“00”。
成员3,占1个字节,表示链接器的幅版本号,此值不会影响程序的执行,我们这里填充零,此值为“00”。
成员4,占4个字节,表示可执行代码的长度,此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
成员5,占4个字节,表示初始化数据的长度(数据段)。此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
成员6,占4个字节,表示未初始化数据的长度(bss段)。此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
(在介绍成员7之前,有必要了解一个很重要的知识------文件映射到内存。在可执行程序运行之前,PE加载器将
把PE文件加载到进程空间的内存中去,并且初始化每个段实体。那么加载到内存中的哪个地址去呢?这将由
IMAGE_OPTIONAL_HEADER32结构的成员10的值指出加载的起始地址(又叫基地址)。这个值通常是“00400000”, 那么PE
文件的首地址“00000”就被映射到内存地址“00400000”处,那么相对于文件偏移10个字节的地址为“00010”,被映射
到内存后的偏移也应该是10个字节,映射后的地址应该为“00400010”。)
成员7,4个字节,表示代码的入口RVA(文件映射到内存的偏移地址)地址,程序从这儿开始执行。PE装载器准备
运行的PE文件的第一个指令的RVA。若您要改变整个执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先被
执行。那么这个值我们怎么得到呢?我们知道在文件中有个.text段,他包含了所有的代码,我们可以从中找到我们的入口
地址,在这里就是.text段里的第一行代码,也就是.text段的首地址,而在.text段头部就给出了他映射到内存后的首地
址的偏移,我们找到他取出添到此处,这里为“00100000”。(此处不理解没关系,我们讲完段结构后自能迎刃而解。)
成员8,4个字节,表示可执行代码起始位置。当然就是.text段的首地址,此值不会影响程序的执行,我们这里
填充零,此值为“00000000”。
成员9,4个字节,表示初始化数据的起始位置,此值不会影响程序的执行,我们这里填充零,此值为“00000000”。
成员10,4个字节,就是上面所讲的文件映射到内存是的基地址。PE文件的优先装载地址。通常设为“00400000”,
PE装载器将尝试把文件装到虚拟地址空间的00400000h处。字眼”优先”表示若该地址区域已被其他模块占用,那PE装载器会
选用其他空闲地址。我们这里的值设为“00400000”。
成员11,4个字节,表示段加载后在内存中的对齐方式。内存中节对齐的粒度。例如,如果该值是4096 (1000h),
那么每节的起始地址必须是4096的倍数。若第一节从401000h开始且大小是10个字节,则下一节必定从402000h开始,
即使401000h和402000h之间还有很多空间没被使用。因为Windows管理内存采用分页管理的方式,而每页的大小为4k,也
就是1000h,所以我们这个值为“00100000”。
成员12,4个字节,表示段在文件中的对齐方式。文件中节对齐的粒度。例如,如果该值是(200h),,那么每节的起
始地址必须是512的倍数。若第一节从文件偏移量200h开始且大小是10个字节,则下一节必定位于偏移量400h: 即使偏移
量512和1024之间还有很多空间没被使用。此值最好设为200h,所以该成员的值为“00020000”。
成员13,2个字节,表示操作系统主版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
成员14,2个字节,表示操作系统副版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
成员15,2个字节,表示程序主版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
成员16,2个字节,表示程序副版本号,此值不会影响程序的执行,我们这里填充零,此值为“0000”。
成员17,2个字节,表示子系统主版本号。win32子系统版本。PE文件是专门为Win32设计的,该子系统版本必定是
4.0那么此处值为“04”。
成员18,2个字节,表示子系统副版本号,根据上面所说,此值应为“00”。
成员19,2个字节,此值一般为“00”。
成员20,4个字节,表示程序调入后占用内存大小(字节),等于所有段的长度之和。所有头和节经过节对齐处理后
的大小。我们知道,我们文件PE结构总长小于1000h,但是内存中的对齐粒度是1000h,所以PE结构被映射后要占1000h,尽
管很多空间没有使用,另外我们有3个段,每个段的长度小于1000h,但是被映射后同样要占1000h,所以总共占用内存的大
小为1000h + 3 * 1000h = 4000h,因此此值为“00400000”。
成员21,4个字节,表示所有文件头的长度之和(从文件开始到第一个段之间的大小)。所有头+节表的大小,也就等
于文件尺寸减去文件中所有节的尺寸。可以以此值作为PE文件第一节的文件偏移量。那么我们怎么得到这个值呢?我们的
PE文件结构总大小为:64 + 112 + 4 + 20 + 224 + 3 * 40 = 544 byte 转化成十六进制为220h,那么此值就是220h吗?
不是的,因为我们文件中的对齐粒度是200h,那么220h实际上要占用400h的空间,所以此值为“00040000”。
成员22,4个字节,表示校验和。它仅用在驱动程序中,在可执行文件中可能为0。它的计算方法Microsoft不公开,
在imagehelp.dll中的CheckSumMappedFile()函数可以计算它,此处我们设为填充零,此值为“00000000”。
成员23,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,2个字节,表示Dll状态,我们这里填充零,此值为“0000”。
成员25,4个字节,保留堆栈大小,我们这里填充零,此值为“00000000”。
成员26,4个字节,启动后实际申请的堆栈数,可随实际情况变大,我们这里填充零,此值为“00000000”。
成员27,4个字节,保留堆大小,我们这里填充零,此值为“00000000”。
成员28,4个字节,实际堆大小,我们这里填充零,此值为“00000000”。
成员29,4个字节,装载标志,我们这里填充零,此值为“00000000”。
成员30,4个字节,在讲这个成员之前,我们应该先了解成员31,成员31实际上是一个IMAGE_DATA_DIRECTORY结构
的数组,成员30的值就是表示该数组的大小。通常有16个元素,所以此值为:“10000000”。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
成员31,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)Thread local storage目录
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG (10)Load configuration 目录
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (11)Bound import directory目录
IMAGE_DIRECTORY_ENTRY_IAT (12)Import Address Table输入地址表目录
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
是不是所有的目录表都要关心呢?其实要把这些目录表都研究清楚是个很大的课题,对于我们这个程序,只需关心第2个
元素,导入目录,它标识了我们的程序从其他模块导入的函数信息。因为我们要显示一个消息框,所以要导入user32.dll
库中的MessageBoxA函数,程序退出,又要导入kernel32.dll库中的ExitProcess函数,这个目录表需要使用。然而上面已
说明每个目录是一个IMAGE_DATA_DIRECTORY结构,该结构具有两个成员,第一个成员表示目录表的起始RVA地址,第二个
成员表示目录表的长度。这两个值要根据.rdata段实体来确定,暂时先不填写。为了记录该位置,我们先都填写为x,即:
“xxxxxxxx”,”xxxxxxxx”。其余的统统添零即可。
接下来是各段头部,我们这里有3个段,.text(代码段), .rdata(只读数据段),data(全局变量数据段)。每段是一
个IMAGE_SECTION_HEADER 结构,具有10个成员。首先我们来看.text段。
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,8个字节,表识该段的名称,我们这里是.text,那么此值是他的ASKII码应该为“2E74657874000000”。
成员2,4个字节,表示有效代码所占的字节数。我们这里所有代码数一下总共26h个,固此值为“26000000”。
成员3,4个字节,表示在.text段映射到内存中的起始地址,那么这个值如何得来呢?我们知道.text是紧跟PE结构后
的,然后整个PE结构映射到内存后占的大小为1000h(因为PE结构小于1000h个字节,而对齐力度粒度是1000h),那么此值
便得到了,为“00100000”。
成员4,4个字节,表示.text段在文件中所占的大小。因为我们的实际代码只有26h个字节,那么这个值是不是26h呢?
并不是,一定要注意段在文件中的对齐粒度是200h,所以此值为“00020000”。
成员5,4个字节,表示.text段在文件中的起始地址,上面已经计算过PE文件的总长度为400h,他实际上也就是.text
的起始偏移地址,此值为“00040000”。
成员6,7,8,9,均占4个字节,都仅用于目标文件,我们这里统统填为零。
成员10,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个二进制位进行或运算最终得到此成员值“20000060”。
这个整个.text头就编写完毕,按照上面的方法,分别在编写.rdata段和.data段。因为要对齐,所以后面的代码用零补齐。
最后的编写结果如下:
000B0 50 45 00 00 4C 01 03 00 00 00 00 00 00 00 00 00 PE..L.........
000C0 00 00 00 00 E0 00 02 00 0B 01 00 00 00 00 00 00 ....?.......
000D0 00 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00 ...............
000E0 00 00 00 00 00 00 40 00 00 10 00 00 00 02 00 00 ......@.......
000F0 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 ...............
00100 00 40 00 00 00 04 00 00 00 00 00 00 03 00 00 00 .@............
00110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00120 00 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 ...............
00130 10 20 00 00 3C 00 00 00 00 00 00 00 00 00 00 00 ..<...........
00140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00180 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00190 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
001A0 00 00 00 00 00 00 00 00 2E 74 65 78 74 00 00 00 .........text...
001B0 26 00 00 00 00 10 00 00 00 02 00 00 00 04 00 00 &............
001C0 00 00 00 00 00 00 00 00 00 00 00 00 20 00 00 60 ............ ..`
001D0 2E 72 64 61 74 61 00 00 92 00 00 00 00 20 00 00 .rdata..?... ..
001E0 00 02 00 00 00 06 00 00 00 00 00 00 00 00 00 00 ..............
001F0 00 00 00 00 40 00 00 40 2E 64 61 74 61 00 00 00 ....@[email protected]...
00200 16 00 00 00 00 30 00 00 00 02 00 00 00 08 00 00 ....0........
00210 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 C0 ............@..
00220 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00230 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00240 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00250 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00260 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00270 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00280 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00290 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
002A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
002B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
002C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
002D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
002E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
002F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00300 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00310 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00320 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00330 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00340 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00350 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00360 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00370 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00380 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00390 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
003A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
003B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
003C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
003D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
003E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
003F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
至此,我们已经完成了PE结构的编写。是不是已经大功告成了呢?别着急,为了让我们写的程序可以运行,我们还要
完成.text(代码段), .rdata(只读数据段),data(全局变量数据段)三个段的实体部分。
首先编写.text段,他紧接着PE结构后面,但是我们如何编写这些内容呢?前面已经说过,.text段中存放所有的可执行
代码(机器码),我们可以通过先编写汇编指令(调用MessageBoxA和ExitProcess两个函数),然后反汇编出机器代码抄
到这里就可以了。这里有一点要注意,我们在为MessageBoxA函数传递参数时,如何将“Hello World!”字符串以及消息
框的标题“消息框”字符串传过去呢?这就要用到我们的.data(全局变量数据段)了,我们可以把这两个字符串放到这个段
中,然后把字符串的偏移首地址作为参数传给MessageBoxA即可。因为要以200h对齐,所以剩余部分用零补齐,最终得到
的代码如下:
00400 6A 00 68 00 30 40 00 68 07 30 40 00 6A 00 E8 07 [email protected]@.j.?
00410 00 00 00 6A 00 E8 06 00 00 00 FF 25 08 20 40 00 ...j.?...% @.
00420 FF 25 00 20 40 00 00 00 00 00 00 00 00 00 00 00 %. @...........
00430 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00440 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00450 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00460 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00470 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00480 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00490 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
004A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
004B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
004C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
004D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
004E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
004F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00500 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00510 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00520 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00530 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00540 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00550 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00560 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00570 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00580 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00590 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
005A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
005B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
005C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
005D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
005E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
005F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
接下来完成.rdata段,这个段非常重要,也有些繁琐。因为这个程序我们只用完成数据目录数组的第二个元素导入表
目录,这个值我们当时没有填写,暂时添的x作为标记,现在我们要一并解决这个问题。前面已经说过,每个数据目录具有
两个成员,第一个成员表示目录表的起始RVA地址,第二个成员表示目录表的长度。对于我们这个导入表目录来说,他指的
就是导入表了,这个导入表实际上是一个IMAGE_IMPORT_DESCRIPTOR 结构数组,每个结构包含PE文件引入函数的一个相关
DLL的信息。比如,如果该PE文件从10个不同的DLL中引入函数,那么这个数组就有10个成员。该数组以一个全0的成员结尾。
那么.rdata段的内容应该就是IMAGE_IMPORT_DESCRIPTOR 结构数组了,我们这里用到user32.dll和kennel32.dll两个库,
那么这个结构数组的元素个数就是2了。第一个元素的成员1的地址是不是就是.rdata的首地址呢?然后依次完成其余成员?
不是这样的,这里有个规律,我们导入了多少个函数,那么就要空出8乘以导入函数个数个字符的空间(为什么要空些空间
呢?要放什么呢?一会便可知晓),在其后才是IMAGE_IMPORT_DESCRIPTOR结构成员的首地址。在这里我们导入了2个函数,
那么应该空2 * 8 = 16个字符,.rdata段的首地址是600h,那么IMAGE_IMPORT_DESCRIPTOR结构成员的首地址应该是610h。
这时我们便得到了数据目录数组的第二个元素导入表目录结构成员1的值,是不是就是“10060000”呢?不是的,因为这里
的地址是RVA(映射后内存的地址),而文件的600h被映射为2000h,所以610h被映射后的RVA为2010h,故此值为
“10200000”,我们可以把其中的“xxxxxxxx”替换为“10200000”。数据目录数组的第二个元素导入表目录结构成员
2的值是指IMAGE_IMPORT_DESCRIPTOR 结构数组的长度,IMAGE_IMPORT_DESCRIPTOR 结构长度是20个字节,我们这里的
MAGE_IMPORT_DESCRIPTOR 结构数组元素是2个,并且以全零结尾,那么总长度就是3 * 20 = 60byte,转换成十六进制是
3C,我们可以把另一个“xxxxxxxx”替换为“3C000000”。
下面我们填写导入表即IMAGE_IMPORT_DESCRIPTOR 结构数组的成员。第一个数组元素的有关user32.dll信息,5个成员
中:
成员1,4个字节,他实际上是指向一个 IMAGE_THUNK_DATA 结构数组的RVA,而IMAGE_THUNK_DATA 结构数组记录所有从
某个.dll库中导入函数名称的RVA。IMAGE_THUNK_DATA 结构位于IMAGE_IMPORT_DESCRIPTOR 结构之后,由
IMAGE_IMPORT_DESCRIPTOR 结构数组的起始地址和长度可以得到IMAGE_THUNK_DATA 结构数组的起始地址。即610h + 3Ch =
64Ch,那么这个IMAGE_IMPORT_DESCRIPTOR 结构数组的第一个元素的成员1的值是不是就是64Ch呢?并不是的,因为这是一个
RVA值,所以映射后的值应该是“4C200000”。
成员2,成员3各4各字节,用处不大,我们用零填充。
成员4,4个字节,是指向DLL名字的RVA,即指向DLL名字的指针,也是一个ASCIIZ字符串。我们将在后面编写
“user32.dll”字符串,计算得到其RVA为“6A200000”。
成员5,4个字节,指向一个 IMAGE_THUNK_DATA 结构数组的RVA,同成员1一样,但是此IMAGE_THUNK_DATA 数组结构保
存了所有导入函数的指针。PE文件被装载到内存时,用引入函数真实地址来替代由这里的IMAGE_THUNK_DATA 数组里的元
素值。那么这里填哪里的地址呢?这就要用到我们刚刚空下的空间了。我们将此值设为“08200000”。
按照上面方法填写导入表即IMAGE_IMPORT_DESCRIPTOR 结构数组的成员。第二个数组元素的有关kennel.dll信息的5个
成员的值,我们这里分别为“5420000”,“00000000”,“00000000”,“84200000”,“00200000”。
最后以全零结尾这个IMAGE_IMPORT_DESCRIPTOR 结构数组,即:
“00000000”,“00000000”,“00000000”,“00000000”,“00000000”。
下面填写有关user32.dll信息的IMAGE_IMPORT_DESCRIPTOR 结构数组的IMAGE_THUNK_DATA结构数组。 他记录了所有从
user32.dll库中导入函数名称的RVA。我们这里只有MessageBoxA函数,我们可以在后面编写此字符串,并得到他的地址,
这里为“5C200000”。紧接着以“00000000”结束user32.dll中导入函数的输入。
然后是kennel32.dll信息的IMAGE_IMPORT_DESCRIPTOR 结构数组的IMAGE_THUNK_DATA结构数组。他记录了所有从
kennel32.dll库中导入函数名称的RVA。我们这里只有ExitProcess函数,我们可以在后面编写此字符串,并得到他的地址,
这里为“76200000”。紧接着以“00000000”结束kennel32.dll中导入函数的输入。
最后完成这些字符串的输入,之后要对齐.rdata段的剩余部分,剩余部分用零填充。最终得到的代码如下:
000600 A2 CA 81 7C 00 00 00 00 8A 05 D5 77 00 00 00 00 ⑹....?....
000610 4C 20 00 00 00 00 00 00 00 00 00 00 6A 20 00 00 L ..........j ..
000620 08 20 00 00 54 20 00 00 00 00 00 00 00 00 00 00 ..T ..........
000630 84 20 00 00 00 20 00 00 00 00 00 00 00 00 00 00 ?... ..........
000640 00 00 00 00 00 00 00 00 00 00 00 00 5C 20 00 00 ............\ ..
000650 00 00 00 00 76 20 00 00 00 00 00 00 9D 01 4D 65 ....v ......?Me
000660 73 73 61 67 65 42 6F 78 41 00 75 73 65 72 33 32 ssageBoxA.user32
000670 2E 64 6C 6C 00 00 80 00 45 78 69 74 50 72 6F 63 .dll...ExitProc
000680 65 73 73 00 6B 65 72 6E 65 6C 33 32 2E 64 6C 6C ess.kernel32.dll
000690 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0006A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0006B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0006C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0006D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0006E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0006F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000700 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000710 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000720 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000730 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000740 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000750 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000760 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000770 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000780 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000790 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0007A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0007B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0007C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0007D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0007E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0007F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
最后一个是.data段,这个段非常简单,就是MessageBoxA所需的参数,消息框的标题和内容,最终代码如下:
(注意对齐问题,补足200h字节。)
000800 CF FB CF A2 BF F2 00 48 65 6C 6C 6F 2C 20 57 6F 消息框.Hello, Wo
000810 72 6C 64 20 21 00 00 00 00 00 00 00 00 00 00 00 rld !...........
000820 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000830 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000840 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000850 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000860 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000870 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000880 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000890 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0008A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0008B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0008C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0008D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0008E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0008F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000900 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000910 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000920 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000930 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000940 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000950 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000960 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000970 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000980 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000990 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0009A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0009B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0009C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0009D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0009E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0009F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
好了,到此为止一个完整的显示Hello World!的可执行程序就完成了,赶快双击运行一下吧...费了这么大劲就这么个小
功能,是不是有点事倍功半呢?其实手写这么个程序只是为了更加熟练掌握PE结构,相信当您真正的手工完成了这个程序,
您对PE结构一定有一个非常深刻的理解。(提示:请按照以上步骤完成这个程序,之后再回头从头到尾联系上下文仔细看一遍,
因为很多地方都是前后关联紧密的,只作一遍,或只读一遍是很难融汇贯通的。)
由于水平和时间的原因,难免有许多问题,或者表述不清楚的地方,请高手不吝赐教。