危险漫步博客
新鲜的“黑客思维”就是从全新的角度看待黑客技术,从更高的层面去思考;专注于黑客精神及技术交流分享的独立博客。
文章2306 浏览20613704

一种基于Hook机制的DLL注入方法

说起钩子(Hook),相信不少初涉Win32编程的人会感到迷惑。的确,钩子编程能算是Win32编程中较为高级的主题。如果读者明白回调(Callback)函数的含义,那就不难明白钩子的概念。钩子就是这样一种东西,它通过向windows注册一个用于回调的钩子函数(hook procedure),使windows在发生某些事件并需要传递消息的时候,首先调用钩子函数并传递消息给它。通常钩子函数会对消息进行一番处理之后,再将消息传递到目的应用程序,具体的钩子函数感兴趣的消息类型取决于钩子的类型。常见的类型如WH_KEYBOARD,WH_MOUSE等等,分别表示键盘消息钩子和鼠标消息钩子。本文的读者对象被假定为对Win32程序设计有一定的了解,所以关于钩子类型的更多的说明,请对此不太了解的读者自行参阅相关资料。此外,钩子还有全局钩子和局部钩子的区分。顾名思义,全局钩子对可以对全局范围内的消息进行检测,而局部钩子的“关心”范围只被限制在一个进程。

钩子的通常用法便是对消息进行过滤。比如最初的某些键盘记录程序,盗取QQ账号密码的程序,带有键盘嗅探功能的木马,往往是通过设置键盘消息钩子完成的。还有些程序无法通过任务管理器终止,这也可以通过在系统中设置钩子实现,但是本文讨论的并非是这些钩子的典型用法。在本文中,我们将一起探讨一种通过全局钩子对任意进程进行注入的技巧。当然,这绝非是什么让人望而却步的高深秘技,但是如若要真正对即将讨论的技巧做到理解,还是需要有些基础知识的,在之前我我在危险漫步博客中也没有提到过这个钩子,所以今天将会好好讲讲。

首先,读者需要明白DLL编程,尤其是操作系统将在何时调用DllMain()函数以及在系统调用时给予此函数的参数。说白了,就是要熟悉第二个参数fdwReason的四个可能的取值:

考虑一个将调用某DLL的拥有主线程和另一个新线程的程序的加载过程。首先,当这个程序被执行时,系统首先将以DLL_PROCESS_ATTACH的原因调用DllMain(),接着,第二个线程被创建了。此时,DUMain0将被以DLL_THREAD_ATTACH的原因调用。然后,刚刚创建的线程完成了自己的工作,将要结束了。此时,DILMain()将再一次被调用,不过这次的调用原因被指定为DLL_THREAD_DETACH。一会儿,程序也执行完毕,进程即将退出。此时,DllMain()最后将以DLL_PROCESS_DETACH的原因被调用。以上所描述的就是典型的fdwReason的取值同与之对应的现实情况。

第二点,读者应当明白安装钩子的过程的全部细节。众所周知,在程序中我们是通过调用SetWindowsHookEx()函数来完成一个钩子的设置的。全局钩子的代码必须以动态链接库的形式封装。在设置钩子的程序中,我们首先通过调用LoadLibrary()加载DLL获得DLL的实例句柄,或者通过调用GetModuleHandle()来获得。然后通过调用GetProcAddress()得到钩子函数的地址。当用这些参数调用SetWindowsHookEx()成功后,我们就向操作系统注册了我们的全局钩子。在示例中,我们向系统注册的是一个全局钩子。

然后我们考察一下当操作系统发现了我们的钩子感兴趣的消息的时候的动作。要知道,注册钩子函数的时候,我们提供的钩子函数地址以及钩子函数所在链接库的实例句柄都是与本进程相关的。而运行于保护模式下的进程们的内存空间是彼此隔离的。当系统要调用我们的钩子函数处理发往另一个进程的消息时,会是怎么样做的呢?系统首先会将我们的钩子函数所在的链接库映射到另一个进程的地址空间,然后计算钩子函数的地址,并调用之。关键就在于操作系统会将我们的链接库映射到另一个进程中去。正如上面对DllMain()调用时机的讨论中指出的那样,映射的过程中包含以DLLPROCESS_ATTACH的原因调用DIIMain()的关键步骤。映射后,我们的链接库算是目标进程的一个模块了。换言之,至此,我们的DLL已经注入到了目标进程中。

