之前几节一直是理论性质的东西非常多。本文将会讲到利用之前的知识得出一个一个非常有用的一个应用。(转载请指明来源于breaksoftware的csdn博客)
首先我们说下磁盘上A.exe文件和正在内存中运行的A.xe之间的关系。当我们双击A.exe后,A.exe会运行起来。我们在任务管理器中可以看到A.exe。这个过程很简单,但是双击A.exe之后系统做了什么?这个过程你是否思考过?我不准备在此处详细说这个过程,因为这个过程比较复杂。我只讲一个过程——载入。
磁盘上A.exe在硬盘上,运行时的A.exe的执行体是在内存中。从磁盘到内存,这是个载入的过程。我们查看下A.exe占用的内存和A.exe这个文件的大小,会发现,这两个大小之间不存在任何关系。一则是因为A.exe运行时会载入部分DLL导致内存变大,其次可能是A.exe运行时申请了大量的空间。还有个原因很容易被忽略掉,就是系统在加载这个A.exe时,它是分段读取该文件,分段加载的。比如文件中一个信息是1byte,系统读取该信息后出于内存对齐考虑,可能会给这个信息分配4bytes的空间。是不是感觉我们A.exe被系统加载后,数据的连续性等被破坏了?如图
是的,原来的结构是被打乱的。是不是感觉恐慌?其实不用,这种映射必定存在一定的算法。我们本文就是讨论这种算法的。
讨论这个算法前,我们先说个概念——位置。现在有GPS功能的设备越来越多,玩这个功能的人是否考虑过其中的简单的原理呢?我没有研究过它,但是我想这是天上的卫星和地上(或者空间)一些设备合作计算出来的。比如我们地上有些基站,卫星知道它们的坐标,然后通过一些数据,比如我们“相对‘A基站的距离,“相对‘B基站的距离,从而计算出我们GPS设备和这些坐标的关系,从而得出我们所在的坐标,从而知道我们的位置。可以见得计算我们位置的过程涉及到“相对”这个概念。
文件中数据位置的描述也是使用”相对“来定位的,正在运行的程序中的数据的定位也是通过”相对“来定位的。一个数据位置相对于文件头(第一个字节)的偏移我们称为相对地址(RA),内存中一个数据相对于程序开始处的偏移我们称为相对虚拟地址(RVA)。这两个概念非常重要。
那我们PE文件中对数据的表述是使用RA还是RVA呢?大体可以总结如下:如果要在内存中运行和使用的数据大部分是使用RVA描述的。如果只是文件信息,程序运行时不关心的数据使用RA描述的。我在《PE文件和COFF文件格式分析——签名、COFF文件头和可选文件头2》最后部分,说了一句话“DataDirectory保存了指向“块信息”的目录信息,其中包括偏移(除了IMAGE_DIRECTORY_ENTRY_SECURITY元素是相对文件偏移RA,其他都是相对虚拟首地址偏移RVA)和大小。”IMAGE_DIRECTORY_ENTRY_SECURITY区块保存的是文件的签名信息,在运行程序前,系统加载文件的过程是不会把这块信息加载到内存中的。还有《PE文件和COFF文件格式分析——签名、COFF文件头和可选文件头1》中介绍的IMAGE_FILE_HEADER::PointerToSymbolTable,它指向的数据是符号表,该信息也是程序运行时不关心的,所以它也是RA。程序运行时需要使用的信息的数据地址就要使用RVA来表示了,试想如果这些数据用RA表示,则程序在运行前要根据加载的情况把这些数据在内存中的RVA再算出来,这个工作量是非常大的,是非常不科学的。
在我分析PE文件时,遇到的大部分信息是RVA。于是我想查看该位置的信息,就要通过RVA计算出RA。一般来说文件的结构是比较紧凑的,这样是为了方便文件传输(想想在那个网络非常慢,硬盘那么贵的年代)。而程序的内存中的结构则相对松散些,这个并不是因为内存不值钱,而是为了执行效率,而且一般没谁会把电脑上所有保存的程序都跑起来执行吧?
一般来说,系统加载PE文件时,会先读取文件头信息,查看该文件是否可以重定向(通过判断IMAGE_FILE_HEADER::Characteristics是否包含IMAGE_FILE_RELOCS_STRIPPED)啊。如果不可以,则查看它想加载的位置(IMAGE_OPTIONAL_HEADER32(64)::ImageBase)是否被占用了,如果没有占用,或者即使被占用了但是其允许重定位,就继续读取”节信息“。关于节信息,我在《PE文件和COFF文件格式分析——节信息》中有说明。这儿我们再看下节头信息数据结构
#define IMAGE_SIZEOF_SHORT_NAME 8 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; 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;VirtualAddress保存的是映像文件中该节被加载进内存时,第一个字节相对于映像基址的偏移量。VirtualSize保存的是内存中该节的大小。这组数据和RVA关系很大。
PoiterToRawData保存的是该节第一个字节在文件中相对于文件第一个字节的偏移量。SizeOfRawData保存的是该节在文件中的大小。这组数据和RA关系很大。
那么系统加载这些节可以用下图说明
是不是还是感觉比较乱?但是注意一个现象,看线1、2是平行的,线3、4是平行的,线5、6是平行的。为什么是平行的?在《PE文件和COFF文件格式分析——节信息》一文中我介绍VirtualSize属性时这么说的“VirtualSize属性是节加载进入内存后,节在内存中的大小。如果它比SizeOfRawData大,则多余的部分是用0x00填充的。”
那么这个平行这意味着什么?这意味着如果我们可以利用”相对“这个概念。因为平行就是相对的,如1和2平行而和3不平行。现在假如RVA落在内存的SectionBStart和SectionBEnd之间,则我们可以计算出RVA于SectionBStart的偏移OffsetBSection。然后我们通过节信息找到SectionBStart在文件中的相对地址RA,最后,我们让SectionBstart+OffsetBSection就得到RVA对应的RA了。是的!算法就是如此简单
for ( VecSectionHeaderIter it = m_vecSectionHeaders.begin(); it != m_vecSectionHeaders.end(); it++ ) { if ( dwRVA >= it->VirtualAddress && dwRVA < it->VirtualAddress + it->Misc.VirtualSize ) { if ( 0 == it->SizeOfRawData ) { break; } dwRA = dwRVA - it->VirtualAddress + it->PointerToRawData; bSuc = TRUE; break; } }这儿还要说一种场景:有些PE文件坏坏的让VirtualSize为0,而实际该大小确实存在的,这个时候我们就要修正VirtualSize之后再判断了。
for ( VecSectionHeaderIter it = m_vecSectionHeaders.begin(); it != m_vecSectionHeaders.end(); it++ ) { if ( dwRVA >= it->VirtualAddress && 0 == it->Misc.VirtualSize ) { // 校正虚拟大小 DWORD dwSectionAlignment = ( EOp32 == m_eFileOpType ) ? m_OptionalHeader32.SectionAlignment : m_OptionalHeader64.SectionAlignment; DWORD dwVirtualSize = ( it->SizeOfRawData + (dwSectionAlignment - 1) ) &~(dwSectionAlignment - 1); it->Misc.VirtualSize = dwVirtualSize; if ( dwRVA < it->VirtualAddress + dwVirtualSize ) { dwRA = dwRVA - it->VirtualAddress + it->PointerToRawData; bSuc = TRUE; break; } } }以上算法是讨论了从RVA计算出RA的过程,而通过RA计算出RVA只是这样一个逆向过程。我想如果看懂了以上的原理,那么这个算法是很容易写出来的。
刚接触这块的同学可能会存在一个误区:比如他知道RVA=0x00002100,还知道这个RVA对应的RA=0x00002200。于是可能会想当然的认为该文件的RA = RVA + 0x100。于是他下次遇到RVA=0x00008000时就认为其对应的RA=RVA+0x100=0x00008100!!这个是不对的,就像0x00002200落在上图的SectionAStart和SectionAEnd之间,而0x00008000落在上图的SectionBStart和SectionBEnd之间,线1和3不是平行的,即差值是不同的,不能这么算的。