Disclaimer: This is not a vulnerability in the application itself. This bypass leverages functionality of the CLR and managed heap to manipulate stored values in the target process. This approach could be leveraged in any managed application to control code flow.
Cylance is one of the more prominent EDR solutions available on the market. Cylance touts the ability to utilize machine intelligence to thwart malicious actors from compromising your computer. Let's cover some of the specifics on how the Cylance service is implemented. The userland component is comprised of a collection of .NET assemblies that manage a majority of Cylance’s capabilities including logging all events, detecting and blocking malicious activity in memory, and quarantining abnormal or infected files. Cylance also utilizes a filter driver that is responsible for injecting the memory exploitation defense library (CyMemDef) into every new process. The userland service maintains communication with the filter driver via IOCTLs. The IOCTL calls can include details on process creation, memory I/O, and remote or local thread creation. Minus a few shortcomings, Cylance proves to be effective against attacks involving process injection, process hollowing, reading lsass memory (Mimikatz), etc.
Now that we’ve briefly covered the detection and prevention capabilities of Cylance, we are going to review the role that the CLR Heap plays in this bypass. In every managed application there exists a heap. The heap contains all of the referenced objects created during the application's execution. These objects can be class instances, class members, or fields, and they are constantly being created, modified, and removed by the garbage collector. When the CLR needs to allocate space for a new object, it will call VirtualAlloc to create a region of memory shared by multiple objects. VirtualFree is used when an object is to be destroyed. To learn more about the managed heap please look here.
So at this point, we know that once the CylanceSvc is started, all of its objects are stored in the managed heap. For our convenience, there is an open source project from Microsoft that allows you to traverse the heap and view all of the managed objects. ClrMD is a .NET library that can attach to managed applications as a debugger and view the contents of the heap, and class instances or methods. To find a starting point, we can review the log files created by the service. Thankfully, Cylance is very generous and provides plenty of detail in their log files.
The name contained within brackets on each line pertains to a specific class within an assembly. Following that, are specific methods within that class and then a log message. Using this information we can go back and find the corresponding assembly for the Cylance.Host.MemDef.MemDef class. This class is responsible for defining all of the offending memory operations that warrant an alert and/or process termination, as well as, responding to violation events sent from the driver. We can look for this class by loading all of the assemblies in dnSpy and then search for the desired class.
Opening this class in the Assembly Explorer pane, you can see there are a number of interesting class methods. After all of the methods, the events and fields specific to the MemDef class are shown. One field that stands out to me is the “IsMemDefEnabled” field. One of the great things about dnSpy is that we can analyze this field and find where it is assigned and read from.
When an event is received, before any logic to handle the MemDef message, there is a check to see if script control or memory exploitation defense is enabled. If not, all of the message parsing logic is skipped and then the method returns control to the calling method. So we can deduce that, if we are able to set both the “IsMemDefEnabled” and “IsScriptControlEnabled” values to false, we can bypass any logic to alert on malicious activity. Some of more aggressive policies will not only alert on such activity but will also terminate the offending process. This does not have any effect on the filter driver. There will still be messages sent to the service for suspicious memory operations.
So to accomplish this we will need to meet two prerequisites.
We will need to be in an elevated context. The CylanceSvc process executes as NT AUTHORITY\SYSTEM.
We need to have the ability to write to the CylanceSvc process and adjust memory permissions if necessary.
First, we scan the managed heap for the CylanceSvc process to locate the “Cylance.Host.MemDef.MemDef” object/class. Once we locate this object, we can then obtain the location of our target fields. The ClrMD assembly contains GetHeap() and Heap.EnumerateObjectAddresses() methods to accomplish this. Once we have a reference to the field, if the ‘HasSimpleValue’ property is true, we can use the GetFieldValue method to obtain its current value.
Unfortunately, ClrMD does not provide the ability to change the field value. However, we can get the address of the field in memory. Once we have the address, we can simply write the desired value via VirtualProtectEx and WriteProcessMemory. NOTE: The area of memory for this specific field was not writable. There are other areas of memory in a .NET application that have RWX permissions. This is a necessity for the JIT compiler, therefore VirtualProtectEx would not be required.
Now that the values have been changed, Cylance will simply ignore any offending process if a memory operation is deemed malicious. Also, script control prevents the powershell.exe process from starting. When disabled, powershell.exe will execute but an alert will still be generated. I’ll leave it up to the reader to investigate how to remedy that. There already has been some research into manipulating .NET applications. If you’re interested you should take a look at @malwareunicorn’s presentation on hijacking .NET, as well as research from @ttimzen. @subtee also has a practical example for unlocking constrained language mode here.
You may be asking your self, why wouldn’t I just use sc.exe CylanceSvc stop ? Well, the service is backed by the filter driver. In order for the service to stop, the driver must be unloaded. However, if a FilterUnloadCallback was not registered, the driver cannot be unloaded. Hence, the service cannot be stopped. This may not be the case for Cylance but still a possibility. Even when a FilterUnloadCallback is registered, the driver will not unload if it is a non-mandatory unload. Non-mandatory unloads come from user-mode applications calling the FilterUnload function or another kernel driver. It is possible to even prevent mandatory unloads from being successful. When calling the FltRegisterFilter function, a special flag (FLTFL_REGISTRATION_DO_NOT_SUPPORT_SERVICE_STOP) can be set in the FLT_REGISTRATION structure to prevent mandatory unloads. Mandatory unloads occur when using the sc or net commands. Ultimately, sc or net commands are subject to more scrutiny in command-line auditing solutions. This bypass serves as a more opsec aware alternative to just killing the service.
On a final note, the CyMemDef dll is not present in the CylanceSvc process. I'm not sure if this was an oversight or if an exception was needed in this case.
You can find the source code for the PoC here
Now for a quick demo....