API钩取--DLL注入实现IAT钩取

IAT 钩取工作原理

​ IAT钩取是通过修改IAT中保存的API地址来钩取某个API, 即需要将要钩取的API在用户注入的DLL中重定义, 然后再注入目标进程. 这种方法的缺点是, 如果想要钩取的API不在目标进程的IAT中, 那么就无法使用该技术进行钩取操作. 换言之, 如果要钩取的API是由程序代码动态加载DLL文件而得以使用的, 那么将无法使用这项技术钩取它.

​ 例如, 先向目标进程 (calc.exe) 注入用户DLL (hookiat.dll), 然后在 calc.exe进程的IAT区域SetWindowTextW对应的地址更改为hookiat.dll中用户自定义的Hook函数地址. 这样当 calc.exe调用SetWindowTextW时, 就会跳转至hookiat.dll中的Hook函数, Hook函数执行到最后时调用SetWindowTextWAPI, 即可完成在该API功能正常的情况下监控API的参数和返回结果.

修改 IAT 实现计算器 SetWindowsTextW() API 钩取

实验环境为 Windows 7 (32位) 系统环境.

源代码分析

​ 这个实验的目标是实现在计算器的显示框中用中文数字替代原来的阿拉伯数字显示.

​ 实现这个目标的主要程序是 hookiat.dll, 其各部分源代码如下:

​ 1. DLLMain():

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
	switch( fdwReason )
	{
		case DLL_PROCESS_ATTACH : 
            // 保存原始API地址
           	g_pOrgFunc = GetProcAddress(GetModuleHandle(L"user32.dll"), 
                                        "SetWindowTextW");
            // # hook
            //   用 hookiat!MySetWindowText() 钩取 user32!SetWindowTextW() 
			hook_iat("user32.dll", g_pOrgFunc, (PROC)MySetWindowTextW);
			break;

		case DLL_PROCESS_DETACH :
            // # unhook
            //   将calc.exe的 IAT 恢复原值
            hook_iat("user32.dll", (PROC)MySetWindowTextW, g_pOrgFunc);
			break;
	}
	return TRUE;
}

​ 2. MySetWindowsTextW():

BOOL WINAPI MySetWindowTextW(HWND hWnd, LPWSTR lpString)
{
    wchar_t* pNum = L"零一二三四五六七八九";
    wchar_t temp[2] = {0,};
    int i = 0, nLen = 0, nIndex = 0;

    nLen = wcslen(lpString);
    for(i = 0; i < nLen; i++)
    {
        // 将阿拉伯数字转换为中文数字
        if( L'0' <= lpString[i] && lpString[i] <= L'9' )
        {
            temp[0] = lpString[i];
            nIndex = _wtoi(temp);
            lpString[i] = pNum[nIndex];
        }
    }

    // 调用 user32!SetWindowTextW() API
    // (修改 lpString 缓冲区中的内容)
    return ((PFSETWINDOWTEXTW)g_pOrgFunc)(hWnd, lpString);
}

​ 3. hook_iat()

BOOL hook_iat(LPCSTR szDllName, PROC pfnOrg, PROC pfnNew)
{
	HMODULE hMod;
	LPCSTR szLibName;
	PIMAGE_IMPORT_DESCRIPTOR pImportDesc; 
	PIMAGE_THUNK_DATA pThunk;
	DWORD dwOldProtect, dwRVA;
	PBYTE pAddr;

    // hMod, pAddr = ImageBase of calc.exe
    //             = VA to MZ signature (IMAGE_DOS_HEADER)
	hMod = GetModuleHandle(NULL);
	pAddr = (PBYTE)hMod;

    // pAddr = VA to PE signature (IMAGE_NT_HEADERS)
	pAddr += *((DWORD*)&pAddr[0x3C]);

    // dwRVA = RVA to IMAGE_IMPORT_DESCRIPTOR Table
	dwRVA = *((DWORD*)&pAddr[0x80]);

    // pImportDesc = VA to IMAGE_IMPORT_DESCRIPTOR Table
	pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)hMod+dwRVA);

	for( ; pImportDesc->Name; pImportDesc++ )
	{
        // szLibName = VA to IMAGE_IMPORT_DESCRIPTOR.Name
		szLibName = (LPCSTR)((DWORD)hMod + pImportDesc->Name);
		if( !_stricmp(szLibName, szDllName) )
		{
            // pThunk = IMAGE_IMPORT_DESCRIPTOR.FirstThunk
            //        = VA to IAT(Import Address Table)
			pThunk = (PIMAGE_THUNK_DATA)((DWORD)hMod + 
                                         pImportDesc->FirstThunk);

            // pThunk->u1.Function = VA to API
			for( ; pThunk->u1.Function; pThunk++ )
			{
				if( pThunk->u1.Function == (DWORD)pfnOrg )
				{
                    // 更改内存属性为E/R/W
					VirtualProtect((LPVOID)&pThunk->u1.Function, 
                                   4, 
                                   PAGE_EXECUTE_READWRITE, 
                                   &dwOldProtect);

                    // 修改IAT值, 钩取
                    pThunk->u1.Function = (DWORD)pfnNew;
					
                    // 恢复内存属性
                    VirtualProtect((LPVOID)&pThunk->u1.Function, 
                                   4, 
                                   dwOldProtect, 
                                   &dwOldProtect);					
					return TRUE;
				}
			}
		}
	}
	return FALSE;
}

