在上篇文章(探索Mimikatz-第1部分-Wdigest)中,我们初步探索了Mimikatz。目的很单纯,就是为了搞清楚mimikatz其内部原理,以便开发定制专用的payload。这篇文章中,我们详探一种绕过Microsoft安全控制的一种好方法,该安全控制的主要目的是防止凭据(例如Credential Guard)的转储以及提取。而这对应的功能,就是Mimikatz对SSP的支持。
SSP(Security Support Provider,安全支持提供程序)是一个DLL,在某些身份验证和授权事件过程中,它允许开发人员公开要调用的许多回调函数。正如我们在上一篇文章中所看到的,Wdigest
提供了使用此接口缓存的凭据。
Mimikatz提供了几种不同的技术来利用SSP。首先是Mimilib
,它是一个具有多种功能的DLL,包括实现SSP接口。其次是smemssp
,与前者干的是同一件事,但是是用过patch内存来达到目的,而不是加载DLL。
让我们从传统方式加载SSP开始,即Mimilib。
致谢:如前一篇文章所述,这篇文章大量使用了Mimikatz的源代码,其开发人员投入了大量的时间。感谢Mimikatz,Benjamin Delpy和Vincent Le Toux等父老乡亲。
Mimilib有点万金油,它既支持ServerLevelPluginDll
通过RPC进行横向移动,也支持DHCP服务器调用,甚至还充当WinDBG的扩展。回到本文,我们将研究该库如何充当SSP,从而为攻击者提供一种在受害者输入凭据时能够提取这些信息的方法。
系统使用明文凭据调用SSP,这意味着Mimilib可以窃取明文凭证。Mimilib的SSP函数入口点位于kssp.c中,具体来说是kssp_SpLsaModeInitialize
。
该函数通过mimilib.def
定义文件从DLL作为SpLsaModeInitialize
导出,被lsass
用来初始化一些包含多个回调函数的结构。
对于Mimilib,注册的回调函数为:
SpInitialize --用于初始化SSP并提供函数指针列表。
SpShutDown --要求卸载SSP,从而有机会释放资源。
SpGetInfoFn --提供有关SSP的信息,包括版本,名称和描述。
SpAcceptCredentials --接收由LSA传递并由SSP缓存的纯文本凭据。
如果您阅读了上一篇文章就会知道,WDigest使用SpAcceptCredentials
来缓存凭据,多年来大家一直通过这个切入点来利用,屡试不爽。
并且,SpAcceptCredentials
使用明文凭据的副本进行调用。那么,Mimilib剩下的工作就是简简单单地存储凭据就ok了,而这正是kssp_SpAcceptCredentials
函数所干的活:
NTSTATUS NTAPI kssp_SpAcceptCredentials(SECURITY_LOGON_TYPE LogonType, PUNICODE_STRING AccountName, PSECPKG_PRIMARY_CRED PrimaryCredentials, PSECPKG_SUPPLEMENTAL_CRED SupplementalCredentials)
{
FILE *kssp_logfile;
#pragma warning(push)
#pragma warning(disable:4996)
if(kssp_logfile = _wfopen(L"kiwissp.log", L"a"))
#pragma warning(pop)
{
klog(kssp_logfile, L"[%08x:%08x] [%08x] %wZ\\%wZ (%wZ)\t", PrimaryCredentials->LogonId.HighPart, PrimaryCredentials->LogonId.LowPart, LogonType, &PrimaryCredentials->DomainName, &PrimaryCredentials->DownlevelName, AccountName);
klog_password(kssp_logfile, &PrimaryCredentials->Password);
klog(kssp_logfile, L"\n");
fclose(kssp_logfile);
}
return STATUS_SUCCESS;
}
现在,我不相信mimikatz.exe直接提供了加载Mimilib的功能,但是从Microsoft的文档中我们知道,系统是通过添加注册表项和重新启动来添加SSP 。
经过一番搜索,我发现了这条推文:
上图直接提到了对 AddSecurityPackage
这个API的引用,该API 实际上在@mattifestation的Install-SSP.ps1
脚本中使用,以加载SSP,这意味着实际上可以通过添加Mimilib而无需重新启动系统。并且在SSP加载后,每次身份验证都会将凭据写入到kiwissp.log
文件中:
现在在目标环境中使用SSP有一个缺点,那就是必须在lsass中注册SSP。当需要追踪攻击者的恶意活动的时候,这为防御者提供了很多便利,不管是创建用来引用SSP的注册表项,还是只是lsass进程中的一个异常DLL,都能发现攻击者的蛛丝马迹。另外,SSP还公开了名称和注释,可以使用EnumerateSecurityPackages
函数枚举它们:
#define SECURITY_WIN32
#include <stdio.h>
#include <Windows.h>
#include <Security.h>
int main(int argc, char **argv) {
ULONG packageCount = 0;
PSecPkgInfoA packages;
if (EnumerateSecurityPackagesA(&packageCount, &packages) == SEC_E_OK) {
for (int i = 0; i < packageCount; i++) {
printf("Name: %s\nComment: %s\n\n", packages[i].Name, packages[i].Comment);
}
}
}
如下所示,输出信息显示每个已加载的SSP信息,其中Mimilib
可能会有些与众不同:
有没有办法不让Mimilib
那么突出,那就是修改Mimilib
的SpGetInfo
回调函数返回的描述,该描述被硬编码为:
NTSTATUS NTAPI kssp_SpGetInfo(PSecPkgInfoW PackageInfo)
{
PackageInfo->fCapabilities = SECPKG_FLAG_ACCEPT_WIN32_NAME | SECPKG_FLAG_CONNECTION;
PackageInfo->wVersion = 1;
PackageInfo->wRPCID = SECPKG_ID_NONE;
PackageInfo->cbMaxToken = 0;
PackageInfo->Name = L"KiwiSSP";
PackageInfo->Comment = L"Kiwi Security Support Provider";
return STATUS_SUCCESS;
}
更改Name和Comment字段之后,如下图所示:
显然这仍然不是很好(即使使用如此优秀的名称和注释字段也不行)。因为,Mimilib不需要剥离和重新编译,它包含了许多功能,而不仅仅是充当SSP。
那么到底该如何解决呢?幸运的是Mimikatz还支持misc::memssp
,它提供了一个不错的选择。
MemSSP的原理大致是处理lsass内存,通过标识和patch函数以重定向执行来进行。
看看入口函数kuhl_m_misc_memssp
。如下所示,可以看到lsass进程已打开,并且开始搜索msv1_0.dll
,这是一个支持交互式身份验证的验证程序包:
NTSTATUS kuhl_m_misc_memssp(int argc, wchar_t * argv[])
{
...
if(kull_m_process_getProcessIdForName(L"lsass.exe", &processId))
{
if(hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION, FALSE, processId))
{
if(kull_m_memory_open(KULL_M_MEMORY_TYPE_PROCESS, hProcess, &aLsass.hMemory))
{ if(kull_m_process_getVeryBasicModuleInformationsForName(aLsass.hMemory, L"msv1_0.dll", &iMSV))
{
…
接下来,在内存中搜索,与我们在WDigest中看到的类似:
...
sSearch.kull_m_memoryRange.kull_m_memoryAdress = iMSV.DllBase;
sSearch.kull_m_memoryRange.size = iMSV.SizeOfImage;
if(pGeneric = kull_m_patch_getGenericFromBuild(MSV1_0AcceptReferences, ARRAYSIZE(MSV1_0AcceptReferences), MIMIKATZ_NT_BUILD_NUMBER))
{
aLocal.address = pGeneric->Search.Pattern;
if(kull_m_memory_search(&aLocal, pGeneric->Search.Length, &sSearch, TRUE))
{
…
如果暂停对代码的审查,使用Ghidra
工具,就可以搜索正在使用的匹配模式,定位到这里:
在这里,我们可以看到真实内幕:memssp被用于hook msv1_0.dll中的SpAcceptCredentials
函数,以恢复凭证信息。让我们一头扎进调试器中看看添加后该hook是啥样子的。
首先,我们确认SpAcceptCredentials
包含一个hook:
接下来逐步执行,进入到一个负责创建日志文件的存根,方法是在堆栈上构建文件名并将其传递给fopen:
打开后,传递给SpAcceptCredentials
的凭据将写入此文件:
最后执行直接返回到msv1_0.dll
:
如果您想查看此hook的源代码,可以在kuhl_m_misc.c的misc_msv1_0_SpAcceptCredentials函数中找到。
那么,使用这种技术的风险是什么?我们可以看到上面的hook通过kull_m_memory_copy
函数复制到了lsass中,实际上使用的是WriteProcessMemory
。根据环境的不同,WriteProcessMemory
被另一个进程调用时可能会被检测到,或者被标记为可疑,当这个进程是lsass时就更可疑了。
现在,探索Mimikatz技术的好处之一是使我们能够改变与lsass交互的过程,使蓝队更难以检测它的活动痕迹。因此,让我们看看我们可以做些什么来实现这一过程。
WriteProcessMemory
的情况下重新创建memssp回顾上述两种技术,可以说各有其优缺点。
第一种方法(Mimilib)依赖于注册SSP,可通过EnumerateSecurityPackages
返回一个已注册的SSP列表来定位。另外,如果未修改Mimilib库,则DLL会有许多附加功能。此外,加载AddSecurityProvider
时,注册表值将被修改,目的是在重新启动系统后能够保留SSP。也就是说,这项技术的一大优势在于,它不需要存在潜在风险的WriteProcessMemoryAPI
调用即可实现其目标。
第二种方法(memssp)在很大程度上依赖于受监视的API调用,例如WriteProcessMemory
,该API用于将hook加载到lsass中。但是,此技术的一大优势是它不会出现在已注册的SSP列表中,也不会存在于已加载的DLL中。
那么,有没有更好的办法呢?一般这样子问那肯定是有的,那就是把这两种方法结合起来:使用AddSecurityProvider
来加载代码,同时避免自身出现在已注册的SSP中,以及找到避免直接调用AddSecurityProvider
API的方法。这种方法应该有助于解决那些令人恼火的AV或EDR(取决于hook该函数)。
先来看一下AddSecurityPackage
注册SSP的工作方式,其中涉及一些逆向操作。从公开此API的Secur32.dll
DLL开始。
在Ghidra中打开,可以看到,它实际上只是对sspcli.dll
进行调用的一个封装:
在sspcli.dll中反汇编上图中的AddSecurityPackage
,特别是此函数使用的传出API调用,我们可以看到对NdrClientCall3
的引用,这意味着此函数正在利用RPC。这一步意义非凡,因为此调用需要以某种方式向lsass发出信号,表明应该加载新的SSP:
跟随对NdrClientCall3
的调用时,发现它传递了以下参数:
nProcNum
参数值为3,如果详探sspirpc_ProxyInfo结构,将RPC接口UUID设为4f32adc8-6052-4a04-8701-293ccf2096f0:
现在,我们已经掌握了足够多的信息,可以在RpcView来看看通过sspisrv.dll
公开为SspirCallRpc
的RPC调用:
要使用此调用,需要知道传递的参数,可以从RpcView中找到:
long Proc3_SspirCallRpc(
[in][context_handle] void* arg_0,
[in]long arg_1,
[in][size_is(arg_1)]/*[range(0,0)]*/ char* arg_2,
[out]long* arg_3,
[out][ref][size_is(, *arg_3)]/*[range(0,0)]*/ char** arg_4,
[out]struct Struct_144_t* arg_5);
但是,在执行此调用之前,需要知道要作为参数传递的arg_2值(arg_1标记为arg_2的大小,arg_3,arg_4和arg_5都被标记为“ out”)。我发现执行此操作最简单的方法是:启动调试器并在AddSecurityPackage
调用NdrClientCall3
之前添加一个断点:
暂停执行后,可以转储每个参数中传递的值。使用以下命令获取在arg_1参数传递的缓冲区大小:dq rsp+0x20 L1
:
在这种情况下,传递的缓冲区大小为0xEC字节。现在我们可以转储arg_2:
经过一番探索发现,我能够关联大多数这些值。将输出请求重新格式化为QWORD
并标记,以便清晰看到要处理的数据:
现在我们已经映射了要传递的大多数数据,我们可以尝试发出RPC调用,而不必直接调用AddSecurityPackage
API调用。我为此编写的代码参见此处。
现在已经无需直接调用AddSecurityPackage
就可以加载程序,下一步再看看其他骚姿势。
把sspisrv.dll
加载进Ghidra,看看如何在服务器端处理RPC调用。我们在反汇编SspirCallRpc
时遇到的直接问题是执行流程过程是通过gLsapSspiExtension
传递的:
这实际上是指向函数数组的指针,通过填充lsasrv.dll并指向LsapSspiExtensionFunctions
:
SspiExCallRpc
与RPCView
的内容非常相似,这引起了我的兴趣。此函数验证参数并将执行过程传递到LpcHandler
:
在最终将执行传递给DispatchApi
之前,LpcHandler
负责进一步检查提供的参数:
同样,系统使用另一个函数指针数组来分派LpcDispatchTable
指向的调用:
这是一个很有意思的数组,因为我们很可能会根据名称查找s_AddPackage
,并且索引也与在请求中找到的0xb“函数ID”索引匹配。
继续往下走,到达WLsaAddPackage
函数,该函数检查我们是否有足够的权限调用RPC方法,检查过程为:首先模拟了连接的客户端,然后尝试打开HKLM\System\CurrentControlSet\Control\Lsa
的注册表项(具备读/写权限),如下图:
如果成功(注意这可能是一个新的提权后门),则执行过程会转移至SpmpLoadDll
,该函数被用于将提供的SSP加载到lsass中(通过LoadLibraryExW
命令):
如果成功加载了SSP,则将DLL添加到注册表中以进行自动加载:
我们可能会跳过最后一点,因为我们不会一直停留在这个过程(比如dump个密码就跑路了),另外最好避免接触注册表。防守方可能会利用像ProcessExplorer
这样的工具列出lsass进程中我们的DLL,所以最好不要引起他们的怀疑。因此,我们可以使用RPC调用来传递DLL,并通过DllMain
进程返回一个FALSE来强制我们的SSP加载失败,这将导致跳过修改注册表这一步骤,同时也意味着我们的DLL会从进程中卸载掉。
使用Mimikatz的memssp
作为模板,我制作了一个通过RPC调用加载的DLL,它将与Mimikatz一样,使用相同的hook来patch SpAddCredentials
。源代码在这里
大家也不受此限制从本地系统加载DLL,因为如果通过RPC调用传递了UNC路径,效果也很好(但应确保目标EDR不会将其标记为可疑)。
当然,您也不仅限于使用AddSecurityPackage
加载此DLL 。当我们精心制作了一个独立的DLL来patch memssp
时,可以使用上一篇博文中的SAMR RPC脚本,通过LoadLibrary
加载我们的DLL,并通过SMB共享写回登录尝试。
当然,有很多方法可以提高这些示例的有效性,但是与第1部分一样,我希望本文能给大家打开思路,让大家可以diy自己的SSP。尽管本文仅介绍了几种将SSP加载到lsass中的方法,但是通过了解Mimikatz如何提供此功能,在尝试绕过AV或EDR时,大家还是很有希望能够根据环境调整自己的payload,或者用来测试蓝队在Mimilib
和memssp
之外的防护功能。