读书笔记_windows下的混合钩子(HOOK)_part 1
在windows,所谓的混合钩子,指的是即使用用户态下的钩子,又使用内核态的钩子。优点是既能实现强大的功能,又能避免被安全软件发现。
混合HOOK使用IAT HOOK 勾住用户空间进程,下面来看IAT HOOK
每个API函数的地址都保存在IAT表中,每个CALL指令所使用的地址都是相应函数登记在IAT表中的地址。所以可以修改IAT表中的地址换成用户自己的API PROXY函数地址,这样函数的调用过程首先是调用自己的函数,然后再调用原来的过程。
API PROXY 这个过程是核心,包含两段程序,结构HOOKAPIPROXY用来保存函数原来的地址和函数名,PROXYFUN用来自己定义的一些操作:
typedef struct{
byte PushCode1; //0xff
ULONG OrgAddr;
byte PushCode2; //0xff
ULONG NameAddr;
byte JmpCode; //0xe9
ULONG JmpParam;
}*PHOOKAPIPROXY, HOOKAPIPROXY;
每个API函数的IAT项都执行了一个HOOKAPIPROXY结构,其包含了三层含义:
1. Push 原来的IAT地址
2. Push函数名称的地址
3. Jmp到同一个proxyFun()程序段中
来看ProxyFun()
_declspec(naked) void ProxyFun()
{
//局部变量的定义
DWORD ByteWrite;//写入日志文件中的字节
PCHAR pFunctionName; //取函数名称
char Str[200];//字符串
ULONG Param[PARAMNO]; //取出的参数
char StrParam[200], StrTemp[20];
ULONG nParam, i;
ULONG Result;
_asm{
push ebp
mov ebp, esp
sub esp, __LOCAL_SIZE
push esi
push edi
push ebx
//先取出函数名称地址。
mov eax,[ebp+4]
mov pFunctionName, eax
//下面拷贝参数PARAMNO * 4
mov ecx, PARAMNO
mov esi, ebp
add esi, 12 + PARAMNO * 4
//4:原函数EIP+4:原来函数地址+ 4:FUNCTIONNAME + PARAMNO参数
nextmove:
mov eax, [esi]
push eax
sub esi, 4
dec ecx
jnz nextmove
mov nParam, esp
//调用原来的函数。
call dwordptr [ebp + 8]
mov Result, eax //保存可能的返回值
mov ecx, esp
//先复原ESP
mov esp, nParam
add esp, PARAMNO * 4
//判断到底有几个参数。
sub ecx,nParam
shr ecx, 2
mov nParam, ecx
jcxz noparam //若没有参数
//下面拷贝参数
lea esi,Param
mov edi, ebp
add edi, 16
nextparam:
mov eax, [edi]
mov [esi], eax
add esi, 4
add edi, 4
loop nextparam
noparam: //若没有参数,便直接出来。
}
//往文件中写数据。
//(此处略,下面有详细说明)....
//准备返回
_asm{
mov eax, Result
mov ecx, nParam
shl ecx, 2
pop ebx
pop edi
pop esi
mov esp, ebp
pop ebp
add esp, 8 //4:原来函数地址+ 4:FUNCTIONNAME PARAMNO参数
pop edx
add esp, ecx
push edx
ret
}
}
1. 函数名称
在[EBP+4]的地址中保存着函数的地址保存着函数的名称的地址
2. 函数参数
Win API函数调用方式(stdcall)的一种技巧。为了计算参数个数,我们先将PARAMNO个DWORD堆栈值保存在ESP中。然后调用原来的函数(call[ebp+8]), 由于WINAPI调用方式的函数在返回前释放堆栈中的参数,所以在函数返回后ESP值与原来的不同,前提是每个参数都是一样的,它们之间的差值就是参数的个数。
3. 函数的返回值
调用完参数后,函数的返回值放在EAX中。即使VOID也会记录EAX的值
4. 返回至函数调用点
函数的调用点EIP的值放在堆栈的[EBP + 12]中
上面的代码中,我们没有涉及到代码的保存部分。这是个独立的部分,我们将信息保存在LOG文件中。
//打开文件
hLog = CreateFile("c://ApiHook.txt", GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
hEvent = CreateEvent(NULL, FALSE, TRUE, "BitBltEvent");
....
//记录信息
StrParam[0] = '/0';
for(i = 0; i < nParam; i ++)
{
sprintf(StrTemp, " 0x%08x ", Param[i]);
strcat(StrParam, StrTemp);
}
WaitForSingleObject(hEvent, INFINITE);
SetFilePointer(hLog, 0, NULL, FILE_END);
if ((ULONG)pFunctionName >= 0x80000000)
sprintf(Str, "[Ord: 0x%x]参数个数: %d [%s]返回值: 0x%x /x0d/x0a", (ULONG)pFunctionName - 0x80000000, nParam,StrParam, Result);
else
sprintf(Str, "[%s]参数个数: %d [%s]返回值: 0x%x /x0d/x0a", pFunctionName, nParam, StrParam, Result);
WriteFile(hLog,Str, strlen(Str), &ByteWrite, NULL);
SetEvent(hEvent);
...
//关闭文件
CloseHandle(hEvent);
CloseHandle(hLog);
|
因为可能有多个线性调用API函数并被截获,所有需要用一个事件内核对象(hEvent)来同步对文件的写操作。
需要注意上面的记录信息这段代码是不能放在proxyfun中的。因为你可能已经截获了WaitForSingleObject、SetFilePointer、WriteFile或者SetEvent,这样会引起递归调用而引起死锁和栈溢出。为了解决这个问题,我们在HOOK API前先将这些函数的地址保存在全局变量中。
WriteFileAddr = (ULONG)WriteFile;
SetFilePointerAddr = (ULONG)SetFilePointer;
SetEventAddr = (ULONG)SetEvent;
WaitForSingleObjectAddr = (ULONG)WaitForSingleObject;
|
于是proxyfun的信息记录过程将变成如下:
#define PSETFILEPOINTER DWORD (WINAPI *)(HANDLE, LONG, PLONG, DWORD)
#define PWRITEFILE BOOL (WINAPI *)(HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED)
#define PWAITFORSINGLEOBJECT DWORD (WINAPI *)(HANDLE, DWORD)
#define PSETEVENT BOOL (WINAPI *)(HANDLE)
...
//往文件中写数据。
StrParam[0] = '/0';
for(i = 0; i < nParam; i ++)
{
sprintf(StrTemp, " 0x%08x ", Param[i]);
strcat(StrParam, StrTemp);
}
((PWAITFORSINGLEOBJECT)WaitForSingleObjectAddr)(hEvent, INFINITE);
((PSETFILEPOINTER)SetFilePointerAddr)(hLog, 0, NULL, FILE_END);
if ((ULONG)pFunctionName >= 0x80000000)
sprintf(Str, "[Ord: 0x%x]参数个数: %d [%s]返回值: 0x%x /x0d/x0a", (ULONG)pFunctionName - 0x80000000, nParam, StrParam, Result);
else
sprintf(Str, "[%s]参数个数: %d [%s]返回值: 0x%x /x0d/x0a", pFunctionName, nParam, StrParam, Result);
((PWRITEFILE)WriteFileAddr)(hLog,Str,strlen(Str),&ByteWrite,NULL);
((PSETEVENT)SetEventAddr)(hEvent);
|
这样便完成了信息的记录工作了。
使用此方法进行API截获时,你会发现有一些API函数总是截获不了,比如消息循环中的GetMessage等消息,这是因为编译器做了大量的优化工作,它可能早将IAT中函数地址保存在某个寄存器或者变量(就像上文介绍的信息记录的方法)了,所以你即使改变了IAT表中的地址值,也不会截获到这些API调用。这便是IAT法的最大的缺点——不能完全截获。
这个方法的优点是实现简单,兼容性好。
接下来看混合钩子的例子:
Windows提供了一个函数PsSetImageLoadNotifyRoutine,该函数可以在加载目标进程或DLL时获得通知,该函数注册了一个每次将映像加载到内存中都要调用的驱动程序回调例程。PsSetImageLoadNotifyRoutine是在内核中使用的例程,它只有一个参数,即回调例程的地址,这个回调例程的声明如下:
Void MyImageLoadNotify ( IN PUNICODE_STRING, IN HANDLE, INPIMAGE_INFO);
UNICODE_STRING 包含了由内核加载的模块名;HANDLE参数是模块加载到的目标进程的PID,钩子需要放到该PID的内存上下文中。IMAGE_INFO结构包含了钩子需要的有用的信息,例如加载到内存中映像的基地址。IMAGE_INFO结构定义如下:
Typedef struct _IMAGE_INFO {
Union {
ULONG Properties;
Struct {
ULONG ImageAddressingMode : 8;
ULONG SystemModeImage : 1;
ULONG ImageMappedToAllPids : 1;
ULONG Reserved : 22;
};
};
PVOID ImageBase;
ULONG ImageSelector;
ULONG ImageSize;
ULONG ImageSectionNumber;
}IMAGE_INFO, *PIMAGE_INFO;
在回调函数中,首先要判断它是否为要勾住其IAT的模块,如果不知道进程中哪些模块导入了一个要过滤的特定函数,则可以勾住指向勾住函数的所有IAT。一下示例调用HookImportsOfImage来分析模块并找到其IAT项,从而勾住所有的模块。可以通过process ID或image name来过滤自己需要的IAT,如果不知道就需要勾住所有的IAT
VOID MyImageLoadNotify ( IN PUNICODE_STRING FullImageName, IN HANDLEProcessID, IN PIMAGE_INFO ImageInfo)
{
UNICODE_STRINGu_targetDLL;
DbgPrint(“Image name:%ws\n”, FullImageName->Buffer);
RtlInitUnicodeString(&u_targetDLL, L”\\WINDOWS\\system32\\kernel32.dll”);
If (RtlCompareUnicodeString ( FullImageName, &u_targetDLL, TRUE) == 0)
{
HookImportsOfImage (ImageInfo->ImageBase, PorcessId);
}
}
HookImportsOfImage也是自己定义的函数,用来内存中的PE文件。Windows中大多数的二进制文件都采用了可移植执行文件(Portable Executable,PE)的格式。PE中包含的大多数项都具有相对虚拟地址(Relative Virtual Address, RVA),它们是实际数据相对于PE的内存加载位置的偏移量。钩子在这里的需要检查每个PE导入的DLL。下面介绍分析PE的过程:
介绍一下PE文件,PE文件的两个关键是内存映射文件机制和相对虚拟地址。
首先看内存映射机制。磁盘上的可执行文件和当它被windows调入内存后事非常像的,可以把磁盘上的文件映射到内存中去,而PE的文件的作用就像一个多个格子的整理箱一样,当需要映射时,就会分配一个这样的PE结构,然后把需要载入的模块(DLL,EXE等)按每个段(好像整理箱中的格子)直接放到内存空间中,这就是所谓的内存映射文件机制。
再看相对虚拟地址(RVA)。RVA是文件映射到内存的偏移。例如,载入器把一个文件映射到虚拟地址0x10000开始的内存块,如果一个映像中的实际的表的首地址是0x10464, 那么它RVA就是0x464。如下的公式所示:
(虚拟地址0x10464) — ( 基地址0x10000) = RVA0x00464
把RVA转化成一个有用的指针,只需把RVA值加载到模块的基地址上即可。基地址是内存映射EXE和DLL文件的首址。
分享到:
相关推荐
PB串口通讯数据监控,使用钩子HOOK API
windows hookHOOK API是指截获特定进程或系统对某个API函数的调用,使得API的执行流程...Windows下的应用程序都建立在API函数至上,所以截获API是一项相当有用的技术,它使得用户有机会干预其它应用程序的程序流程。
VC++ 2008编译的钩子源代码,编译通过,没错误
一个网友写的非常清晰明白的鼠标钩子看星星的程序
API Hook基本原理和实现,钩子的特性
WINCE下的键盘钩子程序,让你轻松捕获键盘消息
VB内嵌汇编实现单个类文件实现子类化钩子.
通过例子介绍了Windows系统服务调用的基本知识及Hook SSDT的方法
hook function for windows 7
键盘钩子 hook 键盘钩子 hook 键盘钩子 hook 键盘钩子 hook 键盘钩子 hook 键盘钩子 hook 键盘钩子 hook 键盘钩子 hook
这是一个记录键盘输入的钩子,可是监听键盘输入,特定路径文本输出,并且还是实现任务栏托盘显示。
windows CE的键盘钩子,已经过测试,sdk下测试。
delphi开发的钩子测试程序,源代码中包含了各种系统钩子的使用方法。
线程钩子的事例代码。适合初学者。大家自己看吧。
Driver_hook Driver_hook Driver_hook Driver_hook
钩子函数运用,结合java语言获取键盘、鼠标响应监控信息。
一个简单的鼠标钩子程序 实现适时获取当前鼠标所在窗口的标题,并将其显示在一个EDITBOX中
枚举全局钩子_如何枚举系统中所有的钩子hook
codeproject上的API HOOK相关文档整理稿
对Windows API函数进行HOOK测试源码