驱动开发:内核解析PE结构节表
在笔者上一篇文章《驱动开发:内核解析PE结构导出表》介绍了如何解析内存导出表结构,本章将继续延申实现解析PE结构的PE头,PE节表等数据,总体而言内核中解析PE结构与应用层没什么不同,在上一篇文章中LyShark封装实现了KernelMapFile()内存映射函数,在之后的章节中这个函数会被多次用到,为了减少代码冗余,后期文章只列出重要部分,读者可以自行去前面的文章中寻找特定的片段。
Windows NT 系统中可执行文件使用微软设计的新的文件格式,也就是至今还在使用的PE格式,PE文件的基本结构如下图所示:

在PE文件中,代码,已初始化的数据,资源和重定位信息等数据被按照属性分类放到不同的Section(节区/或简称为节)中,而每个节区的属性和位置等信息用一个IMAGE_SECTION_HEADER结构来描述,所有的IMAGE_SECTION_HEADER结构组成了一个节表(Section Table),节表数据在PE文件中被放在所有节数据的前面.
上面PE结构图中可知PE文件的开头部分包括了一个标准的DOS可执行文件结构,这看上去有些奇怪,但是这对于可执行程序的向下兼容性来说却是不可缺少的,当然现在已经基本不会出现纯DOS程序了,现在来说这个IMAGE_DOS_HEADER结构纯粹是历史遗留问题。
DOS头结构解析: PE文件中的DOS部分由MZ格式的文件头和可执行代码部分组成,可执行代码被称为DOS块(DOS stub),MZ格式的文件头由IMAGE_DOS_HEADER结构定义,在C语言头文件winnt.h中有对这个DOS结构详细定义,如下所示:
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // DOS的头部
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; // 指向了PE文件的开头(重要)
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
在DOS文件头中,第一个字段e_magic被定义为MZ,标志着DOS文件的开头部分,最后一个字段e_lfanew则指明了PE文件的开头位置,现在来说除了第一个字段和最后一个字段有些用处,其他字段几乎已经废弃了,这里附上读取DOS头的代码。
void DisplayDOSHeadInfo(HANDLE ImageBase)
{
PIMAGE_DOS_HEADER pDosHead = NULL;
pDosHead = (PIMAGE_DOS_HEADER)ImageBase;
printf("DOS头: %x\n", pDosHead->e_magic);
printf("文件地址: %x\n", pDosHead->e_lfarlc);
printf("PE结构偏移: %x\n", pDosHead->e_lfanew);
}
PE头结构解析: 从DOS文件头的e_lfanew字段向下偏移003CH的位置,就是真正的PE文件头的位置,该文件头是由IMAGE_NT_HEADERS结构定义的,定义结构如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // PE文件标识字符
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
如上PE文件头的第一个DWORD是一个标志,默认情况下它被定义为00004550h也就是P,E两个字符另外加上两个零,而大部分的文件属性由标志后面的IMAGE_FILE_HEADER和IMAGE_OPTIONAL_HEADER32结构来定义,我们继续跟进IMAGE_FILE_HEADER这个结构:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // 运行平台
WORD NumberOfSections; // 文件的节数目
DWORD TimeDateStamp; // 文件创建日期和时间
DWORD PointerToSymbolTable; // 指向符号表(用于调试)
DWORD NumberOfSymbols; // 符号表中的符号数量
WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HANDLER32结构的长度
WORD Characteristics; // 文件的属性 exe=010fh dll=210eh
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
继续跟进 IMAGE_OPTIONAL_HEADER32 结构,该结构体中的数据就丰富了,重要的结构说明经备注好了:
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion; // 连接器版本
BYTE MinorLinkerVersion;
DWORD SizeOfCode; // 所有包含代码节的总大小
DWORD SizeOfInitializedData; // 所有已初始化数据的节总大小
DWORD SizeOfUninitializedData; // 所有未初始化数据的节总大小
DWORD AddressOfEntryPoint; // 程序执行入口RVA
DWORD BaseOfCode; // 代码节的起始RVA
DWORD BaseOfData; // 数据节的起始RVA
DWORD ImageBase; // 程序镜像基地址
DWORD SectionAlignment; // 内存中节的对其粒度
DWORD FileAlignment; // 文件中节的对其粒度
WORD MajorOperatingSystemVersion; // 操作系统主版本号
WORD MinorOperatingSystemVersion; // 操作系统副版本号
WORD MajorImageVersion; // 可运行于操作系统的最小版本号
WORD MinorImageVersion;
WORD MajorSubsystemVersion; // 可运行于操作系统的最小子版本号
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; // 内存中整个PE映像尺寸
DWORD SizeOfHeaders; // 所有头加节表的大小
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve; // 初始化时堆栈大小
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes; // 数据目录的结构数量
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
IMAGE_DATA_DIRECTORY数据目录列表,它由16个相同的IMAGE_DATA_DIRECTORY结构组成,这16个数据目录结构定义很简单仅仅指出了某种数据的位置和长度,定义如下:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // 数据起始RVA
DWORD Size; // 数据块的长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
上方的结构就是PE文件的重要结构,接下来将通过编程读取出PE文件的开头相关数据,读取这些结构也非常简单代码如下所示。
// 署名权
// right to sign one's name on a piece of work
// PowerBy: LyShark
// Email: me@lyshark.com
NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
DbgPrint("hello lyshark.com \n");
NTSTATUS status = STATUS_SUCCESS;
HANDLE hFile = NULL;
HANDLE hSection = NULL;
PVOID pBaseAddress = NULL;
UNICODE_STRING FileName = { 0 };
// 初始化字符串
RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");
// 内存映射文件
status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);
if (!NT_SUCCESS(status))
{
return 0;
}
// 获取PE头数据集
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
PIMAGE_FILE_HEADER pFileHeader = &pNtHeaders->FileHeader;
DbgPrint("运行平台: %x\n", pFileHeader->Machine);
DbgPrint("节区数目: %x\n", pFileHeader->NumberOfSections);
DbgPrint("时间标记: %x\n", pFileHeader->TimeDateStamp);
DbgPrint("可选头大小 %x\n", pFileHeader->SizeOfOptionalHeader);
DbgPrint("文件特性: %x\n", pFileHeader->Characteristics);
DbgPrint("入口点: %p\n", pNtHeaders->OptionalHeader.AddressOfEntryPoint);
DbgPrint("镜像基址: %p\n", pNtHeaders->OptionalHeader.ImageBase);
DbgPrint("镜像大小: %p\n", pNtHeaders->OptionalHeader.SizeOfImage);
DbgPrint("代码基址: %p\n", pNtHeaders->OptionalHeader.BaseOfCode);
DbgPrint("区块对齐: %p\n", pNtHeaders->OptionalHeader.SectionAlignment);
DbgPrint("文件块对齐: %p\n", pNtHeaders->OptionalHeader.FileAlignment);
DbgPrint("子系统: %x\n", pNtHeaders->OptionalHeader.Subsystem);
DbgPrint("区段数目: %d\n", pNtHeaders->FileHeader.NumberOfSections);
DbgPrint("时间日期标志: %x\n", pNtHeaders->FileHeader.TimeDateStamp);
DbgPrint("首部大小: %x\n", pNtHeaders->OptionalHeader.SizeOfHeaders);
DbgPrint("特征值: %x\n", pNtHeaders->FileHeader.Characteristics);
DbgPrint("校验和: %x\n", pNtHeaders->OptionalHeader.CheckSum);
DbgPrint("可选头部大小: %x\n", pNtHeaders->FileHeader.SizeOfOptionalHeader);
DbgPrint("RVA 数及大小: %x\n", pNtHeaders->OptionalHeader.NumberOfRvaAndSizes);
ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
ZwClose(hSection);
ZwClose(hFile);
Driver->DriverUnload = UnDriver;
return STATUS_SUCCESS;
}
运行如上这段代码,即可解析出ntdll.dll模块的核心内容,如下图所示;

