简介星号密码查看器.zip 内含一款可运行在 Windows 环境下的密码显形工具面向需要读取登录窗口中被星号遮挡的密码的开发人员、系统管理员及学习者。它通过识别窗口类名与句柄对目标控件上的星号内容进行提取便于密码找回、测试输入或代码调试。压缩包体积仅 384KB共 4 个文件主体为可直接运行的 exe 程序另附 htm 格式的下载说明、txt 格式的使用说明以及一个 url 格式的互联网帮助快捷方式能帮助用户快速上手并解决使用中遇到的问题。目前已有 392 人浏览学习适合对 Windows 底层机制和系统编程感兴趣的入门者作为案例参考。从资料中可学习到 FindWindow、GetWindowText 等 WinAPI 的使用思路理解窗口句柄、类名在 GUI 自动化中的意义同时也能获得一个直接可用的密码查看器方便日常维护和应急场景。需要注意该工具仅应在获得授权的情况下使用避免触碰隐私和法律红线。1. 星号密码查看器一个被误解的老工具能解决什么问题前阵子某同事找我说某公司网盘客户端一直勾着“记住密码”换了电脑要重新登录密码死活想不起来。这种事几乎人人都遇到过密码框里一排星号你想看它后面到底是什么系统却不给任何入口。星号密码查看器解决的就是这个问题——它不是黑客工具不碰网络、不碰数据库只向目标密码框所在的控件发一条WM_GETTEXT消息请控件自己把明文交出来。显示层拦截了星号的绘制但文本消息接口没有拦截这是 Windows 兼容性的老设计。这篇我按自己实际折腾过的路径来写先讲清楚密码框的存储与显示为什么分离再给一个十几行的 Python 最小脚本直接跑通然后封装成带界面的小工具并打包接着是读不到密码时的五个典型踩坑案例最后把“读密码”接进自动化流水线。适合三类人运维要做客户端自动化、测试要写 UI 脚本、普通用户要找回自己记住的密码。唯一的前提是只读自己设备或合法授权的目标别拿它去做不该做的事。2. 原理先行密码框的星号只是一层“显示掩护”不是加密很多人第一次听说这个工具时会下意识觉得它用了什么“内存扫描”“Hook 拦截”的黑科技。其实 Windows 的密码框远没有想象中神秘它只是一个带了特殊样式的普通控件星号只是绘制阶段的替换结果。2.1 密码框的真实身份一个开了 ES_PASSWORD 样式的 Edit 控件在 Win32 体系里你看到的登录框、密码输入框绝大多数是标准 Edit 控件类名就叫Edit。文本框和密码框是同一个控件类唯一区别是创建时有没有带ES_PASSWORD样式值为 0x0020。带了这个样式系统在重绘控件时就把真实字符用密码字符替代默认是圆点具体是什么可以通过EM_GETPASSWORDCHAR消息查出来。关键点在于明文一直存在控件内部的缓冲区里也就是目标进程的堆内存中。星号处理发生在绘制阶段不改动内部存储。这个设计意味着只要你能拿到这个控件的句柄就有机会把明文读出来。先给一段用来“验明正身”的代码判断某个窗口是不是密码框以及它用的是什么替代字符import ctypes from ctypes import wintypes user32 ctypes.WinDLL(user32, use_last_errorTrue) GWL_STYLE -16 ES_PASSWORD 0x0020 WM_GETPASSWORDCHAR 0x00D4 def is_password_edit(hwnd): style user32.GetWindowLongPtrW(hwnd, GWL_STYLE) return bool(style ES_PASSWORD) def password_char(hwnd): code user32.SendMessageW(hwnd, WM_GETPASSWORDCHAR, 0, 0) return chr(code) if code else NoneGetWindowLongPtrW取出的是控件样式的完整位标志用ES_PASSWORD做与运算即可判断。WM_GETPASSWORDCHAR返回替代字符的 Unicode 码点大多数情况下是 U25CF实心圆点。这个判断在后面枚举子控件时会用到能帮你从一堆文本框里精确筛出真正的密码框。注意GetWindowLongPtrW在 32 位 Python 里也能工作但我在项目里统一用 64 位 Python少踩一个数据宽度相关的坑。2.2 消息循环里那条关键路径WM_GETTEXT 为什么能拿到明文Win32 窗口消息本质上是一次跨模块的函数调用。SendMessage是同步的发送方阻塞等待控件处理完消息并返回结果。WM_GETTEXT的语义是“把控件中的文本复制到调用方缓冲区”密码框控件处理这条消息时并不区分自己有没有ES_PASSWORD样式直接把内部缓冲区里的真实文本交出去了。这个行为不是漏洞而是延续了几十年的兼容性设计。辅助功能工具、自动化测试工具、屏幕阅读器都要靠这套消息接口干活系统如果在这里拦截大量老软件会直接失灵。所以从 API 层面看读密码框明文就是一条标准路径整个过程不需要注入、不需要 HookWM_GETTEXTLENGTH 0x000E WM_GETTEXT 0x000D length user32.SendMessageW(hwnd, WM_GETTEXTLENGTH, 0, 0) buf ctypes.create_unicode_buffer(length 1) got user32.SendMessageW(hwnd, WM_GETTEXT, length 1, buf) print(buf.value)先用WM_GETTEXTLENGTH拿长度再创建对应大小的缓冲区length 1多出来的一个位置放字符串终止符。create_unicode_buffer是 ctypes 里处理宽字符串的推荐方式它会自动处理编码转换。这里有个细节WM_GETTEXT的wParam传的是缓冲区能容纳的最大字符数不是字节数lParam传缓冲区指针返回值是实际复制的字符数。EM_GETLINE之类的消息在密码框上是无效的认准WM_GETTEXT就行。2.3 选型我为什么选 Python ctypes 而不是 C#做同一个工具C# 的SendMessageP/Invoke 也很方便甚至写出来更“正宗”。但我实际落地的选择是 Python ctypes原因很实际整个逻辑只有几十行Python 改起来快不依赖 Visual Studio 工程而且可以随时用print把句柄、长度、样式值打出来调试。C# 更适合做成长期分发给普通同事的正式小工具打包体积小、双击即用。方案上手速度运行时依赖打包体积适合场景Python ctypes快十几行跑通Python 解释器或 PyInstaller 打包较大通常 20-60MB运维脚本、自动化测试、快速验证C# WinForms中等需 VS 工程.NET 运行时较小几 MB分发给普通同事的固定工具如果你只是自己用、自己调试或者要把读密码这一步接进自动化流水线Python 版本足够。如果你要做一个同事双击就能用的“傻瓜工具”再考虑用 C# 重写一版也不迟底层都是那一条消息迁移成本很低。3. 用 Python ctypes 在本地跑通最小命令原理说完就动手。整个工具拆成三步找到目标窗口、在窗口里找到密码框控件、把密码文本读出来。下面每一步都给完整代码和参数说明照着敲就能跑。3.1 按窗口标题定位FindWindowW 与遍历兜底最直接的定位方式是按窗口标题找顶层窗口。FindWindowW接受两个参数第一个是类名传None表示不限制类名第二个是窗口标题。注意必须用带W后缀的宽字符版本否则中文标题会乱码import ctypes from ctypes import wintypes user32 ctypes.WinDLL(user32, use_last_errorTrue) def find_window_by_title(title): hwnd user32.FindWindowW(None, title) return hwnd实际使用时FindWindowW经常因为标题里含隐藏字符、标题栏动态变化而返回 0。我一般会再写一个遍历兜底用EnumWindows枚举所有顶层窗口逐个读标题做包含匹配。这样哪怕目标软件在标题里加了版本号、用户名也能靠关键词捞到EnumWindowsProc ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p) def find_window_by_keyword(keyword): result [] def enum_callback(hwnd, _): length user32.GetWindowTextLengthW(hwnd) buf ctypes.create_unicode_buffer(length 1) user32.GetWindowTextW(hwnd, buf, length 1) if keyword in buf.value: result.append(hwnd) return True user32.EnumWindows(EnumWindowsProc(enum_callback), None) return resultEnumWindows的回调返回True继续枚举返回False停止。这里用GetWindowTextLengthW先取标题长度避免固定缓冲区截断长标题。这个版本比FindWindowW更可靠代价是慢一点点但几毫秒的差距可以忽略。3.2 枚举子控件锁定真正的密码框找到顶层窗口后密码框通常是它的子控件。用EnumChildWindows遍历所有后代窗口先过滤类名必须是Edit再用上一章的is_password_edit确认带不带ES_PASSWORD样式。双重过滤能有效避开那些普通文本框def find_edit_controls(parent_hwnd): edits [] def child_callback(hwnd, _): class_buf ctypes.create_unicode_buffer(256) user32.GetClassNameW(hwnd, class_buf, 256) if class_buf.value Edit and is_password_edit(hwnd): edits.append(hwnd) return True user32.EnumChildWindows( parent_hwnd, EnumWindowsProc(child_callback), None ) return editsGetClassNameW取控件类名256 字符的缓冲区足够用了标准控件类名长度不会超过这个数。有些自绘界面类名可能是Edit的变体比如加了边框样式的Edit还是同一个类这步过滤基本够用。如果目标窗口里有多个密码框比如“旧密码”“新密码”“确认密码”这个函数会全部列出来后续可以按控件坐标进一步过滤或者直接全部打印出来人工判断。3.3 发送 WM_GETTEXT 把明文取回来现在把读文本封装成一个函数然后把前面的步骤串起来。注意WM_GETTEXT对缓冲区长度为零的控件会直接返回 0所以先查长度能省掉无谓的分配def get_edit_text(hwnd): length user32.SendMessageW(hwnd, WM_GETTEXTLENGTH, 0, 0) if length 0: return buf ctypes.create_unicode_buffer(length 1) got user32.SendMessageW(hwnd, WM_GETTEXT, length 1, buf) return buf.value def get_password_from_window_title(title): hwnd find_window_by_title(title) if not hwnd: print(找不到窗口:, title) return None for edit_hwnd in find_edit_controls(hwnd): pw get_edit_text(edit_hwnd) if pw: print(密码:, pw)create_unicode_buffer(length 1)中的1是给宽字符串终止符留的位置少了这一个字符SendMessageW写入时可能越界。实际运行中如果目标窗口是当前登录窗口且密码框里有内容这段代码会直接打印出明文。多个密码框都非空的话会依次打印顺序一般和控件创建顺序一致。3.4 一行命令跑起来参数化入口手动测试时每次改代码里的标题很烦我把入口改成命令行参数这样对自动化脚本也友好import sys if __name__ __main__: if len(sys.argv) 1: get_password_from_window_title(sys.argv[1]) else: print(用法: python pwview.py \窗口标题\)python pwview.py 某客户端登录窗口这个命令行的好处是能被其他脚本直接调用也可以接到自动化流程里。如果你只是临时看一眼也可以把sys.argv改成input()运行时粘贴标题效果一样。4. 把脚本封装成可用的图形小工具从命令行到双击即用脚本能在命令行里跑通之后下一步是给它一个界面。我自己用命令行就够了但如果你要把它交给不碰终端的同事或者自己也想省点事用 PySide6 套一层壳是最快的路径。4.1 界面骨架输入标题、读密码、显示结果先安装依赖然后写一个最简单的窗口一个输入框填窗口标题一个按钮触发读取一个标签显示结果pip install PySide6import sys from PySide6.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QLineEdit, QPushButton, QLabel ) class PasswordViewer(QWidget): def __init__(self): super().__init__() self.setWindowTitle(星号密码查看器) layout QVBoxLayout(self) self.title_input QLineEdit() self.title_input.setPlaceholderText(输入或悬停获取窗口标题) layout.addWidget(self.title_input) self.btn QPushButton(读取密码) self.btn.clicked.connect(self.read_password) layout.addWidget(self.btn) self.result_label QLabel(结果会显示在这里) self.result_label.setTextInteractionFlags( self.result_label.textInteractionFlags() | 1 ) layout.addWidget(self.result_label) def read_password(self): title self.title_input.text().strip() passwords get_password_from_window_title(title) if passwords: self.result_label.setText(\n.join(passwords)) else: self.result_label.setText(未读到位权限或控件类型问题) if __name__ __main__: app QApplication(sys.argv) win PasswordViewer() win.show() sys.exit(app.exec())界面本身不包含任何取密码逻辑只是把第三章的get_password_from_window_title包装进了按钮回调。setTextInteractionFlags让结果可以被选中复制这是工具类界面一个容易被忽略的细节——不能复制的结果等于没结果。4.2 鼠标悬停自动抓窗口标题让用户手敲标题还是不够方便尤其目标窗口标题很长或带空格时。我在界面里加了一个悬停抓取鼠标移到目标窗口的标题栏上界面自动把标题填进输入框省掉手动拷贝。实现上用QTimer每 200ms 轮询一次鼠标位置再调用WindowFromPoint拿句柄、读标题from PySide6.QtCore import QTimer import ctypes def grab_title_under_mouse(): pt wintypes.POINT() user32.GetCursorPos(ctypes.byref(pt)) hwnd user32.WindowFromPoint(pt) if not hwnd: return length user32.GetWindowTextLengthW(hwnd) if length 0: return buf ctypes.create_unicode_buffer(length 1) user32.GetWindowTextW(hwnd, buf, length 1) title buf.value if title: win.title_input.setText(title) timer QTimer(win) timer.timeout.connect(grab_title_under_mouse) timer.start(200)为什么用轮询而不是鼠标事件Windows 没有暴露全局的“鼠标悬停到某窗口”事件最通用、依赖最少的方式就是定时采样。200ms 的间隔既不卡界面也不会漏掉短暂停留。注意WindowFromPoint返回的是屏幕上该坐标处最顶层的窗口如果鼠标悬在密码框上拿到的可能是子控件此时标题往往为空直接跳过即可。4.3 打包成单文件 exe参数与杀毒误报打包用 PyInstaller命令不长但有几个参数值得说清楚pip install pyinstaller pyinstaller -F -w --uac-admin --clean --name PasswordViewer pwview_gui.py-F是生成单文件 exe-w是去掉控制台窗口GUI 程序不需要黑框--uac-admin是让生成的 exe 默认请求管理员权限--clean清理上次编译缓存。打包前需要确认你用的是 64 位 Python这直接影响后续能不能读到 64 位目标进程的窗口具体原因在下一章讲。注意--uac-admin会让每次启动都弹 UAC 确认框而且杀毒软件对带提权 manifest 的小工具误报率会更高。我自己的习惯是先不打这个参数遇到权限读不到的窗口再重新打包一个提权版本。杀毒误报是这个工具绕不开的痛。ctypes直接调SendMessage跨进程读窗口内容在静态扫描的特征库里非常像敏感行为加上 PyInstaller 打包壳被隔离是常有的事。我一般会在-w之外尽量少加其他混淆参数减小壳的体积和异常特征。给自己的机器用就源码运行给同事分发前先在 VirusTotal 上扫一眼心里有数。5. 避坑与排查读不到密码的5个真实案例读不到密码时大部分问题都集中在进程位数、权限、控件类型这三件事上。下面每条都是我至少遇到过一次的真实场景按现象、原因、解决的顺序写遇到类似问题可以直接对照。5.1 工具是 32 位目标窗口是 64 位SendMessage 直接静默失败现象窗口句柄找到了甚至能枚举出Edit控件但WM_GETTEXTLENGTH返回 0WM_GETTEXT拿到的也是空字符串。原因在 64 位 Windows 上32 位进程向 64 位进程发送窗口消息本就受到架构隔离限制再加上完整性级别隔离UIPI跨位数发消息经常被静默丢弃。工具自己很可能是用 32 位 Python 写的或者打包时用了 32 位解释器。解决用 64 位 Python 重新跑脚本、重新打包。判断方法很简单在 Python 里打印ctypes.sizeof(ctypes.c_void_p)结果是 8 就是 64 位是 4 就得换解释器。发布 exe 给同事时优先构建 64 位版本能少掉一半兼容性问题。提示发布工具时优先做 64 位版本兼容性和稳定性都更好。5.2 目标窗口以管理员运行工具没提权句柄拿得到但消息被系统丢弃现象窗口标题匹配正常控件也能枚举出来但读取结果始终为空。换了一个普通权限的测试窗口立刻就能读到。原因Windows 的完整性级别隔离UIPI禁止低完整性进程向高完整性进程发送消息。目标软件以管理员权限运行完整性级别高你的普通权限工具发的WM_GETTEXT会被系统直接丢弃SendMessage 不报错、只是不生效。解决让工具也以管理员权限运行。一种是右键 exe“以管理员身份运行”一种是用 PyInstaller 的--uac-admin参数打包出提权版本。注意 UAC 提权后整个工具都运行在高完整性级别读取逻辑不用改权限够了消息就能送达。5.3 目标不是标准 Edit 控件枚举不到密码框读了个寂寞现象枚举子控件时类名列表里根本没有Edit或者类名是Edit但ES_PASSWORD样式压根没设置。还有更极端的连子控件都枚举不到。原因新版客户端大量使用自绘 UI比如某个 Electron 套壳、某个 Tauri 程序、Java Swing 界面。这些框架的密码输入框不是原生 Win32 控件星号是框架自己画的明文要么存在框架自己的私有内存里要么就是绘制结果标准消息机制根本够不着。解决先花半分钟确认控件类型别在自绘 UI 上浪费时间。用EnumChildWindows打印出所有子控件的类名亲眼看到没有Edit类名就可以判断这条路走不通。这类程序要读密码得走 UI Automation 的ValuePattern或者直接查它自己的配置存储。血泪经验确认控件类型后再决定方案能省下一整个下午。5.4 中文标题匹配不上A/W API 混用与不可见字符现象窗口标题明明是中文FindWindowW却返回 0。有的脚本里用了FindWindowA而 ANSI 版本在中文标题上会直接乱码匹配永远不会成功。原因两个层面。一是 API 版本A 后缀是 ANSI 版本在处理非 ASCII 字符时按系统默认代码页转换中文环境可能没问题但跨区域部署就出错二是标题字符串可能包含全角空格、零宽空格等不可见字符你看到的标题和系统里存的标题不完全一样。解决统一用W后缀的宽字符 APIPython 的str天生是 Unicode传给FindWindowW不需要额外编码处理。排查不可见字符时打印repr(title)一眼就能看出有没有特殊符号。再不行就用 3.1 节的EnumWindows包含匹配别死磕精确标题。5.5 杀毒与 EDR 实时拦截exe 刚生成就被隔离现象PyInstaller 打包完exe 目录里还热乎着杀毒软件直接隔离或者运行时弹风险提示点允许之后读取结果也是空的。原因跨进程读窗口内容在安全产品眼里是敏感行为静态扫描命中“读取其他进程数据”的特征码加上提权 manifest触发概率更高。这不是误报那么简单而是行为本身就符合监控规则。解决分场景处理。自己用就源码运行别打包要分发先把 exe 加入杀毒软件白名单再发给同事时明确说明用途和原理。这个工具的合规边界很清晰只用于自己设备、自己账号、被授权的系统。任何绕开他人授权读取密码的做法都不该做也不用做技术上也没有必要。6. 进阶技巧把“读密码”变成自动化流水线的一环工具做到这里功能其实完整了。但真正让它发挥价值的地方是嵌进自动化流程里当“取密码”的管道。6.1 最小自测窗口五分钟验证工具本身调试这个工具时我不建议拿真实业务软件反复试容易碰到各种权限和控件问题。更快的办法是自己先建一个目标窗口用标准的密码框验证工具链是否正常import tkinter as tk root tk.Tk() root.title(演示登录窗口) entry tk.Entry(root, show*) entry.pack(padx20, pady10) entry.insert(0, test_pass_123) root.mainloop()python pwview.py 演示登录窗口如果这个窗口能读出test_pass_123说明你本机的 Windows 消息链路没问题后面排查真实目标时就排除了环境因素。这个小窗口也是验证打包后的 exe 是否正常、杀毒是否拦截的最好道具。6.2 读密码 GUI 自动登录的组合姿势有次帮同事处理某内部系统的自动化登录密码被客户端存成了加密配置数据库里也是哈希绕了大半天最后就是从旧版客户端的密码框里把明文捞回来流程才跑通。那次之后我常把这个工具和 GUI 自动化工具拼在一起用import pyautogui import time title 某内部系统登录窗口 pw get_password_from_window_title(title) pyautogui.click(200, 400) # 用户名框坐标按实际界面调整 time.sleep(0.3) pyautogui.write(某用户名) pyautogui.press(tab) time.sleep(0.3) pyautogui.write(pw) pyautogui.press(enter)组合的时候注意两件事一是读密码要在窗口已经加载完成后进行否则密码框还没创建好二是pyautogui的坐标依赖界面布局窗口位置变了就翻车我一般会先用pyautogui.locateOnScreen找控件位置再点击而不是写死坐标。这套组合最典型的应用场景是软件实现了“记住密码”但用户要的是明文或者自动化脚本需要在无人值守时完成登录。读出来的密码拿到手后再按需写进配置、填入表单或做交接记录。这个工具我放在常用目录里平时几乎用不上但真到用的时候能省下小半天。它的核心逻辑就那几十行难的不是读而是知道什么时候该用、什么时候不该用。希望帮到你。本文还有配套的精品资源点击获取