Wednesday, 1 January 2020

Empirically Assessing Windows Service Hardening

In the past few years there's been numerous exploits for service to system privilege escalation. Primarily they revolve around the fact that system services typically have impersonation privilege. What this means is given access to a suitable token handle of an administrator (say through the Rotten Potato attack) you can impersonate and elevate from a lower-privileged service account to SYSTEM. The problem for discovers of these attacks is that Microsoft do not consider them something which needs to be fixed with a security bulletin, as having SeImpersonatePrivilege is basically a massive security hole. However MS go and fix them silently making it unclear if they care or not.

Of course, none of this is really new, Cesar Cerrudo detailed these sorts of service attacks in Token Kidnapping and Token Kidnapping's Revenge. The novel element recently is how to get hold of the access token, for example via negotiating local NTLM authentication. Microsoft seem to have been fighting this fire for almost 10 years and still have not gotten it right. In shades of UAC, a significant security push to make services more isolated and secure has been basically abandoned because (presumably) MS realized it was an indefensible boundary.

That's not to say there hasn't been interesting service account to SYSTEM bugs which Microsoft have fixed. The most recent example is CVE-2019-1322 which was independently discovered by multiple parties (DonkeysTeamIlias Dimopoulos and Edward Torkington/Phillip Langlois of NCC). To understand the bug you probably should read up one of the write-ups (NCC one here) but the gist is, the Update Orchestrator Service has a service security descriptor which allowed "NT AUTHORITY\SERVICE" full access. It so happens that all system services, including lower-privileged ones have this group and so you could reconfigure the service (which was running as SYSTEM) to point to any other binary giving a direct service to SYSTEM privilege escalation.

That begs the question, why was CVE-2019-1322 special enough to be fixed and not issues related to impersonation? Perhaps it's because this issue didn't rely on impersonate privileges being present? It is possible to configure services to not have impersonate privilege, so presumably if you could go from a non-impersonate service to an impersonate service that would count as a boundary? Again probably not, for example this bug which abuses the scheduled task service to regain impersonate privilege wouldn't likely be fixed by Microsoft.

That lack of clarity is why I tweeted to Nate Warfield and ultimately to Matt Miller asking for some advice with respect to the MSRC Security Servicing Guidelines. The result is, even if the service doesn't have impersonate privilege it wouldn't be a defended boundary if all you get is the same user with additional privileges as you can't block yourself from compromising yourself. This is the UAC argument over again, but IMO there's a crucial difference, Windows Service Hardening (WSH) was supposed to fix this problem for us in Vista. Unsurprisingly Cesar Cerrudo also did a presentation about this at the inaugural (maybe?) Infiltrate in 2011.

The question I had was, is WSH still as broken as it was in 2011? Has anything changed which made WSH finally live up to its goal of making a service compromise not equal to a full system compromise? To determine that I thought I'd run an experiment on Windows 10 1909. I'm only interested in the features which WSH touches which led me to the following hypothesis:

"Under Windows Service Hardening one service without impersonate privilege can't write to the resources of another service which does have the privilege, even if the same user, preventing full system compromise."

The hypothesis makes the assumption that if you can write to another service's resources then it's possible to compromise that other service. If that other service has SeImpersonatePrivilege then that inevitably leads to full system compromise. Of course that's not necessarily the case, the resource being written to might be uninteresting, however as a proxy this is sufficient as the goal of WSH is to prevent one service modifying the data of another even though they are the same underlying user.

WSH Details

Before going into more depth on the experiment, let's quickly go through the various features of WSH and how they're expressed. If you know all this you can skip to the description of the experiment and the results.

Limited Service Accounts and Reduced Privilege

This feature is by far the oldest attempt to harden services, the introduction of the LOCAL SERVICE (LS) and NETWORK SERVICE (NS) accounts. Prior to the accounts introduction there was only two ways of configuring the user for a system service on Windows, either the fully privileged SYSTEM account or creating a local/domain user which has the "Log on as a Service" right. The two accounts where introduced in XP SP2 (I believe) after worms such as Blaster basically got SYSTEM privilege through remotely attacking exposed services. The two service accounts are not administrator accounts which means they shouldn't be able to directly compromise the system. The accounts are very similar on Windows 10 1909, they are both assigned the following groups*:

BUILTIN\Users
CONSOLE LOGON
Everyone
LOCAL
NT AUTHORITY\Authenticated Users
NT AUTHORITY\LogonSessionId_X_Y
NT AUTHORITY\SERVICE
NT AUTHORITY\This Organization

* Technically this isn't 100% accurate, on my machine the LS account has some extra capability groups, but we'll ignore those for this blog post.

No Administrator group in sight. Each service token gets a unique Logon Session ID SID which will be important later. The service accounts also have a limited set of privileges, as shown below:

SeAssignPrimaryTokenPrivilege
SeAuditPrivilege
SeChangeNotifyPrivilege
SeCreateGlobalPrivilege
SeImpersonatePrivilege
SeIncreaseQuotaPrivilege
SeIncreaseWorkingSetPrivilege
SeShutdownPrivilege
SeSystemTimePrivilege†
SeTimeZonePrivilege
SeUndockPrivilege

† NETWORK SERVICE doesn't have SeSystemTimePrivilege.

The two privileges I've highlighted, SeAssignPrimaryTokenPrivilege and SeImpersonatePrivilege give these accounts effectively full system access when combined with a suitable privileged token. Part of WSH is also giving control over what privileges the service account actually requires. The default is to allow all privileges, however when configuring a service you can specify a list of privileges to restrict the service to. For example the CDPSvc service is configured to only require SeImpersonatePrivilege. Quite why they bother to put this restriction on the service I don't know ¯\_(ツ)_/¯.

What's the difference between LS and NS? The primary difference is LS has no network credentials, so accessing network resources as that user would only succeed as an anonymous login. NS on the other hand is created with the credentials of the computer account and so can interact with the network for resources allowed by that authentication. This only really matters to domain joined machines, standalone machines would not share the computer account with anyone else.

Per-Service SID

The first big addition in WSH was the Per-Service SID. This SID is automatically added to the group list of default groups shown previously by the SCM when creating the service's primary token. The service SID is also added with the SE_GROUP_OWNER flag set and is not mandatory, which means it can be set as the token's default owner when creating new resources and it can disabled. The basic idea is a service can ACL its resources to this SID to prevent other services from accessing them. The use of a service SID is optional, but the majority of default services are configured to use it. An example SID for CDPSvc is as follows:

S-1-5-80-3433512109-503559027-1389316256-1766580070-2256751264

