摘要:本文将探索内核中解析PE文件的相关内容。

本文分享自华为云社区《驱动开发:内核PE结构VA与FOA转换》,作者: LyShark 。

本章将探索内核中解析PE文件的相关内容,PE文件中FOA与VA、RVA之间的转换也是很重要的,所谓的FOA是文件中的地址,VA则是内存装入后的虚拟地址,RVA是内存基址与当前地址的相对偏移,本章还是需要用到封装的KernelMapFile()映射函数,在映射后对其PE格式进行相应的解析,并实现转换函数。

首先先来演示一下内存VA地址与FOA地址互相转换的方式,通过使用WinHEX打开一个二进制文件,打开后我们只需要关注如下蓝色注释为映像建议装入基址,黄色注释为映像装入后的RVA偏移。

通过上方的截图结合PE文件结构图我们可得知0000158B为映像装入内存后的RVA偏移,紧随其后的00400000则是映像的建议装入基址,为什么是建议而不是绝对?别急后面慢来来解释。

通过上方的已知条件我们就可以计算出程序实际装入内存后的入口地址了,公式如下:

VA(实际装入地址) = ImageBase(基址) + RVA(偏移) => 00400000 + 0000158B = 0040158B

找到了程序的OEP以后,接着我们来判断一下这个0040158B属于那个节区,以.text节区为例,下图我们通过观察区段可知,第一处橙色位置00000B44 (节区尺寸),第二处紫色位置00001000 (节区RVA),第三处00000C00 (文件对齐尺寸),第四处00000400 (文件中的偏移),第五处60000020 (节区属性)。

得到了上方text节的相关数据,我们就可以判断程序的OEP到底落在了那个节区中,这里以.text节为例子,计算公式如下:

虚拟地址开始位置:节区基地址 + 节区RVA => 00400000 + 00001000 = 00401000
虚拟地址结束位置:text节地址 + 节区尺寸 => 00401000 + 00000B44 = 00401B44

经过计算得知 .text 节所在区间(401000 - 401B44) 你的装入VA地址0040158B只要在区间里面就证明在本节区中,此处的VA地址是在401000 - 401B44区间内的,则说明它属于.text节。

经过上面的公式计算我们知道了程序的OEP位置是落在了.text节,此时你兴致勃勃的打开x64DBG想去验证一下公式是否计算正确不料,这地址根本不是400000开头啊,这是什么鬼?

上图中出现的这种情况就是关于随机基址的问题,在新版的VS编译器上存在一个选项是否要启用随机基址(默认启用),至于这个随机基址的作用,猜测可能是为了防止缓冲区溢出之类的烂七八糟的东西。

为了方便我们调试,我们需要手动干掉它,其对应到PE文件中的结构为 IMAGE_NT_HEADERS -> IMAGE_OPTIONAL_HEADER -> DllCharacteristics 相对于PE头的偏移为90字节,只需要修改这个标志即可,修改方式 x64:6081 改 2081 相对于 x86:4081 改 0081 以X86程序为例,修改后如下图所示。

经过上面对标志位的修改,程序再次载入就能够停在0040158B的位置,也就是程序的OEP,接下来我们将通过公式计算出该OEP对应到文件中的位置。

.text(节首地址) = ImageBase + 节区RVA => 00400000 + 00001000 = 00401000
VA(虚拟地址) = ImageBase + RVA(偏移) => 00400000 + 0000158B = 0040158B
RVA(相对偏移) = VA - (.text节首地址) => 0040158B - 00401000 = 58B
FOA(文件偏移) = RVA + .text节对应到文件中的偏移 => 58B + 400 = 98B

经过公式的计算,我们找到了虚拟地址0040158B对应到文件中的位置是98B,通过WinHEX定位过去,即可看到OEP处的机器码指令了。

接着我们来计算一下.text节区的结束地址,通过文件的偏移加上文件对齐尺寸即可得到.text节的结束地址400+C00= 1000,那么我们主要就在文件偏移为(98B - 1000)在该区间中找空白的地方,此处我找到了在文件偏移为1000之前的位置有一段空白区域,如下图:

