首页 / 资讯中心 / 文章详情

ntpwedit 0.7 源码解析:离线 SAM 密码重置工具的实现与二次开发

ntpwedit 0.7 源码解析:离线 SAM 密码重置工具的实现与二次开发 ★ FEATURED ARTICLE
简介NTPWEdit 0.7 的 C 源码包面向需要研究 Windows 本地账户密码修改原理、或希望二次开发系统维护工具的程序员与运维人员。该工具通过直接编辑 SAM 文件实现密码更改或删除须在 WinPE 或挂载硬盘的离线环境下运行支持 Windows 2000 至 Windows 10 的 32/64 位系统。压缩包共 85 个文件约 236KB以 35 个 .h 头文件与 26 个 .c 源文件为主体另有 txt 说明、cmd/sh 构建脚本、manifest、rc 资源及 Makefile 等涵盖 libntpw、libsam、ntreg、libtomcrypt 等核心模块与对话框控件实现并附 MinGW/MSVC 多套编译配置。已有 444 人学习下载。读者可借此理清 SAM 解析、注册表操作与密码重置的完整代码路径掌握跨平台构建与界面封装思路为系统安全工具开发提供可复用的参考实现。1. 从一次密码重置需求说起ntpwedit 0.7 到底能干什么有位做桌面运维的朋友遇到过这么个场景一台离线机器上的本地账户密码没人记得了系统进不去重装又舍不得里面的数据。他翻出一个不到 200KB 的小工具在 PE 环境里点了几下账户密码就清空了。这个工具就是 ntpwedit而这次要拆的是它的 0.7 版本 C 源码包ntpwedit-0.7-src.zip。ntpwedit 是一个专门用来编辑离线系统账户数据库的工具核心能力是读取和修改 SAM 文件里的本地用户信息包括清空密码、修改密码、启用被禁用的账户、调整用户组归属。它不依赖目标系统启动只要拿到 SAM 文件所在的分区就能干活所以常被用在系统维护、应急恢复这类场景。适合谁看这份源码一是想搞懂 Windows 账户数据库结构的人二是需要把类似能力集成到自己工具链里的开发者三是单纯想学一个完整 Win32 C 小项目怎么组织的人。源码体量不大但麻雀虽小五脏俱全注册表解析、加密算法、界面逻辑都在里面。2. 项目结构与技术选型一个 Win32 小工具是怎么搭起来的2.1 目录结构逐个看把ntpwedit-0.7-src.zip解压后目录结构大致是这样的ntpwedit-0.7-src/ ├── ntpwedit.sln # VS 解决方案文件 ├── ntpwedit.vcproj # 项目文件 ├── src/ │ ├── main.cpp # 程序入口WinMain │ ├── ntpwedit.cpp # 主窗口逻辑与消息处理 │ ├── ntpwedit.h # 主窗口声明 │ ├── sam.cpp # SAM 数据库读写核心 │ ├── sam.h # SAM 相关结构体与函数声明 │ ├── crypto.cpp # 加密解密相关 │ ├── crypto.h │ ├── registry.cpp # 注册表 hive 文件解析 │ ├── registry.h │ ├── resource.h # 资源 ID 定义 │ └── ntpwedit.rc # 资源脚本对话框、菜单 ├── res/ # 图标等资源 └── README.txt这个划分很清晰registry负责底层 hive 文件格式解析sam在它之上封装账户数据库的语义操作crypto处理密码哈希相关的加解密ntpwedit是界面层。分层明确改起来不容易牵一发动全身。2.2 技术栈与依赖项目是纯 Win32 C没有 MFC没有 Qt界面全部用原生 API 手写。编译环境是 Visual Studio解决方案文件是.sln加.vcproj的老格式说明它面向的是 VS2005 到 VS2010 那个年代的工程体系。用新版本 VS 打开会提示升级项目升级后一般能正常编译。依赖方面几乎没有第三方库加密部分是自己实现的注册表解析也是自己写的。这意味着你不需要配 vcpkg、不需要拉子模块clone 下来直接编译就行。常见做法是直接用 VS 打开 sln选 Release 配置平台选 Win32编译产出就是一个独立的 exe。提示项目默认可能是 Unicode 字符集如果你的环境编译报字符相关的错检查项目属性里的字符集设置改成「使用 Unicode 字符集」即可。2.3 为什么不用现成的库有人会问解析注册表 hive 文件、处理 SAM为什么不找个现成的库我的理解是这个工具的目标就是单文件、零依赖、能在 PE 环境里直接跑。引入任何第三方库都会让体积膨胀、部署变复杂。自己实现虽然代码量大但可控性最强也方便针对 SAM 这种特定格式做裁剪。这种取舍在小工具开发里很常见值得借鉴。3. 核心模块拆解从 hive 文件到账户密码的完整链路3.1 注册表 hive 解析一切从文件头开始SAM 文件本质上是一个注册表 hive 文件格式和C:\Windows\System32\config\下的其他 hive 一样。要读它第一步是解析文件头拿到根键的偏移。// registry.cpp 中读取 hive 文件头的简化逻辑 bool RegistryHive::Open(const std::wstring path) { hFile CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile INVALID_HANDLE_VALUE) return false; // 读取前 4096 字节的 base block BYTE baseBlock[4096]; DWORD read 0; if (!ReadFile(hFile, baseBlock, sizeof(baseBlock), read, NULL)) return false; // 校验签名必须是 regf if (memcmp(baseBlock, regf, 4) ! 0) return false; // 根键偏移在固定位置0x24 处是 root cell offset rootOffset *(DWORD*)(baseBlock 0x24); return true; }这段代码做了三件事打开文件、读取 base block、校验签名。regf是注册表 hive 的固定魔数对不上就说明文件不对。rootOffset是根键在文件中的偏移后续所有键值查找都从这里出发。参数上path是 SAM 文件的完整路径在 PE 环境下通常是X:\Windows\System32\config\SAM。3.2 键值遍历找到 SAM 里的用户列表拿到根键后要一层层往下找。SAM 数据库的结构是SAM\Domains\Account\Users\Names每个用户名对应一个子键子键里存着该用户的 RID 和 V 结构体偏移。// sam.cpp 中枚举用户的简化逻辑 bool SamDatabase::EnumUsers(std::vectorUserInfo users) { RegistryKey namesKey; // 路径逐级定位到 Names 键 if (!rootKey.OpenSubKey(LSAM\\Domains\\Account\\Users\\Names, namesKey)) return false; // 遍历 Names 下的所有子键每个子键名就是一个用户名 std::vectorstd::wstring subKeys; namesKey.EnumSubKeys(subKeys); for (const auto name : subKeys) { RegistryKey userKey; if (!namesKey.OpenSubKey(name, userKey)) continue; UserInfo info; info.username name; // 默认值里存着该用户对应的 RID userKey.GetDefaultValue(info.rid); users.push_back(info); } return true; }这里的关键是理解 SAM 的组织方式Names键下每个子键的名字就是用户名子键的默认值是一个 4 字节的 RID。拿到 RID 后再去Users键下找以 RID 十六进制命名的子键那里才有真正的密码数据。这种「名字索引 RID 数据」的两级结构是为了支持快速按名查找和按 RID 引用。3.3 密码哈希处理V 结构体里的玄机真正的密码信息藏在Users\RID键的V值里。这个V是一个变长结构体里面包含用户名、全名、密码哈希、账户标志位等字段。0.7 版本里对这块的处理是核心难点。// sam.cpp 中解析 V 结构体的关键片段 bool SamDatabase::ParseVStructure(const std::vectorBYTE vData, UserInfo info) { if (vData.size() 0xCC) return false; // 偏移 0x0C 处是用户名长度0x10 处是用户名偏移 DWORD nameLen *(DWORD*)(vData[0x0C]); DWORD nameOff *(DWORD*)(vData[0x10]); if (nameOff nameLen vData.size()) return false; // 用户名是 UTF-16 编码 info.username.assign((wchar_t*)vData[nameOff], nameLen / sizeof(wchar_t)); // 偏移 0x9C 附近是密码哈希相关字段 // 具体偏移随版本略有差异需要按结构体定义对齐 info.passwordHashOffset 0x9C; return true; }V 结构体的字段偏移是硬编码的这是这个项目最「脆」的地方。不同 Windows 版本的 SAM 结构可能有细微差异偏移对不上就会读出错乱的数据。0.7 版本针对当时主流的系统做了适配但如果你要支持更新的系统这块需要仔细核对。参数上vData是从注册表读出来的原始字节info是解析结果输出。3.4 清空密码改哪几个字节清空密码不是把哈希删掉而是把密码哈希字段设为一个特定值让系统认为该账户没有密码。核心操作是定位到 V 结构体里的哈希区域写入空密码对应的数据。// 清空密码的核心操作 bool SamDatabase::ClearPassword(const std::wstring username) { UserInfo info; if (!FindUser(username, info)) return false; // 重新读取该用户的 V 结构体 std::vectorBYTE vData; if (!ReadUserVData(info.rid, vData)) return false; // 将密码哈希长度字段置零表示无密码 // 具体偏移需要和 ParseVStructure 里的定义保持一致 DWORD hashLenOffset 0x9C; *(DWORD*)(vData[hashLenOffset]) 0; // 写回 SAM 文件 return WriteUserVData(info.rid, vData); }写回的时候要注意SAM 文件在系统运行时是被锁定的所以这个操作必须在离线环境比如 PE下进行。另外修改前务必备份原文件这是血泪经验——改坏了 SAM系统就彻底进不去了。4. 编译与运行从源码到能用的 exe4.1 编译环境准备需要一台 Windows 机器装 Visual Studio。版本上VS2010 到 VS2019 都有人成功编译过VS2022 需要升级项目文件。没有其他依赖不需要额外装 SDK 之外的东西。步骤上先用 VS 打开ntpwedit.sln会提示项目文件格式较旧选择升级。升级后检查项目属性确认字符集是 Unicode平台是 Win32。然后选 Release 配置直接生成解决方案。产物在Release目录下是一个独立的 exe。4.2 运行方式与典型流程这个工具不能直接在运行中的系统上操作当前系统的 SAM因为文件被占用。典型用法是用 PE 启动目标机器或者把目标硬盘挂到另一台机器上。运行 ntpwedit在界面里选择 SAM 文件路径通常是X:\Windows\System32\config\SAM。工具会列出所有本地账户选中要操作的用户。点「清空密码」或「修改密码」确认后写回。注意操作前一定要备份 SAM 文件。直接复制一份SAM和配套的SAM.LOG到安全位置出问题能还原。4.3 验证是否生效改完后重启目标系统用被修改的账户登录。如果清空了密码直接回车应该能进如果改了密码用新密码登录。如果登录失败大概率是哈希字段没写对或者写回时文件损坏。这时候用备份还原重新来一遍。5. 避坑与常见问题那些年踩过的 SAM 坑5.1 编译报错「无法打开包括文件 windows.h」现象是编译时提示找不到windows.h。原因通常是没装 Windows SDK或者项目属性里的 SDK 版本选错了。解决方法是打开 VS Installer确认装了对应版本的 Windows SDK然后在项目属性里把「Windows SDK 版本」改成已安装的版本。5.2 运行后读不到用户列表现象是选了 SAM 文件但列表是空的。原因可能是 SAM 文件路径不对或者文件被占用导致读取失败。解决方法是确认路径指向的是真正的 SAM 文件而不是备份并且确保是在离线环境下操作。如果是在 PE 里检查盘符对不对。5.3 清空密码后系统进不去现象是改完重启账户登录还是失败甚至系统报错。原因是写回 SAM 时数据损坏或者哈希字段偏移算错了。解决方法是提前备份出问题用备份还原。另外确认操作的是目标系统的 SAM别改错了盘。5.4 新系统上偏移对不上现象是在较新的 Windows 版本上读出来的用户名乱码或者密码改不动。原因是 V 结构体的字段偏移随系统版本变化0.7 版本的硬编码偏移不适用。解决方法是参考新系统的 SAM 结构文档调整ParseVStructure里的偏移常量。这块没有后悔药只能对着结构定义一点点核对。5.5 编译出来的 exe 在 PE 里跑不起来现象是 exe 在正常系统能跑在 PE 里报缺 DLL。原因是编译时用了动态运行库PE 环境里没有对应的 CRT。解决方法是在项目属性里把「运行库」改成「多线程 (/MT)」静态链接 CRT这样 exe 就能独立运行。6. 二次开发把 ntpwedit 改造成你自己的工具拿到这份源码最实际的改造方向是把它集成到自己的维护工具链里或者扩展它支持更多操作。这里说几个我实际试过的思路。第一个方向是加命令行支持。现在它是纯 GUI批量操作很不方便。可以在main.cpp的WinMain里解析lpCmdLine如果带了/clear /user:xxx /sam:path这样的参数就直接走核心逻辑不弹界面。核心函数ClearPassword已经是独立的直接调用就行。改造时注意参数解析要处理引号和空格路径里带空格的场景很常见。第二个方向是支持更多账户属性。现在只能改密码和启用/禁用其实 V 结构体里还有全名、描述、账户过期时间等字段。照着ParseVStructure的模式加偏移常量、加读写函数就行。我一般会先写一个 dump 函数把 V 结构体的所有字段按偏移打出来确认哪些位置是什么再动手改。第三个方向是适配新系统。前面提过偏移是硬编码的要支持新版本得先搞清楚新结构。做法是拿一个已知密码的账户在目标系统上导出 SAM用工具 dump 出 V 结构体对比已知密码的哈希位置反推偏移。这个过程比较费时间但一次搞定就能长期用。验证改造是否成功我习惯用一个最小闭环准备一个测试虚拟机建一个测试账户用改造后的工具改密码重启验证能否登录。这个流程走通了再上真实环境。从那以后我每次改 SAM 相关代码都强制先在虚拟机里跑一遍完整流程确认没问题才碰真实数据。希望这些能帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站