(所有压缩包的密码均为1)
references:
- http://sandsprite.com/CodeStuff/Understanding_imports.html
- https://github.com/stephenfewer/ReflectiveDLLInjection
如果你不熟悉PE文件中va和rva的概念,可以参考这里
其实PE文件的导入表非常好理解,只要看懂下面这张图就行了
其中最关键的就是IMAGE_IMPORT_DIRECTORY
结构体,我们主要关注第一个和最后一个字段
- rvaImportLookupTable
- rvaImportAddressTable
用最简单的一句话来描述这两个字段就是前者是给PE Loader用来查找导入DLL的函数地址的,后者是给PE文件中的代码用来得到正确的函数地址的
PE Loader通过前者从DLL中获取到正确的函数地址,然后填充到后者中,最终在代码执行的时候,会通过后者来获取函数地址
这里我使用一个很简单的PE文件来举例子,我这个PE文件只有两个导入函数
分别是kernel32.ExitProcess
和user32.MessageBoxA
在IDA中打开该PE文件
查看call cs:MessageBoxA
对应的16进制为FF15D70F0000
,使用defuse.ca反编译得到如下结果
可以看到,这里call指令的操作数是从内存地址rip+0xFD7
取出来的一个QWORD,也就是说在编译和链接阶段,生成的PE文件中的代码,并不会直接call导入函数,而是从一个内存地址中取出导入函数的地址,而在PE Loader将PE正确加载到内存之前,这个地址中并没有存储正确的导入函数地址,PE Loader的工作就是根据rvaImportLookupTable
找到正确的导入函数地址,然后放到这个地址中
rip总是下一条指令的地址,即0x140001039
,那么计算出来的内存地址就是0x140002010
,Optional Header中ImageBase字段的值为0x140000000
那么这个地址的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中进行调试来更好地理解