导入表是PE文件中一个重要的表项,负责声明从其他的库中调入函数。一般代表着这个PE文件使用了哪些其他库的函数。
首先我们先了解下几个名词:
PE文件:PE结构的文件,一般为可执行程序,.exe、.dll、.sys、.vxd、.ocx、.com等。
VA (Virtual Address): 虚拟地址,代表着PE文件映射进内存之后的地址。
RVA (Reverse Virtual Address):相对虚拟地址,对于PE文件映射进内存之后相对基地址偏移。
FOA(File Offset Address):文件中的偏移(文件映射进内存后内存对其并不一定与文件中一致,因此ROA不一定等于RVA)。
接下来我们将通过PE文件格式去手动解析导入表的位置以及结构。
首先从文件起始开始解析为_IMAGE_DOS_HEADER结构,但实际上这个结构大多数都是没有使用的,主要是为了兼容很早以前的DOS。会使用到的数据结构有:e_magin若不等于”MZ”则代表不是一个DOS程序,随后e_lfanew代表着 _IMAGE_NT_HEADERS结构的FOA(E8)
_IMAGE_NT_HEADERS结构:
Signature 为PE代表这是个PE程序。该结构中另含有_IMAGE_FILE_HEADER与_IMAGE_OPTIONAL_HEADER64 两个结构。
导入表与导入表都存放在IMAGE_OPTIONAL_HEADER64结构的DataDirectory数组中
IMAGE_OPTIONAL_HEADER64结构:
其中DataDirectory在结构中的偏移是0x88
对于这个数组的下标描述如下:
可以看到有 IMAGE_DIRECTORY_ENTRY_IMPORT,除此之外还有一个IMAGE_DIRECTORY_ENTRY_IAT,这个是IAT(导入地址表),仅在当程序被加载完成后才生效,里面存放的是每个函数的RVA。
IMAGE_DATA_DIRECTORY数组的类型结构:
注意这里给出的是VirutalAddress(VA)需要转换为FOA在文件中查看数据。
RVA2FOA的算法首先需要找到相应的段信息
可以看到.rdata段的虚拟地址范围是7f000~B2000
文件地址在0x7d800
而我们导入表的地址为0x0a6f8c:大小是0x0794的大小
RVA2FOA的算法:
RVA-段基址+段在文件中的基址获得FOA
0x0a6f8c - 0x7f000 + 0x7d800 = 0xA578C
接下来是一个_IMAGE_IMPORT_DESCRIPTOR 的数组
_IMAGE_IMPORT_DESCRIPTOR结构:
每一个_IMAGE_IMPORT_DESCRIPTOR结构描述一个DLL。
其中name是Dll的名称的RVA。
同样的RVA2FOA
可以看到这个DLL名字叫”api-ms-win-core-rtlsupport-l1-1-0.dll”
随后我们需要获得这个Dll中的其他函数的引用,其中就有OriginalFirstThunk的结构也是一个数组,结构体中说明这是一个IMAGE_THUNK_DATA结构体的RVA。
FirstThunk在文件中与OriginalFirstThunk一致,但如果加载入内存,并做好了初始化操作,FirstThunk指向的是IAT(IMAGE_DIRECTORY_ENTRY_IAT)一张导入函数地址表。
为了保证一致我们选择OriginalFirstThunk结构。这个结构指向IMAGE_THUNK_DATA的RVA(0x0A8F40)
IMAGE_THUNK_DATA结构:
可以看到这里似乎啥都有,但一定要注意这是个union共同体,它所描述的只能是其中的一个意思。
因为这个数据在不同的情况下是不同的数据,导入函数有函数名称和ID两种,在这里也就有两种不同的表达方式。Ordinal就是函数ID。而ForwarderString就是函数名称。
但是如何区分究竟是ID导入还是函数名称导入呢。主要看这个union的最高位,如果最高位为1,那么这个就是ID导入,取低2字节(0xffff)作为ID
但是大多数函数导入的方式都是函数名称,我们试着获得第一个函数的函数名称,在结构的注释中说明这个RVA实际指向的是PIMAGE_IMPORT_BY_NAME,我们解析下
同样的RVA2FOA
随后套用PIMAGE_IMPORT_BY_NAME结构体
可以看到0x02是Hint(实际上是函数ID),而后是一个变长字符数组,函数名称为”RtlCaptureContext”
加载器会通过和我们一样的方式去获取、加载这些DLL并且通过加载入的DLL中的导出表获得需要使用的函数地址,填入IAT使得建立了程序与DLL之间的调用对接。
PETOOLS的PE编辑器: