《Windows核心编程》之“Windows挂钩”(二)

    本文接上篇,继续探讨“Windows挂钩”,包括:跨进程的“窗口子类化”,使用Windows Hook注入DLL,跨进程窗口通讯,进程间共享内存和示例程序调试总结。

一、跨进程的“窗口子类化”     subclass a window created by another process

    上一篇最后,我们讲到通过“窗口子类化”来扩展窗口的行为。其关键是将自定义的窗口处理函数(NewWndProc)通过调用user32.dll中的SetWindowLongPtr函数与指定窗口关联起来。当线程调用DispatchMessage时,将取得的是NewWndProc函数的地址。原本发往OldWndProc函数的消息,会转发给NewWndProc。

LONG DispatchMessage(CONST MSG * msg)
{
    LONG lResult;
	WNDPROC lpfnWndProc = (WNDPROC)GetWindowLongPtr(msg.hwnd, msg.message);
	lResult = lpfnWndProc(msg.hwnd, msg.message, msg.wParam, msg.lParam);
	return(lResult);
}

    当然,这一切的前提是,NewWndProc函数也位于被子类化的窗口所在的进程空间。因为,DispatchMessage同大多数Windows APIs一样,默认只操作调用它的线程所属的进程空间。

    如果我们需要子类化其他进程的窗口,即NewWndProc函数位于我们的进程空间,而不是位于被子类化的窗口所在的进程空间,那么DispatchMessage在引用我们传入的函数地址值时,实际访问的是目标进程的某个地址,根本就不可能调到NewWndProc函数。

    因此,我们要想子类化其他进程的窗口的窗口,第一步就是要把NewWndProc函数注入到目标进程的地址空间。《Windows核心编程》一书中介绍了多种方法来讲NewWndProc函数注入到目标进程地址空间。


二、Windows Hook

1,Windows Hook

    Windows系统提供了一种从进程A将一个函数注入到进程B并截取进程B某个窗口的消息的方法——Windows Hook。

    先来看MSDN怎么说:点击打开链接

A hook is a point in the system message-handling mechanism where an application can install a subroutine to monitor the message traffic in the system and process certain types of messages before they reach the target window procedure.

hook is a mechanism by which an application can intercept events, such as messages, mouse actions, and keystrokes. A function that intercepts a particular type of event is known as a hook procedure. A hook procedure can act on each event it receives, and then modify or discard the event.

The following some example uses for hooks:

  • Monitor messages for debugging purposes
  • Provide support for recording and playback of macros
  • Provide support for a help key (F1)
  • Simulate mouse and keyboard input
  • Implement a computer-based training (CBT) application
Note  Hooks tend to slow down the system because they increase the amount of processing the system must perform for each message. You should install a hook only when necessary, and remove it as soon as possible.
    Windows Hook主要包括:Hook Chains、Hook Procedures和Hook types这么几个概念,在此我就不展开说了,有兴趣的直接看MSDN文档。
    MSDN也给出了两个使用Hook的示例:Installing and Releasing Hook Procedures和Monitoring System Events,点击打开链接。 在此,简单描述一下前一个示例。
    
    
2,Installing and Releasing Hook Procedures

1)SetWindowsHookEx

Installs an application-defined hook procedure into a hook chain. You would install a hook procedure to monitor the system for certain types of events. These events are associated either with a specific thread or with all threads in the same desktop as the calling thread.

Syntax

C++
HHOOK WINAPI SetWindowsHookEx(
  _In_ int       idHook,
  _In_ HOOKPROC  lpfn,
  _In_ HINSTANCE hMod,
  _In_ DWORD     dwThreadId
);


    要从我们的进程(进程A)向目标进程(进程B)的某个窗口安装挂钩,就需要在A进程中调用SetWindowsHookEx函数。例如:

Hook hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hInstDll, 0);

    第1个参数WH_MESSGAE表示要安装的挂钩的类型。第2个参数GetMsgProc是一个函数的地址(位于进程A的地址空间),在目标窗口即将处理一条消息的时候,系统应该调用这个函数。第3个参数hInstDll表示一个DLL,这个DLL中包含了GetMsgProc函数。在Windows中,hInstDll的值是进程地址空间中DLL被映射到的虚拟内存地址。最后一个参数0表示要给哪个线程安装挂钩。一个线程可能会调用SetWindowsHookEx并传入系统中另一个线程的线程标识符(一般是线程ID)。通过给这个参数传0,我们告诉系统要给系统中的所有GUI线程安装挂钩。

    从SetWindowsHookEx的原型,我们可以推测:

1)Hook Procedure函数既要载入进程A的地址空间,又要载入进程B的地址空间,且进程A要知道该函数在进程B中的虚拟内存地址。第2个参数表示的就是Hook Procedure函数。

2)Hook Procedure函数要放在一个独立的DLL中。一个函数要同时载入两个进程空间,那么最好的设计是将该函数放到一个DLL中,作为一个导出函数,让进程A和进程B都载入该DLL。第3个参数表示的就是这个DLL。

3)进程A需要自己去加载InstDll,进程B不需要。SetWindowsHookEx会将InstDll加载到指定GUI线程所在进程(进程B)的地址空间。严格来说,SetWindowsHookEx只是注册,真正的加载过程如下:


1)进程B中的一个线程准备向一个窗口派送一条消息(dispatch message)。

2)系统检查该线程是否已经安装了WH_GETMESSAGE挂钩。

3)系统检查GetMsgProc所在的DLL是否已经被映射到进程B的地址空间中。

4)如果DLL尚未被映射,那么系统会强制将该DLL映射到进程B,并将进程B中该DLL的锁计数器(lock count)递增。

5)由于DLL的hInstDll是在进程B中映射的,因此系统会对它进行检查,看它与该DLL在进程A中的位置是否相同。

    如果hInstDLL在进程A和进程B的位置相同,那么GetMsgProc函数在这两个进程空间的虚拟内存地址也就会相同。这种情况,系统就可以直接在进程B中调用GetMsgProc。

    如果hInstDLL不同,系统会通过以下公式换算,来获取GetMsgProc在进程B中的地址

GetMsgProc B = hInstDll B + (GetMsgProc A - hInstDll A)

6)系统在进程B中调用GetMsgProc函数,返回后,系统会递减该DLL在进程B中的锁计数器。


Hook Procedure函数如下:

LRESULT CALLBACK HookProc(
  int nCode, 
  WPARAM wParam, 
  LPARAM lParam
)
{
   // process event
   ...

   return CallNextHookEx(NULL, nCode, wParam, lParam);
}

3,Monitoring System Events

    SetWindowsHookEx函数的第一个参数表示Hook的类型,它对应一种系统事件(event)。Monitoring System Events示例展示了对它支持的7种系统事件的监视和处理。可以查看MSDN文档,了解这7种事件的具体含义。

   《Windows核心编程》中的DIPS示例展示的就是其中的“WM_GETMESSAGE”事件的处理。MSDN说:Installs a hook procedure that monitors messages posted to a message queue.它监视所有发送给被监视线程的消息。


    另一个值得注意的是“WM_KEYBOARD”事件。它配合GetKeyNameText函数,可以监视用户敲击的键盘按键。

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    ......
	case WM_KEYDOWN:
		//MessageBox(NULL, TEXT("key down"), NULL, MB_OK);
		WCHAR keyname[128];
		MessageBox(NULL, keyname, NULL, MB_OK);
		break;	
    ......
    case WM_DESTROY:
        PostQuitMessage(0); 
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}


4,Subclass a window created by another process

    注意,SetWindowsHookEx会将整个DLL映射到目标进程,而仅仅是Hook Procedure函数。这就意味着,该DLL内的所有函数都存在进程B中,可以为进程B调用。

    回到之前“跨进程窗口子类化”的话题,我们可以先给创建窗口(目标窗口)的线程设置一个WH_GETMESSAGE挂钩,然后当GetMsgProc被调用的时候,我们可以在它内部调用SetWindowLongPtr来派送子类窗口。当然,子类窗口的过程函数必须和GetMsgProc函数在同一个DLL中。



、API Hook的原理 这里的API既包括传统的Win32 APIs,也包括任何Module输出的函数调用。熟悉PE文件格 式的朋友都知道,PE文件将对外部Module输出函数的调用信息保存在输入表中,即.idata段。 下面首先介绍本段的结构。 输入表首先以一个IMAGE_IMPORT_DESCRIPTOR(简称IID)数组开始。每个被PE文件隐式链接 进来的DLL都有一个IID.在这个数组中的最后一个单元是NULL,可以由此计算出该数组的项数。 例如,某个PE文件从两个DLL中引入函数,就存在两个IID结构来描述这些DLL文件,并在两个 IID结构的最后由一个内容全为0的IID结构作为结束。几个结构定义如下: IMAGE_IMPORT_DESCRIPTOR struct union{ DWORD Characteristics; ;00h DWORD OriginalFirstThunk; }; TimeDateStamp DWORD ;04h ForwarderChain DWORD ;08h Name DWORD ;0Ch FirstThunk DWORD ;10h IMAGE_IMPROT_DESCRIPTOR ends typedef struct _IMAGE_THUNK_DATA{ union{ PBYTE ForwarderString; PDWORD Functions; DWORD Ordinal; PIMAGE_IMPORT_BY_NAME AddressOfData; }u1; } IMAGE_IMPORT_BY_NAME结构保存一个输入函数的相关信息: IMAGE_IMPORT_BY_NAME struct Hint WORD ? ;本函数在其所驻留DLL的输出表中的序号 Name BYTE ? ;输入函数的函数名,以NULL结尾的ASCII字符串 IMAGE_IMPORT_BY_NAME ends OriginalFirstThunk(Characteristics):这是一个IMAGE_THUNK_DATA数组的RVA(相对于PE文件 起始处)。其中每个指针都指向IMAGE_IMPORT_BY_NAME结构。 TimeDateStamp:一个32位的时间标志,可以忽略。 ForwarderChain:正向链接索引,一般为0。当程序引用一个DLL中的API,而这个API又引用别的 DLL的API时使用。 NameLL名字的指针。是个以00结尾的ASCII字符的RVA地址,如"KERNEL32.DLL"。 FirstThunk:通常也是一个IMAGE_THUNK_DATA数组的RVA。如果不是一个指针,它就是该功能在 DLL中的序号。 OriginalFirstThunk与FirstThunk指向两个本质相同的数组IMAGE_THUNK_DATA,但名称不同, 分别是输入名称表(Import Name Table,INT)和输入地址表(Import Address Table,IAT)。 IMAGE_THUNK_DATA结构是个双字,在不同时刻有不同的含义,当双字最高位为1时,表示函数以 序号输入,低位就是函数序号。当双字最高位为0时,表示函数以字符串类型的函数名 方式输入,这时它是指向IMAGE_IMPORT_BY_NAME结构的RVA。 三个结构关系如下图: IMAGE_IMPORT_DESCRIPTOR INT IMAGE_IMPORT_BY_NAME IAT -------------------- /-->---------------- ---------- ---------------- |01| 函数1 ||02| 函数2 || n| ... |"USER32.dll" | |--------------------| | | FirstThunk |---------------------------------------------------------------/ -------------------- 在PE文件中对DLL输出函数的调用,主要以这种形式出现: call dword ptr[xxxxxxxx] 或 jmp [xxxxxxxx] 其中地址xxxxxxxx就是IAT中一个IMAGE_THUNK_DATA结构的地址,[xxxxxxxx]取值为IMAGE_THUNK_DATA 的值,即IMAGE_IMPORT_BY_NAME的地址。在操作系统加载PE文件的过程中,通过IID中的Name加载相应 的DLL,然后根据INT或IAT所指向的IMAGE_IMPORT_BY_NAME中的输入函数信息,在DLL中确定函数地址, 然后将函数地址写到IAT中,此时IAT将不再指向IMAGE_IMPORT_BY_NAME数组。这样[xxxxxxxx]取到的 就是真正的API地址。 从以上分析可以看出,要拦截API的调用,可以通过改写IAT来实现,将自己函数的地址写到IAT中, 达到拦截目的。 另外一种方法的原理更简单,也更直接。我们不是要拦截吗,先在内存中定位要拦截的API的地址, 然后改写代码的前几个字节为 jmp xxxxxxxx,其中xxxxxxxx为我们的API的地址。这样对欲拦截API的 调用实际上就跳转到了咱们的API调用去了,完成了拦截。不拦截时,再改写回来就是了。 这都是自己从网上辛辛苦苦找来的,真的很好啊
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值