The SID is derived by generating a SHA1 hash of the service name and adding that as the SID's RIDs (with an extra 80 at the start to signify it's a service SID). The use of a hash should make it extremely unlikely two services would generate the same SID.

Of course it's up to the service to actually ACL their resources appropriately. To aid in that the token's default DACL is also configured to the following (for CDPSvc):

- Type  : Allowed
- Name  : NT AUTHORITY\SYSTEM
- Access: Full Access

- Type  : Allowed
- Name  : OWNER RIGHTS
- Access: ReadControl

- Type  : Allowed
- Name  : NT SERVICE\CDPSvc
- Access: Full Access

The three entries grant SYSTEM and the service SID full access to any resources with this DACL. It then limits the owner of the resource through OWNER RIGHTS to only READ_CONTROL access. This directly prevents one service account accessing the resources of another for write access. Unfortunately the default DACL is only applied when there's no other access control specified, either explicitly at creation time or due to inheritance. 

One other thing to point out is that Windows still has shared services through the use of SVCHOST. If multiple services are registered in a specific SVCHOST instance then the SCM will create the token with all service SIDs in the group list and default DACL even if a service isn't currently loaded in the host. That has become less of an issue since Windows 1703, as long as you have greater that 3.5GB of RAM services will run in separate SVCHOST instances and all services will be totally separate.

Write-Restricted Token

The second big addition to WSH was the concept of Write-Restricted (WR) tokens. Restricted token's have existed since Windows 2000 and are created using the NtFilterToken system call. The basic concept is the token can have a list of additional groups which are consulted when ever an access check is performed. First the access check is run on the default group list, if access would be granted the access check is run again on the restricted SIDs. If the second check is successful then the access check passes, if not access is denied. 

Restricted tokens are used for sandboxing (such as in Chrome) but are difficult to setup correctly as it blocks all access equally including reading critical files on disk. WR tokens solve the access problem by only blocking write access but leaving read and execute access alone. 

In order for a service configured as WR to write to a resource the associated security descriptor must contain the required access for one of the following restricted SIDs.

Everyone
NT AUTHORITY\LogonSessionId_X_Y
NT AUTHORITY\WRITE RESTRICTED
NT SERVICE\SERVICE_NAME

The WRITE RESTRICTED SID is a special group SID which resources can apply if they expect a service to write to the resource. This SID is also added to the token's groups by the SCM so that it can be used to pass both checks. By combining service SIDs and WR the amount of resources a service can modify should be significantly reduced.

And the Rest

There's a few things which are technically part of service hardening which won't really consider for the experiment:

The main one is additional rules in the firewall to block network services or requests being made from a service. This is arguably more to prevent remote compromise than it is to prevent cross-service attacks. 

Another is Session 0 Isolation and System Integrity Level. Session 0 Isolation was introduced to prevent Shatter Attacks, by preventing any windows being created by a service on the same desktop as a normal user. System Integrity Level through UIPI then prevents attacks even if the service did create a window on a normal user desktop as it'd be at a much higher IL (even than Administrators). The System IL does admittedly also have a security access check function but it's not that important for cross-service attacks.

Experiment Procedure

On to the experiment itself. Based on the hypothesis I presented earlier the goal is to determine if you can write to resources of one service from another service even though they're the same user. To make this testable I decided on the following procedure:

Step 1: Build an access token for a service which doesn't exist on the system.
Step 2: Enumerate all resources of a specific type which are owned by the token owner and perform an access check using the token.
Step 3: Collate the results based on the type of resource and whether write access was granted.

The reason for choosing to build a token for a non-existent service is it ensures we should only see the resources that could be shared by other services as the same user, not any resources which are actually designed to be accessible by being created by a service. These steps need to be repeated for different access tokens, we'll use the following five:
  • LOCAL SERVICE
  • LOCAL SERVICE, Write Restricted
  • NETWORK SERVICE
  • NETWORK SERVICE, Write Restricted
  • Control
We'll test both normal service SID and WR versions of the access token to see if it makes much of a difference. One thing to determine is what to use as a control. Ideally the control would be another service account with WSH disabled. However I couldn't find a way to disable WSH entirely to do this test, so instead we need some other control. If our hypothesis holds and WSH is effective we'd expect no resources to be writable, therefore we need to pick a control account where we know this is not true. The easiest is just to use the current logged on user account, it should be able to access almost all its own resources.

What resources do we want to inspect? The obvious type is Process/Thread resources. Getting write access to either of these in another service is probably a trivial to get full system compromise through impersonate. We'd want to get a bigger picture however, it'd be useful to include Files, Registry keys and Named Kernel Objects. These resources might not directly lead to compromise but it does give us a general idea of the maximum impact. 

It's worth noting that the hypothesis made a point to specify writing to the resources of a service which has impersonate privilege from one which does not. However this experimental process will only base the analysis on whether the resource is owned by the service user. This is intentional, it'd be too complex to attribute the resource to a specific service in all cases. However an assumption is made that more services running as a specific user have impersonate privilege than do not, therefore in all probability any resource you can write to is probably owned by one of them. We could verify that assumption if we liked, but I'll probably not.

Finally, a good experiment should be something which can be repeatable and verifiable. To that end I'll provide all the code necessary to perform the steps, written in PowerShell and using my NtObjectManager module. If you want to re-run the experiment you should be able to do so and produce a very similar set of results.

Experiment Procedure Detail

On to specific PowerShell steps to perform the experiment. First off you'll need my NtObjectManager module, specifically at least version 1.1.25 as I've added a few extra commands to simplify the process. You will also need to run all the commands as the SYSTEM user, some command will need it (such as getting access tokens) others benefit for the elevated privileges. From an admin command prompt you can create a SYSTEM PowerShell console using the following command:

Start-Win32ChildProcess -RequiredPrivilege SeTcbPrivilege,SeBackupPrivilege,SeRestorePrivilege,SeDebugPrivilege powershell

This command will find a SYSTEM process to create the new process from which also has, at a minimum, the specified list of privileges. Due to the way the process is created it'll also have full access to the current desktop so you can spawn GUI applications running at system if you need them.

The experiment will be run on a VM of Windows 1909 Enterprise updated to December 2019 from a split-token admin user account. This just ensures the minimum amount of configuration changes and additional software is present. Of course there's going to be variability on the number of services running at any one time, there's not a lot which can be done about that. However it's expected that the result should be same even if the individual resources available are not. If you were concerned you could rerun the experiment on multiple different installs of Windows at different times of day and aggregate the results.

Creating the Access Tokens

We need to create 5 access tokens for the test. Ideally we'd like to create the four service tokens using the exact method used by the SCM. We could register our unknown service and start the service to steal its token. There is also an undocumented RGetServiceProcessToken SCM RPC method in newer versions of Windows 10. However I think creating a service risks some resources being populated with that service's identity which might not be what we really want. Instead we can use LogonUserExExW which is what the SCM uses, with the LOGON32_LOGON_SERVICE type to create LS and NS tokens. This will work as long as we have SeTcbPrivilege. We'll then just add the appropriate groups, convert to WR,  and remove privileges as necessary. We can get to the LogonUserExExW API using Get-NtToken. I've wrapped up everything into a function Get-ServiceToken, you can see the full function in the final script. Using this function we can create all the tokens we need using the following commands:

$tokens = @()
$tokens += Get-ServiceToken LocalService FakeService
$tokens += Get-ServiceToken LocalService FakeService -WriteRestricted
$tokens += Get-ServiceToken NetworkService FakeService
$tokens += Get-ServiceToken NetworkService FakeService -WriteRestricted

For the control token we'll get the unmodified session access token for the current desktop. Even though we're running as SYSTEM as we're running on the same desktop we can just use the following command:

$tokens += Get-NtToken -Session -Duplicate

Random note. When calling LogonUserExExW and requesting a service SID as an additional group the call will fail with access denied. However this only happens if the service SID is the first NT Authority SID in the additional groups list. Putting any other NT Authority SID, including our new logon session SID before the service SID makes it work. Looking at the code in LSASRV (possibly the function LsapCheckVirtualAccountRestriction) it looks like the use of a service SID should be restricted to the first process (based on its PID) that used a service SID which would be the SCM. However if another NT Authority SID is placed first the checking loop sets a boolean flag which prevents the loop checking any more SIDs and so the service SID is ignored. I've no idea if this is a bug or not, however as you need TCB privilege to set the additional groups I don't think it's a security issue.

Resource Checking and Result Collation

With the 5 tokens in hand we can progress to assessing accessible resources. The original purpose of my Sandbox Analysis tools was finding accessible resources from a sandbox process, however the same code is capable of finding resources accessible from any access token, including service tokens.

First as way of example lets run checks for process and threads:

$ps = Get-AccessibleProcess -Tokens $tokens `
    -CheckMode ProcessOnly -AllowEmptyAccess
$ts = Get-AccessibleProcess -Tokens $tokens `
    -CheckMode ThreadOnly -AllowEmptyAccess

We can pass a list of tokens to the checking command, this improves performance as we only do the enumeration of resources for every token group then do the access check. Each generated access result has a TokenId property which indicates the unique ID of the token which was used for the check, this allows us to extract the correct results later. We also specify the AllowEmptyAccess option, which will generate a result even if the access check fails and the token has no access to the resource. This will be useful to allow us to assess what resources are owned by the token's owner SID but we were not granted access.

Let's do the rest of the resources:

$os = Get-AccessibleObject \ -Recurse `
    -Tokens $tokens -AllowEmptyAccess
$fs = Get-AccessibleFile -Win32Path "$env:SystemDrive\" `
    -FormatWin32Path -Recurse -Tokens $tokens -AllowEmptyAccess
