吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 790|回复: 11
收起左侧

[C&C++ 原创] Windows 11下部分软件鼠标悬停托盘图标失效临时兼容

[复制链接]
周易 发表于 2024-12-7 12:44
本帖最后由 周易 于 2024-12-7 12:50 编辑

0x00 前言

Windows 11对TrayNotifyWnd进行了改动,导致部分软件鼠标悬停托盘图标失效。本文以“火绒安全软件”为例进行分析。在本例中,Windows 11下无法通过像Windows 10那样鼠标悬停托盘图标打开“功能消息”。

0x01 原因分析

通过语言模型分析HipsTray.exe,得到以下结果。

// Function to find the ToolbarWindow32 by traversing the window hierarchy.
HWND FindToolbarWindow32_439420()
{
    // Find the Shell_TrayWnd window, which is the top-level window for the system tray.
    HWND hWnd_Shell_TrayWnd = FindWindowW(L"Shell_TrayWnd", NULL);
    if (hWnd_Shell_TrayWnd)
    {
        // Find the TrayNotifyWnd window, which is a child of Shell_TrayWnd.
        HWND hWnd_TrayNotifyWnd = FindWindowExW(hWnd_Shell_TrayWnd, NULL, L"TrayNotifyWnd", NULL);
        if (hWnd_TrayNotifyWnd)
        {
            // Find the SysPager window, which is a child of TrayNotifyWnd.
            HWND hWnd_SysPager = FindWindowExW(hWnd_TrayNotifyWnd, NULL, L"SysPager", NULL);
            if (hWnd_SysPager)
            {
                // Return the ToolbarWindow32 window, which is a child of SysPager.
                return FindWindowExW(hWnd_SysPager, NULL, L"ToolbarWindow32", NULL);
            }
        }
    }
    return NULL; // Return NULL if any of the windows are not found.
}

// Method implementation to get the button rectangle under the cursor position.
signed int Class::GetCursorPosButtonRect_439570(RECT *rectButton)
{
    RECT rect{};                   // Temporary RECT structure to store window coordinates.
    BOOL bUpdateCursorPos = FALSE; // Flag to determine if cursor position needs to be updated.

    // Check if the rectButton pointer is NULL and return an error code if it is.
    if (!rectButton)
        return -22;

    POINT cursorPos{}; // Point structure to store the current cursor position.
    // Get the current cursor position and return an error code if it fails.
    if (!GetCursorPos(&cursorPos))
        return -13;

    // Get the window handle at the current cursor position.
    HWND hWnd = WindowFromPoint(cursorPos);
    WCHAR ClassName[MAX_PATH]{}; // Array to store the class name of the window at the cursor position.
    // Get the class name of the window at the cursor position and return an error code if it fails.
    if (!GetClassNameW(hWnd, ClassName, 260))
        return -13;

    // Check if the class name is not "ToolbarWindow32".
    if (_wcsicmp(ClassName, L"ToolbarWindow32") != 0)
    {
        // If Field is non-zero, return an error code.
        if (this->Field)
            return -14;

        // Check if the class name is not "TrayNotifyWnd".
        if (_wcsicmp(ClassName, L"TrayNotifyWnd") != 0)
        {
            // Check if the class name is not "TopLevelWindowForOverflowXamlIsland".
            if (_wcsicmp(ClassName, L"TopLevelWindowForOverflowXamlIsland") != 0)
                return -14;

            // Get the window rectangle and update the cursor position if necessary.
            GetWindowRect(hWnd, &rect);
            cursorPos.y = rect.top;
            bUpdateCursorPos = TRUE;
        }

        // Find the ToolbarWindow32 window using the helper function.
        hWnd = FindToolbarWindow32_439420();
        // If the ToolbarWindow32 window is not found, return an error code.
        if (!hWnd)
            return -14;
    }

    // Get the button size from the ToolbarWindow32.
    DWORD button_hw = (DWORD)SendMessageW(hWnd, TB_GETBUTTONSIZE, 0, 0);
    WORD button_h = HIWORD(button_hw); // Height of the button.
    WORD button_w = LOWORD(button_hw); // Width of the button.

    // If the cursor position does not need to be updated, get the window rectangle.
    if (!bUpdateCursorPos)
        if (!GetWindowRect(hWnd, &rect))
            return -13;

    // Calculate the button rectangle based on the cursor position and button size.
    LONG left_align = rect.left + button_w * ((cursorPos.x - rect.left) / button_w);
    rectButton->left = left_align;
    rectButton->right = button_w + left_align;
    LONG top_align = rect.top + button_h * ((cursorPos.y - rect.top) / button_h);
    rectButton->top = top_align;
    rectButton->bottom = button_h + top_align;

    // Return 0 on success.
    return 0;
}

