深入分析Mimikatz:SSP

阅读量    52582 | 评论 1   稿费 200

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

0x00 前言

在前一篇文章中,我们开始深入分析Mimikatz。我们的想法很简单,就是想澄清Mimikatz内部的工作原理,以便开发自定义和有针对性的payload。微软引入了一些安全控制机制(如Credential Guard),避免攻击者转储凭据信息。在本文中,我们将回顾一下绕过这种机制的巧妙方法,然后提取我们所需的凭据。这里我们想要分析的是Mimikatz所支持的SSP功能。

SSP(Security Support Provider)是一个DLL,允许开发者提供一些回调函数,以便在特定认证和授权事件期间调用。在前一篇文章中,我们可以了解到WDigest正是使用这个接口来缓存凭据。

Mimikatz为我们提供了利用SSP的其他一些不同技术。首先是“Mimilib”,这是具备各种功能的一个DLL,其中一个功能就是实现了SSP接口。其次是“memssp”,这是完成相同任务的另一种有趣方式,但这种方法需要patch内存,而不是单单加载DLL那么简单。

首先试一下以传统方式来加载SSP:Mimilib。

备注:与前一篇文章相同,本文大量用到了Mimikatz源代码,Mimikatz开发人员在这上面花了大量精力。感谢Mimikatz、Benjamin Delpy以及Vincent Le Toux的杰出工作。

 

0x01 Mimilib

Mimilib就像变色龙一样,支持利用ServerLevelPluginDll来通过RPC进行横向移动、DHCP Server Callout,甚至也可以作为WinDBG扩展。在本文中,我们主要关注的是这个库如何充当SSP角色,使攻击者能在受害者输入凭据时提取到目标信息。

系统在调用SSP时,会通过SSP接口传递明文凭据,这意味着我们可以提取到明文凭据,这也是Mimilib的理论基础。Mimilib SSP功能的入口点位于kssp.c中的kssp_SpLsaModeInitialize函数。DLL通过mimilib.def定义文件,将该函数导出为SpLsaModeInitializelsass会使用该函数来初始化包含多个回调的一个结构体。

Mimilib注册的回调函数包括:

  • SpInitialize:用来初始化SSP,提供一个函数指针列表。
  • SpShutDown:卸载SSP时就会被调用,以便释放资源。
  • SpGetInfoFn:提供SSP相关信息,包括版本、名称以及描述。
  • SpAcceptCredentials:接收LSA传递的明文凭据,以便SSP缓存。

如果大家看过上一篇文章,就知道WDigest会使用SpAcceptCredentials来缓存凭据,这也是多年来我们一直能成功提取凭据的切入点。

了解这些背景后,Mimilib所需要做的就是在SpAcceptCredentials被调用后保存传入的明文凭据,这正是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,但根据微软的官方文档,我们可以添加注册表项、重启系统就能添加SSP。

然而经过一番搜索后,我找到了一则推文:

这里直接提到了AddSecurityPackage这个API,@mattifestationInstall-SSP.ps1脚本中利用这个API来加载SSP。这意味着实际上我们可以在不重启的情况下添加Mimilib。当添加成功后,我们发现每次进行身份认证时,凭据信息都会被写入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: %snComment: %snn", packages[i].Name, packages[i].Comment);
        }
    }
}

如下图所示,输出结果中包含已加载每个SSP的相关信息,其中大家可能会注意到有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,这是我们可以使用的另一个较好的候选方案。

 

0x02 MemSSP

MemSSP回到了处理lsass内存的老路上,这一次MemSSP会识别并patch一些函数,重定向执行逻辑。

来看一下源头函数:kuhl_m_misc_memssp。这里我们可以看到代码会打开lsass进程,开始搜索msv1_0.dll,这个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.dllSpAcceptCredentials函数,以便恢复凭据信息。让我们使用调试器看一下添加后的hook长啥样子。

首先我们确认SpAcceptCredentials中包含一个hook:

接下来当我们逐步执行时,会进入一段代码逻辑,其中会在栈上创建一个文件名,将其传递给fopen,以便创建一个log文件:

一旦打开该文件,传递给SpAcceptCredentials的凭据就会被写入该文件中:

最后,执行流程会被重定向回msv1_0.dll

如果大家想查看负责这个hook的代码,可以在kuhl_m_misc.c源文件的misc_msv1_0_SpAcceptCredentials函数中找到相应代码。

那么我们使用这种技术的风险在哪呢?从前面分析中,我们可以看到hook会通过kull_m_memory_copy被拷贝到lsass中,该函数实际上使用的是WriteProcessMemory。根据目标具体环境,调用WriteProcessMemory插入另一个进程可能会被检测到,或者被标记为可疑行为,等等。特别当目标进程是lsass时,这种行为更加可疑。

现在对我们来说,深入分析Mimikatz使用的具体技术可以帮我们修改与lsass的交互行为,使蓝队更难发现我们的踪迹。接下来让我们看一下如何增加整个过程的复杂度。

 

0x03 不使用WriteProcessMemory重构memssp

回顾前面分析的技术后,我们能找到各自的优点以及缺点。

第一种方法(Mimilib)需要注册SSP,而这种行为可以通过EnumerateSecurityPackages枚举已注册的SSP列表来定位。此外,如果Mimilib库没有经过修改,那么DLL中还包含大量其他功能。另外一方面,当使用AddSecurityProvider来加载时,注册表键值会被修改,以便系统重启时还能保持SSP驻留。也就是说,这种方法最大的优点在于不需要调用有潜在风险的WriteProcessMemory API就能完成任务。

第二种方法(memssp)需要依赖容易被监控的API(如WriteProcessMemory),利用这些API来hook到lsass中。这种方法的最大优点就是不会存在于已注册的SSP列表中,也不会存在于已加载的DLL中。

那么我们可以做些什么呢?我们可以将这两种方法结合起来,使用AddSecurityProvider来加载我们的代码,同时避免自己出现在已注册的SSP列表中。我们需要找到方法避免直接调用AddSecurityProvider API,如果成功的话,这样就能绕过各种烦人的AV或者EDR(这些解决方法可能会hook这个函数)。

让我们先来看看AddSecurityPackage注册SSP的具体过程,这意味我们需要做一些逆向分析。我们先观察导出该API的DLL:Secur32.dll

在Ghidra中打开这个DLL,就可以看到这实际上是个封装库,会调用sspcli.dll

反汇编sspcli.dll中的AddSecurityPackage(特别是该函数所使用的外部API调用),我们可以找到NdrClientCall3,这意味着该函数正在使用RPC。这一点很正常,因为这个调用需要向lsass发送信号,通知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_1arg_2的大小,arg_3arg_4以及arg_5都标记为out)。我发现完成该任务最简单的方法就是启动调试器,然后在AddSecurityPackage调用NdrClientCall3之前插入断点:

暂停执行后,我们可以dump出传入的每个参数的值。我们可以使用dq rsp+0x20 L1来获取arg_1参数中传递的缓冲区大小值。

因此,我们知道在这种情况下,传入的缓冲区大小为0xEC字节。现在我们可以使用如下命令来dump出arg_2

经过一番挖掘后,我成功将大多数值关联起来。让我们以QWORD来重新格式化输出,这样能较为清晰地梳理我们正在处理的数据:

现在我们已经映射出传入的大部分数据,我们可以尝试在不直接使用AddSecurityPackage API的情况下发起RPC调用。大家可以访问Gist下载我构造的代码。

在不直接调用AddSecurityPackage下我们已经能够加载包,接下来我们看看能否进一步使这个过程更加隐蔽。

让我们使用Ghidra载入sspisrv.dll,观察服务端如何处理RPC调用。反汇编SspirCallRpc后,我们很快就发现执行流程会通过gLsapSspiExtension来传递:

这实际上使指向函数数组的一个指针,通过lsasrv.dll提供,会指向LsapSspiExtensionFunctions

我们对SspiExCallRpc比较感兴趣,这与我们在RPCView中观察到的非常相似。该函数会验证参数值,并将执行流程传递给LpcHandler

LpcHandler在将执行权交给DispatchApi之前,会进一步检查所提供的参数:

同样,这里会使用另一个函数数组指针来调度LpcDispatchTable所指向的函数调用:

现在我们应该对这个数组比较感兴趣,因为我们可能会根据函数名,找到其中的s_AddPackage,并且这个函数的索引值与我们在请求中找到的0xb “Function ID”索引值相匹配。

沿着线索进一步走下去,我们找到了WLsaAddPackage,该函数首先会检查我们是否具备足够的权限来调用RPC方法,具体操作就是模拟(impersonate)客户端,然后尝试以Read/Write权限打开HKLM\System\CurrentControlSet\Control\Lsa注册表项:

如果操作成功(请注意这是可用于权限提升的一个较新颖的后门技术),那么执行权就会继续交给SpmpLoadDll,后者会通过LoadLibraryExW将我们提供的SSP加载到lsass中:

如果SSP成功加载,那么DLL就会被添加到注册表中,以实现自动加载:

这里我们可能希望跳过这个操作,因为我们不希望使用这种方法实现本地驻留,并且如果没有必要,我们也不希望涉及到注册表操作。理想状态下,如果引起怀疑(比如防御方通过ProcessExplorer来分析时),我们还希望这个DLL不会出现在lsass载入列表中。因此我们可以使用RPC调用来传递我们的DLL,在SSP的DllMain中返回FALSE,强制SSP加载失败。这样就会跳过注册表修改操作,也意味着我们的DLL会从进程中卸载。

我以Mimikatz的memssp作为模板构造了一个DLL,可以通过我们的RPC调用来加载,使用Mimikatz所用的相同hook来patch SpAddCredentials。大家可以访问Gist下载源代码。

使用我们的AddSecurityPackage RPC调用来加载DLL的整个过程参考此处视频

使用这种方法时,我们也不一定需要在本地系统中才能加载DLL,如果通过RPC调用,我们也可以使用UNC路径(但我们需要确保EDR并不会将这种操作标记为可疑行为)。

当然,我们也不一定要使用AddSecurityPackage来加载这个DLL。我们构造了一个独立版的DLL,可以实现memssp patch。我们可以使用前一篇文章中的SAMR RPC脚本,利用该脚本通过LoadLibrary来加载我们的DLL,获取使用SMB共享的登录操作信息,整个过程可以参考此处视频

此外,还有很多方法能够改进这些方法的有效性。但与前一篇文章一样,我希望本文能给大家提供一个思路,让大家了解如何构造自己的SSP,以便在行动中更加得心应手。本文只提供了能够隐蔽将SSP载入lsass过程的一些参考方法,澄清Mimikatz实现该过程的具体原理。大家可以根据这些信息,在实际环境中定制自己的payload,以便绕过AV或者EDR,或者可以用来测试蓝队在Mimilib和memssp之外是否存在其他检测能力。

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多