2.1 PE结构:文件映射进内存

微软技术分享
优质创作者: 编程框架技术领域
领域专家: 操作系统技术领域
2023-11-19 10:22:32

PE结构是Windows系统下最常用的可执行文件格式,理解PE文件格式不仅可以理解操作系统的加载流程,还可以更好的理解操作系统对进程和内存相关的管理知识,在任何一款操作系统中,可执行程序在被装入内存之前都是以文件的形式存放在磁盘中的,在早期DOS操作系统中,是以COM文件的格式存储的,该文件格式限制了只能使用代码段,堆栈寻址也被限制在了64KB的段中,由于PC芯片的快速发展这种文件格式极大的制约了软件的发展。

为了应对这种局面,微软的工程师们就发明了新的文件格式(EXE文件),该文件格式在代码段前面增加了文件头结构,文件头中包括各种说明数据,如程序的入口地址,堆栈的位置,重定位表等,显然可执行文件的格式是操作系统工作方式的真实写照,不同的系统之间文件格式千差万别,从而导致不同系统中的可执行文件无法跨平台运行。

PE结构包含了各类结构体,DOS头,PE标识,文件头,可选头,目录结构,节表,导入表,导出表,重定位表,资源表等等,要想掌握PE结构首相要对这些表有一个整体上的认识,Windows NT 系统中可执行文件使用微软设计的新的文件格式,也就是至今还在使用的PE格式,PE文件的基本结构如下图所示:

在PE文件中,代码,已初始化的数据,资源和重定位信息等数据被按照属性分类放到不同的Section(节区/或简称为节)中,而每个节区的属性和位置等信息用一个IMAGE_SECTION_HEADER结构来描述,所有的IMAGE_SECTION_HEADER结构组成了一个节表(Section Table),节表数据在PE文件中被放在所有节数据的前面。

在PE文件中将同样属性的数据分类放在一起是为了统一描述这些数据装入内存后的页面属性,由于数据是按照属性在节中放置的,不同用途但是属性相同的数据可能被放在同一个节中,PE文件头被放置在节和节表的前面,上面介绍的是真正的PE文件,为了兼容以前的DOS系统,所以保留了DOS的文件格式,接下来笔者将带大家从最基本的读入文件开始依次实现对PE文件的解析,并使用C语言实现一个PeView结构解析器。

在解析PE文件之前,我们首先要做的则是将PE文件从磁盘中读入到内存,有两种方式可以实现,一种是通过ReadFile函数将完整的数据读入内存,该方法会消耗更多的内存资源这里并不推荐使用,第二种方式则是采用映射的模式,所谓的映射则是将一个磁盘中的部分数据读入内存,当需要使用该片区域时由操作系统动态的装载一部分,该方式也是笔者推荐的一种实现模式;

一般来说映射文件的流程是,使用CreateFile()打开一个磁盘文件,接着使用CreateFileMapping()函数创建文件的内存映像,最后使用MapViewOfFile()读取映射中的内存并返回一个句柄,后面的程序就可以通过该句柄操作打开后的文件。

CreateFile

用来创建或打开文件的API函数,它可以接受一个文件名作为输入参数,并返回一个文件句柄。文件句柄是用来标识打开的文件的唯一标识符,后续对该文件的操作需要使用这个句柄。下面是CreateFile函数的原型:

HANDLE CreateFile(
    LPCTSTR lpFileName,          // 文件名或路径
    DWORD dwDesiredAccess,       // 访问权限
    DWORD dwShareMode,           // 共享模式
    LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全属性
    DWORD dwCreationDisposition, // 创建选项
    DWORD dwFlagsAndAttributes,  // 文件属性
    HANDLE hTemplateFile         // 模板文件句柄
);

其中,各个参数的含义如下:

  • lpFileName:指向null结尾字符串的指针,该字符串是文件名或文件的路径。
  • dwDesiredAccess:一个32位的AccessMask值,用来表示对文件的访问权限。
  • dwShareMode: 一个32位的ShareMode值,它表示其他进程可以如何访问文件。
  • lpSecurityAttributes:指向SECURITY_ATTRIBUTES结构体的指针,表示安全属性。
  • dwCreationDisposition:一个32位的值,它表示对文件的创建选项如何操作。
  • dwFlagsAndAttributes:一个32位的值,用来指定文件的属性和标志。
  • hTemplateFile:可选的模板文件句柄,用来将文件属性/属性设置为其它文件的属性/属性。

