Ответ 1
EDIT: https://github.com/somanuell/SoBrowserAction
Вот скриншот моей работы.
То, что я сделал:
1. Выход из защищенного режима
Регистрация BHO должна обновить ключ HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\Low Rights\ElevationPolicy
. См. Понимание и работа в защищенном режиме Internet Explorer.
Я выбираю процесс, потому что он отмечен как "лучшая практика" и его легче отлаживать, но RunDll32Policy
тоже может сделать трюк.
Найдите файл rgs
, содержащий ваши настройки реестра BHO. Это тот, который содержит upadte для ключа реестра '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%
НЕ является предопределенным.
Зачем использовать макрос? Вы можете избежать этого нового кода в вашем файле RGS, при условии, что ваш MSI содержит это обновление в реестре. Поскольку работа с MSI может быть болезненной, часто проще предоставить пакет "полной самостоятельной регистрации". Но если вы не используете макрос, вы не можете разрешить пользователю выбирать каталог установки. Использование макроса позволяет динамически обновлять реестр с правильным каталогом установки.
Как заставить макрос работать?
Найдите макрос DECLARE_REGISTRY_RESOURCEID
в заголовке вашего класса BHO и прокомментируйте его. Добавьте следующее определение функции в этот заголовок:
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);
}
Этот код заимствован из реализации ATL для DECLARE_REGISTRY_RESOURCEID
(в моем случае это тот, который поставляется с VS2010, при необходимости проверяет вашу версию ATL и код обновления). Макрос IDR_CSOBABHO
- это идентификатор ресурса ресурса REGISTRY
, добавляющий RGS в ваш файл RC.
Переменная 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);
}
Большое спасибо Младену Янковичу.
Как облегчить процесс брокера?
Одно возможное место в реализации SetSite
. Это будет много раз, но мы будем иметь дело с этим в самом процессе. Мы увидим позже, что брокерский процесс может выиграть от получения в качестве аргумента HWND для хостинга IEFrame. Это можно сделать с помощью метода 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, если он еще не обработан предыдущим экземпляром.
Скорее, "инжектор" может быть долговечным, в конечном счете, бесконечным процессом, который должен будет постоянно наблюдать за Рабочем столом, обрабатывая новые IEFrames по мере их появления. Единственность процесса может быть гарантирована именованным мьютексом.
В настоящее время я попытаюсь перейти к принципу KISS (Keep It Simple, Stupid). То есть: короткоживущий инжектор. Я точно знаю, что это приведет к специальной обработке в BHO для случая Tab Drag And Drop'ed на рабочий стол, но я увижу это позже.
Переход на этот маршрут включает в себя инъекцию Dll, которая выживает в конце инжектора, но я передам ее самой Dll.
Вот код для процесса инжектора. Он устанавливает привязку WH_CALLWNDPROCRET
для потока, на котором размещается IEFrame, используйте SendMessage
(с определенным зарегистрированным сообщением), чтобы немедленно запустить инъекцию Dll, а затем удаляет крючок и завершает работу. BHO Dll должен экспортировать обратный вызов CallWndRetProc
с именем HookCallWndProcRet
. Пути ошибок опущены.
#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. Выживающая инъекция: "зацепите меня сильнее"
Временную загрузку Dll в основном IE-процессе достаточно, чтобы добавить новую кнопку на Панели инструментов. Но возможность отслеживать WM_COMMAND
для этой новой кнопки требует больше: постоянно загружаемая Dll и крюк все еще на месте, несмотря на завершение процесса подключения. Простое решение - снова подключить поток, передав дескриптор экземпляра Dll.
Как только каждое открытие вкладки приведет к созданию нового BHO-экземпляра, таким образом, новый процесс инжектора, функция hook должна иметь способ узнать, действительно ли текущий поток уже подключен (я не хочу просто добавлять крючок для каждого вкладка, которая не очищается)
Локальное хранилище потоков - это способ:
- Выделите индекс TLS в
DllMain
, дляDLL_PROCESS_ATTACH
. - Сохраните новый
HHOOK
как данные TLS и используйте это, чтобы узнать, нить уже подключена. - Отключить при необходимости, когда
DLL_THREAD_DETACH
- Освободите индекс TLS в
DLL_PROCESS_DETACH
Это приводит к следующему коду:
// 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.
См. мой другой ответ на вопрос, поскольку в настоящее время я не могу редактировать ответ с использованием формата кода.