【原创】托管注入深入研究

CsToD 2008-12-09 08:20:11
加精
这是我发表在《黑客防线》2008年12期上的一篇文章,这里是网络上的首发。
附件是我写的一个工具“超级间谍(SuperSpy)”,主要包括一个窗口探测功能,甚至能够探测到VC自带的SPY++所不能探测到的窗口;当然,还包括这里提到的“托管注入”功能。
程序需运行在.Net2.0以上,如果需要使用注入功能,则需要.Net3.5以上。

超级间谍下载地址:
http://download.csdn.net/source/850509


托管注入深入研究
作者:ssyfzy(十三亿分之一的缩写)
前言:

本文的重点不在于介绍如何注入托管代码,而是侧重于介绍我的研究过程,这里面有很多曲折,可能会使你感到琐屑,但正所谓“授人以鱼,不如授人以渔”,了解了这个过程,会使你理解得更深刻。

正文:

网上关于dll注入的文章实在太多,但基本上都是针对Win32 dll的,而很少涉及到托管dll。
首先让我们来看看Win32 dll是如何注入的,通常有两种方法:钩子和远程线程。而远程线程更灵活,所以本文主要讨论远程线程的方法,为了便于交流,先明确以下概念:
1. 主程序:用于将dll注入到其它进程的exe
2. dll:被注入其它进程的dll
3. 宿主:dll将被注入的其它程序
前两个程序都是我们自己写的,而第3个是原来就有的。

远程注入的基本步骤为:主程序通过CreateRemoteThread函数迫使宿主调用LoadLibrary函数加载dll,从而执行dll的入口函数DllMain,只要我们把代码放到DllMain里,就可以被调用了。

现在我们来看托管dll的注入:

用C#或VB.NET写的dll没有DllMain函数,我们自然想到了功能强大的C++。通常,我们把用C#或VB.NET写的dll,或者用C++写的,但编译为/clr:pure的dll称为托管dll
而把用C++编写的,但编译为/clr的dll称为混合dll。
混合dll也可以调用托管代码,所以也可以将其称为托管dll,本文所说的托管dll注入,实际上是混合dll的注入。


我们首先想到的是用常规方法来注入混合dll,结果会发现:只要在DllMain函数里调用了托管代码,程序就会崩溃。
也许你还会想到下面的方法:
定义一个类,在其构造函数里调用托管代码,然后在全局域里定义这个类的一个变量,当我们这样做了后会发现,注入后,什么也没有执行。
查阅MSDN,我们找到了答案:
DllMain不能直接或间接地调用托管代码,并且全局变量不会进行初始化。
这样的结果让人非常沮丧!

难道真的就没有办法了吗?
网友CiCi给出了一个解决方案:
写一个混合dll,在其中定义一个导出函数,在这个导出函数里可以调用托管代码。
然后写一个非托管dll(也就是Win32 dll),在其DllMain函数里调用前面那个混合dll的导出函数。

这个方案能够解决问题,并且我在一段时间里,也一直使用这个方法。但总觉得不完美:必须使用两个dll。

有没有办法用一个dll就解决问题呢?答案是肯定的。

我在一次偶然的机会中,发现了一个现象:
如里用钩子来实现注入,则全局变量会得到初始化。这个发现让我非常困惑:为什么远程线程注入就不会初始化呢?
我考虑这两种方法的区别:钩子注入时,宿主会调用dll中的钩子回调函数。

于是我大胆设想,只要宿主调用了dll中的任意一个函数,全局变量就会得到初始化。
于是我在dll中定义了一个空实现的函数(因为我的目地是迫使全局变量初始化,而不是去执行这个函数)

结果正如我所料,全局变量被初始化了,其构造函数中的托管代码被调用了!

这里其实已经实现了托管代码的注入,当然,还远远不够完善。

这时候,我又觉得全局变量是多余的了:既然我能够使宿主调用dll中的一个特定函数,为什么不直接把托管代码放到这个函数中呢?
我马上进行了测试,也成功了。

之前我是这样实现让宿主调用dll中的一个特定函数的:
在dll被注入之后,我在DllMain函数里将这个特定函数的地址写到共享内存中(这时DllMain里没有调用托管代码,所以可以执行),然后主程序读取共享内存中的值,再通过远程线程迫使宿主调用这个特定函数。

