滴水三期:day39.1-IAT表和导入表

PE文件结构详解:导入表、IAT表与DLL函数调用
文章详细阐述了PE文件(如.exe和.dll)中导入表(ImportTable)的作用,包括IAT(ImportAddressTable)和INT(ImportNameTable)表的结构和功能。在程序运行前,调用DLL函数时,call指令后面的地址是间接寻址,指向INT表中的函数名称或序号;运行后,IAT表会被填充为DLL中函数的实际内存地址。文章还介绍了如何通过结构体解析PE文件的导入表信息,以及不同程序在运行前后IAT表的变化情况,强调了导入表在动态链接过程中的关键角色。

一、引入IAT表

1.调用自己写的函数

1)在内存中(ImageBuffer)
  • 编写一个C程序,调用自己写好的函数

    #include "stdafx.h"
    void my_method(){
         
         
    	int a = 1;
    	a++;
    	printf("%d",a);
    }
    int main(int argc, char* argv[]){
         
         
    	my_method();
    	return 0;
    }
    
  • 可以通过OD查看程序再运行时的数据,也可以直接在VC上运行查看反汇编(这里使用VC查看方便一些)。发现:这里调用函数时,call后面跟的是0xFFFF3B7D(前面说过硬编码为E8的call后面跟的地址是相对地址:0x40100a - (0x40D488 + 5) = 0xFFFF3B7D),即实际上call的是0x40100a这个地址;再按F11跳到0x40100a,发现这是一个跳板:jmp 0x401010(硬编码是E9 01 00 00 00),最终0x401010就是自己写的函数开始的地址

    212658 image-20230413225753048
2)在硬盘上(FileBuffer)
  • 先通过PETool查看程序的ImageBase和文件、内存对齐粒度,可以算出0x40100a对应的文件偏移地址为0x100a

    213323
  • 接着我们用Winhex打开程序,查看一下程序未运行时0x100a地址处和运行时跳板处硬编码一样:都是E9 01 00 00 00,那么jmp真正要跳转的地址为:0x00000001 + 0x0000100F = 0x00001010(跟E8后面算地址的方法一样)

    image-20230413230251715
  • 最后看一下0x1010地址处刚好就是自己写的函数的硬编码

    230656
3)结论
  • 一个程序调用了自己写的函数:无论运行前还是运行时,call后面的地址已经“写死”,就是一个跳板的绝对地址,而跳板跳的就是自己写的函数地址;故程序在编译后,call后面的地址就写死了

  • 敢写死是因为,自己写的函数存储地址都是在程序本身的.exe中,而程序运行时,.exe装载到程序的虚拟内存中是不会有别的PE文件和它争抢位置的,所以.exe会按照程序的ImageBase正常装载,进而这些写死的地址值就是有效的

    和调用系统DLL中的函数结论做一个对比

2.调用DLL中的函数

1)在内存中(ImageBuffer)
  • MessageBox()函数位于user32.dll中,属于系统提供的API

  • 现在编写一个C程序,调用MessageBox()函数;

    #include "stdafx.h"
    #include <windows.h>
    int main(int argc, char* argv[]){
         
         
    	MessageBox(0,0,0,0); 
    	ExitProcess(0); //这是一个退出进程函数,也是系统API
    	return 0;
    }
    
  • 编译成Release版后,使用OD打开这个.exe程序,找到程序OEP,回车进去(前面学过,一般情况下,找三个连续的push,后面这个call就是程序入口)

    183811
  • 找到程序调用MessageBox()函数这里,发现call后面跟的是一个间接寻址[0040509C],即真正call的实际上是0x40509C地址中的值!所以我们输入db 40509C,去看看0x40509C这个地址中存的什么:发现存的是MessageBox函数的内存中的绝对地址0x77D36476,这个函数地址不是.exe程序本身的地址,而是使用的一个DLL中的地址,我们可以再打开工具栏的E查看一下

    小提示:这里call的硬编码是FF15(不是E8),FF15的call后面跟的地址是内存的绝对地址(E8的是相对地址)

    4049 84316
