深入分析Mimikatz:SSP

栏目: C · 发布时间: 5年前

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

深入分析Mimikatz:SSP

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
SpShutDown
SpGetInfoFn
SpAcceptCredentials

如果大家看过上一篇文章,就知道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。

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

深入分析Mimikatz:SSP

这里直接提到了 AddSecurityPackage 这个API, @mattifestationInstall-SSP.ps1 脚本中利用这个API来加载SSP。这意味着实际上我们可以在不重启的情况下添加Mimilib。当添加成功后,我们发现每次进行身份认证时,凭据信息都会被写入 kiwissp.log 文件中。

深入分析Mimikatz:SSP

现在在目标环境中使用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的身影:

深入分析Mimikatz:SSP

那么我们是否可以采取一些隐蔽措施呢?最明显的应该就是修改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 字段,结果如下所示:

深入分析Mimikatz:SSP

好吧,显然这还远远不够(即使我们修改了名称以及注释字段)。要注意一点,在没有充分剥离并重新编译之前,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怀抱,就可以搜索代码正在使用的匹配模式,然后找到如下位置:

深入分析Mimikatz:SSP

这里我们来看看代码实际上在执行哪些操作。memssp正被用来hook msv1_0.dllSpAcceptCredentials 函数,以便恢复凭据信息。让我们使用调试器看一下添加后的hook长啥样子。

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

深入分析Mimikatz:SSP

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

深入分析Mimikatz:SSP

深入分析Mimikatz:SSP

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

深入分析Mimikatz:SSP

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

深入分析Mimikatz:SSP

如果大家想查看负责这个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

深入分析Mimikatz:SSP

反汇编 sspcli.dll 中的 AddSecurityPackage (特别是该函数所使用的外部API调用),我们可以找到 NdrClientCall3 ,这意味着该函数正在使用RPC。这一点很正常,因为这个调用需要向 lsass 发送信号,通知 lsass 应当加载一个新的SSP:

深入分析Mimikatz:SSP

跟踪 NdrClientCall3 ,我们可以找到传入的如下参数:

深入分析Mimikatz:SSP

其中 nProcNum 参数值为 3 ,如果我们深入分析 sspirpc_ProxyInfo 结构,可以看到RPC接口的UUID值为 4f32adc8-6052-4a04-8701-293ccf2096f0

深入分析Mimikatz:SSP

现在我们已经掌握足够多的信息,可以通过RpcView来观察通过 sspisrv.dll 公开的 SspirCallRpc RPC调用:

深入分析Mimikatz:SSP

为了使用这个调用,我们需要知道传入的参数。我们可以通过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 之前插入断点:

深入分析Mimikatz:SSP

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

深入分析Mimikatz:SSP

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

深入分析Mimikatz:SSP

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

深入分析Mimikatz:SSP

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

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

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

深入分析Mimikatz:SSP

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

深入分析Mimikatz:SSP

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

深入分析Mimikatz:SSP

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

深入分析Mimikatz:SSP

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

深入分析Mimikatz:SSP

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

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

深入分析Mimikatz:SSP

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

深入分析Mimikatz:SSP

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

深入分析Mimikatz:SSP

这里我们可能希望跳过这个操作,因为我们不希望使用这种方法实现本地驻留,并且如果没有必要,我们也不希望涉及到注册表操作。理想状态下,如果引起怀疑(比如防御方通过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之外是否存在其他检测能力。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Introduction to Computer Science Using Python

Introduction to Computer Science Using Python

Dierbach, Charles / 2012-12 / $ 133.62

Introduction to Computer Science Using Python: A Computational Problem-Solving Focus introduces students to programming and computational problem-solving via a back-to-basics, step-by-step, objects-la......一起来看看 《Introduction to Computer Science Using Python》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具