Windows system objects are one of the interesting areas of binary application assessments that are often ignored or misunderstood. Many people don’t realise that abstract Windows application programming concepts such as mutexes, events, semaphores, shared memory sections, and jobs all come together under the purview of the Windows Object Manager. These objects, like those in the filesystem and registry namespaces, have all sorts of interesting security impacts when not properly managed.
This blog post relates to an advisory. See CVE-2014-1213: Denial of Service in Sophos Anti-Virus for the release.
One of the major differences of the system object namespace, versus filesystem and registry namespaces, is the concept of a default Discretionary Access Control List (DACL). These DACLs are the cornerstone of the Windows security model, and are used to describe which entities (users, groups, etc.) have specific types of access to an object. When you view the permissions on a file or directory, you’re looking at a direct representation of the DACL for that object. Each rule within a DACL is called an Access Control Entry (ACE). When an object in any namespace is created and the application does not explicitly provide a DACL, the system looks at the parent container to see if it has any ACEs within its DACL that are marked as inheritable. If it finds some, it applies them across into a new DACL for the newly created object. There are special rules around inheritance for containers, but we won’t get into that here. If there are no inheritable ACEs, it resorts to applying the default DACL for the namespace. This is where things get interesting from a security perspective; the system object namespace, in contrast with registry and filesystem namespaces, has no default DACL. In this situation, the system applies an empty DACL, which allows everyone full access to the object.
This is a corner-case that many developers fall foul of. Objects created in the local container (i.e. the system object container for the current session) inherit some ACEs from the session container, but the global container has no inheritable ACEs, and therefore objects within it that are created without an explicit DACL will end up with an empty DACL. We can see this in action by viewing the DACLs applied to the global and session containers, using a tool such as WinObj:
Notice that all the ACEs in the global container are marked as “Inherit None”, meaning that child objects will not inherit them as part of their DACL. As such, if you create a system object such as a mutex or an event through the usual CreateMutex or CreateEvent API calls, and fail to explicitly provide a DACL, all users on the system will have unrestricted access to that object.
Whilst digging into security issues around this common mistake, I found a number of vulnerabilities in a range of products. In general the impacts of being able to mess with these were low, usually causing the affected application to lock up or stop working in some way. In Sophos Endpoint Security, however, the impact was more interesting. Most anti-malware software consists of three major sections: a user-facing GUI for controlling and monitoring the product, a high privilege user-mode service for performing various scanning features, and one or more kernel-mode modules (commonly referred to as drivers) that provide filesystem filters, notification of new threads and processes, low-level memory access, hook detection, and other kernel-level functionality. Communicating quickly and reliably between these components is a daunting task, especially when your messages have to traverse across the user-mode / kernel-mode barrier. Enter global system objects. Mutexes, events, semaphores, and shared memory sections in the global container of the system object namespace are all directly accessible from both user-mode and kernel-mode. When combined properly, these object types allow a developer to create an inter-process communications framework that is fast, reliable, and thread-safe.
One example of this might be a feature where a filesystem filter driver needs to notify the user-mode service that new data has been written to disk, so that it can scan it. Three named objects – an event, a mutex, and a shared memory section – are created within the global namespace, so that both components can access them. The event is used to signal that a write operation is pending, the mutex is used to ensure that the shared memory section is accessed by only one thread at a time, and the shared memory section is used to hold information about the event. The whole process is rather complex, and is best described in a diagram:
As you can see, the user-mode service is responsible for checking the write operations before they are allowed. The decision is passed back to the driver, which either completes the write or rejects it, issuing an appropriate error code.
Now, imagine you let a low-privilege user interact with these objects. For one, they may be able to wait on the event object themselves and modify the shared memory section via a race condition. This can be somewhat mitigated by various integrity checks, but isn’t outside the realms of possibility. Another issue is that all of these components modify their state, and in some cases block execution, when the event and mutex objects are waited upon or signalled. Imagine that a malicious local user acquires the mutex, then signals the event. The user-mode service continues execution (step 7) and attempts to acquire the mutex (step 8), but since the malicious user has already acquired it, the service thread is now blocked. From this point on, the driver’s calls to have write operations checked go unheeded. Although the architecture is not identical, this is precisely the mechanism in which Sophos Endpoint Security failed.
As the advisory describes, CVE-2014-1213 relates to a lack of DACLs applied to system objects. As we discussed above, failure to explicitly supply a DACL when creating system objects results in the object being created with the default DACL for the namespace, which is null (i.e. empty). The impact is that a local low-privilege user can manipulate these objects as they wish. Since this can lead to disk IO requests being ignored, or at least heavily delayed, the system eventually cannot continue. In many cases it simply locks up and becomes unresponsive, as user-mode programs and subsystems (e.g. SMSS / CSRSS) cannot complete blocking disk operations. In some cases, the system will recognise the pattern of failures and forcefully terminate the system with a bugcheck (BSoD) in order to reduce the potential for permanent damage to the system state. Of course, this isn’t particularly interesting from a security perspective if you only consider a desktop environment, but imagine the impact on a terminal services system with hundreds or thousands of users.
Sophos have now patched this issue in engine 3.50, which went live on the 21st of January. Portcullis have independently verified this fix as being effective after the update is applied and the system is rebooted.