65.9K
CodeProject 正在变化。 阅读更多。
Home

密码字段隐藏器(以及一些 C++ 实用类)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (10投票s)

2009年4月9日

CPOL

13分钟阅读

viewsIcon

55936

downloadIcon

1499

用于取消掩码密码编辑控件和 INPUT 字段的实用程序,以及用于实现它的几个有用的 C++ 类。

Password Unhider in action

目录

引言

我有一个长期存在的困扰——我终于解决了它。这个小工具就是成果,我在这里发布它有两个原因。第一,也许你也有同样的困扰,而这是个解决方案。第二,在编写它的过程中,我使用了几个简单易用的工具类,它们可能对你也有用。

这个挥之不去的困扰是什么?就是那些显示星号或圆点而不是你输入的密码的讨厌的密码编辑控件。哦,是的,这是一个“安全”功能。因为可能有人在你身后窥探你的密码。但实际上,在我日常使用的 4 台电脑上,从来没有人,我的意思是,从来没有人会在我身后。它们就在我的书桌上,在我家里!但是,我使用强密码,包含标点符号、数字等各种字符——有时我真的无法确定我是否输入了正确的密码。

所以,这个简单的小工具就驻留在托盘区——哦,我是说,任务栏通知区域,每当有一个密码字段阻碍我视线时,我就可以简单地点击它的图标,密码字段就会变成一个普通的文本字段。问题解决了!

这确实是一个相当普通的旧式 Win32 程序。尽管如此,我还是使用了一些你可能会觉得有用的工具类。我有一个类,它封装了通过 WM_COPYDATA 将消息(字节缓冲区)从一个 Windows 程序发送到另一个程序的功能,还有一个通用的消息循环,以及一个方便易用的任务分派器,适用于在消息循环的空闲时间内运行任务。所有这些都很小巧简单,你可以在几分钟内阅读代码,了解它们的作用以及是否适合你的需求。如果你觉得某个有用,尽管使用它吧!

密码字段是如何被取消掩码的

PasswordUnhider 使用两种不同的技术来取消隐藏(取消掩码)密码字段。

首先,一些密码字段是普通 Win32 程序中的标准编辑控件,它们设置了密码掩码字符——要么控件是使用 ES_PASSWORD 样式创建的,要么它收到了一个带有非零密码字符的 EM_SETPASSWORDCHAR 通知。该实用程序会搜索这些编辑控件,并向它们发送一个带有零密码字符的 EM_SETPASSWORDCHAR 通知。

// Set the password character on an edit control - which is in another process
bool SetPasswordChar(HWND hWnd, wchar_t c)
{
    // Try SendMessage first
    ::SendMessage(hWnd, EM_SETPASSWORDCHAR, c, 0);
    DWORD err = ::GetLastError();
    if (err != 0)
    {
        // PostMessage can succeed where SendMessage failed.
        // (I'm not sure what the security model is that explains this.)
        BOOL b = ::PostMessage(hWnd, EM_SETPASSWORDCHAR, c, 0);
        if (!b)
        {
            err = ::GetLastError();
            // Well, it didn't work
            return false;
        }
    }
    ::InvalidateRect(hWnd, NULL, TRUE);
    return true;
}

为了找到这些控件,该实用程序会枚举桌面窗口(::EnumDesktopWindows API),然后对于每一个是应用程序的桌面窗口,它会枚举所有子窗口,寻找密码控件(使用 ::EnumChildWindows API)。

为了确定一个窗口是否是密码控件——也就是说,一个设置了密码隐藏字符的编辑控件——该实用程序会向该窗口发送一个 EM_GETPASSWORDCHAR 消息,并查看其返回结果。只有编辑控件——或者行为类似编辑控件的控件——会响应这个通知(可能有一些例外)。我无法查看类名或窗口过程,因为应用程序可能使用了被超级类或子类化但仍表现得像密码字段的编辑控件。