于是我又想,既然可以在主程序中用远程线程迫使宿主调用dll中的特定函数,为什么不直接在dll中用相同的方法去调用呢?
我还没有来得及验证,马上又想到:dll跟宿主是一个进程,即使用远程线程,传给CreateRomoteThread函数的第一个参数也是-1(GetCurrentProcess()返回-1),为什么不直接用“近程”线程CreateThread呢?

需要着重强调地是:我当时用CreateThread的目的只是让它调用一个特定函数,而不是要去创建一个线程,虽然最终的确创建了一个线程,不过对于此目的,貌似只是一个副作用。

我非常兴奋,马上进行了验证,成功了!

现在我来总结一下托管代码注入的过程:
首先定义一个线程回调函数,可以在其中调用托管代码:
DWORD CALLBACK ThreadProc(LPVOID lp)
{
//可以在此调用托管代码
return 0;
}
然后在dll的入口函数DllMain里调用CreateThread函数:
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{

switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
CreateThread(0,0,ThreadProc,0,0,0);
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
这样就实现了托管代码的注入。


深入话题:

通常我们会有这样的要求:在主程序中单击某个按扭,使宿主执行dll中的一段代码,多次单击就多次执行。

上面的代码无法实现这个目地,因为只有dll刚被注入到宿主时,DllMain中的DLL_PROCESS_ATTACH才会收到通知,我们自然想到:每次执行后,卸载掉dll,下次单击后,DLL_PROCESS_ATTACH又会收到通知;遗憾的是,目前我还没有办法卸载掉这个dll。

对于非托管dll,我们可以通过FreeLibrary来卸载(如果是在dll内部卸载,还必须借助FreeLibraryAndExitThread函数,关于dll的自卸载,由于没有在这个程序中使用,这里就不做介绍了)
而对于托管dll,查阅MSDN,我们得到的结论是:.Net不支持dll的卸掉,dll只能随着应用程序域的卸载而卸载。

对于混合dll,应该如何卸载呢?
我尝试用Win32 dll的方式卸载,结果也“真的”卸载了,这是因为:
1. DllMain中的DLL_PROCESS_DETACH收到了通知
2. 用模块查看工具去看,宿主中确实不存在注入的dll了
但事实上是失败了,至少有两个问题:
1. 关闭宿主时,会提示出错
2. 再次注入时,虽然DLL_PROCESS_ATTACH会收到通知,但用CreateThread创建的新线程并不被执行

既然MSDN上都说无法卸载托管dll,那这个问题就搁浅了吧。


虽然不能卸载这个dll,但并不是说就不能实现前面提到的问题。
实际上,每次主程序通过CreateRemoteThread函数在宿主创建线程时,DllMain中的DLL_THREAD_ATTACH和DLL_THREAD_DETACH也会收到通知,当然最好不要在这里直接调用CreateThread,这是因为:
1. 如果不做处理,直接在DLL_THREAD_ATTACH中调中CreateThread,显然会造成死循环:每产生一个线程,DLL_THREAD_ATTACH就会收到通知,然后又去创建线程,它又会得到通知……
2. 虽然多次单击按扭注入时,DLL_THREAD_ATTACH都会得到通知;但反过来,得到了通知,不意味着就是远程注入:一个最明显的情况是,对CreateThread的调用就会导致DLL_THREAD_ATTACH收到通知,但这并非远程注入

我的解决办法是:每次主程序调用CreateRemoteThread迫使宿主调用LoadLibrary加载dll的时候,都会使模块计数器加1,根据模块计数器的值的变化,就可以确定是否是远程注入了。

示例程序说明:

示例程序包括3个文件:
1. SuperSpy.exe 用C#写的主程序
2. Invoke.dll 用C++写的dll,也只能用C++来写
3. PropertyControl.dll 用VB.NET写的插件,它不是必需的
除了Invoke.dll必须用C++外,其余两个可以用任意语言写。

你也许会感到奇怪,为什么有两个dll?
其中PropertyControl.dll是一个插件,你完全可以把所有的代码都放到Invoke.dll中,之所以用插件的形式,只是为了方便大家编写自已的插件,插件的规范是,在任意一个类中实现以下成员(你可以用你习惯的语言来编写这个插件):
public static void Inject();
public static string Description
{
get;
}
两个成员都必须是公共、静态的,其中Inject方法是注入后要调用的方法;Description属性是用于描述插件作用的。

我自已实现的插件的功能是:查询和编辑一个托管程序中所有窗口(包括控件)的属性,这是一个很强大的功能,我举两个例子:
1. 星号密码查看。由于已经注入,本来显示为星号的密码,可以直接通过Text属性获得
2. 灰色按扭突破。只需将窗口的Enabled属性设置成true即可

以往要实现这两个功能,都需要分别编写相关的程序,而现在仅仅是这个插件的一个简单应用。而且,你可以编写自己的插件,来实现自己的要求。

致谢:

感谢CiCi提供了一个解决方案;感谢李马给予了我不少帮助。

联系:

如果你有什么问题,请来信到:ssyfzy@126.com


...全文
2272 155 打赏 收藏 转发到动态 举报
写回复
用AI写文章
155 条回复
切换为时间正序
请发表友善的回复…
发表回复
larissa523 2012-02-09
  • 打赏
  • 举报
回复
学习了。
yojinlin 2012-02-09
  • 打赏
  • 举报
回复
學習了。
dangmao 2012-02-01
  • 打赏
  • 举报
回复
mark之
mrsupersky 2011-07-21
  • 打赏
  • 举报
回复
[Quote=引用 144 楼 cstod 的回复:]
引用 141 楼 KKND2006 的回复:
你还是没搞清楚我在说什么

我说的是:不管DLL还是调用DLL来搞Remote Thread,整个事情用C#来做就显得很麻烦,束手束脚,所以做这种东西只是一种娱乐而已

类似于:如何用GB玩WII的游戏,发现GB的确可以启动WII的游戏,然后又碰到很多困难.那么我的观点就是:让GB玩GB的游戏,让WII玩WII的游戏;让GB玩WII的游戏只是……
[/Quote]

挖坟,我挺喜欢的,我觉得LZ真是太强大了,到今天才找到。。。这么好的文章、帖子、真是可惜啊,不过还好还好,

对于KKND2006的话,我只想说,你确实如LZ所说,没搞清楚概念,没错非托管DLL互注入是没什么,

但关键是难在将托管的DLL注入到非托管的DLL的程序中去,并且正常运行。。。

你会吗?

  • 打赏
  • 举报
回复
强悍
jin20000 2009-04-02
  • 打赏
  • 举报
回复
LZ解决问题的思路不错,
deepblue37 2009-03-30
  • 打赏
  • 举报
回复
楼主能提供下插件源码么? 有点抽象不太明白~~~
deepblue37 2009-03-29
  • 打赏
  • 举报
回复
不得不说,楼主真的很强大~~~我怎么就没想到呢~~~ 折服~~~
jaledge 2009-02-08
  • 打赏
  • 举报
回复
没下载了,可惜。
fantasyzc 2009-02-08
  • 打赏
  • 举报
回复
顶LZ的精神!鄙视只说没成果的!
flying_net 2009-02-08
  • 打赏
  • 举报
回复
学习
CsToD 2008-12-16
  • 打赏
  • 举报
回复
[Quote=引用 141 楼 KKND2006 的回复:]
你还是没搞清楚我在说什么

我说的是:不管DLL还是调用DLL来搞Remote Thread,整个事情用C#来做就显得很麻烦,束手束脚,所以做这种东西只是一种娱乐而已

类似于:如何用GB玩WII的游戏,发现GB的确可以启动WII的游戏,然后又碰到很多困难.那么我的观点就是:让GB玩GB的游戏,让WII玩WII的游戏;让GB玩WII的游戏只是一种KUSO的娱乐.
[/Quote]

怎么还是回帖不看贴呢?

你仔细看看我的原话:
你概念都没有弄清楚,你把“实现托管代码的注入”理解为了“用C#实现dll的注入”。
注意:我没有限定你的“手段”,你用什么语言都行;我强调的是“目的”,也就是能让宿主调用托管代码。
我反复给你强调,用什么语言都行,你还一个劲的说“整个事情用C#来做就显得很麻烦,束手束脚”,你觉得麻烦你可以用VC啊,我什么时候说过非要用C#了?!
我只是说:用”VC6.0“这种”手段“无法实现”注入托管代码“这种目的,你当然可以选择使用C#/VB.NET/VC.NET啊

你举的例子也极不恰当,关键的问题不是复不复杂的问题,是能不能解决的问题。

你提到《Windows核心编程》,这本书我当然看过。我使用的就是类似其中的一种方法,但问题是,照搬其方法是不行的?
DllMain不能直接或间接地调用托管代码,并且全局变量不会进行初始化。

你这个人不仅不思进取,还胡搅蛮缠,你仔细看看原文要把你累死啊?

如果是技术问题,我很乐意与之探讨;而这样一个各种语言孰优孰劣的问题,我没有兴趣。

对于这样一个根本不理会别人的回话,只一个人一个劲的在那发牢骚的人,我想我是不必回答你了,你好自为之吧。




deyter 2008-12-15
  • 打赏
  • 举报
回复
mark
再见品月 2008-12-15
  • 打赏
  • 举报
回复
[Quote=引用 138 楼 the2ndface 的回复:]
LZ很深很刻很强大
WO很迷很糊很茫然
[/Quote]

这位仁兄头像很搞。。。

顺便拜下LZ。。。太强咧。。。目前俺还看不懂
Wesley 2008-12-15
  • 打赏
  • 举报
回复
LZ很深很刻很强大
WO很迷很糊很茫然
CsToD 2008-12-15
  • 打赏
  • 举报
回复
[Quote=引用 110 楼 KKND2006 的回复:]
800年前就写过了,只是是VC写的,没想过用C#去干这种事情,我觉得C#不适合,束手束脚
对于插入别人的程序, <WINDOWS核心编程>里面提供了不止一种方法的,我记得起码是四种
另外现在的杀毒软件都禁止这种行为的,金山词霸也是用的Remote Thread,结果被Macfee给干掉了,结果不能取词,所以我发现dict.cn比金山词霸要好...

还有就是,VC做界面不是想象中那么麻烦的,MFC做界面也是拖拽式的,非常方便,纯控制界面的代码也不比C#多多少
程序写多了感觉C#写和C++写,代码量没有减少多少,当然0代码的CRUD除外...
[/Quote]

只能说明一个问题,那就是楼主回帖不看帖(因为我不相信有人看了后还会提出这样的问题)
你概念都没有弄清楚,你把“实现托管代码的注入”理解为了“用C#实现dll的注入”。
注意:我没有限定你的“手段”,你用什么语言都行;我强调的是“目的”,也就是能让宿主调用托管代码。

别的也不多说,你看看这句话就行了:
DllMain不能直接或间接地调用托管代码,并且全局变量不会进行初始化。

好了,现在该你给大家展示如何用VC6.0实现让宿主调用托管代码了。
ximi82878 2008-12-15
  • 打赏
  • 举报
回复
不错,谢谢分享,收藏了
KKND2006 2008-12-15
  • 打赏
  • 举报
回复
141楼引用出问题了哈

应该是引用

---------------------------
只能说明一个问题,那就是楼主回帖不看帖(因为我不相信有人看了后还会提出这样的问题)
你概念都没有弄清楚,你把“实现托管代码的注入”理解为了“用C#实现dll的注入”。
注意:我没有限定你的“手段”,你用什么语言都行;我强调的是“目的”,也就是能让宿主调用托管代码。

别的也不多说,你看看这句话就行了:
DllMain不能直接或间接地调用托管代码,并且全局变量不会进行初始化。

好了,现在该你给大家展示如何用VC6.0实现让宿主调用托管代码了。
-----------------------------
KKND2006 2008-12-15
  • 打赏
  • 举报
回复
[Quote=引用 137 楼 CsToD 的回复:]
引用 110 楼 KKND2006 的回复:
800年前就写过了,只是是VC写的,没想过用C#去干这种事情,我觉得C#不适合,束手束脚
对于插入别人的程序, <WINDOWS核心编程>里面提供了不止一种方法的,我记得起码是四种
另外现在的杀毒软件都禁止这种行为的,金山词霸也是用的Remote Thread,结果被Macfee给干掉了,结果不能取词,所以我发现dict.cn比金山词霸要好...

还有就是,VC做界面不是想象中那么麻烦的,MFC做界面也是拖拽式的,非常方便,纯…
[/Quote]


你还是没搞清楚我在说什么

我说的是:不管DLL还是调用DLL来搞Remote Thread,整个事情用C#来做就显得很麻烦,束手束脚,所以做这种东西只是一种娱乐而已

类似于:如何用GB玩WII的游戏,发现GB的确可以启动WII的游戏,然后又碰到很多困难.那么我的观点就是:让GB玩GB的游戏,让WII玩WII的游戏;让GB玩WII的游戏只是一种KUSO的娱乐.

如上`
aimeast 2008-12-12
  • 打赏
  • 举报
回复
终于看到这篇文章了,慢慢看看
加载更多回复(135)

110,534

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Web++
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