This is the second blog post in a series (see part one here) about using COM and WinRT inside the Less Privileged AppContainer (LPAC) sandbox. In the previous entry, we introduced the lpacCom capability and explained that COM initialization refuses to proceed without it. We also highlighted three bypass ideas and discussed two of them. Today we will develop the third. Yet the journey won't stop there because (spoilers), lpacCom guards more than COM initialization. But first, a bit more background knowledge.
As a flexible programming model, COM needs to accommodate objects with different concurrency and reentrancy requirements in a way that would naturally provide them with suitable guarantees. If we try to categorize objects based on their thread safety, we can identify three main possibilities (listed in the order of increasing restrictions):
These categories directly translate to the following threading models that an installer can select at component registration:
Free or Neutral (the difference is not relevant here; both are multi-threaded)ApartmentSingle (assumed by default if there is no value)Here we finally arrive at apartments, which are an abstraction for managing assignments between threads and components. Every component instance resides for its entire lifetime in a single apartment and can be accessed directly only by threads belonging to this apartment. Threads select their apartment at COM initialization (based on the parameter passed to CoInitializeEx) by either creating a new one or reusing an existing apartment.
The caller can choose between two primary options:
COINIT_MULTITHREADED, which puts the thread into a multi-threaded apartment (MTA) and allows direct access to all free-threaded objects. If several threads use this flag, they all end up in the same apartment. This behavior indicates that MTA threads are conceptually indistinguishable and, thus, there can only be one (or zero) MTAs in a process.COINIT_APARTMENTTHREADED, which creates a new single-threaded apartment (STA). As a result, a process can host any number of STAs, with each containing exactly one thread. This design guarantees that apartment-threaded components never encounter concurrent access. It also means that STA threads need to process window messages to allow COM to proxy external calls against objects in their apartment. This requirement is especially critical for the first STA created in a process because it becomes responsible for hosting all single-threaded (a stricter version of apartment-threaded) components. The first STA is, thus, called the Main STA.It is worth mentioning that since STA threads use window messages to serialize requests, they are inherently incompatible with the win32k lockdown mitigation. This mitigation prevents the process from issuing any graphics-related syscalls (and hence, loading
user32.dll, creating windows, and pumping messages) but appears rarely outside of the heavily restricted application sandbox environments.
Hopefully, this discussion sheds light on why CoCreateInstance refuses to function without CoInitializeEx. COM wants to assign the new object to an apartment, but the calling thread doesn't have one. In the first bypass, we instantiated the component directly, circumventing the need for an apartment but risking a later failure. In the second bypass, we allowed apartment creation to proceed by patching the capability check. What we want to try next is to find an indirect way to create apartments.
COM initialization is generally a per-thread operation, but it can have process-wide side effects. We already saw one related to single-threaded apartments, where only the first STA receives the title of the Main STA. Multi-threaded initialization offers another example, which Raymond Chen explained in his post subtitled "The curse of the implicit MTA".
In summary, threads can become part of the multi-threaded apartment in two ways: explicitly, via a CoInitializeEx call, and implicitly, as a side effect of the MTA's mere existence in the process. Since both cases end up in using the same apartment, functions such as CoCreateInstance treat them equally. Implicit MTA membership, of course, only applies as a fallback, so any explicit choice takes precedence.
In a world where CoInitializeEx is the only API creating the multi-threaded apartment, we would still have the bootstrapping problem. Luckily, the reality is not like that. Enter CoIncrementMTAUsage - a Windows 8+ API addition (with a rather descriptive name!) that ensures the multi-threaded apartment exists in the process, and, thus, automatically puts all apartmentless threads into implicit MTA.
WINOLEAPI
CoIncrementMTAUsage(
_Out_ CO_MTA_USAGE_COOKIE* pCookie
);
It's unclear whether Microsoft forgot to lock this API behind a capability check or decided not to (due to its use in package-related features). The result is identical: it becomes by far the simplest lpacCom bypass. Just replace CoInitializeEx with CoIncrementMTAUsage and CoUninitialize with CoDecrementMTAUsage.
One caveat with this method is that it limits CoCreateInstance and related functions to components that declare themselves as MTA-compatible (i.e., using Free or Both as their threading model).
"But wasn't COM supposed to hide implementation details such as the component's thread safety requirements?" you might ask. Indeed, when an MTA thread attempts to instantiate a single- or an apartment-threaded component, COM tries to fulfill the request by using a helper (worker) STA thread. To become STA, the worker calls CoInitializeEx and, hence, hits RtlCheckTokenCapability:
Here you can see stacks of two threads. The one on the left is the implicit MTA thread that attempts to create an instance of a single-threaded component. After determining that the component is not MTA-compatible and that there are no STAs in the process, it spawns a worker thread and delegates instantiation to it. The picture on the right shows this worker thread attempting to assume the role of the Main STA and hitting the capability check.
It appears that we have returned to the starting point and need the initialization bypass from part one of this series to make STA components work after all. However, here is the twist: this time, patching RtlCheckTokenCapability will not be enough; CoCreateInstance will still return ERROR_ACCESS_DENIED. By bringing cross-apartment communication to the table, we substantially increased the difficulty. A formidable obstacle is now in the way.
Location transparency requires making components accessible across apartment boundaries. These scenarios include both local cross-apartment instantiation (like from the previous example) and remote (out-of-process) activation. After all, deploying component code out-of-process is a natural continuation of placing it in a dedicated apartment in-process. COM design principles heavily utilize this conceptual similarity, all while allowing local access to benefit from various optimizations. Either way, both end up relying on RPC. And RPC, as you might suspect, involves interacting with logic outside of the sandbox and our ability to patch it.
Specifically, before COM can perform cross-apartment activation (whether in- or out-of-process), it needs to initialize the remoting facilities and invoke CoInitializeSecurity. As the documentation explains, the programmer can choose to call this function manually; otherwise, the system will do it implicitly. Internally, CoInitializeSecurity connects to the ALPC port of the RPCSS service and attempts to invoke ILocalObjectExporter's Connect method.
Old-school resources might refer to RPCSS as a Service Control Manager (SCM). This terminology, however, fell out of use around the introduction of another Service Control Manager (a component running inside
services.exeresponsible for starting service processes) in Windows NT. While nowadays an overwhelming number of references to SCM imply the NT SCM, one might still find occasional mentions of the COM SCM.
As shown on the diagram, before dispatching the request, the RPC runtime verifies the identity and permissions of the caller. First, rpcrt4!RPC_INTERFACE::EnforceInterfaceSecurityDescriptor performs an access check against the interface's security descriptor:
| SID | Comment |
| Everyone | Most non-sandboxed callers |
| BUILTIN\Administrators | Highly-privileged callers |
| NT AUTHORITY\SYSTEM | Highly-privileged callers |
| APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES | Non-LPAC AppContainer callers |
| APP CAPABILITY\lpacCom | LPAC callers with the capability |
| NT AUTHORITY\ANONYMOUS LOGON | Anonymous non-sandoxed callers |
This particular security descriptor is not configurable. It is created inside rpcss!CreateSd and protects several core remoting interfaces, including ILocalObjectExporter, ILocalObjectExporterCrossVM, ISCM, ISCMActivator, IMachineActivatorControl, IROT, and IActivationKernel.
Second, rpcrt4!RPC_INTERFACE::DoSecurityCallbackHelper invokes a security callback, which, in this case, refers to rpcss!LocalInterfaceOnlySecCallback. The callback, in turn, performs another access check against a similar security descriptor:
| SID | Comment |
| Everyone | Most non-sandboxed callers |
| APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES | Non-LPAC AppContainer callers |
| APP CAPABILITY\lpacCom | LPAC callers with the capability |
| BUILTIN\Performance Log Users | Special group members (non-sandboxed) |
| BUILTIN\Distributed COM Users | Special group members (non-sandboxed) |
| NT AUTHORITY\ANONYMOUS LOGON | Anonymous non-sandoxed callers |
In both lists, we can see the familiar lpacCom capability SID appearing again, alongside entries that grant access to non-sandboxed and non-LPAC AppContainer callers. This time, however, the checks execute within a system service and are therefore beyond the reach of our bypasses. Contrary to the use of lpacCom we saw earlier, this implementation is indeed a secure way of enforcing the capability requirement. Naturally, it applies to all out-of-process COM activation; it just so happened to also affect our in-process cross-apartment scenario.
The second security descriptor is part of the DCOM family, which you might've guessed based on its mention of Distributed COM Users. There are four in total: so-called access permissions, access restrictions, launch permissions, and launch restrictions. Distributed COM differentiates between "access" and "launch" actions based on whether the request attempts to interact with an already running component server process or to spawn a new one.
You can view and edit these security descriptors via the built-in Component Management MMC snap-in or (since not long ago) the Canary builds of System Informer. The raw values reside under HKLM\SOFTWARE\Microsoft\Ole:
If any of the binary values are missing, the system falls back to the default (hard-coded) security returned by CoGetSystemSecurityPermissions. We don't recommend changing these settings on production systems. If you do, keep in mind that the service enforces certain restrictions on the DACL and will silently reset the security descriptor to the defaults upon deviation. Plus, the caller must notify DCOM/RPCSS about the change by calling combase!UpdateDCOMSettings. Administrative permissions are required, of course.
At this point, it should be clear that out-of-process COM is unusable from a Less-Privileged AppContainer without lpacCom. There are, however, still more interesting access checks to explore if we can satisfy the lpacCom requirement. In addition to the shared access/launch restrictions, DCOM has configurable security settings for activating specific components. COM uses a concept of AppIDs for assigning server-level security and remoting configuration to groups of CLSIDs.
For example, CLSID_BackgroundCopyManager is a CLSID for interacting with the Background Intelligent Transfer Service (BITS). If we look it up in the registry, we can find the following information:
[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{4991d34b-80a1-4291-83b6-3328366b9097}]
@="Background Intelligent Transfer Control Class 1.0"
"AppID"="{69AD4AEE-51BE-439b-A92C-86AE490E8B30}"
Searching for the corresponding AppID key reveals:
[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\AppID\{69AD4AEE-51BE-439b-A92C-86AE490E8B30}]
@="Background Intelligent Transfer Service"
"LaunchPermission"=hex:01,00 ; truncated for brevity
"LocalService"="BITS"
Decoding the launch permissions shows the following access control list:
| SID | Comment |
| BUILTIN\Administrators | Highly-privileged callers (non-sandboxed) |
| NT AUTHORITY\INTERACTIVE | Interactively logged on users (non-sandboxed) |
| BUILTIN\Remote Management Users | Special group members (non-sandboxed) |
| NT AUTHORITY\SERVICE | Services (non-sandboxed) |
| NT AUTHORITY\SYSTEM | Highly-privileged callers (non-sandboxed) |
| APP CAPABILITY\internetClient | LPAC and non-LPAC AppContainer capability holders |
| APP CAPABILITY\internetClientServer | LPAC and non-LPAC AppContainer capability holders |
| APP CAPABILITY\privateNetworkClientServer | LPAC and non-LPAC AppContainer capability holders |
As you can see, AppContainer callers (whether LPAC or not) need at least one of the internet-granting capabilities to activate the BITS component.
Assessing the attack surface of out-of-process components reachable from AppContainer with specific capabilities is an interesting research topic that we will not cover in this series. There are great tools available that can aid in this endeavour, most notably the NtObjectManager PowerShell module and OleView.NET, both by James Forshaw.
Now we've seen both sides of lpacCom. We opened with the in-process check, which was doomed to failure due to its inherent incompatibility with essential security practices. No surprises here; we managed to bypass it, unlocking in-process COM and WinRT in the Less-Privileged AppContainer sandbox, at least as long as we have registryRead or our components can survive manual instantiation. And also don't touch much of the cross-apartment marshalling facilities. Then we saw a set of formidable server-side lpacCom checks in RPCSS and DCOM services that successfully block out-of-process COM. Why did Microsoft decide to include both mechanisms? Perhaps the first one was never intended as a security measure, but rather a shortcut for quickly identifying dependence on a technology in a restricted environment. Still, it served as an excellent exercise for trying to understand COM and its design principles.
While the lpacCom part of this story is over, the next blog post in the series will revolve around registryRead and how we can make CoCreateInstance work without it. The tricks we will discuss allow redirecting component lookup to an arbitrary location and have value outside of sandbox environments. And no, it's much more interesting than a simple TreatAs, so stay tuned.