$ks = Get-AccessibleKey \Registry -FormatWin32Path -Recurse `
    -Tokens $tokens -AllowEmptyAccess

We'll only get the accessible files on the system drive in this case as that'll be the only drive in the VM. Note that Get-AccessibleObject doesn't check ALPC ports, it's not possible to open an ALPC port by name and read its security descriptor. We'll ignore ALPC ports for this experiment, as it's probably worthy of a topic all on its own.

We now have all the results we need in five variables along with the tokens. If you want to run it yourself the final script is on Github here. It'll take a fair amount of time to run but once it's complete you'll find 5 CSV files in the current directory containing the results for each token.

Experiment Results

We now need to do our basic analysis of the results. Let's start with calculating the percentage of writable resources for each token type relative to the total number of resources. From my single experiment run I got the following table:

TokenWritableWritable (WR)Total
Control99.83%N/A13171
Network Service65.00%0.00%300
Local Service62.89%0.70%574

As we expected the control token had almost 100% of the owned resources writable by the user.  However for the two service accounts both had over 60% of their owned resources writable when using an unrestricted token. That level is almost completely eliminated when using a WR token, there were no writable resources for NS and only 4 resources writable from LS, which was less than 1%. Those 4 resources were just Events, from a service perspective not very exciting though there were ACL'ed to everyone which is unusual.

Just based on these numbers alone it would seem that WSH really is a failure when used unrestricted but is probably fine when used in WR mode. It'd be interesting to dig into what types are writable in the unrestricted mode to get a better understanding of where WSH is failing. This is what I've summarized in the following table:

TypeLS Writable%LS WritableNS Writable%NS Writable
Directory0.28%10.51%1
Event1.66%60.51%1
File74.24%26848.72%95
Key22.44%8149.23%96
Mutant0.28%10.51%1
Process0.28%10.00%0
Section0.55%20.00%0
SymbolicLink0.28%10.51%1
Thread0.00%00.00%0

The clear winners, if there is such a thing is Files and Registry Keys taking up over 95% of the resources which are writable. Based on what we know about how WSH works this is understandable. The likelihood is any keys/files are getting their security through inheritance from the parent container. This will typically result in at least the owner field being the service account granted WRITE_DAC access, or the inherited DACL will contain an OWNER CREATOR SID which results an explicit access for the service account.

What is perhaps more interesting is the results for Processes and Threads, neither NS or LS have any writable threads and only LS has a single writable process. This primary reason for the lack of writable threads and processes is due to the default DACL which is used for new processes when an explicit DACL isn't specified. The DACL has a OWNER RIGHTS SID granted only READ_CONTROL access, the result is that even if the owner of the resource is the service account it isn't possible to write to it. The only way to get full access as per the default DACL is by having the specific service SID in your group list.

Why does LS have one writable process? This I think is probably a "bug" in the Audio Service which creates the AUDIODG process. If we look at the security descriptor of the AUDIODG process we see the following:

<Owner>
 - Name  : NT AUTHORITY\LOCAL SERVICE

<DACL>
 - Type  : Allowed
 - Name  : NT SERVICE\Audiosrv
 - Access: Full Access

 - Type  : Allowed
 - Name  : NT AUTHORITY\Authenticated Users
 - Access: QueryLimitedInformation

The owner is LS which will grant WRITE_DAC access to the resource if nothing else is in the DACL to stop it. However the default DACL's OWNER RIGHTS SID is missing from the DACL, which means this was probably set explicitly by the Audio Service to grant Authenticated Users query access. This results in the access not being correctly restricted from other service accounts. Of course AUDIODG has SeImpersonatePrivilege so if you find yourself inside a LS unrestricted process with no impersonate privilege you can open AUDIODG (if running) for WRITE_DAC, change the DACL to grant full access and get back impersonate privileges.

If you look at the results one other odd thing you'll notice is that while there are readable threads there are no readable processes, what's going on? If we look at a normal LS service process' security descriptor we see the following:

<Owner>
 - Name  : NT AUTHORITY\LogonSessionId_0_202349

<DACL>
 - Type  : Allowed
 - Name  : NT AUTHORITY\LogonSessionId_0_202349
 - Access: Full Access

 - Type  : Allowed
 - Name  : BUILTIN\Administrators
 - Access: QueryInformation|QueryLimitedInformation

We should be able to see the reason, the owner is not LS, but instead the logon session SID which is unique per-service. This blocks other LS processes from having any access rights by default. Then the DACL only grants full access to the logon session SID, even administrators are apparently not the be trusted (though they can typically just bypass this using SeDebugPrivilege). This security descriptor is almost certainly set explicitly by the SCM when creating the process.

Is there anything else interesting in writable resources outside of the files and keys? The one interesting result shared between NS and LS is a single writable Object Directory. We can take a look at the results to find out what directories these are, to see if they share any common purpose. The directory paths are \Sessions\0\DosDevices\00000000-000003e4 for NS and \Sessions\0\DosDevices\00000000-000003e5 for LS. These are the service account's DOS Device directory, the default location to start looking up drive mappings. As the accounts can write to their respective directory this gives another angle of attack, you can compromise any service process running as the same used by dropping a mapping for the C: drive and waiting the process to load a DLL. Leaving that angle open seems sloppy, but it's not like there are no alternative routes to compromise another service.

I think that's the limit of my interest in analysis. I've put my results up on Google Drive here if you want to play around yourself.

Conclusions

Even though I've not run the experiment on multiple machines, at different times with different software I think I can conclude that WSH does not provide any meaningful security boundary when used in its default unrestricted mode. Based on the original hypothesis we can clearly write to resources not created by a service and therefore could likely fully compromise the system. The implementation does do a good job of securing process and thread resources which provide trivial elevation routes but that can be easily compromised if there's appropriate processes running (including some COM services). I can fully support this not being something MS would want to defend through issuing bulletins.

However when used in WR mode WSH is much more comprehensive. I'd argue that as long as a service doesn't have impersonate privilege then it's effectively sandboxed if running in with a WR token. MS already support sandbox escapes as a defended boundary so I'm not sure why WR sandboxes shouldn't also be included as part of that. For example if the trick using the Task Scheduler worked from a WR service I'd see that as circumventing a security boundary, however I don't work in MSRC so I have no influence on what is or is not fixed.

Of course in an ideal world you wouldn't use shared accounts at all. Versions of Windows since 7 have support for Virtual Service Accounts where the service user is the service SID rather than a standard service account and the SCM even limits the service's IL to High rather than System. Of course by default these accounts still have impersonate privilege, however you could also remove that.

Thursday, 5 December 2019

The Mysterious Case of a Broken Virus Scanner

On my VM (with a default Windows 10 1909) I used for my series of AppLocker I wanted to test out the new Edge.  I opened the old Edge and tried to download the canary installer, however the download failed, Edge said the installer had a virus and it'd been deleted. How rude! I also tried the download in Chrome on the same machine with the same result, even ruder!

Downloading Edge Canary in Edge with AppLocker. Shows a bar that the download has been deleted because it's a virus.

Oddly it worked if I turned off DLL Rule Enforcement, but not when I enabled it again. My immediate thought might be the virus checking was trying to map the executable and somehow it was hitting the DLL verification callback and failing as the file was in my Downloads folder which is not in the default rule set. That seemed pretty unlikely, however clearly something was being blocked from running. Fortunately AppLocker maintains an Audit Log under "Applications and Services Logs -> Microsoft -> Windows -> AppLocker -> EXE and DLL" so we can quickly diagnose the failure.

Failing DLL load in audit log showing it tried to load %OSDRIVE%\PROGRAMDATA\MICROSOFT\WINDOWS DEFENDER\PLATFORM\4.18.1910.4-0\MPOAV.DLL

The failing DLL load was for "%OSDRIVE%\PROGRAMDATA\MICROSOFT\WINDOWS DEFENDER\PLATFORM\4.18.1910.4-0\MPOAV.DLL". This makes sense, the default rules only permit %WINDOWS% and %PROGRAMFILES% for normal users, however %OSDRIVE%\ProgramData is not allowed. This is intentional as you don't want to grant access to locations a normal user could write to, so generally allowing all of %ProgramData% would be asking for trouble. [update:20191206] of course this is known about (I'm not suggesting otherwise), AaronLocker should allow this DLL by default.

I thought it'd at least be interesting to see why it fails and what MPOAV is doing. As the same failure occurred in both Edge (I didn't test IE) and Chrome it was clearly some common API they were calling. As Chrome is open source it made more sense to look there. Tracking down the resource string for the error lead me to this code. The code was using the Attachment Services API. Which is a common interface to verify downloaded files and attachments, apply MOTW and check for viruses.

When the IAttachmentExecute::Save method is called the file is checked for viruses using the currently registered anti-virus COM object which implements the IOfficeAntiVirus interface. The implementation for that COM class is in MPOAV.DLL, which as we saw is blocked so the COM object creation fails. And a failure to create the object causes the Save method to fail and the Attachment Services code to automatically delete the file so the browser can't even do anything about it such as ask the user. Ultra rude!

You might wonder how is this COM class is registered? An implementor needs to register their COM object with a Category ID of "{56FFCC30-D398-11d0-B2AE-00A0C908FA49}". If you have OleViewDotNet setup (note there are other tools) you can dump all registered classes using the following PowerShell command:

Get-ComCategory -CatId '56FFCC30-D398-11d0-B2AE-00A0C908FA49' | Select -ExpandProperty ClassEntries

On a default installation of Windows 10 you should find a single class, "Windows Defender IOfficeAntiVirus implementation" registered which is implemented in the MPOAV DLL. We can try and create the class with DLL enforcement to convince ourselves that's the problem:

PowerShell error when creating MSOAV COM object. Fails with AppLocker policy block error.

No doubt this has been documented before (and I've not looked [update:20191206] of course Hexacorn blogged about it) but you could probably COM hijack this class (or register your own) and get notified of every executable downloaded by the user's web browser. Perhaps even backdoor everything. I've not tested that however ;-)

This issue does demonstrate a common weakness with any application allow-listing solution. You've got to add a rule to allow this (probably undocumented) folder in your DLL rules. Or you could allow-list all Microsoft Defender certificates I suppose. Potentially both of these criteria could change and you end up having to fix random breakage which wouldn't be fun across a large fleet of machines. It also demonstrates a weird issue with attachment scanning, if your AV is somehow misconfigured things will break and there's no obvious reason why. Perhaps we need to move on from using outdated APIs to do this process or at least handle failure better.

Wednesday, 20 November 2019

The Internals of AppLocker - Part 4 - Blocking DLL Loading

This is part 4 in a short series on the internals of AppLocker (AL). Part 1 is here, part 2 here and part 3 here. As I've mentioned before this is how AL works on Windows 10 1909, it might differ on other versions of Windows.

In the first three parts of this series I covered the basics of how AL blocked process creation. We can now tackle another, optional component, blocking DLL loading. If you dig into the Group Policy Editor for Windows you will find a fairly strong warning about enabling DLL rules for AL:

Warning text on DLL rules staying that enabling them could affect system performance.

It seems MS doesn't necessarily recommend enabling DLL blocking rules, but we'll dig in anyway as I can't find any official documentation on how it works and it's always interesting to better understand how something works before relying on it.

We know from the part 1 that there's a policy for DLLs in the DLL.Applocker file. We might as well start with dumping the Security Descriptor from the file using the Format-AppLockerSecurityDescriptor function from part 3, to check it matches our expectations. The DACL is as follows:

 - Type  : AllowedCallback
 - Name  : Everyone
 - Access: Execute|ReadAttributes|ReadControl|Synchronize
 - Condition: APPID://PATH Contains "%WINDIR%\*"

 - Type  : AllowedCallback
 - Name  : Everyone
 - Access: Execute|ReadAttributes|ReadControl|Synchronize
 - Condition: APPID://PATH Contains "%PROGRAMFILES%\*"

 - Type  : AllowedCallback
 - Name  : BUILTIN\Administrators
 - Access: Execute|ReadAttributes|ReadControl|Synchronize
 - Condition: APPID://PATH Contains "*"

 - Type  : Allowed
 - Name  : APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES
 - Access: Execute|ReadAttributes|ReadControl|Synchronize

 - Type  : Allowed
 - Name  : APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES
 - Access: Execute|ReadAttributes|ReadControl|Synchronize

Nothing shocking here, just our rules written out in a security descriptor. However it gives us a hint that perhaps some of the enforcement is being done inside the kernel driver. Unsurprisingly if you look at the names in APPID you'll find a function called SrpVerifyDll. There's a good chance that's our target to investigate.

By chasing references you'll find the SrpVerifyDll function being called via a Device IO control code to an device object exposed by the APPID driver (\Device\SrpDevice). I'll save you the effort of reverse engineering, as it's pretty routine. The control code and input/output structures are as follows:

// 0x225804
#define IOCTL_SRP_VERIFY_DLL CTL_CODE(FILE_DEVICE_UNKNOWN, 1537, \
            METHOD_BUFFERED, FILE_READ_DATA)

struct SRP_VERIFY_DLL_INPUT {
    ULONGLONG FileHandle;
    USHORT FileNameLength;
    WCHAR FileName[ANYSIZE_ARRAY];
};

struct SRP_VERIFY_DLL_OUTPUT {
    NTSTATUS VerifyStatus;
};

Looking at SrpVerifyDll itself there's not much to really note. It's basically very similar to the verification done for process creation I described in detail in part 2 and 3:
  1. An access check token is captured and duplicated. If the token is restricted query for the logon session token instead.
  2. The token is checked whether it can bypass policy by being SANDBOX_INERT or a service.
  3. Security attributes are gathered using AiGetFileAttributes on the passed in file handle.
  4. Security attributes set on token using AiSetTokenAttributes.
  5. Access check performed using policy security descriptor and status result written back to the Device IO Control output.
It makes sense the the security attributes have to be recreated as the access check needs to know the information about the DLL being loaded not the original executable. Even though a file name is passed in the input structure as far as I can tell it's only used for logging purposes.

There is one big difference in step 1 where the token is captured over the one I documented in part 3. In process blocking if the current token was a non-elevated UAC token then the code would query for the full elevated token and use that to do the access check. This means that even if you were creating a process as the non-elevated user the access check was still performed as if you were an administrator. In DLL blocking this step does not take place, which can lead to a weird case of being able to create a process in any location, but not being able to load any DLLs in the same directory with the default policy. I don't know if this is intentional or Microsoft just don't care?

Who calls the Device IO Control to verify the DLL? To save me some effort I just set a breakpoint on SrpVerifyDll in the kernel debugger and then dumped the stack to find out the caller:

Breakpoint 1 hit
appid!SrpVerifyDll:
fffff803`38cff100 48895c2410      mov qword ptr [rsp+10h],rbx
0: kd> kc
 # Call Site
