COModo: From Sandbox to SYSTEM (CVE-2019–3969)

David Wells
Tenable TechBlog
Published in
12 min readJul 22, 2019

Antivirus (AV) is a great target for vulnerability hunting: Large attack surface, complex parsing, and various components executing with high privileges. So a couple of months ago, I decided looked at the latest Comodo Antivirus v12.0.0.6810. I ended up finding a few cool things, however one I thought was worth covering here, which is a sandbox escape as well as a privilege escalation to SYSTEM. In this article, we will be abusing various COM objects, bypassing binary signing checks, and hijacking services. I think you will find this to be an interesting one, so let’s get right into it.

Comodo Sandboxing Primer

First, I’d like to give a primer on Comodo’s sandboxing technology, which Comodo refers to as “containment”, I will be using “sandbox”/”containment” interchangeably in this article. This technology restricts untrusted applications (RTATC) to execute in a sandbox-like environment alongside other running processes on the OS. It features a hybrid of user mode hooks along with a kernel mode driver, preventing any modification to files or registry on the machine. File and registry reads are allowed, but as soon as a write occurs, file I/O is diverted to a sandbox filesystem and future reads are then read back from that sandboxed filesystem to give the illusion that the process is interacting with the filesystem.

Comodo accomplishes this through its filter driver Cmdguard.sys, which registers an _FLT_REGISTRATION structure using FltRegisterFilter API. This structure contains callback routines for various IRPs received from user mode applications, such as file access, file writing, etc and can intercept/act on them accordingly. Also worth noting is that ALPC (a type of Microsoft IPC used for many OS components) is sandboxed as well, which diverts attempted ALPC connections to “sandboxed” svchost.exe instances, preventing sandbox escapes via RPC/ALPC. Below is an illustration of how this containment technology works

Figure 1 — Illustration from Reverse Engineering Comodo’s Containment Technology

Cmdguard.sys not only filters file/registry I/O, but also keeps track of running processes by registering a CREATE_PROCESS_NOTIFY_ROUTINE (using PsSetCreateProcessNotifyRoutine). As a process runs, containment status, trust level, and other Comodo relevant attributes are kept track of in a kernel-stored process list. Cmdguard.sys exposes “filter ports” that user-mode Comodo components can talk to. Containment status is set by sending a specific message to its filterport named “cmdAuthPort”. The target process specified in the message then has its “containment” flag set by the kernel mode driver.

Guard64.dll (which is injected into just about every running process) is responsible for sending these containment message from user-mode after contained process creation. For example, Guard64.dll will hook Explorer.exe’s CreateProcessInternalW API so that when a user executes an untrusted process, the hook sends a “containment” message to Cmdguard.sys filter port. Now when untrusted process starts, it is deemed “contained” by the driver and file/registry I/O is prevented. Also, Cmdguard.sys will inject a Guard64.dll into the contained process, which will perform aggressive user-mode hooking.

Figure 2 —Cmdguard.sys’ dll Injection Routine

Below, we can see just a few of user-mode hooks Guard64.dll will set.

Figure 3 — Guard64.dll Usermode Hooks

A common role these hooks play is preventing contained processes from connecting to existing securable objects created by non-contained processes. For this, it will append a “!comodo_6” tag to each object name created or opened by a contained process, preventing name collision (or connection) with existing securable objects on the OS.

In fact, this is how its RPC/ALPC containment works! RPC/ALPC traffic is diverted to contained Svchost.exe instances (seen in the first diagram above). This is because “!comodo_6” is appended to port names contained processes attempt to connect to, and the contained Svchost.exe instances happened to create port names with “!comodo_6” appended to them since they themselves are contained. Here we can see a contained MSI installer trying to run and ends up with a sandboxed MSIexec.exe service component created (under cmdvirth.exe) after issuing its RPC calls.

Figure 4 — Contained ALPC Spawns Contained MSIExec Instance

Bypassing these user mode hooks is trivial, but containment escaping through that is not easy. It was not possible, for example, to simply patch ALPC related hooks and escape via WMI, as these were also monitored and blocked by CmdAgent.exe. Now that I have provided a small primer on Comodo containment, let’s look into how we can achieve an escape and escalation.

Making a Comodo COM Client

Comodo uses many IPC mechanisms between its various AV components: Filter Ports, Shared Memory, LPC, and COM. COM is what we will focus on here. A good primer for COM can be found here, but in short it stands for “Component Object Model” and is a Microsoft technology to allow different modules to create and interface with various objects defined by the COM server. COM servers can be local (just a COM server dll loaded in current process) or remote, a COM server running as a remote process and interacted with over ALPC. Exploitation-wise, remote provides a much more interesting scenario.

We happen to know Comodo has the capability to invoke scan jobs from low-privilege processes such as explorer.exe (via it’s Context Shell Handler — (the menu that appears when user right clicks)) or Cis.exe (Comodo client GUI). These scan jobs are executed by invoking routines in CAVWP.exe which runs as SYSTEM.

