对于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了。
如果查看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!h3DesKey
和lsasrv!hAesKey
的引用。跟踪对这些地址的引用发现,地址lsasrv!LsaInitializeProtectedMemory
用于为每个地址分配一个初始值。具体来说,每个键都是基于对BCryptGenRandom
的调用生成的:
这意味着,每次lsass启动时都会随机生成一个新密钥,在解密任何缓存的WDigest凭据之前,必须先提取新密钥。
回到Mimikatz源代码,我们看到,确实存在对LsaInitializeProtectedMemory
函数的搜寻,并再次提供了针对不同Windows版本和体系结构的完整签名列表:
如果我们在Ghidra
中进行搜索,我们会发现它位于此处:
由上图可以看到对hAesKey
地址的引用。因此,类似于上述签名搜索,Mimikatz会寻找内存中的加密密钥。
接下来,我们需要了解Mimikatz如何将密钥从内存中取出。为此,需要使用Mimikatz中的kuhl_m_sekurlsa_nt6_acquireKey
,高亮处显示了该工具在支持不同OS版本方面所需要的长度。我们看到hAesKey
和h3DesKey
(实际上是从BCRYPT_KEY_HANDLE
返回的类BCryptGenerateSymmetricKey
)指向内存中的一个结构,该结构由包含所生成的对称AES
和3DES
密钥的字段组成。可以在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
是否设为1
或g_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
时成功触发了它:
因此,在研究分解器时,我想寻找一种替代方法,将代码加载到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)即可。
经过进一步的搜索后,在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相关工具的任何人提供帮助。