接下来的所有问题都将归结到DLL的设计上了。有一个问题我还没有说明,即我们的DLL是被动调用的。如何通过编程,可以实时判断出加载我们的DLL的进程并作出不同的行为昵?以下展示的是本文的示例代码,在代码中给出了答案。这样处理的结果是,我们的代码就可以有完全的针对性了。

让我们详细地分析一下这个程序。

首先是foo()函数。正如读者所见,这个函数实际上什么也不做。它只是一个占位函数,因为我们在设置钩子的时候需要一个钩子函数,而钩子函数自有其特定的格式,所以虽然这个函数没有实际用处,但却是必须的。

接下来就进入了DlIMain(),这里我们首先对调用原因进行判定。因为我们的程序的目的是注入目标进程,所以只在链接库第一次被进程加载的时候执行就可以了。接下来我们创建了一个新的线程用于执行函数的主要代码。有些读者可能会以后为什么我们不把程序的主要代码放到DllMain()里面呢?这是因为如果在DllMain()中设置太多的代码而不及时返回,在程序运行时候就会观察到明显的卡顿现象。(我曾猜测,通常说的当计算机感染病毒以后运行速度明显变慢,而且经常死机的现象与此有关。)为了使一切显得依旧正常,我们将主要代码放在了别处,而让DllMain()及时返回。

然后我们研究一下线程函数aThreadProc()。线程函数中最主要的功能是判断加载DLL的进程的名称。首先,我们使用CreateToolhelp32Snapshot()获得了当前进程的模块快照旬柄。因为在执行快照函数的时候,我们的DLL已经在目标进程的地址空间中了,所以这里GetCurrentProcessld()执行后得到的是目标进程的PID。接着我们使用Module32First()获得模块快照中的第一个模块的信息。这里用的到一点额外的知识是第一个模块的名字通常是就是主程序的名字,而进程的名字通常也就是主程序的名字。所以这里我们通过查看第一个模块的名字,进而间接的获得进程的名字。Module32First()函数使用的MODULEENTRY32结构中包含了很多有用的字段,但这里我们只使用到szModule,这便是模块的名字。

接着我们动态加载msvcrt.dll并从其中获得了Strcmp()函数的地址,接着用它比较szModule是否与iexplore.exe相同。在示例代码中,我们要注入的目标是lnternet Explorer,所以这里指定的是iexplore.exe。这里可以自由替换为其他的名字,以应读者自己的要求,注入不同的目标。

最后,在示例代码中,当我们发现了iexplore.exe后,只是简单的弹出了一个MessageBox显示下进程名字而已。在实际的项目中,if语句中包含的,将是全部用于注入后做进一步处理的代码。因为这只是个示例,所以我们不做别的,只弹出一个小窗口了事。

将以上代码编译为一个动态链接库文件的过程因为很是简单,所以就不再赘述了,但是要提醒的一点是,链按时需要将符号foo导出。至此,主要的工作已经完成。接下来测试一下,使用如下测试代码:

这个程序将加载上一步生成的mydll.dll文件,获得foo()的地址,并设置钩子,然后延时一段时间。之所以这样做,是为了我们实际考察程序的执行效果。三十秒的时间后,程序将钩子卸载,一切又将恢复正常。在运行测试程序的时候,应确保其与mydll.dll位于同一个目录。

预期的计划是,在钩子有效期间,运行其他程序没有任何异常,而运行Intemet Explorer时,在按下任意按键后(因为我们设置的是键盘钩子),会弹出一个MessageBox提示iexplore.exe。

至此,我们已经完成了全部的工作。简单做一点总结吧,我们首先是利用了钩子函数的加载过程,将自己的链接库加载到了其他的进程空间中。我们知道当链接库被加载的时候,DllMain()函数会自动执行。通过巧妙的设计DllMain()的代码,我们实现了在其他进程空间中执行任意代码的方法。

本文所描述的方法,只是一种常见技术的巧妙的应用。它一反常态,将钩子函数的加载过程巧妙地与进程注入结合起来。我想,明白钩子函数的加载过程的人有很多,但能想到这种思路的人却相对较少。这给我们的启示是什么呢?回答是因人而异的,但就我自己而言,从中学到的一点即:创新的实践是来自于创新的想法的。

相关推荐