专栏首页方亮PE文件和COFF文件格式分析——RVA和RA相互计算

PE文件和COFF文件格式分析——RVA和RA相互计算

        之前几节一直是理论性质的东西非常多。本文将会讲到利用之前的知识得出一个一个非常有用的一个应用。(转载请指明来源于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不是平行的,即差值是不同的,不能这么算的。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 朴素、Select、Poll和Epoll网络编程模型实现和分析——Epoll模型

            在阅读完《朴素、Select、Poll和Epoll网络编程模型实现和分析——Select模型》和《朴素、Select、Poll和Epoll网络编...

    方亮
  • PE文件和COFF文件格式分析——签名、COFF文件头和可选文件头1

            本文将讨论PE文件中非常重要的一部分信息。(转载请指明来源于breakSoftware的CSDN博客)

    方亮
  • 在windows程序中嵌入Lua脚本引擎--建立一个简易的“云命令”执行的系统

              在《在windows程序中嵌入Lua脚本引擎--使用VS IDE编译Luajit脚本引擎》开始处,我提到某公司被指责使用“云命令”暗杀一些软...

    方亮
  • 高级Java研发师在解决大数据问题上的一些技巧

    众所周知, Java 在处理数据量比较大的时候,加载到内存必然会导致内存溢出,而在一些数据处理中我们不得不去处理海量数据,在做数据处理中,我们常见的手段是分解,...

    风火数据
  • 通过数据驱动目标优化交互系统(CS AI)

    有效的优化对于现实世界中的交互式系统至关重要,以提供令人满意的用户体验以响应不断变化的用户行为。然而,寻找优化交互式系统的目标通常是挑战性的(例如,在面向任务的...

    小童
  • 女神背心变内衣,只需草图画几笔 | 中山+字节跳动等新研究

    前一眼还是金色长发的歪果仁姑娘,在这张图上随意勾勒个帽子形状,下一秒就变成了真实效果,还看不出人为增补痕迹。

    量子位
  • 《Objective-C-高级编程》干货三部曲(一):引用计数篇

    总结了Effective Objective-C之后,还想读一本进阶的iOS书,毫不犹豫选中了《Objective-C 高级编程》:

    用户1740424
  • 误删除libc.so.6的解决

    最近安装一个软件需要glibc-2.17。 使用ldd --version 发现系统的glibc版本为 glibc-2.12,当时没有想到更好的方法,就尝试将...

    joshua317
  • TCP服务器端和客户端建立连接 - 服务器端的回调处理

    服务器端createServer的回调函数被执行时,说明来了一个新的客户端发起的连接:

    Jerry Wang
  • Vue实现对数组、对象的深拷贝、复制

    当组件间传递对象时,由于此对象的引用类型指向的都是一个地址(除了基本类型跟null,对象之间的赋值,只是将地址指向同一个,而不是真正意义上的拷贝),如下 数组:...

    庞小明

扫码关注云+社区

领取腾讯云代金券