12558网页游戏私服论坛

 找回密码
 立即注册
游戏开服表 申请开服
游戏名称 游戏描述 开服状态 游戏福利 运营商 游戏链接
攻城掠地-仿官 全新玩法,觉醒武将,觉醒技能 每周新区 经典复古版本,长久稳定 进入游戏
巅峰新版攻 攻城掠地公益服 攻城掠地SF 新兵种、新武将(兵种) 进入游戏
攻城掠地公 散人玩家的天堂 新开 进入游戏
改版攻城掠 上线即可国战PK 稳定新区 全新改版,功能强大 进入游戏
少年江山 高福利高爆率 刚开一秒 江湖水落潜蛟龙 进入游戏
太古封魔录 开服送10亿钻石 福利多多 不用充钱也可升级 进入游戏
神魔之道 签到送元宝 稳定开新区 送豪华签到奖励 进入游戏
神奇三国 统帅三军,招揽名将 免费玩新区 激情国战,征战四方 进入游戏
龙符 三日豪礼领到爽 天天开新区 助你征战无双 进入游戏
王者之师 免费领豪华奖励 免费玩新区 6元送6888元宝 进入游戏
三国霸业 战车-珍宝-觉醒-攻城掠地SF-全新玩法 免费玩新区 攻城掠地私服 进入游戏
手游私服盒子 各类免费游戏 0.1折送海量资源 各类手游私服 进入游戏
皇家MU2 《奇迹 2:传奇》韩国网禅公司《奇迹》正统续作。 3D锁视角Mmrpg 暗黑3+传奇+流放之路+奇迹 进入游戏
查看: 360|回复: 0

PE文件笔记十四 导出表

[复制链接]

59

主题

59

帖子

128

积分

实习版主

Rank: 7Rank: 7Rank: 7

积分
128
发表于 2021-7-26 01:29:15 | 显示全部楼层 |阅读模式
继续PE系列笔记的更新
PE其它笔记索引可前去:
PE文件笔记一 PE介绍
前面在学习了关于节的各种操纵,但更之前的扩展PE头的DataDirectory中各表项的含义还没详细介绍
这次来学习DataDirectory[0]也就是导出表的详细内容
导出表

导出表作用

一个可执行程序是由多个PE文件组成
仍旧拿先前的EverEdit.exe为例,查看运行它所需的全部模块
使用OD载入EverEdit.exe,然后点击上方的e来查看全部模块


可以看到,该程序除了包含EverEdit.exe这个模块外还包含不少其它的dll(动态链接库),这些dll为程序提供一些函数
就好比MessageBoxA这个弹窗的函数就是由user32.dll这个模块提供的
以上这些模块都发挥着其作用,使得程序得以正常运行
一个程序引用哪些模块是由其导入表决定的
与导入表相对的便是导出表,导出表则是决定当前的PE文件可以或许给其它PE文件提供的函数
拿前面提到的user32.dll为例,其导出表肯定是包含MessageBoxA这个函数的
归纳一下导入表和导出表

  • 导入表:该PE文件还使用哪些PE文件
  • 导出表:该PE文件提供了哪些函数给其它PE文件
什么是导出表

导出表就是记录该PE文件提供给其它PE文件的函数的一种结构
定位导出表

定位导出表原理

在前面的笔记:PE文件笔记五 PE文件头之扩展PE头中还剩下一个DataDirectory的结构没有详细说明
DataDirectory是一个数组,每个数组成员对应一个表,如导入表、导出表、重定位表等等
回顾先前的笔记,能得到导出表对应的下标为0
宏定义值含义IMAGE_DIRECTORY_ENTRY_EXPORT0导出表即DataDirectory[0]表示导出表
接下来来详细研究一下DataDirectory数组成员的结构
先给出C语言中 该成员在扩展PE头里的定义
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];可以看到数组成员的结构为IMAGE_DATA_DIRECTORY
IMAGE_DATA_DIRECTORY

typedef struct _IMAGE_DATA_DIRECTORY {    DWORD   VirtualAddress;    DWORD   Size;} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;IMAGE_DATA_DIRECTORY成员数据宽度说明VirtualAddressDWORD(4字节)表的起始位置(RVA)SizeDWORD(4字节)表的巨细VirtualAddress

