VS2022写汇编

  • 由于要用到vs2022编写汇编程序所以先来简单介绍一下在vs2022中如何可以设置编译汇编。先新建一个项目

image-20250701181212392

  • 然后选择新建这个类型的项目

image-20250701181246713

  • 点击Windows桌面引导后就选择如图选项

image-20250701181352226

  • 之后右键新创建的项目,选择生成依赖项--->生成自定义

image-20250701181501755

  • 勾选masm选项

image-20250701181525445

  • 现在就可以新建一个.asm后缀的文件,写入如下代码,注意下面这个汇编代码是在i386架构下的

image-20250701181557776

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.386
.model flat, stdcall
option casemap:none

extern MessageBoxA@16:proc
extern ExitProcess@4:proc

.data
msgTitle db "PE", 0
msgText db "Hello world!", 0

.code
main:
push 0
push offset msgTitle
push offset msgText
push 0
call MessageBoxA@16

push 0
call ExitProcess@4
ret
end main

  • 既然是i386架构下的汇编,我们就需要将这里设置为x86,建议debug模式

image-20250701181815141

  • 之后查看编译器是ml.exe还是ml64.exe,因为x86x64的汇编语法上会存在不同,使用ml64编译i386的程序会编译不通过。

image-20250701181930556

image-20250701182019631

  • 如果是release模式,在编译的时候就会出现这个问题

image-20250701182127025

  • 这个也比较好解决,解决方法就是在下图的命令行这一栏添加这么一句ml的汇编命令/SAFESEH:NO

image-20250701182250518

image-20250701182319450

  • 这样就可以编译通过了

image-20250701182409366

PE文件结构简述

  • 每个成熟的带有后缀名的文件比如.exe.pdf.jpg等文件都会有属于自己特有的文件头和文件尾。当我们查看或者运行这些文件的时候,操作系统并不是根据后缀名来识别这些文件类型。操作系统是通过识别这些文件的文件头和文件尾来判断这些文件具体是属于哪一类型。
  • PE,英文全称为portable executable是可移植可执行文件。
  • 对于PE可执行文件即.exe后缀的文件也是如此,PE文件也有属于自己的文件结果(文件头和文件尾),在这些文件中隐藏着一些程序运行的信息,并且一些反调试技术也与PE文件格式相关,所以在学Windows用户程序逆向之前,先要具体详细地学习一下PE文件结构。
  • PE文件头大概有如下结构:
1
2
3
4
5
6
7
8
9
10
11
IMAGE_DOS_HEADER;  			//DOS头
IMAGE_NT_HEADERS; //NT头,NT头包含了两个子结构
IMAGE_FILE_HEADER; //NT的文件头
IMAGE_OPTIONAL_HEADER; //NT的选项头文件,为可执行文件指定选项
IMAGE_SECTION_HEADER; //段头,段头与段数据的风格就是类似于Pascal风格字符串.有几个段数据,就会出现段头,
..... //段头都被放在一起
IMAGE_SECTION_HEADER; //段头
SECTION_DATA; //段数据
....
SECTION_DATA; //段数据
user data; //附加数据,属于自定义类型,不属于格式一类.
  • PE文件处于内存中的时候通常称为映像

PE文件结构

  • 上面我们已经使用VS2022汇编出了exe文件,先使用010Editor查看一下这个exe文件PE结构。

DOS头

  • MZ是一个DOS头的识别部分,当操作系统执行这个可执行文件的时候,并不是看拓展名,而是看这个文件的二进制形式,其实DOS头的标志性标识其实就是0x4D 0x5A也就是MZMZ其实是DOS最早期作者的名字缩写。
  • 而这个DOS头中我们标注CC的这边实际上是可以任意修改的、可以使用的,修改后并不影响其执行。标注CC的地方其实是为DOS文件执行的时候提供环境信息,这个环境信息包括寄存器值、可选重定位表、中断向量表、文件句柄表等
  • 最后的32位其实标明了新结构的位置(NT头的位置),其中0xC8其实就是PE结构标识符中P字母的位置。

image-20250701190415703

  • PEDOS头这边还有一段数据,其实也就是这一段数据。这一部分被称为Stub数据,通常被叫做残留数据(包含二进制指令和数据)。
  • 当该文件在DOS下执行时,其就会真正从offset=0x40这个位置开始执行,最先开始执行0x0E
  • DOS执行后就会出现这样的提示:This program cannot be run in DOS mode.

