由Hook引起的XP Explorer崩溃

有用户反应XP系统上一拖动桌面上的图标即会导致桌面重启,验证后,证实和360安全的升级有关,360安全9.0及后续版本和我们的软件有冲突。

360安全本身太激进,作为一款安全及防毒软件来说,激进并不是什么好事,这也是我不喜欢用360安全软件的原因之一。但是在事情未查出原因之前,不能随便怪罪360,毕竟在二者不共存时,均不会有桌面崩溃的事情发生。

先通过排除法,发现冲突发生在Hook上,而且是在特定的响应拖拽事件的API的Hook上:

    ole32.dll: DoDragDrop

问题定位之后照理应该很直接了,但分析新的DoDragDrop的实现及相关代码,并没有发现明显问题。

我们的代码在一个DLL文件中,并根据需要注入被Hook的进程,此案中便是Explorer.exe。调试起来稍有些周折,但也不算太麻烦。对此类的应用层调试问题,用VS (Visual Studio)远程调试最方便。远程attach到目标XP系统上的explorer.exe进程,然后触发崩溃事件,VS直接响应,显示问题出在一条很正常的压栈(push)操作上,再查看堆栈指针,果然已经耗尽,esp所指内存为非法地址(空页)。

初步揣测可能是堆栈不够所致,explorer.exe默认的size of stack reserve是0x400000,即4M字节。为了验证是不是堆栈的问题,直接将修改explorer.exe的PE文件头,将reserved stack size改成了64M(0x4000000)。

再测试时,explorer.exe在拖动桌面图标时还是会崩溃,只是会等上一会,不像之前一拖动便立即崩溃,看来问题并不是堆叠不够所致。

通过VS中断,发现Call stack中全是iNetSafe.dll (360安全的一个模块),由此断定出现了嵌套调用,死循环。

后面继续跟踪代码的调用过程发现了这样一个有趣的事,先将相关代码列出来:

ole32.dll地址空间 【774E0000 - 7761E000】:
_DoDragDrop@16:
775D0DC0 E9 0B DA 13 EE       jmp         6570E7D0
775D0DC5 83 EC 4C                sub         esp,4Ch
。。。。。。

iNetSafe.dll的地址空间 【65700000 - 65737000】:
6570E7D0 E9 AB 3A 1E 9B       jmp         CDragDropObj::NewDoDrapDrop (8F2280h) 
6570E7D5 57                           push        edi 
。。。。。。
6570E7F5 8B 4C 24 18          mov         ecx,dword ptr [esp+18h] 
6570E7F9 8B 54 24 14          mov         edx,dword ptr [esp+14h] 
6570E7FD 51                         push        ecx 
6570E7FE 8B 4C 24 14          mov         ecx,dword ptr [esp+14h] 
6570E802 52                   push        edx 
6570E803 51                   push        ecx 
6570E804 56                   push        esi 
6570E805 FF D0                call        eax                     [eax: 6FFE00A0]
6570E807 5F                   pop         edi 
6570E808 5E                   pop         esi 
6570E809 C2 10 00             ret         10h 
6570E80C 8B C7                mov         eax,edi 
6570E80E 5F                   pop         edi 
6570E80F 5E                   pop         esi 
6570E810 C2 10 00             ret         10h

360 iNetSafe.dll 所维护的Hook地址信息:
6FFE00A0 E9 DB 21 91 90       jmp         CDragDropObj::NewDoDrapDrop (8F2280h) 
6FFE00A5 E9 1B 0D 5F 07       jmp         _DoDragDrop@16+5 (775D0DC5h) 

MyHook32.dll地址空间 【008F0000 - 00C5F000】:
CDragDropObj::NewDoDrapDrop:
008F2280 55                   push        ebp 
008F2281 8B EC                mov         ebp,esp 
008F2283 8B 0D 38 70 C1 00    mov         ecx,dword ptr […::m_OldDoDragDrop 0C17038h] 
                                                                  0C17038h:0EC9002C  ECX = 0EC9002C 
008F2289 33 C0                xor         eax,eax 
008F228B 85 C9                test        ecx,ecx 
008F228D 74 03                je          CDragDropObj::NewDoDrapDrop+12h (8F2292h) 
008F228F 5D                   pop         ebp 
008F2290 FF E1                jmp         ecx                              [ecx = 0EC9002C]
008F2292 5D                   pop         ebp 
008F2293 C2 10 00             ret         10h 

MyHook32.dll: 原地址及代码
0EC9002C 56                         push        esi 
0EC9002D 8B 74 24 08          mov         esi,dword ptr [esp+8] 
0EC90031 E9 9F E7 A7 56      jmp         6570E7D5 

流程分析:

