第22章 DLL注入和API拦截
本章内容
22.1 DLL注入的一个例子
22.2 使用注册表来注入DLL
22.3 使用Windows挂钩来注入DLL
22.4 使用远程线程来注入DLL
22.5 使用木马DLL来注入DLL
22.6 把DLL作为调试器来注入
22.7 使用CreateProcess来注入代码
22.8 API拦截的例子
通常在操作系统中,每个进程都有自己独立的虚拟地址空间,这样不会破坏其他进程的数据。这样要和其他进程通信或操控其他进程就比较麻烦。
应用程序需要跨进程边界来访问另一个进程地址空间的情况如下:
1)想要从另一个进程创建的窗口派生之类窗口
2)需要确定另一个进程正在使用哪些DLL
3)给另一个进程安装挂钩(hook other processes)
22.1 DLL注入的一个例子
例如需要改变另一个进程所创建窗口实例的行为(subclass)。可以调用SetWindowLongPtr来修改其默认的窗口函数。(虽然MSDN上说无法直接subclass另一个进程创建的窗口,是因为无法跨越进程地址空间来访问另一个进程的数据)
例如执行以下代码:
SetWindowLongPtr(hWnd, GWLP_WNDPROC, MySubclassProc);
告知系统所有发到hWnd窗口的消息,应该由MySubclassProc来处理,而不是用该窗口的标准窗口过程处理。
从另一个进程(subclass)窗口的问题在于,窗口过程在另一个进程的地址空间中。
例如图22-1是一个简化的视图,显示了窗口过程如何处理收到的消息。进程A正在运行,它已经创建了一个窗口。user32.dll被映射到进程A的地址空间中,负责对发到和发往进程A的任何窗口的消息进行接收和派送。User32.dll检查收到一个消息的时候,会先确定该窗口的WndProc地址,然后调用它并在参数中传入窗口句柄,消息,以及wParam和lParam。 WndProc处理完毕以后,User32.dll会进入下一轮询并等待对下一条窗口消息的处理。
假设进程B要从进程A对其一个子窗口Subclass. 需要执行以下步骤:
1)需要获得进程A的需要被SubClass的窗口句柄。例如FindWindow
2)接着调用SetWindowLongPtr“试图”改变WndProc的地址。 因为SetWindowLongPtr会检查试图修改的窗口句柄是否属于另一个进程,若是则返回。
假设能让SetWindowLongPtr修改成功,那么发送到这个窗口上的消息都会重新转发到MySubclassProc上。可是MySubclassProc函数是属于进程B的地址空间,而在进程A中并不存在这个函数。于是这就会导致一个内存访问违规。
想让系统知道MySubclassProc在进程B的地址空间,并在调用之类窗口的时候切换CONTEXT
处于一下原因MS没有实现这个功能。
1)应用程序很少需要从其他进程的窗口subclass.大多数应用程序只处理自己的窗口
2)切换活动进程会耗费非常多的CPU时间
3)进程B中的一个线程必须执行MySubclassProc的代码,系统应该尝试使用哪个线程?一个已有线程还是创建一个线程?
4)User32.dll怎样才能知道与该窗口关联的窗口过程的地址在另一个进程中还是当前进程中?
通过某种方法,让MySubclassProc的进入到A的地址空间中,就能够调用SetWindowLongPtr并把MySubclassProc的地址传给他。这称为dll注入
22.2 使用注册表来注入DLL
在注册表项目:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\
AppInit_Dlls键值可能包含一个DLL的文件名或一组DLL的文件名(通过空格或逗号分离)
第一个DLL的文件名可以包含路径,其他DLL包含的路径将被忽略。最好将自己的DLL放到Windows的系统目录,这样就不必指定路径。
一下例子使用C:\MyLib.dll
并把LoadAppInit_DLLs的值修改为1 。
当User32.dll被映射到一个进程时,会收到DLL_PROCESS_ATTACH通知,当User32.dll对其处理时,会取得上述注册表的键值,并调用LoadLibrary来载入这个字符串指定的每个DLL。系统载入每个DLL的时候,会调用它们的DllMain函数并将参数设为DLL_PROCESS_ATTACH,这样每个DLL可以对其进行初始化。
(在DLL初始化调用Kernel32.dll可能没问题,而调用其他DLL可能会导致错误,甚至蓝屏)
这种方法有一些缺点:
1)DLL只会被映射到使用了user32.dll的进程,所有基于GUI的应用都会用到user32.dll。而CUI应用不会。这种方法就无法注入到编译器或连接器
2)DLL会被注入到所有的GUI程序,有时候仅想注入到一个或少数几个应用。(如果DLL本身代码存在bug,例如死循环这将对所有正常运行的应用程序造成影响)
3)如果DLL被映射到所有基于GUI的应用程序,在应用程序终止前,它将一直存在地址空间。
22.3 使用Windows挂钩来注入DLL
可以使用挂钩(HOOK)来将一个DLL注入到进程的地址空间中。
例如:进程A(类似Spy++)为了查看系统中各窗口处理了哪些消息,安装了一个WH_GETMESSAGE挂钩。这个挂钩是通过调用SetWindowsHookEx来安装的,例如
HHOOK hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hInstDll, 0);
第一个参数 WH_GETMESSAGE表示要安装挂钩的类型。
第二个参数GetMsgProc是一个函数的地址(在当前进程的地址空间)在窗口即将处理一条消息的时候,系统应该调用这个函数
第三个参数hInstDll标识一个DLL,这个DLL包含了GetMsgProc函数。hInstDll的值就是该DLL被映射到当前进程地址空间的虚拟地址
第四个参数0表示要给哪个线程安装挂钩。通常传入0表示给所有GUI线程安装挂钩
接下来看看会发生什么:
1)进程B中的一个线程准备向一个窗口派送一条消息
2)系统检查该线程是否已经安装了WH_GETMESSAGE挂钩
3)系统检查GetMsgProc所在的DLL是否已经被映射到进程B的地址空间中
4)如果DLL尚未被映射,那么系统是强制将该DLL映射到进程B的地址空间中,并将进程B中该DLL的锁计数器(lock count)递增
5)由于DLL的hInstDll是在进程B中映射的,因此系统会对它进程检查,看它与该DLL在进程A中的位置是否相同。
如果hInstDll相同, 那么两个进程地址空间中,GetMsgProc位于相同的位置。这样系统可以在进程A的地址空间调用GetMsgProc
如果hInstDll不同,系统必须确定GetMsgProc函数在进程B的地址空间中的虚拟内存地址。这个二地址通过下面公式得出:
GetMsgProc B = (hInstDll B - hInstDll A)+ GetMsgProc A //这里笔者将公式做了一个修改更容易理解
6)系统在进程B中递增该DLL的锁计数器
7)系统在进程B的地址空间调用GetMsgProc函数
8)当GetMsgProc返回的时候,系统递减该DLL在进程B中的锁计数器
注意:当系统把挂钩过滤函数(hook filter function)所在的DLL注入或映射到地址空间时,会映射整个DLL,而不仅仅是挂钩过滤函数。这使得DLL内的所有函数存在与进程B,能够被进程B中的任何线程调用。
这样为了从另一个进程的窗口来subclass。 可以先给创建该窗口的线程设置一个WH_GETMESSAGE挂钩,然后当GetMsgProc函数被调用的时候,就可以调用SetWindowLongPtr来进行窗口的Subclass。(当然窗口过程必须和GetMsgProc函数在同一个DLL中)
和注册表的方法相比,该方法允许我们在不需要该DLL的时候从进程的地址空间撤销对其映射。调用一下函数
WINUSERAPI
BOOL
WINAPI
UnhookWindowsHookEx(
_In_ HHOOK hhk);
当一个线程调用 UnhookWindowsHookEx的时候,系统会遍历自己内部的一个已经注入过该DLL的进程列表,并将该DLL的锁计数器递减,当所计数器减到0的时候,系统会自动从进程的地址空间撤掉对该DLL的映射。步骤6)中系统在调用GetMsgProc函数之前,会递增该DLL的锁计数器。可以防止内存访问违规。
如果锁计数器没有递增,那么进程B中的线程试图执行GetMsgProc的时候,系统中的另一个线程可能会调用UnhookWindowsHookEx函数,从而引起内存访问违规
Desktop Item Position Saver(DIPS)工具
DIPS.exe使用窗口挂钩将一个DLL注入到Explorer.exe(桌面)的地址空间中。
当切换了桌面分辨率以后桌面上的图标排序混乱了。作者设计了DIPS这个工具 给DIPS的命令行参数是S的时候,会为桌面上每个图标都添加一个注册表项:
HKEY_CURRENT_USER\Software\Wintellect\Desktop Item Position Saver
DIPS会为每个图标保存一个位置。比如要玩游戏修改分辨率,在修改分辨率以前运行DIPS S 然后打完游戏以后运行DIPS R,这时候DIPS会打开注册表找到那些保存过位置的图标并将其位置恢复到运行DIPS S的位置
(Windows桌面原来就是一个ListView控件,但是很多消息LVM_GETITEM和LVM_GETITEMPOSITION是不能夸进程边界的)
因为LVM_GETITEM消息要求在其LPARAM参数中传入一个LV_ITEM数据结构。这个内存地址只对发送消息的进程有意义,接受消息的进程是无法使用的。为了让DIPS能够正常工作必须将代码注入到Explorer.exe这样才能成功将LVM_GETITEM和LVM_GETITEMPOSITION消息发送到桌面的ListView控件。
DIPS.exe运行的时候,先获取桌面的ListView控件的窗口句柄:
// The Desktop ListView window is the
// grandchild of the ProgMan Window.
HWND hWndLV = GetFirstChild(GetFirstChild(FindWindow(TEXT("ProgMan"), NULL)));
使用Spy++可以捕获到一下这层关系。
捕获一个一个类为ProgMan的窗口,(Program Manager是为了兼容老版本的Windows设计的应用程序)Progman只有一个类别为SHELLDLL_DefView的子窗口。该窗口也只有一个子窗口类别是SysListView32 .这个SysListView32就是桌面的ListView控件窗口
接着可以通过GetWindowThreadProcessId来确定创建该窗口的线程标识符。然后把线程标识符传递给SetDIPSHook(在DIPSLib.cpp内实现)
该函数会给线程安装一个WH_GETMESSAGE挂钩,并调用下面的函数来强制唤醒Windows资源管理器线程
PostThreaMessage(dwThreadId, WM_NULL, 0, 0);
由于已经安装了WH_GETMESSAGE挂钩系统会将DIPSLib.dll注入到Windows资源管理器的地址空间并调用GetMsgProc函数。
该函数会检查是否被第一次调用,如果是第一个调用,会创建一个标题为"Wintellect DIPS"的隐藏窗口(由资源管理器所创建)
DIPS线程已经从SetDIPSHook调用中返回并接着调用以下函数
GetMessage(&msg, NULL, 0, 0);
这个调用将线程切换到睡眠状态,直到消息队列中出现消息为止。
使用线程消息队列来进行线程同步(比起内核对象还更容易)
当DIPS可执行文件中的线程被唤醒,它知道服务器对话框"Wintellect DIPS"已经创建完成,就调用FindWindow来找到该窗口。现在就可以通过窗口消息在客户(DIPS)和服务器(隐藏窗口)之间通信了。
为了告诉我们对话框去保存或恢复桌面图标的位置,只需要发送一条消息
SendMessage(hWndDIPS, WM_APP, (WPARAM)hWndLV, bSave);
对话框收到该消息。WPARAM参数是一个窗口句柄,表示要操作ListView控件,LPARAM是一个布尔值表示是要保持图标位置还是恢复图标位置。
当与对话框通信完成以后,为了让服务器终止(Wintellect DIPS),像对话框发送了一条WM_CLOSE消息,让其销毁自己。
最后在DIPS终止之前,再次调用SetDIPSHook,告知函数把已经安装的WH_GETMESSAGE挂钩清除。之后操作系统会自动从Explorer.exe的地址空间将DIPSLib.dll卸载。
(必须先销毁对话框再清除挂钩,否则对话框收到下一条消息会导致Windows资源管理器内存访问违规。)
源代码如下:
DIPS.cpp
/******************************************************************************
Module: DIPS.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#include "..\CommonFiles\CmnHdr.h"
#include <windowsx.h>
#include <tchar.h>
#include "resource.h"
#include "..\DIPSLib\DIPSLib.h"
//
BOOL Dlg_OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam) {
chSETDLGICONS(hWnd, IDI_DIPS);
return TRUE;
}
//
void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {
switch (id) {
case IDC_SAVE:
case IDC_RESTORE:
case IDCANCEL:
EndDialog(hWnd, id);
break;
}
}
//
INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
chHANDLE_DLGMSG(hWnd, WM_INITDIALOG, Dlg_OnInitDialog);
chHANDLE_DLGMSG(hWnd, WM_COMMAND, Dlg_OnCommand);
}
return FALSE;
}
//
int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int) {
// Convert command-line character to uppercase.
CharUpperBuff(pszCmdLine, 1);
TCHAR cWhatToDo = pszCmdLine[0];
if ((cWhatToDo != TEXT('S')) && (cWhatToDo != TEXT('R'))) {
// An invalid command-line argument; prompt the user.
cWhatToDo = 0;
}
if (cWhatToDo == 0) {
// No command-line argument was used to tell us what to
// do; show usage dialog box and prompt the user.
switch (DialogBox(hInstExe, MAKEINTRESOURCE(IDD_DIPS), NULL, Dlg_Proc)) {
case IDC_SAVE:
cWhatToDo = TEXT('S');
break;
case IDC_RESTORE:
cWhatToDo = TEXT('R');
break;
}
}
if (cWhatToDo == 0) {
// The user doesn't want to do anything.
return 0;
}
// The Desktop ListView window is the grandchild of the ProgMan window.
HWND hWndLV = GetFirstChild(GetFirstChild(
FindWindow(TEXT("ProgMan"), NULL)));
chASSERT(IsWindow(hWndLV));
// Set hook that injects our DLL into the Explorer's address space. After
// setting the hook, the DIPS hidden modeless dialog box is created. We
// send messages to this window to tell it what we want it to do.
chVERIFY(SetDIPSHook(GetWindowThreadProcessId(hWndLV, NULL)));
// Wait for the DIPs server window to be created.
MSG msg;
GetMessage(&msg, NULL, 0, 0);
// Find the handle of the hidden dialog box window.
HWND hWndDIPS = FindWindow(NULL, TEXT("Wintellect DIPS"));
// Make sure that the window was created.
chASSERT(IsWindow(hWndDIPS));
// Tell the DIPS window which ListView window to manipulate
// and whether the items should saved or restored.
BOOL bSave = (cWhatToDo == TEXT('S'));
SendMessage(hWndDIPS, WM_APP, (WPARAM)hWndLV, bSave);
// Tell the DIPS window to destroy itself. Use SendMessage
// instead of PostMessage so that we know the window is
// destroyed before the hook is removed.
SendMessage(hWndDIPS, WM_CLOSE, 0, 0);
// Make sure that the window was destroyed.
chASSERT(!IsWindow(hWndDIPS));
// Unhook the DLL, removing the DIPS dialog box procedure
// from the Explorer's address space.
SetDIPSHook(0);
return 0;
}
DIPSLib.h
/******************************************************************************
Module: DIPSLib.h
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#if !defined(DIPSLIBAPI)
#define DIPSLIBAPI extern "C" __declspec(dllimport)
#endif
///
// External function prototypes
DIPSLIBAPI BOOL WINAPI SetDIPSHook(DWORD dwThreadId);
End of File //
/******************************************************************************
Module: DIPSLib.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#include "..\CommonFiles\CmnHdr.h"
#include <windowsx.h>
#include <commctrl.h>
#define DIPSLIBAPI extern "C" __declspec(dllexport)
#include "DIPSLib.h"
#include "resource.h"
//
#ifdef _DEBUG
// This function forces the debugger to be invoked
void ForceDebugBreak() {
__try{ DebugBreak(); }
__except (UnhandledExceptionFilter(GetExceptionInformation())) {}
}
#else
#define ForceDebugBreak()
#endif
//
// Forward reference
LRESULT WINAPI GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam);
INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
//
// 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
#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")
//
// Nonshared variables
HINSTANCE g_hInstDll = NULL;
//
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad) {
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
// DLL is a