If we can understand how to connect ourselves to this service (as they do), then perhaps we can uncover a new attack surface for ourselves and may find even more interesting functions than just “scanning”. This remote interaction with CAVWP.exe happens to be through COM, as CAVWP.exe is an out-of-proc COM server as we see in the registry.

Figure 5 — CAVWP.exe COM Server

Let’s see how Explorer.exe and the Comodo COM client’s remotely trigger these “scans” through COM. As I previously mentioned, one of the low-privileged clients that can initiate these scans is Explorer.exe’s Shell Context Menu handler that Comodo registers — CavShell.dll

Figure 6 — Comodo Context Menu Handler in Explorer.exe

Reversing the shell extension handler, I find where the “scan” client COM routine is implemented. Understanding this function is going to give us the keys on how to successfully engineer our custom COM client.

Figure 7 — Cavshell.dll’s (Context Menu Handler) “Scan File” Routine

This particular call to CoGetClassObject is interesting. CoGetClassObject returns a pointer to an interface for an object associated with the supplied CLSID. From looking the CLSID up in the registry, we see it is for a “Cis Gate Class” and soon realize CAVavWp.exe has nothing to do with this class, and the actual COM server for this class is CmdAgent.exe.

Figure 8 — CLSID (CLSID_CisGate)Reveals COM Server is Cmdagent.exe

It turns out CmdAgent acts as a proxy between these low-privilege COM clients and CavWp, as CavWp will scan on behalf of your request to CmdAgent through CisGate interface. Keep in mind, my main goal here is just to get these initial bindings setup and understood so we can explore more attack surface.

After reverse engineering the client (as well as a bit of CmdAgent itself), we port over the COM stubs with appropriate method offsets and re-engineer our code to mimic what these COM clients were doing and execute.

Figure 9 — Re-engineering a “Fake” Comodo COM Client

One Problem: Code Signing

But running this code fails at the “CreateInstance” call in CisClassFactory as it returns E_ACCESSDENIED. This is odd since we have the same process privileges as Explorer.exe and Cis.exe (which can get away with such calls). Why can’t we?

Here we have CmdAgent.exe in debugger, we broke where it receives the “CreateInstance” call we made and see it branches off to a custom E_ACCESS_DENIED message.

Figure 10 — Cmdagent.exe Blocking Unsigned Processes Interacting via COM

This means it wasn’t Windows telling us ACCESS_DENIED, but rather Comodo itself through its own decision.

This particular “decision” is based on a signature check, which verifies that the COM client requesting an instance is a trusted “signed” binary. Looking at the signing check routine in Cmdagent.exe, it looked like signers could be either Comodo or Microsoft, and this makes sense, being that Explorer.exe or Cis.exe are the only expected clients that should be invoking COM methods in CmdAgent.exe.

Figure 11 — Cmdagent.exe Signature Checking Classes (HardMicrosoftSigner/HardComodoSigner)

The signature check was simply bypassed however by….wait…let’s see if you can see the problem. Here is CmdAgent.exe resolving the COM client’s process name to later invoke a signature check from disk:

Figure 12 — Cmdagent.exe Looking Up COM Client’s Full Image Name

As you may know, GetModuleFileNameEx just queries the target process’ PEB->Ldr->InMemoryOrderModuleList for full image name. This is in our control of course and can be easily changed within our own process.

An alternative solution to this, is to code inject a trusted Microsoft or Comodo binary and issue our COM requests from there. Comodo however prevents dll injection, so in order to pull this off, we would have to process hollow a trusted Comodo binary instead.

This is a more cumbersome route than manipulating our own PEB, but it gave us great advantages. One of them is that the Cmdguard.sys driver will NOT inject a Guard64.dll that puts annoying hooks in us. Below shows Cmdguard.sys and how it calls “InjectDll” routine only if process is deemed “untrusted”. “IsProcessUntrusted” will pass if we hollow C:\Program Files\COMODO\COMODO Internet Security\CmdVirth.exe for example, because the driver trusts according to executable path, which we are mimicking when we hollow such trusted process.

Figure 13 — Cmdguard.sys Skips Guard64.dll Injection if Process is “Trusted”

We now add a process hollowing routine which hollows out an instance of “C:\Program Files\COMODO\COMODO Internet Security\CmdVirth.exe” (signed by Comodo) and replace the executable code with our own. This injected code will now do the same COM routines we had before, except now it will pass signature check and also have no hooks. We re-run the code and successfully trigger a scan job from our low-privilege and contained status!

Figure 14 — Hooray! We Successfully Triggered a Scan From our Custom Unsigned COM Client

Hunting for Abusable COM Interfaces

With scans workings (from a contained process), we can now look for more interesting COM interfaces to play with in CmdAgent. Looking at the other ~60 methods for the CisGate interface we have obtained, they honestly didn’t look too interesting, and the ones that did, had their fair usage of CoImpersonateClient which prevents the logic bugs I was aiming for. There are still more methods to investigate, as there are more supported interfaces we can get. Remember how we use CreateInstance to create a CisGate object in CmdAgent.exe? There are probably more objects we can create, each with more methods to play with. Back to the CmdAgent!