函数返回值为一个文件对象的句柄,如果函数执行失败,则返回INVALID_HANDLE_VALUE(即-1)。

CreateFileMapping

用来创建文件的内存映像的API函数。它可以将一个文件映射到内存中,这样我们就可以像访问内存一样访问文件。这个函数需要传入一个文件句柄以及一个映像的大小。它返回一个句柄,表示创建的内存映像。下面是CreateFileMapping函数的原型:

HANDLE CreateFileMapping(
    HANDLE hFile,                     // 文件句柄
    LPSECURITY_ATTRIBUTES lpAttributes, // 安全属性
    DWORD flProtect,                    // 内存保护属性
    DWORD dwMaximumSizeHigh,           // 文件映像的高32位字节大小
    DWORD dwMaximumSizeLow,            // 文件映像的低32位字节大小
    LPCTSTR lpName                     // 映像名
);

其中,各个参数的含义如下:

  • hFile:要映射到内存中的文件的句柄
  • lpAttributes:指向SECURITY_ATTRIBUTES结构体的指针,它描述内存映射对象的安全性,如果为NULL,则内存映射对象不可继承。
  • flProtect:一组标志位,它们指定内存映射区域的内存保护属性;
  • dwMaximumSizeHigh:文件映像的高32位字节大小
  • dwMaximumSizeLow:文件映像的低32位字节大小
  • lpName:映像名,可以为NULL;而且,如果该参数不为空,映像对象就成为本地系统对象,可以通过名字查找映像。

函数返回值为一个文件映射对象的句柄,如果函数执行失败,返回值为NULL。

MapViewOfFile

用来读取映射中的内存的API函数。它需要传入一个映像的句柄以及一个偏移量,用来指定从哪个位置开始读取内存。该函数返回一个指向映射内存的指针,我们可以使用它来读取或修改映射内存中的数据。下面是MapViewOfFile函数的原型:

LPVOID MapViewOfFile(
    HANDLE hFileMappingObject,  // 文件映射对象的句柄
    DWORD dwDesiredAccess,      // 访问权限
    DWORD dwFileOffsetHigh,     // 文件偏移的高32位字节个数
    DWORD dwFileOffsetLow,      // 文件偏移的低32位字节个数
    SIZE_T dwNumberOfBytesToMap // 要映射到内存中的字节数
);

其中,各个参数的含义如下:

  • hFileMappingObject:文件映射对象的句柄,可以使用CreateFileMapping函数创建,表示要映射到内存中的文件或共享内存的句柄。
  • dwDesiredAccess:一个32位的AccessMask值,用来表示对内存的访问权限。可以设置为FILE_MAP_READ、FILE_MAP_WRITE、FILE_MAP_ALL_ACCESS等。
  • dwFileOffsetHigh:文件偏移的高32位字节个数。
  • dwFileOffsetLow:文件偏移的低32位字节个数。
  • dwNumberOfBytesToMap:要映射到内存中的字节数。

函数返回值为指向映射内存的指针,如果函数执行失败,则返回NULL。在使用完内存映像后,读者记得使用UnmapViewOfFile()函数来释放映像内存,使用CloseHandle()函数来关闭文件句柄和映像句柄,以便操作系统可以回收资源。

有了上述几个关键API函数那么实现内存映射功能将会变得很容易实现,直接来看一下如下代码,当程序运行后会自动将c://pe/x86.exe目录下的文件读入内存,并返回一个lpMapAddress文件句柄;

#include <iostream>
#include <Windows.h>
#include <ImageHlp.h>

#pragma comment(lib,"Imagehlp.lib")

// --------------------------------------------------
// 定义全局变量,来存储 DOS头部/NT头部/Section头部
// --------------------------------------------------
PIMAGE_DOS_HEADER DosHeader = nullptr;
PIMAGE_NT_HEADERS NtHeader = nullptr;
PIMAGE_FILE_HEADER FileHead = nullptr;
PIMAGE_SECTION_HEADER pSection = nullptr;

