[原创][Windows C++]LSA 认证、安全与通知包的注册表枚举

发布时间:2026/7/22 2:07:19
[原创][Windows C++]LSA 认证、安全与通知包的注册表枚举 LSA 认证、安全与通知包的注册表枚举本次检查的目标是只读列出本机 LSA 的认证包、安全包、通知包和 OSConfig 安全包并为每个包名保留来源、候选 DLL、PE 架构和嵌入签名状态。要得到这组结果分为四步定位四个 LSA 包列表的完整注册表来源按返回字节长度读取并解析REG_MULTI_SZ包名为每个包名构造 DLL 候选路径并确认文件状态读取候选 DLL 的 PE Machine 字段并检查 Authenticode 签名完整流程为四个注册表来源 - 原始注册表字节 - 受边界限制的包名 - 候选 DLL - PE 架构与签名状态。一、第一步定位四个 LSA 包列表的完整注册表来源第一步要确认每份配置由哪个 Windows 组件使用。Local Security AuthorityLSA本地安全机构管理本机安全策略、登录身份验证相关协调和审核。lsass.exe是承载 LSA 服务的重要系统进程。系统启动和用户登录时LSA 会协调多类安全组件。第一步还要区分包名和模块文件。包列表中的msv1_0、kerberos一类条目是逻辑包名。包名来自注册表配置模块路径、PE 架构、签名状态和运行时加载状态都属于后续信息。四个包列表分别保存什么三个主列表的职责不同。Authentication Packages中的组件参与用户身份验证协议处理。Security Packages中的组件为 SSPISecurity Support Provider Interface安全支持提供者接口等安全服务提供者接口提供实现。Notification Packages中的组件接收 LSA 相关通知。Lsa\OSConfig\Security Packages是第四个独立来源。它与 LSA 主键下的Security Packages同名完整注册表路径不同。记录结果时要同时保存完整键路径和值名这样才能追溯某个包名来自哪一份配置。Lsa 主键 Authentication Packages身份验证协议相关组件 Security Packages安全支持提供者组件 Notification PackagesLSA 通知相关组件 OSConfig Security Packages另一来源的安全包配置逐份打开并记录来源认证包、安全包和通知包位于 LSA 主键。Authentication Packages、Security Packages、Notification Packages是三个独立值Lsa\OSConfig下的Security Packages也需要单独读取。每个值的读取身份由“完整键路径 值名 包类别”组成。Security Packages在两个键下同名按值名合并会丢失配置来源。打开注册表键的函数返回可查询的键句柄。固定包列表只需要KEY_QUERY_VALUE权限成功后的句柄必须关闭。// 意义打开一个注册表子键。// 返回ERROR_SUCCESS 表示成功。其它返回值是 Win32 错误码。// 成功时 phkResult 接收 HKEY调用方必须用 RegCloseKey 关闭。LSTATUSRegOpenKeyExW(HKEY hKey,// 预定义根键或已经打开的父键。LPCWSTR lpSubKey,// 相对键路径。DWORD ulOptions,// 保留必须为 0。REGSAM samDesired,// 所需权限此处为 KEY_QUERY_VALUE。PHKEY phkResult// 接收新键句柄。);// 意义关闭由 RegOpenKeyExW 返回的键句柄。// 返回ERROR_SUCCESS 表示成功。预定义根键不需要关闭。LSTATUSRegCloseKey(HKEY hKey);正确读取时让每个值独立失败。OSConfig子键或其中的值未设置时记录该状态认证包、安全包和通知包仍继续读取。constexprwchar_tkLsaKey[]LSystem\\CurrentControlSet\\Control\\Lsa;HKEY lsaKeynullptr;constLSTATUS statusRegOpenKeyExW(HKEY_LOCAL_MACHINE,kLsaKey,0,KEY_QUERY_VALUE,lsaKey);if(statusERROR_SUCCESS){// 依次读取 Authentication Packages、Security Packages、Notification Packages。RegCloseKey(lsaKey);}// 错误示例用同一个字符串字段保存所有值名。// 两处 Security Packages 会被误认为同一份配置。record.nameSecurity Packages;完成第一步后四份配置都有独立的完整键路径和值名。接下来要读取每个值返回的原始字节并在明确边界内得到包名。二、第二步按返回字节长度读取并解析REG_MULTI_SZ注册表读取函数以字节数描述值数据。第一次调用传入空数据指针取得长度第二次调用按该字节数读取原始数据。读取期间值发生变化时ERROR_MORE_DATA需要触发扩容重试。// 意义读取注册表值的类型与原始数据。// 返回ERROR_SUCCESS 表示成功。ERROR_FILE_NOT_FOUND 表示值未设置。// ERROR_MORE_DATA 表示 lpData 的字节容量不足。LSTATUSRegQueryValueExW(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 字符边界查看。DWORD typeREG_NONE;DWORD byteCount0;LSTATUS statusRegQueryValueExW(lsaKey,LAuthentication Packages,nullptr,type,nullptr,byteCount);if(statusERROR_SUCCESS){std::vectorBYTEbytes(byteCount);DWORD copiedBytesbyteCount;statusRegQueryValueExW(lsaKey,LAuthentication Packages,nullptr,type,bytes.empty()?nullptr:bytes.data(),copiedBytes);if(statusERROR_SUCCESS){bytes.resize(copiedBytes);}}多字符串使用单个 NUL 分隔条目使用双 NUL 结束列表。msv1_0\0kerberos\0\0表示两个包名。最后两个 NUL 是结构边界不能依赖wcslen从未知内存寻找它们。std::vectorstd::wstringpackages;constauto*charactersreinterpret_castconstwchar_t*(bytes.data());conststd::size_t characterCountbytes.size()/sizeof(wchar_t);std::size_t begin0;while(begincharacterCount){std::size_t endbegin;while(endcharacterCountcharacters[end]!L\0){end;}if(endbegin){break;// 已抵达双 NUL 中的第一个终止字符。}packages.emplace_back(charactersbegin,end-begin);if(endcharacterCount){break;// 缺少 NUL 时只保留 API 返回范围内的最后片段。}beginend1;}缺少双 NUL 时仍可显示已返回的片段同时记录终止缺失。长度受限的循环不访问bytes之外损坏配置也不会变成越界读取。// 错误示例假定注册表总会提供 NUL 结尾。constwchar_t*textreinterpret_castconstwchar_t*(bytes.data());std::wcouttext;// 可能越过 bytes 的边界。完成第二步后已经得到每个列表项的来源和受长度限制的包名。接下来要把包名转换为可检查的文件候选同时保留它只是候选这一事实。三、第三步为每个包名构造 DLL 候选路径并确认文件状态包列表中的msv1_0、scecli这类内容是包标识。它们没有路径和扩展名时可以构造系统目录 包名 .dll作为文件定位候选。候选路径不能替代 LSA 实际加载结果。系统目录函数返回实际复制的字符数。返回值小于缓冲区容量时才表示完整路径已写入。返回值达到容量时需要扩大缓冲区。// 意义取得系统目录例如 C:\Windows\System32。// 返回成功时为复制的字符数不包含结尾 NUL。0 表示失败。// 缓冲区不足时返回所需容量调用方必须扩大缓冲区。UINTGetSystemDirectoryW(LPWSTR lpBuffer,// 接收系统目录的输出缓冲区。UINT uSize// 缓冲区容量单位为 wchar_t 个数。);构造候选路径时保留原始包名。已有绝对路径时保留原路径。没有扩展名时添加.dll。引号、内嵌 NUL、回车和制表符都作为无效包名状态显示不把它们拼成文件路径。std::wstring candidate;std::wstringfileName(packageName);if(!HasFileExtension(fileName)){fileNameL.dll;}if(IsAbsolutePath(fileName)){candidatefileName;}else{candidatesystemDirectoryL\\fileName;}候选路径构造完成后还要确认它代表的是可读取文件。GetFileAttributesW返回文件或目录的属性位返回INVALID_FILE_ATTRIBUTES时表示路径不存在或当前账户无法读取属性此时应立即读取GetLastError。属性中带有FILE_ATTRIBUTE_DIRECTORY时路径指向目录不能继续按 DLL 读取。// 意义取得指定路径的文件系统属性。// 返回成功时返回属性位。失败时返回 INVALID_FILE_ATTRIBUTES随后用 GetLastError 取得原因。DWORDGetFileAttributesW(LPCWSTR lpFileName// 输入UTF-16 的 NUL 结尾路径。不能为 nullptr。);constDWORD attributesGetFileAttributesW(candidate.c_str());if(attributesINVALID_FILE_ATTRIBUTES){constDWORD errorGetLastError();// 记录候选路径不可检查及 error仍保留前面读取到的来源与包名。}elseif((attributesFILE_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并允许其它进程读、写、删除文件读取结束后关闭句柄。// 意义打开已有文件。// 返回成功时返回 HANDLE。失败时返回 INVALID_HANDLE_VALUE错误原因由 GetLastError 返回。HANDLECreateFileW(LPCWSTR lpFileName,DWORD dwDesiredAccess,DWORD dwShareMode,LPSECURITY_ATTRIBUTES lpSecurityAttributes,DWORD dwCreationDisposition,DWORD dwFlagsAndAttributes,HANDLE hTemplateFile);// 意义取得文件长度。// 返回非零表示成功。lpFileSize 接收有符号 64 位字节数。BOOLGetFileSizeEx(HANDLE hFile,PLARGE_INTEGER lpFileSize);// 意义移动文件读写位置。// 返回非零表示成功。liDistanceToMove 是 64 位字节偏移。BOOLSetFilePointerEx(HANDLE hFile,LARGE_INTEGER liDistanceToMove,PLARGE_INTEGER lpNewFilePointer,DWORD dwMoveMethod);// 意义从当前文件偏移读取字节。// 返回非零只表示请求已完成。lpNumberOfBytesRead 仍必须等于请求字节数。BOOLReadFile(HANDLE hFile,LPVOID lpBuffer,DWORD nNumberOfBytesToRead,LPDWORD lpNumberOfBytesRead,LPOVERLAPPED lpOverlapped);// 意义关闭 CreateFileW 返回的普通句柄。// 返回非零表示成功。BOOLCloseHandle(HANDLE hObject);正确解析先验证 DOS 签名和e_lfanew范围再验证 PE 签名然后读取文件头。每一个偏移都限制在真实文件大小内截断文件直接报告格式错误。IMAGE_DOS_HEADER dos{};ReadExact(file,dos,sizeof(dos));if(dos.e_magic!IMAGE_DOS_SIGNATURE||dos.e_lfanew0){// ERROR_BAD_EXE_FORMAT}LARGE_INTEGER offset{};offset.QuadPartdos.e_lfanew;SetFilePointerEx(file,offset,nullptr,FILE_BEGIN);DWORD signature0;IMAGE_FILE_HEADER header{};ReadExact(file,signature,sizeof(signature));ReadExact(file,header,sizeof(header));if(signatureIMAGE_NT_SIGNATURE){// header.Machine 可与 IMAGE_FILE_MACHINE_AMD64、IMAGE_FILE_MACHINE_I386 等比较。}仅检查MZ不足以确认 PE 文件。e_lfanew可能指向文件外部PE 签名也可能错误读取前的文件大小与偏移校验缺一不可。读取到 PE Machine 后已经知道候选文件声明的目标架构。接下来把签名结果单独记录文件格式、架构和信任状态就不会互相覆盖。将文件签名状态作为独立结果文件存在且 PE 头有效后可以检查嵌入签名。信任检查关闭界面并限制为本地缓存结果仍受本机证书和缓存内容影响返回状态码应原样保留。// 意义按指定信任操作验证文件签名。// 返回ERROR_SUCCESS 表示信任检查成功。其它 LONG 值为信任提供程序状态码。LONGWinVerifyTrust(HWND hwnd,// 无 UI 检查时传 nullptr。GUID*pgActionID,// 指向 WINTRUST_ACTION_GENERIC_VERIFY_V2。LPVOID pWVTData// 指向已初始化的 WINTRUST_DATA。);验证完成后要调用一次WTD_STATEACTION_CLOSE。这个调用关闭信任提供程序的状态数据成功和失败路径都需要执行。WINTRUST_FILE_INFO fileInfo{};fileInfo.cbStructsizeof(fileInfo);fileInfo.pcwszFilePathcandidate.c_str();WINTRUST_DATA data{};data.cbStructsizeof(data);data.dwUIChoiceWTD_UI_NONE;data.fdwRevocationChecksWTD_REVOKE_NONE;data.dwUnionChoiceWTD_CHOICE_FILE;data.pFilefileInfo;data.dwStateActionWTD_STATEACTION_VERIFY;data.dwProvFlagsWTD_CACHE_ONLY_URL_RETRIEVAL;GUID actionWINTRUST_ACTION_GENERIC_VERIFY_V2;LONG statusWinVerifyTrust(nullptr,action,data);data.dwStateActionWTD_STATEACTION_CLOSE;WinVerifyTrust(nullptr,action,data);完整可运行程序在附件https://wangweicm.lanzouu.com/iMFYM3xrmete