Monday, 3 June 2024

Working your way Around an ACL

There's been plenty of recent discussion about Windows 11's Recall feature and how much of it is a garbage fire. Especially a discussion around how secure the database storing all those juicy details of your banking details, sexual peccadillos etc is from prying malware. Spoiler, it's only protected through being ACL'ed to SYSTEM and so any privilege escalation (or non-security boundary *cough*) is sufficient to leak the information. 

However, I've not spent the time to setup Recall on any machine I own and the files are probably correctly ACL'ed. Therefore, this blog isn't here to talk about that, instead I was following a thread about Recall and the security of the database by Albacore on Mastodon and one toot in particular caught my interest.

"@DrewNaylor File Explorer always runs unelevated, Administrators also have access to C:\Program Files\WindowsApps yet you simply can't open it in File Explorer without breaking ACLs no matter how you try."

I thought this wasn't true based on what I know about the "C:\Program Files\WindowsApps" folder, so I decided to see if I can get it show in an unelevated explorer. It turns out to be more complex than it should be for various reasons, so let's dig in.

What is the WindowsApps Folder?

The WindowsApps folder is used to store system installations of packaged applications. Think UWP, Desktop Bridge, Calculator etc. And it's true, if you try and view the folder from a non-elevated application it's gives you access denied:

PS> ls 'C:\Program Files\WindowsApps\'
ls : Access to the path 'C:\Program Files\WindowsApps' is denied.

Why would Microsoft do this as it doesn't seem like a security sensitive location? If I had to guess it's to stop a user browsing to the packaged applications and double clicking on the executable files and being confused when that doesn't work. Packaged applications are mostly normal PE files, however, they can't be executed directly. Instead a complex sequence involving COM and/or the container APIs need to be invoked to setup the runtime environment. This guess seems likely because if you know the name of the packaged application there's nothing stopping you listing it's contents, it's only the top level WindowsApps folder which is blocked.

PS> ls 'C:\Program Files\WindowsApps\Microsoft.WindowsCalculator_11.2403.6.0_x64__8wekyb3d8bbwe'

    Directory: C:\Program Files\WindowsApps\Microsoft.WindowsCalculator_11.2403.6.0_x64__8wekyb3d8bbwe

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        2024-05-15     19:42                AppxMetadata
d-----        2024-05-15     19:42                Assets
-a----        2024-05-15     19:42          54073 AppxBlockMap.xml
-a----        2024-05-15     19:42          11431 AppxManifest.xml
-a----        2024-05-15     19:42          12255 AppxSignature.p7x
-a----        2024-05-15     19:42        4179968 CalculatorApp.dll
-a----        2024-05-15     19:42          19456 CalculatorApp.exe
<SNIP>

It's seems likely that the reason you can't access the WindowsApps folder is due to ACLs. Therefore, from an admin PowerShell prompt we can inspect what the ACLs are:

PS> Format-Win32SecurityDescriptor 'C:\Program Files\WindowsApps\' -MapGeneric
Path: C:\Program Files\WindowsApps\
Type: File
Control: DaclPresent, DaclAutoInherited, DaclProtected

<SNIP>

 - Type  : AllowedCallback
 - Name  : BUILTIN\Users
 - SID   : S-1-5-32-545
 - Mask  : 0x001200A9
 - Access: GenericExecute|GenericRead
 - Flags : None
 - Condition: Exists WIN://SYSAPPID

I've removed all of the output baring the final ACE as this is the important one. The rest of the ACL doesn't have any other ACE which would refer to a normal user, only administrators. It shows that the BUILTIN\Users group should get read and execute access, however, only if the WIN://SYSAPPID security attribute exists in the user's access token. I'm not going to explain how this works, see my series on AppLocker for how conditional ACEs are used, or buy my book. This is why you can't just list the folder in explorer, the process token doesn't have the WIN://SYSAPPID attribute and so access is denied.

What's the WIN://SYSAPPID attribute for? It's added to access tokens when a packaged application is executed and contains information about the package identity. This can then be referenced by an application, or the kernel in this case to check for information about the package the process in a member of. As setting this security attribute on a token requires SeTcbPrivilege it's hard to spoof. 