// Determine if a window is an edit control of some kind
bool IsPasswordEditControl(HWND hWnd)
{
    if (!::IsWindow(hWnd))
        return false;

    // Make sure the window class isn't on our list of non-edit controls
    // (that answer message EM_GETPASSWORDCHAR)
    wchar_t szClass[128];
    int n = ::GetClassName(hWnd, szClass, ARRAYSIZE(szClass));
    for (unsigned int i = 0; i < s_notEditControlClasses.size(); i++)
        if (s_notEditControlClasses[i] == szClass)
            return false;

    // Check for edit control by checking to see if it understands
    // EM_GETPASSWORDCHAR (and if the password char is set!)
    DWORD msgResult = 0;
    LRESULT r = ::SendMessageTimeout(hWnd, EM_GETPASSWORDCHAR, 0, 0, 
      SMTO_ABORTIFHUNG | SMTO_NORMAL, MSG_TIMEOUT, &msgResult);

    return (r != 0) && (msgResult != 0);
}

(我确实发现一个类名为“Acrobat IEHelper Object”的窗口对 EM_GETPASSWORDCHAR 返回一个非零值——这就是上面提到的例外——所以我也使用了一个小的异常类名数组来过滤掉这些误报。目前,数组中只有一个类名:“Acrobat IEHelper Object”。)

其次,一些密码字段是网页中的 INPUT 元素。在 IE 中,这些不是用 Win32 编辑控件实现的,所以使用了不同的技术。

首先,我使用 ShellWindows COM 对象找到所有浏览器(IE)窗口,它是一个包含所有浏览器窗口的集合。从每个窗口(表示为一个 COM 对象,而不是窗口句柄)中,可以检索到 HTML 文档,然后从每个 HTML 文档中,可以检索到所有 INPUT 元素的集合。接着,可以检查每个 INPUT 元素,看它是否有一个值为“password”的“type”属性。

一旦找到一个密码类型的 INPUT 元素,就需要将其类型更改为“text”。不幸的是,出于安全原因,你不能仅仅更改属性值。相反,该实用程序会获取该 INPUT 元素的外部 HTML,进行文本替换,将“type=password”替换为“type=text”,创建一个新的 INPUT 元素,使其与原始元素相同,只是更改了此属性,然后将其“修补”到树中,替换掉原始的 INPUT 元素。

// Begin the replacement by getting the outer HTML.
if (SUCCEEDED(hr))
{
    hr = pElement->get_outerHTML(&bstrOuterHTML);
}

// The string "type=password" ought to be in there somewhere ...
if (SUCCEEDED(hr))
{
    outer = bstrOuterHTML;
    wstring outerLower = MakeLower(outer);
    iFind = Find(outerLower, wstring(L"type=password"));
    hr = BOOL_TO_HR(iFind >= 0);
}

// ... so remove it.
if (SUCCEEDED(hr))
{
    outer.erase(iFind, _tcslen(L"type=password"));
    outer.insert(iFind, L"type=text");
    bstrNewElement = CComBSTR(outer.c_str());
    hr = PTR_NON_NULL(pDoc2 = pDoc3_);
}

// Create a new HTML element from the modified outer HTML
if (SUCCEEDED(hr))
{
    hr = SUCCEEDED_AND_PTR_NON_NULL(pDoc2->createElement(bstrNewElement, 
                                    &pNewElement), pNewElement);
}

// And now hook up our new HTML (INPUT) element replacing the original
// INPUT element.
if (SUCCEEDED(hr))
{
    hr = SUCCEEDED_AND_PTR_NON_NULL(pElement->get_parentElement(&pParent), pParent);
}
hr = SUCCEEDED_AND_PTR_NON_NULL(hr, pParentDOM = pParent);
hr = SUCCEEDED_AND_PTR_NON_NULL(hr, pElementDOM = pElement);
hr = SUCCEEDED_AND_PTR_NON_NULL(hr, pNewElementDOM = pNewElement);

if (SUCCEEDED(hr))
{
    hr = pParentDOM->replaceChild(pNewElementDOM, pElementDOM, &pReplaceDOM);
}

(不幸的是,当你这样做时,输入到该输入元素中的任何文本都会丢失。所以:在 HTML 字段中输入密码之前点击图标。)

划分工作