// --------------------------------------------------
// 读取并设置文件基址以及文件大小
// --------------------------------------------------
CHAR GlobalFilePath[2048] = { 0 }; // 保存文件路径
DWORD GlobalFileSize = 0;          // 定义文件大小
DWORD GlobalFileBase = 0;          // 保存文件的基地址
DWORD IsOpen = 0;                  // 设置文件是否已经打开

// --------------------------------------------------
// 打开文件操作
// --------------------------------------------------
HANDLE OpenPeFile(LPCSTR FileName)
{
    HANDLE hFile, hMapFile, lpMapAddress = NULL;

    hFile = CreateFileA(FileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE)
    {
        printf("[-] 打开文件失败 \n");
        exit(0);
    }
    GlobalFileSize = GetFileSize(hFile, NULL);
    if (GlobalFileSize != 0)
    {
        printf("[+] 已读入文件 \n");
    }

    hMapFile = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, GlobalFileSize, NULL);
    if (hMapFile == NULL)
    {
        printf("[-] 创建映射对象失败\n");
        exit(0);
    }

    lpMapAddress = MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, GlobalFileSize);
    if (lpMapAddress != NULL)
    {
        // 设置读入文件基地址
        GlobalFileBase = (DWORD)lpMapAddress;

        // 获取DOS头并判断是不是一个有效的DOS文件
        DosHeader = (PIMAGE_DOS_HEADER)GlobalFileBase;
        if (DosHeader->e_magic != IMAGE_DOS_SIGNATURE)
        {
            printf("[-] 文件不属于DOS结构 \n");
            exit(0);
        }

        // 获取 NT 头并判断是不是一个有效的PE文件
        NtHeader = (PIMAGE_NT_HEADERS)(GlobalFileBase + DosHeader->e_lfanew);
        if (NtHeader->Signature != IMAGE_NT_SIGNATURE)
        {
            printf("[-] 文件不属于PE结构 \n");
            exit(0);
        }

        // 判断是不是32位程序
        if (NtHeader->OptionalHeader.Magic != 0x010B)
        {
            printf("[-] 无法调试非32位PE文件\n");
            exit(0);
        }

        // 获取到文件头指针
        FileHead = &NtHeader->FileHeader;

        // 获取到节表头
        pSection = IMAGE_FIRST_SECTION(NtHeader);
    }

    return lpMapAddress;
}

