ramensoftware / windhawk

The customization marketplace for Windows programs: https://windhawk.net/
https://windhawk.net
GNU General Public License v3.0
1.07k stars 28 forks source link

Late injection with Control Panel #103

Closed aubymori closed 9 months ago

aubymori commented 9 months ago

The first time launching Control Panel after Explorer has started, it will sometimes launch before mods have initialized.

Demonstration

Module: ExplorerFrame.dll Hooked functions: CAddressBand::GetBandInfo, CUniversalSearchBand::GetBandInfo

Initial launch (Windhawk): image The address bar size which the mod is supposed to affect is unaffected.

After updating address bar (clickng on and off of it): image The address bar size is now updated, which shows that the mod was in fact initialized, just after the initial call of the functions hooked.

Initial launch (cold-patched DLL): image The address bar size is adjusted by default.

Initial launch (partially functional): image CAddressBand::GetBandInfo was hooked before Control Panel opened, but CUniversalSearchBand::GetBandInfo was not.

aubymori commented 9 months ago

I should provide the mod in question. Here:

The mod (really long!) ```cpp // ==WindhawkMod== // @id explorer-frame-tweaks // @name Explorer Frame Tweaks // @description Tweaks to the File Explorer frame. // @version 1.0.0 // @author aubymori // @github https://github.com/aubymori // @include explorer.exe // @include notepad.exe // @compilerOptions -lgdi32 // ==/WindhawkMod== // ==WindhawkModReadme== /* # Explorer Frame Tweaks */ // ==/WindhawkModReadme== // ==WindhawkModSettings== /* - shrinkAddress: true $name: Shrink address bar $description: Shrink the address and search bars to the size they had before Windows 10 19H15 - nonModernSearch: true $name: Disable modern search $description: Reverts the search to the one from before Windows 10 19H1 - noUpButton: true $name: Hide "Up" button $description: Hides the "Up" button, like Windows 7 */ // ==/WindhawkModSettings== #ifdef _WIN64 #define sWINAPI L"__cdecl" #else #define sWINAPI L"__stdcall" #endif #include struct { BOOL bShrinkAddress; BOOL bNonModernSearch; BOOL bNoUpButton; } settings; #pragma region "From shobjidl_core.h" typedef enum _SIGDN { SIGDN_NORMALDISPLAY = 0, SIGDN_PARENTRELATIVEPARSING = (int)0x80018001, SIGDN_DESKTOPABSOLUTEPARSING = (int)0x80028000, SIGDN_PARENTRELATIVEEDITING = (int)0x80031001, SIGDN_DESKTOPABSOLUTEEDITING = (int)0x8004c000, SIGDN_FILESYSPATH = (int)0x80058000, SIGDN_URL = (int)0x80068000, SIGDN_PARENTRELATIVEFORADDRESSBAR = (int)0x8007c001, SIGDN_PARENTRELATIVE = (int)0x80080001, SIGDN_PARENTRELATIVEFORUI = (int)0x80094001 } SIGDN; typedef ULONG SFGAOF; typedef DWORD SICHINTF; MIDL_INTERFACE("43826d1e-e718-42ee-bc55-a1e261c37bfe") IShellItem : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE BindToHandler( /* [unique][in] */ __RPC__in_opt IBindCtx *pbc, /* [in] */ __RPC__in REFGUID bhid, /* [in] */ __RPC__in REFIID riid, /* [iid_is][out] */ __RPC__deref_out_opt void **ppv) = 0; virtual HRESULT STDMETHODCALLTYPE GetParent( /* [out] */ __RPC__deref_out_opt IShellItem **ppsi) = 0; virtual HRESULT STDMETHODCALLTYPE GetDisplayName( /* [in] */ SIGDN sigdnName, /* [annotation][string][out] */ _Outptr_result_nullonfailure_ LPWSTR *ppszName) = 0; virtual HRESULT STDMETHODCALLTYPE GetAttributes( /* [in] */ SFGAOF sfgaoMask, /* [out] */ __RPC__out SFGAOF *psfgaoAttribs) = 0; virtual HRESULT STDMETHODCALLTYPE Compare( /* [in] */ __RPC__in_opt IShellItem *psi, /* [in] */ SICHINTF hint, /* [out] */ __RPC__out int *piOrder) = 0; }; #pragma endregion // "From shobjidl_core.h" #pragma region "IDeskBand::GetBandInfo hooks" /* https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/ns-shobjidl_core-deskbandinfo */ typedef struct DESKBANDINFO { DWORD dwMask; POINTL ptMinSize; POINTL ptMaxSize; POINTL ptIntegral; POINTL ptActual; WCHAR wszTitle[256]; DWORD dwModeFlags; COLORREF crBkgnd; } DESKBANDINFO; /* https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ideskband-getbandinfo */ typedef long (* IDeskBand_GetBandInfo_t)(void *, DWORD, DWORD, DESKBANDINFO *); long IDeskBand_GetBandInfo( void *pThis, DWORD dwBandID, DWORD dwViewMode, DESKBANDINFO *pdbi, IDeskBand_GetBandInfo_t orig, LONG height = 31 ) { long res = orig( pThis, dwBandID, dwViewMode, pdbi ); HDC hDC = GetDC(NULL); int nDpi = GetDeviceCaps(hDC, LOGPIXELSX); ReleaseDC(NULL, hDC); height = MulDiv(height, nDpi, 96); pdbi->ptActual.y = height; pdbi->ptMinSize.y = height; pdbi->ptMaxSize.y = height; return res; } IDeskBand_GetBandInfo_t CAddressBand_GetBandInfo_orig; long CAddressBand_GetBandInfo_hook( void *pThis, DWORD dwBandID, DWORD dwViewMode, DESKBANDINFO *pdbi ) { return IDeskBand_GetBandInfo( pThis, dwBandID, dwViewMode, pdbi, CAddressBand_GetBandInfo_orig ); } IDeskBand_GetBandInfo_t CUniversalSearchBand_GetBandInfo_orig; long CUniversalSearchBand_GetBandInfo_hook( void *pThis, DWORD dwBandID, DWORD dwViewMode, DESKBANDINFO *pdbi ) { return IDeskBand_GetBandInfo( pThis, dwBandID, dwViewMode, pdbi, CUniversalSearchBand_GetBandInfo_orig, 28 ); } #pragma endregion // "IDeskBand::GetBandInfo hooks" typedef bool (* IsControlPanel_t)(IShellItem *); IsControlPanel_t IsControlPanel_orig; bool IsControlPanel_hook( IShellItem* item ) { return true; } typedef bool (* IsControlPanelProcess_t)(void); IsControlPanelProcess_t IsControlPanelProcess_orig; bool IsControlPanelProcess_hook(void) { return true; } typedef bool (* CUniversalSearchBand_IsModernSearchBoxEnabled_t)(void); CUniversalSearchBand_IsModernSearchBoxEnabled_t CUniversalSearchBand_IsModernSearchBoxEnabled_orig; bool CUniversalSearchBand_IsModernSearchBoxEnabled_hook(void) { return false; } typedef void (* CUpBand__CreateUpButtons_t)(void); CUpBand__CreateUpButtons_t CUpBand__CreateUpButtons_orig; void CUpBand__CreateUpButtons_hook(void) { return; } void LoadSettings(void) { settings.bShrinkAddress = Wh_GetIntSetting(L"shrinkAddress"); settings.bNonModernSearch = Wh_GetIntSetting(L"nonModernSearch"); settings.bNoUpButton = Wh_GetIntSetting(L"noUpButton"); } BOOL Wh_ModInit(void) { LoadSettings(); HMODULE hExplorerFrame = LoadLibraryW(L"ExplorerFrame.dll"); if (!hExplorerFrame) { Wh_Log(L"Failed to load ExplorerFrame.dll"); return FALSE; } #if 0 WH_FIND_SYMBOL symbol; HANDLE search = Wh_FindFirstSymbol(hExplorerFrame, NULL, &symbol); do { Wh_Log(L"%s", symbol.symbol); } while (Wh_FindNextSymbol(search, &symbol)); return TRUE; #endif if (settings.bShrinkAddress) { WindhawkUtils::SYMBOL_HOOK hooks[] = { { { L"public: virtual long " sWINAPI L" CAddressBand::GetBandInfo(unsigned long,unsigned long,struct DESKBANDINFO *)" }, (void **)&CAddressBand_GetBandInfo_orig, (void *)CAddressBand_GetBandInfo_hook, false }, { { L"public: virtual long " sWINAPI L" CUniversalSearchBand::GetBandInfo(unsigned long,unsigned long,struct DESKBANDINFO *)" }, (void **)&CUniversalSearchBand_GetBandInfo_orig, (void *)CUniversalSearchBand_GetBandInfo_hook, false }, { { /* L"bool " sWINAPI, L" IsControlPanel(struct IShellItem *)" */ L"bool __cdecl IsControlPanel(struct IShellItem *)" }, (void **)&IsControlPanel_orig, (void *)IsControlPanel_hook, false }, { { /* L"bool " sWINAPI, L" IsControlPanelProcess(void)" */ L"bool __cdecl IsControlPanelProcess(void)" }, (void **)&IsControlPanelProcess_orig, (void *)IsControlPanelProcess_hook, false } }; if (!HookSymbols(hExplorerFrame, hooks, ARRAYSIZE(hooks))) { Wh_Log(L"Failed to hook one or more GetBandInfo functions"); return FALSE; } } if (settings.bNonModernSearch) { WindhawkUtils::SYMBOL_HOOK hook = { { L"private: bool " sWINAPI L" CUniversalSearchBand::IsModernSearchBoxEnabled(void)" }, (void **)&CUniversalSearchBand_IsModernSearchBoxEnabled_orig, (void *)CUniversalSearchBand_IsModernSearchBoxEnabled_hook, false }; if (!HookSymbols(hExplorerFrame, &hook, 1)) { Wh_Log(L"Failed to hook CUniversalSearchBand::IsModernSearchBoxEnabled"); return FALSE; } } if (settings.bNoUpButton) { WindhawkUtils::SYMBOL_HOOK hook = { { L"private: void " sWINAPI L"CUpBand::_CreateUpButtons(void)" }, (void **)&CUpBand__CreateUpButtons_orig, (void *)CUpBand__CreateUpButtons_hook, true }; if (!HookSymbols(hExplorerFrame, &hook, 1)) { Wh_Log(L"Failed to hook CUpBand::_CreateUpButtons"); return FALSE; } } return TRUE; } void Wh_ModSettingsChanged(BOOL *bReload) { *bReload = TRUE; LoadSettings(); } ```
m417z commented 9 months ago

