深入解析FindPass:从lsass内存中提取Windows明文凭据
简介这份资源是一个针对Windows NT/2000系统的密码查询工具名为findpass来源于经典的PasswordReminder项目改造。它通过在winlogon进程内存中搜索明文存储的域名和用户名再定位并解码后续的加密口令实现管理员在拥有调试权限的前提下找回本机使用msgina.dll图形登录的账户密码。压缩包共5个文件约106KB包含可执行程序FindPass.exe、配套的pulist.exe进程列表工具、FindPass.cpp源码、说明文档HTML以及一张运行效果截图。源码对研究winlogon认证机制、系统内部API调用和Win2K密文解码算法有较高参考价值。该资源已有793人学习下载适合对Windows底层安全机制感兴趣的系统管理员、安全测试人员或逆向学习者参考实践。1. 先搞清楚 FindPass 解决什么问题从 lsass 内存里捞明文凭据做渗透测试和应急响应的人大概率都遇到过这种场景拿下一台 Windows 主机的控制权想横向移动结果发现手头只有 hash 没有明文密码。想翻配置文件、翻注册表、翻脚本翻半天一无所获。这时候如果系统里还留着明文凭据缓存那 findpass 这类工具就是一条近路——它不像 mimikatz 那样需要加载驱动或用 sekurlsa 模块而是直接扫描 lsass 进程内存把以明文或可逆形式保存的密码串找出来。换句话说它解决的是「用户名密码查询」这个具体诉求一台主机上哪些账号在跑、密码是什么、能不能直接复用去登其他机器。这篇文章不是给你讲攻击思路的而是把 findpass 这个工具的来龙去脉拆开它为什么能查出密码、代码里到底在干什么、实际用的时候参数怎么设、哪些地方容易翻车以及拿到结果后怎么验证。适合正在做内网测试的人也适合蓝队做自查——你总得知道自己的机器上有没有这种「明文口令黑匣子」能被别人翻出来。2. 为什么它能直接查到密码解密原理与核心代码阅读2.1 凭据从哪来lsass 进程与明文缓存Windows 登录认证的核心进程是 lsass.exe它负责处理本地登录、域登录、服务启动等场景下的凭据校验。为了让用户下次登录能更顺畅很多场景下系统会把密码以可逆形式缓存下来比如自动登录、计划任务、服务账号、RDP 保存的凭据甚至某些版本的 DPAPI 保护数据。lsass 进程内存里就保存着这些数据的解密版本至少在认证完成后的短时间内明文密码或可用于解密的密钥都在内存里躺着。findpass 的基本思路就是把这个进程的内存抓出来然后在里面搜。搜什么搜的是密码出现的上下文特征。密码在内存里不会孤零零地出现它通常跟着用户名、域名、特定结构体标记一起存在。所以工具要做两件事第一找到 lsass 进程并读取它的内存空间第二按已知的特征签名去匹配把候选密码区域识别出来。2.2 搜索内存空间的思路签名、偏移与模式这里要理解两类搜索方式。一类是用系统自带调试权限直接调用进程内存读取接口遍历所有可读的内存块逐块扫描另一类是先把 lsass 内存 dump 到文件再离线分析。findpass 的早期实现大多走的是前一种因为它要的是即时结果不想落盘。后一种更适合事后的内存取证。扫描的时候签名表是关键。不同 Windows 版本的 lsass 内部结构体布局不一样找密码的偏移也会变。早期版本里密码前会有固定的 WDigest 标记或 LSA 结构特征后来微软加了 Credential Manager 和 DPAPI 的加密保护明文密码不是每次都出现。所以这工具偶尔会返回空结果不是它坏了而是目标系统上当前没有可捕获的明文凭据。// 核心搜索段示意通过内存块遍历查找带签名的凭据区域 for (MEMORY_BASIC_INFORMATION mbi; ; ) { // 只处理已提交且在用户态地址空间内的内存块 if (mbi.State MEM_COMMIT mbi.Protect PAGE_READWRITE) { // 逐块拷贝到缓冲区后按照特征值匹配 CopyMemory(buf, mbi.BaseAddress, mbi.RegionSize); for (DWORD i 0; i mbi.RegionSize - 4; i) { // 命中已知的用户名字符串首部标记 if (*(DWORD*)buf[i] userMarker) { // 回退固定偏移读取密码字段起始位置 ParseCredential(buf[i], credOut); } } } }这段代码代表了一种常见做法先过滤内存块类型只看可读写区域缩小盲目扫描的范围然后按用户标记去定位再通过偏移读取密码。参数上要注意 PAGE_READWRITE 保护类型不是所有凭据都在这个页面里有些版本会把凭据放到 PAGE_EXECUTE_READ 里所以搜索条件不能写死要留出切换的开关。2.3 关键代码段怎么读结构体定义与偏移表如果你打开 findpass 的源码最先见到的不是主函数而是一堆结构体和版本偏移表。这是因为凭据在 lsass 内部是以结构体链表形式保存的每个 Credential 结构里包含用户名字符串、域名字符串、密码字符串但这些字段的位置在系统更新后会有变动。工具维护一张与系统构建号匹配的偏移表核心循环里就是拿这张表去套。// 凭据结构体定义与版本偏移示例 typedef struct _CREDENTIAL_ENTRY { LIST_ENTRY ListEntry; // 前向/后向链表指针 ULONG UserNameLen; // 用户名长度 WCHAR UserName[MAX_SIZE]; // 用户名字符串 ULONG PasswordLen; // 密码长度 WCHAR Password[MAX_SIZE]; // 密码字符串 } CREDENTIAL_ENTRY, *PCREDENTIAL_ENTRY;这里有个容易误导人的地方结构体里写了 UserName 和 Password 的固定数组实际内存里并非总是连续存放。有些系统版本里密码字符串是指针指向另一块内存区域有些版本里长度字段在前、字符串在后编码还是 UTF-16LE所以扫描匹配时要做字节序转换。看代码时重点看 GetCredential 函数处理的是哪种情况然后对应调整自己的哈希表或特征串。很多人在二次开发时报错都是因为直接按固定偏移读忽略了不同构建版本的差异。3. 实操编译、运行与参数说明3.1 环境准备工具链与目标系统版本用 findpass 之前先想清楚运行环境。工具本身是 C/C 写的在 Windows 上编译最常见的是用 Visual Studio 的命令行编译环境也可以用 MinGW。编译时注意目标平台要跟你要测的系统一致——x64 系统必须编译 x64 版本用 32 位程序去读 64 位 lsass 是读不全的。# Visual Studio 开发者命令行环境下直接编译 cl /O2 /D WIN32_LEAN_AND_MEAN findpass.cpp /Fe:findpass.exe /link user32.lib advapi32.lib这条命令里 /O2 是速度优化对内存扫描这种大循环有帮助/D 定义宏是为了少引 Windows 头文件里用不到的模块/Fe 指定输出文件名链接库 user32 和 advapi32 是读取进程内存和获取调试权限要用的。如果你在 Windows 10 之后的系统上跑旧版代码常会遇到编译过但运行时报错——那是因为没定义 _WIN32_WINNT 宏默认按老版本 API 编译。// 获取 SeDebugPrivilege 权限的标准开头许多工具里都有 if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, hToken)) return ERROR; LookupPrivilegeValue(NULL, SE_DEBUG_NAME, luid); AdjustTokenPrivileges(hToken, FALSE, tp, sizeof(tp), NULL, NULL);这是工具运行的第一步提权到 Debug 权限。没有这个权限OpenProcess 打开 lsass 会返回拒绝访问。注意这是个动态行为不是你有管理员组就一定成功有些环境里策略限制了调试权限的授予。这里提权失败是最高频的报错点错误码 1314 表示权限不足0 表示成功。3.2 命令行参数与标准用法工具编译好后最简单的用法是不带参数直接跑默认查询本机 lsass 内存。也有的版本支持指定 PID 或进程名便于在目标机器上直接指定要扫描的进程。findpass.exe findpass.exe /pid:1234 findpass.exe /user:administrator findpass.exe /out:result.txt/pid 参数用于你已知 lsass 的 PID 但进程名被改的情况/user 可以过滤特定用户名减少输出噪声/out 是把结果写到文件而不是控制台适合结果量大或需要继续脚本处理的场景。实际用的时候我一般先把不带参数的跑一遍确认输出格式和字段再决定要不要加过滤条件。原因是不同编译版本输出字段顺序不完全一样先摸底再规划后续自动化更稳。3.3 输出格式与结果解析findpass 的典型输出长这样 findpass [*] 本机信息: hostnameWIN-E2K8R2 [*] lsass PID: 668 [] 找到 2 条凭据 [] User: Administrator Domain: WORKGROUP [] Password: Th3Pssw0rd! [] User: sa Domain: SQLSERVICE [] Password: SqlAdmin8866输出解析有三个关键点。第一Domain 字段是 WORKGROUP 时代表本地账号是域名时代表域账号横向移动时要区分对待。第二Password 字段里的内容可能是明文也可能是十六进制串十六进制一般是找不到明文但拿到了可逆密文这种情况下要配合解密函数才能还原最终密码。第三进程 PID 是 668 这条信息和后面命令执行时间要记录后续跟审计日志比对时要用。拿到结果后验证步骤不能省。拿到的密码先在原机验证一次确认能用再拿到目标机去试避免被各种密码策略坑。验证方式可以直接用命令行net use \\192.168.1.20\c$ /user:administrator Th3Pssw0rd!这条命令成功了说明凭据有效如果返回 1326说明密码错了或账号被锁定那就要回到内存数据里重新找。注意 net use 本身也会在目标连接时留下记录在做测试时要想清楚时机不是所有验证都适合现场敲。4. 避坑指南权限、杀软、审计与转储方式4.1 现象工具运行毫无输出也没有报错第一次跑 findpass 最常遇到的情况命令执行完屏幕上只有一行提示没有任何凭据输出。第一次接触的人会以为工具失效其实大多数时候是当前会话里没有可捕获的明文凭据。原因有几个系统版本过高微软默认关闭了 WDigest 明文缓存用户登录后已经超过了一段时间凭据被系统自动清掉目标账号根本没有做过交互式登录。解决方式不是反复重跑而是先确认操作系统版本和账号登录状态。查看注册表里 WDigest 是否被显式关闭是优先做的检查项reg query HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest /v UseLogonCredential值为 0 或不存在时说明明文凭据默认不缓存。这种情况下即使拿到内存转储也未必能找到明文密码。所以找密码不只是工具问题还是配置问题。4.2 现象提示 OpenProcess 失败或拒绝访问这个报错出现得非常多表象是工具弹出一行错误然后退出。常见原因有两个当前进程没有管理员权限或者管理员组权限被 UAC 过滤掉了。很多人在管理员账号下双击运行但实际上没以管理员身份启动权限令牌里没有 SeDebugPrivilege。解决以管理员身份重新启动控制台再运行工具。更稳的方式是先把提权代码单独编译成一个服务或计划任务来跑因为有些环境里 SeDebugPrivilege 不允许从交互式令牌中获取。还有一种情况是系统上开了 Credential Guardlsass 进程被虚拟化隔离普通读取根本拿不到内存内容。当你确认权限没问题却依然 OpenProcess 失败十有八九是 Credential Guard 在拦路这属于硬边界不是工具能绕过的趁早换思路。4.3 现象扫描结果和任务管理器里看到的凭据项目对不上有时候 findpass 输出了一条用户名但密码是空字符串或者长度不对。这说明解析结构体时偏移没对齐。同一台机器上如果系统打了新补丁lsass 内部结构可能发生变化旧版本的 findpass 会跟着翻车。这种场景下最直接的做法是不要只靠结构体解析而是直接做模式搜索在内存中找到用户名后往后找长度为 12~64 的 Unicode 字符串把周围可能的密码上下文都拉出来再人工确认。更好的选择是把内存转储下来然后配合其他分析工具做交叉验证procdump.exe -ma lsass.exe lsass.dmp这样转出来的完整内存文件可以反复分析不必在目标机上来回试探。注意转储文件本身是敏感数据分析完要立刻删除或加密保存不能留在目标机器里。4.4 现象工具被杀软或 EDR 拦掉内存扫描类工具的特征码很早就被各大安全产品盯上了编译出来不改签名直接传上去很容易被查杀。很多情况下你刚把文件落地还没运行杀软就弹窗清掉了。这是因为工具里调用的 GetProcAddress、OpenProcess、AdjustTokenPrivileges 这几组 API 序列本身就是敏感行为特征跟工具本身的字符串特征叠加触发点很多。解决的方式是把工具做成白名单环境里的免检模式或者用 PowerShell 反射加载的方式在内存里执行文件不落地。但这样做的本质是规避检测要考虑合规风险。蓝队视角下这个现象恰好可以作为检测点关注进程是否调用 OpenProcess 打开 lsass并且请求了 PROCESS_VM_READ 权限这是最典型的凭据窃取行为。4.5 现象拿到密码但拿去登目标机器时提示账号被锁排查这种问题要往回看清楚。第一条确认拿到的密码是不是和当前登录密码同源很多时候 findpass 扫到的是缓存过的旧密码并没有真正被更新。第二条是检查账号锁策略如果目标机开了账户锁定阈值反复试错会导致账号锁定这个锅只能算在操作者头上。解决方式是拿结果后在原机先做一次本地校验确认密码和当前会话匹配对于域账号优先在域内一台不那么敏感的主机上验证对于本地管理员组账号验证后立刻清除相关登录会话避免留下过多痕迹。要养成的习惯是每拿到一条凭据就记录来源进程、扫描时间、当时系统版本再用独立方式验证一次。两边对上了才敢拿去横向。5. 进阶玩法批量查询、自动化与结果落地5.1 批量脚本组合 findpass单台机器手动跑一次很简单但内网里几十台机器逐台去跑就不现实了。常见做法是写一个批处理脚本把 findpass 分发到多台机器上执行再把结果统一回收到一个目录然后集中解析。echo off for /f %%i in (serverlist.txt) do ( psexec \\%%i -u admin -p password -c findpass.exe -out:%%i.txt copy /y \\%%i\C$\temp\%%i.txt result\ )这段脚本里的 psexec 是远程执行工具-c 参数把本地文件复制到远程再执行-out 让每台机器把结果输出到独立文件避免互相覆盖最后统一拷贝回来。脚本里要特别注意远程执行时 findpass 的权限还是受限的psexec 默认创建的进程未必有 SeDebugPrivilege所以这里实际上要先远程创建一个计划任务以 SYSTEM 或高权限令牌来启动 findpass否则会大规模地「运行成功但没有结果」。5.2 与转储分析比对有时候 findpass 在目标机器上拿不到结果但你可以把内存转储拷回本地慢慢分析。常见做法是先跑一个扫描工具找到候选区域再用内存取证工具做深度解码。import re def extract_password(blob): pattern rb(?Ppassword[^\x00-\x1f\x7f-\xff]{6,32})\x00 matches [m.group(password).decode(utf-16le, ignore) for m in re.finditer(pattern, blob)] return matches这段 Python 代码的核心作用是从内存转储里提取出符合长度 6~32 的 Unicode 字符串作为密码候选。为什么不直接用 findpass因为转储文件里有很多二进制噪声直接按结构体解析容易漏正则过滤能找出更多看似可疑的串再人工筛选。参数上长度下限设置成 6 是因为多数系统的密码策略最短就是 6 位你想提高精准度可以调到 8但代价是漏掉短密码。5.3 防御侧验证自查本机是否存在明文缓存蓝队视角下findpass 这类工具最大的价值是帮你自查如果攻击者拿到了你机器的系统权限能不能直接翻出明文密码。你可以用一条命令快速确认本机 WDigest 是否启用了明文缓存reg query HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest /v UseLogonCredential和前面第 4 章那条命令一样但用途完全不同。这次不是排查工具问题而是确认攻击面。如果返回值为 1说明系统仍允许 WDigest 明文缓存建议尽快关闭并重启相关服务。另一个自查点是检查 lsass 进程是否被非系统进程打开了调试句柄这可以用进程句柄查询工具看凡是出现对你机器上 lsass 带有 PROCESS_VM_READ 权限的句柄都是异常信号。把这个行为写成告警规则比单纯封禁文件哈希更有效。6. 验证方法与技巧从一次实测到一套检查清单拿 findpass 的源码和目标机器做过一次完整验证后我发现这套流程能不能跑通关键在于你先定义清楚「成功」的标准。不是看到一长串密码就叫成功而是密码经过了原机校验、目标机可用、且不引起告警才算真正拿到了可复用的凭据。我自己的验证习惯分四步走。第一步在隔离环境里用同一版本的系统镜像部署目标机编译工具后运行确认输出格式和密码解码逻辑是通的第二步在镜像里制造一个已知密码的账号交互式登录一次后运行工具看抓出来的结果和已知密码是否一致第三步把得到的凭据用网络登录方式验证确认能登录到目标服务第四步也是容易被忽略的一步检查事件日志里有没有出现异常登录记录把工具本身的行为痕迹控制在可解释的范围内。wevtutil qe Security /q:*[System[(EventID4624)]] /rd:true /c:1 /f:text这条命令是查最近一次成功登录事件用来验证你刚才用 findpass 拿到的密码登其他机器时系统侧是否正常记录了这次登录。如果你做的是一次合规测试这条记录就是你的操作凭证如果是应急响应分析这条记录能告诉你攻击者是否也用了同样的手段。从那以后我每次拿到这类凭据搜索工具都会强制走一遍「编译 → 隔离环境验证 → 目标机单次执行 → 日志核对」的流程而不是直接往目标机上一丢就跑。工具本身是死的你对它的边界和输出有多熟悉它才有多大的实际价值。希望这篇文章能帮你在下一次遇到「用户名密码查询」需求时少走一段弯路。本文还有配套的精品资源点击获取