接着来实现解析节表,PE文件中的所有节的属性定义都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排列而成,每个结构邮过来描述一个节,节表总被存放在紧接在PE文件头的地方,也即是从PE文件头开始偏移为00f8h的位置处,如下是节表头部的定义。
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize; // 节区尺寸
} Misc;
DWORD VirtualAddress; // 节区RVA
DWORD SizeOfRawData; // 在文件中对齐后的尺寸
DWORD PointerToRawData; // 在文件中的偏移
DWORD PointerToRelocations; // 在OBJ文件中使用
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; // 节区属性字段
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
解析节表也很容易实现,首先通过pFileHeader->NumberOfSections获取到节数量,然后循环解析直到所有节输出完成,这段代码实现如下所示。
// 署名权
// right to sign one's name on a piece of work
// PowerBy: LyShark
// Email: me@lyshark.com
NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
DbgPrint("hello lyshark.com \n");
NTSTATUS status = STATUS_SUCCESS;
HANDLE hFile = NULL;
HANDLE hSection = NULL;
PVOID pBaseAddress = NULL;
UNICODE_STRING FileName = { 0 };
// 初始化字符串
RtlInitUnicodeString(&FileName, L"\\??\\C:\\Windows\\System32\\ntdll.dll");
// 内存映射文件
status = KernelMapFile(FileName, &hFile, &hSection, &pBaseAddress);
if (!NT_SUCCESS(status))
{
return 0;
}
// 获取PE头数据集
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBaseAddress;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((PUCHAR)pDosHeader + pDosHeader->e_lfanew);
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders);
PIMAGE_FILE_HEADER pFileHeader = &pNtHeaders->FileHeader;
DWORD NumberOfSectinsCount = 0;
// 获取区块数量
NumberOfSectinsCount = pFileHeader->NumberOfSections;
DWORD64 *difA = NULL; // 虚拟地址开头
DWORD64 *difS = NULL; // 相对偏移(用于遍历)
difA = ExAllocatePool(NonPagedPool, NumberOfSectinsCount*sizeof(DWORD64));
difS = ExAllocatePool(NonPagedPool, NumberOfSectinsCount*sizeof(DWORD64));
DbgPrint("节区名称 相对偏移\t虚拟大小\tRaw数据指针\tRaw数据大小\t节区属性\n");
for (DWORD temp = 0; temp<NumberOfSectinsCount; temp++, pSection++)
{
DbgPrint("%10s\t 0x%x \t 0x%x \t 0x%x \t 0x%x \t 0x%x \n",
pSection->Name, pSection->VirtualAddress, pSection->Misc.VirtualSize,
pSection->PointerToRawData, pSection->SizeOfRawData, pSection->Characteristics);
difA[temp] = pSection->VirtualAddress;
difS[temp] = pSection->VirtualAddress - pSection->PointerToRawData;
}
ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
ZwClose(hSection);
ZwClose(hFile);
Driver->DriverUnload = UnDriver;
return STATUS_SUCCESS;
}
运行驱动程序,即可输出ntdll.dll模块的节表信息,如下图;