原始程序在一个进程中完成了所有工作:它运行托盘图标,并在收到指令时搜索并更改密码字段。在我编写程序之前,我就知道搜索桌面上的所有窗口以查找密码字段,以及搜索所有浏览器窗口以查找 INPUT 元素会需要一些时间。所以,我希望在搜索过程中,用户能得到一些反馈。我想让托盘图标在进程进行时闪烁。

对于这样一个简单的实用程序,我不想处理多线程,所以我找到了我可靠的空闲时间任务分派器,并用它将密码取消掩码过程分解成许多小的任务。查看每个窗口会成为一个独立的任务,查找每个 INPUT 元素也是如此。这样,我就可以在应用程序空闲时运行这些任务,然后系统托盘类就可以通过 Windows 定时器消息平滑地动画显示托盘图标。

但是,事与愿违,图标动画并不平滑。例如,获取 ShellWindows 集合以及处理每个 INPUT 元素等一些操作花费了相当长的时间。这是可以理解的,因为这些操作是在跨进程操作网页的 DOM。

因此,我重构了程序,使其现在运行在两个进程中。用户启动的原始进程负责显示和管理托盘图标,仅此而已。当用户点击取消密码字段掩码时,程序会启动自身的一个副本,使用命令行参数来告诉它该做什么,以及如何与原始进程通信。

第二个进程会向原始进程发送消息,报告它取消了哪些密码字段。这样,原始进程就可以向用户显示通知气球作为反馈。

(任务项的部分仍然存在,即使第二个进程实际上不需要它,因为它没有需要保持响应的 UI。它本来就在工作,没有理由移除它。)

(顺便说一下,我知道让一个进程运行一个系统托盘图标,占用几兆字节的私有数据等等,是一种资源浪费,但我不在乎。我希望那些密码字段能够按需取消掩码,并且我愿意为此付出代价。这不像是一些设备制造商(鼠标、轨迹球或显卡)安装在我的机器上,并决定一直运行,以防哪天我想更改屏幕分辨率——而我从不这样做。)

有用的 C++ 类

CMessagePump—应用程序主消息循环

CMessagePump 是一个实现应用程序主消息循环的类。它会一直运行直到收到 WM_QUIT

它可以这样参数化:

  • 它将处理多个加速键表。
  • 它将处理一个无模式对话框(用于键盘处理)。
  • 它将检查并在没有可用消息时执行任务(“空闲任务”)。
  • 它将为发送到线程的消息(而不是窗口)调用回调。

这是一个演示如何创建消息循环、对其进行参数化然后启动它的示例。

// Create the message pump
auto_ptr<CMessagePump> s_msgPump(new CMessagePump(hInstance));

// Set up accelerator table, and hook up
// the work item dispatcher to run in the idle time
s_msgPump->AddAccelerators(::LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_PASSWORD)))
          .SetIdleWorkTest(pdispatcher, &CWorkItemDispatcher::HaveWork)
          .SetIdleWorkFunction(pdispatcher, &CWorkItemDispatcher::DoWork);

// Pump messages, don't return until WM_QUIT
ret = s_msgPump->DoMessagePump();

CWorkItemDispatcher—易于使用的任务分派器,适用于空闲时间处理

CWorkItemDispatcher 实现了一个任务队列,按需执行一个任务。

在深入了解如何使用它之前,你心中肯定有一个问题,所以我先回答:喂,为什么世界还需要另一个任务分派器?

嗯,它其实不需要。但是,我还是喜欢这个。它非常简单,所以它妨碍你并出现问题的可能性非常小。它非常易于使用,所以任务可以构建为类,如果你需要状态;或者你可以通过在其上实现一个合适的 0 或 1 参数方法来使用你手头的任何对象;或者你可以使用任何可以塞进 std::tr1::function 的东西——这给了你很多选择,并最大限度地减少了你不得不绕远路来创建任务的可能性。尽管如此,它仍然实现了优先级,并且还实现了延迟任务,这些任务会在未来的某个时间触发。

而且,它与 CMessagePump(或你拥有的任何其他消息循环)平滑集成,以创建一个平稳运行的 Win32 应用程序。

