Windows下的钩子
2011-9-9
API钩子应用程序将kernel32.dll加载到自己的私有空间0x0001000~0x7FFE0000之间,所以本地进程只要能访问目标进程的地址空间,就可以直接重写kernel32.dll中或应用程序导入表中的任何函数。通过利用机器码重写DLL文件中特定的文件或重写目标应用程序中的导入表,使其指向自己想要的函数。利用这种方式,可以完成隐藏进程,隐藏网络端口,将文件操作重定向到其他文件、防止应用程序打开特定进程的句柄功能。下面简单介绍几种钩子:
导入地址表钩子(import Address table hooking)。
当应用程序使用另一个库中的函数时,必须导入该函数的地址。都是通过IAT来实现的。
应用程序文件系统映像的IMAGE_IMPORT_DESCRIPTOR结构,它包含导入函数的DLL名称,和两个IMAGE_IMPROT_BY_NAME数组指针,它包含了导入函数的名称。这种方式对于显示调用DLL无效。
内联函数钩子
内联函数钩子远比IAT钩子强大,在实现内联函数钩子时,实际上是重写目标函数的代码字节, 所以无论目标进程如何或何时解析函数地址,都能够勾住函数。
内联函数钩子要重写目标函数的多个起始字节。保存原始字节后,在目标函数前5个字节中放置一个立即跳转指令。内联函数补丁的里程碑论文
Detours: Binary Interccption of Win32 function, G. Hunt and D. Brubacker
使用注册表注入DLL
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\Current Verion\Windows\AppInit_Dlls
将该键值设为修改了目标进程的IAT或直接修改了kernel32.Dll和ntdll.dll的DLL。
使用windows钩子注入DLL
定义了一个能够钩住另个进程中的窗口消息的函数,使用SetWindowsHookEx()。
鼠标hook的一个示例:
LRESULT WINAPI MouseProc(int nCode,WPARAM wparam,LPARAM lparam)
{
LPMOUSEHOOKSTRUCT pMouseHook=(MOUSEHOOKSTRUCT FAR *)lparam;
if (nCode>=0)
{
/*============================================================================
如下代码是得到父窗口标题文本的做法
HWND glhTargetWnd=pMouseHook->hwnd; //取目标窗口句柄
HWND ParentWnd=glhTargetWnd;
while (ParentWnd !=NULL)
{
glhTargetWnd=ParentWnd;
ParentWnd=GetParent(glhTargetWnd); //取应用程序主窗口句柄
}
===========================================================================*/
// HWND glhTargetWnd=XYZWindowFromPoint(NULL,pMouseHook->pt); //用上面的一段注释掉的代码替换此行是得到父窗口标题文本的做法
// if(glhTargetWnd!=glhPrevTarWnd)
// {
// char szCaption[256];
// GetWindowText(glhTargetWnd,szCaption,100);
//取目标窗口标题
// if(IsWindow(glhDisplayWnd))
glhDisplayWnd=FindWindow(NULL,"DEMO");
SendMessage(glhDisplayWnd,WM_SETTEXT,0,0);
// glhPrevTarWnd=glhTargetWnd;
//保存目标窗口
// }
}
return CallNextHookEx(glhHook,nCode,wparam,lparam);
//继续传递消息
}
1. 使用远程线程注入DLL
Remote thread
HANDLE WINAPI CreateRemoteThread(
__in HANDLE hProcess,
__in LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in SIZE_T dwStackSize,
__in LPTHREAD_START_ROUTINE lpStartAddress,
__in LPVOID lpParameter,
__in DWORD dwCreationFlags,
__out LPDWORD lpThreadId
);
其中hProcess为远程进程的句柄,lpThreadAttributes为线程安全描述,指向SECURITY_ATTRIBUTES结构指针。
dwStackSize 表示线程栈的大小,并以字节为单位
lpStartAddress 表示一个LPTHREAD_START_ROUTINE类型的指针,指向在远程进程中执行的函数地址。
lpParameter表示传入的参数。
DwCreationFlags 创建线程的标志
LpThreadID 输出线程的ID,
返回值为新线程的句柄,失败的话就返回NULL。
下面使用两种方式使用CreateRemoteThread。
首先创建一个函数FuncA,我们的目标是将它放入计算器中运行。
Static DWORD WINAPI FuncA(LPVOID pData)
{
//
Return *(DWORD*)pData;
}
这里定义了另外一个函数FuncB,用于确定我们代码的大小
下面来定位目标进程,这里是一个计算器
HWND hStart = ::FindWindow(TEXT(“Calculator”), NULL);
通过下面代码来得到线程的句柄
DWORD PID, TID;
TID = ::GetWindowThreadProcessID( hStart, &PID);
HANDLE hProcess;
hProcess = OpenProcess(PROCESS_ALL_ACCESSS, false, PID);
目标进程中的内存的属性一般只是只读的,所以通过修改目标进程的属性为PAGE_READWRITE才可以对目标进程进行写操作。
代码实现如下:
char szBuffer[10];
cbParamSize = 10;
*(DWORD*) szBuffer = 1000;
Void *pDataRemote = (char*) VirtualAllocEx ( hProcess, 0, sizeof(szBuffer), MEM_COMMIT, PAGE_READWRITE);
通过WriteProcessMemory把需要的内容分配到目标进程中分配的变量
::WriteProcessMemory ( hProcess, pDataRemote, szBuffer, sizeof(szBuffer), NULL);
在目标进程中分配代码地址空间,
首先计算代码的大小,以字节为单位
DWORD cbCodeSize=((LPBYTE) FuncB – (LPBYTE)FuncA);
分配代码地址空间
PDWORD pCodeRemote = (PDWROD) VirtualAllocEx( hProcess, 0, cbCodeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
写内容到目标进程中分配的代码地址空间
WriteProcessMemory(hProcess, pCodeRemote, &FuncA, cbCodeSize, NULL)
VirtualAllocEx() 是用来在进程的虚拟地址空间内分配内存用的,如果第二个参数如果设为0时,那么所分配内存的位置就由VirtualAllocEx()函数自己决定。
最后调用CreateRemoteThread()来在目标进程中执行代码
HANDLE hThread = CreateRemoteThread(hProcess,
NULL,
0,
(LPTHREAD_STRAT_ROUTINE)pCodeRemote,// 线程函数
pDataRemote,// 传递的参数
0,
NULL);
DWORD code;
If (hThread)
{
::WaitForSingleObject(hThread, INFINITE);
::GetExitCodeThread(hThread, &code);
TRACE(“run and return %d\n”, code);
::CloseHandle(hThread);
}
这里需要注意:需要使用WaitForSingleObject等待线程结束;
可以使用GetExitCodeThread获得返回值
对于句柄,需要使用CloseHandle关闭。
最后需要对现场进行清理:
首先释放空间:
::VirtualFreeEx(hProcess,pCodeRemote,cbCodeSize,MEM_RELEASE);
::VirtualFreeEx(hProcess,pDataRemote,cbParamSize,MEM_RELEASE);
::CloseHandle ( hProcess);
下面我们来怎样将一个动态库植入到远程进程中,大致的步骤和以上相似,两个关键是需要将动态库的路径作为变量传入变量空间。另外一个是在使用函数CreateRemoteThread()时,需要将GetProcAddress作为目标执行函数。
第一参数是要注入线程的进程的句柄,可以通过进程标识符(PID),调用OpenProcess,
第4个参数为目标进程中的LoadLibrary()地址。可以使用目标进程中的LoadLibrary()函数。由于该地址必须存在于目标进程中,因此必须将Kernel32.dll加载到目标进程中,这种方法才有效,原因是LoadLibrary是由Kernel32.dll导出的,获得LoadLibrary的地址,可以使用
GetProcAddress(GetModuleHandle(TEXT(“Kernel32”)), “LoadLibraryA”);
通过LoadLibrary得到目标进程要注入DLL的地址,是在Kernel32.dll位于目标进程中同一个基地址处。(这种情况是经常出现的,因为DLL加载到内存中时,重新确定DLL的基地址会很耗时,微软也希望避免避免由于重新确定DLL基址所浪费的时间。LoadLibrary的格式与返回类型与结构体THREAD_START_ROUTINE函数相同,可以用作第四个参数。
第5个参数lpParameter是LoadLibrary函数参数的内存地址。本地进程不能只传递一个字符串,因为这只能指向本地进程的一个地址,与目标进程没有关系。通过VirtualAllocEx函数可以分配目标进程中的内存。然后调用WriteProcesMemory函数来把我们所需的dll的路径与名称传递到目标进程。
简单过程如下:
hThread = ::GreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) :: GetProcAddress(GetProcAddress(GetModuleHandle(TEXT(“Kernel32”)), “LoadLibraryA”);
, “LoadLibraryA”), pDataRemote, 0, NULL);
然后可以调用我们的函数进行一些操作
在执行之后需要释放掉这个动态库,类似的
hThread = ::GreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE) :: GetProcAddress( hModule, “FreeLibrary”), 参数, 0, NULL);