导入地址表(IAT)Hook

前言

在上一个Hook文章中,我利用C++虚函数的特性,实现了虚函数表钩子(VMT Hook)

VMT Hook实现起来简单,但使用范围有限。只能用来挂钩有虚函数的对象。

今天介绍一个适用范围相对更广一些的Hook技术,也就是导入地址表(IAT)Hook。

什么是IAT?

如果你熟悉程序编译的过程,你会知道链接分为两种:

  • 静态链接:代码从其所在的静态链接库中拷贝到最终的可执行程序中,在程序被执行时,这些代码会被装入到该进程的虚拟地址空间
  • 动态链接:代码被放到动态链接库中,在程序执行时,动态链接库的全部内容会被映射到运行时相应进行的虚拟地址的空间

而IAT(导入地址表)就是动态链接的实现方法。

PE标头包含许多数据表,其中包含有关可执行文件的不同信息。而IAT顾名思义,就是保存了外部的动态链接库中导出函数地址的表。

在运行时,操作系统解析导入表,尝试加载指定的dll并解析导入函数的地址。如果此过程失败,则程序将无法加载

上图提示,找不到VC++运行库

攻击方法

我们可以用两种方法来攻击它:

  1. 直接篡改可执行程序的文件(不推荐,因为如今大多数程序都有签名,大多数杀软会标记被篡改过的程序)
  2. 在运行时注入代码并修改IAT

通过在运行时注入代码,我们可以避免在硬盘上留下痕迹。尽管仍然可能会被检测到,但与直接篡改文件相比,安全得多。

实现

先从基础开始,先尝试Hook我们自己的进程IAT。用我们自己的函数覆盖Kernel32!Sleep的地址。

定义框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 用于RVA(相对虚拟地址)转VA(虚拟地址)
template <typename T>
T rva_to_va(void* base, ptrdiff_t rva) {
static_assert(std::is_pointer<T>::value, "rva_to_va return type must be a pointer.");
return reinterpret_cast<T>(static_cast<uint8_t*>(base) + rva);
}

// 用于保存原始函数的地址
using sleep_t = void* (WINAPI*)(DWORD);
sleep_t original_sleep;

// 我们的钩子函数
void WINAPI my_sleep(DWORD ms) {
}

bool hook_iat(const char* module_name, const char* function_name, void* hook_func, void** original_func) {
return false;
}

int main() {
void* original_func;
hook_iat("kernel32.dll", "Sleep", my_sleep, &original_func);
}

PE标头

MSDN上的PE格式文档

本文假定你熟悉PE格式中的IAT部分。

我们要分析IAT,所以得在标头中定位IAT

首先得获得指向PE Header开头的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 获取DOS头指针
auto dos_header = reinterpret_cast<IMAGE_DOS_HEADER*>(GetModuleHandle(nullptr));
// 获取NT头指针
auto nt_header = rva_to_va<IMAGE_NT_HEADERS*>(dos_header, dos_header->e_lfanew);
// 特判,保证获取到了有效的NT头指针
if (nt_header->Signature != IMAGE_NT_SIGNATURE)
return false;