00 appid!SrpVerifyDll
01 appid!AipDeviceIoControlDispatch
02 nt!IofCallDriver
03 nt!IopSynchronousServiceTail
04 nt!IopXxxControlFile
05 nt!NtDeviceIoControlFile
06 nt!KiSystemServiceCopyEnd
07 ntdll!NtDeviceIoControlFile
08 ADVAPI32!SaferpIsDllAllowed
09 ADVAPI32!SaferiIsDllAllowed
0a ntdll!LdrpMapDllNtFileName
0b ntdll!LdrpMapDllFullPath
0c ntdll!LdrpProcessWork
0d ntdll!LdrpLoadDllInternal
0e ntdll!LdrpLoadDll

Easy, it's being called from the function SaferiIsDllAllowed which is being invoked from LdrLoadDll. This of course makes perfect sense, however it's interesting that NTDLL is calling a function in ADVAPI32, has MS never heard of layering violations? Let's look into LdrpMapDllNtFileName which is the last function in NTLL before the transition to ADVAPI32. The code which calls SaferiIsDllAllowed looks like the following:

NTSTATUS status;

if ((LoadInfo->LoadFlags & 0x100) == 0 
        && LdrpAdvapi32DllHandle) {
  status = LdrpSaferIsDllAllowedRoutine(
        LoadInfo->FileHandle, LoadInfo->FileName);
}