接着我么通过公式计算一下文件偏移为0xF43的位置,其对应到VA虚拟地址是多少,公式如下:

.text(节首地址) = ImageBase + 节区RVA => 00400000 + 00001000 = 00401000
VPK(实际大小) = (text节首地址 - ImageBase) - 实际偏移 => 401000-400000-400 = C00
VA(虚拟地址) = FOA(.text节) + ImageBase + VPK => F43+400000+C00 = 401B43

计算后直接X64DBG跳转过去,我们从00401B44的位置向下全部填充为90(nop),然后直接保存文件。

再次使用WinHEX查看文件偏移为0xF43的位置,会发现已经全部替换成了90指令,说明计算正确。

到此文件偏移与虚拟偏移的转换就结束了,那么这些功能该如何实现呢,接下来将以此实现这些转换细节。

FOA转换为VA: 首先来实现将FOA地址转换为VA地址,这段代码实现起来很简单,如下所示,此处将dwFOA地址0x84EC00转换为对应内存的虚拟地址。

// 署名权
// 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\\ntoskrnl.exe");
// 内存映射文件
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;
DWORD64 dwFOA = 0x84EC00;
DWORD64 ImageBase = pNtHeaders->OptionalHeader.ImageBase;
DWORD NumberOfSectinsCount = pNtHeaders->FileHeader.NumberOfSections;
DbgPrint("镜像基址 = %p | 节表数量 = %d \n", ImageBase, NumberOfSectinsCount);
for (int each = 0; each < NumberOfSectinsCount; each++)
{
DWORD64 PointerRawStart = pSection[each].PointerToRawData; // 文件偏移开始位置
DWORD64 PointerRawEnds = pSection[each].PointerToRawData + pSection[each].SizeOfRawData; // 文件偏移结束位置
// DbgPrint("文件开始偏移 = %p | 文件结束偏移 = %p \n", PointerRawStart, PointerRawEnds);
if (dwFOA >= PointerRawStart && dwFOA <= PointerRawEnds)
{
DWORD64 RVA = pSection[each].VirtualAddress + (dwFOA - pSection[each].PointerToRawData); // 计算出RVA
DWORD64 VA = RVA + pNtHeaders->OptionalHeader.ImageBase; // 计算出VA
DbgPrint("FOA偏移 [ %p ] --> 对应VA地址 [ %p ] \n", dwFOA, VA);
}
}
ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
ZwClose(hSection);
ZwClose(hFile);
Driver->DriverUnload = UnDriver;
return STATUS_SUCCESS;
}

运行效果如下所示,此处之所以出现两个结果是因为没有及时返回,一般我们取第一个结果就是最准确的;

VA转换为FOA: 将VA内存地址转换为FOA文件偏移,代码与如上基本保持一致。

// 署名权
// 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\\ntoskrnl.exe");
// 内存映射文件
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;
DWORD64 dwVA = 0x00007FF6D3389200;
DWORD64 ImageBase = pNtHeaders->OptionalHeader.ImageBase;
DWORD NumberOfSectinsCount = pNtHeaders->FileHeader.NumberOfSections;
DbgPrint("镜像基址 = %p | 节表数量 = %d \n", ImageBase, NumberOfSectinsCount);
for (DWORD each = 0; each < NumberOfSectinsCount; each++)
{
DWORD Section_Start = ImageBase + pSection[each].VirtualAddress; // 获取节的开始地址
DWORD Section_Ends = ImageBase + pSection[each].VirtualAddress + pSection[each].Misc.VirtualSize; // 获取节的结束地址
DbgPrint("Section开始地址 = %p | Section结束地址 = %p \n", Section_Start, Section_Ends);
if (dwVA >= Section_Start && dwVA <= Section_Ends)
{
DWORD RVA = dwVA - pNtHeaders->OptionalHeader.ImageBase; // 计算RVA
DWORD FOA = pSection[each].PointerToRawData + (RVA - pSection[each].VirtualAddress); // 计算FOA DbgPrint("VA偏移 [ %p ] --> 对应FOA地址 [ %p ] \n", dwVA, FOA);
}
}
ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
ZwClose(hSection);
ZwClose(hFile);
Driver->DriverUnload = UnDriver;
return STATUS_SUCCESS;
}

