星号密码查看器 — 原理与实现详解


阅读导航

章节 内容
[一、从「星号」说起](#章节 内容 一、从「星号」说起 密码框里实际存的是什么 二、Windows 窗口基础 HWND、消息、跨进程读取 三、和键盘记录器的区别 主动拾取 vs 被动监听 四、整体架构 Electron + C++ 引擎 五、一次拾取的完整时间线 30ms 轮询与状态机 六、probeAt 探测流水线 从坐标到文本 七、三条读取路径 Edit / IE·MSAA / ComboBox 八、辅助机制 高亮、黑名单、隐私 九、真实场景走查 Xshell、IE、现代浏览器 十、能力边界与安全观 能读什么、不能读什么 十一、常见误解 FAQ 辟谣与答疑 附录 A 路径与状态对照总表) 密码框里实际存的是什么
[二、Windows 窗口基础](#章节 内容 一、从「星号」说起 密码框里实际存的是什么 二、Windows 窗口基础 HWND、消息、跨进程读取 三、和键盘记录器的区别 主动拾取 vs 被动监听 四、整体架构 Electron + C++ 引擎 五、一次拾取的完整时间线 30ms 轮询与状态机 六、probeAt 探测流水线 从坐标到文本 七、三条读取路径 Edit / IE·MSAA / ComboBox 八、辅助机制 高亮、黑名单、隐私 九、真实场景走查 Xshell、IE、现代浏览器 十、能力边界与安全观 能读什么、不能读什么 十一、常见误解 FAQ 辟谣与答疑 附录 A 路径与状态对照总表) HWND、消息、跨进程读取
[三、和键盘记录器的区别](#章节 内容 一、从「星号」说起 密码框里实际存的是什么 二、Windows 窗口基础 HWND、消息、跨进程读取 三、和键盘记录器的区别 主动拾取 vs 被动监听 四、整体架构 Electron + C++ 引擎 五、一次拾取的完整时间线 30ms 轮询与状态机 六、probeAt 探测流水线 从坐标到文本 七、三条读取路径 Edit / IE·MSAA / ComboBox 八、辅助机制 高亮、黑名单、隐私 九、真实场景走查 Xshell、IE、现代浏览器 十、能力边界与安全观 能读什么、不能读什么 十一、常见误解 FAQ 辟谣与答疑 附录 A 路径与状态对照总表) 主动拾取 vs 被动监听
[四、整体架构](#章节 内容 一、从「星号」说起 密码框里实际存的是什么 二、Windows 窗口基础 HWND、消息、跨进程读取 三、和键盘记录器的区别 主动拾取 vs 被动监听 四、整体架构 Electron + C++ 引擎 五、一次拾取的完整时间线 30ms 轮询与状态机 六、probeAt 探测流水线 从坐标到文本 七、三条读取路径 Edit / IE·MSAA / ComboBox 八、辅助机制 高亮、黑名单、隐私 九、真实场景走查 Xshell、IE、现代浏览器 十、能力边界与安全观 能读什么、不能读什么 十一、常见误解 FAQ 辟谣与答疑 附录 A 路径与状态对照总表) Electron + C++ 引擎
[五、一次拾取的完整时间线](#章节 内容 一、从「星号」说起 密码框里实际存的是什么 二、Windows 窗口基础 HWND、消息、跨进程读取 三、和键盘记录器的区别 主动拾取 vs 被动监听 四、整体架构 Electron + C++ 引擎 五、一次拾取的完整时间线 30ms 轮询与状态机 六、probeAt 探测流水线 从坐标到文本 七、三条读取路径 Edit / IE·MSAA / ComboBox 八、辅助机制 高亮、黑名单、隐私 九、真实场景走查 Xshell、IE、现代浏览器 十、能力边界与安全观 能读什么、不能读什么 十一、常见误解 FAQ 辟谣与答疑 附录 A 路径与状态对照总表) 30ms 轮询与状态机
[六、probeAt 探测流水线](#章节 内容 一、从「星号」说起 密码框里实际存的是什么 二、Windows 窗口基础 HWND、消息、跨进程读取 三、和键盘记录器的区别 主动拾取 vs 被动监听 四、整体架构 Electron + C++ 引擎 五、一次拾取的完整时间线 30ms 轮询与状态机 六、probeAt 探测流水线 从坐标到文本 七、三条读取路径 Edit / IE·MSAA / ComboBox 八、辅助机制 高亮、黑名单、隐私 九、真实场景走查 Xshell、IE、现代浏览器 十、能力边界与安全观 能读什么、不能读什么 十一、常见误解 FAQ 辟谣与答疑 附录 A 路径与状态对照总表) 从坐标到文本
[七、三条读取路径](#章节 内容 一、从「星号」说起 密码框里实际存的是什么 二、Windows 窗口基础 HWND、消息、跨进程读取 三、和键盘记录器的区别 主动拾取 vs 被动监听 四、整体架构 Electron + C++ 引擎 五、一次拾取的完整时间线 30ms 轮询与状态机 六、probeAt 探测流水线 从坐标到文本 七、三条读取路径 Edit / IE·MSAA / ComboBox 八、辅助机制 高亮、黑名单、隐私 九、真实场景走查 Xshell、IE、现代浏览器 十、能力边界与安全观 能读什么、不能读什么 十一、常见误解 FAQ 辟谣与答疑 附录 A 路径与状态对照总表) Edit / IE·MSAA / ComboBox
[八、辅助机制](#章节 内容 一、从「星号」说起 密码框里实际存的是什么 二、Windows 窗口基础 HWND、消息、跨进程读取 三、和键盘记录器的区别 主动拾取 vs 被动监听 四、整体架构 Electron + C++ 引擎 五、一次拾取的完整时间线 30ms 轮询与状态机 六、probeAt 探测流水线 从坐标到文本 七、三条读取路径 Edit / IE·MSAA / ComboBox 八、辅助机制 高亮、黑名单、隐私 九、真实场景走查 Xshell、IE、现代浏览器 十、能力边界与安全观 能读什么、不能读什么 十一、常见误解 FAQ 辟谣与答疑 附录 A 路径与状态对照总表) 高亮、黑名单、隐私
[九、真实场景走查](#章节 内容 一、从「星号」说起 密码框里实际存的是什么 二、Windows 窗口基础 HWND、消息、跨进程读取 三、和键盘记录器的区别 主动拾取 vs 被动监听 四、整体架构 Electron + C++ 引擎 五、一次拾取的完整时间线 30ms 轮询与状态机 六、probeAt 探测流水线 从坐标到文本 七、三条读取路径 Edit / IE·MSAA / ComboBox 八、辅助机制 高亮、黑名单、隐私 九、真实场景走查 Xshell、IE、现代浏览器 十、能力边界与安全观 能读什么、不能读什么 十一、常见误解 FAQ 辟谣与答疑 附录 A 路径与状态对照总表) Xshell、IE、现代浏览器
[十、能力边界与安全观](#章节 内容 一、从「星号」说起 密码框里实际存的是什么 二、Windows 窗口基础 HWND、消息、跨进程读取 三、和键盘记录器的区别 主动拾取 vs 被动监听 四、整体架构 Electron + C++ 引擎 五、一次拾取的完整时间线 30ms 轮询与状态机 六、probeAt 探测流水线 从坐标到文本 七、三条读取路径 Edit / IE·MSAA / ComboBox 八、辅助机制 高亮、黑名单、隐私 九、真实场景走查 Xshell、IE、现代浏览器 十、能力边界与安全观 能读什么、不能读什么 十一、常见误解 FAQ 辟谣与答疑 附录 A 路径与状态对照总表) 能读什么、不能读什么
[十一、常见误解 FAQ](#章节 内容 一、从「星号」说起 密码框里实际存的是什么 二、Windows 窗口基础 HWND、消息、跨进程读取 三、和键盘记录器的区别 主动拾取 vs 被动监听 四、整体架构 Electron + C++ 引擎 五、一次拾取的完整时间线 30ms 轮询与状态机 六、probeAt 探测流水线 从坐标到文本 七、三条读取路径 Edit / IE·MSAA / ComboBox 八、辅助机制 高亮、黑名单、隐私 九、真实场景走查 Xshell、IE、现代浏览器 十、能力边界与安全观 能读什么、不能读什么 十一、常见误解 FAQ 辟谣与答疑 附录 A 路径与状态对照总表) 辟谣与答疑
[附录 A](#章节 内容 一、从「星号」说起 密码框里实际存的是什么 二、Windows 窗口基础 HWND、消息、跨进程读取 三、和键盘记录器的区别 主动拾取 vs 被动监听 四、整体架构 Electron + C++ 引擎 五、一次拾取的完整时间线 30ms 轮询与状态机 六、probeAt 探测流水线 从坐标到文本 七、三条读取路径 Edit / IE·MSAA / ComboBox 八、辅助机制 高亮、黑名单、隐私 九、真实场景走查 Xshell、IE、现代浏览器 十、能力边界与安全观 能读什么、不能读什么 十一、常见误解 FAQ 辟谣与答疑 附录 A 路径与状态对照总表) 路径与状态对照总表

一、从「星号」说起

1.1 屏幕上看到的 ≠ 内存里保存的

很多 Windows 程序里的密码框,看起来是一串 *,但控件内部通常一直保存着您输入的真实字符(在 Win32 里往往是 UTF-16 宽字符串,存在 Edit 控件的内部缓冲区里)。

以标准 Win32 Edit 控件为例,样式里有一个位叫 ES_PASSWORD(0x0020)。它的含义是:

绘制文本时,用密码字符(默认 *)代替每个真实字符。

不是 加密、不是 哈希、不是把明文删掉。就像 Word 里把字体设成白色------字还在,只是显示方式变了。

因此,只要程序用的是「带密码样式的经典文本框」,并且您在本机有权限向该窗口发消息,就有可能通过 Windows 提供的合法接口问一句:「你缓冲区里现在是什么?」------答案就是明文。

1.2 星号密码查看器在做什么

可以概括为四步:

  1. 您主动在工具里按住拾取热区(左键不松开);
  2. 把鼠标移到目标程序的输入框上;
  3. 工具根据屏幕坐标找到下面的窗口(HWND),再按控件类型选策略读取文本;
  4. 读到的内容只显示在工具自己的窗口里,默认不上传、不写文件。

拾取时:左侧为工具窗,右侧为目标程序;引擎跨进程读取目标控件内的文本

1.3 它「破解」了什么?

更准确的说法是:它没有破解加密,只是读取 UI 控件里已有的缓冲区内容

  • 若网站在浏览器里用 HTTPS 传输密码,传输层仍是加密的;工具读的是已经解密并填进输入框之后、留在本机控件内存里的那一份。
  • 若程序把密码哈希后只存数据库、界面上根本不保留明文,这类场景工具也读不到------因为本来就没有明文可读

二、Windows 窗口基础(科普)

要理解拾取引擎,需要先弄清几个 Win32 概念。不必会写 C++,知道「系统怎么组织界面」即可。

2.1 HWND:窗口句柄

每个可见(或不可见)的窗口、控件,在系统里都有一个 HWND(窗口句柄)------可以理解成系统内部的「身份证号」。

  • 一个对话框是一个 HWND
  • 对话框里的密码框,往往是它的子窗口 ,有另一个 HWND
  • ComboBox 里可编辑的那一行,本质又是嵌在 ComboBox 里的 子 Edit

鼠标点在屏幕上时,系统需要回答:「这个坐标最上面是哪个窗口?」------这就是 WindowFromPoint 做的事。

2.2 窗口树:父窗口与子窗口

Windows 界面是一棵

text 复制代码
顶级窗口(例如 Xshell 主窗)
  └─ 对话框 #32770
       └─ Edit(真正的密码输入区)

有时鼠标下的 HWND父对话框,而不是里面的 Edit。拾取引擎会:

  1. RealChildWindowFromPoint 从坐标向下穿透几层;
  2. 若仍不准,对整棵子树 EnumChildWindows ,找类名像 EditTEdit 且包含鼠标点的最小矩形。

这就是为什么「点在对话框空白处,仍能读到里面密码框」------引擎会主动找子 Edit

2.3 SendMessage:跨进程「问一句」

SendMessage(WM_GETTEXT, ...) 的意思是:向目标窗口发送「请把你缓冲区的文字复制到我给的缓冲区里」。

要点:

概念 说明
跨进程 目标程序在另一个进程;Windows 内核会把消息投递到对方线程,由对方窗口过程处理
同步 SendMessage 会等对方处理完才返回
权限 若目标进程完整性级别更高(例如「以管理员运行」),普通用户进程可能被 UIPI 拒绝

这不是注入、不是 Hook,而是 Win32 公开文档化的消息机制,Spy++、Accessibility Insights 等工具同样依赖类似思路。

2.4 宽字符与长度上限

Edit 内部多用 UTF-16(wchar_t 。本工具单次最多读取 120 个宽字符(与经典实现保持一致,足够覆盖常见密码框;极长文本可能截断)。


三、和键盘记录器的区别

维度 星号密码查看器 典型键盘记录器
触发 您按住拾取、主动指向目标 常后台静默运行
数据来源 目标控件的 WM_GETTEXT / MSAA 等 全局键盘 Hook / 驱动
进程行为 不注入目标进程 常注入或挂钩
结果去向 仅本工具界面 可能写文件、外传
适用场景 核对已输入、忘记复制出来的密码 恶意窃听

结论 :原理是「读控件内存」,不是「记您按了哪些键」。两者在合规与风险上完全不同------本工具设计前提是您在本机、对您有权操作的软件、主动发起拾取


四、整体架构:界面与拾取引擎如何配合

工具分三层:React 界面Electron 主进程C++ 拾取引擎(DLL)
#mermaid-svg-zBSeIYovZteHx247{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zBSeIYovZteHx247 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zBSeIYovZteHx247 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zBSeIYovZteHx247 .error-icon{fill:#552222;}#mermaid-svg-zBSeIYovZteHx247 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zBSeIYovZteHx247 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zBSeIYovZteHx247 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zBSeIYovZteHx247 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zBSeIYovZteHx247 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zBSeIYovZteHx247 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zBSeIYovZteHx247 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zBSeIYovZteHx247 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zBSeIYovZteHx247 .marker.cross{stroke:#333333;}#mermaid-svg-zBSeIYovZteHx247 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zBSeIYovZteHx247 p{margin:0;}#mermaid-svg-zBSeIYovZteHx247 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zBSeIYovZteHx247 .cluster-label text{fill:#333;}#mermaid-svg-zBSeIYovZteHx247 .cluster-label span{color:#333;}#mermaid-svg-zBSeIYovZteHx247 .cluster-label span p{background-color:transparent;}#mermaid-svg-zBSeIYovZteHx247 .label text,#mermaid-svg-zBSeIYovZteHx247 span{fill:#333;color:#333;}#mermaid-svg-zBSeIYovZteHx247 .node rect,#mermaid-svg-zBSeIYovZteHx247 .node circle,#mermaid-svg-zBSeIYovZteHx247 .node ellipse,#mermaid-svg-zBSeIYovZteHx247 .node polygon,#mermaid-svg-zBSeIYovZteHx247 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zBSeIYovZteHx247 .rough-node .label text,#mermaid-svg-zBSeIYovZteHx247 .node .label text,#mermaid-svg-zBSeIYovZteHx247 .image-shape .label,#mermaid-svg-zBSeIYovZteHx247 .icon-shape .label{text-anchor:middle;}#mermaid-svg-zBSeIYovZteHx247 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zBSeIYovZteHx247 .rough-node .label,#mermaid-svg-zBSeIYovZteHx247 .node .label,#mermaid-svg-zBSeIYovZteHx247 .image-shape .label,#mermaid-svg-zBSeIYovZteHx247 .icon-shape .label{text-align:center;}#mermaid-svg-zBSeIYovZteHx247 .node.clickable{cursor:pointer;}#mermaid-svg-zBSeIYovZteHx247 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zBSeIYovZteHx247 .arrowheadPath{fill:#333333;}#mermaid-svg-zBSeIYovZteHx247 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zBSeIYovZteHx247 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zBSeIYovZteHx247 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zBSeIYovZteHx247 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zBSeIYovZteHx247 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zBSeIYovZteHx247 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zBSeIYovZteHx247 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zBSeIYovZteHx247 .cluster text{fill:#333;}#mermaid-svg-zBSeIYovZteHx247 .cluster span{color:#333;}#mermaid-svg-zBSeIYovZteHx247 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zBSeIYovZteHx247 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zBSeIYovZteHx247 rect.text{fill:none;stroke-width:0;}#mermaid-svg-zBSeIYovZteHx247 .icon-shape,#mermaid-svg-zBSeIYovZteHx247 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zBSeIYovZteHx247 .icon-shape p,#mermaid-svg-zBSeIYovZteHx247 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zBSeIYovZteHx247 .icon-shape .label rect,#mermaid-svg-zBSeIYovZteHx247 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zBSeIYovZteHx247 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zBSeIYovZteHx247 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zBSeIYovZteHx247 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} IPC: 开始/结束拾取
每约 30ms
pickerProbeAt x y
WindowFromPoint SendMessage MSAA
state text path
picker:update
用户按住拾取热区
Electron 界面 React
Electron 主进程
C++ 拾取引擎
其他程序的窗口控件
读取光标 + 左键状态

4.1 为什么拾取放在主进程?

渲染网页的 Renderer 进程 沙箱化、焦点受限,很难稳定获得「全屏鼠标坐标」并调用原生 DLL。

主进程可以:

  • screen.getCursorScreenPoint() 读光标;
  • setInterval(..., 30) 约每 30ms 采样一次;
  • 通过 FFI 调用 C++ 的 pickerProbeAt
  • webContents.send('picker:update', ...) 推给界面。

4.2 为什么用 C++ 动态库?

读取其他程序窗口需要直接调用 Win32 APICOM (IE 的 IHTMLDocument2IAccessible)。C++ 延迟低,也和 UI 解耦:界面只展示,引擎只探测。

进程初始化时会 OleInitialize,保证 MSAA/COM 调用稳定。

4.3 如何避免拾取到自己?

开始拾取时,主进程把本工具窗口的 native HWND 传给引擎(pickerBegin(exclude_hwnd))。引擎会:

  • WindowFromPoint 命中本窗或其子窗 → 返回空,不读;
  • 若同一进程 ID 的窗口 → 视为自身;
  • 沿 GetParent 链向上,任何一级是 exclude 窗口都跳过。

4.4 界面层(React)做什么?

usePicker hook 监听 picker:update / picker:ended:更新坐标、结果文本、路径标签(如 Win32 Edit · ES_PASSWORD)。不直接碰 Win32,只消费主进程推送的结构化结果。


五、一次拾取的完整时间线

5.1 状态机

#mermaid-svg-9cpgI2TMPaGTI9hD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9cpgI2TMPaGTI9hD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9cpgI2TMPaGTI9hD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9cpgI2TMPaGTI9hD .error-icon{fill:#552222;}#mermaid-svg-9cpgI2TMPaGTI9hD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9cpgI2TMPaGTI9hD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9cpgI2TMPaGTI9hD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9cpgI2TMPaGTI9hD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9cpgI2TMPaGTI9hD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9cpgI2TMPaGTI9hD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9cpgI2TMPaGTI9hD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9cpgI2TMPaGTI9hD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9cpgI2TMPaGTI9hD .marker.cross{stroke:#333333;}#mermaid-svg-9cpgI2TMPaGTI9hD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9cpgI2TMPaGTI9hD p{margin:0;}#mermaid-svg-9cpgI2TMPaGTI9hD defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-9cpgI2TMPaGTI9hD g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-9cpgI2TMPaGTI9hD g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-9cpgI2TMPaGTI9hD g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-9cpgI2TMPaGTI9hD g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-9cpgI2TMPaGTI9hD g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-9cpgI2TMPaGTI9hD .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-9cpgI2TMPaGTI9hD .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-9cpgI2TMPaGTI9hD .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-9cpgI2TMPaGTI9hD .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-9cpgI2TMPaGTI9hD .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-9cpgI2TMPaGTI9hD .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-9cpgI2TMPaGTI9hD .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-9cpgI2TMPaGTI9hD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9cpgI2TMPaGTI9hD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9cpgI2TMPaGTI9hD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9cpgI2TMPaGTI9hD .edgeLabel .label text{fill:#333;}#mermaid-svg-9cpgI2TMPaGTI9hD .label div .edgeLabel{color:#333;}#mermaid-svg-9cpgI2TMPaGTI9hD .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-9cpgI2TMPaGTI9hD .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-9cpgI2TMPaGTI9hD .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-9cpgI2TMPaGTI9hD .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-9cpgI2TMPaGTI9hD .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-9cpgI2TMPaGTI9hD .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9cpgI2TMPaGTI9hD .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9cpgI2TMPaGTI9hD #statediagram-barbEnd{fill:#333333;}#mermaid-svg-9cpgI2TMPaGTI9hD .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9cpgI2TMPaGTI9hD .cluster-label,#mermaid-svg-9cpgI2TMPaGTI9hD .nodeLabel{color:#131300;}#mermaid-svg-9cpgI2TMPaGTI9hD .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-9cpgI2TMPaGTI9hD .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-9cpgI2TMPaGTI9hD .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-9cpgI2TMPaGTI9hD .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-9cpgI2TMPaGTI9hD .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-9cpgI2TMPaGTI9hD .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-9cpgI2TMPaGTI9hD .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-9cpgI2TMPaGTI9hD .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-9cpgI2TMPaGTI9hD .note-edge{stroke-dasharray:5;}#mermaid-svg-9cpgI2TMPaGTI9hD .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-9cpgI2TMPaGTI9hD .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-9cpgI2TMPaGTI9hD .statediagram-note text{fill:black;}#mermaid-svg-9cpgI2TMPaGTI9hD .statediagram-note .nodeLabel{color:black;}#mermaid-svg-9cpgI2TMPaGTI9hD .statediagram .edgeLabel{color:red;}#mermaid-svg-9cpgI2TMPaGTI9hD #dependencyStart,#mermaid-svg-9cpgI2TMPaGTI9hD #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-9cpgI2TMPaGTI9hD .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9cpgI2TMPaGTI9hD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 在拾取热区按下左键
移动鼠标 每30ms探测
松开左键
Idle
Picking

状态 用户侧 系统侧
空闲 显示上次结果或占位 不探测
拾取中 十字光标、坐标变化、目标白框 pickerProbeAt 循环;IE 会话可能保持
结束 提示已退出 pickerEnd:擦高亮、clearIeSession

5.2 时序(简化)

目标程序 C++引擎 主进程 React 用户 目标程序 C++引擎 主进程 React 用户 #mermaid-svg-bVeOcGk6FfAnAE3S{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bVeOcGk6FfAnAE3S .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bVeOcGk6FfAnAE3S .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bVeOcGk6FfAnAE3S .error-icon{fill:#552222;}#mermaid-svg-bVeOcGk6FfAnAE3S .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bVeOcGk6FfAnAE3S .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bVeOcGk6FfAnAE3S .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bVeOcGk6FfAnAE3S .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bVeOcGk6FfAnAE3S .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bVeOcGk6FfAnAE3S .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bVeOcGk6FfAnAE3S .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bVeOcGk6FfAnAE3S .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bVeOcGk6FfAnAE3S .marker.cross{stroke:#333333;}#mermaid-svg-bVeOcGk6FfAnAE3S svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bVeOcGk6FfAnAE3S p{margin:0;}#mermaid-svg-bVeOcGk6FfAnAE3S .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bVeOcGk6FfAnAE3S text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-bVeOcGk6FfAnAE3S .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-bVeOcGk6FfAnAE3S .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-bVeOcGk6FfAnAE3S .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-bVeOcGk6FfAnAE3S .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-bVeOcGk6FfAnAE3S #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-bVeOcGk6FfAnAE3S .sequenceNumber{fill:white;}#mermaid-svg-bVeOcGk6FfAnAE3S #sequencenumber{fill:#333;}#mermaid-svg-bVeOcGk6FfAnAE3S #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-bVeOcGk6FfAnAE3S .messageText{fill:#333;stroke:none;}#mermaid-svg-bVeOcGk6FfAnAE3S .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bVeOcGk6FfAnAE3S .labelText,#mermaid-svg-bVeOcGk6FfAnAE3S .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-bVeOcGk6FfAnAE3S .loopText,#mermaid-svg-bVeOcGk6FfAnAE3S .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-bVeOcGk6FfAnAE3S .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-bVeOcGk6FfAnAE3S .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-bVeOcGk6FfAnAE3S .noteText,#mermaid-svg-bVeOcGk6FfAnAE3S .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-bVeOcGk6FfAnAE3S .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bVeOcGk6FfAnAE3S .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bVeOcGk6FfAnAE3S .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bVeOcGk6FfAnAE3S .actorPopupMenu{position:absolute;}#mermaid-svg-bVeOcGk6FfAnAE3S .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-bVeOcGk6FfAnAE3S .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bVeOcGk6FfAnAE3S .actor-man circle,#mermaid-svg-bVeOcGk6FfAnAE3S line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-bVeOcGk6FfAnAE3S :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop 每30ms且左键仍按下 在热区按下左键 beginPickerSession pickerBegin exclude_hwnd getCursorScreenPoint pickerProbeAt x y WindowFromPoint / SendMessage / MSAA 文本或拒绝 state text path picker:update 松开左键 pickerEnd picker:ended

5.3 引擎返回的状态

state 含义 界面典型文案
Success 读到非空文本 显示明文 + 路径标签
IeHint 进入 IE 区域但未命中 input 「已进入 IE 浏览器区域」
Denied UIPI 等权限拒绝 「无法访问目标窗口...」
Unsupported 类名黑名单或不支持 「当前控件不支持读取」
Empty 无窗口、自身窗口、空控件 空白或占位

六、probeAt 探测流水线

每次 pickerProbeAt(x, y) 大致按下面顺序执行(与实现一致):
#mermaid-svg-CmKjHXMNBlNQNiyB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CmKjHXMNBlNQNiyB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CmKjHXMNBlNQNiyB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CmKjHXMNBlNQNiyB .error-icon{fill:#552222;}#mermaid-svg-CmKjHXMNBlNQNiyB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CmKjHXMNBlNQNiyB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CmKjHXMNBlNQNiyB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CmKjHXMNBlNQNiyB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CmKjHXMNBlNQNiyB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CmKjHXMNBlNQNiyB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CmKjHXMNBlNQNiyB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CmKjHXMNBlNQNiyB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CmKjHXMNBlNQNiyB .marker.cross{stroke:#333333;}#mermaid-svg-CmKjHXMNBlNQNiyB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CmKjHXMNBlNQNiyB p{margin:0;}#mermaid-svg-CmKjHXMNBlNQNiyB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CmKjHXMNBlNQNiyB .cluster-label text{fill:#333;}#mermaid-svg-CmKjHXMNBlNQNiyB .cluster-label span{color:#333;}#mermaid-svg-CmKjHXMNBlNQNiyB .cluster-label span p{background-color:transparent;}#mermaid-svg-CmKjHXMNBlNQNiyB .label text,#mermaid-svg-CmKjHXMNBlNQNiyB span{fill:#333;color:#333;}#mermaid-svg-CmKjHXMNBlNQNiyB .node rect,#mermaid-svg-CmKjHXMNBlNQNiyB .node circle,#mermaid-svg-CmKjHXMNBlNQNiyB .node ellipse,#mermaid-svg-CmKjHXMNBlNQNiyB .node polygon,#mermaid-svg-CmKjHXMNBlNQNiyB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CmKjHXMNBlNQNiyB .rough-node .label text,#mermaid-svg-CmKjHXMNBlNQNiyB .node .label text,#mermaid-svg-CmKjHXMNBlNQNiyB .image-shape .label,#mermaid-svg-CmKjHXMNBlNQNiyB .icon-shape .label{text-anchor:middle;}#mermaid-svg-CmKjHXMNBlNQNiyB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CmKjHXMNBlNQNiyB .rough-node .label,#mermaid-svg-CmKjHXMNBlNQNiyB .node .label,#mermaid-svg-CmKjHXMNBlNQNiyB .image-shape .label,#mermaid-svg-CmKjHXMNBlNQNiyB .icon-shape .label{text-align:center;}#mermaid-svg-CmKjHXMNBlNQNiyB .node.clickable{cursor:pointer;}#mermaid-svg-CmKjHXMNBlNQNiyB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CmKjHXMNBlNQNiyB .arrowheadPath{fill:#333333;}#mermaid-svg-CmKjHXMNBlNQNiyB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CmKjHXMNBlNQNiyB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CmKjHXMNBlNQNiyB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CmKjHXMNBlNQNiyB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CmKjHXMNBlNQNiyB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CmKjHXMNBlNQNiyB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CmKjHXMNBlNQNiyB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CmKjHXMNBlNQNiyB .cluster text{fill:#333;}#mermaid-svg-CmKjHXMNBlNQNiyB .cluster span{color:#333;}#mermaid-svg-CmKjHXMNBlNQNiyB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CmKjHXMNBlNQNiyB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CmKjHXMNBlNQNiyB rect.text{fill:none;stroke-width:0;}#mermaid-svg-CmKjHXMNBlNQNiyB .icon-shape,#mermaid-svg-CmKjHXMNBlNQNiyB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CmKjHXMNBlNQNiyB .icon-shape p,#mermaid-svg-CmKjHXMNBlNQNiyB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CmKjHXMNBlNQNiyB .icon-shape .label rect,#mermaid-svg-CmKjHXMNBlNQNiyB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CmKjHXMNBlNQNiyB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CmKjHXMNBlNQNiyB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CmKjHXMNBlNQNiyB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否







pickerProbeAt
picking?
Empty
resolveHwndAtPoint
非Combo且可找子Edit?
hwnd = 子Edit
保持 hwnd
ie_root 存在且 hwnd 无效或自身?
probeIeInner 路径2b
hwnd 有效?
擦高亮 clearIeSession Empty
probeWindow
类名分支
probeComboBox
probeEdit
probeIeServer
probeGeneric

关键设计点:

  1. 子 Edit 优先:点在对话框上也能找到内嵌密码框。
  2. 路径 2b :IE 会话建立后,即使鼠标 briefly 经过工具窗口,仍用缓存的 ie_rootaccHitTest,避免 IE 内拾取「闪断」。
  3. 离开 IE 时清理 :进入 probeEdit / probeComboBox / probeGeneric 前调用 clearIeSession(),防止用错误的 IE 根对象继续 hitTest。

七、三条读取路径

定位到 HWND 后,按类名 分支。三者互斥,优先级体现在 probeWindow 的分支顺序上。
#mermaid-svg-zObh1WUp7j2hmCzj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zObh1WUp7j2hmCzj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zObh1WUp7j2hmCzj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zObh1WUp7j2hmCzj .error-icon{fill:#552222;}#mermaid-svg-zObh1WUp7j2hmCzj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zObh1WUp7j2hmCzj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zObh1WUp7j2hmCzj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zObh1WUp7j2hmCzj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zObh1WUp7j2hmCzj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zObh1WUp7j2hmCzj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zObh1WUp7j2hmCzj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zObh1WUp7j2hmCzj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zObh1WUp7j2hmCzj .marker.cross{stroke:#333333;}#mermaid-svg-zObh1WUp7j2hmCzj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zObh1WUp7j2hmCzj p{margin:0;}#mermaid-svg-zObh1WUp7j2hmCzj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zObh1WUp7j2hmCzj .cluster-label text{fill:#333;}#mermaid-svg-zObh1WUp7j2hmCzj .cluster-label span{color:#333;}#mermaid-svg-zObh1WUp7j2hmCzj .cluster-label span p{background-color:transparent;}#mermaid-svg-zObh1WUp7j2hmCzj .label text,#mermaid-svg-zObh1WUp7j2hmCzj span{fill:#333;color:#333;}#mermaid-svg-zObh1WUp7j2hmCzj .node rect,#mermaid-svg-zObh1WUp7j2hmCzj .node circle,#mermaid-svg-zObh1WUp7j2hmCzj .node ellipse,#mermaid-svg-zObh1WUp7j2hmCzj .node polygon,#mermaid-svg-zObh1WUp7j2hmCzj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zObh1WUp7j2hmCzj .rough-node .label text,#mermaid-svg-zObh1WUp7j2hmCzj .node .label text,#mermaid-svg-zObh1WUp7j2hmCzj .image-shape .label,#mermaid-svg-zObh1WUp7j2hmCzj .icon-shape .label{text-anchor:middle;}#mermaid-svg-zObh1WUp7j2hmCzj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zObh1WUp7j2hmCzj .rough-node .label,#mermaid-svg-zObh1WUp7j2hmCzj .node .label,#mermaid-svg-zObh1WUp7j2hmCzj .image-shape .label,#mermaid-svg-zObh1WUp7j2hmCzj .icon-shape .label{text-align:center;}#mermaid-svg-zObh1WUp7j2hmCzj .node.clickable{cursor:pointer;}#mermaid-svg-zObh1WUp7j2hmCzj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zObh1WUp7j2hmCzj .arrowheadPath{fill:#333333;}#mermaid-svg-zObh1WUp7j2hmCzj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zObh1WUp7j2hmCzj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zObh1WUp7j2hmCzj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zObh1WUp7j2hmCzj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zObh1WUp7j2hmCzj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zObh1WUp7j2hmCzj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zObh1WUp7j2hmCzj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zObh1WUp7j2hmCzj .cluster text{fill:#333;}#mermaid-svg-zObh1WUp7j2hmCzj .cluster span{color:#333;}#mermaid-svg-zObh1WUp7j2hmCzj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zObh1WUp7j2hmCzj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zObh1WUp7j2hmCzj rect.text{fill:none;stroke-width:0;}#mermaid-svg-zObh1WUp7j2hmCzj .icon-shape,#mermaid-svg-zObh1WUp7j2hmCzj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zObh1WUp7j2hmCzj .icon-shape p,#mermaid-svg-zObh1WUp7j2hmCzj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zObh1WUp7j2hmCzj .icon-shape .label rect,#mermaid-svg-zObh1WUp7j2hmCzj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zObh1WUp7j2hmCzj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zObh1WUp7j2hmCzj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zObh1WUp7j2hmCzj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是





屏幕坐标
定位 HWND
ComboBox 类?
子 Edit 或 CB_GETLBTEXT
Edit 类?
密码绕过 / WM_GETTEXT
Internet Explorer_Server?
WM_HTML_GETOBJECT + MSAA
WM_GETTEXT 或 不支持


7.1 路径一:Win32 Edit 密码框

适用 :类名为 Edit,或 TEdit(Delphi)、ThunderRT6TextBox(VB6)、含 .EDIT. 的 MFC 等。

核心思路 :密码样式只影响绘制 ;临时去掉密码模式 → WM_GETTEXT立即恢复

7.1.1 ES_PASSWORD 绕过(逐步)
步骤 API / 消息 说明
1 GetWindowLongPtr(GWL_STYLE) 读取样式,检查 ES_PASSWORD
2 SetWindowWord + SetWindowLongPtr 清密码位 促使控件刷新内部状态(经典实现顺序)
3 SendMessage(WM_GETTEXT) 跨进程读宽字符,最多 120
4 SetWindowLongPtr 写回原始 style 用户通常无感知

伪代码:

text 复制代码
style = GetWindowLongPtr(hwnd, GWL_STYLE)
if (style & ES_PASSWORD) {
    SetWindowWord(hwnd, GWL_STYLE, 0)
    SetWindowLongPtr(hwnd, GWL_STYLE, style & ~ES_PASSWORD)
    SendMessage(hwnd, WM_GETTEXT, buf, ...)
    SetWindowLongPtr(hwnd, GWL_STYLE, style)  // 恢复
}
7.1.2 多级 fallback(真实顺序)

probeEdit 并非只试一种办法,而是按失败顺序逐级降级(与源码一致):

顺序 策略 路径标签示例 典型场景
ES_PASSWORD 位存在 → 样式绕过 Win32 Edit · ES_PASSWORD 标准密码框
EM_SETPASSWORDCHAR 置 0 再读,读完后恢复 * Edit · EM_SETPASSWORDCHAR Xshell 等:显示 * 但未置 ES_PASSWORD
无 ES_PASSWORD 位仍尝试样式绕过 Win32 Edit · ES_PASSWORD 部分程序样式位读不准
普通 WM_GETTEXT Win32 Edit · WM_GETTEXT 普通 Edit、非密码
SendMessageTimeout 500ms (同上) 目标线程繁忙防卡死

任一步若 ERROR_ACCESS_DENIED → 状态 Denied(UIPI)。

7.1.3 子 Edit 穿透
  • RealChildWindowFromPoint:最多向下 16 层;
  • EnumChildWindows + 最小面积 Edit:点在父窗上找真正输入区。

ComboBox 的可编辑区另见 7.3


7.2 路径二:IE / Trident(MSAA)

适用 :类名 Internet Explorer_Server------仍用 IE/Trident 内核嵌入页面的老式程序(部分工控、老 ERP、旧版内嵌 WebView)。

不适用 :Chrome、Edge(Chromium)、Firefox 的网页------它们的渲染层不是 Win32 Edit,也没有上述类名(见 九、场景走查)。

7.2.1 什么是 MSAA?

MSAA(Microsoft Active Accessibility) 是 Windows 早期的无障碍接口。屏幕阅读器用它获取「这是什么控件、当前值是多少」。

IE 的 HTML 元素会映射到 IAccessible 树:角色(role)可以是 passwordtext,值(value)即输入框里的字符串。

工具走的是文档化的 COM 接口,不是私有协议。

7.2.2 阶段 A --- 建立 IE 会话
  1. 向 IE 控件发注册消息 WM_HTML_GETOBJECT
  2. 通过 ObjectFromLresult 拿到文档相关对象;
  3. 取得 IHTMLDocument2 ,再拿到文档的 IAccessible缓存ie_root_)。

若 COM 失败,仍可能显示 IeHint (「已进入 IE 浏览器区域」),路径标签 IE · WM_HTML_GETOBJECT

7.2.3 阶段 B --- accHitTest 递归

对缓存的根对象:

  1. accHitTest(x, y) --- 坐标先试 IE 客户区(ScreenToClient),未命中再试屏幕坐标;
  2. 检查命中元素 role 是否为 ROLE_SYSTEM_PASSWORD / ROLE_SYSTEM_TEXT(或字符串 "password" / "text");
  3. 匹配则 get_accValue 读当前值;
  4. 未匹配则沿 HTML 父节点调整坐标递归,深度上限 32,防死循环。

还可降级尝试 IHTMLElement 相关接口(实现中的补充路径)。

7.2.4 路径 2b --- IE 内持续追踪

鼠标移过工具窗口时,WindowFromPoint 可能命中工具自身,但 ie_root_ 仍有效 。此时走 probeIeInner,继续用 IE 根做 hitTest,使在 IE 页面内移动时光标不必一直压在 IE 控件上也能更新结果。

7.2.5 何时 clearIeSession?

进入 Edit、ComboBox、Generic 分支前都会 clearIeSession() (释放 IAccessible、清空 ie_server_hwnd_),避免从 IE 切到普通控件后仍用 IE 树探测。


7.3 路径三:ComboBox 与普通控件

7.3.1 普通控件

对实现了 WM_GETTEXT 的标准控件直接读取;失败则 UnsupportedDenied

7.3.2 ComboBox 结构
text 复制代码
ComboBox / ComboBoxEx32
  ├─ Edit(CBS_DROPDOWN 可编辑时)
  └─ ListBox(下拉列表)
类型 样式 读取方式 路径标签
可编辑下拉 CBS_DROPDOWN CB_GETEDITCONTROL → 对子 Edit 走路径一 ComboBox · Edit
只读下拉 CBS_DROPDOWNLIST CB_GETCURSEL + CB_GETLBTEXT ComboBox · CB_GETLBTEXT
ComboBoxEx32 扩展控件 GetWindow(GW_CHILD) 找到内嵌标准 ComboBox 再处理 同上

ComboBoxEx32 细节 :外层类名是 ComboBoxEx32,真正逻辑在第一个子 ComboBox 上;若 Edit 区有预设文本,需宿主正确初始化(回归靶场 ComboBoxPickerTest 验证了 CBEM_SETEDITTEXT 等场景)。

鼠标点在 Edit 区域 → findComboBoxEditAtPointprobeEdit ;否则 probeComboBoxList 读当前选中项文字。


八、辅助机制

8.1 XOR 白色高亮

切换目标时,在目标客户区XOR 模式R2_XORPEN)画 3 像素白色空心矩形:

  • 第一次 XOR:白框出现;
  • 同一矩形再 XOR 一次:恢复背景(无需保存像素)。

切换目标或结束拾取时 eraseHighlight 擦除。

8.2 类名黑名单

对已知无法通过 Win32 消息可靠读取的类名,直接 Unsupported ,避免长时间 SendMessage 挂起或误导:

类名(部分) 原因
Chrome_WidgetWin_1 / Chrome_RenderWidgetHostHWND Chromium 多进程渲染,非 Edit
MozillaWindowClass / 含 Firefox Gecko 自绘
ApplicationFrameWindow / Windows.UI.Core.CoreWindow UWP
Credential Dialog Xaml Host 系统凭据 UI

ChromeMozillaFirefox 子串的类名也会拦截。

8.3 路径标签(path)

成功或部分成功时,界面右下角显示简短标签,表示本次命中的策略,便于您判断场景是否被支持,例如:

  • Win32 Edit · ES_PASSWORD
  • Edit · EM_SETPASSWORDCHAR
  • ComboBox · Edit
  • IE · MSAA

8.4 隐私与数据流

text 复制代码
目标控件内存 → C++ 引擎(栈上 wchar 缓冲)→ UTF-8 字符串 → 主进程 → React 状态
  • 默认 写磁盘、发网络;
  • 探测分配的字符串用完即释放;
  • 复制到剪贴板需您主动点击复制

九、真实场景走查

9.1 Xshell 连接对话框(已验证)

现象 :密码框显示 *,类名常为 Edit 或类似,未必ES_PASSWORD

引擎行为

  1. WindowFromPoint → 可能先命中对话框 → findEditAtPoint 找到子 Edit;
  2. ES_PASSWORD 绕过可能失败;
  3. EM_SETPASSWORDCHAR 清 0WM_GETTEXT 成功 → 恢复 *
  4. 标签 Edit · EM_SETPASSWORDCHAR

这是路径一 fallback ② 的典型用例。

9.2 老式 IE 内嵌表单

现象 :窗口类名 Internet Explorer_Server ,页面里有 <input type="password">

引擎行为

  1. 首次进入:WM_HTML_GETOBJECT 建立 ie_root_
  2. 移动鼠标:accHitTest 命中 role=password → get_accValue
  3. 移过工具窗:路径 2b 仍可能更新;
  4. 移到 IE 外普通 Edit:clearIeSession 后改走路径一。

9.3 Chrome / Edge 里的网站密码框(读不到)

原因(分层说明)

  1. 窗口类名是 Chrome 自有类,在黑名单内;
  2. 网页输入发生在 渲染进程 的独立 surface 里,不是 Win32 Edit
  3. 密码框由 Blink 绘制,不响应 WM_GETTEXT
  4. 现代浏览器对跨进程 accessibility 也做了严格隔离。

结论 :不是「工具偷懒」,而是架构上就没有经典 Win32 密码缓冲区可读。应用内密码框、Win32 原生对话框才是主战场。

9.4 以管理员运行的程序

若目标 高完整性 (管理员),工具 普通用户 运行 → SendMessage 返回 ACCESS_DENIED → 界面 Denied

解决办法 :让工具与目标同级权限(都普通或都管理员),而非绕过 UIPI------那是系统故意设计的安全边界。

9.5 ComboBox 回归靶场

项目内 ComboBoxPickerTest 提供多种 ComboBox 变体(含 ES_PASSWORD 子 Edit、ComboBoxEx32),用于验证:

  • 点在 Edit 区 → ComboBox · Edit
  • 只读列表 → ComboBox · CB_GETLBTEXT

便于开发回归,也帮助理解「下拉框里藏着一个 Edit」这一结构。


十、能力边界与安全观

10.1 通常可以读取

  • 标准 Win32 Edit 密码框、普通 Edit
  • EM_SETPASSWORDCHAR 型掩码(Xshell 等)
  • 对话框、终端连接窗内的子 Edit
  • 可编辑 / 只读 ComboBox
  • IE/Trident 宿主内 <input type="password">(MSAA)

10.2 通常无法读取

  • Chromium / Firefox 等现代浏览器内网页密码框
  • WPF PasswordBoxQt 自绘、纯 UWP
  • 与工具 UIPI 不一致的高权限进程
  • 仅显示占位符、内部无明文的自绘控件
  • 服务端从不把明文放进 UI 的场景

10.3 合规使用

适合在您有权操作的设备与软件 上:核对已输入密码、运维排障、学习 Win32 机制。

不适合 :未授权访问他人设备、绕过他人系统的访问控制。

本工具不能替代 KeePass、Bitwarden 等正规凭据管理。


十一、常见误解 FAQ

Q:读到明文是不是说明网站加密没用?

A:不是。HTTPS 保护的是传输过程 ;密码进浏览器后要在输入框里暂存明文才能提交,读的是本机 UI 层,与 TLS 无关。

Q:会不会偷偷上传密码?

A:实现上数据流止于本机界面;是否复制到剪贴板由您点击决定。若您自行改源码或装不可信构建,则另当别论------请使用可信来源。

Q:和「浏览器保存密码后查看」有何不同?

A:浏览器保存的是持久化凭据存储 (常加密);本工具读的是当前这次填在控件里的字符串,不写库。

Q:为什么有时显示「已进入 IE 区域」但没有字?

A:鼠标在 IE 控件上但未对准 password/text 角色元素(例如点在空白处),状态为 IeHint,继续移到输入框即可。

Q:120 字符够用吗?

A:对绝大多数密码框足够;极长 token 可能被截断------这是与经典工具一致的上限设计。

Q:松开鼠标后目标窗口会变吗?

A:ES_PASSWORD / EM_SETPASSWORDCHAR 路径都会在读完后恢复原样式或掩码字符;正常应无可见副作用。

Q:Linux / macOS 呢?

A:当前引擎仅实现 Win32;非 Windows 平台 probeAt 返回「仅支持 Windows」。


附录 A:路径与状态对照总表

条件 策略 成功标签 失败状态
Edit + ES_PASSWORD 样式绕过 Win32 Edit · ES_PASSWORD Denied / Empty
Edit + 掩码 char EM_SETPASSWORDCHAR Edit · EM_SETPASSWORDCHAR Denied / Empty
Edit 普通 WM_GETTEXT Win32 Edit · WM_GETTEXT Denied / Empty
Internet Explorer_Server MSAA IE · MSAA IeHint
ComboBox 可编辑 CB_GETEDITCONTROL → Edit ComboBox · Edit Unsupported
ComboBox 只读 CB_GETLBTEXT ComboBox · CB_GETLBTEXT Unsupported
Chrome / Firefox / UWP 类 黑名单 --- Unsupported
高完整性目标 --- --- Denied

结语

星号密码查看器的原理可以浓缩为:

密码框里的字一直在,* 只是画出来的;在用户主动拾取的前提下,用 Windows 公开 API 把这段字读到自己窗口里。

读懂 Win32 的「显示与存储分离」,就读懂了这类工具 90% 的边界:能读的是经典控件缓冲区,读不了的是现代浏览器与自绘 UI

若您想亲手试用,请参阅 《功能介绍》。若希望继续深入,可在 Microsoft Learn 查阅 ES_PASSWORDWM_GETTEXTIAccessibleCB_GETEDITCONTROLUIPI 等主题。