PE32格式学习之手动创建一个简单的Windows程序
1256710576241886 发表于 江苏 技术文章 2427浏览 · 2024-03-15 06:12

本项目通过手动创建一个简单的exe文件,介绍PE32可执行文件的基本结构。作为入门级别的教程,本文尽可能把需要的前置知识将到最低。其他必要的知识在构建文件的时候讲解。

一、项目目标:手动创建简单的PE.exe程序。

功能:弹出一个如下图的对话框(图)

二、基础知识

在开始正式创建PE文件之前,默认操作者掌握以下概念(不懂的,可以自行学习,不难):

  • 懂得二进制、十进制、十六进制的表示、计算和转换
  • 理解计算机中 bit 和 byte 的概念、组织方式和表示。
  • 理解BYTE、WORD、DWORD的概念。
  • 掌握 little ending 规范。
  • 了解 ASCII 编码和 C 字符串规则。
  • 学习过C语言,知道struct结构的尤佳,但不强求。
  • 会使用至少一种二进制编辑工具,推荐 ImHex,易上手开源无费用。

基础知识 1:进程虚拟地址空间

Widnows中运行一个程序,系统会首先为这个程序创建一个“进程”。进程是程序运行的容器,包含程序运行所需要的各种资源。
我们知道程序只有加载到内存才能运行,磁盘上的exe程序加载到内存哪里呢?
与我们想象的直接加载到计算机的物理内存不同,操作系统通过“进程”容器给程序提供了一个叫做“虚拟内存地址空间”的东西。磁盘上的exe文件是加载到这个“虚拟内存空间”中的。现代操作系统这么设计的主要目的就是屏蔽操作物理内存的底层细节,让程序员开发的程序面对一个一致的,完整的运行空间。
以手动创建的PE.exe为例,其运行时的虚拟内存空间环境大致如下图:

由于栈是基于线程的,为了让本项目涉及到的知识最简单,这里不做介绍。

基础知识 2:内存页和物理内存映射

现代操作系统都是多任务操作系统,可以同时运行很多进程。根据上面介绍的,每一个32位进程都有 4G 的虚拟内存空间,显然我们的计算机物理内存是不够的。通过观察上面的PE.exe虚拟内存分布,可以发现绝大多数虚拟内存是用不上的。而系统DLL其实每一个进程都要用到。所以操作系统巧妙的将虚拟内存“裁减”成一块一块同样大小的内存片,同时把物理内存也“裁减”成同样大小的内存片。只有需要使用的虚拟内存片,操作系统才会将其映射到实际的物理内存片上。这种机制可以轻松实现内存共享,比如系统DLL的物理内存片,可以映射到所有进程的虚拟内存空间中。如下图所示:

“裁减”的内存片的大小很有讲究。裁小了,映射的开销就会增大,最终会得不偿失;裁大了,又会造成许多浪费。现代操作系统根据计算机一般配备的物理内存大小,普遍选用 4KB(4096) 字节大小的内存分片。

基础知识 3:PE32可执行文件基本结构

一个典型的exe可执行程序一般包含:

  • 文件头:PE32文件头是多个结构的组合
  • .code 节:包含用户代码编译后的二进制指令流
  • .data 节:包含代码运行前需要预初始化的数据,例如全局变量等
  • .idata 节:一般包含程序运行需要用到的系统函数等的导入表。

可以参考“基础知识 4”里面的图。本项目按照这种结构手动构建PE.exe。

基础知识 4:从磁盘 File 到内存 Image

将可执行程序从磁盘文件被加载到内存的程序,一般称为Loader。这个过程中Loader会做许多工作,十分复杂。就理解这个项目来说,需要了解以下两项:

PE.exe在磁盘上的时候我们称为可执行文件,但被加载到内存时,我们将其称为Image。

1、程序并不是原封不动拷贝的,而是有个“拉伸”过程。

图中可以看出section在文件中和加载到虚拟内存中,起始地址对齐的位置不同。为什么是512和4096呢?因为传统磁盘一个扇区是512个字节,而前面讲过内存页的大小是 4KB,也就是4096个字节。显然磁盘文件保存形式更紧凑,节约存储空间。而内存模式,因为是以内存页的形式将虚拟内存映射到物理内存,用不到的虚拟内存不映射到物理内存,所以也不存在“浪费”一说。