The ICisClassFactory->CreateInstance function creates a desired object and returns requested interface pointer for it by wrapping a call to CisGate->QueryInterface. For those that might not know, QueryInterface is a core function in IUnknown, the base class that all COM classes inherit from. In short, this function resolves a riid (interface identifier) to an object interface so that a client (such as ourselves) can invoke methods on it. With this knowledge, we can see what more interfaces we can obtain by reversing CmdAgent.exe’s QueryInterface function and observing its supported interfaces.

Figure 15 — CmdAgent.exe’s Supported Interfaces

We spot the list of supported_interfaces that their QueryInterface supports and named each GUID as found in our registry. The IID_ICisFacade riid is the one used to return a CisGate object. Been there — done that. The next interesting one to look at is IID_IServiceProvider. Reading about IID_IServiceProvider, it sounded like it can yield all sorts of different things. Searching for the IID_IServiceProvider GUID in Cis.exe (Comodo GUI Client), we find it being used. Reversing engineering this usage will give us a good bearing on how to use it ourselves, and what kind of services they are trying to obtain and do what with.

Registry Reads

Here is Cis.exe and how I found it using “IServiceProvider” to QueryService. It’s using it to obtain a Comodo defined “SvcRegistryAccess” object.

Figure 16 — Cis.exe Obtaining ISvcRegistryAccess

The specific usage seen in Cis.exe revealed it was getting an SvcRegistryAccess object from CmdAgent.exe, and then invoking a method on it to read a registry key from and send back the data. Having a SYSTEM process read registry for you already sounds like an interesting attack vector, but I also had a hunch that the developers did not just make this SvcRegistryAccess a “getter-only” class. Back to CmdAgent to see how this COM class is implemented.

In CmdAgent, we can see the ISvcRegistryAccess method that they remotely invoked is directly reading the reg value and returning data to the client with no CoImpersinateClient. Awesome! this means we can read registry values as SYSTEM since that’s the privilege level CmdAgent is running in.

Figure 17 — CmdAgent.exe ISvcRegistryAccess’s Method Reading Registry (as SYSTEM — no Impersonation)

Registry Writes

Now let’s see if this COM object supports registry writes. Digging around its vtable, we see a method that calls into “RegSetValueExW”.

Figure 18 — CmdAgent.exe Reveals More Interesting Methods to Invoke (Setting Registry Values)
Figure 19 — CmdAgent.exe ISvcRegistryAccess Method Sets Registry Value (as SYSTEM — no Impersonation)

It seems pretty clear that if we invoke this method, we can get a registry write as SYSTEM since I don’t see any impersonation APIs being called. We change our COM client code to obtain an IServiceProvider and resolve an ISvcRegistryAccess, and then invoke this “registry write” method. If we look at how we obtained our regInterface, through calling “GetRegInterface”, we actually see the CmdAgent.exe implementation only created a read-only reg key handle, so of course trying to invoke the “write registry” method will result in ACCESS_DENIED issues. I luckily found another method within ISvcRegKey vtable that replaces our registry key handle to “writable” by passing some additional arguments.

We simply add a call to this method with proper args to obtain “writable” ISvcRegistryAccess.

Figure 20 — Modifying Our COM Client to Obtain Writable Registry Interface

Putting it all together, we come up with the following code, and get a registry write as SYSTEM!

Figure 21 — Our Final COM Client Code
Figure 22 — Successfully Overwrote Data in Privileged Registry Key

Exploitation to SYSTEM

From the top, we run our sandboxed application, which then process hollows a Comodo signed binary (to bypass CmdAgent signature check) which then runs our COM code we wrote and does a registry write as SYSTEM from our “contained” process. A practical escalation through this would be to hijack an existing service and a decent service to hijack was CmdAgent.exe itself. By replacing the ImagePath data in HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\CmdAgent, this would replace the CmdAgent service with our own binary, which will run as SYSTEM.

Figure 23 — CmdAgent.exe Service in Registry

However, we would need CmdAgent service to restart if we wanted to instantly gain these SYSTEM privileges (opposed to waiting for next restart). Luckily, we have a way of doing that, as I found a way we can crash CmdAgent, which will cause the service to be “revived” and mistakenly start our ImagePath we wrote to protected registry key. The CmdAgent crash is very simple, as the process exposes a Section Object of structural data that writable by EVERYONE:

Figure 24 — CmdAgent.exe Section Object Exposing Writable Object Data (SharedMemoryDictionary)

CmdAgent refers to this buffer as a “SharedMemoryDictionary”, which is an entire class object just exposed in shared memory. We can crash this by writing bad size data in object members causing an out of bounds read when CmdAgent tries to read this SharedMemoryDictionary (which is constantly) causing CmdAgent to crash. When service is revived, it executes our new binary instead, which instantly escalates us to SYSTEM.

Figure 25 — Successful Escalation to SYSTEM

PoC

Code: https://github.com/tenable/poc/tree/master/Comodo/Comodo%20Antivirus

--

--