Since all hooks are placed in Wh_ModInit, they're placed before the target process (explorer.exe) can resume execution, unless for some reason Windhawk is unable to intercept the process creation, in which case the injection is done asynchronously. See Mod lifetime.

Let's start by finding out whether the injection is done asynchronously. You can find out by adding the following log in Wh_ModInit:

#ifdef _WIN64
    const size_t OFFSET_SAME_TEB_FLAGS = 0x17EE;
#else
    const size_t OFFSET_SAME_TEB_FLAGS = 0x0FCA;
#endif

    bool isInitialThread = *(USHORT*)((BYTE*)NtCurrentTeb() + OFFSET_SAME_TEB_FLAGS) & 0x0400;
    Wh_Log(L"isInitialThread=%d", isInitialThread);

If the output is isInitialThread=0, the injection is done asynchronously. Otherwise, if the output is isInitialThread=1, the injection could be done synchronously, or asynchronously if Windhawk was quick enough.

aubymori commented 9 months ago

Since all hooks are placed in Wh_ModInit, they're placed before the target process (explorer.exe) can resume execution, unless for some reason Windhawk is unable to intercept the process creation, in which case the injection is done asynchronously. See Mod lifetime.

Let's start by finding out whether the injection is done asynchronously. You can find out by adding the following log in Wh_ModInit:

