简介:NTPWEdit 0.7 是一款通过直接修改 SAM 文件来重置或删除本地系统账户密码的开机密码更改工具,本资源为其完整 C++ 源码包,面向需要研究 Windows 密码机制、二次开发或定制 PE 维护工具的开发者与系统运维人员。压缩包共 85 个文件,约 236KB,以 35 个 .h 头文件与 26 个 .c 源文件为主体,辅以 txt 说明、cmd/sh 构建脚本、manifest 清单、rc 资源脚本及 Makefile 等,覆盖 libntpw、libsam、ntreg、libtomcrypt 等核心模块与对话框控件库,结构清晰便于按模块研读。该工具支持 Windows 2000 至 Windows 10 的 32/64 位系统,需在 WinPE 或挂载硬盘环境下运行。目前已有 440 人学习下载,读者可从中获取密码修改原理、注册表与 SAM 解析逻辑、跨平台构建配置及完整工程组织方式,适合作为系统安全与底层开发的学习参考。
1. ntpwedit 0.7 源码包到底解决了什么问题
手上有一台离线 Windows 机器,本地管理员密码丢了,域账号也进不去,安全模式照样弹登录框。这种场景下,能救场的工具不多,ntpwedit 是其中一个被反复提起的名字。它做的事情很聚焦:直接挂载 SAM 数据库文件,把指定本地账户的密码哈希清空或改写,重启后即可用空密码登录,再自行重设。标题里的ntpwedit-0.7-src.zip就是这个工具的 C++ 源码压缩包,版本 0.7,用 Microsoft Visual C++ 工具链构建,属于典型的 Win32 原生小工具。
对一线工程师来说,拿到这份源码的价值不在“能改密码”这个动作本身,而在于它是一份体量小、依赖少、逻辑完整的 Windows 安全子系统操作样本:如何用 C++ 打开被系统占用的注册表配置单元、如何解析 SAM 里的 F 值和 V 值结构、如何在不破坏账户其他属性的前提下只动密码字段。想学 Win32 底层文件与注册表操作、想给 PE 工具做二次开发、想在授权范围内做应急恢复,这份源码都值得读一遍。下面按“先搞懂它在干什么,再动手编译,再排坑,最后讲进阶”的顺序展开。
2. 从 SAM 结构到 C++ 实现:ntpwedit 0.7 的核心机制
2.1 SAM 里密码到底存在哪:F 值与 V 值
Windows 本地账户信息存放在%SystemRoot%\System32\config\SAM这个注册表配置单元文件里。每个账户在SAM\Domains\Account\Users\下有一个以 RID 十六进制命名的子键,比如管理员 RID 是 500,对应子键名000001F4。这个子键下有两个关键二进制值:
F值:记录账户的权限、登录次数、过期时间等元数据,长度固定。V值:记录用户名、全名、密码哈希、密码提示等,长度可变。
密码哈希在V值里有固定偏移。对本地账户,偏移0x9C开始是 LM 哈希(16 字节),偏移0xAC开始是 NT 哈希(16 字节)。ntpwedit 的核心动作就是把这两段 16 字节区域清零,或者写入新的哈希。清零后账户密码为空,登录时系统对空密码哈希校验通过。
注意:这里说的是本地账户。域账户的凭据不在本地 SAM 里,改 SAM 对域登录无效,这是很多人第一次用就翻车的地方。
理解了这个布局,再看源码里SAM_ENTRY、USER_F、USER_V这些结构体就不会懵。ntpwedit 0.7 的代码量不大,主要文件围绕注册表加载、账户枚举、哈希改写三块展开,读起来负担不重。
2.2 用 C++ 加载离线 SAM:RegLoadKey 的关键调用
系统运行时 SAM 被独占,直接CreateFile打开会失败。ntpwedit 的做法是先把 SAM 文件复制出来,或者从 WinPE/另一套系统里操作,然后用RegLoadKey把配置单元挂载到一个临时键下,之后就能像普通注册表一样读写。
// 将离线 SAM 挂载到 HKLM\TempHive 下 // samPath 指向复制出来的 SAM 文件,例如 C:\backup\SAM HKEY hKey; LONG lResult = RegLoadKey(HKEY_LOCAL_MACHINE, L"TempHive", // 临时挂载点名称 samPath); // SAM 文件完整路径 if (lResult != ERROR_SUCCESS) { // 常见失败:文件被占用、权限不足、路径含中文 wprintf(L"RegLoadKey failed: %ld\n", lResult); return; } // 挂载成功后,账户路径变为: // HKLM\TempHive\SAM\Domains\Account\Users\000001F4逻辑说明:RegLoadKey把磁盘上的注册表配置单元文件映射进当前注册表树,之后所有RegOpenKeyEx、RegQueryValueEx、RegSetValueEx都能作用到离线文件上。参数HKEY_LOCAL_MACHINE是挂载父键,TempHive是自定义的挂载名,只要不冲突即可。samPath必须是本地路径,网络路径在部分环境下会失败。
操作完成后必须调用RegUnLoadKey卸载,否则配置单元一直占用,文件无法回写或删除。这一步漏掉是新手最常见的坑,表现为“改完了但重启没效果”,其实是改动还在内存里没落盘。
2.3 定位账户并改写哈希:偏移与写入
挂载之后,枚举SAM\Domains\Account\Users下的子键,每个子键名就是 RID 的十六进制。读取V值,按前面说的偏移定位哈希区。
// 读取 V 值并清零 NT 哈希(偏移 0xAC,长度 16 字节) BYTE vData[2048]; DWORD vSize = sizeof(vData); lResult = RegQueryValueEx(hUserKey, L"V", NULL, NULL, vData, &vSize); if (lResult == ERROR_SUCCESS && vSize > 0xAC + 16) { // 清零 NT 哈希 memset(vData + 0xAC, 0, 16); // 清零 LM 哈希(偏移 0x9C) memset(vData + 0x9C, 0, 16); // 写回 RegSetValueEx(hUserKey, L"V", 0, REG_BINARY, vData, vSize); }逻辑说明:RegQueryValueEx把整个V值读进缓冲区,vSize返回实际长度。偏移0xAC和0x9C是本地账户的标准布局,但要注意V值长度可变,写入时必须用原始vSize,不能截断,否则会破坏用户名等其他字段。清零两个哈希后,账户密码即为空。
参数上唯一需要留意的是缓冲区大小。V值一般不超过 1KB,给 2048 字节足够。如果账户很多,逐个处理即可,不需要一次性全部读入。
3. 编译 ntpwedit 0.7 源码:从解压到跑通的最小路径
3.1 工具链选择:Visual Studio 与 redistributable 的关系
源码是 C++ 写的,用 Microsoft Visual C++ 编译。标题热词里反复出现microsoft visual c++ 2015-2022 redistributable (x64) 下载,这里要分清两件事:redistributable 是运行库,给“运行别人编译好的程序”用;编译源码需要的是 Visual Studio 或 Build Tools,不是 redistributable。很多人下载了 redistributable 发现没法编译,就是混淆了这两者。
我一般用 Visual Studio 2022 Community,安装时勾选“使用 C++ 的桌面开发”工作负载,包含 MSVC 编译器和 Windows SDK。如果不想装完整 IDE,装 Build Tools for Visual Studio 2022 也行,命令行cl.exe直接编译。源码是 0.7 版本,年代较早,用新工具链编译通常需要处理一些字符集和警告问题,下面会讲。
3.2 解压与工程结构确认
把ntpwedit-0.7-src.zip解压到一个不含中文和空格的路径,比如D:\work\ntpwedit-0.7-src。解压后一般能看到.cpp、.h源文件和可能的.vcxproj或.dsp工程文件。老版本可能只有.dsp(VC6 工程),新 Visual Studio 打开会提示升级,按提示转换即可。
如果只有源码没有工程文件,就手动建一个空的控制台或 Win32 项目,把.cpp加进去。ntpwedit 是 Win32 GUI 程序,入口是WinMain,建项目时选“Windows 桌面应用程序”而不是控制台。
# 用 Build Tools 命令行编译的最小流程 # 先进入开发者命令提示符(Developer Command Prompt) cd /d D:\work\ntpwedit-0.7-src cl /EHsc /DUNICODE /D_UNICODE ntpwedit.cpp /link user32.lib advapi32.lib逻辑说明:/EHsc启用标准 C++ 异常处理,/DUNICODE /D_UNICODE让代码走宽字符分支,避免L"..."字符串和RegLoadKey的宽字符参数不匹配。/link后面跟需要链接的库,advapi32.lib提供注册表 API,user32.lib提供窗口相关 API。如果源码里还用了其他库,按编译报错的unresolved external symbol逐个补。
3.3 编译期常见报错与处理
老代码在新编译器下最常见的三类问题:
第一类是fopen、strcpy等函数被标记为不安全,报C4996。可以在文件顶部加#define _CRT_SECURE_NO_WARNINGS,或者在项目属性里关闭 SDL 检查。热词里出现的c++ 64位 fopen报安全错误就是这个问题,本质是 MSVC 的安全开发生命周期检查,不是代码有 bug。
第二类是字符集问题,char*和wchar_t*混用报C2664。统一用宽字符版本,把RegOpenKeyEx写成RegOpenKeyExW,字符串加L前缀。
第三类是LNK2019未解析外部符号,通常是漏链接库。注册表操作要advapi32.lib,界面要user32.lib,RegLoadKey还需要管理员权限运行,编译通过不代表运行能成功。
提示:编译出来的 exe 必须以管理员身份运行,否则
RegLoadKey直接返回ERROR_ACCESS_DENIED。右键“以管理员身份运行”是最省事的验证方式。
4. 避坑与排查:改 SAM 时最容易翻车的五个点
4.1 现象:改完重启密码没变
原因:RegLoadKey挂载后没有调用RegUnLoadKey,改动停留在内存映射里,没有回写到 SAM 文件。或者操作的是复制出来的 SAM,但重启用的是原始文件。
解决:每次修改后确保RegUnLoadKey(HKEY_LOCAL_MACHINE, L"TempHive")返回ERROR_SUCCESS,再关闭程序。如果操作的是副本,改完要把副本覆盖回System32\config\SAM,覆盖前先备份原文件。
4.2 现象:RegLoadKey 返回 32 或 5
原因:错误码 32 是文件被占用,5 是权限不足。系统运行时 SAM 被独占,直接挂载原始路径必然失败。
解决:在 WinPE 或另一套系统下操作,或者先把 SAM 复制到其他目录再挂载副本。复制时用copy命令可能因占用失败,可以用esentutl或卷影副本。权限问题就提权,确认进程以管理员运行。
4.3 现象:账户列表里找不到目标用户
原因:枚举的是当前系统的 SAM,不是目标系统的;或者目标账户是域账户,不在本地 SAM 里。
解决:确认挂载的 SAM 文件路径指向目标系统。域账户改本地 SAM 无效,需要用域控侧的手段,这超出 ntpwedit 的能力范围。另外 RID 500 是内置管理员,有些系统默认禁用,改完还要启用账户。
4.4 现象:改完能登录但用户配置文件丢失
原因:只清了密码哈希,但账户的F值里某些标志位被误改,或者V值写入时长度不对,破坏了用户名等字段。
解决:写入V值时严格用原始长度,只改哈希区那 32 字节,其余原样保留。改之前把整个V值备份成.reg或二进制文件,出问题能还原。这是血泪经验,别嫌麻烦。
4.5 现象:编译出的 exe 在目标机器上跑不起来
原因:目标机器缺 Visual C++ 运行库,或者 exe 是 64 位而目标系统是 32 位。
解决:在目标机器装对应的microsoft visual c++ redistributable,或者编译时选静态链接运行库(/MT代替/MD),这样不依赖外部 DLL。位数要匹配,32 位系统跑不了 64 位 exe。
5. 进阶:把 ntpwedit 的思路用到自己的 C++ 工具里
读源码的最终目的是能改、能扩。ntpwedit 0.7 这套“复制配置单元 → RegLoadKey 挂载 → 定位偏移 → 改写 → 卸载”的流程,可以迁移到很多离线注册表操作场景,比如离线修改系统服务启动项、离线清理启动项、离线读取已安装软件列表。核心 API 就那几个,难点在偏移和结构。
一个实用的进阶方向是加一个“备份与还原”功能:在改写V值之前,把原始二进制导出到文件,提供一个还原入口。这样即使改错也有后悔药。实现上就是在RegQueryValueEx之后、RegSetValueEx之前,把vData和vSize写进一个带 RID 命名的文件。
另一个方向是批量处理。如果一次要清多个账户,把账户枚举和改写封装成循环,注意每次操作后检查返回值,别一个失败影响后续。下面是一个批量清空密码的骨架:
// 遍历 Users 下所有子键,逐个清空密码哈希 HKEY hUsers; RegOpenKeyEx(hHive, L"SAM\\Domains\\Account\\Users", 0, KEY_READ, &hUsers); DWORD index = 0; WCHAR subKeyName[256]; DWORD nameLen = 256; while (RegEnumKeyEx(hUsers, index++, subKeyName, &nameLen, NULL, NULL, NULL, NULL) == ERROR_SUCCESS) { // 跳过 Names 子键,它不是账户 if (_wcsicmp(subKeyName, L"Names") == 0) { nameLen = 256; continue; } // 打开子键,读取 V 值,按偏移清零,写回 // ...(同 2.3 的逻辑) nameLen = 256; // 重置长度,准备下一次枚举 } RegCloseKey(hUsers);逻辑说明:RegEnumKeyEx逐个返回子键名,index递增。每次循环要把nameLen重置为缓冲区大小,否则第二次调用会因长度不足失败,这是 Win32 枚举 API 的经典坑。跳过Names子键是因为它存的是用户名到 RID 的映射,不是账户本体。
验证改动是否生效,不要只看程序返回成功。挂载后重新读取V值,确认偏移0xAC处 16 字节全为零,再卸载。有条件的话在虚拟机里做完整重启验证,别拿生产机试。
参数上唯一要记住的边界:V值偏移0x9C和0xAC适用于本地账户,不同 Windows 版本可能有细微差异,动手前先用十六进制编辑器看一眼实际布局,确认用户名后面紧跟的就是哈希区。这个习惯帮我避过好几次“偏移对不上”的玄学问题。
我自己现在的习惯是:任何离线注册表操作,先复制文件、再备份原始值、再动手,三步缺一不可。工具是死的,数据是活的,改坏了没有后悔药。希望帮到你。
本文还有配套的精品资源,点击获取