插入DLL和挂接API
在Microsoft Windows中,每个进程都有它自己的私有地址空间。当使用指针来引用内存时,指针的值将引用你自己进程的地址空间中的一个内存地址。你的进程不能创建一个其引用属于另一个进程的内存指针。因此,如果你的进程存在一个错误,改写了一个随机地址上的内存,那么这个错误不会影响另一个进程使用的内存。
独立的地址空间对于编程人员和用户来说都是非常有利的。对于编程人员来说,系统更容易捕获随意的内存读取和写入操作。对于用户来说,操作系统将变得更加健壮,因为一个应用程序无法破坏另一个进程或操作系统的运行。当然,操作系统的这个健壮特性是要付出代价的,因为要编写能够与其他进程进行通信,或者能够对其他进程进行操作的应用程序将要困难得多。
有些情况下,必须打破进程的界限,访问另一个进程的地址空间,这些情况包括:
• 当你想要为另一个进程创建的窗口建立子类时。
• 当你需要调试帮助时(例如,当你需要确定另一个进程正在使用哪个DLL时)。
• 当你想要挂接其他进程时。
本章将介绍若干种方法,可以用来将DLL插入到另一个进程的地址空间中。一旦你的DLL进入另一个进程的地址空间,就可以对另一个进程为所欲为。这一定会使你非常害怕,因此,究竟应该怎样做,要三思而后行。
1.插入DLL:一个例子
假设你想为由另一个进程创建的窗口建立一个子类。你可能记得,建立子类就能够改变窗口的行为特性。若要建立子类,只需要调用SetWindowLongPtr函数,改变窗口的内存块中的窗口过程地址,指向一个新的(你自己的)WndProc。Platform SDK文档说,应用程序不能为另一个进程创建的窗口建立子类。这并不完全正确。为另一个进程的窗口建立子类的关键问题与进程地址空间的边界有关。
当调用下面所示的SetWindowsLongPtr函数,建立一个窗口的子类时,你告诉系统,发送到或者显示在hwnd设定的窗口中的所有消息都应该送往MySubclassProc,而不是送往窗口的正常窗口过程:
SetWindowLongPtr(hwnd, nIndex, MySubclassProc);
参数说明
nlndex | 说明 |
---|---|
GWL_EXSTYLE |
设定一个新的扩展风格。更多信息,请见CreateWindowEx。
|
GWL_STYLE | 设定一个新的窗口风格。 |
GWL_WNDPROC | 为窗口过程设置一个新的地址。 |
GWL_HINSTANCE | 设置一个新的应用程序实例句柄。 |
GWL_ID | 设置一个新的窗口标识符。 |
GWL_USERDATA |
设置与该窗口相关的用户数据。这些用户数据可以在程序创建该窗口时被使用。用户数据的初始值为0。
|
当hWnd参数标识了一个对话框时,也可使用下列值: | |
DWL_DLGPROC | 设置对话框过程的新地址。 |
DWL_MSGRESULT | 设置对话框中的消息处理程序的返回值。 |
DWL_USER | 设置的应用程序所私有的新的额外信息,例如句柄或指针。 |
换句话说,当系统需要将消息发送到指定窗口的WndProc时,要查看它的地址,然后直接调用WndProc。在本例中,系统发现MySubclassProc函数的地址与窗口相关联,因此就直接调用MySubclassProc函数。
由于对这个问题的解决并没有什么万全之策,因此Microsoft决定不让SetWindowsLongPtr改变另一个进程创建的窗口过程。
不过仍然可以为另一个进程创建的窗口建立子类—只需要用另一种方法来进行这项操作。这并不是建立子类的问题,而是进程的地址空间边界的问题。如果能将你的子类过程的代码放入进程A的地址空间,就可以方便地调用SetWindowsLongPtr函数,将进程A的地址传递给MySubclassProc函数。我将这个方法称为将DLL“插入”进程的地址空间。有若干种方法可以用来进行这项操作。下面将逐个介绍它们。
2.使用注册表来插入DLL
如果你曾经多少使用过Windows操作系统,你肯定熟悉注册表的情况。整个系统的配置都是在注册表中维护的,可以通过调整它的设置来改变系统的行为特性。
下图显示了使用Registry Editor(注册表编辑器)时该关键字中的各个项目的形式。该关键字的值包含一个D L L文件名或者一组D L L文件名(用空格或逗号隔开)。由于空格用来将文件名隔开,因此必须避免使用包含空格的文件名。列出的第一个D L L文件名可以包含一个路径,但是包含路径的其他D L L均被忽略。由于这个原因,最好将你的D L L放入Wi n d o w s的系统目录中,这样就不必设定路径。在窗口中,我将该值设置为单个D L L路径名C : M y L i b . d l l。
图 注册表窗口
当重新启动计算机及Wi n d o w s进行初始化时,系统将保存这个关键字的值。然后,当U s e r 3 2 . d l l库被映射到进程中时,它将接收到一个D L L P R O C E S S AT TA C H通知。当这个通知被处理时,U s e r 3 2 . d l l便检索保存的这个关键字中的值,并且为字符串中指定的每个D L L调用L o a d L i b r a r y函数。当每个库被加载时,便调用与该库相关的D l l M a i n函数,其f d w R e a s o n的值是D L L P R O C E S S AT TA C ,这样,每个库就能够对自己进行初始化。由于插入的D L L在进程的寿命期中早早地就进行了加载,因此在调用函数时应该格外小心。调用k e r n e l 3 2 . d l l中的函数时应该不会出现什么问题,不过调用其他D L L中的函数时就可能产生一些问题。U s e r 3 2 . d l l并不检查每个库是否已经加载成功,或者初始化是否取得成功。
在插入D L L时所用的所有方法中,这是最容易的一种方法。要做的工作只是将一个值添加到一个已经存在的注册表关键字中。不过这种方法也有它的某些不足:
• 由于系统在初始化时要读取这个关键字的值,因此在修改这个值后必须重新启动你的计算机—即使退出后再登录,也不行。当然,如果从这个关键字的值中删除D L L,那么在计算机重新启动之前,系统不会停止对库的映射操作。
• 你的D L L只会映射到使用U s e r 3 2 . d l l的进程中。所有基于G U I的应用程序均使用U s e r 3 2 . d l l,不过大多数基于C U I的应用程序并不使用它。因此,如果需要将D L L插入编译器或链接程序,这种方法将不起作用。
• 你的D L L将被映射到每个基于G U I的应用程序中,但是必须将你的库插入一个或几个进程中。你的D L L映射到的进程越多,“容器”进程崩溃的可能性就越大。毕竟在这些进程中运行的线程是在执行你的代码。如果你的代码进入一个无限循环,或者访问的内存不正确,就会影响代码运行时所在进程的行为特性和健壮性。因此,最好将你的库插入尽可能少的进程中。
• 你的D L L将被映射到每个基于G U I的应用程序中。这与上面的问题相类似。在理想的情况下,你的D L L只应该映射到需要的进程中,同时,它应该以尽可能少的时间映射到这些进程中。假设在用户调用你的应用程序时你想要建立Wo r d P a d的主窗口的子类。在用户调用你的应用程序之前,你的D L L不必映射到Wo r d P a d的地址空间中。如果用户后来决定终止你的应用程序的运行,那么你必须撤消Wo r d P a d的主窗口。在这种情况下,你的D L L将不再需要被插入Wo r d P a d的地址空间。最好是仅在必要时保持D L L的插入状态。
3.使用Windows挂钩来插入DLL
可以使用挂钩将DLL插入进程的地址空间。为了使挂钩能够像它们在16位Windows中那样工作,Microsoft不得不设计了一种方法,使得DLL能够插入另一个进程的地址空间中。
下面让我们来看一个例子。进程A(类似Microsoft Spy++的一个实用程序)安装了一个挂钩WNGETMESSAGE,以便查看系统中的各个窗口处理的消息。该挂钩是通过调用下面的SetWindowsHookEx函数来安装的:
HHook hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hinstDll, 0);
第一个参数W H G E T M E S S A G E用于指明要安装的挂钩的类型。第二个参数G e t M s g P r o c用于指明窗口准备处理一个消息时系统应该调用的函数的地址(在你的地址空间中)。第三个参数h i n s t D l l用于指明包含G e t M s g P r o c函数的D L L。在Wi n d o w s中,D L L的h i n s t D l l的值用于标识D L L被映射到的进程的地址空间中的虚拟内存地址。最后一个参数0用于指明要挂接的线程。
对于一个线程来说,它可以调用S e t Wi n d o w s H o o k E x函数,传递系统中的另一个线程的I D。通过为这个参数传递0,就告诉系统说,我们想要挂接系统中的所有G U I线程。现在让我们来看一看将会发生什么情况:
1) 进程B中的一个线程准备将一条消息发送到一个窗口。
2) 系统查看该线程上是否已经安装了W H G E T M E S S A G E挂钩。
3) 系统查看包含G e t M s g P r o c函数的D L L是否被映射到进程B的地址空间中。
4) 如果该D L L尚未被映射,系统将强制该D L L映射到进程B的地址空间,并且将进程B中的D L L映像的自动跟踪计数递增1。
5) 当D L L的h i n s t D l l用于进程B时,系统查看该函数,并检查该D L L的h i n s t D l l是否与它用于进程A时所处的位置相同。
如果两个h i n s t D l l是在相同的位置上,那么G e t M s g P r o c函数的内存地址在两个进程的地址空间中的位置也是相同的。在这种情况下,系统只需要调用进程A的地址空间中的G e t M s g P r o c函数即可。
如果h i n s t D l l的位置不同,那么系统必须确定进程B的地址空间中G e t M s g P r o c函数的虚拟内存地址。这个地址可以使用下面的公式来确定:
GetMsgProc B = hinstDll B + (GetMsgProc A - hinstDll A)
将GetMsgProc A的地址减去hinstDll A的地址,就可以得到G e t M s g P r o c函数的地址位移(以字节为计量单位)。将这个位移与hinstDll B的地址相加,就得出G e t M s g P r o c函数在用于进程B的地址空间中该D L L的映像时它的位置。
6) 系统将进程B中的D L L映像的自动跟踪计数递增1。
7) 系统调用进程B的地址空间中的G e t M s g P r o c函数。
8) 当G e t M s g P r o c函数返回时,系统将进程B中的D L L映像的自动跟踪计数递减1。
注意,当系统插入或者映射包含挂钩过滤器函数的D L L时,整个D L L均被映射,而不只是挂钩过滤器函数被映射。这意味着D L L中包含的任何一个函数或所有函数现在都存在,并且可以从进程B的环境下运行的线程中调用。
未完待续...