2012年12月13日补充:
这篇文章写的时候是我还在上学的时候,所以不管是从技术实现角度还是文笔都显得很嫩,在此向所有无意间看到这篇文章的人表示抱歉。我写了这篇文章之后2年有人想问我要源代码,唉,如果我下次写文章一定贴上源代码,不过那么老的代码我实在是不大情愿找出来了。
我希望这篇文章已经把实现原理的每一个细节都点到了,虽然讲了很多废话,但是细节都埋在废话当中。
我自己简单看了一下我自己的文章(说实话我也有点忘记了),然后把实现流程做一个归纳:
1、在想要收到通知的项目(简称“原项目”)中,至少要包含一个窗口用以接受消息,并且必须保证在别的进程中能找到这个窗口句柄。
2、创建一个dll项目(简称“新项目”),在dllMain函数中根据DLL_PROCESS_ATTACH和DLL_PROCESS_DETACH向原窗口发送消息。
3、新项目中导出两个函数,一个用来增加hook一个用来删除hook,这两个函数的实现就在代码中(StartupHook/CloseHook),这是实现hook的基本技术。
4、想一个办法加载一次这个dll,比方说在原项目中调用LoadLibrary,然后去调StartupHook,这样在运行时,windows系统会把这个dll强制插入到所有的进程中(除了系统认为没有权限插入dll的进程),以及会为新创建的进程插入此dll。
5、第一次加载dll是手动的,之后为绝大部分进程“注射”dll是由系统完成的,最后卸载也是手动的,此时系统会把此dll从那些强制注射的进程中卸载掉。这其实就是整个程序的全部执行流程了。
以下是原文章内容:
第二次写文章,不好意思,和第一次理由相同,就是网上没有找到符合此问题的满意答案。
在一开头,我们需要统一语言,本人使用C++开发(确切的说是练习开发)windows程序,不是MFC。
还有本人使用Visual Studio 2008作为集成开发环境。
不敢称此为技术,只能叫方法。前面的三点全是不同情况的分析(意味着废话),需要马上知道方法的请直接看第四点。
我的需求是制作一个任务管理器。
方法有几种,可供参考:
第一,用窗口的计时器机制,不断枚举进程。
(顺便提及一下,关于如何枚举进程,建议学习CreateToolhelp32Snapshot及相关函数的使用,网上亦有文可考,在此不做累述,因为不是重点。)
这么做当然很方便,但是会出现一些问题,首先,设置一个计时器(SetTimer)的开销比较大,一个任务管理器本来就是小程序,如果为了偶尔发生的一些CreateProcess函数而设置一个不停地做无用功的计时器,过于浪费系统资源;其次,你的计时器时间间隔设置成多少?如果时间长了,就不能及时的反应进程信息,如果时间短了,自然频率就上去了,开销增大。
所以此方法适合widows程序的初学者尝试学习,不应该作为一个软件成品的一部分。
第二,使用windows的消息钩子技术,钩取进程启动和关闭消息。
当然,此方法完全不能使用,只是个人在一段时期内曾经用过,但是比第一种方法还要差。
首先,消息钩子只适用与“存在消息发送和接受”的程序,换句话说,就是窗口程序,windows这个操作系统并没有提供一种机制,可以在内核模式下(进程一级)捕获消息,当然是出于安全性考虑。所以只能发送消息给窗口,那么就马上否定了能够抓到后台运行程序的可能性。
其次,对于一个窗口程序而言,什么消息代表它的建立和灭亡?显然WM_CREATE和WM_QUIT消息(或者相关消息),但是很明显,有一条消息不是windows钩子能够抓到的,那就是WM_CREATE消息,因为windows消息钩子只能钩那些存在于消息队列中的消息,而WM_CREATE并不进消息队列,所以适用范围又减小了一半。
前两种方法是我最开始使用的方法,2个方法结合了使用,所以既没能保住效率,也没能保住即时性。
第三种方法比较难理解,但是是一种“正确”的方法,就是说它完全可以实现我们需要的功能,就是使用API Hook技术。
当然,这个方法需要深刻理解PE格式还有windows的内核工作原理。
我们知道windows启动一个进程,一定会调用一个API函数就是CreateProcess(),而结束一个进程则可能用到TerminateProcess()和ExitProcess(),那么我只要知道程序什么时候调用了这些函数,就可以明确地知道打开进程、关闭进程事件。
(当然这里也没法详细讲解API Hook技术,因为不是本文重点,所以需要了解的朋友请自己参考《windows核心编程》上的内容,里面有详细的解释说明,网上也有,但是讲得都不是很能说明问题(个人观点),因此建议还是直接看书比较好。我的书是第四版,在第四部分,22章里面讲到这个技术。)
此技术当然也有问题,我挂接了一个API且不说很烦,我还需要手动把dll插到各个能够调用CreateProcess的函数中,还好我们一般运行程序都是用双击点开的,那么只要挂接explorer.exe就行了;但是万一有人使用命令行模式打开呢?还需要挂接cmd.exe和taskmgr.exe(默认的那个任务管理器也具有命令行功能)。
第四种方法不是我最近想到的,但是是我最近忽然意识到的,只能怪我原来对于windows钩子和dll的理解不够深刻,所以没能发现原来这么简单就可以了。
需要一节基本的预备知识:dll映射机制、钩子原理,当然还有windows进程如何调用一个dll和窗口机制。
1、把没用的先去掉,窗口机制是为了能把捕获到的事件发送给我需要的程序,我前面说了,windows并没有提供一种机制能够使得进程与进程之间能够自由通信,所以需要依赖窗口,好在任务管理器不可能没有窗口(不然拿什么现实给用户?总不好自己写显卡驱动吧^_^),所以只要捕获到之后对着窗口发送消息就OK了。
2、dll是一种“代码共享”机制,为了节省物理内存。dll本身不能执行任何代码,它属于“应用程序”,但是没有程序入口函数;它可以分配内存但是只能在映射到某一个进程地址空间之后才能分配,而分配的内存也不属于dll而是属于加载dll的进程和线程;可以说,当dll被加载了之后几乎失去了它作为dll的所有特征标志(引用《windows核心编程》原话)。我的理解就是如果dll没有被映射,它没有任何可利用价值。
3、钩子的原理也在核心编程中讲到,用一句话来简单描述,就是“一个强势插入的dll”。
比如对于一个消息钩子,如果消息队列中一条消息准备发送到某进程的某窗口,那么系统会首先检查是否为这个进程插入了消息钩子,如果有,那么检查是否将钩子所在的dll映射到了进程地址空间,如果没有,那么进行一次映射,如果有,那么需要检查数据一致性(这个由系统完成),接下来就是调用我们提供给消息钩子的那个回调函数,直到回调函数运行完毕,那么消息才最终发送给应用程序。
4、刚刚我已经讲了,dll不会“自动”地执行代码,而是只能由进程调用LoadLibrary()函数加载到地址空间里面之后才能够体现它的价值。但是虽然它没有入口函数,却有一个消息通知函数,函数原型为:BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad) 这个函数的源代码可以由集成开发环境自动生成。
这个函数的唯一功能就是提供了进程钩取、进程释放、线程钩取、线程释放4个事件的消息通知,此函数非常强大,在此不予多讲。
好了,关键在这第四点里,我们现在所需要的不就是进程启动通知吗,而一个进程加载一个dll的事件是可以被我们捕捉到的。那么自然可以想到:
我们的目的是:我希望设计一个程序,这个程序能够让操作系统做出一个改变,这个改变使得每当一个程序启动/关闭的时候都能够给我的程序发送一个消息以指明发生的启动/关闭事件。
有了第四点,这个问题等价于:我希望能够设计一个程序,这个程序能够让操作系统做出一种改变,这种改变使得每当一个程序启动的时候,都能够加载一个指定的dll,这下不就行了?
那么怎么保证后面说的那种情况呢?显然,“钩子”可以实现这个功能啊。
综上所述,最关键的一句话就是:我只要设计一个dll,让这个dll成为一个钩子,插入到所有的进程,当我在这个dll的进程钩取、释放通知里面实现各一个函数,这个函数的功能是发送一条消息到我所指定的窗口中去。
讲到现在,问题解决了,如果上面讲的话能够完全明白,以下的代码就没有用处了,以便于不浪费一些理解能力非常强的人的宝贵的时间。
//---------------------------DLL部分--------------------------------------------
//---------------------------ProcHook.h---------------------------------------
extern "C"
{
__declspec(dllexport)
void StartupHook(void);
__declspec(dllexport)
void CloseHook(void);
}
void ProcessHookUp();
void ProcessHookDelete();
//--------------------------End Of File----------------------------------------
//--------------------------dllmain.cpp---------------------------------------
#include"ProcHook.h"
int count = 0;
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
ProcessHookUp();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
ProcessHookDelete();
break;
}
return TRUE;
}
//--------------------------End Of File----------------------------------------
//--------------------------ProcHook.cpp--------------------------------
#include"ProcHook.h"
HHOOK hThis = NULL;
LRESULT CALLBACK ProcessClose(int nCode, WPARAM wParam, LPARAM lParam)
{
return CallNextHookEx(hThis, nCode, wParam, lParam);
}
void StartupHook(void)
{
hThis = SetWindowsHookExW(WH_GETMESSAGE, ProcessClose, GetModuleHandleW(L"ProcHook.dll"), 0);
}
void CloseHook(void)
{
UnhookWindowsHookEx(hThis);
}
void ProcessHookUp()
{
DWORD proId = GetCurrentProcessId();
HWND hWnd = FindWindow(L"MyClass", L"MyWindow");
if (hWnd)
PostMessage(hWnd, WM_WINDOW, 0, proId);
else
CloseHook();
}
void ProcessHookDelete()
{
DWORD proId = GetCurrentProcessId();
HWND hWnd = FindWindow(L"MyClass", L"MyWindow");
if (hWnd)
PostMessage(hWnd, WM_WINDOW, 1, proId);
else
CloseHook();
}
//--------------------------End Of File----------------------------------------
//--------------------------调用部分-------------------------------------------
#include"ProcHook.h"
#pragma comment(lib, "ProcHook.lib")
class A()
{
public:
A();
~A();
};
A::A()
{
StartupHook();
}
A::~A()
{
CloseHook();
}
//-------------------------End Of File----------------------------------------
最后在WinMain函数(或者其他的程序切入函数)中声明一个A类的对象即可,当函数执行完毕自动撤销。或者窗口被清除了也可以自动撤销。
注意:为了尽可能不让大家看到忒长的代码,我只是从我的程序代码中抽取了有用的部分,由于不是编译过的,所以难免会发生失误,语法错误或者保留了我自己的程序上面的一部分,使得有些地方看不明白什么用,如果发生了那也请大家原谅。