驱动开发:内核解析PE结构节表的更多相关文章
- PE知识复习之PE的节表
PE知识复习之PE的节表 一丶节表信息,PE两种状态.以及重要两个成员解析. 确定节表位置: DOS + NT头下面就是节表. 确定节表数量: 节表数量在文件头中存放着.可以准确知道节表有多少个. 节 ...
- 【驱动】网卡驱动·linux内核网络分层结构
Preface Linux内核对网络驱动程序使用统一的接口,并且对于网络设备采用面向对象的思想设计. Linux内核采用分层结构处理网络数据包.分层结构与网络协议的结构匹配,既能简化数据包处理流程 ...
- 滴水 10/13号完成 打印出DOS PE头 节表 开源
#include<stdio.h> #include<Windows.h> int szie2; #pragma warning(disable : 4996) LPVOID ...
- [PE结构]导入表与IAT表
导入表的结构导入表的结构 typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for termi ...
- Windows驱动开发-内核常用内存函数
搞内存常用函数 C语言 内核 malloc ExAllocatePool memset RtlFillMemory memcpy RtlMoveMemory free ExFreePool
- PE节表详细分析
目录 PE节表详细分析 0x00 前言 0x01 PE节表分析 节表结构 节表数量 节表名字 节表大小 节位置 节表属性 0x02 代码编写 PE节表详细分析 0x00 前言 上一篇文章我们学习了PE ...
- 手写PE结构解析工具
PE格式是 Windows下最常用的可执行文件格式,理解PE文件格式不仅可以了解操作系统的加载流程,还可以更好的理解操作系统对进程和内存相关的管理知识,而有些技术必须建立在了解PE文件格式的基础上,如 ...
- 关于pe结构
每一种操作系统它最重要的格式就是它的可执行文件格式, 因为操作系统就是为了支持这些文件而生成的,内核里面有很多机制,也是配合这种文件格式设计的. 换句话说,这种文件格式也是适合操作系统设计的. 比如: ...
- 【PE结构】由浅入深PE基础学习-菜鸟手动查询导出表、相对虚拟地址(RVA)与文件偏移地址转换(FOA)
0 前言 此篇文章想写如何通过工具手查导出表.PE文件代码编程过程中的原理.文笔不是很好,内容也是查阅了很多的资料后整合出来的.希望借此加深对PE文件格式的理解,也希望可以对看雪论坛有所贡献.因为了解 ...
- 修改记事本PE结构弹计算器Shellcode
目录 修改记事本PE结构弹计算器Shellcode 0x00 前言 0x01 添加新节 修改节数量 节表位置 添加新节表信息 0x02 添加弹计算器Shellcode 修改代码 0x03 修改入口点 ...
随机推荐
- 第一章 1.1.1节 Kubeadm安装K8S高可用集群
1.1 安装前必读 请不要使用带中文的服务器和克隆的虚拟机. 生产环境建议使用二进制的方式安装. 文档中的IP地址要更换成自己的IP地址,要谨记!!! 1.2 基本环境配置 kubeadm安装方式自1 ...
- 微软 New Bing AI 申请与使用保姆级教程(免魔法)
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 大家好,我是小彭. 最近的 AI 技术实在火爆,从 OpenAI 的 ChatGPT,到微软的 New Bi ...
- 端口转发、Http Tunnel、内网穿透
原文链接:https://www.yuque.com/tec-nine/architecture/mgxc71 SSH 命令帮助 命令行选项有: -a 禁止转发认证代理的连接. -A 允许转发认证代理 ...
- OpenJudge 1.8编程基础之多维数组
04:错误探测 1.描述 给定n*n由0和1组成的矩阵,如果矩阵的每一行和每一列的1的数量都是偶数,则认为符合条件.你的任务就是检测矩阵是否符合条件,或者在仅改变一个矩阵元素的情况下能否符合条件.&q ...
- zookeeper重启,线上微服务全部掉线,怎么回事?
注册中心zookeeper被重启,线上微服务全部掉线,怎么回事?! 最近因为一次错误的运维操作,导致线上注册中心zk被重启.而zk重启后发现所有线上微服务开始不断掉线,造成了持续30分钟的P0故障. ...
- Laravel 代码开发最佳实践(持续更新)
我们这里要讨论的并不是 Laravel 版的 SOLID 原则(想要了解更多 SOLID 原则细节查看这篇文章)亦或是设计模式,而是 Laravel 实际开发中容易被忽略的最佳实践. 内容概览 单一职 ...
- React框架使用
一:使用Vite创建React项目 二:React中组件使用 import React, { Component, useState } from "react"; //使用cla ...
- 基于SqlSugar的开发框架循序渐进介绍(28)-- 快速构建系统参数管理界面
在参照一些行业系统软件的时候,发现一个做的挺不错的系统功能-系统参数管理,相当于把任何一个基础的系统参数碎片化进行管理,每次可以读取一个值进行管理,这样有利于我们快速的处理业务需求,是一个挺好的功能. ...
- mysql 自动挂掉
今天在看后台的时候,发现登录不上去了,登录页面是可以访问,但是就是登录不上去,上了后台看了一下,说mysql连接超时,然后我重启了一下服务器,发现依然报mysql的错误,我尝试连接mysql, 报了一 ...
- 高性能、快响应!火山引擎 ByteHouse 物化视图功能及入门介绍
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 物化视图是指将视图的计算结果存储在数据库中的一种技术.当用户执行查询时,数据库会直接从已经预计算好的结果中获取数据 ...