内容简介:在前一篇SSP(Security Support Provider)是一个DLL,允许开发者提供一些回调函数,以便在特定认证和授权事件期间调用。在前一篇文章中,我们可以了解到WDigest正是使用这个接口来缓存凭据。
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 定义文件,将该函数导出为 SpLsaModeInitialize , lsass 会使用该函数来初始化包含多个回调的一个结构体。
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。
然而经过一番搜索后,我找到了一则推文:
这里直接提到了 AddSecurityPackage 这个API, @mattifestation 在 Install-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.dll 的 SpAcceptCredentials 函数,以便恢复凭据信息。让我们使用调试器看一下添加后的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_1 为 arg_2 的大小, arg_3 、 arg_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之外是否存在其他检测能力。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
新零售:低价高效的数据赋能之路
刘润 / 中信出版集团 / 2018-9 / 65.00元
小米新零售,如何做到20倍坪效? 天猫小店,如何利用大数据助力线下零售? 盒马鲜生,为什么坚持必须用App才能买单? 名创优品,实体小店在电商冲击下,如何拥抱春天? 新零售的未来在何方?什么样的思维模式才可应对? 新零售,不是商界大佬的专用名词,它就在我们生活触手可及的各个角落——小到便利店的酸奶,大到京东商城的冰箱,都蕴含着消费者、货物、经营场所三者共同作用的经济逻......一起来看看 《新零售:低价高效的数据赋能之路》 这本书的介绍吧!