返回
顶部

(所有压缩包的密码均为1)

references:

如果你不熟悉PE文件中va和rva的概念,可以参考这里

其实PE文件的导入表非常好理解,只要看懂下面这张图就行了

img

其中最关键的就是IMAGE_IMPORT_DIRECTORY结构体,我们主要关注第一个和最后一个字段

  • rvaImportLookupTable
  • rvaImportAddressTable

用最简单的一句话来描述这两个字段就是前者是给PE Loader用来查找导入DLL的函数地址的,后者是给PE文件中的代码用来得到正确的函数地址的

PE Loader通过前者从DLL中获取到正确的函数地址,然后填充到后者中,最终在代码执行的时候,会通过后者来获取函数地址

这里我使用一个很简单的PE文件来举例子,我这个PE文件只有两个导入函数

1698225319171

分别是kernel32.ExitProcessuser32.MessageBoxA

在IDA中打开该PE文件

1698225462002

查看call cs:MessageBoxA对应的16进制为FF15D70F0000,使用defuse.ca反编译得到如下结果

1698225565115

可以看到,这里call指令的操作数是从内存地址rip+0xFD7取出来的一个QWORD,也就是说在编译和链接阶段,生成的PE文件中的代码,并不会直接call导入函数,而是从一个内存地址中取出导入函数的地址,而在PE Loader将PE正确加载到内存之前,这个地址中并没有存储正确的导入函数地址,PE Loader的工作就是根据rvaImportLookupTable找到正确的导入函数地址,然后放到这个地址中

rip总是下一条指令的地址,即0x140001039,那么计算出来的内存地址就是0x140002010,Optional Header中ImageBase字段的值为0x140000000

1698225936074

那么这个地址的rva就是0x2010,和PE-bear中显示的USER32.dll的FirstThunk字段的值是一致的(FirstThunk就是rvaImportAddressTable的第一个entry)

这里有一段摘抄于RDI的代码(有一个地方进行了修改),大家可以结合上面理解一下

// uiValueD就是rvaImportLookupTable的地址,这里通过和0x8000000000000000进行and运算来判断是否通过ordinal来定位导入函数的地址
if (uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG)
{
    // uiLibraryAddress是导入的DLL在内存中的基地址,这里获取到导入DLL的NT Header地址
    uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;

    // 获取到导入DLL的DATA_DIRECTORY结构体的地址
    uiNameArray = (ULONG_PTR) & ((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];

    // 获取到导入DLL的导出表的地址
    uiExportDir = (uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress);

    // 从导出表中获取到导出函数数组地址
    uiAddressArray = (uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfFunctions);

    // 从前面那张导入表结构体的示意图我们知道,如果使用ordinal的方式来定位导入函数的地址,那么除了最高位,剩下的bit位将用于表示ordinal的值
    // 不过这里不知道为啥进行and运算的是0xFFFF,只有16bit,可能ordinal的值最大也就这么大了吧
    // 获取到ordinal value之后,和导出表的base字段值相减,因为每个entry占4bytes,相减的结果乘以4再加上导出函数数组的地址,就是导出函数相对于DLL基地址的偏移的地址
    uiAddressArray += ((IMAGE_ORDINAL(((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal) - ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->Base) * sizeof(DWORD));

    // 使用基地址+偏移得到导入函数的正确地址,并将该地址放到rvaImportAddressTable中
    DEREF(uiValueA) = (uiLibraryAddress + DEREF_32(uiAddressArray));
}
else
{
    // 根据导入表结构体示意图我们可以知道,如果最高bit位为0,那么uiValueD中保存的就是IMAGE_IMPORT_BY_NAME结构体的rva
    // 和PE文件的基地址相加即可得到IMAGE_IMPORT_BY_NAME结构体的地址
    uiValueB = (uiBaseAddress + DEREF(uiValueD));

    // 通过GetProcAddress获取指定名称的函数地址,并将其放入rvaImportAddressTable中
    DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress((HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name);
}

我把项目文件也放上来,大家可以在VS中进行调试来更好地理解