// 获取导入描述符指针
auto import_descriptors = rva_to_va<IMAGE_IMPORT_DESCRIPTOR*>(dos_header,
nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
// 循环解析导入描述符中的条目
for (size_t i = 0; import_descriptors[i].Characteristics != 0; i++) {
// 获得模块名,并输出
auto dll_name = rva_to_va<char*>(dos_header, import_descriptors[i].Name);
std::cout << dll_name << std::endl;
}

如果编译并运行该代码,则会打印你的进程使用的所有导入模块(可能因编译器而异):

1
2
3
4
KERNEL32.dll
MSVCP140D.dll
VCRUNTIME140D.dll
ucrtbased.dll

找到了所有模块后,就可以开始找函数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for (size_t i = 0; import_descriptors[i].Characteristics != 0; i++) {
auto dll_name = rva_to_va<char*>(dos_header, import_descriptors[i].Name);
std::cout << '[' << dll_name << ']' << std::endl;

if (!import_descriptors[i].FirstThunk || !import_descriptors[i].OriginalFirstThunk)
return false;

auto thunk = rva_to_va<IMAGE_THUNK_DATA*>(dos_header, import_descriptors[i].FirstThunk);
auto original_thunk = rva_to_va<IMAGE_THUNK_DATA*>(dos_header, import_descriptors[i].OriginalFirstThunk);
for (; original_thunk->u1.Function; original_thunk++, thunk++) {
// 跳过通过序数导入的
if (original_thunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)
continue;

auto import = rva_to_va<IMAGE_IMPORT_BY_NAME*>(dos_header, original_thunk->u1.AddressOfData);
std::cout << import->Name << std::endl;
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[KERNEL32.dll]
GetModuleHandleW
FreeLibrary
VirtualQuery
GetProcessHeap
HeapFree
HeapAlloc
GetLastError
GetStartupInfoW
InitializeSListHead
GetSystemTimeAsFileTime
GetCurrentProcessId
QueryPerformanceCounter
IsProcessorFeaturePresent
TerminateProcess
GetCurrentProcess
SetUnhandledExceptionFilter
UnhandledExceptionFilter
WideCharToMultiByte
MultiByteToWideChar
RaiseException
IsDebuggerPresent
GetCurrentThreadId
GetProcAddress
Sleep
TerminateProcess
TlsGetValue
UnhandledExceptionFilter
VirtualProtect
VirtualQuery
...

OK,我们已经成功找到了对应模块的导入函数。下面就是通过修改导入表来实现IAT Hook

实现IAT Hook

修改IAT很简单,找到需要修改的函数后,使用VirtualProtect去除内存保护,写入新地址,然后再恢复内存保护

完整的Hook IAT函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
bool hook_iat(const char* module_name, const char* function_name, void* hook_func, void** original_func) {
// 获取DOS头指针
auto dos_header = reinterpret_cast<IMAGE_DOS_HEADER*>(GetModuleHandle(nullptr));
// 获取NT头指针
auto nt_header = rva_to_va<IMAGE_NT_HEADERS*>(dos_header, dos_header->e_lfanew);
// 特判,保证获取到了有效的NT头指针
if (nt_header->Signature != IMAGE_NT_SIGNATURE)
return false;

// 获取导入描述符指针
auto import_descriptors = rva_to_va<IMAGE_IMPORT_DESCRIPTOR*>(dos_header,
nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

// 循环解析导入描述符中的条目
for (size_t i = 0; import_descriptors[i].Characteristics != 0; i++) {
auto dll_name = rva_to_va<char*>(dos_header, import_descriptors[i].Name);
// 如果不是我们要找的模块,就跳过
if (_strcmpi(dll_name, module_name) != 0)
continue;

if (!import_descriptors[i].FirstThunk || !import_descriptors[i].OriginalFirstThunk)
return false;

auto thunk = rva_to_va<IMAGE_THUNK_DATA*>(dos_header, import_descriptors[i].FirstThunk);
auto original_thunk = rva_to_va<IMAGE_THUNK_DATA*>(dos_header, import_descriptors[i].OriginalFirstThunk);
for (; original_thunk->u1.Function; original_thunk++, thunk++) {
// 跳过通过序数导入的
if (original_thunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)
continue;

auto import = rva_to_va<IMAGE_IMPORT_BY_NAME*>(dos_header, original_thunk->u1.AddressOfData);
// 如果不是我们要找的函数,就跳过
if (_strcmpi(function_name, import->Name) != 0)
continue;

// 找到了我们要Hook的函数
// 先将内存保护改为可写入
MEMORY_BASIC_INFORMATION mbi;
VirtualQuery(thunk, &mbi, sizeof(mbi));
if (!VirtualProtect(mbi.BaseAddress, mbi.RegionSize, PAGE_EXECUTE_READWRITE, &mbi.Protect))
return false;

// 保存原函数地址
*original_func = reinterpret_cast<void**>(thunk->u1.Function);
// 将Hook函数地址覆盖上去
thunk->u1.Function = reinterpret_cast<uintptr_t>(hook_func);
// 恢复内存保护
DWORD _;
if (VirtualProtect(mbi.BaseAddress, mbi.RegionSize, mbi.Protect, &_))
return true;
}
}

return false;
}

现在我们就可以在主函数中调用它了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 更新钩子函数
void WINAPI my_sleep(DWORD ms) {
std::cout << "[?] Hooked Sleep Function Called!" << std::endl;
std::cout << "Sleeping for: 0x" << ms << std::endl;
// 调用原函数
original_sleep(ms);
}

int main() {
void* original_func;

std::cout << "IAT Hook Example by AmazingPP\n" << std::endl;

if (!hook_iat("kernel32.dll", "Sleep", my_sleep, &original_func)) {
std::cout << "[-] Hooking failed! error: " << GetLastError() << std::endl;
}
else {
std::cout << std::hex <<
"[?] Old Address: 0x" << original_func << std::endl <<
"[+] New Address: 0x" << my_sleep << std::endl;
original_sleep = static_cast<sleep_t>(original_func);
Sleep(0x10000);
}

return 0;
}

输出:

上面的例子是简单的面向过程封装。使用上述代码,你可以Hook任何Win32 API。
你也可以把代码构建成DLL,任何将其注入到需要攻击的进程中。

总结

  • 执行速度:10
  • 编写难度:3
  • 检测率:5

IAT Hook基于PE文件在Windows上的工作方式。
基本思想依旧是函数指针的替换,将导入函数重定向。
对于简单的API挂钩很方便,而且没有性能损失
且可运用于DirectX挂钩,实现内部绘制层,进而实现透视等功能