有经验不难发现该段代码大意为获取鼠标悬停托盘图标的方形区域。在Windows 10中,触发消息时鼠标悬停位置对应的窗口类可能为ToolbarWindow32,也即包含多个托盘图标的托盘区域,通过GetWindowRect获取得到托盘的方形区域,再由TB_GETBUTTONSIZE获取得到单个托盘图标的大小,稍加计算即可完成。

然而,在Windows 11中,触发消息时鼠标悬停位置对应的窗口类可能为TrayNotifyWnd。在这一情况下,该段代码尝试在Shell_TrayWnd>TrayNotifyWnd>SysPager>ToolbarWindow32下二次查找。不幸的是,Windows 11对TrayNotifyWnd进行了改动,以24H2为例,没有SysPager>ToolbarWindow32。查找失效,该段代码自然返回错误。

至此,我们找到了原因。那么应该如何修复呢?直接修改代码是不可行的。一般在解决兼容性问题场景中,我们无法获取代码。那么修改既有软件呢?抛开EULA协议等因素,无论是修改文件还是修改内存,都面临难以同步软件版本的问题。即使使用特征匹配,也存在假阴性和假阳性。此外,修改文件还会破坏既有数字签名。在本例中,作为安全软件,修改其内存更是不可能的。

0x02 修复方案

那么应该如何修复呢?非常简单,其实不妨“模仿”一个假冒的窗口即可。大概代码如下。

#include <stdlib.h>
#include <windows.h>

int main()
{
    HWND hWnd_Shell_TrayWnd;
    hWnd_Shell_TrayWnd = FindWindowA("Shell_TrayWnd", NULL);
    if (!hWnd_Shell_TrayWnd)
        abort();

    HWND hWnd_TrayNotifyWnd;
    hWnd_TrayNotifyWnd = FindWindowExA(hWnd_Shell_TrayWnd, NULL, "TrayNotifyWnd", NULL);
    if (!hWnd_TrayNotifyWnd)
        abort();

    HWND hWnd_SysPager;
    hWnd_SysPager = FindWindowExA(hWnd_TrayNotifyWnd, NULL, "SysPager", NULL);
    if (!hWnd_SysPager)
        hWnd_SysPager = CreateWindowA("SysPager", NULL, WS_CHILD, 0, 0, 0, 0, hWnd_TrayNotifyWnd, NULL, NULL, NULL);
    if (!hWnd_SysPager)
        abort();

    HWND hWnd_ToolbarWindow32;
    hWnd_ToolbarWindow32 = FindWindowExA(hWnd_SysPager, NULL, "ToolbarWindow32", NULL);
    if (!hWnd_ToolbarWindow32)
        hWnd_ToolbarWindow32 = CreateWindowA("ToolbarWindow32", NULL, WS_CHILD, 0, 0, 0, 0, hWnd_SysPager, NULL, NULL, NULL);
    if (!hWnd_ToolbarWindow32)
        abort();

    MSG msg;
    BOOL fGotMessage;
    while ((fGotMessage = GetMessageA(&msg, NULL, 0, 0)) != 0 && fGotMessage != -1)
    {
        TranslateMessage(&msg);
        DispatchMessageA(&msg);
    }
}

在实际使用时,应当完善以上代码,如做好资源管理释放等。代码大意为逆向实现部分软件的判断逻辑,创建必要的窗口供查找。编译运行以上代码,此时鼠标悬停托盘图标已经可以得到正确响应了,不妨在这之后手动结束。

