当前位置: 首页 > article >正文

【第34节】windows原理:PE文件的导出表和导入表

目录

一、导出表

1.1 导出表概述

1.2 说明与使用

二、导入表

2.1 导入表概述

2.2 说明与使用


一、导出表

1.1 导出表概述

(1)导出行为和导出表用途:PE文件能把自身的函数、变量或者类,提供给其他PE文件使用,这种行为就叫导出。导出表专门用来存放这些导出项目的信息。当一个PE文件要调用其他PE文件里的导出函数(变量、类)时,依靠导出表就能迅速找到它们在文件里的位置。一般来说,这些被导出的函数、变量、类,也叫做符号(Symbol)。
(2)导出项序号的特点:每一个被导出的函数(变量、类),都有一个独一无二的序号。在有些情况下,可能找不到对应的函数名(变量、类名),但函数(变量、类)的地址和序号是存在的,听说可以通过序号来调用这类函数。
(3)导出表的内容组成:导出表里面记录的内容,包含了函数(变量、类)的地址、序号,还有函数(变量、类)名。
(4)导出表的查找方法:数据目录表的第一个元素里有相对虚拟地址,利用前面讲过的相对虚拟地址转文件偏移的办法,就能找到导出表的位置(后面会给出具体代码) 。

导出表的数据结构如下:

typedef struct _IMAGE_EXPORT_DIRECTORY  {
    DWORD   Characteristics;      //1( 没用)保留值,恒为0
    DWORD  TimeDateStamp;    //2( 没用)和文件头中的时间一样的。
    WORD   MajorVersion;  //3(没 用)主版本号
    WORD   MinorVersion;    //4 ( 没用 )次版本号
    DWORD  Name;             //5(有用)本PE文件的名字,也就是谁导出的这些函数(变量,类)
    DWORD  Base;             //6(有用)序号基数
    DWORD  NumberOfFunctions;//7 (重要) 函数数量
    DWORD  NumberOfNames;    //8   (重要)  函数名称数量
    DWORD  AddressOfFunctions;//9(重要)函数地址表的相对虚拟地址//RVA from base of image
    DWORD AddressOfNames;    //10(重要)函数名称表的相对虚拟地址//RVA from base of image
    DWORD AddressOfNameOrdinals;//11(重要)序号表的相对虚拟地址//RVA from base of image
}IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;

1.2 说明与使用

说明
(1)导出表存储位置:导出表一般在.edata段,不过.edata段通常会合并到.rdata段。
(2)序号基数:有个序号基数,从序号表得到的序号得加上它,才是真实函数序号,这方便在函数地址、序号和函数名之间互相查找。
(3)表项地址定位:导出表最后三个成员是相对虚拟地址,转换成文件偏移后,能找到函数地址表、函数名称表和序号表。
(4)函数名称表:函数名称表存的是函数名的相对虚拟地址,得再转换成文件偏移才能用,解析时要注意。
(5)导出表各表关系:
    - 序号顺序:序号不是按顺序排列的。
    - 对应关系:序号和函数名一一对应,两个表相同位置元素对应,所以结构体没设序号数量,因为它和函数名数量一样。
    - 序号中断:序号可能中断,比如缺1、5,但不代表没对应的函数地址。
    - 数量关系:函数个数比函数名个数多,多出来的可能是序号导出函数,也可能是地址填0的无效函数。
    - 表间关联:序号表元素值对应函数地址表位置,该位置的函数地址就是对应函数名的地址,靠这个把三个表联系起来。
    - 序号导出函数:函数地址表有地址值,但序号表没对应序号,说明是序号导出函数,序号就是它在函数地址表中的位置,没函数名。
    - 无效函数:函数地址表元素填0,就是无效函数,没序号和函数名对应。

看下面这张导出表图示,能更好理解导出表。

        函数表5、7位置是无效函数,2、3、4、6、8位置是函数名导出,注意函数名位置和地址表地址位置不同,1、9是序号导出函数,序号就是本身位置。

