1.6节中找到了kernel32.dll的基地址,这一节,来解决第二个重要问题,即解析kernel32.dll的导出表,找到LoadLibraryA和GetProcAddress的地址。
DLL导出的函数信息位于导出表中,因此,首先,要在PE文件中找到导出表(IMAGE_EXPORT_DIRECTORY)的地址,这位于数据目录(IMAGE_DATA_DIRECTORY)中。而数据目录位于PE扩展头(IMAGE_OPTIONAL_HEADER)的最后。
下图为PE文件的结构:
PE文件开头为一个DOS头(IMAGE_DOS_HEADER),大小固定为64个字节,其中,最后4个字节指定PE头的偏移量(IMAGE_NT_HEADER),即PE头标识(Signture)的偏移量,中间有一段大小不固定的DOS Stub。PE头标识之后是标准PE头(IMAGE_FILE_HEADER),大小固定为20字节,之后是扩展PE头(IMAGE_OPTIONAL_HEADER32),数据目录就位于扩展PE头的末尾。
数据目录包含16种(导出表,导入表,资源表等),每种占8个字节,导出表项位于第一项。图44中已经标出了数据目录距离DOS头的偏移,为120(0x78)个字节。(注:引用的原图有误,中间少算了8个字节)。这个偏移也就是导出表项的偏移,因为导出表项是第一项。
导出表项的偏移并不是导出表的偏移,每个数据目录项8个字节,共两个字段,如下:
/*****************************************************************************/
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;
/*****************************************************************************/
第一个字段VirtualAddress表示导出表距离PE文件开头的偏移,第二个字段Size为导出表的大小,根据这两个字段,可以定位导出表。
下面来看导出表的具体结构,如下:
/*****************************************************************************/
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base; // +0x10 函数起始需要
DWORD NumberOfFunctions; // +0x14 导出函数个数
DWORD NumberOfNames; // +0x18以函数名导出的函数个数
DWORD AddressOfFunctions; // +0x1c 导出函数地址表
DWORD AddressOfNames; // +0x20 函数名称地址表
DWORD AddressOfNameOrdinals; // +0x24函数序号地址表
} IMAGE_EXPORT_DIRECTORYM, *pIMAGE_EXPORT_DIRECTORY;
/*****************************************************************************/
标红的字段为重要的字段。其中,AddressOfFunctions所指的区域依次保存了所有导出函数的地址,个数由NumberOfFunctions决定。AddressOfNames所指的区域依次保存了对应函数的函数名,个数由NumberOfNames决定。AddressOfNameOrdinals与AddressOfNames一一对应,指定函数在AddressOfFunctions的索引值,即将函数名与地址联系起来。需要注意的是AddressOfNameOrdinals中的序号为一个字(两个字节),而不是像地址为4个字节。
AddressOfFunctions的个数可能会大于AddressOfNames,因为有的函数可能没有导出函数名,此时,没有AddressOfNameOrdinals,就找不到函数地址了。
记住,我们要找的两个函数LoadLibraryA和GetProcAddress,我们知道的只有函数名,因此,查找从AddressOfNames开始,比较函数名,然后根据索引(第几个),在AddressOfNameOrdinals查找到函数在AddressOfFunctions的索引值(前面说了,AddressOfNames和AddressOfNameOrdinals一一对应),然后,从AddressOfFunctions找到对应索引处的地址即可。注意,PE文件中的地址均为相对地址(RVA),因此,实际使用时要加上模块(exe,dll)实际加载基地址,换为实际地址。
现在,又到了写代码实现的时候了,比较麻烦的地方是字符串的比较,查找指定名称函数的代码可以实现为一个函数,输入为函数的名称,输出为函数的地址。我们还要用到上一节的内容,即获取kernel32.dll的基地址。
/*****************************************************************************/
// example_9 从kernel32.dll中查找函数地址
#include <stdio.h>
// 获取kernel32.dll的基地址
int get_kernel32_base()
{
__asm
{
mov eax, fs:[0x30] // PEB
mov eax, [eax+0x0c] // PEB->Ldr
mov eax, [eax+0x1c] // PEB->Ldr.InInitializationOrderModuleList.Flink(指向第一个元素)
mov eax, [eax] // 指向第二个元素
mov eax, [eax+0x08] // kernel32.dll基地址
}
}
int compare_string( char* symbol1, char* symbol2 )
{
__asm
{
mov esi, [ebp+8]; // symbol1
mov edi, [ebp+12]; // symbol2
xor eax, eax
compare_loop:
mov al, [esi] // 取下一个字符
mov bl, [edi]
cmp al, bl // 比较是否相等
jnz equal_no
test al, al // symbol2是否结尾
jz equal_yes
test bl, bl // symbol1是否结尾
jz equal_yes
inc esi
inc edi
jmp compare_loop
equal_no:
sub al, bl
equal_yes:
}
}
int get_func_addr( char* symbol )
{
__asm
{
call get_kernel32_base // 获取kernel32.dll基地址
mov ebx, [eax+0x3c] // PE头的偏移
mov ebx, [eax+ebx+0x78] // 数据目录导出表项的偏移
add ebx, eax // 导出表地址
mov ecx, [ebx+0x18] // NumberOfNames
mov edx, [ebx+0x20] // AddressOfNames的RVA
add edx, eax // 转换为VA
search_loop:
jecxz error_done
dec ecx
pushad
mov esi, [edx+ecx*4]
add esi, eax // 函数名
push esi
mov edi, [ebp+8]; // 参数: char* symbol
push edi
call compare_string // 比较函数名
add esp, 8
test eax, eax // eax为则相等
popad
jnz search_loop
mov edx, [ebx+0x24] // 函数序号表RVA
add edx, eax // 函数序号表VA
mov cx, [edx+ecx*2] // 函数序号
mov edx, [ebx+0x1c] // 函数地址表RVA
add edx, eax // 函数地址表VA
mov edx, [edx+ecx*4] // 函数地址RVA
add edx, eax // 函数地址VA
jmp done
error_done:
xor eax, eax
done:
mov eax, edx
}
}
int main()
{
printf("0x%x\n", get_func_addr("LoadLibraryA"));
return 0;
}
/*****************************************************************************/