The call to SaferiIsDllAllowed  is actually made from a global function pointer. This makes sense as NTDLL can't realistically link directly to ADVAPI32. Something must be initializing these values, and that something is LdrpCodeAuthzInitialize. This initialization function is called during the loader initialization process before any non-system code runs in the new process. It first checks some registry keys, mostly importantly whether "\Registry\Machine\System\CurrentControlSet\Control\Srp\GP\DLL" has any sub-keys, and if so it proceeds to load the ADVAPI32 library using LdrLoadDll and query for the exported SaferiIsDllAllowed function. It stores the DLL handle in LdrpAdvapi32DllHandle and the function pointer 'XOR' encrypted in LdrpSaferIsDllAllowedRoutine.

Once SaferiIsDllAllowed is called the status is checked. If it's not STATUS_SUCCESS then the loader backs out and refuses to continue loading the DLL. It's worth reiterating how different this is from WDAC, where the security checks are done inside the kernel image mapping process. You shouldn't be able to even create a mapped image section which isn't allowed by policy when WDAC is enforced. However with AL loading a DLL is just a case of bypassing the check inside a user mode component.

If we look back at the calling code in LdrpMapDllNtFileName we notice there are two conditions which must be met before the check is made, the LoadFlags must not have the flag 0x100 set and LdrpAdvapi32DllHandle must be non-zero.

The most obvious condition to modify is LdrpAdvapi32DllHandle. If you already have code running (say VBA) you could use WriteProcessMemory to modify the memory location of LdrpAdvapi32DllHandle to be 0. Now any calls to LoadLibrary will not get verified and you can load any DLL you like outside of policy. In theory you might also be able to get the load of ADVAPI32 to fail. However unless LdrLoadDll returns STATUS_NOT_FOUND for the DLL load then the error causes the process to fail during initialization. As ADVAPI32 is in the known DLLs I can't see an easy way around this (I tried by renaming the main executable trick from the AMSI bypass).

The other condition, the LoadFlags is more interesting. There still exists a documented LOAD_IGNORE_CODE_AUTHZ_LEVEL flag you can pass to LoadLibraryEx which used to be able to bypass AppLocker DLL verification. However, as with SANDBOX_INERT this in theory was limited to only System and TrustedInstaller with KB2532445, although according to Stefan Kanthak it might not be blocked. That said I can't get this flag to do anything on Windows 10 1909 and tracing through LdrLoadDll it doesn't look like it's ever used. Where does this 0x100 flag come from then? Seems it's set by the LDrpDllCharacteristicsToLoadFlags function at the start of LdrLoadDll. Which looks like the following:

int LdrpDllCharacteristicsToLoadFlags(int DllCharacteristics) {
  int load_flags = 0;
  // ...
  if (DllCharacteristics & 0x1000)
    load_flags |= 0x100;
   
  return load_flags;
}