#ifdef _WIN64
    const size_t OFFSET_SAME_TEB_FLAGS = 0x17EE;
#else
    const size_t OFFSET_SAME_TEB_FLAGS = 0x0FCA;
#endif

    bool isInitialThread = *(USHORT*)((BYTE*)NtCurrentTeb() + OFFSET_SAME_TEB_FLAGS) & 0x0400;
    Wh_Log(L"isInitialThread=%d", isInitialThread);

If the output is isInitialThread=0, the injection is done asynchronously. Otherwise, if the output is isInitialThread=1, the injection could be done synchronously, or asynchronously if Windhawk was quick enough.

I'm seeing isInitialThread=0 on both initial injection to the already running explorer process and on the injection to the first instance of Control Panel.

m417z commented 9 months ago

I'm seeing isInitialThread=0 on both initial injection to the already running explorer process and on the injection to the first instance of Control Panel.

What does "the injection to the first instance of Control Panel" mean? I assume you have the "Launch folder windows in a separate process" option enabled, right? To understand why the injection is done asynchronously, we need to understand:

  1. Which process creates the Control Panel's process
  2. Is Windhawk injected into that process
  3. How is the process created

I assume that the answer to 3 is - nothing special, since it's a regular process (should be nothing exotic like conhost.exe).

Regarding 1 and 2, can you check it out? For 1, you can find it out with procmon by looking at process creation events. For 2, you can use procexp and look for the windhawk.dll module in the process.

