If you've tried dumping lsass.exe process memory from an endpoint where CylancePROTECT is running, you know you will be having a hard time.
This lab shows how it's still possible to dump the process memory and bypass Cylance (or any other Antivirus/Endpoint Detection & Response solution) that uses userland API hooking to determine if a program is malicious during its execution.
Hooking is an old technique and I've read about it in the past, but never had a chance to play with it, until I stumbled upon a post by Hoang Bui who wrote about unhooking EDRs https://medium.com/@fsx30/bypass-edrs-memory-protection-introduction-to-hooking-2efb21acffd6.
API hooking could be compared to a web proxy - all API calls (including their arguments) that your application makes (say
OpenProcess, etc), are intercepted and inspected by AVs/EDRs which then decide if the action/intent of the program is malicious or not.
The way EDR vendors hook userland APIs is by hijacking/modifying function definitions (APIs) found in Windows DLLs such as
Function definitions are modified by inserting a
jmp instruction at their very beginning. Those
jmp instructions will change program's execution flow - the program will get redirected to the EDRs inspection module which will evaluate if the program exhibits any suspicious behaviour and it will do so by analyzing the arguments that were passed to the function that the EDR is hooking/monitoring. This redirection is sometimes called a
Hopefully the below diagram helps clarify it further:
It's worth noting that not all the functions get hijacked by AVs/EDRs. Usually only those functions that are known to be abused over and over again in the wiled that get hooked - think
NtQueueApcThread and similar.
Note that this lab is based on another lab where I wrote a small C++ program that used
MiniDumpWriteDump Windows API to dump the lsass.exe process memory - Dumping LSASS without Mimikatz == Reduced Chances of Getting Flagged by AVs.
Let's try and run the code (referenced in the above lab) on a system that is monitored by CylancePROTECT. The program gets killed with
Violation: LsassReadstraight away:
As you could have guessed, it Cylance hooks the
MiniDumpWriteDump API call. To be more precise, it actually hooks a
ntdll.dll which is called under the hood by the
Executing the program with debugger, it can be observed that very early in the process execution, a Cylance Memory Protection Module
CyMemDef64.dll gets injected into
Invoke-CreateMemoryDump.exe (my program that leverages MiniDumpWriteDump) process - this is the module that will be inspecting the API calls made by Invoke-CreateMemoryDump.exe:
Since we know that
NtReadVirtualMemory, we can take a peek at
NtReadVirtualMemory function definition to see if there's anything suspicious about it:
We immediately see that the first instruction of the function is a
jmp instruction to some weird memory address which falls outside the
ntdll module's memory address ranges:
Let's dissassemble that address:
We can immediately see that there are further
jmp instructions to Cylance Memory Protection Module
CyMemDef64.dll - this confirms that the function
NtReadVirtualMemory is hooked:
To confirm that our program will eventually call
NtReadVirtualMemory, we can put a breakpoint on it and continue our program's execution - as shown in the below screengrab, the breakpoint is hit:
If we continue the program execution at this point, it will be redirected (
jmp instruction) to Cylance's Memory Protection Module and the program will be bust with the
Violation: LsassRead message.
In order to unhook or, in other words, restore the hooked function to its initial state, we need to know how it looked like before it got modified by Cylance.
This is easy to do by checking the first 5 bytes of the function
NtReadVirtualMemory that can be found in c:\windows\system32\ntdll.dll before it gets loaded into memory. We can see the function's Relative Virtual Address (RVA) in ntdll's DLL exports table - which in my case is
00069C70 (will probably be different on your system):
If we convert the RVA to the physical file location (which is the same as RVA since the file is not yet in memory), we can see that the first 5 bytes of the function are
4c 8b d1 b8 c3:
What the above means is that if we replace the first 5 bytes (
e9 0f 64 f8 cf) of the
NtReadVirtualMemory that were injected by Cylance, to
4c 8b d1 b8 3c, Cylance should become blind and no longer monitor
MiniDumpWriteDump API calls.
With this information, we can update the program and instruct it to find the address of function
NtReadVirtualMemory and unhook it by writing the bytes
4c 8b d1 b8 3c to the beginning of that function as shown in line 17 below:
Recompiling and running the program again dumps lsass.exe process memory successfully without Cylance interferance:
We can now take the dump file offline and load it into mimikatz...
I only unhooked one function, but the process could be automated to unhook all functions by comparing function definitions in the DLL on the disk with their definitions in memory. If the function definition in memory is different, it meants it is hooked and should be patched with instructions found in the definition on the disk.
Great references below, including Cylance themselves talking about unhooking: