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

    本文主要结合之前两篇介绍的“Windows挂钩”知识,讲解《Windows核心编程》一书中的“DIPS"示例程序和我的调试经验。


一、程序功能介绍

1,功能需求

    DIPS.exe应用程序使用了Windows Hook来将一个DLL注入到Explorer.exe的地址空间中,并在内部给桌面的ListView控件发送LVM_GETITEM和LVM_GETITEMPOSITON消息,获取桌面的ListView的子项及其位置坐标,然后将这些信息写入系统注册表,待需要的时候重新取回。

    之所以要“注入DLL”,是因为LVM_GETITEM消息要求我们在它的LPARAM参数中传入一个LV_ITEM数据结构。由于这个内存地址只对发送消息的进程又意义,因此接收消息的进程是无法使用它的。唯有将这个地址注入到接收消息的进程,才能发送成功。

2,名词解释

1)DIPS —— Desktop Item Position Saver (桌面图标位置保存工具)

2)LVM —— ListView Manage

3)Explorer.exe,就是我们俗称的“资源管理器”,Windows操作系统中,通过图形界面看到的所有东西,比如:桌面、C盘、D盘、文件夹等都是在这个“资源管理器”的投影。我们鼠标双击打开桌面上的应用程序,也是由Explorer.exe调用CreateProcess来实现。

    此外,Explorer.exe这个名字是在“任务管理器”中可以看到的,事实上,它也是有窗口的,只不过它的窗口时隐藏的。我们可以通过VS自带的“spy++”工具来查看它的窗口。打开“spy++”,搜索(alt+F3)类“Progman”,注意,它区分大小写。



    看上图左边,红圈中,“00010132”是它的窗口句柄,“Program Manager”是窗口的标题,“Progman”是窗口的类名。Progman窗口只有为一个自窗口“SHELLDLL_DefView”,而它也自由一个孙窗口“SysListView32”即桌面ListView。所以,DIPS中,通过如下方式来获取桌面窗口句柄:

   HWND hWndLV = GetFirstChild(GetFirstChild(
      FindWindow(TEXT("ProgMan"), NULL)));

    上图右边是该窗口的右键属性,可以比较这个进程ID和 Explorer.exe在“任务管理器”中的PID,它们的值是相等的(需要进行进制换算)。

3,注册表项

    通过“regedit"命令(register edit)可以打开系统注册表。DIPS需要添加一个注册表项”HKEY_CURRENT_USER\Software\Wintellect\Desktop Item Position Saver”。我们在调试的时候,可以先查看对应位置是否有这个项。


二、代码调试

    Jeffrey Richter设计的这个DIPS程序展示了很多项比较高级的编程技巧,下面我将我的体会一一列出。

1,DIPS工程对DIPSLib工程的引用

    安装Windows Hook,需要在实施进程和目标进程都加载Hook Procedure所在的DLL。但是,我找遍DIPS工程(实施进程),从代码到工程文件,都没找到它是怎么加载DIPSLib.dll文件的。一般来说,一个工程加载DLL文件,有以下几种方式:

1)在代码中显示调用LoadLibrary函数进行加载。

2)在代码中使用“#pragma comment(lib,"***")”伪指令,隐式加载某个DLL。

3)在“VS工程属性 -> 链接器 -> 附加依赖项”中填入待加载的DLL的对应lib文件名。kernel32.dll、user32.dll等系统DLL就是这么加入的。

    但是,我检查了这几个地方,都没有发现。于是,我也用notepad++逐个打开DIPS工程下的各个文件,逐个搜索“DIPSLib”,终于在“DIPS.vcxproj”文件发现了它的踪影。原来,Jeffrey Richter应用了project reference的方法,建立了DIPS工程对DIPSLib工程的强依赖关系。参考MSDN文档:点击打开链接

    下面,我将举例来说明这个方法。假设我要新建一个DIPS.exe和一个DIPSLib.dll工程,且前者需要加载后者,那么我们可以在一个solution中进行新建和管理这两个工程的代码。

1)先用VS正常新建DIPS.exe工程。

2)通过“右键解决方案 -> 添加 -> 新建项目”的方式添加DIPSLib.dll工程到同一个solution下。

3)在VS的“解决方案资源管理器”中,展开DIPS.exe的目录树,可以看到下面第一项就是“引用(reference)”,第二项是“外部依赖项”。”右键引用 -> 添加引用“即可打开一个对话框,该对话框中会列出同一个solution下的其他工程项目。我们只需要勾选”DIPSLib“,然后确认关闭对话框即可。


2,hInstDll的句柄

    在MSDN的Installing and Releasing Windows Hook的示例代码中,它是通过LoadLibrary函数的返回值来获得hInstDll的句柄,然后传给SetWindowsHookEx作为第3个参数。

    而Jeffrey将SetWindowsHookEx函数也放到了DLL中,他巧妙地利用DLL的入口点获取DLL自身的句柄,如下:

BOOL WINAPI DllMain(<span style="color:#ff0000;">HINSTANCE hInstDll</span>, DWORD fdwReason, PVOID fImpLoad) {

   switch (fdwReason) {

      case DLL_PROCESS_ATTACH:
         // DLL is attaching to the address space of the current process.
         <span style="color:#ff0000;">g_hInstDll = hInstDll;</span>
         break;

    注意,g_hInstDll是非共享的全局变量,这样,在DIPS进程和在目标进程中的g_hInstDll的值是不一样的。

    此外,由于SetWindowsHookEx和GetMsgProc都位于DLL中,也就不需要像MSDN的示例代码一样通过GetProcAddress函数来获取GetMsgProc的地址了。


3,内存映射文件共享数据段

    DIPSLib利用了内存映射文件的自定义共享段的方式来在DLL的不同实例之间,即实施进程和目标进程之间,来共享数据(全局变量)。如下:

// Instruct the compiler to put the g_hHook data variable in 
// its own data section called Shared. We then instruct the 
// linker that we want to share the data in this section 
// with all instances of this application.
#pragma data_seg("Shared")
HHOOK g_hHook = NULL;
DWORD g_dwThreadIdDIPS = 0;
#pragma data_seg()

// Instruct the linker to make the Shared section
// readable, writable, and shared.
#pragma comment(linker, "/section:Shared,rws")

4,client and server模式

    DIPS利用Hook Procedure函数,在目标进程创建了一个隐藏的模态对话框"Wintellect DIPS"。

      // Create the DIPS Server window to handle the client request.
      CreateDialog(g_hInstDll, MAKEINTRESOURCE(IDD_DIPS), NULL, Dlg_Proc);

    这个对话框的Dlg_Proc函数通过共享的线程ID向DIPS发消息,而DIPS通过FindWindow获得该对话框的句柄,然后通过SendMessage相这个对话框发消息,它们之间形成一个client and server的关系。


5,利用线程消息队列来进行线程同步

    DIPS和"Wintellect DIPS"模态对话框之间有一个跨进程的同步,Jeffrey巧妙地利用了线程消息队列来同步,而不是自定义一个锁(需要跨进程)。

   // Wait for the DIPS server window to be created.
   MSG msg;
   GetMessage(&msg, NULL, 0, 0);

    查看MSDN的文档,对GetMesage说明如下:

Retrieves a message from the calling thread's message queue. The function dispatches incoming sent messages until a posted message is available for retrieval.

Unlike GetMessage, the PeekMessage function does not wait for a message to be posted before returning.

    此外,在《深入浅出MFC》一书中,侯老师展示了“在消息循环的时候,用PeekMessage替代GetMessage,并调用OnIdle函数进行空闲时间的后台工作”。


6,读写系统注册表

    在DIPSLib的SaveListViewItemPositions和RestoreListViewItemPositions中,展示了对系统注册表的读写。大家可以看源码学习一下。


7,SendMessage和PostMessage的区别

    SendMessage需要等WndProcedure函数返回,而PostMessage仅仅是将消息发送到指定线程的消息队列返回。MSDN文档说明如下:

Sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified window and does not return until the window procedure has processed the message.

To send a message and return immediately, use the SendMessageCallback or SendNotifyMessage function. To post a message to a thread's message queue and return immediately, use the PostMessage or PostThreadMessage function.


8,WM_GETMESSAGE和GetMsgProc

    SetWindowsHookEx函数的第一个参数表示Hook的类型,对应监视不同的系统事件(event)。DIPS使用的是WM_GETMESSAGE类型的Hook,查看MSDN文档:

WH_GETMESSAGE 3

Installs a hook procedure that monitors messages posted to a message queue. For more information, see the GetMsgProc hook procedure.

GetMsgProc 的说明:

An application-defined or library-defined callback function used with the SetWindowsHookEx function. The system calls this function whenever the GetMessage orPeekMessage function has retrieved a message from an application message queue. Before returning the retrieved message to the caller, the system passes the message to the hook procedure.

The HOOKPROC type defines a pointer to this callback function. GetMsgProc is a placeholder for the application-defined or library-defined function name.

    因此,DIPS在调用SetHook给目标线程安装好Hook后,第一件事就是给目标线程发消息,主动激活它的HookProcedure。

         // The hook was installed successfully; force a benign message to 
         // the thread's queue so that the hook function gets called.
         bOk = PostThreadMessage(dwThreadId, WM_NULL, 0, 0);

    此外,在HookProcedure函数中,为了避免被其他消息击中多次,它定义了一个静态的计数器。

LRESULT WINAPI GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam) {

   static BOOL bFirstTime = TRUE;

   if (bFirstTime) {
    ...... // only once
   }

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


二、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、付费专栏及课程。

余额充值