现在有很多比 Windows 资源管理器功能强大的资源管理程序,如 Total Commander、xplorer2 和 Ultra Explorer 等等。于是乎,我们就会有了一个新的需求——在使用系统热键 Win + E 的时候能呼出我们自定义的资源管理程序,而不是默认的 Windows 资源管理器。但是,我一直没有找到一个合适的辅助工具来实现这个功能,网上大多的热键工具不是无法替换默认的 Win + E,就是不够完美——例如键入后仍会弹出资源管理器,或弹出开始菜单,甚至有时候会导致 explorer.exe 崩溃退出。
于是我决定自己实现这一功能,最初我选择了键盘钩子,但我发现钩子虽然能捕获到这个热键,但是并不能对这个热键的处理进行重新定向,除我的程序之外,系统仍然会收到这个消息。当然,这样就可以解释为什么大多能够拦截 Win + E 的热键工具不够完美的原因了。
于是我决定使用更猥琐的方法,从 explorer.exe 的内部开始探究。
我的切入点是弹出的资源管理器窗口,既然这个窗口是新建的,那么必然会经过 CreateWindow,于是我选择了 NtUserCreateWindowEx 作为我的断点:
1 | bp USER32!NtUserCreateWindowEx |
接下来按下 Win + E,断点如约命中。检查其堆栈,发现:
1 2 3 4 5 6 7 8 9 10 11 12 | ChildEBP RetAddr 01c1f3cc 77d2e442 USER32!NtUserCreateWindowEx 01c1f478 77d2d0d6 USER32!_CreateWindowEx+0x1ed 01c1f4b4 77f596af USER32!CreateWindowExW+0x33 01c1f51c 7d6984e2 SHLWAPI!SHCreateWorkerWindowW+0xac 01c1f53c 7d698ea0 SHELL32!CShellExecute::_CreateHiddenDDEWindow+0x1e 01c1f560 7d61d9fc SHELL32!CShellExecute::_DDEExecute+0x79 01c1f57c 7d5d9fea SHELL32!CShellExecute::_TryExecDDE+0x2d 01c1f588 7d5d9b04 SHELL32!CShellExecute::_TryInvokeApplication+0x29 01c1f59c 7d5d9a36 SHELL32!CShellExecute::ExecuteNormal+0xb1 01c1f5b0 7d5d99d2 SHELL32!ShellExecuteNormal+0x30 01c1f5cc 7d5e619b SHELL32!ShellExecuteExW+0x8d |
这就是说,资源管理器窗口是通过 ShellExecuteEx 启动的。现在清除原有断点,重新在 ShellExecuteExW 处下断点,并再次按下 Win + E。断点命中后,堆栈信息如下:
1 2 3 4 5 6 | ChildEBP RetAddr 01c1fccc 0103ae0f SHELL32!ShellExecuteExW 01c1fd14 01021599 Explorer!_ShowFolder+0x62 01c1fd30 01021691 Explorer!CTray::_HandleGlobalHotkey+0xb4 01c1fde8 01001b5c Explorer!CTray::v_WndProc+0xb6d 01c1fe0c 77d18734 Explorer!CImpWndProc::s_WndProc+0x65 |
这是一个典型的窗口过程流程,于是检查 CImpWndProc::s_WndProc 的各个参数,如下图所示。

于是,我的想法初步形成:子类化 Shell_TrayWnd 这个窗口,处理其 WM_HOTKEY 消息即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #define ID_EXPLORER 504 LRESULT WINAPI TrayWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { if (WM_HOTKEY == uMsg) { if (ID_EXPLORER == wParam) { MessageBox(NULL, _T("Hello, World!"), _T("Hello"), MB_TOPMOST); return 0; } } // … return CallWindowProc(g_pfnTrayWnd, hWnd, uMsg, wParam, lParam); } |
但是,实际操作起来却没有成功,原因是我使用 SetWindowLong 替换 Shell_TrayWnd 的窗口过程后,原有的窗口过程又会被瞬间替换回去。也许这是 explorer.exe 做的特殊保护处理?继续调查下去是一件很不靠谱的事情,于是我选择了曲线救国——不改动窗口过程,而是直接将窗口过程做内联挂钩(inline hook)处理。最终的挂钩代码如下,使用了 Detours。
1 2 3 4 5 6 7 8 | HWND hWnd = FindWindow(_T("Shell_TrayWnd"), NULL); if (NULL != hWnd) { g_pfnTrayWnd = (WNDPROC)GetWindowLong(hWnd, GWL_WNDPROC); DetourTransactionBegin(); DetourAttach((PVOID*)&g_pfnTrayWnd, TrayWndProc); DetourTransactionCommit(); } |
由于挂钩后的 TrayWndProc 必须位于 explorer.exe 的地址空间之中,因此我将完整的示例代码写在了一个 DLL 之中。测试的时候,只需要设法(比如远程注入)让 explorer.exe 加载该 DLL 就可以看到效果了。
当然,这还没有结束。接下来的问题是——如何让 explorer.exe 自动加载这个 DLL?方法很多,这里就不详述了,诸君可任选其一:
- 利用注册表的 AppInit_DLLs 键;
- 将 DLL 写成 BHO;
- 将 DLL 写成外壳扩展(Shell Extension)。
示例代码见附件。
附件:winkey.zip
好,顶个
李马大侠,万一开始时是在不知道该在哪个函数处设断点,该怎么调试呢?比如打开IE窗口 在NtUserCreateWindowEx处设断点就没啥用啊
具体情况具体分析。另外,我不太清楚你要挂什么钩子,又和 NtUserCreateWindowEx 有什么关系。
我想截获每次打开IE窗口的事件,该挂接哪个函数啊
试试 BHO。
貌似都被人给遗忘了哟….
还是用 autohotkey 简单
#e::
run, D:\totalcmd\TOTALCMD.EXE
return
run, 后边设置想要启动的资源管理器程序路径即可。
我这里讨论的并不是“如何替换 Win + E 键”,而是“如何更好的替换 Win + E 键”。实际动手试一下就会发现,键盘钩子并不是一个很好的方案。
创建远程线程注射到explorer.exe,子类化Shell_TrayWnd,
拦截WM_HOTKEY即可成功。不需要你说的这么复杂。
在我写的clock++力已经实现
http://hi.baidu.com/cdutboy/blog/item/3440813973f53528b9998f78.html
case WM_HOTKEY:
{
//拦截win+E
if( MOD_WIN==LOWORD(lParam) && (‘E’==HIWORD(lParam)) )
{
//API_MessageBox(hWnd,L”拦截win+E”,L”提示”,MB_OK);
return TRUE; //拦截了
}
break;
}
也许你并没仔细看我这篇文章?另,我并不认为远线程是一个好的解决方案。