英文版:http://www.codeproject.com/Articles/4610/Three-Ways-to-Inject-Your-Code-into-Another-Proces
目录
Windows 钩子
CreateRemoteThread 和 LoadLibrary 技术
�D�D进程间通信
CreateRemoteThread 和 WriteProcessMemory 技术
�D�D如何用该技术子类化远程控件
�D�D何时使用 CreateRemoteThread 和 WriteProcessMemory 技术
结束语
附录A
附录B
附录C
附录D
附录E
附录F
参考资料
简介
本文将讨论如何把代码注入不同的进程地址空间,然后在该进程的上下文中执行注入的代码。 我们在网上可以查到一些窗口/密码侦测的应用例子,网上的这些程序大多都依赖 Windows 钩子技术来实现。本文将讨论除了使用 Windows 钩子技术以外的其它技术来实现这个功能。如图一所示:
图一 WinSpy 密码侦测程序
为了找到解决问题的方法。首先让我们简单回顾一下问题背景。
要“读取”某个控件的内容�D�D无论这个控件是否属于当前的应用程序�D�D通常都是发送 WM_GETTEXT 消息来实现。这个技术也同样应用到编辑控件,但是如果该编辑控件属于另外一个进程并设置了 ES_PASSWORD 式样,那么上面讲的方法就行不通了。用 WM_GETTEXT 来获取控件的内容只适用于进程“拥有”密码控件的情况。所以我们的问题变成了如何在另外一个进程的地址空间执行:
1.::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer );
通常有三种可能性来解决这个问题。
1.将你的代码放入某个 DLL,然后通过 Windows 钩子映射该DLL到远程进程;
2.将你的代码放入某个 DLL,然后通过 CreateRemoteThread 和 LoadLibrary 技术映射该DLL到远程进程;
3.如果不写单独的 DLL,可以直接将你的代码拷贝到远程进程�D�D通过 WriteProcessMemory�D�D并用 CreateRemoteThread 启动它的执行。本文将在第三部分详细描述该技术实现细节;
第一部分: Windows 钩子
范例程序�D�D参见HookSpy 和HookInjEx
Windows 钩子主要作用是监控某些线程的消息流。通常我们将钩子分为本地钩子和远程钩子以及系统级钩子,本地钩子一般监控属于本进程的线程的消息流,远程钩子是线程专用的,用于监控属于另外进程的线程消息流。系统级钩子监控运行在当前系统中的所有线程的消息流。
如果钩子作用的线程属于另外的进程,那么你的钩子过程必须驻留在某个动态链接库(DLL)中。然后系统映射包含钩子过程的DLL到钩子作用的线程的地址空间。Windows将映射整个 DLL,而不仅仅是钩子过程。这就是为什么 Windows 钩子能被用于将代码注入到别的进程地址空间的原因。
本文我不打算涉及钩子的具体细节(关于钩子的细节请参见 MSDN 库中的 SetWindowHookEx API),但我在此要给出两个很有用心得,在相关文档中你是找不到这些内容的:
1.在成功调用 SetWindowsHookEx 后,系统自动映射 DLL 到钩子作用的线程地址空间,但不必立即发生映射,因为 Windows 钩子都是消息,DLL 在消息事件发生前并没有产生实际的映射。例如:
如果你安装一个钩子监控某些线程(WH_CALLWNDPROC)的非队列消息,在消息被实际发送到(某些窗口的)钩子作用的线程之前,该DLL 是不会被映射到远程进程的。换句话说,如果 UnhookWindowsHookEx 在某个消息被发送到钩子作用的线程之前被调用,DLL 根本不会被映射到远程进程(即使 SetWindowsHookEx 本身调用成功)。为了强制进行映射,在调用 SetWindowsHookEx 之后马上发送一个事件到相关的线程。
在UnhookWindowsHookEx了之后,对于没有映射的DLL处理方法也一样。只有在足够的事件发生后,DLL才会有真正的映射。
2.当你安装钩子后,它们可能影响整个系统得性能(尤其是系统级钩子),但是你可以很容易解决这个问题,如果你使用线程专用钩子的DLL映射机制,并不截获消息。考虑使用如下代码:
01.BOOL APIENTRY DllMain( HANDLE hModule,
02. DWORD ul_reason_for_call,
03. LPVOID lpReserved )
04.{
05. if( ul_reason_for_call == DLL_PROCESS_ATTACH )
06. {
07. // Increase reference count via LoadLibrary
08. char lib_name[MAX_PATH];
09. ::GetModuleFileName( hModule, lib_name, MAX_PATH );
10. ::LoadLibrary( lib_name );
11.
12. // Safely remove hook
13. ::UnhookWindowsHookEx( g_hHook );
14. }
15. return TRUE;
16.}
那么会发生什么呢?首先我们通过Windows 钩子将DLL映射到远程进程。然后,在DLL被实际映射之后,我们解开钩子。通常当第一个消息到达钩子作用线程时,DLL此时也不会被映射。这里的处理技巧是调用LoadLibrary通过增加 DLLs的引用计数来防止映射不成功。
现在剩下的问题是如何卸载DLL,UnhookWindowsHookEx 是不会做这个事情的,因为钩子已经不作用于线程了。你可以像下面这样做:
就在你想要解除DLL映射前,安装另一个钩子;
发送一个“特殊”消息到远程线程;
在钩子过程中截获这个消息,响应该消息时调用 FreeLibrary 和 UnhookWindowsHookEx;
目前只使用了钩子来从处理远程进程中DLL的映射和解除映射。在此“作用于线程的”钩子对性能没有影响。
下面我们将讨论另外一种方法,这个方法与 LoadLibrary 技术的不同之处是DLL的映射机制不会干预目标进程。相对LoadLibrary 技术,这部分描述的方法适用于 WinNT和Win9x。
但是,什么时候使用这个技巧呢?答案是当DLL必须在远程进程中驻留较长时间(即如果你子类化某个属于另外一个进程的控件时)以及你想尽可能少的干涉目标进程时。我在 HookSpy 中没有使用它,因为注入DLL 的时间并不长�D�D注入时间只要足够得到密码即可。我提供了另外一个例子程序�D�DHookInjEx�D�D来示范。HookInjEx 将DLL映射到资源管理器“explorer.exe”,并从中/解除影射,它子类化“开始”按钮,并交换鼠标左右键单击“开始”按钮的功能。
HookSpy 和 HookInjEx 的源代码都可以从本文的中获得。
第二部分:CreateRemoteThread 和 LoadLibrary 技术
范例程序�D�DLibSpy
通常,任何进程都可以通过 LoadLibrary API 动态加载DLL。但是,如何强制一个外部进程调用这个函数呢?答案是:CreateRemoteThread。
首先,让我们看一下 LoadLibrary 和FreeLibrary API 的声明:
1.HINSTANCE LoadLibrary(
2.LPCTSTR lpLibFileName // 库模块文件名的地址
3.);
4.
5.BOOL FreeLibrary(
6.HMODULE hLibModule // 要加载的库模块的句柄
7.);
现在将它们与传递到 CreateRemoteThread 的线程例程�D�DThreadProc 的声明进行比较。
1.DWORD WINAPI ThreadProc(
2.LPVOID lpParameter // 线程数据
3.);
你可以看到,所有函数都使用相同的调用规范并都接受 32位参数,返回值的大小都相同。也就是说,我们可以传递一个指针到LoadLibrary/FreeLibrary 作为到 CreateRemoteThread 的线程例程。但这里有两个问题,请看下面对CreateRemoteThread 的描述:
1.CreateRemoteThread 的 lpStartAddress 参数必须表示远程进程中线程例程的开始地址。
2.如果传递到 ThreadFunc 的参数lpParameter�D�D被解释为常规的 32位值(FreeLibrary将它解释为一个 HMODULE),一切OK。但是,如果 lpParameter 被解释为一个指针(LoadLibraryA将它解释为一个串指针)。它必须指向远程进程的某些数据。
第一个问题实际上是由它自己解决的。LoadLibrary 和 FreeLibray 两个函数都在 kernel32.dll 中。因为必须保证kernel32存在并且在每个“常规”进程中的加载地址要相同,LoadLibrary/FreeLibray 的地址在每个进程中的地址要相同,这就保证了有效的指针被传递到远程进程。
第二个问题也很容易解决。只要通过 WriteProcessMemory 将 DLL 模块名(LoadLibrary需要的DLL模块名)拷贝到远程进程即可。
所以,为了使用CreateRemoteThread 和 LoadLibrary 技术,需要按照下列步骤来做:
1.获取远程进程(OpenProcess)的 HANDLE;
2.为远程进程中的 DLL名分配内存(VirtualAllocEx);
3.将 DLL 名,包含全路径名,写入分配的内存(WriteProcessMemory);
4.用 CreateRemoteThread 和 LoadLibrary. 将你的DLL映射到远程进程;
5.等待直到线程终止(WaitForSingleObject),也就是说直到 LoadLibrary 调用返回。另一种方法是,一旦 DllMain(用DLL_PROCESS_ATTACH调用)返回,线程就会终止;
6.获取远程线程的退出代码(GetExitCodeThread)。注意这是一个 LoadLibrary 返回的值,因此是所映射 DLL 的基地址(HMODULE)。
在第二步中释放分配的地址(VirtualFreeEx);
7.用 CreateRemoteThread 和 FreeLibrary从远程进程中卸载 DLL。传递在第六步获取的 HMODULE 句柄到 FreeLibrary(通过 CreateRemoteThread 的lpParameter参数);
8.注意:如果你注入的 DLL 产生任何新的线程,一定要在卸载DLL 之前将它们都终止掉;
9.等待直到线程终止(WaitForSingleObject);
此外,处理完成后不要忘了关闭所有句柄,包括在第四步和第八步创建的两个线程以及在第一步获取的远程线程句柄。现在让我们看一下 LibSpy 的部分代码,为了简单起见,上述步骤的实现细节中的错误处理以及 UNICODE 支持部分被略掉。
01.HANDLE hThread;
02.char szLibPath[_MAX_PATH]; // “LibSpy.dll”模块的名称 (包括全路径);
03.void* pLibRemote; // 远程进程中的地址,szLibPath 将被拷贝到此处;
04.DWORD hLibModule; // 要加载的模块的基地址(HMODULE)
05.HMODULE hKernel32 = ::GetModuleHandle("Kernel32");
06.
07.// 初始化szLibPath
08.//...
09.// 1. 在远程进程中为szLibPath 分配内存
10.// 2. 将szLibPath 写入分配的内存
11.pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath),
12. MEM_COMMIT, PAGE_READWRITE );
13.::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath,
14. sizeof(szLibPath), NULL );
15.
16.// 将"LibSpy.dll" 加载到远程进程(使用CreateRemoteThread 和 LoadLibrary)
17.hThread = ::CreateRemoteThread( hProcess, NULL, 0,
18. (LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
19. "LoadLibraryA" ),
20. pLibRemote, 0, NULL );
21.::WaitForSingleObject( hThread, INFINITE );
22.
23.// 获取所加载的模块的句柄
24.::GetExitCodeThread( hThread, &hLibModule );
25.
26.// 清除
27.::CloseHandle( hThread );
28.::VirtualFreeEx( hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE );
假设我们实际想要注入的代码�D�DSendMessage �D�D被放在DllMain (DLL_PROCESS_ATTACH)中,现在它已经被执行。那么现在应该从目标进程中将DLL 卸载:
01.// 从目标进程中卸载"LibSpy.dll" (使用 CreateRemoteThread 和 FreeLibrary)
02.hThread = ::CreateRemoteThread( hProcess, NULL, 0,
03. (LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
04. "FreeLibrary" ),
05. (void*)hLibModule, 0, NULL );
06.::WaitForSingleObject( hThread, INFINITE );
07.
08.// 清除
09.::CloseHandle( hThread );
进程间通信
到目前为止,我们只讨论了关于如何将DLL 注入到远程进程的内容,但是,在大多数情况下,注入的 DLL 都需要与原应用程序进行某种方式的通信(回想一下,我们的DLL是被映射到某个远程进程的地址空间里了,不是在本地应用程序的地址空间中)。比如秘密侦测程序,DLL必须要知道实际包含密码的控件句柄,显然,编译时无法将这个值进行硬编码。同样,一旦DLL获得了秘密,它必须将它发送回原应用程序,以便能正确显示出来。
幸运的是,有许多方法处理这个问题,文件映射,WM_COPYDATA,剪贴板以及很简单的 #pragma data_seg 共享数据段等,本文我不打算使用这些技术,因为MSDN(“进程间通信”部分)以及其它渠道可以找到很多文档参考。不过我在 LibSpy例子中还是使用了 #pragma data_seg。细节请参考 LibSpy 源代码。