表的起始位置,是一个相对虚拟地址(RVA),不了解RVA的可以回顾先前的:PE文件笔记七 VA与FOA转换
Size

表的巨细
根据前面的分析可以得出:
IMAGE_DATA_DIRECTORY这个结构只记录 表的位置和巨细,并没有涉及表的详细结构
定位导出表流程


  • 找到扩展PE头的最后一个成员DataDirectory
  • 获取DataDirectory[0]
  • 通过DataDirectory[0].VirtualAddress得到导出表的RVA
  • 将导出表的RVA转换为FOA,在文件中定位到导出表
按流程定位导出表

要分析的实例

这次分析的程序以MyDll.dll为例(本身编写的dll,只提供了加减乘除的导出函数)
给出导出函数的定义声明
EXPORTSAdd @12 Sub @15 NONAMEMultiply @17Divide @10再给出详细的导出函数内容
int _stdcall Add(int x, int y){        return x+y;}int _stdcall Sub(int x, int y){        return x-y;}int _stdcall Multiply(int x, int y) {        return x * y;}int _stdcall Divide(int x, int y) {        return x / y;}完整的DLL源代码和DLL程序在后面的附件中,有需要可以自行取用
找到DataDirectory

使用WinHex打开MyDll.dll,先找到PE文件头的起始地址:0xF8

再数24个字节(PE文件头标记巨细+标准PE头巨细),到达扩展PE头:0xF8+24=248+24=272=0x110
然后在数224-128=96个字节(扩展PE头巨细减去DataDirectory巨细)DataDirectory巨细= _IMAGE_DATA_DIRECTORY巨细×16=8*16
DataDirectory首地址 = 扩展PE头地址+96=0x110+96=272+96=368=0x170

获取DataDirectory[0]

而导出表为DataDirectory[0],也就是从首地址开始的8个字节就是描述导出表的IMAGE_DATA_DIRECTORY
IMAGE_DATA_DIRECTORY成员值说明VirtualAddress0x00018FB0表的起始位置(RVA)Size0x00000190表的巨细得到导出表的RVA

于是得到导出表对应的RVA为:0x18FB0
RVA转换FOA