由于这种加载时候的“拉伸”过程存在,程序中变量/字段的相对位置(相对于文件/内存Image起始位置)会不一样。所以产生FOA和RVA的概念。

  • FOA:File Offset Address,字段在文件中,相对文件起始位置的偏移量。我们手动创建PE.exe时就需要使用这个偏移量来定位需要编辑的字段。
  • RVA:Relative Virtual Address,字段在内存Image中,相对Image起始位置的偏移量。编辑字段值时需要特别注意。

2、根据导入表加载相应的DLL,并将实际的函数虚拟地址更新到导入表的地址表中。

导入表是一个新概念,这里只简单提一下,不理解没关系。后面讲到 .idata节的时候,会详细介绍这一部分。这是理解windows可执行文件格式中很重要的内容,也是本项目知识点中理解起来最困难的部分。不过不要害怕,只要认真学习这个简化到及至的例子,其实也就那么回事。

三、创建PE32可执行文件框架

按照“基础知识 3”介绍的典型PE32可执行文件基本结构,准备创建的PE.exe文件,分为4个部分:文件头、.code 节、.data 节、.idata 节。

由于PE.exe功能简单,本项目设定每个部分大小都是512字节。

使用二进制编辑工具(如imHex),创建一个512*4=2048个字节,值全为0的二进制文件,保存为PE.exe

四、编辑PE.exe文件头

PE32文件头十分复杂,本文尽可能只介绍必须用到的部分,尽量降低复杂度。相信只要有点耐心,看着复杂的文件头,其实际要编辑的地方并不是很多。

PE32文件头大致可以分为如图的几个部分:DOS Header、PE File Header、PE Optional Header、Section Table(Section Headers Array)

下面我们来逐一编辑创建这些文件头。

1、DOS Header

FOA RVA Size Field Value Description
0 0 2 e_magic 0x5A4D DOS头标志 ‘MZ’
2 2 58 e_xxx 00 DOS头字段,可以忽略
0x3C 0x3C 4 e_lfanew 0x80 指向PE Header头部的偏移量
0x40 0x40 0x40 DOS Stub 00 DOS 模式下的一段程序,可以忽略

2、PE File Header

FOA RVA Size Field Value Description
0x80 0x80 4 PE Signature 0x4550 PE头标志 ‘PE\x00\x00'
0x84 0x84 2 Machine 0x014C 表示这个文件需要运行在 Intel 386 or later processors and compatible processors。
0x86 0x86 2 NumberOfSections 3 本项目规划3个节:.code、.data、.idata
0x88 0x88 4 TimeDateStamp 00 文件创建时间,可以忽略
0x8C 0x8C 4 PointerToSymbolTable 00 指向符号表的偏移量,本项目没有符号表,可以忽略
0x90 0x90 4 NumberOfSymbols 00 没有符号,可以忽略
0x94 0x94 2 SizeOfOptionalHeader 0xE0 PE32 的 OptionalHeader 大小
0x96 0x96 2 Characteristics 0x0102 IMAGE_FILE_EXECUTABLE_IMAGE \ IMAGE_FILE_32BIT_MACHINE
文件的属性:32位的可执行文件

3、PE Optional Header

