Windows导出表结构及GetProcAddress实现

Windows导出表结构

Windows导出表分为三个部分:

  1. 导出地址表EAT,里面存的函数的RVA
  2. 导出名称表ENT,存的是函数名
  3. 序号表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;

完整的demo见https://github.com/BlackIce417/MyGetProcAddress

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值