使用
(1)导出表信息提取:想提取导出表所有信息并过滤无效地址,得用循环,循环次数是函数地址个数。根据函数地址位置,在序号表找对应值,这个值的位置就是函数名位置。

        代码思路如下,假设目标文件已读入内存,首地址为pFile(void类型指针),且用到之前计算偏移的函数:

for(Ordinal=0;Ordinal<函数个数;dwOrdinal++){
    if(!导出函数数组[Ordinal])
        continue ;
    for(Index=0;Index<函数个数;dwIndex++){
        if(导出序号数组[Index]== Ordinal ){
            输出带函数名的导出函数信息
        }
        elseif(已经遍历完导出表){
            输出不带函数名的导出函数信息
        }
    }
}

在LoadPE中,点击导出表的“...”即可弹出导出表界面,序号代表结构体中成员的序号。

导出表偏移:这个偏移是用数据目录中的相对虚拟地址算出来的

特征:特征值恒0

函数地址:函数地址的相对虚拟地址

函数名地址:函数名称的相对虚拟地址

函数名顺序地址:序号表的相对虚拟地址

名称:名称的相对虚拟地址

字符串名称:根据名称的地址算出编移存储的名称

(2)根据函数名查找函数地址:导出表还有其他用途,比如给定一个函数名,查找其函数地址,这类似于GetProcAddress()函数。在编写壳程序等特殊场景中会用到此功能。该方法比上述代码更简单,直接根据函数名找到其位置,该位置序号的值即为所要查找函数的地址索引。若要查找已加载到内存后的dll中的函数地址,就不应再使用文件偏移,而应直接使用虚拟地址(VA)。

二、导入表

2.1 导入表概述

(1)导入行为和导入表用途:PE文件运行过程中,如果用到其他PE文件里的函数、变量或者类,这种行为就叫做导入。导入表专门记录这部分信息。
(2)导入表的查找方法:数据目录表的第二个元素能帮我们定位导入表,查找的办法和找导出表一样。
(3)导入表的存储内容:导入表会记录从其他PE文件导入的函数名和序号。当PE文件加载到内存后,导入表还会保存这些函数的实际内存地址。
(4)导入表的结构特点:一个PE文件往往需要多个其他PE文件提供支持,所以导入表通常有多个。从结构上来说,导入表是一个结构体数组,数组以一个全零元素作为结束标志,数组里每个元素,都对应着一个PE文件的导入信息 。

2.2 说明与使用

导入表相关数据结构如下:

typedef struct _IMAGE_IMPORT_DESCRIPTOR
union {
    DWORD   Characteristics;   
    DWORD  OriginalFirstThunk;//1(重要)指向一个结构体数组的相对虚拟地址(RVA), 结构体数组叫输入名称表
}  DUMMYUNIONNAME;     
DWORD TimeDateStamp;          //2(有用)没用
DWORD ForwarderChain;    //3(有用)转发机制用到,这里不探讨
DWORD Name;         //4(有用)导入的PE文件的名字的相对虚拟地址RVA
DWORD FirstThunk;    //5(重要)指向一个结构体数组的相对虚拟地址(RVA),结构体数组叫做输入地址表(IAT:Import Address Table)
}IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;