0xFF 后记

写到这里,有许多不得不提的话。实际上,有个形象的词语形容我们所做的事:“decoy”。这样的解决方案上个世纪就有之,也时常被人捡起。作为参考,不妨看看Raymond Chen的The Old New Thing博客。

https://devblogs.microsoft.com/oldnewthing/20060109-27/?p=32723
https://devblogs.microsoft.com/oldnewthing/20060410-17/?p=32703
https://devblogs.microsoft.com/oldnewthing/20241105-00/?p=110472

这里引用很早一篇博客里面的话。

The solution: Create a “decoy” Control Panel window with the same class name as Windows 3.1, so that this program would find it. The purpose of these “decoys” is to draw the attention of the offending program, taking the brunt of the mistreatment and doing what they can to mimic the original behavior enough to keep that program happy. In this case, it waited patiently for the garbage WM_COMMAND message to arrive and dutifully launched the Printers Control Panel.

Nowadays, this sort of problem would probably have been solved with the use of a shim. But this was back in Windows 95, where application compatibility technology was still comparatively immature. All that was available at the time were application compatibility flags and hot-patching of binaries, wherein the values are modified as they are loaded into memory. Using hot-patching technology was reserved for only the most extreme compatibility cases, because getting permission from the vendor to patch their program was a comparatively lengthy legal process. Patching was considered a “last resort” compatibility mechanism not only for the legal machinery necessary to permit it, but also because patching a program fixes only the versions of the program the patch was developed to address. If the vendor shipped ten versions of a program, ten different patches would have to be developed. And if the vendor shipped another version after Windows 95 was delivered to duplication, that version would be broken when Windows 95 hit the shelves.

It is important to understand the distinction between what is a documented and supported feature and what is an implementation detail. Documented and supported features are contracts between Windows and your program. Windows will uphold its end of the contract for as long as that feature exists. Implementation details, on the other hand, are ephemeral; they can change at any time, be it at the next major operating system release, at the next service pack, even with the next security hotfix. If your program relies on implementation details, you’re contributing to the compatibility cruft that Windows carries around from release to release.

可以发现,近20年前这段话,和我们今天所考虑的何其相似。Windows一直以良好的兼容性著称,这背后是大量不为人知的工作,也带来了大量的历史包袱,甚至被人误解(如之前explorer.exe检测360)。Windows 11诞生以来,似乎有意减少历史包袱,也造成了更多部分软件不兼容的问题。历史车轮滚滚向前,是是非非难以评说,作为我们,能做好的,就是少一些“hack into”,少依赖具体实现,多支持文档特性。

免费评分

参与人数 4威望 +1 吾爱币 +23 热心值 +3 收起 理由
苏紫方璇 + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Lighthouse + 1 + 1 热心回复!
Ming2022 + 1 我很赞同!
laozhang4201 + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

LiiJuu 发表于 2024-12-10 14:11
前端时间遇见了这个问题,这时候 任务管理器也无法加载出来。火绒确实是好使,扫描出来,一下就自动修复了,大神的解析也很到位。
shiyuzhe 发表于 2024-12-13 09:25
win11作为新系统是这样的,经常会有问题,但说实在的,出这么久了,还这样真的不合适,最后谢谢大佬
fx7 发表于 2024-12-7 16:07
freckle 发表于 2024-12-7 16:41
看不懂,完全看不懂
Ysanky 发表于 2024-12-7 17:34
我很赞同
Lighthouse 发表于 2024-12-7 18:11
感谢大佬分享,努力学习学习
SDXIAO 发表于 2024-12-7 22:01
1、负责认点说,这种修复是乱修复
SDXIAO 发表于 2024-12-7 22:01
1、合理的修复方案是重启资源管理器
fys2008 发表于 2024-12-9 12:57
本帖最后由 fys2008 于 2024-12-9 13:07 编辑

谢谢,看起来可以。
t1099778271 发表于 2024-12-11 19:55
我也遇到了    大佬们都咋解决的
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-1-7 19:45

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表