If we pass in 0x1000 as a DllCharacteristics flag (this doesn't seem to work by putting it in the DLL PE headers as far as I can tell) which is the second parameter to LdrLoadDll then the DLL will not be verified against the DLL policy. The DLL Characteristic flag 0x1000 is documented as IMAGE_DLLCHARACTERISTICS_APPCONTAINER but I don't know what API sets this flag in the call to LdrLoadDll. My original guess was LoadPackagedLibrary but that doesn't seem to be the case.

A simple PowerShell script to test this flag is below:
If you run Start-Dll "Path\To\Any.DLL" where the DLL is not in an allowed location you should find it fails. However if you run Start-Dll "Path\To\Any.DLL" 0x1000 you'll find the DLL now loads.

Of course realistically the DLL blocking is really more about bypassing the process blocking by using the DLL loader instead. Without being able to call LdrLoadDll or writing to process memory it won't be easy to bypass the DLL verification (but of course it will not impossible).

This is the last part on AL for a while, I've got to do other things. I might revisit this topic later to discuss AppX support, SmartLocker and some other fun tricks.

Tuesday, 19 November 2019

The Internals of AppLocker - Part 3 - Access Tokens and Access Checking

This is part 3 in a short series on the internals of AppLocker (AL). Part 1 is here, part 2 here and part 4 here.

In the last part I outlined how process creation is blocked with AL. I crucially left out exactly how the rules are processed to determine if a particular user was allowed to create a process. As it makes more sense to do so, we're going to go in reverse order from how the process was described in the last post. Let's start with talking about the access check implemented by SrppAccessCheck.

Access Checking and Security Descriptors

For all intents the SrppAccessCheck function is just a wrapper around a specially exported kernel API SeSrpAccessCheck. While the API has a few unusual features for this discussion might as well assume it to be the normal SeAccessCheck API. 

A Windows access check takes 4 main parameters:
  • SECURITY_SUBJECT_CONTEXT which identifies the caller's access tokens.
  • A desired access mask.
  • A GENERIC_MAPPING structure which allows the access check to convert generic access to object specific access rights.
  • And most importantly, the Security Descriptor which describes the security of the resource being checked.
Let's look at some code.

NTSTATUS SrpAccessCheckCommon(HANDLE TokenHandle, BYTE* Policy) {
    
    SECURITY_SUBJECT_CONTEXT Subject = {};
    ObReferenceObjectByHandle(TokenHandle, &Subject.PrimaryToken);
    
    DWORD SecurityOffset = *((DWORD*)Policy+4)
    PSECURITY_DESCRIPTOR SD = Policy + SecurityOffset;
    
    NTSTATUS AccessStatus;
    if (!SeSrpAccessCheck(&Subject, FILE_EXECUTE
                          &FileGenericMapping, 
                          SD, &AccessStatus) &&
        AccessStatus == STATUS_ACCESS_DENIED) {
        return STATUS_ACCESS_DISABLED_BY_POLICY_OTHER;
    }
    
    return AccessStatus;
}

The code isn't very complex, first it builds a SECURITY_SUBJECT_CONTEXT structure manually from the access token passed in as a handle. It uses a policy pointer passed in to find the security descriptor it wants to use for the check. Finally a call is made to SeSrpAccessCheck requesting file execute access. If the check fails with an access denied error it gets converted to the AL specific policy error, otherwise any other success or failure is returned.

The only thing we don't really know in this process is what the Policy value is and therefore what the security descriptor is. We could trace through the code to find how the Policy value is set , but sometimes it's just easier to breakpoint on the function of interest in a kernel debugger and dump the pointed at memory. Taking the debugging approach shows the following:

WinDBG window showing the hex output of the policy pointer which shows the on-disk policy.

Well, what do we have here? We've seen those first 4 characters before, it's the magic signature of the on-disk policy files from part 1. SeSrpAccessCheck is extracting a value from offset 16, which is used as an offset into the same buffer to get the security descriptor. Maybe the policy files already contain the security descriptor we seek? Writing some quick PowerShell I ran it on the Exe.AppLocker policy file to see the result:

PowerShell console showing the security output by the script from Exe.Applocker policy file.

Success, the security descriptor is already compiled into the policy file! The following script defines two functions, Get-AppLockerSecurityDescriptor and Format-AppLockerSecurityDescriptor. Both take a policy file as input and returns either a security descriptor object or formatted representation:

If we run Format-AppLockerSecurityDescriptor on the Exe.Applocker file we get the following output for the DACL (trimmed for brevity):

 - Type  : AllowedCallback
 - Name  : Everyone
 - Access: Execute|ReadAttributes|ReadControl|Synchronize
 - Condition: APPID://PATH Contains "%WINDIR%\*"

 - Type  : AllowedCallback
 - Name  : BUILTIN\Administrators
 - Access: Execute|ReadAttributes|ReadControl|Synchronize
 - Condition: APPID://PATH Contains "*"

 - Type  : AllowedCallback
 - Name  : Everyone
 - Access: Execute|ReadAttributes|ReadControl|Synchronize
 - Condition: APPID://PATH Contains "%PROGRAMFILES%\*"

 - Type  : Allowed
 - Name  : APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES
 - Access: Execute|ReadAttributes|ReadControl|Synchronize

 - Type  : Allowed
 - Name  : APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES
 - Access: Execute|ReadAttributes|ReadControl|Synchronize

We can see we have two ACEs which are for the Everyone group and one for the Administrators group. This matches up with the default configuration we setup in part 1. The last two entries are just there to ensure this access check works correctly when run from an App Container.

The most interesting part is the Condition field. This is a rarely used (at least for consumer version of the OS) feature of the security access checking in the kernel which allows a conditional expression evaluated to determine if an ACE is enabled or not. In this case we're seeing the SDDL format (documentation) but under the hood it's actually a binary structure. If we assume that the '*' acts as a globbing character then again this matches our rules, which let's remember:
  • Allow Everyone group access to run any executable under %WINDIR% and %PROGRAMFILES%.
  • Allow Administrators group to run any executable from anywhere.
This is how AL's rules are enforced. When you configure a rule you specify a group, which is added as the SID in an ACE in the policy file's Security Descriptor. The ACE type is set to either Allow or Deny and then a condition is constructed which enforces the rule, whether it be a path, a file hash or a publisher.

In fact let's add policy entries for a hash and publisher and see what condition is set for them. Download a new policy file from this link and run the Set-AppLockerPolicy command in an admin PowerShell console. Then re-run Format-ApplockerSecurityDescriptor:

 - Type  : AllowedCallback
 - Name  : Everyone
 - Access: Execute|ReadAttributes|ReadControl|Synchronize
 - Condition: (Exists APPID://SHA256HASH) && (APPID://SHA256HASH Any_of {#5bf6ccc91dd715e18d6769af97dd3ad6a15d2b70326e834474d952753
118c670})

 - Type  : AllowedCallback
 - Name  : Everyone
 - Access: Execute|ReadAttributes|ReadControl|Synchronize
 - Flags : None
 - Condition: (Exists APPID://FQBN) && (APPID://FQBN >= {"O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US\MICROSOFT® WINDOWS
® OPERATING SYSTEM\*", 0})

We can now see the two new conditional ACEs, for a SHA256 hash and the publisher subject name. Basically rinse and repeat as more rules and conditions are added to the policy they'll be added to the security descriptor with the appropriate ACEs. Note that the ordering of the rules are very important, for example Deny ACEs will always go first. I assume the policy file generation code correctly handles the security descriptor generation, but you can now audit it to make sure.

While we now understand how the rules are enforced, where does the values for the condition, such as APPID://PATH come from? If you read the (poor) documentation about conditional ACEs you'll find these values are Security Attributes. The attributes can be either globally defined or assigned to an access token. Each attribute has a name, then a list of one or more values which can be strings, integers, binary blobs etc. This is what AL is using to store the data in the access check token.

Let's go back a step and see what's going on with AiSetAttributesExe to see how these security attributes are generated.

Setting Token Attributes

The AiSetAttributesExe function takes 4 parameters:
  • A handle to the executable file.
  • Pointer to the current policy.
  • Handle to the primary token of the new process.
  • Handle to the token used for the access check.
The code isn't doesn't look very complex, initially:

NTSTATUS AiSetAttributesExe(
            PVOID Policy, 
            HANDLE FileHandle, 
            HANDLE ProcessToken, 
            HANDLE AccessCheckToken) {
  
    PSECURITY_ATTRIBUTES SecAttr;
    AiGetFileAttributes(Policy, FileHandle, &SecAttr);
    NTSTATUS status = AiSetTokenAttributes(ProcessToken, SecAttr);
    if (NT_SUCCESS(status) && ProcessToken != AccessCheckToken)
        status = AiSetTokenAttributes(AccessCheckToken, SecAttr);
    return status;
}

All the code does it call AiGetFileAttributes, which fills in a SECURITY_ATTRIBUTES structure, and then calls AiSetTokenAttributes to set them on the ProcessToken and the AccessCheckToken (if different). AiSetTokenAttributes is pretty much a simple wrapper around the exported (and undocumented) kernel API SeSetSecurityAttributesToken which takes the generated list of security attributes and adds them to the access token for later use in the access check.

The first thing AiGetFileAttributes does is query the file handle for it's full path, however this is the native path and takes the form \Device\Volume\Path\To\File. A path of this form is pretty much useless if you wanted to generate a single policy to deploy across an enterprise, such as through Group Policy. Therefore the code converts it back to a Win32 style path such as c:\Path\To\File. Even then there's no guarantee that the OS drive is C:, and what about wanting to have executables on USB keys or other removable drives where the letter could change?

To give the widest coverage the driver also maintains a fixed list of "Macros" which look like Environment variable expansions. These are used to replace the OS drive components as well as define placeholders for removable media. We already saw them in use in the dump of the security descriptor with string components like "%WINDIR%". You can find a list of the macros here, but I'll reproduce them here:
  • %WINDIR% - Windows Folder.
  • %SYSTEM32% - Both System32 and SysWOW64 (on x64).
  • %PROGRAMFILES% - Both Program Files and Program Files (x86).
  • %OSDRIVE% - The OS install drive.
  • %REMOVABLE% - Removable drive, such a CD or DVD.
  • %HOT% - Hot-pluggable devices such as USB keys.
Note that SYSTEM32 and PROGRAMFILES will map to either 32 or 64 bit directories when running on a 64 bit system (and presumably also ARM directories on ARM builds of Windows?). If you want to pick a specific directory you'll have to configure the rules to not use the macros.

To hedge its bets AL puts every possible path configuration, native path, Win32 path and all possible macroed paths as string values in the APPID://PATH security attribute.

AiGetFileAttributes continues, gathering the publisher information for the file. On Windows 10 the signature and certificate checking is done in multiple ways, first checking the kernel Code Integrity module (CI), then doing some internal work and finally falling back to calling over RPC to the running APPIDSVC. The information, along with the version number of the binary is put into the APPID://FQBN attribute, which stands for Fully Qualified Binary Name.

The final step is generating the file hash, which is stored in a binary blob attribute. AL supports three hash algorithms with the following attribute names:
  • APPID://SHA256HASH - Authenticode SHA256.
  • APPID://SHA1HASH - Authenticode SHA1
  • APPID://SHA256FLATHASH - SHA256 over entire file.
As the attributes are applied to both tokens we should be able to see them on the primary token of a normal user process. By running the following PowerShell command we can see the added security attributes on the current process token.

PS> $(Get-NtToken).SecurityAttributes | ? Name -Match APPID

Name       : APPID://PATH
ValueType  : String
Flags      : NonInheritable, CaseSensitive
Values     : {
   %SYSTEM32%\WINDOWSPOWERSHELL\V1.0\POWERSHELL.EXE,
   %WINDIR%\SYSTEM32\WINDOWSPOWERSHELL\V1.0\POWERSHELL.EXE,  
    ...}

Name       : APPID://SHA256HASH
ValueType  : OctetString
Flags      : NonInheritable
Values     : {133 66 87 106 ... 85 24 67}

Name       : APPID://FQBN
ValueType  : Fqbn
Flags      : NonInheritable, CaseSensitive
Values     : {Version 10.0.18362.1 - O=MICROSOFT CORPORATION, ... }


Note that the APPID://PATH attribute is always added, however APPID://FQBN and APPID://*HASH are only generated and added if there are rules which rely on them.

The Mystery of the Twin Tokens

We've come to the final stage, we now know how the security attributes are generated and applied to the two access tokens. The question now is why is there two tokens, the process token and one just for access checking?

Everything happens inside AiGetTokens, which is shown in a simplified form below:


NTSTATUS AiGetTokens(HANDLE ProcessId,

PHANDLE ProcessToken,

PHANDLE AccessCheckToken)

{

  AiOpenTokenByProcessId(ProcessId, &TokenHandle);

  NTSTATUS status = STATUS_SUCCESS;
  *Token = TokenHandle;
  if (!AccessCheckToken)
    return STATUS_SUCCESS;

  BOOL IsRestricted;
  status = ZwQueryInformationToken(TokenHandle, TokenIsRestricted, &IsRestricted);
  DWORD ElevationType;
  status = ZwQueryInformationToken(TokenHandle, TokenElevationType,
&ElevationType);

  HANDLE NewToken = NULL;
  if (ElevationType != TokenElevationTypeFull)
      status = ZwQueryInformationToken(TokenHandle, TokenLinkedToken,
&NewToken);

  if (!IsRestricted
    || NT_SUCCESS(status)
    || (status = SeGetLogonSessionToken(TokenHandle, 0,
&NewToken), NT_SUCCESS(status))
    || status == STATUS_NO_TOKEN) {
    if (NewToken)
      *AccessCheckToken = NewToken;
    else
      *AccessCheckToken = TokenHandle;
  }

  return status;
}

Let's summarize what's going on. First, the easy one, the ProcessToken handle is just the process token opened from the process, based on its PID. If the AccessCheckToken is not specified then the function ends here. Otherwise the AccessCheckToken is set to one of three values
  1. If the token is a non-elevated (UAC) token then use the full elevated token.
  2. If the token is 'restricted' and not a UAC token then use the logon session token.
  3. Otherwise use the primary token of the new process.
We can now understand why a non-elevated UAC admin has Administrator rules applied to them. If you're running as the non-elevated user token then case 1 kicks in and sets the AccessCheckToken to the full administrator token. Now any rule checks which specify the Administrators group will pass.

Case 2 is also interesting, a "restricted" token in this case is one which has been passed through the CreateRestrictedToken API and has restricted SIDs attached. This is used by various sandboxes especially Chromium's (and by extension anyone who uses it such as Firefox). Case 2 ensures that if the process token is restricted and therefore might not pass the access check, say the Everyone group is disabled, then the access check is done instead against the logon session's token, which is the master token from which all others are derived in a logon session.

If nothing else matches then case 3 kicks in and just assigns the primary token to the AccessCheckToken. There are edges cases in these rules. For example you can use CreateRestrictedToken to create a new access token with disabled groups, but which doesn't have restricted SIDs. This results in case 2 not being applied and so the access check is done against the limited token which could very easily fail to validate causing the process to be terminated.

There's also a more subtle edge case here if you look back at the code. If you create a restricted token of a UAC admin token then process creation typically fails during the policy check. When the UAC token is a full admin token the second call to ZwQueryInformationToken will not be made which results in NewToken being NULL. However in the final check, IsRestricted is TRUE so the second condition is checked, as status is STATUS_SUCCESS (from the first call to ZwQueryInformationToken) this passes and we enter the if block without ever calling SeGetLogonSessionToken. As NewToken is still NULL AccessCheckToken is set to the primary process token which is the restricted token which will cause the subsequent access check to fail. This is actually a long standing bug in Chromium, it can't be run as UAC admin if AppLocker is enforced.

That's the end of how AL does process enforcement. Hopefully it's been helpful. Next time I'll dig into how DLL enforcement works.

Locking Resources to Specific Processes

Before we go, here's a silly trick which might now be obvious. Ever wanted to restrict access to resources, such as files, to specific processes? With the AL applied security attributes now you can. All you need to do is apply the same conditional ACE syntax to your file and the kernel will do the enforcement for you. For example create the text file C:\TEMP\ABC.TXT, now to only allow notepad to open it do the following in PowerShell:

Set-NtSecurityDescriptor \??\C:\TEMP\ABC.TXT `
     -SecurityDescriptor 'D:(XA;;GA;;;WD;(APPID://PATH Contains "%SYSTEM32%\NOTEPAD.EXE"))' `
     -SecurityInformation Dacl

Make sure that the path is in all upper case. You should now find that while PowerShell (or any other application) can't open the text file you can open and modify it just fine in notepad. Of course this won't work across network boundaries and is pretty easy to get around, but that's not my problem ;-)