简介FindPass 是一份面向 Windows 系统安全研究与口令取证学习者的工具资料包针对 Windows NT4 与 Windows 2000 平台利用管理员权限获取 SeDebugPrivilege 后打开 winlogon 进程在其内存空间中匹配明文存放的域名与用户名定位紧随其后的编码口令再调用 RtlRunDecodeUnicodeString 还原登录密码Win2K 下因哈希字节未随密文保存改用 0x00 至 0xFF 逐值尝试并以可见字符校验结果。压缩包共 5 个文件、约 106KB包含 FindPass.exe 主程序、pulist.exe 进程枚举辅助工具、FindPass.cpp 完整源码、一份 readme 说明页和一张运行效果截图源码中的中文注释对关键偏移与 API 调用作了标注。目前已有 796 人学习下载。读者可从中梳理跨进程内存读取、PEB 遍历、特征串匹配与口令解码的完整链路并基于源码完成编译调试与二次改造。1. 密码查询工具 findpass本地凭据找回这件事到底怎么落地findpass这个名字在检索里出现频率不低但多数人对它的期待是错的。它不是“一键破解邮箱密码”的神器也不是能远程读取别人账号的黑客工具。它属于本地凭据查询这一类小工具把浏览器保存的登录信息、系统里存储的凭据条目、配置文件里残留的明文口令找出来用于自己机器上的账号找回、迁移、清点。真正需要它的场景很朴素换了新电脑旧机器上保存的一堆站点账号想不起来接手的测试环境里配置文件散落着数据库口令需要统一登记做安全自查时想看清自己机器上到底存了多少明文密码。适合运维、测试、安全自查的从业者也适合普通用户找回自己的账号。翻车最多的情况是把这类工具当成攻击手段去用方向从一开始就错了。2. findpass 的工作边界它查什么、凭什么能查到在动手之前必须先弄清楚一件事findpass之所以能“查出密码”不是因为有什么神奇算法而是因为很多软件在本地保存凭据时本身就留了可逆的痕迹。理解这一点才知道什么情况下它有效、什么情况下它只会返回空结果。2.1 本地凭据的四种典型存放形态常见的凭据落地方式大致分四类findpass这类工具的价值就在于把这四类一网打尽。第一类是浏览器保存的登录信息。主流浏览器会把账号密码存进一个 SQLite 数据库文件密码字段用系统级密钥加密过。这类数据的可恢复性取决于系统账户是否可用只要当前登录用户和存储时是同一个解密链条就是通的。第二类是系统凭据管理器。Windows 有凭据管理器Linux 有钥匙环一类机制macOS 有钥匙串。这些位置存的条目往往被应用直接引用工具读到的就是应用能用到的。第三类是配置文件里的明文或弱编码口令。数据库连接串、FTP 客户端配置、SSH 工具配置、各类客户端软件的 ini/json/xml 配置这类东西散落在用户目录里被大量工具忽略却恰恰是最高频的泄露源。第四类是应用程序自定义的存储格式。有些软件自己造了个二进制文件或注册表项来存口令做一层简单的异或或固定密钥加密。这类东西没有通用解法只能针对具体软件单独处理。提示判断一个目标值不值得查先看它属于哪一类。前两类依赖当前用户上下文后两类依赖文件是否还在。搞错归类后面的命令全是白跑。2.2 选型理由为什么不直接用浏览器自带的导出有人会问浏览器不是自带密码导出功能吗为什么还要额外的工具。这里有个实际差别。浏览器导出通常只覆盖它自己那一份数据而且新版浏览器导出往往要求再验证一次系统密码批量导出还会弹确认。当你面对的是“十几种软件、几十个配置目录”的清点任务时逐个打开软件去点导出效率极低。findpass这一类的思路是扫目录、按已知格式解析、统一输出。它的优势不在单点能力而在覆盖面和一键式批处理。代价是它对每个软件的支持都是硬编码的格式适配软件改了存储格式它就可能失效。这个取舍必须提前说清楚否则用起来会怀疑工具本身有问题。2.3 环境准备与最小可用验证先把运行环境备齐。多数实现是 Python 写的依赖集中在几个解析库上。建议用独立虚拟环境避免污染主环境。# 建独立环境Python 3.8 以上即可 python -m venv fp-env source fp-env/bin/activate # Windows 用 fp-env\Scripts\activate # 装解析 SQLite、读取系统凭据、解析二进制所需的依赖 pip install pycryptodome pywin32 sqlite-utils这三行里pycryptodome负责加密字段的解密原语pywin32在 Windows 下访问凭据接口sqlite-utils用来快速校验数据库结构。Linux 下pywin32装不上是正常的跳过即可不影响配置文件扫描部分。装完先跑最小验证确认工具本身能起来、能定位数据源再谈结果。# 只做数据源探测不输出任何凭据内容 python findpass.py --scan-only --verbose # 输出示例脱敏说明 # [] browser profile found: /home/user/.config/.../Login Data # [] config dirs scanned: 37 # [-] credential store: skipped (platform not supported)--scan-only是关键参数它只报告找到了哪些数据源、哪些能解析、哪些跳过不打印任何敏感值。第一次上手我一般强制先跑这个模式确认扫描范围符合预期再放开输出。--verbose会打印每个被跳过的路径和跳过原因排查“为什么没查到”时全靠它。如果--scan-only报出的数据源数量是 0问题基本不在工具而在扫描根目录设错了。多数实现默认扫当前用户目录用--root显式指定能立刻验证。# 显式指定扫描根目录验证路径是否被正确识别 python findpass.py --scan-only --root C:\Users\someuser --verbose # 缩小到单个应用目录定位某个软件为何未命中 python findpass.py --scan-only --root C:\Users\someuser\AppData\Roaming\SomeClient--root接受绝对路径Windows 下注意引号路径里有空格不引会被 shell 拆开这是新手第一个翻车点。把范围缩到单个应用目录是排查“某个软件明明存了密码却没结果”的最快手段——如果缩到这个目录还是 0 个数据源说明该软件的存储位置不在这个目录下或者格式不被支持跟解密能力无关。3. 完整跑通一次查询从参数到结果输出上一章解决的是“数据在哪、能不能碰”这一章解决“怎么把它真正读出来、结果怎么用”。整个流程分三步定范围、配输出、校验结果。3.1 参数怎么设范围、类型、输出三组开关把参数按用途分组记比死记命令行好用得多。参数组常用参数作用常见误区范围控制--root--depth限定扫描起点与递归深度深度设太大导致扫到系统目录速度骤降类型过滤--type browser,config只处理指定类别类型名写错时静默跳过不报错输出控制--format json--out决定结果格式与落盘位置直接打印到终端敏感信息进 shell 历史调试--scan-only--verbose只探测不输出、打印跳过原因全程开着 verbose日志里混入敏感片段--depth的默认值通常是 3 到 5 层。调到 10 以上会让扫描时间成倍增长而多数应用的凭据文件都在 4 层以内。--type的取值必须和工具文档一致写错不会报错只会安静地什么都不做这是最容易被误判成“工具失效”的地方。输出一定要落盘不要只在终端看。# 完整查询结果写文件避免敏感信息留在 shell 历史 python findpass.py \ --root $HOME \ --depth 4 \ --type browser,config,credential \ --format json \ --out ./fp-result.json # 查看结果条数与分类统计不直接看明文 python -c import json d json.load(open(./fp-result.json)) print(total:, len(d)) from collections import Counter print(Counter(x.get(source_type) for x in d)) 这段的关键在最后那个统计脚本先看总数和分类分布判断结果是否符合预期再决定要不要打开看具体条目。直接cat整个 JSON 是把敏感信息往终端和滚动缓冲里塞属于习惯性失误。--out指向的目录要确认有写权限失败时工具往往只给一句笼统的写入错误。3.2 单类查询以浏览器凭据为例的完整链路浏览器这类是最典型的链路也最完整适合作为理解整个机制的样本。它的处理分四步定位数据库、读出加密字段、取系统密钥、解密。import sqlite3, json, base64 # 1. 定位登录数据库路径随浏览器和平台不同需按实际情况调整 db /path/to/profile/Login Data con sqlite3.connect(ffile:{db}?modero, uriTrue) # 只读打开避免锁库 # 2. 只取必要字段不 select * cur con.execute( SELECT origin_url, username_value, password_value FROM logins ) rows cur.fetchall() print(条目数:, len(rows)) # 3. 加密字段是二进制先取出原始字节解密逻辑单独处理 for url, user, enc in rows[:3]: print(url, user, len(enc) if enc else 0) con.close()逻辑上有三处值得说明。第一用modero只读打开因为浏览器运行时数据库是锁着的可写打开会直接失败只读模式在多数情况下能绕过锁。第二SELECT明确列名而不是*既避免读到不需要的字段也让不同版本的字段增减不至于让脚本整体崩掉。第三打印时用len(enc)而不是直接输出密文先确认字段确实有内容再去处理解密避免把“字段为空”和“解密失败”混为一谈。解密这一段依赖系统密钥跨平台差异很大工具内部通常做了分支。自己写脚本验证时要注意Windows 下密钥来自系统级加密接口Linux 下取决于钥匙环是否解锁macOS 下走钥匙串。当前用户上下文不对解密一定失败报错通常是填充错误或密钥长度为 0看到这类报错先查上下文别怀疑算法。注意解密出来的结果只应写进受控文件并在用完后删除不要放进版本库、云盘同步目录或聊天记录。这个习惯比工具本身重要。3.3 配置文件类最高命中率、最容易被忽略的一类实际清点里配置文件类的命中率往往高于浏览器类因为它不依赖解密只要文件在、格式认得就能直接读出来。典型目标是数据库连接串、客户端配置、部署脚本里的硬编码口令。# 先按文件名特征筛出候选文件缩小范围再逐个看 find $HOME -maxdepth 4 -type f \ \( -name *.ini -o -name *.conf -o -name *.env -o -name config*.json \) \ -size -2M 2/dev/null | head -50 # 在候选文件里搜常见口令键名只输出文件名和行号不直接打印值 grep -rniE (password|passwd|pwd|secret|token)\s*[:] \ --include*.ini --include*.conf --include*.env \ $HOME 2/dev/null | cut -d: -f1,2 | head -30第一条命令用-maxdepth和-size双重限制避免扫进大文件和深目录2/dev/null压掉权限报错噪音。第二条用cut只保留文件名和行号是刻意的先拿到位置清单再逐个确认而不是把一堆明文口令刷满屏幕。这个“先定位后取值”的顺序是我在多次误操作之后固定下来的习惯。如果grep结果里出现大量打包产物或依赖目录里的匹配加--exclude-dir排掉。# 排除依赖与构建目录减少噪音 grep -rniE (password|passwd|pwd)\s*[:] $HOME/projects \ --exclude-dirnode_modules --exclude-dir.git --exclude-dirdist \ --exclude-dirvenv 2/dev/null | cut -d: -f1,2--exclude-dir可以写多个顺序无关。这一步能把结果量压掉一大半剩下的基本都是有价值的真实配置。3.4 结果校验怎么确认查到的不是脏数据拿到结果不等于拿到正确结果。常见的脏数据有三类过期条目站点已改密码本地还是旧的、重复条目同一账号在多处存储、误报键名叫 password 但值是占位符。校验的最小做法是抽样验证加规则过滤。import json, re d json.load(open(./fp-result.json)) bad re.compile(r^(x|\*|changeme|your_?password|todo|none|null|)$, re.I) clean [x for x in d if x.get(password) and not bad.match(x[password].strip())] print(原始:, len(d), 过滤后:, len(clean)) # 按站点归并同站点保留最后一条通常是最新 seen {} for x in clean: seen[x.get(origin, )] x print(去重后:, len(seen))bad这个正则覆盖了占位符和空值两类误报re.I让匹配不区分大小写。按站点归并时“保留最后一条”依赖结果的输出顺序如果工具不保证顺序就得改成按时间字段排序后再取。这一步不做后面拿着过期密码去登录会白折腾很久。4. 避坑与常见问题五条血泪记录这一类工具用起来翻车点集中且多数不是工具的问题。下面五条是按出现频率排的。4.1 查到一堆结果登录全是错的现象结果文件里有几十条记录逐条试下来几乎都登不上。原因本地存的密码是历史快照站点改密码后本地不会同步更新尤其是不走浏览器同步的客户端软件。另外同一个站点可能存在多条记录你取到的是旧的那条。解决先按站点去重并保留最新记录见 3.4 的归并逻辑再抽样验证而不是全量试。同时明确一点本地凭据只能证明“曾经用过这个密码”不能证明“现在还能用”。把它当线索清单用别当权威密码本。4.2 浏览器数据库读不出来报 database is locked现象脚本连接登录数据库时直接抛锁库错误。原因浏览器正在运行持有数据库的写锁。SQLite 的锁机制在写事务期间会阻止其他连接。解决两种路子。一是完全退出浏览器再读二是用只读 URI 打开file:...?modero多数情况下能绕开写锁读到已提交的数据。不要用复制文件的方式绕过浏览器运行期间复制可能拿到不一致的副本读出来是残缺的。4.3 解密报错密钥长度为 0 或填充错误现象前面步骤都正常到解密环节报密钥为空、填充校验失败。原因解密依赖当前登录用户的系统上下文。用管理员权限另开一个会话运行、或者换了个系统账户运行密钥链就对不上。容器或远程会话里尤其常见。解决回到存储凭据时的那个用户会话里运行别用提权终端。如果必须在别的上下文跑明确告知自己这部分数据在当前上下文中不可恢复不要反复改代码试算法——问题不在算法。4.4 扫描很慢或者扫到一半卡死现象--scan-only跑了几分钟没结束或者卡在某个目录不动。原因扫描根目录设到了系统盘根或包含大量文件的目录递归深度又开得很大或者遇到了特殊文件管道、设备文件、超大日志。解决把--root收到用户目录--depth控制在 4 以内配上-size类限制排掉大文件。卡在某个目录时用--verbose看最后打印的路径基本能定位到具体文件把它加进排除列表。4.5 结果文件被同步走了现象查完忘了清理结果文件躺在云盘同步目录或项目仓库里被同步或提交了。原因--out图省事指向了默认工作目录而这个目录恰好被同步工具或版本控制覆盖。解决--out固定指向一个专用临时目录用完即删。项目目录里加.gitignore兜底不够因为同步工具不认这个。这条最不起眼后果却最重我现在的做法是输出路径永远显式写全绝不用相对路径。5. 进阶把一次性查询变成可复用的清点流程单次跑通只是开始。真正省时间的是把这件事固化成一套可重复的清点流程换机器、换环境都能直接跑并且结果可对比。5.1 用配置文件固化常用组合每次手敲一长串参数早晚会敲错一个把范围放大。用一份配置把常用组合定下来。# fp-profile.yaml default: root: ~ depth: 4 type: [browser, config] format: json out: /tmp/fp/result-{date}.json scan_only: true # 默认只探测显式改 false 才输出值 strict: root: ~/projects depth: 2 type: [config] exclude_dirs: [node_modules, .git, dist, venv]把scan_only的默认值设成true是个刻意的设计默认安全要输出值必须显式改。这和很多工具的默认行为相反但在这个场景下默认不打印敏感值比默认打印更合理。{date}占位符让每次结果独立成文件方便前后对比。5.2 两次清点做差集定位新增与遗留清点的真正价值在对比。换机器、清理配置、上线前自查都需要知道“这次比上次多了什么、少了什么”。import json, sys def load(p): return {(x.get(origin,), x.get(username,)): x for x in json.load(open(p))} a, b load(sys.argv[1]), load(sys.argv[2]) new b.keys() - a.keys() gone a.keys() - b.keys() print(新增:, len(new), 消失:, len(gone)) for k in list(new)[:10]: print( , k[0])用(origin, username)做键而不是只用站点是因为同站点多账号很常见只用站点会把它们合并掉。这个差集脚本我一般在两个时间点各跑一次新增项重点看——新出现的凭据往往意味着新装的软件或新配的环境是清点的重点对象。5.3 一条固定习惯最后说个不是技术的东西。这类工具最大的风险从来不是查不到而是查到了之后没管住。从那以后我每次跑完不管结果多少都强制走一遍同一个收尾结果文件确认内容、按需处理后立即删除、临时目录清空、检查一遍没有落在同步目录里。这个流程写在脚本最后和查询本身绑在一起不靠记忆。比如在输出脚本末尾加一句shutil.rmtree(/tmp/fp, ignore_errorsTrue)让它成为默认动作而不是额外步骤。工具能查到什么取决于环境能不能安全收场取决于习惯。希望帮到你。本文还有配套的精品资源点击获取