int main(int argc, char * argv[])
{
    HANDLE BaseAddr = OpenPeFile("c://pe/x86.exe");
    printf("[+] 入口地址 = %x \n", BaseAddr);

    system("pause");
    return 0;
}
...全文
34 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
-------------------------- 我是汇编爱好者 QQ695367480 高手别找我了 我是菜鸟。 -------------------------- 内容简介: Windows环境下32位汇编语言是一种全新的编程语言。它使用与C++语言相同的API接口,不仅可以用来开发出大型的软件,而且是了解操作系统运行细节的最佳方式。本书从编写应用程序的角度,从“Hello World!”这个简单的例子开始到编写多线程、注册表和网络通信等复杂的程序,通过60多个实例逐渐深入Win32汇编语言的方方面面。本书作者罗云彬拥有十余年汇编语言编程经验,是汇编编程网站http://asm.yeah.net和汇编编程论坛http://win32asm.yeah.net的站长。本书是作者多年来编程工作的总结,适合于欲通过Win32汇编语言编写Windows程序的读者。 第1章 背景知识 1.1 Win32的软硬件平台(1) 1.1 Win32的软硬件平台(2) 1.2 Windows的特色 1.3 必须了解的东西(1) 1.3 必须了解的东西(2) 1.3 必须了解的东西(3) 1.3 必须了解的东西(4) 1.3 必须了解的东西(5) 第2章 准备编程环境 2.1 Win32可执行文件的开发过程 2.2 编译器和链接器(1) 2.2 编译器和链接器(2) 2.2 编译器和链接器(3) 2.3 创 建 资 源 2.4 make工具的用法(1) 2.4 make工具的用法(2) 2.5 获 取 资 料 2.6 构建编程环境 第3章 使用MASM 当搭建编译和对编译器的使用不再成为绊脚石的时候,初学者的问题往往集中在对Windows程序结构的迷惑上,消息驱动体系、窗口过程、与硬件隔绝的图形接口及资源文件等相对于DOS程序来说都是全新的内容,接下来的4章将深入讨论这些内容,通过这几章,读者应该开始习惯以Windows的方式考虑问题了(脑海中的DOS逐渐远去...),这就是本书的初级篇: 3.1 Win32汇编源程序的结构(1) 3.1 Win32汇编源程序的结构(2) 3.1 Win32汇编源程序的结构(3) 3.2 调用API(1) 3.2 调用API(2) 3.2 调用API(3) 3.3 标号、变量和数据结构(1) 3.3 标号、变量和数据结构(2) 3.3 标号、变量和数据结构(3) 3.3 标号、变量和数据结构(4) 3.3 标号、变量和数据结构(5) 3.4 使用子程序 3.5 高 级 语 法(1) 3.5 高 级 语 法(2) 3.6 代 码 风 格(1) 3.6 代 码 风 格(2) 第4章 第一个窗口程序 4.1 开始了解窗口(1) 4.1 开始了解窗口(2) 4.1 开始了解窗口(3) 4.2 分析窗口程序(1) 4.2 分析窗口程序(2) 4.2 分析窗口程序(3) 4.2 分析窗口程序(4) 4.2 分析窗口程序(5) 4.3 窗口间的消息互发 4.4 实 验(1) 4.4 实 验(2) 4.4 实 验(3) 第5章 使用资源 5.1 菜单和加速键(1) 5.1 菜单和加速键(2) 5.1 菜单和加速键(3) 5.1 菜单和加速键(4) 5.1 菜单和加速键(5) 5.1 菜单和加速键(6) 5.1 菜单和加速键(7) 5.2 图标和光标(1) 5.2 图标和光标(2) 5.3 位 图 5.4 对 话 框(1) 5.4 对 话 框(2) 5.4 对 话 框(3) 5.4 对 话 框(4) 5.4 对 话 框(5) 5.4 对 话 框(6) 5.4 对 话 框(7) 5.4 对 话 框(8) 5.4 对 话 框(9) 5.4 对 话 框(10) 5.4 对 话 框(11) 5.5 字符串资源/5.6 版本信息资源(1) 5.6 版本信息资源(2) 5.7 二制资源和自定义资源 第6章 定时器 6.1 定时器简介/6.2 定时器的使用(1) 6.2 定时器的使用(2) 6.3 取Windows时间 第7章 图形操作 Windows系统不像DOS系统,它的应用程序界面是规范化的,统一的界面来自大量的系统界面控件,学习这些控件就等于学习如何编写Windows界面,下面的界面篇中的两章将探讨这方面的内容: 7.1 GDI原理(1) 7.1 GDI原理(2) 7.1 GDI原理(3) 7
目录 第一篇 x86 基础 第1 章数与数据类型2 1.1 数 2 1.1.1 数字 2 1.1.2 二制数 3 1.1.3 二制数的排列 3 1.1.4 十六制数 5 1.1.5 八制数与十制数 5 1.2 数据类型 6 1.2.1 integer 数 6 1.2.2 floating-point 数. 9 1.2.3 real number(实数)与NaN(not a number) . 11 1.2.4 unsupported 编码值 14 1.2.5 浮点数精度的转换 15 1.2.6 浮点数的溢出 17 1.2.7 BCD 码 20 1.2.8 SIMD 数据 21 第2 章 x86/x64 编程基础 23 2.1 选择编译器 23 2.2 机器语言 24 2.3 Hello world 25 2.3.1 使用寄存器传递参数 26 2.3.2 调用过程 27 2.3.3 定义变量 27 2.4 16 位编程、32 位编程,以及64 位编程 28 2.4.1 通用寄存器 28 2.4.2 操作数大小 30 2.4.2 64 位模式下的内存地址 30 2.4.4 内存寻址模式 31 2.4.5 内存寻址范围 34 2.4.6 使用的指令限制 34 2.5 编程基础 34 2.5.1 操作数寻址 35 2.5.2 传送数据指令 39 2.5.3 位操作指令 45 2.5.4 算术指令 47 2.5.5 CALL 与RET 指令 48 2.5.6 跳转指令 48 2.6 编辑与编译、运行 48 第 3 章编写本书的实验例子 50 3.1 实验的运行环境 50 3.2 生成空白的映像文件 52 3.2.1 使用nasm 编译器生成 52 3.2.2 使用bximage 工具 52 3.3 设置bochs 配置文件. 53 3.4 源代码的基本结构 54 3.5 编译源代码55 3.6 映像文件内的组织 55 3.7 使用merge 工具 56 3.7.1 merge 的配置文件 57 3.7.2 执行merge 命令 57 3.8 使用U 盘启动真实机器 58 3.8.1 使用merge 工具写U 盘 58 3.8.2 使用hex 编辑软件写U 盘 59 3.9 编写boot 代码 60 3.9.1 LBA 转换为CHS 62 3.9.2 测试是否支持int 13h 扩展功能 63 3.9.3 使用int 13h 扩展读磁盘 64 3.9.4 最后看看load_module() 64 3.10 总结 66 第4 章处理器的身份 67 4.1 测试是否支持CPUID 指令 67 4.2 CPUID 指令的术语及表达 68 4.3 基本信息与扩展信息 68 4.4 处理器的型号(family,model 与stepping) 72 4.5 最大的物理地址和线性地址 73 4.6 处理器扩展状态信息74 4.6.1 探测Processor Extended State 子叶 75 4.6.2 Processor Extended State 子叶所需内存size 76 4.6.3 Processor Extended State 的保存 77 4.6.4 Processor Extended State 的恢复 78 4.7 处理器的特性 78 4.8 处理器的Cache 与TLB 信息 80 4.9 MONITOR/MWAIT 信息 83 4.10 处理器的long mode 84 第 5 章了解 Flags 85 5.1 Eflags 中的状态标志位 86 5.1.1 signed 数的运算 86 5.1.2 unsigned 数的运算 89 5.2 IOPL 标志位 90 5.3 TF 标志与RF 标志 93 5.4 NT 标志 95 5.5 AC 标志 96 5.6 VM 标志 98 5.7 eflags 寄存器的其他事项 99 第 6 章处理器的控制寄存器 101 6.1 CR8 102 6.2 CR3 103 6.3 CR0 104 6.3.1 保护模式位PE 104 6.3.2 x87 FPU 单元的执行环境 104 6.3.3 CR0.PG 控制位 108 6.3.4 CR0.CD 与CR0.NW 控制位 108 6.3.5 CR0.WP 控制位 110 6.3.6 CR0.AM 控制位 110 6.4 CR4 110 6.4.1 CR4.TSD 与CR4.PCE 控制位 110 6.4.2 CR4.DE 与CR4.MCD 控制位 111 6.4.3 CR4.OSFXSR 控制位 111 6.4.4 CR4.VMXE 与CR4.SMXE 控制位 111 6.4.5 CR4.PCIDE 与CR4.SMEP 控制位 112 6.4.6 CR4.OSXSAVE 控制位 113 6.4.7 CR4 中关于页的控制位 113 6.5 EFER 扩展功能寄存器 114 第 7 章 MSR. 116 7.1 MSR 的使用 116 7.2 MTRR 117 7.2.1 Fixed-range 区域的映射 118 7.2.2 MTRR 的功能寄存器 120 7.3 MSR 中对特殊指令的支持 124 7.3.1 支持sysenter/sysexit 指令的MSR 125 7.3.2 支持syscall/sysret 指令的MSR 126 7.3.3 支持swapgs 指令的MSR 127 7.3.4 支持monitor/mwait 指令的MSR 128 7.4 提供processor feature 管理 129 7.5 其他未列出来的MSR 129 7.6 关于MSR 一些后续说明 129 第二篇 处理器的工作模式 第8 章实地址模式 132 8.1 真实的地址 132 8.2 real mode 的编址 132 8.3 real mode 的状态 133 8.4 段基址的计算 134 8.5 第1 条执行的指令 134 8.6 实模式下的执行环境 135 8.7 实模式下的IVT 135 8.8 突破64K 段限 136 8.9 A20 地址线 137 第 9 章 SMM系统管理模式探索 138 9.1 入SMM 138 9.2 SMM 的运行环境 141 9.2.1 SMRAM 区域 141 9.2.2 SMM 执行环境的初始化 143 9.2.3 SMM 下的operand 与address 144 9.2.4 SMM 下的CS 与EIP 144 9.2.5 SMM 下的SS 与ESP 145 9.3 SMM 里的中断 145 9.4 SMI 的Back-to-Back 响应 147 9.5 SMM 里开启保护模式 147 9.6 SMM 的版本 148 9.7 I/O 指令的重启及Halt 重启 151 9.8 SMM 的退出 152 9.9 SMBASE 的重定位. 153 9.10 SMI 处理程序的初始化 154 9.11 SMM 的安全 156 9.11.1 芯片组的控制 156 9.11.2 处理器对SMRAM 空间的限制 158 9.11.3 cache 的限制 160 9.12 测试SMI 处理程序 161 第 10 章 x86/x64 保护模式体系(上) 163 10.1 x86/x64 的权限 164 10.2 保护模式下的环境 164 10.2.1 段式管理所使用的资源 165 10.2.2 paging 分页机制所使用的资源 165 10.3 物理地址的产生 166 10.4 段式管理机制 167 10.4.1 段式内存管理 168 10.4.2 段式的保护措施 168 10.5 段式管理的数据结构 169 10.5.1 Segment Selector(段选择子) 169 10.5.2 Descriptor Table(描述符表) 172 10.5.3 Segment Selector Register(段寄存器) 174 10.5.4 Segment Descriptor(段描述符) 175 10.5.5 LDT 描述符与LDT 258 10.6 开启保护模式 259 10.6.1 初始化GDT 260 10.6.2 初始化IDT. 262 10.6.3 切换到保护模式 263 第11 章 x86/x64 保护模式体系(下) 265 11.1 物理页面 265 11.1.1 处理器的最高物理地址(MAXPHYADDR) 266 11.1.2 物理页面的大小 267 11.1.3 页转换模式(Paging Mode) 268 11.2 paging 机制下使用的资源 270 11.2.1 寄存器 270 11.2.2 CPUID 查询leaf 270 11.2.3 寄存器的控制位 271 11.2.4 页转换表资源 272 11.3 32 位paging 模式(non-PAE 模式) 273 11.3.1 CR3 结构 274 11.3.2 32 位paging 模式下的PDE 结构 275 11.3.3 使用32 位paging 279 11.4 PAE paging 模式. 282 11.4.1 在Intel64 下的CR3 与PDPTE 寄存器 283 11.4.2 在AMD64 下的CR3 285 11.4.3 PAE paging 模式里的PDPTE 结构 286 11.4.4 PAE paging 模式里的PDE 结构 286 11.4.5 PAE paging 模式里的PTE 结构 288 11.4.6 使用和测试PAE paging 模式 288 11.4.7 使用和测试Execution Disable 功能 292 11.5 IA-32e pagi

6,222

社区成员

发帖
与我相关
我的任务
社区描述
微软技术社区为中国的开发者们提供一个技术干货传播平台,传递微软全球的技术和产品最新动态,分享各大技术方向的学习资源,同时也涵盖针对不同行业和场景的实践案例,希望可以全方位地帮助你获取更多知识和技能。
windowsmicrosoft 企业社区
社区管理员
  • 微软技术分享
  • 郑子铭
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

微软技术社区为中国的开发者们提供一个技术干货传播平台,传递微软全球的技术和产品最新动态,分享各大技术方向的学习资源,同时也涵盖针对不同行业和场景的实践案例,希望可以全方位地帮助你获取更多知识和技能。

予力众生,成就不凡!微软致力于用技术改变世界,助力企业实现数字化转型。

试试用AI创作助手写篇文章吧