深入理解PE,手工制作64位PE程序

这篇具有很好参考价值的文章主要介绍了深入理解PE,手工制作64位PE程序。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

深入理解PE,手工制作64位PE程序

手工构建64位PE程序

深入理解PE,手工制作64位PE程序

制作准备

软件工具准备

  • 010 Editor
  • x64dbg

目标代码

​ 先整理好我们要写入的代码,这样能够保证我们清楚知道要设置多少.text节区的VirtualSize大小,以及IMAGE_IMPORT_DESCRIPTOR ImportDescriptor导入表的结构是什么样的。

#include <iostream>
#include "windows.h"
int main()
{
    MessageBoxExW(nullptr, L"shp666", L"shp666", 0, 0);
    getchar();
}
  • 汇编代码
sub rsp,38
xor eax,eax
lea r8,qword ptr ds:[140001034]
xor r9d,r9d
mov word ptr ss:[rsp+20],ax
lea rdx,qword ptr ds:[140001042]
xor ecx,ecx
call qword ptr ds:[<&MessageBoxExW>]
call qword ptr ds:[<&_fgetchar>]
xor eax,eax
add rsp,38
ret 
  • 操作码
48 83 EC 38 33 C0 4C 8D 05 27 00 00 00 45 33 C9 66 89 44 24 20 48 8D 15 26 00 00 00 33 C9 FF 15 5C 10 00 00 FF 15 76 10 00 00 33 C0 48 83 C4 38 C3 00
  • x64dbg中显示

深入理解PE,手工制作64位PE程序


最后捋一下,我们在程序中使用了2个call

  • 一个是MessageBoxExW函数,它来自user32.dll动态链接库。
  • 另一个是_fgetchar函数,它来自msvcrt.dll库。

所以我们需要两个IMAGE_IMPORT_DESCRIPTOP结构来导入两个库,每个IMAGE_IMPORT_DESCRIPTOP都只有一个导入函数

ok,现在我们可以开始干一场了。

最终需要注意的是,在手工编写PE文件时,文件的编写不一定总是像我们写作一样从上向下写。也可能是先写下面,然后跳回来写上面。我会尽量用符合直觉的顺序编写,如果要进行跳跃式编写,我会提前说明。

创建一个空文件

  1. 打开010 Editor,左上角点击文件->新建->十六进制文件
  2. 更改文件后缀为.exe文件。
  3. 重新打开010 Editor,上方菜单栏点击视图->编辑方式->十六进制

Dos头 IMAGE_DOS_HEADER

IMAGE_DOS_HEADER一共64个字节,主要用来给DOS系统运行使用,但是不代表在非DOS系统使用时我们可以随意填写。

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:2个字节,是一个固定值0x5A4DASCII的代表的值为MZ
  • e_lfanew:4个字节,保存着我们接下来要使用的IMAGE_NT_HEADERS 结构的文件偏移量。

Shift + ctrl + i,我们从0位置,插入64个字节。

深入理解PE,手工制作64位PE程序

​ 填入我们刚才只关注的两个字段的值。因为windows的PE读入为小端序,所以0x5A4D的16进制的表示方式为4D 5A。在3Ch处,我们写入e_lfanew的值为40 00 00 00,表示IMAGE_NT_HEADERS的文件偏移量为0x40h

深入理解PE,手工制作64位PE程序

NT头 IMAGE_NT_HEADERS

​ PE文件结构中的NT头(也称为PE头)指的是Portable Executable(PE)文件的头部结构。该头部结构位于PE文件的起始位置,包含一些元数据和描述信息,用于指定PE文件的结构和属性。NT头是Windows操作系统用于加载和执行可执行文件的重要组成部分。

typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

需要注意32位与64位的NT头结构不同,我这里只展示64位的NT头结构

  • Signature 签名:4字节,标志PE文件格式的标志,通常为"PE\0\0"(ASCII码:50 45 00 00)。
  • FileHeader 文件头:指定PE文件的基本属性和布局信息。包括机器类型、文件类型、文件创建日期和时间、可选头的大小等信息。
  • OptionalHeader 可选头:指定PE文件的高级属性和布局信息。包括程序入口点、区块对齐方式、内存对齐方式、导出表、导入表、资源表等信息。