hook_iat()函数首先从ImageBase开始, 经由PE签名找到IDT. 然后通过for循环比较pImportDesc->nameszDllName("user32.dll"), 通过比较查找到user32.dllIMAGE_IMPORT_DESCRIPTOR结构体地址. pImportDesc->FirstThunk成员所指的就是IAT. 之后通过for循环比较pThunk->u1.FunctionpfnOrg, 查找得到SetWindowTextW的地址. 因为进程的IAT内存区域是只读的, 所以需要将这部分修改为 “可读写” 模式. 最后将SetWindowTextW地址修改为MySetWindowTextW()函数地址.

调试 hookiat.dll 文件

实验环境为 Windows 7 (32位) 系统环境.

​ 1. 首先, 运行计算器, 用Process Explorer查看计算器进程的PID为1604, 如图所示:

1

​ 2. 将calc.exe附加到OllyDbg, 附加成功后F9运行calc.exe进程, 然后设置OllyDbg选项中断于新模块(DLL). 如下图所示. 这样, 注入DLL文件时, 控制权就会转给调试器.

2

​ 3. 在命令行窗口中输入相应参数, 运行InjectDll.exe, 将hookiat.dll注入计算器进程, 如下图所示:

3

​ 4. 由于calc.exe进程发生DLL加载事件, Ollydbg会在可执行模块窗口的hookiat.dll处中断, 如下图所示:

4

​ 5. 取消之前复选的中断于新模块(DLL)选项, 查找DLLMain()代码. 因为DLLMain()函数中使用了SetWindowTextW字符串, 所以在Ollydbg的代码窗口中右键选择查找->所有参考文本字串, 如下图所示:

5

​ 6. 进入SetWindowTextW字符串的代码地址73AC113E处, 经过分析, 可以得知这一部分73AC1130~ 即为DLLMain()函数, 如下图所示:

6

​ 7. 调试DLLMain()函数, 运行到地址72EA1160处, 比较代码与参数输入, 可以得知这里是调用了hook_iat()函数, 如下图所示:

7

​ 8. F7进入hook_iat()函数, 运行到如下部分, 其含义是从PE文件头查找IMAGE_IMPORT_DESCRIPTION (IID Table)的过程:

8

​ 9. 继续调试, 到达如下代码部分, 其含义是在IAT中查找SetWindowTextW API的位置. 73AC10E0地址处的指令, ESI的值为user32.dll的IAT起始地址010010B4, EBP的值为SetWindowTextW的地址7562612B.这部分代码运行循环进入IAT, 查找位于01001110SetWindowsTextW的地址值7562612B:

9

​ 10. 继续调试, 73AC1117地址处的指令将MySetWindowTextW (hook函数)的地址73AC1117覆写到前面从IAT中获取的SetWindowTextW地址01001110, 即实现了IAT中SetWindowTextW API的钩取:

10

​ 11. 完成上述IAT钩取后, 在Ollydbg中按F9正常运行运行calc.exe进程. 用户在计算器界面输入数字, calc.exe会调用IAT中保存的hookiat.MySetWindowTextW()地址. MySetWindowsTextW()函数会先将阿拉伯数字转换为中文数字, 再调用user32.SetWindowTextW(), 测试结果如下:

11

​ 最终, 通过调试与测试, 使用DLL注入技术实现了对计算器IAT的修改, 进而对计算器的 SetWindowsTextW() API 进行钩取与利用.