FOA RVA Size Field Value Description
0x98 0x98 2 Magic 0x010B 标识该文件是 PE32 文件(相对于64位的PE32+:0x20B)
0x9A 0x9A 1 MajorLinkerVersion 00 链接器主版本号,可以忽略
0x9B 0x9B 1 MinorLinkerVersion 00 链接器副版本号,可以忽略
0x9C 0x9C 4 SizeOfCode 0x1000 .code 节需对齐到内存页边界,内存页大小为0x1000。实际代码只有十几个字节。
0xA0 0xA0 4 SizeOfInitializedData 00 已初始化的数据(节)大小,可以忽略
0xA4 0xA4 4 SizeOfUninitializedData 00 未初始化的数据(节)大小,可以忽略
0xA8 0xA8 4 AddressOfEntryPoint 0x1000 程序的入口点RVA。这里设置成 .code 节在内存中的起始RVA
0xAC 0xAC 4 BaseOfCode 0x1000 .code 节在内存Image中的起始地址RVA
0xB0 0xB0 4 BaseOfData 0x2000 .data 节在内存Image中的起始地址RVA
0xB4 0xB4 4 ImageBase 0x400000 文件加载到进程虚拟内存地址空间的起始位置
0xB8 0xB8 4 SectionAlignment 0x1000 每个节在内存Image中需要对齐到内存页大小的边界上
0xBC 0xBC 4 FileAlignment 0x200 每个节在磁盘文件中需要对其到512字节的边界上
0xC0 0xC0 2 MajorOperatingSystemVersion 00 程序运行需要的操作系统主版本号,可以忽略
0xC2 0xC2 2 MinorOperatingSystemVersion 00 程序运行需要的操作系统副版本号,可以忽略
0xC4 0xC4 2 MajorImageVersion 00 程序的主版本号,可以忽略
0xC6 0xC6 2 MinorImageVersion 00 程序的副版本号,可以忽略
0xC8 0xC8 2 MajorSubsystemVersion 6 程序要求的子系统主版本号
0xCA 0xCA 2 MinorSubsystemVersion 1 程序要求的子系统副版本号。Win7的版本号是 6.1
0xCC 0xCC 4 Win32VersionValue 00 保留,必须是00
0xD0 0xD0 4 SizeOfImage 0x4000 程序的内存Image大小,Headers+3Sections,每个对齐到内存页边界后就是4*0x1000
0xD4 0xD4 4 SizeOfHeaders 0x200 不足512字节,对齐到512边界
0xD8 0xD8 4 CheckSum 00 校验值,可以忽略
0xDC 0xDC 2 Subsystem 2 IMAGE_SUBSYSTEM_WINDOWS_GUI Windows图形界面子系统
0xDE 0xDE 2 DllCharacteristics 00 这是一个exe程序,不是一个DLL
0xE0 0xE0 4 SizeOfStackReserve 00 使用系统默认值
0xE4 0xE4 4 SizeOfStackCommit 00 使用系统默认值
0xE8 0xE8 4 SizeOfHeapReserve 00 使用系统默认值
0xEC 0xEC 4 SizeOfHeapCommit 00 使用系统默认值
0xF0 0xF0 4 LoaderFlags 00 保留,必须是00
0xF4 0xF4 4 NumberOfRvaAndSizes 16 data-diretory 结构的数组,每一项都代表一个重要的数据结构的RVA和Size。
0xF8 0xF8 8 Export Table 00 导出表
0x100 0x100 4 Import Table RVA 0x3000 导入表的结构在 .idata 节的起始处
0x104 0x104 4 Import Table Size 20 导入表结构大小
0x108 0x108 8 Resource Table 00 本项目用不到,可以忽略
0x110 0x110 8 Exception Table 00 本项目用不到,可以忽略
0x118 0x118 8 Certificate Table 00 本项目用不到,可以忽略
0x120 0x120 8 Base Relocation Table 00 本项目用不到,可以忽略
0x128 0x128 8 Debug 00 本项目用不到,可以忽略
0x130 0x130 8 Architecture 00 本项目用不到,可以忽略
0x138 0x138 8 Global Ptr 00 本项目用不到,可以忽略
0x140 0x140 8 TLS Table 00 本项目用不到,可以忽略
0x148 0x148 8 Load Config Table 00 本项目用不到,可以忽略
0x150 0x150 8 Bound Import 00 本项目用不到,可以忽略
0x158 0x158 8 IAT 00 本项目用不到,可以忽略
0x160 0x160 8 Delay Import Descriptor 00 本项目用不到,可以忽略
0x168 0x168 8 CLR Runtime Header 00 本项目用不到,可以忽略
0x170 0x170 8 Reserved, must be zero 00 本项目用不到,可以忽略

data-diretory 结构的数组一共16个Entry,每一个Entry的含义都是固定的。本项目中只用到了导入表。

4、Section Table (Section Headers)

Section Table紧接着上面的各种 Headers,本质上是一个Section Header结构的数组,有几个 Section 就有几个数组元素。每个Section Header结构描述该节的一些基本属性。

补充知识:为了加强安全性,操作系统在CPU的配合下,给内存页设置了三个权限:READ、WRITE、EXECUTE。存放代码的内存页一般赋予READ和EXECUTE权限,防止代码被篡改。存放数据的内存页一般赋予READ、WRITE权限,万一攻击者写入恶意代码,页不能执行。

这个概念方便理解 Section Header 里的 Characteristics 字段的含义。Sections 都是要随着 File to Image 被加载到进程虚拟内存中的。

