53-IAT 隐藏和混淆——自定义 GetProcAddress
简介
GetProcAddress
WinAPI 函数从指定的模块句柄中获取导出函数的地址。如果在指定的模块句柄中未找到函数名,该函数将返回 NULL
。
在本模块中,将实现一个代替 GetProcAddress
的函数。新函数的原型如下所示。
FARPROC GetProcAddressReplacement(IN HMODULE hModule, IN LPCSTR lpApiName) {}
GetProcAddress 的工作原理
首先需要解决的问题是 GetProcAddress
WinAPI 如何查找并获取函数的地址。
hModule
参数是已加载 DLL 的基本地址。这是在进程的地址空间中找到 DLL 模块的地址。牢记这一点,可以通过循环遍历提供的 DLL 内部导出的函数,并检查是否存在目标函数的名称来检索函数地址。如果存在有效匹配,则检索地址。
要访问导出的函数,需要访问 DLL 的导出表并在其中循环查找目标函数名称。
读取 - 导出表结构
回想一下解析 PE 标头模块,提到导出表是一个结构,定义为 IMAGE_EXPORT_DIRECTORY
。
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;
这个模块中结构的相关成员是最后三个。
AddressOfFunctions
- 指定导出函数地址数组的地址。AddressOfNames
- 指定导出函数名称地址数组的地址。AddressOfNameOrdinals
- 指定导出函数序数的数组地址。
回忆 - 访问导出表
让我们回顾一下如何检索导出目录 IMAGE_EXPORT_DIRECTORY
。下面的代码片段应该是熟悉的,因为它已在 解析 PE 头部 模块中进行了说明。
函数开头的变量 pBase
是代码片段中唯一新增的内容。创建此变量是为了避免在将相对虚拟地址 (RVAs) 转换为虚拟地址 (VAs) 时稍后进行类型转换。在将 PVOID
数据类型添加到值时,Visual Studio 编译器会引发错误,因此将 hModule
强制转换为 PBYTE
。
FARPROC GetProcAddressReplacement(IN HMODULE hModule, IN LPCSTR lpApiName) {
// 我们这样做是为了避免每次使用 'hModule' 时进行转换
PBYTE pBase = (PBYTE) hModule;
// 获取 DOS 头并执行签名检查
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
return NULL;
// 获取 NT 头并执行签名检查
PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
return NULL;
// 获取可选头
IMAGE_OPTIONAL_HEADER ImgOptHdr = pImgNtHdrs->OptionalHeader;
// 获取图像导出表
// 这是导出目录
PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY) (pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
// ...
}
访问导出函数
获取到 IMAGE_EXPORT_DIRECTORY
结构的指针后,就可以遍历导出的函数。NumberOfFunctions
成员指定了由 hModule
导出的函数数量。因此,循环的最大迭代次数应等于 NumberOfFunctions
。
for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++){
// 搜索目标导出的函数
}
构建搜索逻辑
下一步是为函数构建搜索逻辑。构建搜索逻辑需要使用 AddressOfFunctions
、AddressOfNames
和 AddressOfNameOrdinals
,它们都是包含 RVAs 的数组,引用导出表中的单个唯一函数。
typedef struct _IMAGE_EXPORT_DIRECTORY {
// ...
// ...
DWORD AddressOfFunctions; // 从映像基址开始的 RVA
DWORD AddressOfNames; // 从映像基址开始的 RVA
DWORD AddressOfNameOrdinals; // 从映像基址开始的 RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
由于这些元素是 RVAs,因此必须添加模块的基址 pBase
来获取 VA。前两个代码片段应该比较简单。它们分别获取函数的名称和函数的地址。第三个片段获取函数的序号,这将在下一节中详细说明。
// 获取函数名称数组指针
PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
// 获取函数地址数组指针
PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
// 获取函数序号数组指针
PWORD FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
序号简介
函数的序号是整数,表示 DLL 中导出函数表中函数的位置。导出表组织为函数指针的列表(数组),每个函数根据其在表中的位置分配一个序号值。
![image](https://maldevacademy.s3.amazonaws.com/images/Intermediate/ordinals-getproc.png)
请注意,序号值用于标识函数的地址,而不是其名称。导出表采用这种方式来处理函数名称不可用或不唯一的情况。此外,使用序号获取函数的地址比使用名称更快。因此,操作系统使用序号获取函数的地址。
例如,VirtualAlloc
的地址等于 FunctionAddressArray[VirtualAlloc 的序号]
, 其中 FunctionAddressArray
是从导出表获取的函数地址数组指针。
基于此,下面的代码段将打印指定模块的函数数组中每个函数的序号值。
// 获取函数名称数组指针
PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
// 获取函数地址数组指针
PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
// 获取函数序号数组指针
PWORD FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
// 循环所有导出函数
for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++){
// 获取函数的名称
CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
// 获取函数的序号
WORD wFunctionOrdinal = FunctionOrdinalArray[i];
// 打印
printf("[ %0.4d ] 名称: %s -\t 序号: %d\n", i, pFunctionName, wFunctionOrdinal);
}
GetProcAddressReplacement
部分演示
尽管 GetProcAddressReplacement
尚未完成,但现在它应该输出函数名称及其关联的序号。要测试迄今为止已构建的内容,请使用以下参数调用该函数:
GetProcAddressReplacement(GetModuleHandleA("ntdll.dll"), NULL);
正如预期的那样,函数名称和函数的序号被打印到控制台。
![image](https://maldevacademy.s3.amazonaws.com/images/Intermediate/custom-getproc-109913387-f0fdcc3d-e9aa-48f3-bb97-615758130bad.png)
通过序数值获取地址
有了函数的序数值,可以得到该函数的地址。
// 获取函数名称数组指针
PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
// 获取函数地址数组指针
PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
// 获取函数序数值数组指针
PWORD FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
// 遍历所有导出的函数
for (DWORD i = 0;i < pImgExportDir->NumberOfFunctions;i++){
// 获取函数名
CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
// 获取函数序数值
WORD wFunctionOrdinal = FunctionOrdinalArray[i];
// 通过函数序数值获取函数地址
PVOID pFunctionAddress = (PVOID) (pBase + FunctionAddressArray[wFunctionOrdinal]);
printf("[ %0.4d ] 名称: %s - 地址: 0x%p - 序数值: %d\n", i, pFunctionName, pFunctionAddress, wFunctionOrdinal);
}
为验证此功能,使用 xdbg 打开 notepad.exe
,然后查看 ntdll.dll
的导出信息。
![image](https://maldevacademy.s3.amazonaws.com/images/Intermediate/custom-getproc-209914072-4c8104f3-6208-42c4-8822-479c44d291ce.png)
上图显示 A_SHAUpdate
的地址在 xdbg 和使用 GetProcAddressReplacement
函数中均为 0x00007FFD384D2D10
。不过请注意,每个进程的 Windows 加载器会生成一个新的序数值数组,因此函数的序数值不尽相同。
GetProcAddressReplacement 代码
完整的函数代码的最后一步是比较导出的函数名和目标函数名 lpApiName
。这可以使用 strcmp
函数轻松完成。最后,在匹配时返回函数地址。
FARPROC GetProcAddressReplacement(IN HMODULE hModule, IN LPCSTR lpApiName) {
// 这样做是为了避免每次使用 hModule 时进行强制转换
PBYTE pBase = (PBYTE)hModule;
// 获取 DOS 头并进行签名检查
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
return NULL;
// 获取 NT 头并进行签名检查
PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
return NULL;
// 获取可选头
IMAGE_OPTIONAL_HEADER ImgOptHdr = pImgNtHdrs->OptionalHeader;
// 获取映像导出表
PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY) (pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
// 获取函数名数组指针
PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
// 获取函数地址数组指针
PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
// 获取函数序号数组指针
PWORD FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
// 遍历所有导出的函数
for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++){
// 获取函数名
CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
// 通过其序号获取函数地址
PVOID pFunctionAddress = (PVOID)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
// 查找指定的函数
if (strcmp(lpApiName, pFunctionName) == 0){
printf("[ %0.4d ] FOUND API -\t NAME: %s -\t ADDRESS: 0x%p -\t ORDINAL: %d\n", i, pFunctionName, pFunctionAddress, FunctionOrdinalArray[i]);
return pFunctionAddress;
}
}
return NULL;
}
GetProcAddressReplacement 最终演示
下图展示了 GetProcAddress
和 GetProcAddressReplacement
同时搜索 NtAllocateVirtualMemory
地址的结果。正如预期的那样,二者都获得了正确的函数地址,因此成功构建了 GetProcAddress
的自定义实现。
![image](https://maldevacademy.s3.amazonaws.com/images/Intermediate/custom-getproc-309915517-9f411b29-61c3-4104-9d05-7fa8977ddeca.png)