当Explorer调用_DoDragDrop()函数时,将跳转到iNetSafe的DoDragDrop实现,地址为6570E7D0。iNetSafe的DoDragDrop函数又被MyHook32 Hook了,所以还要继续跳至地址008F2280。MyHook32执行完还会跳转至被Hook前的_DoDragDrop函数,地址存放于0EC90032处,跳转地址为:6570E7D5,即iNetSafe的DoDragDrop函数入口加上5个字节的偏移。同理,iNetSafe的DoDragDrop函数也要调用它所Hook的原DoDragDrop地址,此地址的跳转指令存放于6FFE00A0处。但奇怪的是,位置6FFE00A0所存放的地址竟指向MyHoo32的
NewDoDrapDrop(即8F2280),所以任何对_DoDragDrop的调用,将会陷入一个嵌套的死循环里再也出不来了,直到堆栈被耗尽。

从上面代码上来看,iNetSafe在前,MyHook32在后,所以iNetSafe于6FFE00A0中所保存的原_DoDragDrop地址不应该是MyHook32的,而应该是ole32的_DoDragDrop才是。

但根据Explorer.exe进程的加载模块表,MyHook32.dll的加载order为40,而iNetSafe则为103,明显是MyHook32.dll先被加载,所以Hook的顺序明显违背上面的分析。

至此,方想到问题可能出在MyHook32的Anti-UnHook检查机制上。MyHook32在一个线程里面会实时检测被Hook的函数入口代码,如果不是MyHook32自己的,则会再次尝试Hook,因为360的Hook,便导致了MyHook32的再Hook,所以结果便导致了嵌套Hook的乌龙。

问题已经明确,并不是360的问题,而是出在我们自己的程序上,实在不应该先入为主的怪罪360,尽管其中还是有不少感情因素。

调试终于结束,剩下的的事情便是思考出一个万全的Anti-UnHook机制了。

下图为Explorer.exe所加载的模块(截屏自VS2010):

HookModules-01HookModules-02HookModules-03

注:有几个DLL的图标上均有红叹号,表示是此DLL被加载到的虚拟地址空间并不是此DLL所指定的地址空间。如MyHook32.dll(此版本为DEBUG版本,故体积及占用的地址范围也比较大)其默认的虚拟地址为10000000 - 1036EFFF,而实际上却被加载至008F0000 - 00C5F000。对于DLL文件的加载及从Vista之后所支持的ASLR(Address Space Layout Randomization),以后有机会再单独介绍。

谁动了我的环境变量

突然之间,我的Visual Studio 6.0不能编译任何程序了,总是提示如下错误:

Making help include file...
Compiling resources...
Compiling...
Command line error D2004 : '/Zm' requires an argument
Error executing cl.exe.

就在约1个小时前还是可以的,而且这一个小时内我一直在看代码,并没有安装或卸载过任何软件。

查看plg文件,也没有什么异常。找到错误代码D2004相关的介绍,然后添加/Zm100选项,但错误照旧。

将整个Visual Studio目录copy到另一台Server 2008系统上,竟然是好的。也就是说问题并不是VS6本身,可能是系统环境或动态库的问题。

目前的系统之前已休眠过多次,上次重启约在一周前,还是因为无线网络没有反应才重启的,但在此之后直到现在一切都工作良好。第一反应就是该重启系统了,但在重启之前还想再调查调查,毕竟重启或许能消除问题,但并不能真正解决它。

即然VS6 GUI环境不行,那就尝试下makefile。结果makefile方式编译成功,意料之外!

就在进行makefile的cmd窗口中,查看了下环境变量,发现环境变量竞然是ifskit 2003的编译环境。重新打开了一个cmd窗口,结果还是ifskit 2003的环境,相当诡异。

我之前编译ext2fsd以创建browser文件时用过ifskit 2003,这个窗口到还是是打开的,并没有关闭。但它怎么可能会成为系统默认环境的呢?误操作?我并没有更改过环境变量!难道是系统出错?

百思不得其解之际,想起来之前explorer崩溃过一次,我只得重新加载了explorer.exe进程。随即打开procexp查看explorer.exe进程的Environment:

process_env

问题原来在这里!explorer崩溃时我一般会在taskmgr里重新加载explorer.exe,有时也会在cmd窗口里。而这次,却是在ifskit 2003的编译环境里加载的,结果此环境就被explorer.exe作为子进程继承过去了。然后Visaul Stuido 6.0作为explorer的子进程也继承了同样的环境,结果编译时用错了的编译器。

先验证一下,CTRL-ALT-DEL调出taskmgr,然后加载了一个新的cmd.exe,查看环境变量,确定是正确的,不再是ifskit 2003的编译环境了。然后以新的环境重新启动Vistal Stdio 6.0,尝试编译程序,编译成功,至此问题解决!