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

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

文章目录

  • 深入理解PE,手工制作64位PE程序
    • 手工构建64位PE程序
      • 制作准备
      • 创建一个空文件
      • Dos头 `IMAGE_DOS_HEADER`
      • NT头 `IMAGE_NT_HEADERS`
        • 文件头 `IMAGE_FILE_HEADER`
        • 可选头 `IMAGE_OPTIONAL_HEADER64`
      • 节区头 `IMAGE_SECTION_HEADER`
        • `.text`节区头
        • `.rdata`节区头
      • 节区信息
        • `.text`节区
        • `.rdata`节区
      • 导入表编写(重点)
      • 根据IAT重新写代码
      • 引用

手工构建64位PE程序

深入理解PE,手工制作64位PE程序_第1张图片

制作准备

软件工具准备

  • 010 Editor
  • x64dbg

目标代码

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

#include 
#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中显示

image-20230608110840145


最后捋一下,我们在程序中使用了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程序_第2张图片

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

深入理解PE,手工制作64位PE程序_第3张图片

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程序_第4张图片

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

深入理解PE,手工制作64位PE程序_第5张图片

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

image-20230608115054467

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

TimeDateStamp,我们指定0就可以。

PointerToSymbolTableNumberOfSymbols我们也指定0。

SizeOfOptionalHeader,我们指定0xF0

Characteristics,我们指定0x0022

image-20230608115510489

可选头 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程序_第6张图片

  • 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程序_第7张图片

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

深入理解PE,手工制作64位PE程序_第8张图片

节区头 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程序_第9张图片

节区信息

.text节区

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

深入理解PE,手工制作64位PE程序_第10张图片

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

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

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

.rdata节区

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


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

深入理解PE,手工制作64位PE程序_第11张图片

导入表编写(重点)

​ 我们需要在.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

image-20230609215057029

还记得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

image-20230608152554852

  • 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

image-20230609215127102

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

image-20230609215443059

根据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调用的地址。下面我们会进行计算,但首先,我们先写入这些数据。

image-20230609221547938

​ 在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程序_第12张图片

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

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

image-20230609223833390

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

image-20230609225118037

深入理解PE,手工制作64位PE程序_第13张图片

引用

  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

你可能感兴趣的:(逆向学习,杂谈,逆向,PE结构,丁真)