上篇我们一起看了附加到进程这个功能实现后的样子,这篇我们就来讲一下他的实现原理。如果你还没有看过上一篇里的功能介绍的话,建议回去扫一眼,花不了二分钟的时间,要不然你继续往下看的话,会一头雾水的 。
从上篇的演示中,我们不难看出,要实现附加到进程的功能,至少需要解决两个问题 。
一.如何把HibernatingRhinos.Profiler.Appender.dll送到目标进程,并在这个进程里调用HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.Initialize() 让NhibernatePorfiler 实现初始化
二. 如何给NHProf加一个热键,可以方便的呼出我们的附加到进程。
下面我们就来一个一个的解决这两个问题。
解决第一个问题用到的主要 技术就是进程注入, 要实现进程注入无非就是三种 方法 1.利用全局钩子。2 .利用 CreateRemoteThread和LoadLibrary把你的DLL注入。 3.用 CreateRemoteThread和WriteProcessMemory 直接往目标进程 写指令(这个其实和第二种用的技术是一样的)。 在这里我们采用的是第一种方式 ,这是三种方式中最安全的方式,当然也是适应范围最小的,只能用于使用了USER32的进程中 ,也就是必须是有界面的程序。
进程注入的代码,我们就不细讲了,注释写的已经很详细了,你们可以自已看一看源码,我已经将他们封装在了Injector这个工程里,这是一个VC CLR Library的工程,如果你的VS没有装C++语言的话,就直接引用DLL。编译后的DLL,我也拷贝了一份放在了附录根目录下的Libs文件夹里。名字就叫 Injector.dll 。在这里我们只简单的提一下他的用法。
Injector.dll 的使用很简单,不过在讲它如何用之前,我们还需要做一些假设。
首先我们假设,有一个名为UI的工程,他编译后生成的程序正是实现进程注入的主程序,在这个程序里,可以列出来系统里的所有进程,并且可以选则一个进程然后点击附加到进程按钮后就可以实现进程注入。
然后我们再假设,还有一个名为ProcessViewer的的工程,这是一个类库(Class Library)工程,我们正是要把这个工程编译后生成的DLL,注入到目标进程。
说到这里,如果已经打开了源码的朋友,可能已经看出什么了,是的,这些其实并不是什么假设,而是我们源代码里实际的项目 命名 。
选中的那个就是UI工程了,他上面的那个就是ProcessViewer工程。
从上图还可以看到,除了这两个工程外,还有好几个工程,这里我们来对他们作个简单的介绍。
DotNetHookLibrary : UI调用了他里面的AssembleViewer的IsDotNet方法来验证目标进程是否.Net程序 。
Injector : 这个就是上面介绍的实现注入的核心DLL了。其实就是先把这个工程生成的DLL注入到目标进程,再由这个DLL在目标进程里将我们要注入的DLL再加载进目标进程。
NHProfLancher : 这个是为了解决上面讲的第二个问题:加热键而写的一个NHProf的加载程序。这里就先不讲了,后面会提到。
Win32SDK : 封装API的一个类库工程,本来我的意思是 慢慢的把所有API都封装到这里面,那么以后只要引用这个工程就可以方便的调用API了,不过细想后发现其实意义不大,因为完全没有必要因为只使用了一个API,就引用这么一个庞大的类库,不过已经写了而且引用了,就放在这里了。
看完了上面的所有工程,细心的可能已经发现了,上节里我们使用的那个测试用的被注入的程序的工程这里并没有,是的,为了方便同时调用,我把那个工程放在了另外一个解决方案里,就在附录的根目录下的 测试 文件夹里,测试文件里有一个Test.sln 。打开它 。
这个就是测试用的被注入的目标程序了。
OKAY 描述到这里,基本上可以讲Injector.dll 的使用方法了。
要利用Injector.dll实现进程的注入,首先你得在你的主程序里添加对他的引用(Add Reference)了 ,在我们这里,当然是在UI这个工程里添加对他的引用了。 能打开VC CLR Library类型工程的,直接引用工程,不行的,Browser选中Libs下的 Injector.dll 。引用后,只要在需要的地方调用App.Security.Injector.Launch就可以实现注入了。
我们先来看看 App.Security.Injector.Launch 的定义
//----------------------------------------------------------------------------- // 方法描述 : 启动注入,此函数执行成功,注入便已成功 , 并会在目标进程执行 className.methodName // 参数 : // windowHandle 目标进程主窗体的句柄 // assembly 要注入到目标进程的Assembly // className 注入后,要执行的方法的类名 // methodName 注入后,要执行的方法名 // 返回值 : void //----------------------------------------------------------------------------- void Injector::Launch(System::IntPtr windowHandle, System::Reflection::Assembly^ assembly, System::String^ className, System::String^ methodName)
再看看我们UI工程里的实际代码 (这段代码就在附加进程按钮的单击事件里:))
App.Security.Injector.Launch( targetProcess.MainWindowHandle, typeof(ProcessViewer.ProcessViewer).Assembly, typeof(ProcessViewer.ProcessViewer).FullName, "Entry" );
那么结合前面所讲的。这句执行完成后。ProcessViewer.ProcessViewer所在的Assembly(就是我们的ProcessViewer工程编译后生成的DLL了) 就已经被注入到 主窗体句柄为 targetProcess.MainWindowHandle 的目标进程里了。并且会在目标进程里执行ProcessViewer.ProcessViewer类的Entry 方法 。
当然要明白刚才讲的,还有二个疑问需要解决,第一个就是 targetProcess.MainWindowHandle 从那里来的。 这个其实不难理解,前面也讲了,UI有列出所有进程的功能,而且还可以选择一个进程实现注入,那么这个targetProcess自然就是你选中的那个进程的对象了, targetProcess的类型就是Process 。第二个刚才你也没讲过引用了ProcessViewer那个工程, 直接这样typeof(ProcessViewer.ProcessViewer).Assembly 不会出错吗 ??? 这个吗,当然是要先引用一下ProcessViewer工程的,那么又有问题了,ProcessViewer是要被注入目标进程执行的,那么有必要还要先要加载到我们这个进程里吗?答案是原则上是没有必要的,事实上我们只要知道了这个Assembly的路径就完全可以实现注入了,但在我们的工程里,把传给的Lancher的第二个参数,写成了 要传递一个Assembly对象,所以在我们的工程里是要引入一下的,这样写起来的代码,看起来 显得 更优雅一点。 :)
那么依次类推,要把HibernatingRhinos.Profiler.Appender.dll注入到目标进程,并在目标进程里执行HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.Initialize() 应该怎么做呢
App.Security.Injector.Launch( targetProcess.MainWindowHandle, typeof(HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler).Assembly, typeof(HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler).FullName, "Initialize" );
你的第一反应一定是这样写,原则上(汗~~~,又是原则上)这样写应该也是可以的,但实际上我们并没有这样做(事实上也不能这么,你试试就知道了),我们还是如前面描述的那些那样先把ProcessViewer送到了目标进程,然后在目标进程执行了ProcessViewer的Entry方法。然后才在Entry方法里加载了HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler ,并利用反射原理执行了他的Initialize 方法。我们来看看 ProcessViewer的Entry代码。
public void Entry() { string path = this.OriginalPath + "\\HibernatingRhinos.Profiler.Appender.dll"; string f = System.Environment.CurrentDirectory + "\\HibernatingRhinos.Profiler.Appender.dll"; if (!File.Exists(f)) { File.Copy(path, f); } string className = "HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler"; Assembly assembly = Assembly.LoadFile(f) ; Type type = assembly.GetType(className); MethodInfo mi = type.GetMethod( "Initialize", BindingFlags.Public | BindingFlags.Static, null, Type.EmptyTypes, null ); mi.Invoke(null, null); }
上面的 this.OriginalPath 是在Injector.dll 里传过来,实际就是ProcessViewer.dll所在的目录。
在这段代码先将this.OriginalPath 目录下的HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.dll 先拷贝到目标进程所在的目录里(所以你必须在UI的工程里也引用一下HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.dll ,这样才能保证this.OriginalPath 的目录下有这个DLL),这是必须的,因为只有在目标进程里才有Nhibernate的相关DLL,而HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler的初始化是需要这些的,这也是我们不直接将HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.dll送到目标进程的原因之一 , 另外一个原因是你会发现如果有了ProcessViewer的这个Entry的话那么在将HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.dll加载到目标进程前后,可以更加方便的做一些其它处理,当然这里我们 没有做处理了,但是你要想的话,是很方便的,例如,注入后, 提示一下什么的 …… 当然这是闲话了,将HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.dll拷贝到目标进程的目录后,就是加载它,然后 就是 利用 反射,调用他的Initialize方法。 注意ProcessViewer的Entry 是在目标进程执行的,所以这里的一切都是在目标进程执行 的
至此,我们的第一个问题就算解决了。 下面我们解决第二个问题。
其实刚才在描述我们源码的目录时,已经提到了,实现第二个问题的方法就是为NHProf.exe写一个Lancher , 也就是我们的那个NHProfLancher 工程了。
代码也不细讲了,看一看就明白了,不是很麻烦,在这里只要知道,在这个Lancher里主要做了哪些工作就可以了。
这个Lancher的只要有两方面的工作 1,注册热键,并捕获热键来打开我们的附加到进程工具。 2. 在另外一个线程里,加载执行NHProf.exe 并且一直等到NHPorf.exe退出为止,NHProf.exe一旦退出,此线程立马执行this.close 关闭本程序。然后在Form_Closed事件的处理函数里, 取消热键 。
OKAY, 实现原理介绍完了 , 具体的可以看源码,当然源码编译后,是需要把编译后的文件,全部拷贝到NHibernateProfiler的根目录的,拷过去后还要修改NHProfLancher.exe.config 。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="NHProfPath" value="NHProf.exe"/> <add key="AttachToProcessPath" value="AttachToProcess.exe"/> </appSettings> </configuration>
其中NHProfPath对应的是NHProf.exe的路径,可以是相对的,也可以是绝对的,AttachToProcessPath对应的就是附加到进程工具的路径了,同样的也是可以相对,也可以绝对