aubymori commented 9 months ago

I'm seeing isInitialThread=0 on both initial injection to the already running explorer process and on the injection to the first instance of Control Panel.

What does "the injection to the first instance of Control Panel" mean? I assume you have the "Launch folder windows in a separate process" option enabled, right? To understand why the injection is done asynchronously, we need to understand:

1. Which process creates the Control Panel's process

2. Is Windhawk injected into that process

3. How is the process created

I assume that the answer to 3 is - nothing special, since it's a regular process (should be nothing exotic like conhost.exe).

Regarding 1 and 2, can you check it out? For 1, you can find it out with procmon by looking at process creation events. For 2, you can use procexp and look for the windhawk.dll module in the process.

Here's what I mean by "the injection to the first instance of Control Panel". It seems to create another explorer.exe process. This does not happen on any subsequent instances of Control Panel.

https://github.com/ramensoftware/windhawk/assets/44238627/f08ff77a-ee61-4ce8-991a-c7e84733ab83

  1. explorer.exe creates control.exe (Run dialog), and then control.exe seems to invoke svchost.exe in some way (it doesn't create the svchost.exe process itself) to create the new explorer.exe process for Control Panel. The command line of the new explorer.exe process is C:\Windows\explorer.exe /factory,{5BD95610-9434-43C2-886C-57852CC8A120) -Embedding.
  2. Yes.
  3. See 1.
aubymori commented 9 months ago

I should note that -Embedding command lines actually are really weird and exotic. No command line I have ever seen that has -Embedding, including this one (I've seen them when trying to port old things such as the Windows 7 Help and Support) ever work straight from the Run box.

m417z commented 9 months ago

I could reproduce it on my Windows 10 VM.

  1. Yes.

It seems to me that the answer is no, i.e. windhawk.dll isn't injected into that instance of svchost.exe. That explains why it can't intercept the creation of the explorer.exe process.

After enabling the "Inject into critical system processes" option, the injection becomes synchronous.

So that's a known limitation of Windhawk which, in this case, can be worked around.

aubymori commented 9 months ago

I could reproduce it on my Windows 10 VM.

  1. Yes.

It seems to me that the answer is no, i.e. windhawk.dll isn't injected into that instance of svchost.exe. That explains why it can't intercept the creation of the explorer.exe process.

After enabling the "Inject into critical system processes" option, the injection becomes synchronous.

So that's a known limitation of Windhawk which, in this case, can be worked around.

Ah, yes, you're right. After enabling injection into critical system processes, it works just fine on the initial launch.

aubymori commented 9 months ago

I suppose I'll close this issue for now and add a notice to my mod telling the user to enable the option.