恶意代码的亲密接触之文件搜索和API导址
解析PE文件的导出函数表
PE文件的函数导出机制是进行模块间动态调用的重要机制,对于正常的程序,相关操作是由系统加载器在程序加载前自动完成的,对用户程序是透明的。但要想在病毒代码中实现函数地址的动态解析以取代加载器,那就有必要了解函数导出表的结构了。在图1 中可以看到在PE头结构IMAGE_OPTIONAL_HEADER32结构中包含一个DataDirectory数组结构,该结构包含1 6 个成员,每个成员都是一个IMAGE_DATA_DIRECTORY 结构:
DataDirectory数组的每个结构都指向一个重要的数据结构,第一个成员指向导出函数表(索引0),第2个成员指向PE文件的引入函数表(索引1)。DataDirectory中的第一个成员指向导出函数表的IMAGE_EXPORT_DIRECTORY 结构:
AddressOfFunctions 是一个双字数组,包含了所有导出函数的RVA,另外两个成员AddressOfNames也是一个双字数组,包含了指向导出函数名字的字符串的R V A , AddressOfNameOrdinals 是一个字数组(16bit ),和AddressOfNames 数组是并行的,和AddressOfNames数组一起确定了相应引出函数的序号,该序号可直接用于索引AddressOfFunctions 数组获取导出函数的地址。因此病毒搜索指定的API 就包含了如下步骤:
a)获取NumberOfNames 的值以及AddressOfNames、AddressOfNameOrdinals 和AddressOfFunctions 的数组的地址。
b)搜索AddressOfNames 数组,按字符串对比,若找到相应的A P I,转d
c)若NumberOfNames 名字尚未全部搜索完毕,转b 继续搜索,若搜索完毕,则表明未找到进行错误处理,这一步通常可以省略,因为我们已经知道相应的DLL 中肯定导出了相应的函数。
d)获取当前函数名字指针在AddressOfNames 数组中的索引,在AddressOfNameOrdinals 数组中取出以该值索引的函数序号,以该序号值作为AddressOfFunctions 数组的索引,在AddressOfFunctions 数组中取出导出函数的RVA值,加上基址就得到了运行时导出函数的地址。
看起来似乎比较罗嗦,实际上这是PE设计时为考虑灵活性而做出的牺牲。不过实现起来还是比较简单的,通常汇编代码编译后不到100 字节。以下是在Kernel32 搜索GetProcAddress 的完整代码:
在前面解析导出函数表获取API地址的时候,采用的是直接比较字符串的方法判断是不是找到了相应的API,其实还可以计算函数名字的hash,然后同预计算的hash进行比对,现代的PE 病毒更多采用的hash的方法,其原因在于一般的函数名字长度都大于4字节,而用hash只要占用4个字节或2个字节,可以节省空间,此外还有抗病毒分析的作用,因为hash要比字符串名字费解得多。hash算法的设计只要能保证无冲突即可,可以用crc等成熟算法,也可以设计自己的简单算法。在Elkern 中就使用了crc16算法。
PE文件的函数导出机制是进行模块间动态调用的重要机制,对于正常的程序,相关操作是由系统加载器在程序加载前自动完成的,对用户程序是透明的。但要想在病毒代码中实现函数地址的动态解析以取代加载器,那就有必要了解函数导出表的结构了。在图1 中可以看到在PE头结构IMAGE_OPTIONAL_HEADER32结构中包含一个DataDirectory数组结构,该结构包含1 6 个成员,每个成员都是一个IMAGE_DATA_DIRECTORY 结构:
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; |
DataDirectory数组的每个结构都指向一个重要的数据结构,第一个成员指向导出函数表(索引0),第2个成员指向PE文件的引入函数表(索引1)。DataDirectory中的第一个成员指向导出函数表的IMAGE_EXPORT_DIRECTORY 结构:
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; |
AddressOfFunctions 是一个双字数组,包含了所有导出函数的RVA,另外两个成员AddressOfNames也是一个双字数组,包含了指向导出函数名字的字符串的R V A , AddressOfNameOrdinals 是一个字数组(16bit ),和AddressOfNames 数组是并行的,和AddressOfNames数组一起确定了相应引出函数的序号,该序号可直接用于索引AddressOfFunctions 数组获取导出函数的地址。因此病毒搜索指定的API 就包含了如下步骤:
a)获取NumberOfNames 的值以及AddressOfNames、AddressOfNameOrdinals 和AddressOfFunctions 的数组的地址。
b)搜索AddressOfNames 数组,按字符串对比,若找到相应的A P I,转d
c)若NumberOfNames 名字尚未全部搜索完毕,转b 继续搜索,若搜索完毕,则表明未找到进行错误处理,这一步通常可以省略,因为我们已经知道相应的DLL 中肯定导出了相应的函数。
d)获取当前函数名字指针在AddressOfNames 数组中的索引,在AddressOfNameOrdinals 数组中取出以该值索引的函数序号,以该序号值作为AddressOfFunctions 数组的索引,在AddressOfFunctions 数组中取出导出函数的RVA值,加上基址就得到了运行时导出函数的地址。
看起来似乎比较罗嗦,实际上这是PE设计时为考虑灵活性而做出的牺牲。不过实现起来还是比较简单的,通常汇编代码编译后不到100 字节。以下是在Kernel32 搜索GetProcAddress 的完整代码:
push esi ;esi=VA Kernel32.BASE ;edi=RVA K32.pehdr mov ebp,esi mov edi,[ebp+edi+peh.DataDirectory] push edi esi mov eax,[ebp+edi+peexc.AddressOfNames] mov edx,[ebp+edi+peexc.AddressOfNameOrdinals] call @F db "GetProcAddress",0 @@: pop edi mov ecx,15 sub eax,4 next_: add eax,4 add edi,ecx sub edi,15 mov esi,[ebp+eax] add esi,ebp mov ecx,15 repz cmpsb ;进行字符串比较,判断是否为要查找的函数 jnz next_ pop esi edi sub eax,[ebp+edi+peexc.AddressOfNames] shr eax,1 add edx,ebp movzx eax,word [edx+eax] add esi,[ebp+edi+peexc.AddressOfFunctions] add ebp,[esi+eax*4] ;ebp=Kernel32.GetProcAddress.addr ;use GetProcAddress and hModule to get other func pop esi ;esi=kernel32 Base |
在前面解析导出函数表获取API地址的时候,采用的是直接比较字符串的方法判断是不是找到了相应的API,其实还可以计算函数名字的hash,然后同预计算的hash进行比对,现代的PE 病毒更多采用的hash的方法,其原因在于一般的函数名字长度都大于4字节,而用hash只要占用4个字节或2个字节,可以节省空间,此外还有抗病毒分析的作用,因为hash要比字符串名字费解得多。hash算法的设计只要能保证无冲突即可,可以用crc等成熟算法,也可以设计自己的简单算法。在Elkern 中就使用了crc16算法。