22.1 注入的一个例子(跨进程子类化窗口)
①子类化窗口可以改变窗口的行为,让发往该窗口的消息重新发到我们指定的过程来处理。但这种行为只能在本进程中(如A),对于从一个进程(如B)去子类化另一个进程(如A)时,会遇到无法跨越进程地址空间的边界问题。
②上图,进程B试图调用SetWindowLongPtr将进程A中的hWnd窗口过程,重新指定为MySubClassProc来处理(注意,MySubClassProc在进程B的地址空间中),而不是hWnd窗口的标准窗口处理过程。但因该行为是跨进程的,如果被允许的话,当系统要执行进程A的hWnd窗口过程时,就会试图调用进程A地址空间中另一个地址(该地址的值与进程B中的地址相同),原因是不同进程的地址空间是相互隔离的,所以会出现内存访问违规的错误。因此,为了避免这种错误,微软不允许SetWindowLongPrt修改别的进程的窗口过程。
③要实现跨进程子类化窗口,须用到“DLL注入”技术,将DLL注入到进程的地址空间中。
22.2 使用注册表来注入DLL
(1)注册表项的使用
①AppInit_Dlls键可能会包含一个或一组Dll的文件名(用空格或逗号分隔,第一个Dll的文件名可包含路径,但其他Dll包含的路径会被忽略)。为了让系统使用这个注册表项,还须将LoadAppInit_Dlls的值设为1。
②因为路径可能会被忽略,所以最好将自己的Dll放到Windows的系统目录中,这样就不必指定路径了。
(2)User32.dll的映射
①当User32.dll被映射到一个新的进程时,该进程会收到DLL_PROCESS_ATTACH通知。User32.dll对它进行处理的时候,会取得上述注册表键的值,并调用LoadLibrary来载入这个字符串中指定的每个DLL。
②当系统载入每个DLL时,会调用它们的DllMain函数,并将参数fdwReason设为DLL_PROCESS_ATTACH,这样每个DLL就能够对自己进行初始化。
③由于被注入的DLL是在进程生命期的早期被载入,所以调用Kernel32.dll中的函数是没问题的,但其他DLL中的函数可能会导致问题。
(3)注册表注入DLL的缺点
①基于GUI的应用程序都使用了User32.dll,但大多数基于CUI的应用程序不会使用它。因此想要将DLL注入到编译器或链接器,这种方法是行不通的。
②DLL会被映射进每个基于GUI的应用程序中,导致这些进程的线程都在运行我们的代码,如果我们的代码进入无限循环或错误地访问了内存,那么会影响到“容器”进程(即其他基于GUI的所有进程)的行为和健壮性
③我们的DLL被映射到每个基于GUI应用程序中,其生命期会直到应用程序终止(因为进程终止时,才会发送DLL_PROCESS_DETACH通知给User32.dll,在这里会卸载到我们的DLL)。而理想的情况下,DLL应注入到我们需要的进程中,并且映射的时间越短越好。
22.3 使用Windows钩子来注入DLL
22.3.1 钩子的类型
(1)按作用范围
①局部钩子:仅钩挂属于自身进程的事件
②远程钩子:可钩挂自身进程,还可以钩挂其他进程中发生的事件。又分为基于线程和系统范围的。
(2)按监视消息的类型分类
钩子名称 |
监视消息的类型和时机 |
WH_CALLWNDPROC |
每当调用SendMessage函数时,函数将消息发送给目标窗口前首先调用钩子函数 |
WH_CALLWNDPROCRET |
每当调用SendMessage函数时,函数将消息发送给目标窗口过程后再调用钩子函数 |
WH_GETMESSAGE |
每当调用GetMessage或PeekMessage函数时,函数从程序的消息队列中获取一个消息后调用钩子函数 |
WH_KEYBOARD |
每当调用GetMessage或PeekMessage函数时,如果从消息队列中得到的是WM_KEYUP或WM_KEYDOWN消息时,则调用钩子函数 |
WH_MOUSE |
每当调用GetMessage或PeekMessage函数时,如果从消息队列中得到的是鼠标消息,则调用钩子函数 |
WH_HARDWARE |
每当调用GetMessage或PeekMessage函数时,如果从消息队列中得到的是非鼠标和键盘消息时,则调用钩子函数 |
WH_MSGFILTER |
当用户对对话框、菜单和滚动条有所操作时,系统在发送对应的消息之前调用钩子函数,这种钩子只能是局部的 |
WH_SYSMSGFILTER |
同WH_MSGFILTER,不过是系统范围的 |
WH_SHELL |
当Windows shell程序准备接收一些通知事件前调用钩子函数,如shell被激活或重绘等。外壳应用程序是不接受WH_SHELL消息的,要使应用程序能够接收WH_SHELL,必须调用SystemParametersInfo函数注册该消息。WM_SHELL共有5种情况: ①TaskBar需要重绘某个按钮时 ②当系统地要显示关于Taskbar的一个程序的最小化形式 ③当目前的键盘布局状态改变。 ④当按Ctrl+Esc去执行TaskManager(或相等级别的程序)时。 ⑤只要有个top-level、unowned窗口被创建、起作用或被摧毁 |
WH_DEBUG |
用来给其他钩子函数除错 |
WH_CBT |
当基于计算机的训练(CBT)事件发生时调用钩子函数 |
WH_JOURNALRECORD |
日志记录钩子,用来记录发送给系统消息队列的所有消息 |
WH_JOURNALPLAYBACK |
日志回放钩子,用来回放日志记录钩子记录的系统事件 |
WH_FOREGROUNDIDLE |
系统空闲钩子,当系统空闲的时候调用钩子函数,这样就可以在这里安排一些低优先级的任务 |
22.3.2 钩子程序的结构:
(1)主要的3个功能模块
模块 |
作用 |
①主程序 |
用来实现界面或者其他功能 |
②钩子回调函数 |
用来接收系统发过来的消息 ①对于局部钩子,这些模块可在同一个EXE文件中 ②对于远程钩子这个回调函数必须在一个DLL中 |
③钩子的安装和卸载模块 |
调用SetWindowsHookEx和UnhookWindowsHookEx函数。 ①这部分虽然没有要求,但一般也放在DLL中,因为钩子创建以后会得到一个钩子句柄,这个句柄要在钩子回调函数中,以及卸载钩子的时候用到。 ②如果把这部分代码放在主程序中的话,还需要创建一个函数将它传回给动态链接库,所以不如直接放在库中 |
(2)钩子的安装:SetWindowsHookEx
参数 |
说明 |
int idHook |
要安装的钩子的类型 |
HOOKPROC lpfn |
钩子回调函数(在我们的地址空间或某个DLL) |
HINSTANCE hInstDll |
标识一个DLL,这个DLL包含了钩子回调函数。 如果是局部钩子这里须为NULL。 |
DWORD dwThreadId |
给哪个线程安装钩子。如果为0,表示给系统中所有的GUI线程安装钩子。 |
(3)举例说明钩子的工作过程——以WM_GETMESSAGE为例(进程A监视进程B的窗口消息,设钩子回调函数为GetMsgProc)
①当进程B的一个线程准备GetMessage一条窗口消息时。系统会检查该线程是否安装了WH_GETMESSAGE钩子。
②系统检查GetMsgProc所在的DLL是否己被映射进程B的地址空中。
③如果DLL尚未被映射,那么系统会强制将该DLL映射到进程B的地址空间中,并将进程B中该DLL的锁计数器递增
④由于安装钩子时参数hInstDll和回调函数的地址都是进程A地址空间中的值。当被映射到进程B时,系统会检查该DLL被映射的基地址是否与进程A中的位置相同。
Ⅰ如果相同,那么在两个进程的地址空间中,GetMsgProc函数位于相同的位置,在这情况下,系统可以直接在进程B(课本这里写A,是否是错误?)的地址空间中调用GetMsgProc。
Ⅱ如果不同,则得通过GetMsgProcB = hInstDllB + (GetMsgProcA - hInstDllA)公式计算GetMsgProc在进程B中的虚拟地址。
⑤系统在进程B中递增该DLL的锁计数器,这是因为调用GetMsgProc函数时须先锁定,以防止该DLL被别的线程卸载掉。当GetMsgProc返回后,系统递减该DLL在进程B中的锁计数器。
(4)钩子的卸载:BOOL UnhookWindowsHookEx(HHOOK hHook)
①当一个线程调用UnhookWindowsHookEx时,系统会遍历其内部维护的一个进程列表,该列表记录着那些己经注入过该DLL的进程。
②并将这些进程中该DLL的锁计数递减(注意每个进程都有各自对该DLL的锁计数器)。当某个进程的锁计数器减到0时,系统会自动从该进程的地址空间中撤消对该DLL的映射。
22.3.3 钩子链
(1)钩子链
①每一个钩子都有一个与之相关联的指针列表,称之为钩子链表,由系统来维护。这个列表的指针指向由应用程序定义的、后面由钩子调用的回调函数,也就是该钩子的各个处理子程。
②多个程序可以同时安装同一种钩子(如WH_GETMESSAGE),这些钩子组成一个钩子链,最近加入的钩子放在链表的头部,Windows负责为每种钩子维护一个钩子链。
③当一个事件发生的时候,Windows调用最后安装的钩子,然后由这个钩子的回调函数发起调用下一个钩子的动作,Windows收到这个动作后,再从链表中取出下一个钩子的地址并将调用传递下去。
(2)CallNextHookEx函数:调用下一个钩子回调函数的函数。
【Desktop Item Position Saver(DIPS)工具】——用来保存/恢复桌面图标的位置
(1)可能遇到的几个问题
①获得桌面的ListView控件窗口句柄的方法
A、XP下
//桌面的ListView控件是ProgMan窗口的孙子窗口
hWndLV = GetFirstChild(GetFirstChild(FindWindow(TEXT("ProgMan"),NULL)));
B、Win8下
HWND GetDestopListViewHandle(){ HWND hWnd = NULL,hWndItem; HWND hWndParent = FindWindowEx(0,0,TEXT("WorkerW"),NULL); while (hWndParent && (!hWnd)){ hWndItem = FindWindowEx(hWndParent, 0, TEXT("SHELLDLL_DefView"), NULL); if (hWndItem){ hWnd = FindWindowEx(hWndItem, 0, TEXT("SysListView32"), TEXT("FolderView")); break; } hWndParent = FindWindowEx(0, hWndParent, TEXT("WorkerW"), NULL); } return hWnd; }
②获取创建窗口的线程ID:dwThreadID = GetWindowThreadProcessID(hwnd,NULL)
③为Windows资源管理器创建一个“隐藏窗口”用于与DIPS.exe通信(利用窗口消息)
④卸载钩子时,须先销毁“隐藏窗口”这个对话框,再清除钩子。否则当对话框收到一下条消息时会导致Windows资源管理器的线程引发访问违规。
(2)实例源码
【动态链接库】
/************************************************************************ Module: DIPSLib.h Notices: Copyright(c) 2008 Jeffrey Richter & Christophe Nasarre ************************************************************************/ #pragma once #include <windows.h> #if !defined(DIPSLIBAPI) #define DIPSLIBAPI __declspec(dllimport) #endif ////////////////////////////////////////////////////////////////////////// //导出的外部函数原型 DIPSLIBAPI BOOL WINAPI SetDIPSHook(DWORD dwThreadId); //////////////////////////////////////////////////////////////////////////
//DIPSLib.cpp
/************************************************************************ Module: DIPSLib.cpp Notices: Copyright(c) 2008 Jeffrey Richter & Christophe Nasarre ************************************************************************/ #include "../../CommonFiles/CmnHdr.h" #define DIPSLIBAPI __declspec(dllexport) #include "DIPSLib.h" #include "resource.h" ////////////////////////////////////////////////////////////////////////// #ifdef _DEBUG //这个函数会强制调试器被调用 void ForceDebugBreak(){ __try{ DebugBreak();} __except (UnhandledExceptionFilter(GetExceptionInformation())){} } #else #define ForceDebugBreak() #endif ////////////////////////////////////////////////////////////////////////// //前置声明 LRESULT WINAPI GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam); INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); ////////////////////////////////////////////////////////////////////////// //通知编译器将g_hHook变量放在共享数据段中,以便让不同进程共享 #pragma data_seg("Shared") HHOOK g_hHook = NULL; DWORD g_dwThreadIdDIPS = 0; //安装钩子的线程(这里是DIPS.exe中一个线程) #pragma data_seg() #pragma comment(linker,"/section:Shared,rws") //可读可读、可共享 ////////////////////////////////////////////////////////////////////////// HINSTANCE g_hInstDll = NULL; ////////////////////////////////////////////////////////////////////////// BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad){ switch (fdwReason) { case DLL_PROCESS_ATTACH: g_hInstDll = hInstDll; break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: break; } return (TRUE); } ////////////////////////////////////////////////////////////////////////// BOOL WINAPI SetDIPSHook(DWORD dwThreadId){ BOOL bOk = FALSE; if (dwThreadId != 0){ chASSERT(g_hHook == NULL);//断言钩子还未被安装 //保存我们的线程ID,以便在GetMsgProc函数里可以通过客户程序(DIPS.exe)与 //服务程序(Windows资源管理器)间进行通信 g_dwThreadIdDIPS = GetCurrentThreadId(); //为指定的线程安装WH_GETMESSAGE钩子 g_hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, g_hInstDll, dwThreadId); bOk = (g_hHook != NULL); if (bOk){ //钩子安装成功,发送一条消息到Windows资源管理器的消息队列里,这时资源管理器 //会检测到一条消息,并调用GetMessage,于是钩子函数就开始工作。 bOk = PostThreadMessage(dwThreadId, WM_NULL, 0, 0); } } else{ //当传入dwThreadId为0时,表示要卸载钩子 chASSERT(g_hHook != NULL);//断言钩子己被安装, bOk = UnhookWindowsHookEx(g_hHook); g_hHook = NULL; } return (bOk); } ////////////////////////////////////////////////////////////////////////// LRESULT WINAPI GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam){ static BOOL bFirstTime = TRUE; if (bFirstTime){ //Dll刚被注入 bFirstTime = FALSE; //当调试被注入DLL的进程时,要取消注释 //ForceDebugBreak(); //创建DIPS服务窗口,以处理客户请求 CreateDialog(g_hInstDll, MAKEINTRESOURCE(IDD_DIPS), NULL, Dlg_Proc); //通知DIPS应用程序服务窗口己创建完毕,可以接收请求了 PostThreadMessage(g_dwThreadIdDIPS, WM_NULL, 0, 0); } return (CallNextHookEx(g_hHook, nCode, wParam, lParam)); } ////////////////////////////////////////////////////////////////////////// void Dlg_OnClose(HWND hWnd){ DestroyWindow(hWnd); } ////////////////////////////////////////////////////////////////////////// static const TCHAR g_szRegSubKey[] = TEXT("Software\Wintellect\Desktop Item Position Saver"); ////////////////////////////////////////////////////////////////////////// void SaveListViewItemPositions(HWND hWndLV){ int nMaxItems = ListView_GetItemCount(hWndLV); //删除先前的旧信息 LONG lRet = RegDeleteKey(HKEY_CURRENT_USER, g_szRegSubKey); //创建新的注册表项 HKEY hKey; lRet = RegCreateKeyEx(HKEY_CURRENT_USER, g_szRegSubKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, NULL, &hKey, NULL); chASSERT(lRet == ERROR_SUCCESS); for (int nItem = 0; nItem < nMaxItems;nItem++){ //获取ListItem项的名称及位置 TCHAR szName[MAX_PATH]; ListView_GetItemText(hWndLV, nItem, 0, szName, _countof(szName)); POINT pt; ListView_GetItemPosition(hWndLV, nItem, &pt); //保存名称及位置 lRet = RegSetValueEx(hKey, szName, 0, REG_BINARY, (PBYTE)&pt, sizeof(pt)); chASSERT(lRet == ERROR_SUCCESS); } RegCloseKey(hKey); } ////////////////////////////////////////////////////////////////////////// void RestoreListViewItemPosition(HWND hWndLV){ HKEY hKey; LONG lRet = RegOpenKeyEx(HKEY_CURRENT_USER, g_szRegSubKey, 0, KEY_QUERY_VALUE, &hKey); if (lRet != ERROR_SUCCESS) return; //如果ListView打开了自动排列属性,则暂时关闭 DWORD dwStyle = GetWindowStyle(hWndLV); if (dwStyle & LVS_AUTOARRANGE) SetWindowLong(hWndLV, GWL_STYLE, dwStyle & ~LVS_AUTOARRANGE); lRet = NO_ERROR; for (int nIndex = 0; lRet != ERROR_NO_MORE_ITEMS; nIndex++){ TCHAR szName[MAX_PATH]; DWORD cbValueName = _countof(szName);//必须要在循环体内重新设置该值,因为每次会被RegEnumValue改变,用来接受下一个szName POINT pt; DWORD cbData = sizeof(pt), nItem;//必须要在循环体内重新设置cbData,因为每次会被RegEnumValue改变 //从注册表中读取名称及位置信息 DWORD dwType; lRet = RegEnumValue(hKey, nIndex, szName, &cbValueName, NULL, &dwType, (PBYTE)&pt, &cbData); if (lRet == ERROR_NO_MORE_ITEMS) continue; if ((dwType == REG_BINARY) && (cbData = sizeof(pt))){ //查找ListView控件相应的项目 LV_FINDINFO lvfi; lvfi.flags = LVFI_STRING; lvfi.psz = szName; nItem = ListView_FindItem(hWndLV, -1, &lvfi); if (nItem != -1){ ListView_SetItemPosition(hWndLV, nItem, pt.x, pt.y); } } } //将自动排列属性恢复为原来的值 SetWindowLong(hWndLV, GWL_STYLE, dwStyle); RegCloseKey(hKey); } ////////////////////////////////////////////////////////////////////////// INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){ switch (uMsg) { chHANDLE_DLGMSG(hWnd, WM_CLOSE, Dlg_OnClose); //隐藏服务窗口 case WM_APP: //当调试被注入DLL的进程时,要取消注释 //ForceDebugBreak(); //lParam:表示保存或恢复操作。wParam:桌面ListView的句柄 if (lParam) SaveListViewItemPositions((HWND)wParam); else RestoreListViewItemPosition((HWND)wParam); break; } return (FALSE); }
//resource.h
//{{NO_DEPENDENCIES}} // Microsoft Visual C++ 生成的包含文件。 // 供 22_DipsLib.rc 使用 // #define IDD_DIPS 101 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
//rc文件
// Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // 中文(简体,中国) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h " END 2 TEXTINCLUDE BEGIN "#include ""winres.h"" " "