对于mimikatz的利用方式:封装,打包,进程注入,powershell化,现在又出现了内存转储的利用方式,真可谓无所不用其极。无他,就因为Mimikatz仍然是从 Windows系统lsass上提取凭据时的首选工具。当然,这是由于当Microsoft引入了每个新的安全控制措施,GentilKiwi总是袖手旁观。如果您曾经研究过Mimikatz所做的工作,那么你会发现,这并不是一件容易的事,因为它支持Windows x86和x64的所有版本(最近还增加了支持ARM arch架构的Windows)。当然,随着Mimikatz多年来的成功,蓝队现在非常善于检测其多种形式的用途。本质上,在主机上执行Mimikatz,如果环境完全成熟,你的行为可能会被标记。

通过线上线下采访别人,现在的我对红队的想法是:不仅可以执行脚本,还可以理解他们所使用的工具。而且,我们发现mimikatz出新姿势的速度,永远比不上安全供应商打压的速度。因此,了解mimikatz特定技术如何作用于API调用,可以在防护严密的环境中避免被发现。

话虽这么说,Mimikatz是一种以各种形式附带在大多数后开发工具套件的工具。并且,尽管一些安全供应商正在监视与lsass的进程交互,但更多安全厂商决定尝试识别Mimikatz本身。

我一直在考虑剥离Mimikatz以进行某些活动(主要是那些不可行或不允许进行内存转储的活动),但很长一段时间内我都很困扰,因为平时对它接触比较少,所以我花了很长时间来研究。

因此,在未来几篇博文中,我想探索一下它的神奇之处,从一切开始的地方开始:WDigest。具体来说,探索在lsass中明文凭证是如何被缓存的,以及如何使用将该凭证从内存中将sekurlsa::wdigest提取出来。这将意味着涉及拆卸和调试过程,故而,虽然很难复制Mimikatz所做的工作,但是如果仅使用一小部分可用功能,还是值得根据Mimikatz源代码diy出一个小工具。

最后,我还将探讨在lsass中加载任意DLL的一些方法,希望可以将其与演示的代码示例结合使用。

注意:这篇文章大量使用了Mimikatz的源代码,其开发人员投入了大量的时间。向他们致敬。

那么,神奇的sekurlsa :: wdigest实际上是如何工作的呢?

如前所述,在本文中,我们将讨论的是WDigest,这可以说是Mimikatz最著名的功能了。WDigest凭据缓存默认情况下处于启用状态,直到Windows Server 2008 R2,此后版本禁用了纯文本凭据缓存。

反编译操作系统组件时,我通常喜欢附加调试器,并查看其在运行时如何与操作系统交互。不幸的是,在这种情况下,mimikatz不会像将WinDBG附加到lsass一样那么简单,因为很快Windows会停止运行,然后警告您有待重启。相反,我们必须将之附加到内核并从Ring-0切换到lsass进程。如果你以前从来没有附加过WinDBG内核,可以看看我以前的博客,里面提及了如何去建立一个内核调试器。[传送门](http://blog.xpnsec.com/windows-warbird-privesc/)。

附加了内核调试器后,我们需要获取lsass进程的EPROCESS地址,该地址可通过以下!process 0 0 lsass.exe命令找到:

EPROCESS地址(上图是ffff9d01325a7080)被标记之后,我们可以请求将调试会话切换到lsass进程上下文:

一个简单lm的例子将表明我们现在可以访问WDigest DLL内存空间:

如果此时您发现字符没有被正确处理,可以使用命令.reload /user,通常会有所帮助。

附加了调试器后,就可以深入研究WDigest了。

深入研究wdigest.dll(涉及lsasrv.dll)

如果查看Mimikatz源代码,可以看到内存中识别凭据的进程是扫描签名。使用时下流行的工具Ghidra,看看Mimikatz在寻找什么。

我的计算机版本是Windows 10 x64,因此我将重点介绍PTRN_WIN6_PasswdSet签名,如下图:

将此搜索签名提供给Ghidra之后,我们发现。Mimikatz扫描内存以查找的内容为:

上图出现了LogSessHandlerPasswdSet函数。具体来说,签名引用仅在l_LogSessList指针之外。该指针是从WDigest中提取凭证的关键,但是在行动之前,先做一个备份,然后通过检查交叉引用来弄清楚到底是什么调用了此函数,然后来到了这里:

在这里,我们从WDigest.dll中导出了一个函数SpAcceptCredentials,但这是做什么的呢?

这看起来很有搞头,因为凭证是通过此回调函数传递的。为了确认没有找错地方,在WinDBG中添加一个断点:bp wdigest!SpAcceptCredentials,然后使用runas命令在Windows上生成外壳程序:

这应该足以触发断点。检查调用的参数,可以看到传入的凭据:

如果继续执行并在wdigest!LogSessHandlerPasswdSet上添加另一个断点,则会发现,尽管传递了用户名,但看不到代表密码的参数。但是,如果我们在调用LogSessHandlerPasswdSet之前看一下,我们会发现:

这实际上是用于Control Flow Guard的存根(Ghidra 9.0.3看起来对显示CFG存根有改进),但后续是在调试器中,该调用实际上是针对LsaProtectMemory的:

预料之中,因为我们知道凭据是加密存储在内存中的。不幸的是,LsaProtectMemory函数没有在lsass之外公开,因此我们需要知道如何重新创建这个函数来解密提取的凭证。接下来的反汇编程序显示此调用实际上只是一个针对LsaEncryptMemory的封装:

并且LsaEncryptMemory实际上是BCryptEncrypt的封装调用:

有趣的是,加解密函数是基于所提供的要加密的数据块长度来选择的。如果提供的缓冲区长度可以被8整除(由上图中的param_2&7按位操作提供),则使用AES。如果失败,则使用3Des

现在,我们知道我们的密码由BCryptEncrypt加密,但是密钥呢?上图中,我们可以看到对lsasrv!h3DesKeylsasrv!hAesKey的引用。跟踪对这些地址的引用发现,地址lsasrv!LsaInitializeProtectedMemory用于为每个地址分配一个初始值。具体来说,每个键都是基于对BCryptGenRandom的调用生成的:

这意味着,每次lsass启动时都会随机生成一个新密钥,在解密任何缓存的WDigest凭据之前,必须先提取新密钥。

回到Mimikatz源代码,我们看到,确实存在对LsaInitializeProtectedMemory函数的搜寻,并再次提供了针对不同Windows版本和体系结构的完整签名列表:

如果我们在Ghidra中进行搜索,我们会发现它位于此处:

由上图可以看到对hAesKey地址的引用。因此,类似于上述签名搜索,Mimikatz会寻找内存中的加密密钥。

接下来,我们需要了解Mimikatz如何将密钥从内存中取出。为此,需要使用Mimikatz中的kuhl_m_sekurlsa_nt6_acquireKey,高亮处显示了该工具在支持不同OS版本方面所需要的长度。我们看到hAesKeyh3DesKey(实际上是从BCRYPT_KEY_HANDLE返回的类BCryptGenerateSymmetricKey)指向内存中的一个结构,该结构由包含所生成的对称AES3DES密钥的字段组成。可以在Mimikatz中找到该结构的文档:

typedef struct _KIWI_BCRYPT_HANDLE_KEY {
    ULONG size;
    ULONG tag;  // 'UUUR'
    PVOID hAlgorithm;
    PKIWI_BCRYPT_KEY key;
    PVOID unk0;
} KIWI_BCRYPT_HANDLE_KEY, *PKIWI_BCRYPT_HANDLE_KEY;

通过检查上面引用的UUUR标记,可以将其与WinDBG相关联,以确保我们不会出错:

在偏移量0x10处,我们看到Mimikatz正在引用PKIWI_BCRYPT_KEY,它具有以下结构:

typedef struct _KIWI_BCRYPT_KEY81 {
    ULONG size;
    ULONG tag;  // 'MSSK'
    ULONG type;
    ULONG unk0;
    ULONG unk1;
    ULONG unk2; 
    ULONG unk3;
    ULONG unk4;
    PVOID unk5; // before, align in x64
    ULONG unk6;
    ULONG unk7;
    ULONG unk8;
    ULONG unk9;
    KIWI_HARD_KEY hardkey;
} KIWI_BCRYPT_KEY81, *PKIWI_BCRYPT_KEY81;

毫无疑问,下图内容与WinDBG使用相同的引用标记:

该结构的最后一个成员是对Mimikatz的引用,名为KIWI_HARD_KEY,包含以下内容:

typedef struct _KIWI_HARD_KEY {
    ULONG cbSecret;
    BYTE data[ANYSIZE_ARRAY]; // etc...
} KIWI_HARD_KEY, *PKIWI_HARD_KEY;

该结构由cbSecret键的大小组成,其后是data字段中的实际键。这意味着我们可以使用WinDBG通过以下方式提取此密钥:

由上图可知,h3DesKey长为0x18字节,内容为:

b9 a8 b6 10 ee 85 f3 4f d3 cb 50 a6 a4 88 dc 6e ee b3 88 68 32 9a ec 5a。

知道了这一点,我们可以遵循相同的过程来提取hAesKey

现在我们了解了密钥的提取方式,需要寻找WDigest缓存的实际凭据。回到之前讨论的l_LogSessList指针。此字段对应于一个链表,我们可以使用WinDBG命令!list -x "dq @$extret" poi(wdigest!l_LogSessList)逐步进行浏览:

这个矩阵的结构包含以下字段:

typedef struct _KIWI_WDIGEST_LIST_ENTRY {
    struct _KIWI_WDIGEST_LIST_ENTRY *Flink;
    struct _KIWI_WDIGEST_LIST_ENTRY *Blink;
    ULONG   UsageCount;
    struct _KIWI_WDIGEST_LIST_ENTRY *This;
    LUID LocallyUniqueIdentifier;
} KIWI_WDIGEST_LIST_ENTRY, *PKIWI_WDIGEST_LIST_ENTRY;

在此结构之后是LSA_UNICODE_STRING字段,存在于以下三个偏移处:

• 0x30-用户名
    • 0x40-主机名
    • 0x50-加密密码

可以使用以下命令检查WinDBG的正确路径:

!list -x "dS @$extret+0x30" poi(wdigest!l_LogSessList)

这会将转储的用户名转储为:

最后,我们可以使用类似的命令转储加密的密码:

!list -x "db poi(@$extret+0x58)" poi(wdigest!l_LogSessList)

到此,从内存中提取WDigest凭据所需的所有步骤均已完成。

既然我们拥有提取和解密进程所需的所有信息,将这些信息组合到Mimikatz之外的小型独立工具中是否可行?为了探索这一点,我创建了一个备受欢迎(饱受非议)的POC,传动门。在Windows 10 x64(内部版本1809)上执行时,它提供有关提取凭据过程的详细信息:

这个poc并不是绝对安全的,但我希望通过它来举例说明我们如何着手制作替代工具。

现在,我们了解了如何获取和解密WDigest高速缓存的凭据,我们可以转到另一个影响纯文本凭据收集的区域UseLogonCredential

但是UseLogonCredential为0

众所周知,人们使用mimikatz都是转储明文凭据,因此Microsoft决定默认情况下禁用对该旧协议的支持。当然,有些用户可能正在使用WDigest,因此为了提供重新启用此功能的选项,Microsoft设置了注册表项为HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest\UseLogonCredential。将其从“ 0”切换为“ 1”会强制WDigest重新缓存凭据,这当然意味着渗透测试者就白忙活了…… 但是有一个陷阱,切换此设置需要重新启动操作系统,我还没有遇到一个客户允许在测试环境之外这样做。

显而易见的问题是:为什么需要重新启动计算机才能使其生效?

正如GentilKiwi指出的那样,此更改无需重新启动即可生效。我在本节末尾解释了原因。

让我们再看一看SpAcceptCredentials,经过一番寻找,我们发现了这一点:

可以清楚地看到,该函数使用全局变量检查了两个条件:g_IsCredGuardEnabled是否设为1g_fParameter_UseLogonCredential是否设为0,我们发现采用的代码路径是通过LogSessHandlerNoPasswordInsert而不是上面的LogSessHandlerPasswdSet来调用。顾名思义,此函数会缓存会话,而不是缓存密码,结果就导致我们在使用Windows 2012+时通常遇到的弹框行为。因此,可以合理地假定此变量由上述注册表项值基于其名称来控制,通过跟踪其分配发现情况确实是这样:

通过了解WDigest.dll中的哪些变量可以控制凭据缓存,我们可以在不更新注册表的情况下颠覆它吗?如果我们在运行时使用调试器更新参数g_fParameter_UseLogonCredential,那该怎么办?

恢复运行后,我们看到缓存凭据再一次被存储:

当然,当连接了内核调试器时,大多数事情都是可能的,但是如果有一种在不触发AV/EDR的情况下操作lsass内存的方法(请参阅我们先前的Cylance博客文章,了解如何执行此操作的示例),那么就肯定可以编写用于操纵此变量的工具。我再次编写了一个冗长的工具,以演示如何完成此工作,该工具可以在此处找到

本示例将搜索并更新内存中g_fParameter_UseLogonCredential的值。如果是在受Credential Guard保护的系统上运行,则更新此值所需的修改很简单,留给读者自行练习。

执行POC后,我们发现WDigest现在已重新启用,而无需设置注册表项,从而使我们能够在缓存凭据时提取凭据:

同样,此POC不应被认为是OpSec安全的,只是一个如何制作自己的工具的简单示例。

当然,现在启用WDigest的方法当然会带来风险,主要是对lsass的WriteProcessMemory调用,但是如果环境适合,它提供了一种无需设置注册表值即可启用WDigest的好方法。还有其他获取纯文本凭据的方法,这些方法可能更适合WDigest之外的目标(针对memssp的对象,我们将在以后的文章中进行回顾)。

悄悄话:正如GentilKiwi所指出的那样,要使UseLogonCredential生效其实并不需要重启...所以我们回到反汇编程序。

回顾引用注册表值的其他位置,我们发现了wdigest!DigestWatchParamKey,它监视许多键,包括:

用于在更新时触发此函数的Win32 API是RegNotifyKeyChangeValue

而且,如果我们在WinDBG中添加一个wdigest!DigestWatchParamKey断点,那么我们会在尝试添加UseLogonCredential时成功触发了它:

福利环节-将任意DLL加载到LSASS中

因此,在研究分解器时,我想寻找一种替代方法,将代码加载到lsass中,同时避免hook Win32 API调用或加载SSP。经过一些反汇编尝试后,我在lsasrv.dll中遇到了下图内容:

可以在函数LsapLoadLsaDbExtensionDll中找到对用户提供的值调用LoadLibraryExW的尝试,这使我们可以制作一个要加载到lsass进程中的DLL,例如:

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
// Insert l33t payload here
break;
    }
// Important to avoid BSOD
    return FALSE;
}

重要的是,在DllMain函数结束时,返回一个FALSE用来对LoadLibraryEx强行施加错误。这是为了避免随后调用GetProcAddress。否则,将导致重新启动时出现BSOD,直到删除DLL或注册表项为止。

制作完DLL之后,我们要做的就是创建上述注册表项:

New-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\NTDS -Name LsaDbExtPt -Value "C:\xpnsec.dll"

DLL的加载将在系统重新启动时发生,这使其成为潜在的特权妥协技术,将payload直接注入lsass(当然,只要不启用PPL)即可。

福利环节2-将任意DLL远程加载到LSASS

经过进一步的搜索后,在samsrv.dll中发现了与上述类似的向量。再次通过LoadLibraryEx调用将受控的注册表值加载到lsass中:

同样,我们可以通过添加注册表项并重启来利用此功能,但是触发这种情况要简单得多,因为可以使用SAMR RPC调用来触发它。

有意思的利用方式:通过使用上面的WDigest凭据提取代码来制作DLL,从而为我们转储凭据。

要加载DLL,我们可以使用一个非常简单的Impacket Python脚本来修改注册表,并添加一个密钥到HKLM\SYSTEM\CurrentControlSet\Services\NTDS\DirectoryServiceExtPt指向开放SMB共享上托管的DLL,然后使用对hSamConnect RPC的调用来触发DLL的加载。

可以在此处找到所用DLL的代码,它是对先前示例的修改。

希望本文能使您对WDigest凭据缓存的工作方式以及Mimikatz在"sekurlsa::wdigest"过程中如何读取和解密密码有所了解。更重要的是,我希望它对打算diy相关工具的任何人提供帮助。

原文链接

点击收藏 | 2 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