那么,如何使用它呢?首先,我们来谈谈任务。有一个抽象类 IWorkItem,所有任务都派生自它。

class IWorkItem
{
    public:

        IWorkItem() {}
        virtual ~IWorkItem() {}

        // This is where you actually do the work of your work item.
        // The dispatcher is provided so that you can enqueue further
        // work items. You can even enqueue yourself again.
        virtual void DoWork(CWorkItemDispatcher* dispatcher) = 0;
};

好吧,这很标准。你派生自 IWorkItem,实现 DoWork()(以及构造函数和析构函数,如果你需要的话),然后就可以开始使用了。

或者,你也可以只提供一个函数来运行。CWorkItemFactory 类提供了一系列静态函数,可以从你的函数或类似函数的东西创建任务。

class CWorkItemFactory
{
public:
    // Work functions that take no parameters
    static std::auto_ptr<IWorkItem> New(void (*)());
    static std::auto_ptr<IWorkItem> New(std::tr1::function<void()>);
    template<class T> static std::auto_ptr<IWorkItem> New(T&, void(T::*)());

    // Work functions that take 1 parameter (typically, some kind of cookie)
    template<class TP> static std::auto_ptr<IWorkItem> New(void (*)(TP), TP);
    template<class TP> static std::auto_ptr<IWorkItem> New(std::tr1::function<void(TP)>, TP);
    template<class T, class TP> static std::auto_ptr<IWorkItem> New(T&, void(T::*)(TP), TP);
    ...

这六个 New 函数有两种形式:一种接受 0 参数函数,另一种接受 1 参数函数,以及一个参数对象(在任务运行时提供)。

这两种形式各有三种变体:一种接受简单函数,一种接受 std::tr1::function,一种接受对象和该对象上的方法。

这样就涵盖了许多如何轻松从你现有的函数或方法创建任务的可能性。

入队任务非常简单。分派器提供了一个 AddWork() 方法,它接受一个任务和一个优先级。当你入队任务时,分派器会接管它的所有权。还有一个 AddDelayedWork() 方法,它还接受一个以毫秒为单位的延迟。该任务将被保存在一个单独的队列中,直到(至少)经过这么长时间后,然后它将被转移到常规队列中(在那里它将按优先级顺序执行)。

最后,如何创建和使用任务分派器?一个工厂方法 CWorkItemDispatcher::StandardWorkItemDispatcherFactory() 会返回一个分派器。该分派器有两个有用的方法用于运行任务。

// Returns true if there are any work items in the queue, or if
// there are any delayed work items even if their timers haven't
// expired.
virtual bool HaveWork() = 0;

// Performs (at most) one unit of work (and moves delayed work items
// to the work queue if they're ready to go)
virtual void DoWork() = 0;

这些简单的方​​法非常适合连接到消息循环,或在线程的主循环中。

任务分派器可以有两种方式进行参数化。第一,你可以选择是否需要用锁来保护其任务队列。如果你是单线程程序,则不需要锁。如果你将从不同线程入队任务,则需要锁。(所有任务必须从单个线程消费;也就是说,DoWork() 应该始终从同一线程调用。)

任务分派器参数化的另一种方式是控制它如何“唤醒”其控制器——通常是消息循环——当它有新任务准备运行时。问题是,当没有工作要做时,消息循环可能会进入等待状态。然后,另一个线程可能会添加一个任务,或者到了某个延迟消息运行的时间。消息循环必须被唤醒。你可以控制使用什么 Windows 消息来唤醒消息循环,以及应该向哪个窗口或线程发送消息。

CCopyIPC—使用 WM_COPYDATA 在进程之间进行单向消息传输

CCopyIPC 将通过 Windows 的 WM_COPYDATA 机制将消息——一个字节缓冲区——从一个进程复制到另一个进程。显然,这意味着接收进程必须运行一个消息循环。

发送方和接收方都实例化一个 CCopyIPC 对象。

发送方只需要调用 CCopyIPC::Send,传入一个字节缓冲区或一个对象,以及要发送消息的窗口句柄。窗口句柄是带外传递的。对于 PasswordUnhider,我将其放在启动的进程的命令行上,以处理密码字段。

class CCopyIPC
{
    public:
        ...
        // Send a buffer of bytes to the receiver
        result_t Send(HWND hWndReceiver, uint nBytes, const char* bytes);

