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

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_0kerberos 一类条目是逻辑包名。包名来自注册表配置,模块路径、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 PackagesSecurity PackagesNotification 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_0scecli 这类内容是包标识。它们没有路径和扩展名时,可以构造 系统目录 + 包名 + .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);

完整可运行程序在附件

https://wangweicm.lanzouu.com/iMFYM3xrmete

相关推荐
YCOSA20252 小时前
雨晨 Windows 11 IOT 企业版 LTSC 24H2 轻装 2合1 26100.8968
windows·物联网
汤姆yu3 小时前
CodeGeeX 4完整安装与实操使用全指南
人工智能·windows·ai·智能体·视频模型
_wyt0013 小时前
完全背包问题详解
c++·背包dp
国科安芯4 小时前
星间光链路:AS32S601型抗辐射MCU在空间激光通信终端控制中的技术实现
服务器·网络·单片机·嵌入式硬件·物联网·安全·信息与通信
栩栩云生5 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
皓月斯语5 小时前
B3842 [GESP202306 三级] 春游 题解
数据结构·c++·算法·题解
吐了啊取名字太难5 小时前
美颜系统AI修图本地跑并支持Mac、win、安卓、iOS不卡顿
android·人工智能·windows·数码相机·mac·ai编程
huijingjituan6 小时前
新一代IM聊天软件|构建企业专属智能通讯生态
安全·实时互动·即时通讯·极光鸟·灰鲸
其实防守也摸鱼6 小时前
补天SRC新手入门指南:从0到1的漏洞挖掘之路
网络·python·学习·安全·web安全·数据挖掘·挖洞