Windows导出表结构
Windows导出表分为三个部分:
- 导出地址表EAT,里面存的函数的RVA
- 导出名称表ENT,存的是函数名
- 序号表EOT,作用是建立ENT到EAT的连接。当通过函数名查找函数时,先在ENT里找到函数名对应的下标index1,在EOT通过index1可以找到函数在EAT中的下标index2,进而获取到对应函数的RVA。
GetProcAddress实现
函数定义
FARPROC GetProcAddress(
[in] HMODULE hModule,
[in] LPCSTR lpProcName
);
第一个参数是一个HMODULE,就是我们要查找函数所在的那个dll。
第二个参数是函数名,也可以是EOT中的序号,这个GetProcAddress是支持通过Ordinal查找函数的。
GetProcAddress需要先解析PE文件头然后获取到导出表所在的位置:
PIMAGE_EXPORT_DIRECTORY ExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + OptHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
导出表的结构定义如下:
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;
AddressOfNames是一个DWORD类型的指针的RVA,指向ENT。ENT里并不直接存放名称字符串,而是存放着指向函数名称字符串的RVA。所以总体的转换关系应该是先获取到ENT的RVA,再通过ENT获取到函数名称:
DWORD* pdwNameAddr = (DWORD*)(pBase + ExportDir->AddressOfNames); //获取ENT
CHAR* pFuncName = (CHAR*)(pBase + pdwNameAddr[i]); //获取函数名
AddressOfFunctions也是一个DWORD类型的指针的RVA,指向的是EAT,EAT里存的就是我们要找的函数的RVA。
AddressOfNameOrdinals是一个WORD类型的指针的RVA,取值的时候下标是ENT里的下标,取出来的值就是EAT里对应函数的RVA。
通过函数名查找
如果需要通过函数名查找,那么就需要在ENT中找到函数名对应的下标index1,然后通过EOT找到在EAT中对应的位置,即EOT[index1] = index2,这个index2就是EAT中函数的下标。
WORD ordinal = pwOrdinals[i]; //获取函数在EAT中的下标
DWORD dwFunctionRVA = pdwFunctions[ordinal]; //获取函数的RVA
通过Ordinal查找
除了通过名称获取函数之外,还有一种方法是通过序号获取,相当于跳过了在ENT中查找这个步骤:
WORD wOrg = (WORD)lpProcName - ExportDir->Base;
DWORD dwFunctionRVA = pdwFunctions[wOrg];
Forwarded Export
在 PE 导出表中,如果一个导出项的 RVA 指向的地址位于导出目录(Export Directory)所在的内存范围内,那么这个 RVA 实际上并不是函数代码的入口,而是一个以 null 结尾的 ASCII 字符串。
这个字符串的格式通常是:DllName.ExportName(例如 NTDLL.RtlAllocateHeap)。
我们需要在获取 RVA 后增加一个判断逻辑:如果地址在导出表范围内,就解析字符串并递归调用。
BOOL CheckForwardedExport(_In_ DWORD RVA, _In_ IMAGE_OPTIONAL_HEADER OptHeader) {
return (RVA >= OptHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
&& RVA < OptHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress + OptHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size);
}
当发现了Forwarded Export之后,我们需要解析一下NTDLL.RtlAllocateHeap这个类型的字符串,前半部分是需要加载的dll,这里我直接用LoadLibrary加载了(有空我也可以实现一下),后半部分是要加载的函数名,和前面的查找过程差不多的。
代码如下:
VOID* ForwardedExport(_In_ const CHAR* pForwardStr) {
const CHAR* dot = strchr(pForwardStr, '.');
CHAR pDll[MAX_PATH] = { 0 };
CHAR pFunctionName[MAX_PATH] = { 0 };
memcpy(pDll, pForwardStr, dot - pForwardStr);
strcat_s(pDll, MAX_PATH, ".dll");
strcpy_s(pFunctionName, MAX_PATH, dot + 1);
HMODULE hMod = LoadLibraryA(pDll);
return MyGetProcAddress(hMod, pFunctionName);
}
结果验证
验证结果是采用了的系统API GetProcAdress看获取同一个函数的地址是不是一样的:
WORD wOrg = 10;
VOID* MyAddress = MyGetProcAddress(hmodule, (LPCSTR)wOrg);
FARPROC SysAddress = GetProcAddress(hmodule, (LPCSTR)wOrg);
std::cout << "MyGetProcAddress: " << std::hex << MyAddress << std::endl;
std::cout << "GetProcAddress: " << std::hex << SysAddress << std::endl;
std::cout << "MyGetProcAddress == GetProcAddress: " << (MyAddress == SysAddress) << std::endl;
1368

被折叠的 条评论
为什么被折叠?