2)在硬盘上(FileBuffer)
  • 现在我们看看0x40509C内存绝对地址转换成程序没运行时的地址,这个地址存的是什么?所以首先需要先用PETools查看程序的ImageBase和文件、内存偏移粒度,将0x40509C转化成文件地址为:0 + (0x40509C - 0x400000) = 0x509C

    182736
  • 接着打开Winhex查看程序的0x509C地址中存的是:0x00005538,这是一个文件==偏移地址==

    image-20230413185007455
  • 最后查看0x5538地址中存的是:MessageBoxA的函数名称字符串的ASCII码。会发现这里有很多函数名字符串!(这里就是函数名称表IMAGE_IMPORT_BY_NAME

    image-20230413185255835
3)结论并引入IAT表
  • 发现一个使用了系统DLL函数的程序,在调用DLL函数时都使用了间接寻址,但是

    • 在运行时:call [0x40509C],0x40509C地址存储的是0x77D36476(这是MessageBox()函数的内存绝对地址),即实际上call的是0x77D36476
    • 运行前:call [0x509C],0x509C地址存储的是0x5538(这里没有直接写MessageBox函数的内存绝对地址),即实际上call的是0x5538
  • 所以使用了系统DLL函数的程序,编译后(运行前)并没有直接把函数在DLL中的绝对地址(DLL的ImageBase + RVA = 0x77D36476)写到call后面,而是写了程序自己的.exe空间中的一个文件偏移地址(0x5538),这个文件偏移地址指向了此程序使用的DLL中的函数名称

    • 一个程序会使用很多别的DLL中的函数,所以==运行前:会有很多个上述的文件偏移地址,这些指向了函数名称的文件偏移地址就构成了IAT表!==
  • 但是程序运行时,程序先装载自己的.exe到程序的虚拟内存中,再依次装载使用到的DLL到程序的虚拟内存中,此时DLL在虚拟内存中的位置已经固定了,所以操作系统会把call后面的值改为函数在DLL中的绝对地址(此时DLL的ImageBase + RVA)

    • 运行时:操作系统会把IAT表中的文件偏移地址改成此时对应函数的绝对地址,即运行后==IAT表中的值就全变成了使用的DLL函数在内存中的绝对地址==

    注意:如果发生了DLL装载地址冲突,DLL会往后找空闲内存存放,此时操作系统会先修正重定位表,接着把修正后的函数在DLL中的绝对地址放到call后面

  • 所以IAT表的内容在程序执行前和执行后是不一样的

4)问题
  • 那为什么编译时不直接把MessageBox函数在DLL中的绝对地址放到call后面?
  • 就是因为day37.1-重定位表中学过的:MessageBox所在的DLL在装载时并不一定会按照其指定的ImageBase装载,可能会出现DLL装载地址冲突,导致该DLL最终装载的地址和其ImageBase不一致!那么所有写死的绝对地址都失效了!
  • 但是我们自己写的函数在程序的.exe中,而一个程序运行时它本身的.exe装载地址就是自己的ImageBase,一般不会有其他的PE文件和它抢占,所以直接把函数的内存绝对地址放到call后面即可

3.易错误区

  • 调用函数的地址问题和修复重定位表关系:二者没有任何关系!!!!

  • 程序调用DLL的函数:编译时,call后面的地址值不会写死,而是间接寻址,指向一个DLL中函数名字字符串的地方;但是在运行时,会直接把此时DLL装载到内存后的函数绝对地址写到call后面(间接指向)!所以这里修改的是程序自己的.exe中调用函数的call语句中的地址值

  • 而重定位表修复:修复的地址是DLL中的地址,而不会像上述一样还会帮使用自己函数的程序修复程序调用函数时call后面的地址!所以重定位表修复的是DLL自身当中的一些地址(比如DLL中的函数绝对地址、DLL中的全局变量等),因为可能会遇到DLL装载时没有按照指定的ImageBase正常装载,所以要修改DLL自身中写死的地址

