简介: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(f"file:{db}?mode=ro", uri=True) # 只读打开,避免锁库 # 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()逻辑上有三处值得说明。第一,用mode=ro只读打开,因为浏览器运行时数据库是锁着的,可写打开会直接失败;只读模式在多数情况下能绕过锁。第二,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-dir=node_modules --exclude-dir=.git --exclude-dir=dist \ --exclude-dir=venv 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:...?mode=ro),多数情况下能绕开写锁读到已提交的数据。不要用复制文件的方式绕过,浏览器运行期间复制可能拿到不一致的副本,读出来是残缺的。
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_errors=True),让它成为默认动作而不是额外步骤。工具能查到什么取决于环境,能不能安全收场取决于习惯。希望帮到你。
本文还有配套的精品资源,点击获取