深入分析Mimikatz:SSP

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

内容简介:在前一篇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之外是否存在其他检测能力。


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

查看所有标签

猜你喜欢:

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

Ajax Design Patterns

Ajax Design Patterns

Michael Mahemoff / O'Reilly Media / 2006-06-29 / USD 44.99

Ajax, or Asynchronous JavaScript and XML, exploded onto the scene in the spring of 2005 and remains the hottest story among web developers. With its rich combination of technologies, Ajax provides a s......一起来看看 《Ajax Design Patterns》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具