PE文件结构

January 30, 2019 逆向 访问: 23 次

输入表(IT)

输入表就是保存函数名和其驻留的的DLL名动态链接所需要的信息。输入表的结构是IMAGE_IMPORT_DESCRIPTOR(IID)的数组形式。

IMAGE_IMPORT_DESCRIPTOR
{
    originalFirstThunk  -> 指向输入名称表(INT)
    TimeDateStamp       -> 32位的时间标志
    ForwarderChain      -> 第一个被转向API的索引
    Name                -> DLL名字的指针
    FirstThunk          -> 指向输入地址表(IAT)
}

输入名称表(INT)

originalFirstThunk指向的是输入名称表,不可改写;该数组的元素指向一个IMAGE_IMPORT_BY_NAME结构;

IMAGE_IMPORT_BY_NAME
{
    Hint            ->本函数在其所驻留的DLL的输出表中的序号
    Name            ->含有输出函数的函数名
}

输入地址表(IAT)

FirstThunk指向的是输入地址表,是由PE装载器重写的;该数组的元素指向一个IMAGE_IMPORT_BY_NAME结构;

输入名称表和输入地址表的联系

输入名称表和输入地址表在载入内存之前存的元素是一样的,当载入内存之后,PE装载器就会先搜索originalFirstThunk,如果找到,加载程序就迭代搜索数组中的每个指针,找出每个IMAGE_IMPORT_BY_NAME结构所指向输入函数的地址。然后加载器用函数真正入口地址来代替由FirstThunk原来指向的值;

从图中可以看出来,FirstThunk原本的值已经被函数的真是地址所覆盖了,相对应的第一个函数0x77e5acd9就是该函数的真是地址。
重写前

重写后

实例(PE.exe)

通过loadPE查看该程序的区段基本信息

根据VOffset和ROffset可以求出来相对偏移地址为0x2000-0x600=0x1a00;
指向输入表地址的指针在距离PE文件有0x80处:

可以得到指向输入表地址的指针是0x00002040,这是一个虚拟地址,将其转换成基地址:0x2040-0x1a00=0x640

可以看到阴影的部分就是输入表,该程序的输入表中有两个IID数组,第三个全为0的元素代表着数组到此结束,下面一表格的形式展现出来

originalFirstThunk TimeDateStamp ForwarderChain Name FirstThunk
指向输入名称表(INT) 32位的时间标志 第一个被转向API的索引 DLL名字的指针 指向输入地址表(IAT)
208c 0000 0000 2174 2010
207c 0000 0000 21b4 2000
0000 0000 0000 0000 0000

INT指向的元素是IMAGE_IMPORT_BY_NAME结构:

上图阴影部分就是输入名称表,每一个指针都指向一个IMAGE_IMPORT_BY_NAME结构:

File Offset Hint Apiname
0x710 0x019b LoadIconA
…… …… ……

输出表

当一个DLL函数能被EXE或者是另一个DLL文件使用时,它就被“输出了”。输出信息被保存在输出表中,包括输出函数名、输出序数、函数入口地址。

IMAGE_EXPORT_DIRECTORY STRUCT
{
    Characteristics
    TimeDateStamp
    MajorVersion
    MinorVersion
    Name                        ->模块的真实名称
    Base                        ->基数;序数减去这个基数就是函数地址数组的索引值
    NumberOfFunctions           ->AddressOfFunctions阵列中的元素个数
    NumberOfNames               ->AddressOfNames阵列中的元素个数
    AddressOfFunctions          ->指向函数地址数组
    AddressOfNames              ->函数名字的指针地址
    AddressOfNameOrdinals       ->指向输出序列号数组
}

输出表实例分析(DllDemo.DLL)

数据目录表的第一个成员指向输出表,该指针的具体位置在PE文件头偏移78h处。

计算出文件偏移地址:

Name:4032h-3400h=C32h,指向Dll的名字DllDemo.DLL。
AddressOfNames:402C-3400=C2C;C2C中存放的地址是403E(403E-3400=C3E),指向函数名MsgBox
使用GetProcAddress()来查找DllDemo.DLL里的API函数MsgBox,首先PE装载器获得输出函数名称表(ENT)的起始地址,进而知道这个数组里有一个条目,它对名字进行二进制查找,知道发现字符串MsgBox为止,PE装载器知道MsgBox是数组的第一个条目后,加载器从输出序数表中读取相应的第一个值,这个值是MsgBox的输出序数,使用输出序数作为进入EAT的索引,就可以找到MsgBox的地址。

基址重定位

概念

简单的来说就是当PE文件被装载到虚拟内存的另一个地址中的话,链接器登记的那个地址就是错误的。那么这个时候就需要重定位表来进行调整
在PE文件中,重定位表往往单独作为一块,用“.reloc”来表示
用一个例子来说明一下,将DllDemo.DLL文件载入到IDA中,查看Msgbox函数

可以看到两个加粗的地址,如果该PE文件加载到内存的地址不是基址的话,这两个地址就是需要重定位的数据,现在假如该PE文件被映射到0x870000处的话,windows加载器就会比较基地址和实际的载入地址,计算出一个差值,加载器是将这个差值加给原来的地址上并写回原处,如图所示:

DllDemo.DLL在内存中进行定位处理后的代码:

计算一下差值发现确实是照着的

print hex(0x1b100e-0x40100e+0x402000)
out:
0x1b2000

重定位表的结构

基址重定位表位于一个.reloc区块中,寻找方法是在数据目录表的第6个成员“Base relocation Table”,基址重定位数据采用类似按页分割的方法组织,是有许多重定位块串接成的,每个块中存放4KB(0xFFFFh)的重定位信息,每个重定位数据块的大小必须以DWORD(4字节)对齐。结构如下:

IMAGE_BASE_RELOCATION STAUCT
    VirtualAddress              DWORD       ;重定位数据的开始RVA地址
    SizeOfBlock             DWORD       ;重定位块的长度
    TypeOffset                  WORD            ;重定位项数组
  • VirtualAddress:这组重定位数据的开始RVA地址。各项重定位项的地址加这个值才是该重定位项的完整RVA地址
  • SizeOfBlock:当前重定位结构的大小。因为VirtualAddress和SizeOfBlock的大小都是固定的4个字节,所以这个值减去8就是TypeOffset数组的大小
  • TypeOffset:一个数组。数组没项大小为2个字节(16位),这16位分高4为和低12位。高4位代表重定位类型,低12位是重定位地址,它与VirtualAddress相加就是指向PE映像中需要修改的地址数据的指针

常见的重定位类型表:

重定位表结构图:

实例分析(DllDemo.DLL)

用winhex打开该文件
- 首先找到指向重定位表的指针(数据目录表第6个,相对PE文件头偏移为A0/B0),将其转换为文件偏移地址为E00

- 找到E00,前四个字节是VirtualAddress,再四个字节是SizeOfBlock,后面的是重定位数据

- 分析数据块,100f、1023这两个相对地址需要重定位,转换成文件地址为60f、623

- 这两个地址分别指向402000、403030,再在OllyDbg中看一下这两个地址是否正确


- 如果将这两个地址转换成加载到基地址的地址画的,确实是一致的。

添加新评论