FOA RVA Size Field Value Description
0x178 0x178 8 Name ".code" 字符串".code"
0x180 0x180 4 VirtualSize 0x1000 该节在内存Image中的大小
0x184 0x184 4 VirtualAddress 0x1000 该节在内存Image中的起始地址RVA
0x188 0x188 4 SizeOfRawData 0x200 该节在磁盘文件中的大小
0x18C 0x18C 4 PointerToRawData 0x200 该节在磁盘文件中的起始地址FOA
0x190 0x190 4 PointerToRelocations 00 本项目用不到,可以忽略
0x194 0x194 4 PointerToLinenumbers 00 本项目用不到,可以忽略
0x198 0x198 2 NumberOfRelocations 00 本项目用不到,可以忽略
0x19A 0x19A 2 NumberOfLinenumbers 00 本项目用不到,可以忽略
0x19C 0x19C 4 Characteristics 0x60000020 IMAGE_SCN_CNT_CODE 该节包含执行代码
IMAGE_SCN_MEM_EXECUTE 该节可以被执行
IMAGE_SCN_MEM_READ 该节可以被访问
0x1A0 0x1A0 8 Name ".data" 字符串".data"
0x1A8 0x1A8 4 VirtualSize 0x1000 该节在内存Image中的大小
0x1AC 0x1AC 4 VirtualAddress 0x2000 该节在内存Image中的起始地址RVA
0x1B0 0x1B0 4 SizeOfRawData 0x200 该节在磁盘文件中的大小
0x1B4 0x1B4 4 PointerToRawData 0x400 该节在磁盘文件中的起始地址FOA
0x1B8 0x1B8 4 PointerToRelocations 00 本项目用不到,可以忽略
0x1BC 0x1BC 4 PointerToLinenumbers 00 本项目用不到,可以忽略
0x1C0 0x1C0 2 NumberOfRelocations 00 本项目用不到,可以忽略
0x1C2 0x1C2 2 NumberOfLinenumbers 00 本项目用不到,可以忽略
0x1C4 0x1C4 4 Characteristics 0xC0000040 IIMAGE_SCN_CNT_INITIALIZED_DATA 该节包含初始化数据
IMAGE_SCN_MEM_READ 该节可以被访问
IMAGE_SCN_MEM_WRITE 该节可以被写入
0x1C8 0x1C8 8 Name ".idata" 字符串".idata"
0x1D0 0x1D0 4 VirtualSize 0x1000 该节在内存Image中的大小
0x1D4 0x1D4 4 VirtualAddress 0x3000 该节在内存Image中的起始地址RVA
0x1D8 0x1D8 4 SizeOfRawData 0x200 该节在磁盘文件中的大小
0x1DC 0x1DC 4 PointerToRawData 0x600 该节在磁盘文件中的起始地址FOA
0x1E0 0x1E0 4 PointerToRelocations 00 本项目用不到,可以忽略
0x1E4 0x1E4 4 PointerToLinenumbers 00 本项目用不到,可以忽略
0x1E8 0x1E8 2 NumberOfRelocations 00 本项目用不到,可以忽略
0x1EA 0x1EA 2 NumberOfLinenumbers 00 本项目用不到,可以忽略
0x1EC 0x1EC 4 Characteristics 0xC0000040 IIMAGE_SCN_CNT_INITIALIZED_DATA 该节包含初始化数据
IMAGE_SCN_MEM_READ 该节可以被访问
IMAGE_SCN_MEM_WRITE 该节可以被写入

至此PE.exe的所有头部数据结构都已经编辑完毕,一共 0x1F0 个字节大小。有兴趣的读者可以根据表格内的FOA偏移量和Value值进行编辑和保存。看着挺多,实际大部分地方都是0。

五、编辑.idata

之所以先编辑 .idata节,是因为本项目中这个节存放导入表。而.code节中的代码调用系统函数,需用用到Import Address Table中的存放该函数的地址。

下面我们就来详细介绍——导入表。

1、什么是导入表?

我们在编写程序,需要访问磁盘/网络、获取系统信息、进行程序之间的交互等等,只需要调用一些系统函数,而不需要自己去实现这些功能。这些由操作系统提供的函数,本质上是操作系统对用户提供的一系列服务,被封装成了Application Programming Interface(应用程序编程接口 API)。

这些API函数按照一定功能分类,被放在一个个系统的DLL文件里面。比如我们这个PE.exe就需要用到MessageBoxA这个API函数来显示对话框,MessageBoxA实现在user32.dll文件里面。那么PE.exe怎么在运行时知道,并调用user32.dll文件里面的MessageBoxA函数呢?实际上它不需要知道,这是一个复杂的机制。

  • 首先要在PE.exe文件里面保存需要用到的DLL及其函数的信息,并保留出存放运行时这些函数“实际地址”的存储空间。而保存这些信息的数据结构就是——导入表。
  • 操作系统的Loader在加载PE.exe时,查看导入表,并根据这些信息,将DLL加载到进程虚拟地址空间的高位地址空间中,然后将API函数在进程虚拟地址空间中的“实际地址”填入导入表为其保留的存放空间中。
  • PE.exe执行到调用API函数时,就会直接跳转到Import Address Table(IAT)对应表项中的实际地址。

