继续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
该字段指示的地址指向了一个以"\0"末端的字符串,字符串记录了导出表所在的文件的最初文件名
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
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |