-
-
[原创]LSA 认证、安全与通知包的注册表枚举
-
发表于: 20小时前 360
-
LSA 认证、安全与通知包的注册表枚举
本次检查的目标是:只读列出本机 LSA 的认证包、安全包、通知包和 OSConfig 安全包,并为每个包名保留来源、候选 DLL、PE 架构和嵌入签名状态。
要得到这组结果,分为四步:
- 定位四个 LSA 包列表的完整注册表来源
- 按返回字节长度读取并解析
REG_MULTI_SZ包名 - 为每个包名构造 DLL 候选路径并确认文件状态
- 读取候选 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 同名,完整注册表路径不同。记录结果时要同时保存完整键路径和值名,这样才能追溯某个包名来自哪一份配置。
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 权限,成功后的句柄必须关闭。
// 意义:打开一个注册表子键。
// 返回: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 子键或其中的值未设置时记录该状态,认证包、安全包和通知包仍继续读取。
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);
}
// 错误示例:用同一个字符串字段保存所有值名。
// 两处 Security Packages 会被误认为同一份配置。
record.name = "Security Packages";
完成第一步后,四份配置都有独立的完整键路径和值名。接下来要读取每个值返回的原始字节,并在明确边界内得到包名。
二、第二步:按返回字节长度读取并解析 REG_MULTI_SZ
注册表读取函数以字节数描述值数据。第一次调用传入空数据指针取得长度,第二次调用按该字节数读取原始数据。读取期间值发生变化时,ERROR_MORE_DATA 需要触发扩容重试。
// 意义:读取注册表值的类型与原始数据。
// 返回: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 字符边界查看。
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 从未知内存寻找它们。
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 之外,损坏配置也不会变成越界读取。
// 错误示例:假定注册表总会提供 NUL 结尾。
const wchar_t* text = reinterpret_cast<const wchar_t*>(bytes.data());
std::wcout << text; // 可能越过 bytes 的边界。
完成第二步后,已经得到每个列表项的来源和受长度限制的包名。接下来要把包名转换为可检查的文件候选,同时保留它只是候选这一事实。
三、第三步:为每个包名构造 DLL 候选路径并确认文件状态
包列表中的 msv1_0、scecli 这类内容是包标识。它们没有路径和扩展名时,可以构造 系统目录 + 包名 + .dll 作为文件定位候选。候选路径不能替代 LSA 实际加载结果。
系统目录函数返回实际复制的字符数。返回值小于缓冲区容量时才表示完整路径已写入。返回值达到容量时需要扩大缓冲区。
// 意义:取得系统目录,例如 C:\Windows\System32。
// 返回:成功时为复制的字符数,不包含结尾 NUL。0 表示失败。
// 缓冲区不足时返回所需容量,调用方必须扩大缓冲区。
UINT GetSystemDirectoryW(
LPWSTR lpBuffer, // 接收系统目录的输出缓冲区。
UINT uSize // 缓冲区容量,单位为 wchar_t 个数。
);
构造候选路径时保留原始包名。已有绝对路径时保留原路径。没有扩展名时添加 .dll。引号、内嵌 NUL、回车和制表符都作为无效包名状态显示,不把它们拼成文件路径。
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 读取。
// 意义:取得指定路径的文件系统属性。
// 返回:成功时返回属性位。失败时返回 INVALID_FILE_ATTRIBUTES,随后用 GetLastError 取得原因。
DWORD GetFileAttributesW(
LPCWSTR lpFileName // 输入,UTF-16 的 NUL 结尾路径。不能为 nullptr。
);
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,并允许其它进程读、写、删除文件,读取结束后关闭句柄。
// 意义:打开已有文件。
// 返回:成功时返回 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 签名,然后读取文件头。每一个偏移都限制在真实文件大小内,截断文件直接报告格式错误。
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 头有效后可以检查嵌入签名。信任检查关闭界面并限制为本地缓存,结果仍受本机证书和缓存内容影响,返回状态码应原样保留。
// 意义:按指定信任操作验证文件签名。
// 返回:ERROR_SUCCESS 表示信任检查成功。其它 LONG 值为信任提供程序状态码。
LONG WinVerifyTrust(
HWND hwnd, // 无 UI 检查时传 nullptr。
GUID* pgActionID, // 指向 WINTRUST_ACTION_GENERIC_VERIFY_V2。
LPVOID pWVTData // 指向已初始化的 WINTRUST_DATA。
);
验证完成后要调用一次 WTD_STATEACTION_CLOSE。这个调用关闭信任提供程序的状态数据,成功和失败路径都需要执行。
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);
完整可运行程序在附件

074K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4j5h3&6Y4N6$3g2A6j5$3#2Q4x3X3g2D9j5h3&6*7L8%4g2#2i4K6u0W2j5$3!0E0i4K6u0r3K9f1#2r3h3f1@1K6P5s2u0E0k6i4c8W2
[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!