简单来说,就是PE.exe在导入表中专门给MessageBoxA函数留了个位置,Loader根据导入表信息,加载DLL并用实际的MessageBoxA的地址更新这个位置。.code节中的代码只是调用这个位置的函数。(试想一下如果这个位置换一个其他函数,会是啥情况?这有个术语叫API劫持)

2、导入表的结构

这部分涉及到的知识比较专业,没有计算机编程方面基础的,可以跳过,直接跳到“构造导入表的部分。也可以结合构造导入表的过程,来加深理解。

这个图展示了一个有两个DLL,每个DLL通过函数名分别导入2个函数的导入表基本结构。(当然这只是最基本的导入方式,本项目只解释最基本的概念)

一眼看上去就懵了,是不是复杂到有点不知所云。没关系,我们只需要抓住上面结构的三个关键数据结构:

  • Directory Entry(Import Descriptor) 结构
  • Import Lookup Entry 结构
  • Hit-Name Entry 结构

所谓的Table都是这三种结构分别组成的数组。让我们来了解一下这三个结构:

(1)Directory Entry结构(20字节)

这个结构描述一个需要导入的DLL。

Offset Size Field Description
0 4 Import Lookup Table RVA import lookup table(ILT) 在内存image中的偏移量
4 4 Time/Date Stamp 初始为0,Loader 会将其更新为DLL加载成功的时间。
8 4 Forwarder Chain 设置为0就好。不用关心
12 4 Name RVA DLL名称字符串在内存image中的偏移量。
16 4 Import Address Table RVA (Thunk Table) import address table(IAT) 在内存image中的偏移量。这个值和ILT RVA相同

结合导入表的图,Import Lookup Table RVA(ILT RVA)和 Import Address Table RVA(IAT RVA)实际指向同块内存区域。 这是一个设计上很巧妙的地方。

(2)Import Lookup Entry结构(4字节)

这个结构描述DLL中需要导入的一个函数。可以使用Ordinal或者函数名两种方式导入一个函数。这里只讲解最直观的函数名导入方式。

注意这个结构的大小是4字节,意味着本身就可以作为一个地址指针。

Bit(s) Size Bit field Description
31 1 Ordinal/Name Flag 标志位,表示该函数是Ordinal还是函数名方式导入
15-0 16 Ordinal Number 如果是Ordinal方式导入函数,此16位是代表这个函数的数值。这种方式导入函数速度最快。
30-0 31 Hint/Name Table RVA 如果是函数名方式导入函数,此31位值代表指向 Hint/Name Entry 结构的内存image偏移量。

在PE.exe文件中,这个字段保存函数名称的偏移量。在被加载到进程虚拟内存后,Loader会根据函数名查找真实地址,然后存入这个字段。

在PE.exe文件中,这个字段叫 ILT(Import Lookup Table);在内存中,这个字段叫 IAT (Import Address Table)。区别就在于保存的内容变了。

(3)Hint/Name Entry结构(不定长)

Offset Size Field Description
0 2 Hint 可以看成是对函数名的一种散列值,用来快速匹配函数名对应的函数。
2 variable Name 一个 ASCII 字符串,以NULL结尾。
* 0 or 1 Pad 填充字节,保证下一个Entry的起始地址是偶数。

以函数名导入函数,需要涉及大量的字符串比较,效率不高。所以定义了一个 Hint来加快这种比对查找。

3、手动构造导入表

PE.exe只从user32.dll导入MessageBoxA,所以只有两个Directory Entry(最后一个是表示结束的NULL Entry),两个Import Lookup Table(最后一个是表示结束的NULL Entry)。我们在.idata节的起始位置开始布置导入表。