        // Send an object to the receiver
        template<class T>
        result_t Send(HWND hWndReceiver, const T& data);
        ...

接收方需要做更多工作。它必须在目标窗口的消息循环中处理 WM_COPYDATA 消息。CCopyIPC 提供了一个合适的处理程序,当收到 WM_COPYDATA 消息时,可以用 wparamlparam 调用它。

// Message handler for receiver
LRESULT DoWMCopyData(WPARAM wParam, LPARAM lParam);

接收方还必须提供一个回调函数,在数据到达时被调用。好处是 CCopyIPC 允许你以多种形式提供回调函数。

首先,回调函数可以接收字节缓冲区,也可以接收对象。其次,回调函数可以是简单函数、函数对象或方法。

    ...
    // Callbacks for received data - as a buffer of bytes
    // (the first parameter is the HWND of the sender)

    CCopyIPC& SetDataCallback(void (*)(HWND, uint, const char*));

    CCopyIPC& SetDataCallback(std::tr1::function<void(HWND, uint, const char*)>);

    template<class T>
    CCopyIPC& SetDataCallback(T& t, void (T::*)(HWND, uint, const char*));

    // Callbacks for received data - as a particular self-contained struct

    template<class TD>
    CCopyIPC& SetDataCallback(void (*)(HWND, const TD&));

    template<class TD>
    CCopyIPC& SetDataCallback(std::tr1::function<void(HWND, const TD&)>);

    template<class T, class TD>
    CCopyIPC& SetDataCallback(T& t, void (T::*)(HWND, const TD&));
    ...
};

关注点

我对目前的点击即取消掩码方法感到满意。另一种选择是持续监视系统并随时取消隐藏创建的密码字段。这似乎对用户更方便。

这种方法将使用 Windows 挂钩来处理普通的 Win32 应用程序,例如 CBT 挂钩。或者可以使用 DLL 注入方法,例如使用 Detours。对于 IE 窗口,你可以使用 BHO。

不幸的是,像这样的解决方案可能会导致稳定性问题,甚至可能出现性能问题。我决定,目前,我不需要 0 点击解决方案的便利性。

出现的一个问题是,在大多数情况下,当向另一个进程中的窗口发送 EM_SETPASSWORDCHAR 时,SendMessage 会因 ERROR_ACCESS_DENIED 而失败,但 PostMessage 却能正常工作。我不确定为什么会这样。如果有人能在评论中给我解释一下,我将不胜感激。

限制

这只是一个我为满足我的需求而编写的简单实用程序。我希望它能满足你的需求,但如果不能,……我很抱歉。

特别是

  • 我没有用 Firefox 或 IE 以外的任何浏览器进行测试。事实上,我只用 IE7 测试过。
  • 我只在 Windows XP 上进行过测试。Windows Vista 具有 UIPI(用户界面权限隔离),这可能会干扰进程之间发送的消息,从而更改密码字段。而且,出于这个原因或其他类似原因,在另一个进程中操作 HTML 文档可能也无法正常工作。
  • 它可能无法处理操作系统本身弹出的密码对话框——尽管我没有尝试过。

在所有情况下,我很想听听你遇到的问题,并且非常想听听你是如何解决的!

那些希望自己编译该实用程序或使用我描述的实用程序类的朋友们:我使用了 TR1 的一些功能,如 functionmem_fnbindtuple。这意味着,你至少需要 Visual Studio 2008 SP1。或者,你也可以使用 Boost 的 TR1 实现(尽管我没有尝试过)。

依赖项和致谢

系统托盘图标由优秀的 CSystemTray 类管理,该类由 Chris Maunder 编写,并在他精彩的文章《Adding Icons to the System Tray》中进行了描述。

文章修订历史

  • 2009 年 4 月 9 日:原文。
© . All rights reserved.