但是IMAGE_DATA_DIRECTORY中的VirtualAddress是RVA,需要将其转换成FOA
关于RVA转FOA的内容在 PE文件笔记七 VA与FOA转换中已经详细说明白,这里不再赘述
直接使用在笔记七中写的转换代码盘算出对应的FOA:
// PE.cpp : Defines the entry point for the console application.//#include #include #include #include #include //在VC6这个比较旧的情况里,没有定义64位的这个宏,需要本身定义,在VS2019中无需本身定义#define IMAGE_FILE_MACHINE_AMD64  0x8664//VA转FOA 32位//第一个参数为要转换的在内存中的地址:VA//第二个参数为指向dos头的指针//第三个参数为指向nt头的指针//第四个参数为存储指向节指针的数组UINT VaToFoa32(UINT va, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {        //得到RVA的值:RVA = VA - ImageBase        UINT rva = va - nt->OptionalHeader.ImageBase;        //输出rva        printf("rva:%X\n", rva);        //找到PE文件头后的地址 = PE文件头首地址+PE文件头巨细        UINT PeEnd = (UINT)dos->e_lfanew + sizeof(_IMAGE_NT_HEADERS);        //输出PeEnd        printf("PeEnd:%X\n", PeEnd);        //判断rva是否位于PE文件头中        if (rva < PeEnd) {                //假如rva位于PE文件头中,则foa==rva,直接返回rva即可                printf("foa:%X\n", rva);                return rva;        }        else {                //假如rva在PE文件头外                //判断rva属于哪个节                int i;                for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {                        //盘算内存对齐后节的巨细                        UINT SizeInMemory = ceil((double)max((UINT)sectionArr->Misc.VirtualSize, (UINT)sectionArr->SizeOfRawData) / (double)nt->OptionalHeader.SectionAlignment) * nt->OptionalHeader.SectionAlignment;                        if (rva >= sectionArr->VirtualAddress && rva < (sectionArr->VirtualAddress + SizeInMemory)) {                                //找到所属的节                                //输出内存对齐后的节的巨细                                printf("SizeInMemory:%X\n", SizeInMemory);                                break;                        }                }                if (i >= nt->FileHeader.NumberOfSections) {                        //未找到                        printf("没有找到匹配的节\n");                        return -1;                }                else {                        //盘算差值= RVA - 节.VirtualAddress                        UINT offset = rva - sectionArr->VirtualAddress;                        //FOA = 节.PointerToRawData + 差值                        UINT foa = sectionArr->PointerToRawData + offset;                        printf("foa:%X\n", foa);                        return foa;                }        }}int main(int argc, char* argv[]){        //创建DOS对应的结构体指针        _IMAGE_DOS_HEADER* dos;        //读取文件,返回文件句柄        HANDLE hFile = CreateFileA("C:\\Documents and Settings\\Administrator\\桌面\\user32.dll", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);        //根据文件句柄创建映射        HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, 0, 0);        //映射内容        LPVOID pFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);        //类型转换,用结构体的方式来读取        dos = (_IMAGE_DOS_HEADER*)pFile;        //输出dos->e_magic,以十六进制输出        printf("dos->e_magic:%X\n", dos->e_magic);        //创建指向PE文件头标记的指针        DWORD* peId;        //让PE文件头标记指针指向其对应的地址=DOS首地址+偏移        peId = (DWORD*)((UINT)dos + dos->e_lfanew);        //输出PE文件头标记,其值应为4550,否则不是PE文件        printf("peId:%X\n", *peId);        //创建指向可选PE头的第一个成员magic的指针        WORD* magic;        //让magic指针指向其对应的地址=PE文件头标记地址+PE文件头标记巨细+标准PE头巨细        magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));        //输出magic,其值为0x10b代表32位程序,其值为0x20b代表64位程序        printf("magic:%X\n", *magic);        //根据magic判断为32位程序还是64位程序        switch (*magic) {        case IMAGE_NT_OPTIONAL_HDR32_MAGIC:        {                printf("32位程序\n");                //确定为32位程序后,就可以使用_IMAGE_NT_HEADERS来接收数据了                //创建指向PE文件头的指针                _IMAGE_NT_HEADERS* nt;                //让PE文件头指针指向其对应的地址                nt = (_IMAGE_NT_HEADERS*)peId;                printf("Machine:%X\n", nt->FileHeader.Machine);                printf("Magic:%X\n", nt->OptionalHeader.Magic);                //创建一个指针数组,该指针数组用来存储全部的节表指针                //这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明白一个动态数组                _IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);                //创建指向块表的指针                _IMAGE_SECTION_HEADER* sectionHeader;                //让块表的指针指向其对应的地址                sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));                //计数,用来盘算块表地址                int cnt = 0;                //比较 计数 和 块表的个数,即遍历全部块表                while (cnt < nt->FileHeader.NumberOfSections) {                        //创建指向块表的指针                        _IMAGE_SECTION_HEADER* section;                        //让块表的指针指向其对应的地址=第一个块表地址+计数*块表的巨细                        section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);                        //将得到的块表指针存入数组                        sectionArr[cnt++] = section;                        //输出块表名称                        printf("%s\n", section->Name);                }                VaToFoa32(nt->OptionalHeader.ImageBase +0x18FB0,dos,nt,sectionArr);                break;        }        case IMAGE_NT_OPTIONAL_HDR64_MAGIC:        {                printf("64位程序\n");                //确定为64位程序后,就可以使用_IMAGE_NT_HEADERS64来接收数据了                //创建指向PE文件头的指针                _IMAGE_NT_HEADERS64* nt;                nt = (_IMAGE_NT_HEADERS64*)peId;                printf("Machine:%X\n", nt->FileHeader.Machine);                printf("Magic:%X\n", nt->OptionalHeader.Magic);                //创建一个指针数组,该指针数组用来存储全部的节表指针                //这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明白一个动态数组                _IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);                //创建指向块表的指针                _IMAGE_SECTION_HEADER* sectionHeader;                //让块表的指针指向其对应的地址,区别在于这里加上的偏移为_IMAGE_NT_HEADERS64                sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS64));                //计数,用来盘算块表地址                int cnt = 0;                //比较 计数 和 块表的个数,即遍历全部块表                while (cnt < nt->FileHeader.NumberOfSections) {                        //创建指向块表的指针                        _IMAGE_SECTION_HEADER* section;                        //让块表的指针指向其对应的地址=第一个块表地址+计数*块表的巨细                        section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);                        //将得到的块表指针存入数组                        sectionArr[cnt++] = section;                        //输出块表名称                        printf("%s\n", section->Name);                }                break;        }        default:        {                printf("error!\n");                break;        }        }        return 0;}关键代码:
VaToFoa32(nt->OptionalHeader.ImageBase +0x18FB0,dos,nt,sectionArr);由于先前写的函数是VA转FOA,这里得到的是RVA,于是要先用RVA+ImageBase得到VA
运行代码得到:

获得了FOA为0x79B0,也就是导出表的位置了,定位完成
导出表的结构

定位到了导出表后自然要了解导出表的结构才能解读导出表的内容
给出导出表在C语言中的结构体(在winnt.h中可以找到)

即:
typedef struct _IMAGE_EXPORT_DIRECTORY {    DWORD   Characteristics;    DWORD   TimeDateStamp;    WORD    MajorVersion;    WORD    MinorVersion;    DWORD   Name;    DWORD   Base;    DWORD   NumberOfFunctions;    DWORD   NumberOfNames;    DWORD   AddressOfFunctions;     // RVA from base of image    DWORD   AddressOfNames;         // RVA from base of image    DWORD   AddressOfNameOrdinals;  // RVA from base of image} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;结构体分析

成员数据宽度说明CharacteristicsDWORD(4字节)标记,未用TimeDateStampDWORD(4字节)时间戳MajorVersionWORD(2字节)未用MinorVersionWORD(2字节)未用NameDWORD(4字节)指向该导出表的文件名字符串BaseDWORD(4字节)导出函数起始序号NumberOfFunctionsDWORD(4字节)全部导出函数的个数NumberOfNamesDWORD(4字节)以函数名字导出的函数个数AddressOfFunctionsDWORD(4字节)导出函数地址表RVAAddressOfNamesDWORD(4字节)导出函数名称表RVAAddressOfNameOrdinalsDWORD(4字节)导出函数序号表RVACharacteristics

未使用,固定添补0
TimeDateStamp

Image时间戳的低32位。这表示链接器创建Image的日期和时间。根据系统时钟,该值以自1970年1月1日半夜(00:00:00)后经过的秒数表示
与标准PE头中的TimeDateStamp一致
MajorVersion

未使用,固定添补0
MinorVersion

MinorVersion
Name

该字段指示的地址指向了一个以&quot;\0&quot;末端的字符串,字符串记录了导出表所在的文件的最初文件名
Base

导出函数序号的起始值。DLL中第一个导出函数并不是从0开始的,某导出函数的编号即是从AddressOfFunctions开始的顺序号加上这个值。大致示意图:

如图所示,Fun1的函数编号为nBase+0=200h,Fun2的函数编号为nBase+1=201h,以此类推
NumberOfFunctions

该字段定义了文件中导出函数的总个数
NumberOfNames

在导出表中,有些函数是定义名字的,有些是没有定义名字的。该字段记录了全部定义名字函数的个数。假如这个值是0,则表示全部的函数都没有定义名字。NumbersOfNames肯定小于即是NumbersOfFuctions
AddressOfFunctions

该指针指向了全部导出函数的入口地址的起始。从入口地址开始为DWORD数组,数组的个数由NumbersOfFuctions决定
导出函数的每一个地址按函数的编号顺序依次今后排开。在内存中,可以通过函数编号来定位某个函数的地址
AddressOfNames

该值为一个指针。该指针指向的位置是一连串的DWORD值,这些值均指向了对应的定义了函数名的函数的字符串地址。这一连串的DWORD值的个数为NumberOfNames
AddressOfNameOrdinals

该值也是一个指针,与AddressOfNames是逐一对应关系
差别的是,AddressOfNames指向的是字符串的指针数组,而AddressOfNameOrdinals则指向了该函数在AddressOfFunctions中的索引值
注意:索引值数据类型为WORD,而非DWORD。该值与函数编号是两个差别的概念,两者的关系为:
索引值 = 编号 - Base
字段间关系图示


按结构分析导出表

回到先前得到的导出表的FOA,在WinHex中找到FOA:0x79B0

将对应的数据填入结构体成员中得到:
成员值说明Characteristics0X00000000标记,未用,固定为0TimeDateStamp0xFFFFFFFF时间戳MajorVersion0X0000未用,固定为0MinorVersion0X0000未用,固定为0Name0x0001900A指向该导出表的文件名字符串Base0x0000000A导出函数起始序号NumberOfFunctions0x00000008全部导出函数的个数NumberOfNames0x00000003以函数名字导出的函数个数AddressOfFunctions0x00018FD8导出函数地址表RVAAddressOfNames0x00018FF8导出函数名称表RVAAddressOfNameOrdinals0x00019004导出函数序号表RVAName

存储的值为指针,该指针为RVA,同样需要转成FOA
VaToFoa32(nt->OptionalHeader.ImageBase+0x1900A,dos,nt,sectionArr);运行程序得到结果:

用WinHex找到0x7A0A的位置

得到该导出表的文件名字 字符串为:MyDll.dll
Base

导出函数起始序号为0xA,对应十进制10
回顾一下前面导出函数的定义声明
EXPORTSAdd @12 Sub @15 NONAMEMultiply @17Divide @10不难发现,这里的base=最小的序号=min{12,15,17,10}=10
NumberOfFunctions

全部导出函数的个数为8
明显前面声明的导出函数只有4个,为什么这里显示的导出函数个数为8?
这里的NumberOfFunctions = 最大的序号减去最小的序号+1=17-10+1=8
NumberOfNames

以函数名字导出的函数个数为3,和定义声明中有名称的导出函数 数量一致
AddressOfFunctions

存储的值为指针,该指针为RVA,同样需要转成FOA
VaToFoa32(nt->OptionalHeader.ImageBase+0x18FD8,dos,nt,sectionArr);运行程序得到结果:

用WinHex找到0x79D8的位置

记录下全部导出函数的地址并转化RVA为FOA得到:
Oridinals序号(Oridinals+Base)导出函数地址(RVA)导出函数地址(FOA)0100x000113200x7201110x000000002120x000113020x7023130x000000004140x000000005150x000111EF0x5EF6160x000000007170x000111A40x5A4可以看到只有4个导出函数是有效的,和前面DLL导出声明定义一致
AddressOfNames

存储的值为指针,该指针为RVA,同样需要转成FOA
VaToFoa32(nt->OptionalHeader.ImageBase+0x18FF8,dos,nt,sectionArr);运行程序得到结果:

用WinHex找到0x79F8的位置

记录下全部导出函数名称的地址为
0x00019014
0x00019018
0x0001901F
将RVA转化为FOA:
VaToFoa32(nt->OptionalHeader.ImageBase+0x19014,dos,nt,sectionArr);VaToFoa32(nt->OptionalHeader.ImageBase+0x19018,dos,nt,sectionArr);VaToFoa32(nt->OptionalHeader.ImageBase+0x1901F,dos,nt,sectionArr);运行程序得到结果:

即得到有名称函数的名称地址为:
顺序索引RVAFOA10x190140x7A1420x190180x7A1830x1901F0x7A1F用WinHex找到对应的FOA位置

得到了各导出函数的名称为
顺序索引RVAFOA导出函数名称10x190140x7A14Add20x190180x7A18Divide30x1901F0x7A1FMultiplyAddressOfNameOrdinals

存储的值为指针,该指针为RVA,同样需要转成FOA
VaToFoa32(nt->OptionalHeader.ImageBase+0x19004,dos,nt,sectionArr);运行程序得到结果:

用WinHex找到0x7A04的位置

得到有名称函数的Ordinals
注意Oridinals的数据宽度为2个字节(WORD)
顺序索引Oridinals序号(Oridinals+Base)10x00021220x00001030x000717根据有名称函数的Oridinals结合前面得到的AddressOfFunctions和AdressOfNames,就可以得到函数的名称、函数的地址的关系
顺序索引Oridinals导出函数地址(RVA)导出函数地址(FOA)函数名称10x00020x000113020x702Add20x00000x000113200x720Divide30x00070x000111A40x5A4Multiply导出表分析完毕
由导出表获得导出函数

从前面的分析中可以得知查询导出表有两种方法:

  • 根据导出表函数名称获取导出函数地址
  • 根据导出表函数序号获取导出函数地址
函数名称获取导出函数


  • 根据导出表的函数名称去AddressOfNames指向的每个名称字串查询是否有匹配的字符串
  • 找到匹配的字符串后,根据找到的顺序索引去AddressOfNameOrdinals中找到对应的Ordinals
  • 根据前面找到的Ordinals到AddressOfFunctions中获得函数地址
图解为:

函数序号获取导出函数


  • 根据函数序号-导出表.Base获得导出函数的Ordinal
  • 根据前面找到的Ordinals到AddressOfFunctions中获得函数地址
图解为:

代码实现分析导出表

// PE.cpp : Defines the entry point for the console application.//#include #include #include #include #include //在VC6这个比较旧的情况里,没有定义64位的这个宏,需要本身定义,在VS2019中无需本身定义#define IMAGE_FILE_MACHINE_AMD64  0x8664//VA转FOA 32位//第一个参数为要转换的在内存中的地址:VA//第二个参数为指向dos头的指针//第三个参数为指向nt头的指针//第四个参数为存储指向节指针的数组UINT VaToFoa32(UINT va, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {        //得到RVA的值:RVA = VA - ImageBase        UINT rva = va - nt->OptionalHeader.ImageBase;        //输出rva        //printf("rva:%X\n", rva);        //找到PE文件头后的地址 = PE文件头首地址+PE文件头巨细        UINT PeEnd = (UINT)dos->e_lfanew + sizeof(_IMAGE_NT_HEADERS);        //输出PeEnd        //printf("PeEnd:%X\n", PeEnd);        //判断rva是否位于PE文件头中        if (rva < PeEnd) {                //假如rva位于PE文件头中,则foa==rva,直接返回rva即可                printf("foa:%X\n", rva);                return rva;        }        else {                //假如rva在PE文件头外                //判断rva属于哪个节                int i;                for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {                        //盘算内存对齐后节的巨细                        UINT SizeInMemory = ceil((double)max((UINT)sectionArr->Misc.VirtualSize, (UINT)sectionArr->SizeOfRawData) / (double)nt->OptionalHeader.SectionAlignment) * nt->OptionalHeader.SectionAlignment;                        if (rva >= sectionArr->VirtualAddress && rva < (sectionArr->VirtualAddress + SizeInMemory)) {                                //找到所属的节                                //输出内存对齐后的节的巨细                                //printf("SizeInMemory:%X\n", SizeInMemory);                                break;                        }                }                if (i >= nt->FileHeader.NumberOfSections) {                        //未找到                        printf("没有找到匹配的节\n");                        return -1;                }                else {                        //盘算差值= RVA - 节.VirtualAddress                        UINT offset = rva - sectionArr->VirtualAddress;                        //FOA = 节.PointerToRawData + 差值                        UINT foa = sectionArr->PointerToRawData + offset;                        //printf("foa:%X\n", foa);                        return foa;                }        }}//获取导出表//第一个参数为指向dos头的指针//第二个参数为指向nt头的指针//第三个参数为存储指向节指针的数组void getExportTable(_IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {        _IMAGE_DATA_DIRECTORY exportDataDirectory = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];        UINT exportAddress = VaToFoa32(exportDataDirectory.VirtualAddress+nt->OptionalHeader.ImageBase, dos, nt, sectionArr);        _IMAGE_EXPORT_DIRECTORY* exportDirectory = (_IMAGE_EXPORT_DIRECTORY*) ((UINT)dos+ exportAddress);        printf("导出函数总数:%X\n", exportDirectory->NumberOfFunctions);        printf("导出有名称的函数总数:%X\n", exportDirectory->NumberOfNames);        int i;        for (i = 0; i < exportDirectory->NumberOfNames; i++) {                printf("顺序序号:%d\t", i);                //获取指向导出函数文件名称的地址                UINT namePointerAddress = VaToFoa32(exportDirectory->AddressOfNames + nt->OptionalHeader.ImageBase + 4 * i, dos, nt, sectionArr);                if (namePointerAddress == -1)return;                printf("namePointerAddress:%X\t", namePointerAddress);                //获取指向名字的指针                UINT* nameAddr =(UINT*) ((UINT)dos + namePointerAddress);                printf("nameAddr(RVA):%X\t", *nameAddr);                //获取存储名字的地址                UINT nameOffset = VaToFoa32(*nameAddr + nt->OptionalHeader.ImageBase, dos, nt, sectionArr);                if (nameOffset == -1)return;                printf("nameOffset:%X\t", nameOffset);                //根据名字指针输出名字                CHAR* name = (CHAR*) ((UINT)dos+ nameOffset);                printf("name:%s\t",name);                //由于AddressOfNames与AddressOfNameOrdinals逐一对应,于是可以获得对应的NameOrdinals                //获取存储Ordinals的地址                UINT OrdinalsOffset = VaToFoa32(exportDirectory->AddressOfNameOrdinals + nt->OptionalHeader.ImageBase + 2 * i, dos, nt, sectionArr);                printf("OrdinalsOffset:%X\t", OrdinalsOffset);                if (OrdinalsOffset == -1)return;                WORD* Ordinals =(WORD*)((UINT)dos + OrdinalsOffset);                printf("Ordinals:%d\t", *Ordinals);                //获得Ordinals后可以根据Ordinals到AddressOfFunctions中找到对应的导出函数的地址                UINT* functionAddress=(UINT*)((UINT)dos + VaToFoa32(exportDirectory->AddressOfFunctions + nt->OptionalHeader.ImageBase + 4* *Ordinals, dos, nt, sectionArr));                printf("functionAddress(RVA):%X\n", *functionAddress);        }}int main(int argc, char* argv[]){        //创建DOS对应的结构体指针        _IMAGE_DOS_HEADER* dos;        //读取文件,返回文件句柄        HANDLE hFile = CreateFileA("C:\\Users\\lyl610abc\\Desktop\\MyDll.dll", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0);        //根据文件句柄创建映射        HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READWRITE, 0, 0, 0);        //映射内容        LPVOID pFile = MapViewOfFile(hMap, FILE_SHARE_WRITE, 0, 0, 0);        //类型转换,用结构体的方式来读取        dos = (_IMAGE_DOS_HEADER*)pFile;        //输出dos->e_magic,以十六进制输出        printf("dos->e_magic:%X\n", dos->e_magic);        //创建指向PE文件头标记的指针        DWORD* peId;        //让PE文件头标记指针指向其对应的地址=DOS首地址+偏移        peId = (DWORD*)((UINT)dos + dos->e_lfanew);        //输出PE文件头标记,其值应为4550,否则不是PE文件        printf("peId:%X\n", *peId);        //创建指向可选PE头的第一个成员magic的指针        WORD* magic;        //让magic指针指向其对应的地址=PE文件头标记地址+PE文件头标记巨细+标准PE头巨细        magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));        //输出magic,其值为0x10b代表32位程序,其值为0x20b代表64位程序        printf("magic:%X\n", *magic);        //根据magic判断为32位程序还是64位程序        switch (*magic) {        case IMAGE_NT_OPTIONAL_HDR32_MAGIC:        {                printf("32位程序\n");                //确定为32位程序后,就可以使用_IMAGE_NT_HEADERS来接收数据了                //创建指向PE文件头的指针                _IMAGE_NT_HEADERS* nt;                //让PE文件头指针指向其对应的地址                nt = (_IMAGE_NT_HEADERS*)peId;                printf("Machine:%X\n", nt->FileHeader.Machine);                printf("Magic:%X\n", nt->OptionalHeader.Magic);                //创建一个指针数组,该指针数组用来存储全部的节表指针                //这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明白一个动态数组                _IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);                //创建指向块表的指针                _IMAGE_SECTION_HEADER* sectionHeader;                //让块表的指针指向其对应的地址                sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));                //计数,用来盘算块表地址                int cnt = 0;                //比较 计数 和 块表的个数,即遍历全部块表                while (cnt < nt->FileHeader.NumberOfSections) {                        //创建指向块表的指针                        _IMAGE_SECTION_HEADER* section;                        //让块表的指针指向其对应的地址=第一个块表地址+计数*块表的巨细                        section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);                        //将得到的块表指针存入数组                        sectionArr[cnt++] = section;                        //输出块表名称                        printf("%s\n", section->Name);                }                getExportTable(dos, nt, sectionArr);                break;        }        case IMAGE_NT_OPTIONAL_HDR64_MAGIC:        {                printf("64位程序\n");                //确定为64位程序后,就可以使用_IMAGE_NT_HEADERS64来接收数据了                //创建指向PE文件头的指针                _IMAGE_NT_HEADERS64* nt;                nt = (_IMAGE_NT_HEADERS64*)peId;                printf("Machine:%X\n", nt->FileHeader.Machine);                printf("Magic:%X\n", nt->OptionalHeader.Magic);                //创建一个指针数组,该指针数组用来存储全部的节表指针                //这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明白一个动态数组                _IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);                //创建指向块表的指针                _IMAGE_SECTION_HEADER* sectionHeader;                //让块表的指针指向其对应的地址,区别在于这里加上的偏移为_IMAGE_NT_HEADERS64                sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS64));                //计数,用来盘算块表地址                int cnt = 0;                //比较 计数 和 块表的个数,即遍历全部块表                while (cnt < nt->FileHeader.NumberOfSections) {                        //创建指向块表的指针                        _IMAGE_SECTION_HEADER* section;                        //让块表的指针指向其对应的地址=第一个块表地址+计数*块表的巨细                        section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);                        //将得到的块表指针存入数组                        sectionArr[cnt++] = section;                        //输出块表名称                        printf("%s\n", section->Name);                }                break;        }        default:        {                printf("error!\n");                break;        }        }        return 0;}运行结果


可以看到,得到的结果和先前的手动分析的结果是一致的

使用PE工具:DIE查看导出表,可以看到结果也是一致的
代码说明

这次的代码部门重要是getExportTable这个函数
该函数并不长,代码中用到了较多的类型转换 和 指针相关的内容
要注意的地方是
//获取指向导出函数文件名称的地址UINT namePointerAddress = VaToFoa32(exportDirectory->AddressOfNames + nt->OptionalHeader.ImageBase + 4 * i, dos, nt, sectionArr);//获取存储Ordinals的地址UINT OrdinalsOffset = VaToFoa32(exportDirectory->AddressOfNameOrdinals + nt->OptionalHeader.ImageBase + 2 * i, dos, nt, sectionArr);这里一个是加上4×i;一个是加上2×i
4和2都是偏移量,偏移量取决于要获取的数据的数据宽度
Names的数据宽度为4字节(DWORD),以是每次要加4
而Ordinals的数据宽度为2字节(WORD),以是每次要加2
总结


  • 导出表中还包含了三张小表:导出函数地址表、导出函数名称表、导出函数序号表
  • 导出表中存储了指向这三张表地址的指针,而不是直接存储表的内容
  • 无论是根据函数名称还是根据函数序号获取导出函数都需要用到Ordinals,用Ordinals到导出函数地址表中获取地址
  • 导出表的Base取决于编写DLL时导出定义的最小序号
  • 导出表的NumberOfFuctions取决于编写DLL时导出定义的序号最大差值+1
  • 导出名称表和导出函数序号表只对有名称的导出函数有效
附件

这次提供的附件为本笔记中用到的例子:

包含1个文件夹和1个dll文件
dll文件为本笔记中分析的dll文件,MyDll文件夹则是dll的源代码
有需要者可以自行取用:点我下载

来源:http://www.12558.net
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
楼主热帖
回复

使用道具 举报

*滑块验证:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|12558网页游戏私服论坛 |网站地图

GMT+8, 2024-11-29 05:49 , Processed in 0.078125 second(s), 32 queries .

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表