滴水三期:day39.1-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表中

4.IAT表和INT表

1)定位IAT表和INT表
  • 第一种方法:通过导入表中的FirstThunk(RVA)成员,找到IAT表

    通过OriginalFirstThunk找到INT表

  • 第二种方法:直接通过PE文件的第13个数据目录结构体中的VirtualAddress(RVA)成员,找到IAT表

2)INT表
  • 中文名:导入名称表

  • 结构如图:

    image-20230416183650434
    • 由于别的DLL中的函数可以以函数名称导出,也可以只以序号(NONAME)导出,所以当一个PE文件使用别的DLL中的函数时要考虑函数有名字和函数只有序号两种情况:故INT表中的元素有IMAGE_THUNK_DATA序号两种(都是32位,4字节),判断方法如下:
      • 如果INT表的元素最高位为1:那么除去最高位剩下31位的值,就是函数的导出序号
      • 如果INT表的元素最高位为0:那么这整个值是一个RVA指向IMAGE_IMPORT_BY_NAME表中对应的函数名称结构体
  • IMAGE_THUNK_DATA32结构体:

    struct _IMAGE_THUNK_DATA32{								
        union{								
            BYTE ForwarderString;								
            DWORD Function;								
            DWORD Ordinal;  //序号		
            _IMAGE_IMPORT_BY_NAME* AddressOfData;  //RVA,指向IMAGE_IMPORT_BY_NAME		
        };								
    };						
    

    这个结构体中就是一个联合体,占4字节;所以可以直接把INT表中的元素全部当成DWORD宽度即可(当成DWORD宽度的话,遍历INT表或者读取当中元素要方便很多)

  • _IMAGE_IMPORT_BY_NAME结构体:

    struct _IMAGE_IMPORT_BY_NAME{							
        WORD Hint;  //可能为空(编译器决定);如果不为空,表示函数在导出表中的索引	
        BYTE Name[1];  //函数名称,以0结尾	
    };							
    
    • Hint:这个值不能作为查找函数的手段,很多编译器都会把这个值设置为0x0000,表示为空

    • Name:因为这里要找一个结构来存储函数名称,一般都会想到字符数组,但是函数名称的字符个数是不确定的,那么到底定义多大的字符数组合适呢?为了解决这个问题,干脆定义一个1字节的字符数组,只存储函数名称的第一个字符!那么通过Name就可以得到该字符串的首地址!再依次往后遍历,直到遇到字符串的结尾字符\0,整个函数名称就取到了

      易错误区:其实函数名称表中就是一个2字节hint再加上函数名称字符串这种形式存储,这里定义的结构体_IMAGE_IMPORT_BY_NAME,只是方便我们在内存中取值和遍历!所以不能理解成内存中真正存的数据就是一个2字节hint和1字节的字符!实际上内存中这里存储的形式为2字节hint + n字节函数名称字符串,后面依次类推

3)IAT表
  • 中文名:导入地址表

  • PE文件运行前:IAT表中的内容和INT表的一模一样!

  • PE文件运行后:IAT表中的内容就变成了对应DLL中的函数在内存中的绝对地址

  • IAT表修改过程:当PE文件运行后,先装载.exe,再装载各个使用到的.dll到PE文件的虚拟内存中;等所有DLL装载完成后,再修改IAT表:遍历导出表中每个结构的INT表,无论遍历到的是函数名还是序号,都会作为其中一个参数传给系统函数:GetProcAddress(),此函数根据函数名或序号返回对应函数在内存中的绝对地址,接着把绝对地址存入到IAT表的对应位置

    此函数详情见day35.1-静态、动态链接库中显式链接部分

    image-20230416184031319
  • 当IAT表修改完成后,系统会把程序中调用DLL函数的call语句后面的地址也改了!改成什么?------程序的call后面跟的是间接寻址,即call [0x....],实际上这里的地址改成了IAT表中对应函数绝对地址值的地址!

    • 即运行前:call [0x....]后面间接寻址的地址0x....INT表中某个元素的地址;

    • 而运行后:call [0x....]后面间接寻址的地址0x....,改成了IAT表中某个元素的地址;

  • 为什么有IAT表了,还需要INT表?

    • 原因之一是PE文件加载后,INT表作为函数名字的备份

      运行前后INT表中的元素永远都指向函数名称或者序号,所以如果需要按照名字来查找该程序使用的别的DLL中的函数地址,就可以先通过遍历该程序导出表的INT表(别忘了导出表是由多个相同的结构构成的,每一个结构都对应一个DLL,每一个结构中都有INT表),最终在某一个导出表结构中的INT表中匹配到函数名称(或序号),那么把函数名称和所属结构的DLL通过LoadLibrary()返回的句柄(即所属DLL的ImageBase)传入GetProcAddress()函数,就可以得到该函数地址

    • 如果没有INT表,那么当程序运行后,IAT表中的元素变成了函数的绝对地址,就无法通过函数名来查找了