说明
(1)指针指向特性:OriginalFirstThunk和FirstThunk这两个指针,都指向IMAGE_THUNK_DATA类型的结构体。
(2)磁盘与内存数据变化:在磁盘上的文件里,OriginalFirstThunk和FirstThunk所指向的数据是一样的。基于此,我们可以把输入名称表(INT)当作输入地址表(IAT)的备份。等到文件加载到内存后,加载器会把对应PE文件里函数的真实地址,填充到输入地址表中。这时,输入地址表才成了真正可用的输入地址表。
(3)输入名称表情况:有些文件的输入名称表内容全为零,处于空白状态。这意味着输入地址表有时没有备份。所以在解析输入表时,优先使用输入地址表;当然,也可以同时解析输入名称表和输入地址表,对比查看。
(4)解析终止条件:这两个指针指向的结构体数组,是以全零元素作为结尾的。我们可以利用这一特性,作为解析过程的结束标志。
(5)相关结构体介绍:

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;   //转发用到
        DWORD Function;          //导入函数的地址,在加载到内存后,这里才起作用
        DWORD Ordinal;            //假如是序号导入的,用到这里
        DWORD AddressOfData;     //假如是函数名导入的,用到这里,它指向一个PIMAGE_IMPORT_BY_NAME结构体
    }ul;
}IMAGE_THUNK_DATA32;

        在磁盘文件中,起作用的只有后两个成员。该结构占4个字节,若最高位为1,则序号导入起作用,只需输出一个序号;若最高位为0,则最后一个成员起作用,指向一个PIMAGE_IMPORT_BY_NAME。可使用系统提供的宏IMAGE_SNAP_BY_ORDINAL32()判断最高位是否为1,参数为该结构体。

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD   Hint;
    CHAR Name[1];
}IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;

该结构包含序号和函数名。

使用:

//循环打印INT的内容
while(pINT->u1.Ordinal)
{
    //判断最高位是否不为1,是的话则打印其AddressOfData的内容
    if(!IMAGE_SNAP_BY_ORDINAL32(pINT->u1.Ordinal))
    {
        PIMAGE_IMPORT_BY_NAME pBN =获取ByName;
        printf("%04X %s\r\n",pBN->Hint,pBN->Name);
    }
    pINT++;
    continue;
}
//如果最高位为1,则直接打印Ordinal部分
printf("%04X %s\r\n",pINT->u1.Ordinal&0xFFFF,"(Null)");
pINT++;

原始 FirstThunk:导入名称表的RVA

FirstThunk:导入地址表的RVA

正向链:转发用

ThunkEVA:每-个Thunk统构的RVA(算的)

Thunk 偏移:Thunk结构的编移(算的)

Hint:函数的序号

API名称:函数的名称

        计算每个Thunk结构的相对虚拟地址(RVA)时,只要知道第一个结构的RVA,后续结构的RVA依次加4就能得出。偏移计算方法也是一样,算出第一个偏移后,后续偏移每次加4。

        LoadPE软件会对输入名称表(INT)和输入地址表(IAT)这两个数据域进行解析。软件默认展示INT的解析结果,如果点击软件下方“总是查看FirstThunk”复选框,软件就会显示IAT的解析结果。

        到这里,关于导出表和导入表的解析说明就结束了。

 


http://www.kler.cn/a/614196.html

相关文章:

  • 什么是贴源库
  • PyTorch 深度学习实战(28):对比学习(Contrastive Learning)与自监督表示学习
  • BUUCTF-web刷题篇(2)
  • app036-基于安卓的“快电”APP(编号:12981277)
  • jetson orin nano super AI模型部署之路(三)stable diffusion部署
  • 为什么idea显示数据库连接成功,但操作数据库时,两边数据不同步
  • 53.第二阶段x86游戏实战2-c++实现自动打怪2
  • ACM贪心基础
  • 深度学习在测距模型中的应用
  • 解决Dubbo3调用Springcloud接口报No provider available from registry RegistryDirectory
  • c#winform,倒鸭子字幕效果,typemonkey字幕效果,抖音瀑布流字幕效果
  • Python 序列构成的数组(元组不仅仅是不可变的列表)
  • 深入理解Agentic Workflows
  • 读DAMA数据管理知识体系指南34数据仓库和商务智能概念
  • 在 RK3588 多线程推理 YOLO 时,同时开启硬件解码和 RGA 加速的性能分析
  • bluecode-20240913_1_数据解码
  • 树莓派5智能家居中控:HomeAssistant全配置指南
  • uni-app:指引蒙层
  • Spring中的IOC及AOP概述
  • LeetCode 2360.图中的最长环:一步一打卡(不撞南墙不回头) - 通过故事讲道理