image-20250701190659291

  • 使用IDA反汇编就能看到这一段的具体指令了

image-20250701191610768

  • 接下来为了更熟悉DOS文件结构,就将DOS结构体敲一遍抄下来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef stuct _IMAGE_DOS_HEADER{
WORD e_magic; //0x00 魔数: 存储DOS头"MZ"(0x5A4D,小端序就变成0x4D5A)
WORD e_cblp; //0x02 存储最后一页剩余的字节数
WORD e_cp; //0x04 文件总页数(512字节为单位)
WORD e_crlc; //0x06 重定位表项数
WORD e_cparhdr; //0x08 头部大小(以16字节为单位)
WORD e_minalloc; //0x0A 程序最小额外分配内存(段数)
WORD e_maxalloc; //0x0C 程序最大额外分配内存(段数)
WORD e_ss; //0x0E 初始栈段地址(相对加载地址)
WORD e_sp; //0x10 初始栈指针
WORD e_csum; //0x12 校验和(通常为0,未使用)
WORD e_ip; //0x14 初始指令指针(寄存器IP的初始值)
WORD e_csl; //0x16 初始代码段地址 (相对加载地址)
WORD e_lfarlc; //0x18 重定位表偏移 (从文件头开始的偏移)
WORD e_ovno; //0x1A overlay号 (从文件头开始的偏移)(用于老式 DOS 多阶段加载)
WORD e_res[4]; //0x1C~0x24 保留字段
WORD e_oemid; //0x24 OEM标识符(用户自定义)(系统厂商自定义)
WORD e_oemindfo; //0x26 OEM信息(和e_oemid配对)
WORD e_res2[10]; //0x28~0x3C 额外保留字段
LONG e_lfanew; //0x3C 指向 PE 文件头 (NT头) 偏移
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
  • 其实这一段都是可以人为修改利用

image-20250701193847234

NT头

  • NT头有三个主要结构体。其中一个是NT标识即IMAGE_NT_HEADERS,还有俩个子结构IMAGE_FILE_HEADERIMAGE_OPTIONAL_HEADER

NT头_NT_HEADERS

  • VS中查看NT头,发现NT头定义有区分32位和64

image-20250701194501594

  • 其结构体就是这样(以32位为例子,先介绍32位的)
1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // NT头的标识4字节,"PE\x00\x00" (0x4550 小端序就是0x50 0x45 0x00 0x00)
IMAGE_FILE_HEADER FileHeader; // NT文件头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // NT的选项头文件,为可执行文件指定选项
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
  • 下面是NT文件头文件格式的16进制分布

image-20250701195743221

NT的文件头_FILE_HEADERS

  • 依然是这张图片,其中标CC的就是可以利用的。

image-20250701195743221

  • 直接查看vsIMAGE_FILE_HEADER的结构体

image-20250701200007106

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_FILE_HEADER{
WORD Machine; // 一个宏,表示该程序是什么架构的程序,包括MIPS小端序、Intel 386等不同架构的程序,之后会详细说明
WORD NumberOfSections; // 描述节区总数也就是IMAGE_SECTION_HEADER个数
DWORD TimeDateStamp; // 每个可执行文件产生的时间(可以参考,但是不可信,因为别人可以改)
DWORD PointerToSymbolTable; // 指向COFF符号表的文件偏移(距离文件开头的字节数)
DWORD NumberOfSymbols; // 符号表中包含的符号个数
WORD sizeOfOptionalHeader; // 描述后面的IMAGE_OPTIONAL_HEADER32的大小
WORD Characteristics; // 该可执行文件的属性,可以知道该程序是PE文件中的哪一种.exe、.dll、.sys
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
  • 注意如果合理修改了sizeOfOptionalHeader的值,其实程序是可以正常运行,但是程序可能不能进行动态调试,动态调试可能就会出现问题。

Machin宏定义

  • 上面FILE_HEADER中的Machine宏定义也可以在VS中能看到,某些宏定义可以经过操作组合在一起

image-20250701200837227

  • 在上图中又几个比较重要和常见的架构宏定义
1
2
3
4
#define IMAGE_FILE_MACHINE_I386              0x014c  // Intel 386架构
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM 小端序
#define IMAGE_FILE_MACHINE_AMD64 0x8664 // AMD64 (K8)
  • 介绍一下这些宏定义在的架构和类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // 去掉了重定位信息(常见于绝对地址绑定的程序)
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // 可执行映像(非obj)
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // 去掉调试行号信息
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // 去掉局部符号表信息
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // 工作集裁剪标志
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // 程序能访问大于2GB的地址空间(主要在x64系统上)
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // 用于老系统检测字节序(低位反转)
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32位程序
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // 去除调试信息
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // 来自可移动设备时,复制到交换文件运行
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // 来自网络运行时,复制到交换文件运行
#define IMAGE_FILE_SYSTEM 0x1000 // 标识该文件是系统文件(NTDLL、KERNEL32等).
#define IMAGE_FILE_DLL 0x2000 // 标识该文件是DLL类型的PE文件
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // 只能在但处理器机器上运行
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // 字节高位反转,比较少见,用于字节序检测

#define IMAGE_FILE_MACHINE_UNKNOWN 0 // 表示未知架构
#define IMAGE_FILE_MACHINE_TARGET_HOST 0x0001 // 用于宿主机而非虚拟化环境
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386架构
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS 小端序架构, 0x160为大端序架构
#define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP 0x01a3
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E 小端序
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 小端序
#define IMAGE_FILE_MACHINE_SH5 0x01a8 // SH5
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM 小端序
#define IMAGE_FILE_MACHINE_THUMB 0x01c2 // ARM Thumb/Thumb-2 小端序
#define IMAGE_FILE_MACHINE_ARMNT 0x01c4 // ARM Thumb-2 小端序
#define IMAGE_FILE_MACHINE_AM33 0x01d3 // AM33 架构
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP 0x01f1 // IBM PowerPC 架构
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE 0x0520 // Infineon
#define IMAGE_FILE_MACHINE_CEF 0x0CEF // Infineon TriCore 架构 (混合型 RISC 架构处理器)
#define IMAGE_FILE_MACHINE_EBC 0x0EBC // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64 0x8664 // AMD64 (K8)
#define IMAGE_FILE_MACHINE_M32R 0x9041 // M32R小端序
#define IMAGE_FILE_MACHINE_ARM64 0xAA64 // ARM64小端序
#define IMAGE_FILE_MACHINE_CEE 0xC0EE // CEE虚拟机生成的可执行文件

NT的选项头文件_OPTIONAL_HEADER

  • 选项头有三类32位选项头64位选项头嵌入式选项头。这里主要介绍32位选项头64位选项头
  • FILE_HEADERS倒数第3、4字节就可以得到optional header的大小,其实就是0xE0,从而得到optional head的那一块区域

image-20250701210423532

  • 先查看一下IMAGE_OPTIONAL_HEADER32的结构体

image-20250701212448754

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 这个是32位的选项头重点介绍
// 其实32位的结构和64位头结构一样,只是关于地址描述部分WORD、DWORD和ULONGLONG(无符号长整型)
typedef struct _IMAGE_OPTIONAL_HEADER{
WORD Magic; //0x00类似于PE、MZ这样的识别,有三个宏定义.IMAGE_NT_OPTIONAL_HDR_MAGIC可执行文件,IMAGE_ROM_OPTIONAL_HDR_MAGIC在ROM的文件用作固件(一般是UEFI ROM、BIOS插件模块、或者是在嵌入式设备中的),IMAGE_NT_OPTIONAL_HDR64_MAGIC
BYTE MajorLinkerVersion; // 链接器主版本号
BYTE MinorLinkerversion; // 链接器次版本号
DWORD SizeOfCode; // 当前可执行文件所包含代码的总大小
DWORD SizeOfInitializedData; // 已初始化数据的总大小 .data 、.const已初始化
DWORD SizeOfUninitializedData; // 未初始化数据的总大小 .data? 未初始化 (对应已初始化的数据程序编译的时候需要记录数据内容,对于未初始化的数据不要保留位置,只需要记录SizeOfUninitializedData即可)
// 上面这3个sizeOf都是说明性的东西,可以任意修改不影响执行,但是可以干扰调试调试器(可以用于反调试)
DWORD AddressOfEntryPoint; // 程序入口点地址,简称EP(当前入口点)。OEP(原始入口点),在加壳的时候这么称呼
DWORD BaseOfCode; // 代码基地址,程序偏移多少字节到代码段地址开头
DWORD BaseOfData; // 数据基地址,数据的起始地址
//上面程序基地址和数据基地址都是可以被修改的,这种其实也是反调试的方法之一

// 以上这个部分是三个可选头共同的
DWORD ImageBase; // (建议装载地址)程序默认的加载地址,如0x400000,方便操作系统重定位
DWORD SectionAlignment; // 映像(内存中)每个节(Section)的对齐大小,通常为0x1000
DWORD FileAlignment; // 文件中每个节的对齐大小,通常为0x200,不满足0x200大小的就会补0
WORD MajorOperatingSystemVersion; // 要求的最低操作系统主版本
WORD MinorOperatingSystemVersion; // 要求的最低操作系统次版本
WORD MajorImageVersion; // 应用程序自身主版本号(开发者填写)
WORD MinorImageVersion; // 应用程序自身次版本号
WORD MajorSubsystemVersion; // 所需子系统主版本
WORD MinorSubsystemVersion; // 所需子系统次版本
DWORD Win32VersionValue; // 通常为0,保留字段
DWORD SizeOfImage; // 映像整体大小(对齐到SectionAlignment),用于加载时分配内存,必须对齐于SectionAlignment中的数据
DWORD SizeOfHeaders; // 所有头部大小之和(DOS头+PE头+节表+可选头)
DWORD CheckSum; // 可执行文件的校验和,用户程序一般不用检查校验和,如果是系统文件驱动等就需要检查校验和,判断其是否被修改或者出错误(3环的时候能利用,0环不能)
WORD Subsystem; // 表示运行环境,GUI、Console、Native
WORD DllCharacteristics; // DLL特性标志,例如ASLR、DEP支持等,如0x40动态重定位、NX兼容等
DWORD SizeOfStackReserve; // 初始保留的栈空间大小
DWORD SizeOfStackCommit; // 初始提交(分配)的栈空间
DWORD SizeOfHeapReserve; // 初始保留的堆大小
DWORD SizeOfHeapCommit; // 初始提交的堆大小
DWORD LoaderFlags; // 保留字段,一般为0,可以自己写个值,在程序运行的时候读这个值
DWORD NumberOfRvaAndSizes; // DataDirectory中有效条目数量(通常为16)
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //一个数组,每项是一个数据目录项,包括导入表、导出表、资源表、异常表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
// DataDirectory[0]位导出函数表DLL,DataDirectory[1]位导入函数表,DataDirectory[2]位资源
// DataDirectory[3]异常处理信息
  • 对于DWORD FileAlignment;这个文件对齐,紫色框部分才是代码的部分,但是由于文件需要0x200字节对齐,所以剩余部分需要补充\x00

image-20250702012000846

image-20250702200111516

SizeOfHeaders

  • SizeOfHeaders指的是可执行文件的头部大小,从SizeOfHeaders中可以看出来PE文件头大小为0x400,这个0x400需要与File_aligment这里面的数据对齐(也就是需要满足0x200的整数倍),由于我们的Headers大于0x200所以需要对齐到0x400也就是Headers头结束后,填充\x00当做无效数据

image-20250702200521803

Subsystem

  • 主要介绍一下Subsystem中一些宏,这个字段可以改,但是需要合理的改。

image-20250702201441763

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define IMAGE_SUBSYSTEM_UNKNOWN              0   // Unknown subsystem.
#define IMAGE_SUBSYSTEM_NATIVE 1 // Image doesn't require a subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2 // 主要接触:Windows图形用户界面.Image runs in the Windows GUI subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3 // 主要接触:Windows控制台界面.Image runs in the Windows character subsystem.
#define IMAGE_SUBSYSTEM_OS2_CUI 5 // image runs in the OS/2 character subsystem.
#define IMAGE_SUBSYSTEM_POSIX_CUI 7 // image runs in the Posix character subsystem.
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8 // 主要接触:Windows内核驱动 image is a native Win9x driver.
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9 // Image runs in the Windows CE subsystem.
#define IMAGE_SUBSYSTEM_EFI_APPLICATION 10 //
#define IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER 11 //
#define IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER 12 //
#define IMAGE_SUBSYSTEM_EFI_ROM 13
#define IMAGE_SUBSYSTEM_XBOX 14
#define IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION 16
#define IMAGE_SUBSYSTEM_XBOX_CODE_CATALOG 17

DllCharacteristics

  • 关于Dll的属性描述
1
2
3
4
5
0x0001 	保留字段
0x0002 保留字段
0x0004 保留字段
0x0008 保留字段
0x2000 A WDM driver 基于WDM的一个驱动程序

堆栈保留和提交

1
2
3
4
5
DWORD SizeOfStackReserve;			// 初始保留的栈空间大小,程序能向操作系统申请的最大栈内存
DWORD SizeOfStackCommit; // 初始提交(分配)的栈空间,程序一开始运行时需要先申请的栈大小
DWORD SizeOfHeapReserve; // 初始保留的堆大小,程序能向操作系统申请的最大堆内存
DWORD SizeOfHeapCommit; // 初始提交的堆大小,程序一开始运行时需要先申请的堆大小
// 该字段可以修改,但是需要合理修改。保留需要大于提交的大小,保留需要不超过OS的内存限制

IMAGE_DATA_DIRECTORY

  • 数据目录PE文件中的一个重要结构
1
2
3
4
5
// IMAGE_DATA_DIRECTORY的结构如下
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // 表的虚拟地址
DWORD Size; // 表的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
  • 数据目录数组对应的索引已经规定好了具体的数据表,下面就是具体数据表的宏定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // 重点:导出表Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // 重点:导入表Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // 重点:资源描述Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // 重点:重定位表Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // 重点:TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // 过时了:Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // 过时了:Bound Import Directory in headers
// 早期Window是为了装载常用API,就将API地址固定了,但是由于OS更新比较快,每次更新地址会变,就出现问题,所以之后该文件就废弃了
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // 重点:IAT表 Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor

image-20250703183738734

image-20250703183922862

导入表(IMAGE_DIRECTORY_ENTRY_IMPORT)

  • 最复杂的一个数据目录。

  • 导入表的作用如下:

    • 当程序运行的时候,有时需要调用外部接口,尤其是操作系统API。有点类似于ELF文件中的got表
    • 当操作系统装载我们的可执行文件时,操作系统首先会分析可执行文件需要哪些库。接着分析需要这些库的哪些函数,将需要调用的函数地址,填入到操作系统和编译器约定好的位置。
    • 编译器在编译好代码的时候,运行该程序,该程序在调用操作系统的相关API的时候就会到约定好的地址对这个API进行间接的反问。
    • 而操作系统与应用程序约定好填入API的位置被称为Import Address Table简称IAT,调用API的地址是不固定的。
  • 导入表需要记录俩个东西函数对应的动态库动态库中的具体函数(这两者的关系就相当于学生和班级这一数据关系,一个动态库对应多个班级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
IMAGE_DIRECTORY_ENTRY_IMPORT;	 	//导入表
IMAGE_IMPORY_DESCRIPTOR; //导入表子结构
IMAGE_THUNK_DATA; //导入表子结构
IMAGE_IMPORT_BY_NAME; // 导入表子结构

// 导入表中其实最先是IMAGE_IMPORY_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)
} DUMMYUNIONNAME;
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;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
  • 在编译时,操作系统装载库函数的过程:
    • 首先遍历动态库信息,获得目标的库函数信息
    • 然后读取函数信息表,将目标函数填入对应IAT结构体的中

image-20250703212813870

  • 使用xdbg查看内存

image-20250703213544177

  • 通过表的内存偏移找到真实导入表

image-20250703214019750

image-20250703214044597

image-20250703214109670

  • 以下面张图片来将一下具体的过程:
    • 首先遍历第一个动态库的信息,先会找到库名称(图中阴影部分第一行最后3A 22 00 00 就是存储库函数名称的地址)
    • 接着操作系统就会载入相应的库,如果载入失败,就会弹出缺少.dll依赖

image-20250703221053131

  • 接着会根据24 22 00 00找到对应偏移为24 22 00 00的这个地方其实是一个数组,存放着被引用函数的信息,但是这里由于我们只使用了第一个库中的第一个函数,所以这里的数组长度就为1。
  • 如果多个的话,就会有很多项直到00 00 00 00 结尾,之后2c 22 00 00 就会定位到_IMAGE_IMPORT_BY_NAME结构体

image-20250703220526477

  • 之后就是这样的一个数据,该数据其实就是如下结构体
    • WORD Hint不是很重要,仅供参考
    • CHAR Name[1]其实是一个可变长数组,存放的是函数的名称
    • 这样我们就拿到了函数名称,拿到函数名就可以得到函数对应的地址,得到函数对应地址后

image-20250703215556833

image-20250703215953330

image-20250703220229783

1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

导出表(IMAGE_DIRECTORYT_ENTRY_EXPORT)

导入地址表(Import Address Table)

NT_OPTIONAL_HDR64

ROM_OPTIONAL_HDR

段头

  • PE文件头的这个位置其实就是段头的位置,先来看一下每个段头的结构

image-20250702204723216

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 如果要确定段头,需要先找数据目录
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 0x00段名如:.text 、.rdata、.data、.rsrc、.reloc,类似于注释,描述该节是做什么的(段名称可以重复,也可以乱写)
union {
DWORD PhysicalAddress; // 仅用于某些平台,现在常使用 VirtualSize
DWORD VirtualSize; // 0x08 节的实际大小(而不是节头的大小)
} Misc;
DWORD VirtualAddress; // 0xC 相对虚拟地址
DWORD SizeOfRawData; // 0x10 需要载入内存的数据总大小,文件描述与NT可选头中FileAlignment对应
DWORD PointerToRawData; // 0x14 节数据在文件中的偏移地址
DWORD PointerToRelocations; // 0x18 重定位的信息 (为兼容其他系统所做)
DWORD PointerToLinenumber; // 0x1c 行信息 (为兼容其他系统所做)
WORD NumberOfRelocations; // 0x20 重定位总数 (为兼容其他系统所做)
WORD NumberOfLinenumbers; // 0x24 行总数 (为兼容其他系统所做)
DWORD Characteristics; // 0x28 内存属性,可读、可写、可执行、可共享是位描述W、R、E、S
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
  • 使用.text段来分析一下这个结构体,结合着下面内存映射的来解读一下.text段载入内存
    • 首先是名为.text段的一个段,会从文件偏移0x400的位置将数据载入到内存相对地址为0x1000的位置,载入实际有效的数据为0x27个字节(实际上载入总数是0x200字节即SizeOfRawData的大小)
    • 并且内存要满足与NT可选头中的SectionAlignment对齐
    • VirtualSize这里面的数据是半说明性质的(可以适当修改)
    • 然后给该内存段可执行、可读的权限

image-20250702205544398

image-20250702212827539

image-20250702210926335

VirtualAddress

  • 存储着是文件中的各个Section段载入到内存后的起始偏移地址。所以在内存中的地址应该是这样计算。并且文件载入到内存中每一段的大小需要与NT可选头的SectionAlignment里面的值对齐
1
绝对地址 = VirtualAddress + Image_Base(但是Image_Base不可信这里需要注意)

Characteristics

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
R 可读   
W 可写
E 可执行
S 可共享
#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 // Section can be discarded.
#define IMAGE_SCN_MEM_NOT_CACHED 0x04000000 // Section is not cachable.
#define IMAGE_SCN_MEM_NOT_PAGED 0x08000000 // Section is not pageable.
#define IMAGE_SCN_MEM_SHARED 0x10000000 // Section is shareable.
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 // Section is executable.
#define IMAGE_SCN_MEM_READ 0x40000000 // Section is readable.
#define IMAGE_SCN_MEM_WRITE 0x80000000 // Section is writeable.
// 可读可写可执行不可共享其实就是将这些位坐或运算 0x20000000 | 0x40000000 | 0x80000000
// Characteristics字段可以改,但是需要适当的改


//下面的是 Characteristic的其他宏定义,了解即可
// Section characteristics.
//
// IMAGE_SCN_TYPE_REG 0x00000000 // Reserved.
// IMAGE_SCN_TYPE_DSECT 0x00000001 // Reserved.
// IMAGE_SCN_TYPE_NOLOAD 0x00000002 // Reserved.
// IMAGE_SCN_TYPE_GROUP 0x00000004 // Reserved.
#define IMAGE_SCN_TYPE_NO_PAD 0x00000008 // Reserved.
// IMAGE_SCN_TYPE_COPY 0x00000010 // Reserved.

#define IMAGE_SCN_CNT_CODE 0x00000020 // Section contains code.
#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 // Section contains initialized data.
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // Section contains uninitialized data.

#define IMAGE_SCN_LNK_OTHER 0x00000100 // Reserved.
#define IMAGE_SCN_LNK_INFO 0x00000200 // Section contains comments or some other type of information.
// IMAGE_SCN_TYPE_OVER 0x00000400 // Reserved.
#define IMAGE_SCN_LNK_REMOVE 0x00000800 // Section contents will not become part of image.
#define IMAGE_SCN_LNK_COMDAT 0x00001000 // Section contents comdat.
// 0x00002000 // Reserved.
// IMAGE_SCN_MEM_PROTECTED - Obsolete 0x00004000
#define IMAGE_SCN_NO_DEFER_SPEC_EXC 0x00004000 // Reset speculative exceptions handling bits in the TLB entries for this section.
#define IMAGE_SCN_GPREL 0x00008000 // Section content can be accessed relative to GP
#define IMAGE_SCN_MEM_FARDATA 0x00008000
// IMAGE_SCN_MEM_SYSHEAP - Obsolete 0x00010000
#define IMAGE_SCN_MEM_PURGEABLE 0x00020000
#define IMAGE_SCN_MEM_16BIT 0x00020000
#define IMAGE_SCN_MEM_LOCKED 0x00040000
#define IMAGE_SCN_MEM_PRELOAD 0x00080000

#define IMAGE_SCN_ALIGN_1BYTES 0x00100000 //
#define IMAGE_SCN_ALIGN_2BYTES 0x00200000 //
#define IMAGE_SCN_ALIGN_4BYTES 0x00300000 //
#define IMAGE_SCN_ALIGN_8BYTES 0x00400000 //
#define IMAGE_SCN_ALIGN_16BYTES 0x00500000 // Default alignment if no others are specified.
#define IMAGE_SCN_ALIGN_32BYTES 0x00600000 //
#define IMAGE_SCN_ALIGN_64BYTES 0x00700000 //
#define IMAGE_SCN_ALIGN_128BYTES 0x00800000 //
#define IMAGE_SCN_ALIGN_256BYTES 0x00900000 //
#define IMAGE_SCN_ALIGN_512BYTES 0x00A00000 //
#define IMAGE_SCN_ALIGN_1024BYTES 0x00B00000 //
#define IMAGE_SCN_ALIGN_2048BYTES 0x00C00000 //
#define IMAGE_SCN_ALIGN_4096BYTES 0x00D00000 //
#define IMAGE_SCN_ALIGN_8192BYTES 0x00E00000 //
// Unused 0x00F00000
#define IMAGE_SCN_ALIGN_MASK 0x00F00000

#define IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000 // Section contains extended relocations.

无效数据

  • 无效数据是为了对齐,所以在PE文件头写完了DOS头NT头段头之后如果程序偏移还没有到0x400,此时就需要使用无效数据\x00进行偏移操作。
  • 之后从0x400开始其实就是存放着段数据

image-20250701210829931

段数据

附加数据

VS2022查看PE结构体

  • 新建一个项目,编写如下代码,鼠标选中IMAGE_DOS_HEADER就可以跳转到对应结构体的定义中。
1
2
3
4
#include "stdafx.h"
#include <windows.h>
#include <stdlib.h>
IMAGE_DOS_HEADER;

image-20250701183153887

image-20250701183329984

地址转换

玩转导入表

进阶

  1. 尝试自己编写一个小程序,试着修改一些无关值,查看程序能不能正常执行。
  2. 尝试编写一个像CFF这样的界面,可以读取查看PE文件头结构,还可以修改
  3. 尝试修改DOS那边的stub部分,使得某个简单的程序能在DOS环境下也能实现功能。
  4. 尝试添加一个节头,该节用于注入操作(添加节头即可,注入的代码后面再说)三步走
    • 第一步添加新节描述
    • 第二步添加新节数据
    • 修改NT可选头中的SizeOfImage、修改节表总数即修改NT头中的NumberOfSections的个数
  5. 添加一个没有文件映射的节头即未初始化数据,对应高级语言中的未初始化的全局变量,以及汇编中的.data?
  6. 添加一个变形的未初始化区,即在文件中的数据小于等于0x200,但是映射到内存映射0x2000字节大小
  7. 写一个类似CFF中能自动计算文件偏移,并且可以自动定位数据的程序,可以作为2.程序中的功能
  8. 尝试修改导入表,进行.dllhook操作