In this case we don't need to spoof a specific value of the attribute, it just has to exist. We can't create it, but perhaps there's already an access token we can borrow to give us access instead.

Finding a Suitable Access Token

There's likely to be a process running as the user with the WIN://SYSAPPID attribute set. As the primary token of that process should be the same user then there's nothing stopping us impersonating it to get access to the WindowsApps folder. First let's find a process with a suitable token:

PS> $ps = Get-NtProcess -FilterScript {
  Use-NtObject($token = Get-NtToken -Process $_ -Access Query, Duplicate) {
     "WIN://SYSAPPID" -in $token.SecurityAttributes.Name -and -not $token.AppContainer
  }
}
PS> $ps.Count
7

This script enumerates all accessible processes, then filters them down to only processes with a token having the WIN://SYSAPPID attribute. We also filter out any App Container tokens as they probably won't be able to access the folder either, plus it's just more hassle to deal with them. We also ensure that we can open the token for Duplicate access, as we'll need to call DuplicateToken to get an impersonation token from the primary token. We can see in this example that there are 7 processes matching our criteria.

Finally we can just test access by getting an impersonation token, impersonating it then enumerating the WindowsApps folder.

PS> $token = Get-NtToken -Process $ps[0] -Duplicate
PS> Invoke-NtToken $token { 
   ls 'C:\Program Files\WindowsApps\' 
}

    Directory: C:\Program Files\WindowsApps

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        2024-05-31     07:50                Deleted
d-----        2020-09-25     07:18                DeletedAllUserPackages
d-----        2019-12-07     01:53                Microsoft.Advertising...
<SNIP>

And it works! However, this isn't actually what the toot originally said. I'm supposed to be able to get the WindowsApps folder to show in a non-elevated explorer window. Let's try and do that then.

Finishing the Job

As the process token is our own user and in our own logon session then we meet the criteria to  duplicate a new primary token from that process and use that with CreateProcessAsUser. Unfortunately there's a big problem, if you run a new copy of explorer it realizes there's an instance already running and calls into the existing instance and shows the new window. Therefore there's never a copy of explorer that would run with a UI which has the token you specified. There is a "/SEPARATE" command line argument you can pass, which does create a new UI instance. Unfortunately it's not the process you started that sticks around, instead a new instance of explorer is spawned via COM and that's the one which hosts the UI.

Instead, the "simplest" approach is to just terminate all instances of explorer and spawn a new one. Bit harsh, but fair IMO. You should terminate with a non-zero exit code, otherwise the explorer instance will be automatically restarted. There is a second problem however. If you specify a primary token with the WIN://SYSAPPID attribute you'll find it's no longer there once the process starts. This is due to the kernel stripping this attribute when building a new token for the process. There are various ways around this but the simplest is to start the process suspended, then use NtSetInformationProcess to swap the token to the one with the attribute. Setting the token after creation does not strip the attributes. Putting it all together:

PS> $ex = Get-NtProcess -Name 'explorer.exe'
PS> $ex.Terminate(1)
PS> $token = Get-NtToken -Process $ps[0] -Duplicate -TokenType Primary
PS> $p = New-Win32Process "explorer.exe" -CreationFlags Suspended
PS> Set-NtToken -Process $p -Token $token
PS> Resume-NtProcess -Process $p

Now you can navigate to the WindowsApps folder and see the results.

A screenshot of an explorer window showing the WindowsApps folder being listed.

Hopefully this might give you a bit of insight into the thought process behind trying to bypass ACLs.

UPDATE 2024/06/05 - Turns out I was wrong about Recall being secure. They use the same technique I describe in this blog post except they need a specific WIN://SYSAPPID, for example "MicrosoftWindows.Client.AIX_cw5n1h2txyewy". You can get a token for this attribute by opening the instance of AIXHost.exe, getting its token and using that to access the database files. Or, as the files are owned by the user you can just rewrite the DACLs for the files and gain access that way, no admin required ;-)