5.总结

  • 一个PE文件的导入表有很多个_IMAGE_IMPORT_DESCRIPTOR这种结构构成,每一个这种结构都对应一个DLL以及该DLL中的函数,通过每个结构中的Name可以知道该PE文件使用了哪些DLL中的函数,再通过这个结构中的INT表和IAT表可以知道该PE文件使用了这个DLL中的哪些函数

三、作业

1.打印程序运行前的导入表

  • 要求:打印ipmsg.exe和notepad.exe运行前的导入表,以及INT表、IAT表和函数名称或序号,最后对比两个PE文件的IAT表在运行前有什么不同

    #include "stdafx.h"
    #include <stdlib.h>
    #include <string.h>
    
    typedef unsigned short WORD;
    typedef unsigned int DWORD;
    typedef unsigned char BYTE;
    
    #define MZ 0x5A4D
    #define PE 0x4550
    #define IMAGE_SIZEOF_SHORT_NAME 8
    
    //DOS头
    struct _IMAGE_DOS_HEADER {
    	WORD e_magic;  //MZ标记
    	WORD e_cblp;
    	WORD e_cp;
    	WORD e_crlc;
    	WORD e_cparhdr;
    	WORD e_minalloc;
    	WORD e_maxalloc;
    	WORD e_ss;
    	WORD e_sp;
    	WORD e_csum;
    	WORD e_ip;
    	WORD e_cs;
    	WORD e_lfarlc;
    	WORD e_ovno;
    	WORD e_res[4];
    	WORD e_oemid;
    	WORD e_oeminfo;
    	WORD e_res2[10];
    	DWORD e_lfanew;  //PE文件真正开始的偏移地址
    };
    
    //标准PE头
    struct _IMAGE_FILE_HEADER {
    	WORD Machine;  //文件运行平台
    	WORD NumberOfSections;  //节数量
    	DWORD TimeDateStamp;  //时间戳
    	DWORD PointerToSymbolTable;
    	DWORD NumberOfSymbols;
    	WORD SizeOfOptionalHeader;  //可选PE头大小
    	WORD Characteristics;  //特征值
    };
    
    //数据目录
    struct _IMAGE_DATA_DIRECTORY{
        DWORD VirtualAddress;
        DWORD Size;
    };
    
    //可选PE头
    struct _IMAGE_OPTIONAL_HEADER {
    	WORD Magic;  //文件类型
    	BYTE MajorLinkerVersion;
    	BYTE MinorLinkerVersion;
    	DWORD SizeOfCode;   //代码节文件对齐后的大小
    	DWORD SizeOfInitializedData;  //初始化数据文件对齐后的大小
    	DWORD SizeOfUninitializedData;  //未初始化数据文件对齐后大小
    	DWORD AddressOfEntryPoint;  //程序入口点(偏移量)
    	DWORD BaseOfCode;  //代码基址
    	DWORD BaseOfData;  //数据基址
    	DWORD ImageBase;   //内存镜像基址
    	DWORD SectionAlignment;  //内存对齐粒度
    	DWORD FileAlignment;  //文件对齐粒度
    	WORD MajorOperatingSystemVersion;
    	WORD MinorOperatingSystemVersion;
    	WORD MajorImageVersion;
    	WORD MinorImageVersion;
    	WORD MajorSubsystemVersion;
    	WORD MinorSubsystemVersion;
    	DWORD Win32VersionValue;
    	DWORD SizeOfImage;  //文件装入虚拟内存后大小
    	DWORD SizeOfHeaders;  //DOS、NT头和节表大小
    	DWORD CheckSum;  //校验和
    	WORD Subsystem;
    	WORD DllCharacteristics;
    	DWORD SizeOfStackReserve;  //预留堆栈大小
    	DWORD SizeOfStackCommit;  //实际分配堆栈大小
    	DWORD SizeOfHeapReserve;  //预留堆大小
    	DWORD SizeOfHeapCommit;  //实际分配堆大小
    	DWORD LoaderFlags;
    	DWORD NumberOfRvaAndSizes;  //目录项数目
    	_IMAGE_DATA_DIRECTORY DataDirectory[16]; //数据目录
    };
    
    //NT头
    struct _IMAGE_NT_HEADERS {
    	DWORD Signature;  //PE签名
    	_IMAGE_FILE_HEADER FileHeader;
    	_IMAGE_OPTIONAL_HEADER OptionalHeader;
    };
    
    //节表
    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;  //该节特征属性
    };
    
    //导入表
    struct _IMAGE_IMPORT_DESCRIPTOR{							
        union{							
            DWORD Characteristics;           							
            DWORD OriginalFirstThunk;  //RVA,指向IMAGE_THUNK_DATA结构数组(INT表)
        };							
        DWORD TimeDateStamp;  //时间戳	
        DWORD ForwarderChain;              							
        DWORD Name;  //RVA,指向dll名字字符串存储地址
        DWORD FirstThunk;  //RVA,指向IMAGE_THUNK_DATA结构数组(IAT表)
    };//导入表有很多个这种结构(成员全为0,表示结束)
    
    //INT表中元素指向的函数名称表
    struct _IMAGE_IMPORT_BY_NAME{							
        WORD Hint;  //可能为空(编译器决定);如果不为空,表示函数在导出表中的索引	
        BYTE Name[1];  //函数名称,以0结尾	
    };	
    
    //INT表和运行前IAT表
    struct _IMAGE_THUNK_DATA32{								
        union{								
            BYTE ForwarderString;								
            DWORD Function;								
            DWORD Ordinal;  //序号		
            _IMAGE_IMPORT_BY_NAME* AddressOfData;  //RVA,指向IMAGE_IMPORT_BY_NAME		
        };								
    };
    
    /*计算文件大小函数
    参数:文件绝对路径
    返回值:返回文件大小(单位字节)
    */
    int compute_file_size(char* filePath){
    	FILE* fp = fopen(filePath,"rb");
        if(!fp){
    		printf("打开文件失败");
    		exit(0);
    	}
        fseek(fp,0,2);
    	int size = ftell(fp);
    	//fseek(fp,0,0); 单纯计算文件大小,就不需要还原指针了
    	fclose(fp);
    	return size;
    }
    
    /*将文件读入FileBuffer函数
    参数:文件绝对路径
    返回值:FileBuffer起始地址
    */
    char* to_FileBuffer(char* filePath){
        FILE* fp = fopen(filePath,"rb");
        if(!fp){
    		printf("打开文件失败");
    		exit(0);
    	}
        int size = compute_file_size(filePath);
        
    	char* mp = (char*)malloc(sizeof(char) * size);  //分配内存空间
        if(!mp){
            printf("分配空间失败");
            fclose(fp);
            exit(0);
        }
        
        int isSucceed = fread(mp,size,1,fp);
        if(!isSucceed){
            printf("读取数据失败");
            free(mp);
            fclose(fp);
            exit(0);
        }
        
        fclose(fp);
        return mp;
    }
    
    /*虚拟内存偏移地址->文件偏移地址函数
    参数:FileBuffer起始地址,RVA地址值
    返回值:RVA对应的FOA,返回0则表示非法RVA
    */
    DWORD RVA_to_FOA(char* fileBufferp,DWORD RVA){
    	_IMAGE_DOS_HEADER* _image_dos_header = NULL;
    	_IMAGE_FILE_HEADER* _image_file_header = NULL;
    	_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
    	_IMAGE_SECTION_HEADER* _image_section_header = NULL;
    
    	_image_dos_header = (_IMAGE_DOS_HEADER*)fileBufferp;
    	_image_file_header = (_IMAGE_FILE_HEADER*)(fileBufferp + _image_dos_header->e_lfanew + 4);
    	_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
    	_image_section_header = (_IMAGE_SECTION_HEADER*)((char*)_image_optional_header + 
    		_image_file_header->SizeOfOptionalHeader);
    
    	bool flag = 0; //用来判断最终RVA值是否在节中的非空白区
    	if(RVA < _image_section_header->VirtualAddress)
    		return RVA;
    	for(int i = 0;i < _image_file_header->NumberOfSections;i++){
    		if(RVA >= _image_section_header->VirtualAddress 
    			&& RVA < _image_section_header->VirtualAddress + _image_section_header->Misc.VirtualSize){
    			flag = 1;
    			break;
    		}else{
    			_image_section_header++;
    		}
    	}
    	if(!flag)
    		return 0;
    	DWORD mem_offset_from_section = RVA - _image_section_header->VirtualAddress;
    	return _image_section_header->PointerToRawData + mem_offset_from_section;	
    }
    
    /*打印导入表、INT表、运行前IAT表、函数名称或序号
    参数:需要打印的PE文件绝对路径
    返回值:无
    */
    void importTable_print(char* filePath){
    	char* fileBufferp = to_FileBuffer(filePath);
    
    	_IMAGE_DOS_HEADER* _image_dos_header = NULL;
    	_IMAGE_FILE_HEADER* _image_file_header = NULL;
    	_IMAGE_OPTIONAL_HEADER* _image_optional_header = NULL;
    	_IMAGE_DATA_DIRECTORY* _image_data_directory = NULL;
    	_IMAGE_IMPORT_DESCRIPTOR* _image_import_descriptor = NULL;
    	_IMAGE_THUNK_DATA32* _image_thunk_data32 = NULL;  //指向INT表或IAT表
    	_IMAGE_IMPORT_BY_NAME* _image_import_by_name = NULL;  //指向函数名称表
    
    
    	_image_dos_header = (_IMAGE_DOS_HEADER*)fileBufferp;
    	_image_file_header = (_IMAGE_FILE_HEADER*)(fileBufferp + _image_dos_header->e_lfanew + 4);
    	_image_optional_header = (_IMAGE_OPTIONAL_HEADER*)((char*)_image_file_header + 20);
    	_image_data_directory = (_IMAGE_DATA_DIRECTORY*)(_image_optional_header->DataDirectory + 1);
        if(_image_data_directory->VirtualAddress == 0){
            printf("此文件没有导入表");
            getchar();
            exit(0);
        }
    	_image_import_descriptor = (_IMAGE_IMPORT_DESCRIPTOR*)(fileBufferp + RVA_to_FOA(fileBufferp,_image_data_directory->VirtualAddress));
    
    	printf("***************导入表***************\n");
    	while(1){
    		//根据导入表结构中是否为全0判断导入表是否结束
    		for(int i = 0;i < sizeof(*_image_import_descriptor);i++){
    			if(*((char*)_image_import_descriptor + i) != 0){
    				break;
    			}
    		}
    		if(i == sizeof(*_image_import_descriptor))
    			break;
    
    		//打印导入表
    		char* dllName = fileBufferp + RVA_to_FOA(fileBufferp,_image_import_descriptor->Name);
    		printf("\nName:%08X--->%s\n",_image_import_descriptor->Name,dllName);
    		printf("TimeDateStamp:%08X\n",_image_import_descriptor->TimeDateStamp);
    		printf("OriginalFirstThunk:%08X\n",_image_import_descriptor->OriginalFirstThunk);
    		printf("FirstThunk:%08X\n",_image_import_descriptor->FirstThunk);
    
    		//打印INT表和函数名
    		printf("-----------------INT表-----------------\n");
    		if(_image_import_descriptor->OriginalFirstThunk != 0){
    			_image_thunk_data32 = (_IMAGE_THUNK_DATA32*)(fileBufferp + RVA_to_FOA(fileBufferp,_image_import_descriptor->OriginalFirstThunk));
    			while(*((DWORD*)_image_thunk_data32) != 0){  //直接把INT表中元素当成DWORD来判断即可
    				if((_image_thunk_data32->Ordinal & 0x80000000) == 0x80000000){
    					printf("(以序号方式导出)序号:%d\n",_image_thunk_data32->Ordinal & 0x7FFFFFFF);
    				}else{
    					_image_import_by_name = (_IMAGE_IMPORT_BY_NAME*)(fileBufferp + RVA_to_FOA(fileBufferp,_image_thunk_data32->Ordinal));
    					printf("(以函数名方式导出)函数名RVA:%08X--->(hint:%04X)%s\n",_image_thunk_data32->Function,_image_import_by_name->Hint,_image_import_by_name->Name);
    				}
    				_image_thunk_data32++;
    			}
    		}else{
    			printf("无INT表\n");
    		}
    
    		//打印运行前的IAT表
    		printf("***********运行前IAT表***********\n");
    		_image_thunk_data32 = (_IMAGE_THUNK_DATA32*)(fileBufferp + RVA_to_FOA(fileBufferp,_image_import_descriptor->FirstThunk));
    		while(*((DWORD*)_image_thunk_data32) != 0){
    			if((_image_thunk_data32->Ordinal & 0x80000000) == 0x80000000){
    				printf("(以序号方式导出)序号:%d\n",_image_thunk_data32->Ordinal & 0x7FFFFFFF);
    			}else{
    				_image_import_by_name = (_IMAGE_IMPORT_BY_NAME*)(fileBufferp + RVA_to_FOA(fileBufferp,_image_thunk_data32->Ordinal));
    				printf("(以函数名方式导出)函数名RVA:%08X--->(hint:%04X)%s\n",_image_thunk_data32->Function,_image_import_by_name->Hint,_image_import_by_name->Name);
    			}
    			_image_thunk_data32++;
    		}
    
    		//导出表后移一个结构
    		_image_import_descriptor++;
    	}  //在这里加断点可以方便导出表一个结构一个结构的打印
    
    }
    
    int main(int argc, char* argv[])
    {
    	char* filePath = "D:/IPMsg/ipmsg.exe";
    	//char* filePath = "D:/C-language/file/notepad.exe";
    	importTable_print(filePath);
    	return 0;
    }
    

