编辑:https://github.com/somanuell/SoBrowserAction
这是我正在进行的工作的屏幕截图。
我做过的事情:
1.退出保护模式
BHO 注册必须更新HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\Low Rights\ElevationPolicy 密钥。 See Understanding and Working in Protected Mode Internet Explorer.
我选择过程方式是因为它被称为“最佳实践”并且更易于调试,但RunDll32Policy 也可以解决问题。
找到包含您的 BHO 注册表设置的 rgs 文件。它包含对注册表项'Browser Helper Object' 的更新。在该文件中添加以下内容:
HKLM {
NoRemove SOFTWARE {
NoRemove Microsoft {
NoRemove 'Internet Explorer' {
NoRemove 'Low Rights' {
NoRemove ElevationPolicy {
ForceRemove '{AE6E5BFE-B965-41B5-AC70-D7069E555C76}' {
val AppName = s 'SoBrowserActionInjector.exe'
val AppPath = s '%MODULEPATH%'
val Policy = d '3'
}
}
}
}
}
}
}
GUID 必须是新的,不要使用我的,使用 GUID 生成器。
策略的3 值确保代理进程将作为中等完整性进程启动。 %MODULEPATH%macro 不是预定义的。
为什么要使用宏?
您可以避免在 RGS 文件中使用新代码,前提是您的 MSI 包含对注册表的更新。由于处理 MSI 可能会很痛苦,因此提供“完全自注册”包通常更容易。但是如果你不使用宏,你就不能让用户选择安装目录。使用宏允许使用正确的安装目录动态更新注册表。
如何使宏工作?
在 BHO 类的标题中找到 DECLARE_REGISTRY_RESOURCEID 宏并将其注释掉。在该标头中添加以下函数定义:
static HRESULT WINAPI UpdateRegistry( BOOL bRegister ) throw() {
ATL::_ATL_REGMAP_ENTRY regMapEntries[2];
memset( ®MapEntries[1], 0, sizeof(ATL::_ATL_REGMAP_ENTRY));
regMapEntries[0].szKey = L"MODULEPATH";
regMapEntries[0].szData = sm_szModulePath;
return ATL::_pAtlModule->UpdateRegistryFromResource(IDR_CSOBABHO, bRegister,
regMapEntries);
}
该代码是从DECLARE_REGISTRY_RESOURCEID 的 ATL 实现中借用的(在我的情况下,它是 VS2010 附带的代码,请检查您的 ATL 版本并在必要时更新代码)。 IDR_CSOBABHO 宏是在 RC 文件中添加 RGS 的 REGISTRY 资源的资源 ID。
sm_szModulePath 变量必须包含代理进程 EXE 的安装路径。我选择使它成为我的 BHO 类的公共静态成员变量。一种简单的设置方法是在DllMain 函数中。当regsvr32 加载你的Dll 时,DllMain 被调用,并且注册表被更新为正确的路径。
extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) {
if ( dwReason == DLL_PROCESS_ATTACH ) {
DWORD dwCopied = GetModuleFileName( hInstance,
CCSoBABHO::sm_szModulePath,
sizeof( CCSoBABHO::sm_szModulePath ) /
sizeof( wchar_t ) );
if ( dwCopied ) {
wchar_t * pLastAntiSlash = wcsrchr( CCSoBABHO::sm_szModulePath, L'\\' );
if ( pLastAntiSlash ) *( pLastAntiSlash ) = 0;
}
}
return _AtlModule.DllMain(dwReason, lpReserved);
}
非常感谢 Mladen Janković。
如何启动 Broker 流程?
一个可能的位置是在SetSite 实现中。它将被启动多次,但我们将在过程本身中处理它。稍后我们将看到代理进程可能会受益于接收托管 IEFrame 的 HWND 作为参数。这可以通过IWebBrowser2::get_HWND 方法完成。我想在这里你已经有一个IWebBrowser2* 成员。
STDMETHODIMP CCSoBABHO::SetSite( IUnknown* pUnkSite ) {
if ( pUnkSite ) {
HRESULT hr = pUnkSite->QueryInterface( IID_IWebBrowser2, (void**)&m_spIWebBrowser2 );
if ( SUCCEEDED( hr ) && m_spIWebBrowser2 ) {
SHANDLE_PTR hWndIEFrame;
hr = m_spIWebBrowser2->get_HWND( &hWndIEFrame );
if ( SUCCEEDED( hr ) ) {
wchar_t szExeName[] = L"SoBrowserActionInjector.exe";
wchar_t szFullPath[ MAX_PATH ];
wcscpy_s( szFullPath, sm_szModulePath );
wcscat_s( szFullPath, L"\\" );
wcscat_s( szFullPath, szExeName );
STARTUPINFO si;
memset( &si, 0, sizeof( si ) );
si.cb = sizeof( si );
PROCESS_INFORMATION pi;
wchar_t szCommandLine[ 64 ];
swprintf_s( szCommandLine, L"%.48s %d", szExeName, (int)hWndIEFrame );
BOOL bWin32Success = CreateProcess( szFullPath, szCommandLine, NULL,
NULL, FALSE, 0, NULL, NULL, &si, &pi );
if ( bWin32Success ) {
CloseHandle( pi.hThread );
CloseHandle( pi.hProcess );
}
}
}
[...]
2。注入 IEFrame 线程
看来这可能是最复杂的部分,因为有很多方法可以做到,每一种都有利弊。
代理进程,即“注入器”,可能是一个短暂的进程,只有一个简单的参数(HWND 或 TID),如果之前的实例尚未处理,它必须处理唯一的 IEFrame。
相反,“注入器”可能是一个长期存在的、最终永无止境的进程,它必须不断地观察桌面,在新的 IEFrame 出现时对其进行处理。命名互斥体可以保证进程的唯一性。
目前,我将尝试遵循 KISS 原则(保持简单,愚蠢)。那就是:一个短命的注射器。我确信这将导致在 BHO 中针对将选项卡拖放到桌面的情况进行特殊处理,但我稍后会看到。
走这条路涉及到在注入器结束后仍然存在的 Dll 注入,但我将把它委托给 Dll 本身。
这是注入器进程的代码。它为承载 IEFrame 的线程安装WH_CALLWNDPROCRET 钩子,使用SendMessage(带有特定的注册消息)立即触发 Dll 注入,然后移除钩子并终止。 BHO Dll 必须导出一个名为 HookCallWndProcRet 的 CallWndRetProc 回调。错误路径被省略。
#include <Windows.h>
#include <stdlib.h>
typedef LRESULT (CALLBACK *PHOOKCALLWNDPROCRET)( int nCode, WPARAM wParam, LPARAM lParam );
PHOOKCALLWNDPROCRET g_pHookCallWndProcRet;
HMODULE g_hDll;
UINT g_uiRegisteredMsg;
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE, char * pszCommandLine, int ) {
HWND hWndIEFrame = (HWND)atoi( pszCommandLine );
wchar_t szFullPath[ MAX_PATH ];
DWORD dwCopied = GetModuleFileName( NULL, szFullPath,
sizeof( szFullPath ) / sizeof( wchar_t ) );
if ( dwCopied ) {
wchar_t * pLastAntiSlash = wcsrchr( szFullPath, L'\\' );
if ( pLastAntiSlash ) *( pLastAntiSlash + 1 ) = 0;
wcscat_s( szFullPath, L"SoBrowserActionBHO.dll" );
g_hDll = LoadLibrary( szFullPath );
if ( g_hDll ) {
g_pHookCallWndProcRet = (PHOOKCALLWNDPROCRET)GetProcAddress( g_hDll,
"HookCallWndProcRet" );
if ( g_pHookCallWndProcRet ) {
g_uiRegisteredMsg = RegisterWindowMessage( L"SOBA_MSG" );
if ( g_uiRegisteredMsg ) {
DWORD dwTID = GetWindowThreadProcessId( hWndIEFrame, NULL );
if ( dwTID ) {
HHOOK hHook = SetWindowsHookEx( WH_CALLWNDPROCRET,
g_pHookCallWndProcRet,
g_hDll, dwTID );
if ( hHook ) {
SendMessage( hWndIEFrame, g_uiRegisteredMsg, 0, 0 );
UnhookWindowsHookEx( hHook );
}
}
}
}
}
}
if ( g_hDll ) FreeLibrary( g_hDll );
return 0;
}
3.幸存者注射:“更努力地勾住我”
在 IE 主进程中临时加载 Dll 足以在 Toolbar 中添加一个新按钮。但是要能够监控WM_COMMAND 的那个新按钮需要更多:一个永久加载的 Dll 和一个挂钩,尽管挂钩过程结束了。一个简单的解决方案是再次挂钩线程,传递 Dll 实例句柄。
由于每个选项卡打开都会导致一个新的 BHO 实例化,因此是一个新的注入器进程,钩子函数必须有办法知道当前线程是否已经被钩子(我不想只为每个标签打开,不干净)
线程本地存储是要走的路:
- 在
DllMain 中为DLL_PROCESS_ATTACH 分配一个TLS 索引。
- 将新的
HHOOK 存储为 TLS 数据,并使用它来了解
线程已经上钩了
- 如有必要,当
DLL_THREAD_DETACH
- 释放
DLL_PROCESS_DETACH中的TLS索引
这导致以下代码:
// DllMain
// -------
if ( dwReason == DLL_PROCESS_ATTACH ) {
CCSoBABHO::sm_dwTlsIndex = TlsAlloc();
[...]
} else if ( dwReason == DLL_THREAD_DETACH ) {
CCSoBABHO::UnhookIfHooked();
} else if ( dwReason == DLL_PROCESS_DETACH ) {
CCSoBABHO::UnhookIfHooked();
if ( CCSoBABHO::sm_dwTlsIndex != TLS_OUT_OF_INDEXES )
TlsFree( CCSoBABHO::sm_dwTlsIndex );
}
// BHO Class Static functions
// --------------------------
void CCSoBABHO::HookIfNotHooked( void ) {
if ( sm_dwTlsIndex == TLS_OUT_OF_INDEXES ) return;
HHOOK hHook = reinterpret_cast<HHOOK>( TlsGetValue( sm_dwTlsIndex ) );
if ( hHook ) return;
hHook = SetWindowsHookEx( WH_CALLWNDPROCRET, HookCallWndProcRet,
sm_hModule, GetCurrentThreadId() );
TlsSetValue( sm_dwTlsIndex, hHook );
return;
}
void CCSoBABHO::UnhookIfHooked( void ) {
if ( sm_dwTlsIndex == TLS_OUT_OF_INDEXES ) return;
HHOOK hHook = reinterpret_cast<HHOOK>( TlsGetValue( sm_dwTlsIndex ) );
if ( UnhookWindowsHookEx( hHook ) ) TlsSetValue( sm_dwTlsIndex, 0 );
}
我们现在有了一个几乎完整的钩子函数:
LRESULT CALLBACK CCSoBABHO::HookCallWndProcRet( int nCode, WPARAM wParam,
LPARAM lParam ) {
if ( nCode == HC_ACTION ) {
if ( sm_uiRegisteredMsg == 0 )
sm_uiRegisteredMsg = RegisterWindowMessage( L"SOBA_MSG" );
if ( sm_uiRegisteredMsg ) {
PCWPRETSTRUCT pcwprets = reinterpret_cast<PCWPRETSTRUCT>( lParam );
if ( pcwprets && ( pcwprets->message == sm_uiRegisteredMsg ) ) {
HookIfNotHooked();
HWND hWndTB = FindThreadToolBarForIE9( pcwprets->hwnd );
if ( hWndTB ) {
AddBrowserActionForIE9( pcwprets->hwnd, hWndTB );
}
}
}
}
return CallNextHookEx( 0, nCode, wParam, lParam);
}
AddBrowserActionForIE9 的代码稍后会编辑。
对于 IE9,获取 TB 非常简单:
HWND FindThreadToolBarForIE9( HWND hWndIEFrame ) {
HWND hWndWorker = FindWindowEx( hWndIEFrame, NULL,
L"WorkerW", NULL );
if ( hWndWorker ) {
HWND hWndRebar= FindWindowEx( hWndWorker, NULL,
L"ReBarWindow32", NULL );
if ( hWndRebar ) {
HWND hWndBand = FindWindowEx( hWndRebar, NULL,
L"ControlBandClass", NULL );
if ( hWndBand ) {
return FindWindowEx( hWndBand, NULL,
L"ToolbarWindow32", NULL );
}
}
}
return 0;
}
4.处理工具栏
这部分可能会大大改进:
- 我刚刚创建了一个黑白位图,一切都很好,即:透明的黑色像素。每次我尝试添加一些颜色和/或灰度级时,结果都很糟糕。对于那些“工具栏中的位图魔法”,我一点也不流利
- 位图的大小应取决于工具栏中已有的其他位图的当前大小。我只使用了两张位图(一张“普通”,一张“大”)
- 可以优化强制IE“重绘”工具栏新状态的部分,地址栏的宽度更小。它有效,有一个涉及整个 IE 主窗口的快速“重绘”阶段。
请参阅我对该问题的其他答案,因为我目前无法在代码格式正常工作的情况下编辑答案。