先插入字节

深入理解PE,手工制作64位PE程序

Signature我们要填入0x4550,小端序下50 45 00 00

深入理解PE,手工制作64位PE程序

文件头 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;

主要用来描述PE文件的大致属性。

  • Machine: 这是一个数字,表示可执行文件的目标机器类型(CPU 架构),这个字段可以有很多值,但我们只对其中两个感兴趣,0x8864forAMD640x14cfor i386。有关可能值的完整列表,您可以查看Microsoft 官方文档。

  • NumberOfSections:用来指出文件中存在的节区数量。

  • TimeDateStampUnix类型的时间戳,文件创建时间,16进制表示。

  • PointerToSymbolTable和NumberOfSymbols:这两个字段保存 COFF 符号表的文件偏移量和该符号表中的条目数,但是它们被设置为0表示不存在 COFF 符号表,这是因为 COFF 调试信息已弃用。(已弃用)

  • SizeOfOptionalHeader:NT头结构体最后一个成员为IMAGE_OPTIONAL_HEADER32/64结构体。SizeOfOptionalHeader用来指出它的长度。(IMAGE_OPTIONAL_HEADER32的大小为0xE0,IMAGE_OPTIONAL_HEADER64的大小为0xF0 )

  • Characteristics:表示文件属性的标志,这些属性可以是文件可执行、文件是系统文件而不是用户程序,以及很多其他的东西。可以在Microsoft 官方文档中找到这些标志的完整列表。(标志位可以进行或运算,如果要做一个64位可执行文件的话应该为: IMAGE_FILE_EXECUTABLE_IMAGE|IMAGE_FILE_LARGE_ADDRESS_ = 0x0022


Machine,我们需要填写0x8664

深入理解PE,手工制作64位PE程序

NumberOfSections,我们填写2,我们只需要两个节区就可以完成这个程序。

TimeDateStamp,我们指定0就可以。

PointerToSymbolTableNumberOfSymbols我们也指定0。

SizeOfOptionalHeader,我们指定0xF0

Characteristics,我们指定0x0022

深入理解PE,手工制作64位PE程序

可选头 IMAGE_OPTIONAL_HEADER64
typedef struct _IMAGE_OPTIONAL_HEADER64 {
    // 大小为大小为0xF0
    WORD        Magic;
    BYTE        MajorLinkerVersion;
    BYTE        MinorLinkerVersion;
    DWORD       SizeOfCode;
    DWORD       SizeOfInitializedData;
    DWORD       SizeOfUninitializedData;
    DWORD       AddressOfEntryPoint;
    DWORD       BaseOfCode;
    ULONGLONG   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;
    ULONGLONG   SizeOfStackReserve;
    ULONGLONG   SizeOfStackCommit;
    ULONGLONG   SizeOfHeapReserve;
    ULONGLONG   SizeOfHeapCommit;
    DWORD       LoaderFlags;
    DWORD       NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
  • **Magic:**标识图像文件状态的无符号整数。 最常见的数字是 0x10B,它将其标识为普通可执行文件。 0x107将其标识为 ROM 映像,0x20B将其标识为 PE32+ 可执行文件。

  • **MajorLinkerVersion和MinorLinkerVersion:**链接器主版本号;链接器次要版本号;

  • **SizeOfCode:**代码 (文本) 节(.text)的大小;如果有多个节,则为所有代码节的总和。

  • **SizeOfInitializedData:**该字段保存初始化数据 (.data ) 部分的大小,或者如果有多个部分,则保存所有初始化数据部分的总和。

  • **SizeOfUninitializedData:**该字段保存未初始化数据 ( .bss) 部分的大小,或者如果有多个部分,则为所有未初始化数据部分的总和。

  • **AddressOfEntryPoint(重要 程序EP):**文件加载到内存时入口点的RVA。文档指出,对于程序映像,此相对地址指向起始地址,对于设备驱动程序,它指向初始化函数。对于 DLL,入口点是可选的,在没有入口点的情况下,该AddressOfEntryPoint字段设置为0

  • **ImageBase:**指出文件的优先装入地址。该字段保存图像加载到内存时的第一个字节的首选地址(首选基地址),该值必须是64K的倍数。由于像 ASLR 这样的内存保护以及许多其他原因,该字段指定的地址几乎从未被使用过,在这种情况下,PE 加载程序选择一个未使用的内存范围来加载图像,在将图像加载到该地址后加载器进入一个称为重定位的过程,它修复图像中的常量地址以使用新的图像库,有一个特殊的部分保存有关需要重定位时需要修复的地方的信息,该部分称为重定位部分( .reloc)。

深入理解PE,手工制作64位PE程序

  • SectionAlignment:此字段包含一个值,该值用于内存中的段对齐(以字节为单位),段在内存边界中对齐是该值的倍数。文档指出此值默认为体系结构的页面大小,并且不能小于FileAlignment.
  • FileAlignment:类似于SectionAligment此字段包含一个值,该值用于磁盘上的部分原始数据对齐(以字节为单位),如果部分中实际数据的大小小于该FileAlignment值,则块的其余部分将用零填充以保持对齐边界。文档指出该值应该是 2 的幂,介于 512 和 64K 之间,如果该值SectionAlignment小于体系结构的页面大小,则FileAlignment和 的大小SectionAlignment必须匹配。
  • **MajorOperatingSystemVersion, MinorOperatingSystemVersion, MajorImageVersion, MinorImageVersion, MajorSubsystemVersionand MinorSubsystemVersion😗*这些结构成员指定了所需操作系统的主版本号,所需操作系统的次版本号,镜像的主版本号,镜像的次版本号,主版本号子系统的版本号和子系统的次版本号。
  • **SizeOfImage:**Image文件的大小(以字节为单位),包括所有标题。它会四舍五入为SectionAlignment的倍数,因为在将Image加载到内存中时会使用该值。
  • **SizeOfHeaders:**MS-DOS 存根、PE 标头和节标头的组合大小向上舍入为 FileAlignment 的倍数。
  • **CheckSum:**图像文件的校验和,用于在加载时验证图像。
  • **Subsystem:**运行此映像所需的子系统。 有关详细信息,请参阅 Windows 子系统。
  • **DLLCharacteristics:**该字段定义了可执行映像文件的一些特征,例如它是否NX兼容以及是否可以在运行时重新定位。我不知道它为什么被命名DLLCharacteristics,它存在于普通的可执行映像文件中,并且它定义了可以应用于普通可执行文件的特征。可以在Microsoft 官方文档DLLCharacteristics中找到可能的标志的完整列表。
  • **SizeOfStackReserve, SizeOfStackCommit,SizeOfHeapReserve和SizeOfHeapCommit:**这些字段分别指定要保留的堆栈大小、要提交的堆栈大小、要保留的本地堆空间大小和要提交的本地堆空间大小。
  • **LoaderFlags:**一个保留字段,文档说应该设置为0
  • **NumberOfRvaAndSizes:**可选标头(DataDirectory)的数组大小一般为16。
  • **DataDirectory:**结构数组IMAGE_DATA_DIRECTORY

Magic,我们写入0x20B

MajorLinkerVersionMinorLinkerVersionSizeOfCodeSizeOfInitializedDataSizeOfUninitializedData我们全部填入0。

AddressOfEntryPoint我们写入1000h

BaseOfCode我们写入0。

ImageBase我们写入140000000h

SectionAlignment= 4096

FileAlignment : 512

MajorOperatingSystemVersion、MinorOperatingSystemVersion、MajorImageVersion、MinorImageVersion全部填入0。

MajorSubsystemVersion: 6。关于这个值为什么是6,您可以查看这篇文档。我们的子系统为CONSOLE根据文档最低为5.01,默认为6.0

MinorSubsystemVersionWin32VersionValue:0

SizeOfImage = 3000h

SizeOfHeaders = 200h

CheckSum = 0。

Subsystem = 3,我们要使用控制台程序。

DllCharacteristics、SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、SizeOfHeapCommit、LoaderFlags = 0

NumberOfRvaAndSizes = 16,默认定义16。

IMAGE_DATA_DIRECTORY_ARRAY我们只需要导入表,但是目前不能确定导入表的RVA,所以我们先全部填充0。但是后面我们将要返回来填写此字段。

深入理解PE,手工制作64位PE程序

写完这些值之后我们可以重新打开文件,就能看到编辑器已经可以读入正常的格式了。

深入理解PE,手工制作64位PE程序

节区头 IMAGE_SECTION_HEADER

​ 首先说明的概念,是可执行文件的实际数据的容器,它们占据了 PE 文件中标头之后的其余部分,恰好在节标头之后。

​ 常见的节区如下:

  • **.text:**包含程序的可执行代码。
  • **.data:**包含初始化数据。
  • **.bss:**包含未初始化的数据。
  • **.rdata:**包含只读初始化数据。
  • **.edata:**包含导出表。
  • **.idata:**包含导入表。
  • **.reloc:**包含图像重定位信息。
  • **.rsrc:**包含程序使用的资源,包括图像、图标甚至嵌入式二进制文件。
  • .tls: ( T hread Local S torage) ,为程序的每一个执行线程提供存储。

节区头用来描述节的相关大小和地址信息。存放在可选头与节区之间的位置。

​ 我们需要两个节区.text来保存代码和字符常量。.rdata节区来保存导入表相关结构。

typedef struct _IMAGE_SECTION_HEADER { // 大小为40d字节
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME]; // IMAGE_SIZEOF_SHORT_NAME = 8
    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;

节区头是一个数组,下面介绍成员的属性。

  • Name Section Header 的第一个字段,一个字节数组,其大小IMAGE_SIZEOF_SHORT_NAME包含该节的名称。 具有一个节名不能超过 8 个字符的IMAGE_SIZEOF_SHORT_NAME的值。8对于更长的名称,官方文档提到了一种解决方法,即用字符串表中的偏移量填充此字段,但是可执行映像不使用字符串表,因此 8 个字符的限制适用于可执行映像。
  • PhysicalAddressor VirtualSize: Aunion为同一事物定义了多个名称,该字段包含该部分加载到内存时的总大小。
  • **VirtualAddress😗*文档指出,对于可执行映像,该字段包含加载到内存中时相对于映像基址的部分的第一个字节的地址,对于目标文件,它包含应用重定位之前该部分的第一个字节的地址。
  • **SizeOfRawData😗*此字段包含磁盘上该部分的大小,它必须是 的倍数IMAGE_OPTIONAL_HEADER.FileAlignment
    SizeOfRawData并且VirtualSize可以不同,我们将在后面的帖子中讨论其原因。
  • **PointerToRawData(重要)😗*这个字段表示该节区在文件中的偏移量(文件偏移),即该节区的数据在PE文件中的实际位置。PointerToRawData字段是相对于PE文件起始位置的偏移量,而不是相对于节区表的偏移量。
  • **PointerToRelocations:**指向该部分重定位条目开头的文件指针。它被设置0为用于可执行文件。
  • **PointerToLineNumbers:**指向该部分的 COFF 行号条目开头的文件指针。设置为0因为 COFF 调试信息已弃用。
  • **NumberOfRelocations:**该部分的重定位条目数,它设置0为用于可执行映像。
  • **NumberOfLinenumbers:**该部分的 COFF 行号条目数,设置为0因为 COFF 调试信息已弃用。
  • **Characteristics:**描述部分特征的标志。
    这些特征就像该部分是否包含可执行代码、包含已初始化/未初始化的数据、是否可以在内存中共享一样。 可以在Microsoft 官方文档
    中找到部分特征标志的完整列表。

​ 下面开始写入数据。

148h处开始就是我们的节区头结构的开始。

.text节区头

下面的值我将用直接用16进制表示。

  • Name = 2E 74 65 78 74 00 00 00 ascii的值为.text

  • VirtualSize = 46 00 00 00 十进制为70,这个大小是由我们在节区的数据量来确定,我们这个操作码+字符常量的总占地大小为70,所以这里填写70。

  • VirtualAddress = 00 10 00 00

  • SizeOfRawData = 00 02 00 00

  • PointerToRawData = 00 02 00 00

  • PointerToRelocations、PointerToLineNumbers、NumberOfRelocations、NumberOfLinenumbers = 00 00 00 00 00 00 00 00 00 00 00 00

  • Characteristics = 20 00 00 60主要是权限,我们来个可读可执行。

.rdata节区头

这块就不细说了,只说明三个值

  • VirtualSize = 00 20 00 00
  • SizeOfRawData = 00 02 00 00
  • PointerToRawData = 00 04 00 00

这些都是我们后面计算RWA、RVA非常需要的值。

深入理解PE,手工制作64位PE程序

节区信息

.text节区

.text用来保存代码和字符常量,我们给它设置的起始地址为200h,大小为200h,所以我们要再进行插入

深入理解PE,手工制作64位PE程序

插入之后,我们先不进行数据填充。理由如下:

  • 我们的代码没有办法确定,函数的地址我们现在未知,call xxxx这里的xxxx就是IAT的RVA,但是我们现在还没有进行导入表数据填写,所以无法确定。

所以我们写入200h个空字符后先不要去着急写入代码。后面我们返回来进行填写。

.rdata节区

​ 此节区也先进行填充,我们设置的起始地址为400h大小为200h


到此为止,我们的文件结尾地址应该是如图所示。

深入理解PE,手工制作64位PE程序

导入表编写(重点)

​ 我们需要在.rdata节区编写我们的导入表结构,我们从400h开始,要进行一个IMAGE_IMPORT_DESCRIPTOR数组的编写。

​ 此数组有若干个IMAGE_IMPORTDESCRIPTOR结构体构成,数组的大小并不是固定的,系统会根据最后一个空IMAGE_IMPORT_DESCRIPTOR结构体来判断是否为数组的结束。

​ 之前说过我们有两个DLL文件需要导入,那我们就需要(2 + 1) * sizeof(IMAGE_IMPORT_DESCRIPTOR) =3 * 20(十进制) = 6060个字节需要使用。

返回更新IMAGE_DATA_DIRECTORY Import的值

​ 在编写IMAGE_IMPORT_DESCRIPTOR结构体之前,我们需要设置一个值。我们现在可以确定导入表的文件偏移了,为400h。那么IMAGE_DATA_DIRECTORY Import的值应该是400hRVA2000h+400h-400h = 2000h。大小为60d也就是3ch

深入理解PE,手工制作64位PE程序

还记得IMAGE_DATA_DIRECTORY Import吗?在可选头阶段,我们没有填写导入表的RVA,因为当时我们无法确定节区、地址、大小。

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk;
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain;
    DWORD   Name;
    DWORD   FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
  • OriginalFirstThunk ILT/INT 的 RVA。
  • **TimeDateStamp:**一个时间日期戳,如果未绑定则初始设置为如果绑定则0设置为-1
    在未绑定导入的情况下,时间日期戳会在图像绑定后更新为 DLL 的时间日期戳。
    在绑定导入的情况下,它保持设置为-1并且 DLL 的实时日期戳可以在相应的绑定导入目录表中找到IMAGE_BOUND_IMPORT_DESCRIPTOR
  • **ForwarderChain:**第一个转发器链引用的索引。
    这是负责DLL转发的东西。(DLL 转发是指一个 DLL 将它的一些导出函数转发给另一个 DLL。)
  • **Name:**包含导入 DLL 名称的 ASCII 字符串的 RVA。
  • FirstThunk: IAT 的 RVA。

​ 这个结构中我们只需要关注OriginalFirstThunkNameFirstThunk这三个成员的值就够了。这三个成员的值都是RVA,相对虚拟地址。

​ 为了得到它们的RVA,我们需要先给它们找到一个地址存放它们的数据。


​ 我们先将我们需要的DLLDLL中的函数写入.rdata节区,只有确定了这些值的地址我们才能填写IMAGE_IMPORT_DESCRIPTOR结构体。

IMAGE_IMPORT_DESCRIPTOR数组的大小为60,400h+60d = 43ch,我们从43Ch地址处开始填入。

00 00 4D 65 73 73 61 67 65 42 6F 78 45 78 57 00
55 53 45 52 33 32 2E 64 6C 6C 00 00 6D 73 76 63
72 74 2E 64 6C 6C 00 00 00 5F 66 67 65 74 63 68
61 72 00 00

深入理解PE,手工制作64位PE程序

  • OriginalFirstThunkImport Name Table的RVA,那么我们需要找一个位置设置INT的值。INT的值是DLL导入函数的名称的RVA,也就是MessageBoxExW的RVA。计算公式为0x2000 + 0x43C - 0x400 = 0x203c。INT的值为0x203c,我们写到0x470处。OriginalFirstThunk的值为0x470的RVA,计算后为0x2000 + 0x470 - 0x400 = 0x2070
  • TimeDateStamp、ForwarderChain我们设置0就可以了。
  • Name的值是我们写入.rdataUSER32.dll的字符串地址44ch的RVA,计算一下2000h+400h-44ch = 204ch,所以此处我们填写4C 20 00 00
  • FirstThunk的取值逻辑和OriginalFirstThunk比较相似,我们需要先找到一个地址来存放IAT,然后再计算此IAT的RVA给到FirstThunk。我们选择480h处作为IAT的文件偏移。IAT的值我们可以设置为与INT的相等值,也就是为0x203cFirstThunk的值为480h的RVA,计算公式2000h+400h-480h = 2080h

深入理解PE,手工制作64位PE程序

接下来按同样的方法来写msvcrt.dll的导入表。

深入理解PE,手工制作64位PE程序

根据IAT重新写代码

​ 首先把代码放入0x200的地址偏移处

48 83 EC 38 33 C0 4C 8D 05 27 00 00 00 45 33 C9 66 89 44 24 20 48 8D 15 26 00 00 00 33 C9 FF 15 00 00 00 00 FF 15 00 00 00 00 33 C0 48 83 C4 38 C3 00

​ 这段代码可以看到FF15(call)指令后面都是0,是因为我们还未确定call调用的地址。下面我们会进行计算,但首先,我们先写入这些数据。

深入理解PE,手工制作64位PE程序

​ 在x64程序中,call指令后面需要填写IAT的RVA。有一个计算公式

call xxxx

xxxx = IAT的VA - call指令的VA - call的长度

​ 首先IATVAImageBase + RVA = 140000000h + 2080h = 140002080h

call指令本身的地址0x14000101Ecall的长度为固定值6。

xxxx = 140002080h - 0x14000101E - 6 = 0x105C

_fgetchar的地址用同样的方法计算,结果为0x1076

更新我们的代码

48 83 EC 38 33 C0 4C 8D 05 27 00 00 00 45 33 C9 66 89 44 24 20 48 8D 15 26 00 00 00 33 C9 FF 15 5C 10 00 00 FF 15 76 10 00 00 33 C0 48 83 C4 38 C3 00

深入理解PE,手工制作64位PE程序

​ 现在我们已经可以运行程序,只是弹窗没有字符串,接下来我们添加字符串。

520h的文件偏移处,写入两个宽字符串。

深入理解PE,手工制作64位PE程序

lea指令后面要写入字符串的RVA,和上面的call指令找地址的公式差不多。

我这里直接给出计算过程。

140002120h - 140001006h - 7h = 1113h
14000212eh - 140001015h - 7h = 1112h

再次更新我们的代码

48 83 EC 38 33 C0 4C 8D 05 13 11 00 00 45 33 C9 66 89 44 24 20 48 8D 15 12 11 00 00 33 C9 FF 15 5C 10 00 00 FF 15 76 10 00 00 33 C0 48 83 C4 38 C3 00

深入理解PE,手工制作64位PE程序

深入理解PE,手工制作64位PE程序

引用

  1. 从0手工构造64位PE并手工进行加壳

  2. A dive into the PE file format - PE file structure - Part 4: Data Directories, Section Headers and Sections

  3. 第13章:PE文件格式(2) – IAT、INT文章来源地址https://www.toymoban.com/news/detail-477422.html

到了这里,关于深入理解PE,手工制作64位PE程序的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【C语言深入】深入理解程序的预处理过程

    我们平时所写的每一个.c文件都会经过编译和连接的过程之后才会形成一个可执行程序: 今天我们就来详细的看看编译和连接这两个过程的具体细节。 程序的翻译环境与执行环境 在ANSI C的任何一种实现中,存在两个不同的环境。 第1种是翻译环境,在这个环境中源代码被转换

    2023年04月08日
    浏览(46)
  • 深入理解python虚拟机:程序执行的载体——栈帧

    栈帧(Stack Frame)是 Python 虚拟机中程序执行的载体之一,也是 Python 中的一种执行上下文。每当 Python 执行一个函数或方法时,都会创建一个栈帧来表示当前的函数调用,并将其压入一个称为调用栈(Call Stack)的数据结构中。调用栈是一个后进先出(LIFO)的数据结构,用于管

    2023年04月25日
    浏览(38)
  • 深入理解MapReduce:使用Java编写MapReduce程序【上进小菜猪】

    📬📬我是上进小菜猪,沈工大软件工程专业,爱好敲代码,持续输出干货。 MapReduce是一种用于处理大规模数据集的并行编程模型。由于其高效性和可扩展性,MapReduce已成为许多大型互联网公司处理大数据的首选方案。在本文中,我们将深入了解MapReduce,并使用Java编写一个简

    2024年02月11日
    浏览(47)
  • Ventoy 多合一启动盘制作工具神器 - 将多个系统 Win/PE/Linux 镜像装在1个U盘里

    最近很多操作系统都纷纷发布了新版本,比如 Windows 11、Ubuntu、Deepin、优麒麟、CentOS、Debian 等等,对喜欢玩系统的人来说绝对是盛宴。 不过一般用 Rufus 等工具,一个 U 盘往往只能制作成一个系统的启动盘/安装盘,想要增加另一款系统,只能得重新刻录一遍,每次都要格式化

    2024年02月09日
    浏览(48)
  • “深入理解Spring Boot:构建高效、可扩展的Java应用程序“

    标题:深入理解Spring Boot:构建高效、可扩展的Java应用程序 摘要:Spring Boot是一个用于构建Java应用程序的开源框架,它提供了一种简单且高效的方式来创建独立的、生产级别的应用程序。本文将深入探讨Spring Boot的核心概念和特性,并通过示例代码展示如何使用Spring Boot构建

    2024年02月15日
    浏览(62)
  • PE文件感染程序设计(PE病毒)

    本文主要是记录一次PE病毒设计入门实验,查看了很多帖子,总也找不到系统的指导。也是出于记录一次具体的实验流程,给后来摸索的但是没有思路的朋友们一点点思路。 遍历当前目录,将所有的可执行文件列出来 对这些可执行文件逐个判断是否被感染过,如果感染过,则

    2024年02月07日
    浏览(37)
  • 深入理解JVM虚拟机第十三篇:详解JVM中的程序计数器

    😉😉 学习交流群: ✅✅1:这是孙哥suns给大家的福利! ✨✨2:我们免费分享Netty、Dubbo、k8s、Mybatis、Spring...应用和源码级别的视频资料 🥭🥭3:QQ群:583783824   📚📚  工作微信:BigTreeJava 拉你进微信群,免费领取! 🍎🍎4:本文章内容出自上述:Spring应用课程!💞💞

    2024年02月08日
    浏览(56)
  • 《深入理解计算机系统(CSAPP)》第3章 程序的机器级表示 - 学习笔记

    写在前面的话:此系列文章为笔者学习CSAPP时的个人笔记,分享出来与大家学习交流,目录大体与《深入理解计算机系统》书本一致。因是初次预习时写的笔记,在复习回看时发现部分内容存在一些小问题,因时间紧张来不及再次整理总结,希望读者理解。 《深入理解计算机

    2024年02月07日
    浏览(60)
  • Spring-2-深入理解Spring 注解依赖注入(DI):简化Java应用程序开发

      掌握纯注解开发依赖注入(DI)模式 学习使用纯注解进行第三方Bean注入 问题导入 思考:如何使用注解方式将Bean对象注入到类中 1.1 使用@Autowired注解开启自动装配模式(按类型) 说明:不管是使用配置文件还是配置类,都必须进行对应的Spring注解包扫描才可以使用。@Autowired默

    2024年02月14日
    浏览(58)
  • “深入理解Spring Boot:构建独立、可扩展的企业级应用程序的最佳实践“

    标题:深入理解Spring Boot:构建独立、可扩展的企业级应用程序的最佳实践 摘要:Spring Boot是一个强大的框架,可以帮助开发人员快速构建独立、可扩展的企业级应用程序。本文将深入探讨Spring Boot的核心概念和最佳实践,并通过示例代码演示其用法。 正文: 什么是Spring Bo

    2024年02月14日
    浏览(63)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包