2.验证并对比ipmsg和notepad差别

  • 先验证ipmsg.exe的导入表:

    由于VC6的控制台的显示行数有限,可以在上述代码的我标记的位置加断点,可以把导入表一个结构一个结构打印出来,但是由于篇幅有限,直接一次性打印,虽然显示不全,但是可以通过导入表的后面几个结构来验证正确性

    • 使用PETool打开ipmsg.exe,点击I按钮,选择导入表的倒数第三个结构COMCTL32.dll,再分别点击查看[Original First Thunk]索引查看[First Thunk]索引和VC6打印的数据对比验证

      225734 225835

      可以发现:运行前ipmsg.exe的INT表和IAT表中数据一模一样

  • 再验证notepad.exe的导入表:

    • 使用PETool打开notepad.exe,点击I按钮,选择导入表的第一个结构comdlg32.dll,再分别点击查看[Original First Thunk]索引查看[First Thunk]索引和VC6打印的数据对比验证

      224843 224940

      发现:notepad.exe的IAT表在运行前和INT表中的数据并不一致,由于这个值0x76341E08这个地址很大,而且前面学过这么大的地址有可能是notepad.exe使用到的某一个DLL中的地址,所以我们使用OD验证一下:发现这个地址真的是notepad.exe在运行时comdlg32.dll中的内存绝对地址!(所以大胆猜测一下,对于notepad.exe这种PE文件,IAT表在程序运行前就已经把IAT表中的元素改成了函数在虚拟内存中的绝对地址,后面会学习验证猜想)

      image-20230416225215708
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值