首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
编程技术
发新帖
0
0
[原创]LSA 认证、安全与通知包的注册表枚举
发表于: 2026-7-21 10:49
970
[原创]LSA 认证、安全与通知包的注册表枚举
WangWei_CM.
2026-7-21 10:49
970
# LSA 认证、安全与通知包的注册表枚举 本次检查的目标是:只读列出本机 LSA 的认证包、安全包、通知包和 OSConfig 安全包,并为每个包名保留来源、候选 DLL、PE 架构和嵌入签名状态。 要得到这组结果,分为四步: 1. 定位四个 LSA 包列表的完整注册表来源 2. 按返回字节长度读取并解析 `REG_MULTI_SZ` 包名 3. 为每个包名构造 DLL 候选路径并确认文件状态 4. 读取候选 DLL 的 PE Machine 字段并检查 Authenticode 签名 完整流程为:四个注册表来源 -> 原始注册表字节 -> 受边界限制的包名 -> 候选 DLL -> PE 架构与签名状态。 ## 一、第一步:定位四个 LSA 包列表的完整注册表来源 第一步要确认每份配置由哪个 Windows 组件使用。Local Security Authority(LSA,本地安全机构)管理本机安全策略、登录身份验证相关协调和审核。`lsass.exe` 是承载 LSA 服务的重要系统进程。系统启动和用户登录时,LSA 会协调多类安全组件。 第一步还要区分包名和模块文件。包列表中的 `msv1_0`、`kerberos` 一类条目是逻辑包名。包名来自注册表配置,模块路径、PE 架构、签名状态和运行时加载状态都属于后续信息。 ### 四个包列表分别保存什么 三个主列表的职责不同。`Authentication Packages` 中的组件参与用户身份验证协议处理。`Security Packages` 中的组件为 SSPI(Security Support Provider Interface,安全支持提供者接口)等安全服务提供者接口提供实现。`Notification Packages` 中的组件接收 LSA 相关通知。 `Lsa\OSConfig\Security Packages` 是第四个独立来源。它与 LSA 主键下的 `Security Packages` 同名,完整注册表路径不同。记录结果时要同时保存完整键路径和值名,这样才能追溯某个包名来自哪一份配置。 ```text Lsa 主键 Authentication Packages:身份验证协议相关组件 Security Packages:安全支持提供者组件 Notification Packages:LSA 通知相关组件 OSConfig Security Packages:另一来源的安全包配置 ``` ### 逐份打开并记录来源 认证包、安全包和通知包位于 LSA 主键。`Authentication Packages`、`Security Packages`、`Notification Packages` 是三个独立值,`Lsa\OSConfig` 下的 `Security Packages` 也需要单独读取。 每个值的读取身份由“完整键路径 + 值名 + 包类别”组成。`Security Packages` 在两个键下同名,按值名合并会丢失配置来源。 打开注册表键的函数返回可查询的键句柄。固定包列表只需要 `KEY_QUERY_VALUE` 权限,成功后的句柄必须关闭。 ```cpp // 意义:打开一个注册表子键。 // 返回:ERROR_SUCCESS 表示成功。其它返回值是 Win32 错误码。 // 成功时 phkResult 接收 HKEY,调用方必须用 RegCloseKey 关闭。 LSTATUS RegOpenKeyExW( HKEY hKey, // 预定义根键或已经打开的父键。 LPCWSTR lpSubKey, // 相对键路径。 DWORD ulOptions, // 保留,必须为 0。 REGSAM samDesired, // 所需权限,此处为 KEY_QUERY_VALUE。 PHKEY phkResult // 接收新键句柄。 ); // 意义:关闭由 RegOpenKeyExW 返回的键句柄。 // 返回:ERROR_SUCCESS 表示成功。预定义根键不需要关闭。 LSTATUS RegCloseKey(HKEY hKey); ``` 正确读取时让每个值独立失败。`OSConfig` 子键或其中的值未设置时记录该状态,认证包、安全包和通知包仍继续读取。 ```cpp constexpr wchar_t kLsaKey[] = L"System\\CurrentControlSet\\Control\\Lsa"; HKEY lsaKey = nullptr; const LSTATUS status = RegOpenKeyExW( HKEY_LOCAL_MACHINE, kLsaKey, 0, KEY_QUERY_VALUE, &lsaKey); if (status == ERROR_SUCCESS) { // 依次读取 Authentication Packages、Security Packages、Notification Packages。 RegCloseKey(lsaKey); } ``` ```cpp // 错误示例:用同一个字符串字段保存所有值名。 // 两处 Security Packages 会被误认为同一份配置。 record.name = "Security Packages"; ``` 完成第一步后,四份配置都有独立的完整键路径和值名。接下来要读取每个值返回的原始字节,并在明确边界内得到包名。 ## 二、第二步:按返回字节长度读取并解析 `REG_MULTI_SZ` 注册表读取函数以字节数描述值数据。第一次调用传入空数据指针取得长度,第二次调用按该字节数读取原始数据。读取期间值发生变化时,`ERROR_MORE_DATA` 需要触发扩容重试。 ```cpp // 意义:读取注册表值的类型与原始数据。 // 返回:ERROR_SUCCESS 表示成功。ERROR_FILE_NOT_FOUND 表示值未设置。 // ERROR_MORE_DATA 表示 lpData 的字节容量不足。 LSTATUS RegQueryValueExW( HKEY hKey, // 已打开且具有 KEY_QUERY_VALUE 权限的键。 LPCWSTR lpValueName, // 值名。nullptr 或空字符串表示默认值。 LPDWORD lpReserved, // 保留,必须为 nullptr。 LPDWORD lpType, // 接收 REG_MULTI_SZ 等类型。 LPBYTE lpData, // 接收原始字节。查询长度时传 nullptr。 LPDWORD lpcbData // 输入为容量、输出为实际长度,单位是字节。 ); ``` 正确读取先保存类型,再保存实际字节数。`REG_MULTI_SZ` 只有在字节数能被 `sizeof(wchar_t)` 整除时,才可以按 UTF-16 字符边界查看。 ```cpp DWORD type = REG_NONE; DWORD byteCount = 0; LSTATUS status = RegQueryValueExW( lsaKey, L"Authentication Packages", nullptr, &type, nullptr, &byteCount); if (status == ERROR_SUCCESS) { std::vector<BYTE> bytes(byteCount); DWORD copiedBytes = byteCount; status = RegQueryValueExW( lsaKey, L"Authentication Packages", nullptr, &type, bytes.empty() ? nullptr : bytes.data(), &copiedBytes); if (status == ERROR_SUCCESS) { bytes.resize(copiedBytes); } } ``` 多字符串使用单个 NUL 分隔条目,使用双 NUL 结束列表。`msv1_0\0kerberos\0\0` 表示两个包名。最后两个 NUL 是结构边界,不能依赖 `wcslen` 从未知内存寻找它们。 ```cpp std::vector<std::wstring> packages; const auto* characters = reinterpret_cast<const wchar_t*>(bytes.data()); const std::size_t characterCount = bytes.size() / sizeof(wchar_t); std::size_t begin = 0; while (begin < characterCount) { std::size_t end = begin; while (end < characterCount && characters[end] != L'\0') { ++end; } if (end == begin) { break; // 已抵达双 NUL 中的第一个终止字符。 } packages.emplace_back(characters + begin, end - begin); if (end == characterCount) { break; // 缺少 NUL 时只保留 API 返回范围内的最后片段。 } begin = end + 1; } ``` 缺少双 NUL 时仍可显示已返回的片段,同时记录终止缺失。长度受限的循环不访问 `bytes` 之外,损坏配置也不会变成越界读取。 ```cpp // 错误示例:假定注册表总会提供 NUL 结尾。 const wchar_t* text = reinterpret_cast<const wchar_t*>(bytes.data()); std::wcout << text; // 可能越过 bytes 的边界。 ``` 完成第二步后,已经得到每个列表项的来源和受长度限制的包名。接下来要把包名转换为可检查的文件候选,同时保留它只是候选这一事实。 ## 三、第三步:为每个包名构造 DLL 候选路径并确认文件状态 包列表中的 `msv1_0`、`scecli` 这类内容是包标识。它们没有路径和扩展名时,可以构造 `系统目录 + 包名 + .dll` 作为文件定位候选。候选路径不能替代 LSA 实际加载结果。 系统目录函数返回实际复制的字符数。返回值小于缓冲区容量时才表示完整路径已写入。返回值达到容量时需要扩大缓冲区。 ```cpp // 意义:取得系统目录,例如 C:\Windows\System32。 // 返回:成功时为复制的字符数,不包含结尾 NUL。0 表示失败。 // 缓冲区不足时返回所需容量,调用方必须扩大缓冲区。 UINT GetSystemDirectoryW( LPWSTR lpBuffer, // 接收系统目录的输出缓冲区。 UINT uSize // 缓冲区容量,单位为 wchar_t 个数。 ); ``` 构造候选路径时保留原始包名。已有绝对路径时保留原路径。没有扩展名时添加 `.dll`。引号、内嵌 NUL、回车和制表符都作为无效包名状态显示,不把它们拼成文件路径。 ```cpp std::wstring candidate; std::wstring fileName(packageName); if (!HasFileExtension(fileName)) { fileName += L".dll"; } if (IsAbsolutePath(fileName)) { candidate = fileName; } else { candidate = systemDirectory + L"\\" + fileName; } ``` 候选路径构造完成后还要确认它代表的是可读取文件。`GetFileAttributesW` 返回文件或目录的属性位,返回 `INVALID_FILE_ATTRIBUTES` 时表示路径不存在或当前账户无法读取属性,此时应立即读取 `GetLastError`。属性中带有 `FILE_ATTRIBUTE_DIRECTORY` 时,路径指向目录,不能继续按 DLL 读取。 ```cpp // 意义:取得指定路径的文件系统属性。 // 返回:成功时返回属性位。失败时返回 INVALID_FILE_ATTRIBUTES,随后用 GetLastError 取得原因。 DWORD GetFileAttributesW( LPCWSTR lpFileName // 输入,UTF-16 的 NUL 结尾路径。不能为 nullptr。 ); ``` ```cpp const DWORD attributes = GetFileAttributesW(candidate.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES) { const DWORD error = GetLastError(); // 记录候选路径不可检查及 error,仍保留前面读取到的来源与包名。 } else if ((attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { // 该候选路径是目录,不能把它交给 PE 解析或签名检查。 } else { // 只有普通文件候选才进入下一步。 } ``` 当前系统目录只是一个可验证的文件位置。包名、候选路径、文件存在性和签名检查处于不同阶段,任意一个阶段失败都不应覆盖前一阶段已经读取到的信息。 完成第三步后,已经为每个有效包名保留了候选路径和文件状态。接下来只对可读取的文件检查 PE 头和嵌入签名。 ## 四、第四步:读取候选 DLL 的 PE Machine 字段并检查 Authenticode 签名 DLL 的架构检查需要读取 PE 头。`GetBinaryTypeW` 面向可执行映像,某些 DLL 会返回 `ERROR_BAD_EXE_FORMAT`。读取 `IMAGE_FILE_HEADER::Machine` 可以直接取得 x86、x64、ARM64 等机器类型。 文件打开函数返回 `INVALID_HANDLE_VALUE` 时失败。读取 PE 头只申请 `GENERIC_READ`,并允许其它进程读、写、删除文件,读取结束后关闭句柄。 ```cpp // 意义:打开已有文件。 // 返回:成功时返回 HANDLE。失败时返回 INVALID_HANDLE_VALUE,错误原因由 GetLastError 返回。 HANDLE CreateFileW( LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile ); // 意义:取得文件长度。 // 返回:非零表示成功。lpFileSize 接收有符号 64 位字节数。 BOOL GetFileSizeEx(HANDLE hFile, PLARGE_INTEGER lpFileSize); // 意义:移动文件读写位置。 // 返回:非零表示成功。liDistanceToMove 是 64 位字节偏移。 BOOL SetFilePointerEx( HANDLE hFile, LARGE_INTEGER liDistanceToMove, PLARGE_INTEGER lpNewFilePointer, DWORD dwMoveMethod ); // 意义:从当前文件偏移读取字节。 // 返回:非零只表示请求已完成。lpNumberOfBytesRead 仍必须等于请求字节数。 BOOL ReadFile( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped ); // 意义:关闭 CreateFileW 返回的普通句柄。 // 返回:非零表示成功。 BOOL CloseHandle(HANDLE hObject); ``` 正确解析先验证 DOS 签名和 `e_lfanew` 范围,再验证 PE 签名,然后读取文件头。每一个偏移都限制在真实文件大小内,截断文件直接报告格式错误。 ```cpp IMAGE_DOS_HEADER dos{}; ReadExact(file, &dos, sizeof(dos)); if (dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < 0) { // ERROR_BAD_EXE_FORMAT } LARGE_INTEGER offset{}; offset.QuadPart = dos.e_lfanew; SetFilePointerEx(file, offset, nullptr, FILE_BEGIN); DWORD signature = 0; IMAGE_FILE_HEADER header{}; ReadExact(file, &signature, sizeof(signature)); ReadExact(file, &header, sizeof(header)); if (signature == IMAGE_NT_SIGNATURE) { // header.Machine 可与 IMAGE_FILE_MACHINE_AMD64、IMAGE_FILE_MACHINE_I386 等比较。 } ``` 仅检查 `MZ` 不足以确认 PE 文件。`e_lfanew` 可能指向文件外部,PE 签名也可能错误,读取前的文件大小与偏移校验缺一不可。 读取到 PE Machine 后,已经知道候选文件声明的目标架构。接下来把签名结果单独记录,文件格式、架构和信任状态就不会互相覆盖。 ### 将文件签名状态作为独立结果 文件存在且 PE 头有效后可以检查嵌入签名。信任检查关闭界面并限制为本地缓存,结果仍受本机证书和缓存内容影响,返回状态码应原样保留。 ```cpp // 意义:按指定信任操作验证文件签名。 // 返回:ERROR_SUCCESS 表示信任检查成功。其它 LONG 值为信任提供程序状态码。 LONG WinVerifyTrust( HWND hwnd, // 无 UI 检查时传 nullptr。 GUID* pgActionID, // 指向 WINTRUST_ACTION_GENERIC_VERIFY_V2。 LPVOID pWVTData // 指向已初始化的 WINTRUST_DATA。 ); ``` 验证完成后要调用一次 `WTD_STATEACTION_CLOSE`。这个调用关闭信任提供程序的状态数据,成功和失败路径都需要执行。 ```cpp WINTRUST_FILE_INFO fileInfo{}; fileInfo.cbStruct = sizeof(fileInfo); fileInfo.pcwszFilePath = candidate.c_str(); WINTRUST_DATA data{}; data.cbStruct = sizeof(data); data.dwUIChoice = WTD_UI_NONE; data.fdwRevocationChecks = WTD_REVOKE_NONE; data.dwUnionChoice = WTD_CHOICE_FILE; data.pFile = &fileInfo; data.dwStateAction = WTD_STATEACTION_VERIFY; data.dwProvFlags = WTD_CACHE_ONLY_URL_RETRIEVAL; GUID action = WINTRUST_ACTION_GENERIC_VERIFY_V2; LONG status = WinVerifyTrust(nullptr, &action, &data); data.dwStateAction = WTD_STATEACTION_CLOSE; WinVerifyTrust(nullptr, &action, &data); ``` 完整可运行程序在附件  <mark class="encrypted">0dfK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4j5h3&6Y4N6$3g2A6j5$3#2Q4x3X3g2D9j5h3&6*7L8%4g2#2i4K6u0W2j5$3!0E0i4K6u0r3K9f1#2r3h3f1@1K6P5s2u0E0k6i4c8W2</mark>
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
上传的附件:
011-LSA认证安全与通知包的注册表枚举.zip
(169.16kb,3次下载)
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
0
)
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
WangWei_CM.
32
发帖
2
回帖
40
RANK
关注
私信
他的文章
[原创]ETW监控-KswordARK功能展示
284
[原创]Windows 会话、Session 0 与进程 Token 隔离身份
438
[原创] 进程模块枚举、映像身份与线程启动地址关联
224
[原创]Windows 进程 CPU、内存、I/O 与调度指标采样
1348
[原创]跨位数进程的映像、命令行与环境块读取
540
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
谁下载
×
Fzzz
jyotidwi
mb_lthgjpwj
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部