运行效果如下所示,此处没有出现想要的结果是因为我们当前的VA内存地址并非实际装载地址,仅仅是PE磁盘中的地址,此处如果换成内存中的PE则可以提取出正确的结果;

RVA转换为FOA: 将相对偏移地址转换为FOA文件偏移地址,此处仅仅只是多了一步pNtHeaders->OptionalHeader.ImageBase + dwRVARVA转换为VA的过程其转换结果与VA转FOA一致。

// 署名权
// 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\\ntoskrnl.exe");
// 内存映射文件
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;
DWORD64 dwRVA = 0x89200;
DWORD64 ImageBase = pNtHeaders->OptionalHeader.ImageBase;
DWORD NumberOfSectinsCount = pNtHeaders->FileHeader.NumberOfSections;
DbgPrint("镜像基址 = %p | 节表数量 = %d \n", ImageBase, NumberOfSectinsCount);
for (DWORD each = 0; each < NumberOfSectinsCount; each++)
{
DWORD Section_Start = pSection[each].VirtualAddress; // 计算RVA开始位置
DWORD Section_Ends = pSection[each].VirtualAddress + pSection[each].Misc.VirtualSize; // 计算RVA结束位置
if (dwRVA >= Section_Start && dwRVA <= Section_Ends)
{
DWORD VA = pNtHeaders->OptionalHeader.ImageBase + dwRVA; // 得到VA地址
DWORD FOA = pSection[each].PointerToRawData + (dwRVA - pSection[each].VirtualAddress); // 得到FOA
DbgPrint("RVA偏移 [ %p ] --> 对应FOA地址 [ %p ] \n", dwRVA, FOA);
}
}
ZwUnmapViewOfSection(NtCurrentProcess(), pBaseAddress);
ZwClose(hSection);
ZwClose(hFile);
Driver->DriverUnload = UnDriver;
return STATUS_SUCCESS;
}

运行效果如下所示;

点击关注,第一时间了解华为云新鲜技术~

详解驱动开发中内核PE结构VA与FOA转换的更多相关文章

  1. 详解WebService开发中四个常见问题(2)

    详解WebService开发中四个常见问题(2)   WebService开发中经常会碰到诸如WebService与方法重载.循环引用.数据被穿该等等问题.本文会给大家一些很好的解决方法. AD:WO ...

  2. 详解WebService开发中四个常见问题(1)

    详解WebService开发中四个常见问题(1)   WebService开发中经常会碰到诸如WebService与方法重载.循环引用.数据被穿该等等问题.本文会给大家一些很好的解决方法. AD:WO ...

  3. 详解Android开发中Activity的四种launchMode

    Activity栈主要用于管理Activity的切换.当使用Intent跳转至某个目标Activity,需要根据目标Activity的加载模式来加载. Activity一共有以下四种launchMod ...

  4. 驱动开发:内核特征码扫描PE代码段

    在笔者上一篇文章<驱动开发:内核特征码搜索函数封装>中为了定位特征的方便我们封装实现了一个可以传入数组实现的SearchSpecialCode定位函数,该定位函数其实还不能算的上简单,本章 ...

  5. 驱动开发:内核中实现Dump进程转储

    多数ARK反内核工具中都存在驱动级别的内存转存功能,该功能可以将应用层中运行进程的内存镜像转存到特定目录下,内存转存功能在应对加壳程序的分析尤为重要,当进程在内存中解码后,我们可以很容易的将内存镜像导 ...

  6. 驱动开发:内核遍历进程VAD结构体

    在上一篇文章<驱动开发:内核中实现Dump进程转储>中我们实现了ARK工具的转存功能,本篇文章继续以内存为出发点介绍VAD结构,该结构的全程是Virtual Address Descrip ...

  7. 超全详解Java开发环境搭建

    摘自:https://www.cnblogs.com/wangjiming/p/11278577.html 超全详解Java开发环境搭建   在项目产品开发中,开发环境搭建是软件开发的首要阶段,也是必 ...

  8. 驱动开发:内核R3与R0内存映射拷贝

    在上一篇博文<驱动开发:内核通过PEB得到进程参数>中我们通过使用KeStackAttachProcess附加进程的方式得到了该进程的PEB结构信息,本篇文章同样需要使用进程附加功能,但这 ...

  9. 驱动开发:内核枚举DpcTimer定时器

    在笔者上一篇文章<驱动开发:内核枚举IoTimer定时器>中我们通过IoInitializeTimer这个API函数为跳板,向下扫描特征码获取到了IopTimerQueueHead也就是I ...

  10. 驱动开发:内核枚举PspCidTable句柄表

    在上一篇文章<驱动开发:内核枚举DpcTimer定时器>中我们通过枚举特征码的方式找到了DPC定时器基址并输出了内核中存在的定时器列表,本章将学习如何通过特征码定位的方式寻找Windows ...