FOA RVA Size Field Value Description
0x600 0x3000 4 Import Lookup Table RVA 0x3028 导入查询表位置偏移量
0x604 0x3004 4 Time/Date Stamp 00 时间戳,置0
0x608 0x3008 4 Forwarder Chain 00 用不到,置0
0x60C 0x300C 4 DLL Name RVA 0x3030 DLL名字符串存放位置偏移量
0x610 0x3010 4 Import Address Table RVA (Thunk Table) 0x3028 导入地址表的位置偏移量
0x614 0x3014 20 Null Directory Entry 00 表示导入的DLL结束
0x628 0x3028 4 Import Lookup Table / Import Address Table 0x303B Import Lookup Entries
0x62C 0x302C 4 Null Import Lookup Entry 00 表示Import Lookup Table结束
0x630 0x3030 11 DLL Name "user32.dll\0" DLL名字符串
0x63B 0x303B 2 Hint 00 用不上,置0
0x63D 0x303D 12 Function Name “MessageBoxA\0" 函数名字符串
0x649 0x3049 1 Pad 00 下一个Entry需要对齐到偶数地址

导入表涉及到许多地址偏移量,且都是以内存image中的偏移量表示。这些在文件编辑中,需要明白FOA和RVA的区别以及转换。可以参考前面的基础知识仔细比对,加深理解。实际的布局如下图(这是一个紧凑型的布局):

六、编辑.data

PE.exe显示一个对话框。对话框需要定义“标题”和“显示内容”。这两个字符串保存在.data节中。

FOA RVA Value Description
0x400 0x2000 "Hello, snake!\0" 对话框标题
0x40E 0x200E “This is an example that created a PE file manually." 消息内容

七、编辑 .code 节

顾名思义,这个节就是存放实际代码的。这个项目我们就仅仅显示一个对话框,这需要调用user32.dll里面的MessageBoxA函数。查寻微软的帮助文档,这个函数长这样:

int MessageBoxA(
  [in, optional] HWND   hWnd,       // 要创建的消息框的所有者窗口的句柄。 如果此参数为 NULL,则消息框没有所有者窗口。
  [in, optional] LPCSTR lpText,     // 要显示的消息。 如果字符串由多行组成,则可以在每行之间使用回车符和/或换行符分隔这些行。
  [in, optional] LPCSTR lpCaption,  // 对话框标题。 如果此参数为 NULL,则默认标题为 Error。
  [in]           UINT   uType       // 对话框的内容和行为。 此参数可以是标志组中的标志的组合。
);

Windows API 遵循 __stdcall 规则:参数从右往左入栈,被调用者负责栈平衡。

我们可以根据下面的参数值设置,用汇编写一下这个函数的调用代码:

参数 Value Description
uType 0x40 MB_OK 消息框包含一个按钮: “确定”。 这是默认值。
MB_ICONASTERISK 消息框中将显示一个由圆圈中的小写字母 i 组成的图标。
lpCaption 0x402000 对话框标题字符串的地址,存放在 .data 节的起始位置。Image Base Address + Title String RVA
lpText 0x40200E 对话框显示消息的地址,存放在 标题字符串之后。Image Base Address + Content String RVA
hWnd 00 NULL,对话框不需要所有者。
push 0x40
push 0x402000
push 0x40200E
push 0
call DWORD ptr ds:0x403028
ret

call DWORD ptr ds:0x403028这里的0x403028就是上面设置的MessageBoxA函数的 Import Address Table(IAT) 地址。

代码需要使用变量的“绝对地址”,所以我们编辑的变量字段的RVA偏移量,需要加上 Image Base Address 才能在代码中使用。程序才能正确找到变量字段。

关于 Image Base Address,可以参考“基础知识1”,一般程序的可执行Image会加载到进程虚拟内存空间的 0x00400000 位置。

可以使用 Online x86 / x64 Assembler and Disassembler 这个网站在线将汇编代码翻译成二进制。

将下列字节流写入 .code 节在文件中的起始位置 0x200。

{ 0x6A, 0x40, 0x68, 0x00, 0x20, 0x40, 0x00, 0x68, 0x0E, 0x20, 0x40, 0x00, 0x6A, 0x00, 0xFF, 0x15, 0x28, 0x30, 0x40, 0x00, 0xC3 }

八、收工验证

按照以上步骤手动编辑并保存好文件后,就可以执行PE.exe。应该能弹出文章开头的那个对话框。如果读者能认真操作一遍,是不是到这个时候有点小激动?

文章很长,但实际的操作部分其实并不多。操作看上去复杂,但存在大量值是00的情况,估计5-10分钟就能编辑好,只需要一点点耐心和足够的仔细。

除去操作部分,理解PE32格式最主要的还是对文章中介绍的基础知识的理解。这可能需要花一些时间,最好有一定的编程基础。

3 条评论
某人
表情
可输入 255