二、导入表

1.导入表是什么

  • 前面的IAT表,运行前和运行时的内容是怎么确定下来的,会在导入表的讲解中迎刃而解,所以导入表和IAT表有很大的关系

  • 导入表:即一个PE文件(.dll/.exe等),它如果需要使用使用别的PE文件的函数,就需要一个清单来记录使用的别的PE文件的函数相关信息(使用了哪些DLL、使用了这个DLL里的哪些函数、叫什么名、去哪找等)

  • 所以一般而言:程序通过导入表中的信息来装载DLL到虚拟内存中(比如通过导入表的所有Name成员,知道要装载哪些DLL;或者如果导入表的某个结构中OriginalFirstThunk和FirstThunk都为0,系统就不会加载这个导入表结构对应的DLL,因为操作系统知道你没有使用这个DLL中的任何函数)

    上面内容复习的时候再看。程序需要导入表中的一些信息来加载DLL,但修复导入表中的某些信息是在DLL装载完成后(比如修复IAT表),所以这些都是有过程的,不要混为一谈

  • .exe一般没有导出表、有导入表;.dll一般既有导出表、又有导入表(因为一个.dll可能也需要使用别的.dll提供的函数,且.dll也会提供函数给别的PE文件用)

2.定位导入表

  • 一个PE文件的导入表数据目录在PE文件第二个数据目录结构体

  • 再根据导入表数据目录的VirtualAddress成员找到导出表(RVA转FOA)

    190056

3.导入表结构

  • 导入表中的结构:

    struct _IMAGE_IMPORT_DESCRIPTOR{
         
         							
        union{
         
         							
            DWORD Characteristics;           							
            DWORD OriginalFirstThunk;  //RVA,指向IMAGE_THUNK_DATA结构数组(INT表)
        };							
        DWORD TimeDateStamp;  //时间戳	(用于判断是否有绑定导入表/IAT表中是否已经绑定绝对地址)
        DWORD ForwarderChain;              							
        DWORD Name;  //RVA,指向dll名字字符串存储地址
        DWORD FirstThunk;  //RVA,指向IMAGE_THUNK_DATA结构数组(IAT表)
    };
    //导入表有很多个这种结构(成员全为0,表示结束)
    
  • 注意:导入表不可能就一个这种结构,因为一个PE文件可能会使用多个DLL中的多个函数,一个这样的结构只表示一个DLL中的函数;所以PE文件使用了多少个DLL中的函数,就有多少个这种结构

  • 当遇到有sizeof(_IMAGE_IMPORT_DESCRIPTOR)0x00时,表示导出表结束(相当于一个导入表结构中的成员全为0x00000000,就表示导入表结束)

  • 由于IAT表在程序运行前后是不一样的,所以导入表和IAT表的具体关系结构图示如下:

    1. PE文件运行前

      image-20230416183556587
    2. PE文件运行后

      image-20230416183626572
1)OriginalFirstThunk
  • RVA,指向INT表(导入名称表),如果想得到内存地址,即用ImageBase + RVA即可;如果想得到文件地址,即把RVA转FOA即可
2)Name
  • RVA,指向DLL名字字符串所在地址;比如Name指向的DLL名称为“user32.dll\0”,那么这个结构中记录的就是user32.dll中函数的相关信息;所以一个PE文件的导入表有多少中这种结构,就使用了多少个其他DLL中的函数
3)FirstThunk
  • RVA,指向IAT表(导入地址表)
4)TimeDataStamp

时间戳字段看不懂没事,详情在day40.1-绑定导入表

  • 0(即0x00000000):表示这个导入表结构对应的DLL中的函数绝对地址没有绑定到IAT表中
  • 为**-1**(即0xFFFFFFFF):表示这个导入表结构对应的DLL中函数绝对地址已经绑定到IAT
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值