随机推荐

  1. 谱图论:Laplacian算子及其谱性质

    1 Laplacian 算子 给定无向图\(G=(V, E)\),我们在上一篇博客<谱图论:Laplacian二次型和Markov转移算子>中介绍了其对应的Laplacian二次型: \[ ...

  2. 快速展示原型之Minimal API开发

    Minimal API官网地址: https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/minimal-apis/security?vi ...

  3. 栩栩如生,音色克隆,Bert-vits2文字转语音打造鬼畜视频实践(Python3.10)

    诸公可知目前最牛逼的TTS免费开源项目是哪一个?没错,是Bert-vits2,没有之一.它是在本来已经极其强大的Vits项目中融入了Bert大模型,基本上解决了VITS的语气韵律问题,在效果非常出色的 ...

  4. RLChina2022公开课-博弈论

    纯博弈: 单纯的动机组合,离散的集合 混合博弈: 加入了概率论,以百分比的概率执行不同的的动机.,概率分布 零和博弈.合作博弈.协同博弈 扩展博弈和非完美信息 扩展博弈.贝叶斯博弈 纳什均衡 任何一位 ...

  5. keycloak~为keycloak-services项目添加第三方模块(首创)

    我们在对keycloak框架中的核心项目keycloak-services进行二次开发过程中,发现了一个问题,当时有这种需求,在keycloak-services中需要使用infinispan缓存,我 ...

  6. C/C++ extern “C“ 的问题

    声明 文章中的部分代码引用来在: https://blog.csdn.net/u012234115/article/details/43272441 场景 今天在CSDN中看到了一篇关于 extern ...

  7. Boruvka 算法

    Boruvka算法解决某些问题超级好用. 这些问题形如,给你n个点,每个点有点权,任意两个点之间有边权,边权为两个点权用过某种计算方式得出. 求最小生成树. 通常用 \(O(log n)\) 的时间可 ...

  8. python列表之部分列表——切片

    目录 切片 全索引 半索引 负数索引 遍历切片 复制列表 切片 全索引 我们平常可能需要使用一个列表中的子列表,也就是部分元素,这个时候我们就需要用到切片这个功能啦 要创建切片,就如同range()函 ...

  9. python进程状态

    思考 为啥我VS编辑器里一直在import中报错,但是写到笔记本里就可以直接用 之前俄罗斯方块也是同样问题 安装 pip install transitions 代码 from transitions ...

  10. 数据分析工具 Datainside、Power BI、帆软(FineReport)怎么选?

    Datainside.Power BI和帆软(FineReport)都是数据分析领域常用的工具,它们各自有不同的特点和适用场景.下面将会详细介绍每个工具的功能和优势,以便您进行选择. Datainsi ...