简介
ITA(Import Table Address) 包含有关 PE 文件的信息,例如使用的函数名以及 DLL,此类信息常用于对二进制文件进行标记和检测。例如:
可以看到,这个 PE 文件导入了 CreateRemoteThread
、OpenProcess
、VirtualAllocEx
、WriteProcessMemory
等函数,Shellcode 加载流程尽收眼底。
想要隐藏这些内容,可以使用 GetProcAddress
、GetModuleHandle
或 LoadLibrary
在运行时动态加载这些函数。
但这又会出现其他问题。
- 动态导入的函数相关字符串会出现在 PE 文件中,这个可以用于签名检测
GetProcAddress
和GetModuleHandle
函数的导入信息也会出现在 ITA 中
想隐藏字符串,那只能通过替换,可以通过字符串加密、Hash 运算等方法;对于 GetProcAddress
和 GetModuleHandle
函数的信息,可以通过自实现来隐藏。两者结合。
Hash 函数
Hash 函数的选择,最好是能够轻量实现,并且分布均匀(以防冲突),这里的 Demo 选用 JenkinsOneAtATime32Bit
算法,实现来自 VX-API GitHub 仓库。
1 |
|
为了灵活和方便使用,设置了 HASHA
宏。
实现 GetModuleHandle
GetModuleHandle
函数用于在内存中检索指定 DLL 的句柄。函数返回 DLL 的句柄 (HMODULE
) 或 NULL
。
GetModuleHandle 原理
HMODULE
数据类型是加载的 DLL 的基地址,表示 DLL 在进程地址空间中的位置。因此,替换函数的目标是检索指定 DLL 的基地址。
1 | typedef struct _PEB { |
PEB 结构的 PEB_LDR_DATA Ldr
成员包含进程中加载的 DLL 的信息。
1 | typedef struct _PEB_LDR_DATA { |
对应 LIST_ENTRY
结构是一个双向链表,
1 | typedef struct _LIST_ENTRY { |
微软文档中说明,_PEB_LDR_DATA
的 InMemeoryOrderModuleList 成员包含进程加载模块的双向链表的头部。列表中的每个项都是指向 LDR_DATA_TABLE_ENTRY 结构的指针。并且给出了 _LDR_DATA_TABLE_ENTRY
结构
1 | typedef struct _LDR_DATA_TABLE_ENTRY { |
因此,需要关注的两个重点变量已经找到
- DLL 基地址: PEB -> Ldr -> InMemoryOrderModuleList -> Flink -> DllBase (这个其实不是)
- DLL 文件名: PEB -> Ldr -> InMemoryOrderModuleList -> Flink -> FullDllName
简单实现
1 |
|
对比 Process Hacker 打开的进程信息
发现 pDte->Reserved2[0]
成员是需要的 DLL 基地址。
现在枚举 DLL 文件名,通过 Hash 校验来确定目标 DLL 基地址。可以实现对函数名进行 Hash 计算
1 |
|
这样比较方便去定义,并且统一转换成大写。
接下来更新一下 Yes_GetModuleHandle
函数为 Yes_GetModuleHandle_FromHash
这里大小写使用三元运算符实现,可以定义为一个宏
1 |
|
测试一下效果,没啥问题。
上文的代码中关于
GetModuleHandle
的实现使用了winternl.h
头文件,这里可以使用 NirSoft 与 Process Hacker 的结构体来代替。最终效果 仅需导入<windows.h>
头文件。
实现 GetProcAddress
GetProcAddress
函数从 DLL 句柄中获取导出函数的地址。如果未找到函数名,返回 NULL
。
GetProcAddress 原理
要访问导出的函数,需要访问 DLL 的导出表并在其中循环查找目标函数名称。
PE 头模块时提到,导出表是定义为 IMAGE_EXPORT_DIRECTORY
的结构。微软文档
1 | typedef struct _IMAGE_EXPORT_DIRECTORY { |
需要关注的就是最后三个成员
AddressOfFunctions
- 指定导出函数地址数组的地址。AddressOfNames
- 指定导出函数名称地址数组的地址。AddressOfNameOrdinals
- 指定导出函数的序号数组的地址。
简单实现
1 | FARPROC Yes_GetProcAddress(IN HMODULE hModule, IN LPCSTR lpApiName) { |
效果还可以,与 GetModuleHandle 一样,可以加入 Hash 校验,来确定目标函数。
1 | FARPROC Yes_GetProcAddress_FromHash(IN HMODULE hModule, IN UINT32 ui32FuncHash) { |
在 Main 函数中调用检查
1 |
|
动态加载 Win Api
目前已经实现了 GetModuleHandle 和 GetProcAddress,可以动态加载函数了,在加载之前,需要声明函数类型,以便在获取函数地址后进行强制转换。
Demo 计划实现一个代替 printf 函数的宏实现
1 |
其中用到了以下函数
- wsprintfA (user32.dll)
- HeapFree (kernel32.dll)
- RtlAllocateHeap (ntdll.dll)
- GetStdHandle (kernel32.dll)
- GetProcessHeap (kernel32.dll)
- WriteConsoleA (kernel32.dll)
另外还会用到 LoadLibraryA (kernel32.dll),在某些情况进程中没有 user32.dll 时来加载它。
函数定义
按照声明格式写好模板
1 | typedef HMODULE(WINAPI* fnLoadLibraryA)(LPCSTR lpLibFileName); |
准备好 Hash
1 | // DLL Name Hash |
创建全局函数指针变量,用 api_init 函数初始化所有函数指针
1 | fnLoadLibraryA pLoadLibraryA; |
这样就完成了预期需求。
测试
main.c
1 | int main() { |
main 函数调用 api_init 对自定义 API 进行初始化,然后使用 PRINTA 宏打印信息。
整个项目仅需包含 Windows.h
头文件,来到 Pe-Bear 可以看到,Kernel32.dll 的导入表中的敏感函数导入信息已经去掉,同样加载的 User32.dll 和 NTDLL.dll 也没有显示。
代码
api.c
1 |
|
main.h
1 |
|
main.c
1 |
|
Good Luck.