Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=1332
Windows: CiSetFileCache TOCTOU Security Feature Bypass
Platform: Windows 10 10586/14393/10S not tested 8.1 Update 2 or Windows 7
Class: Security Feature Bypass
Summary:
It’s possible to add a cached signing level to an unsigned file by exploiting a TOCTOU in CI leading to to circumventing Device Guard policies and possibly PPL signing levels.
Description:
Windows Code Integrity has the concept of caching signing level decisions made on individual files. This is done by storing an extended attribute with the name $KERNEL.PURGE.ESBCACHE and filling it with related binary information. As the EA name is a kernel EA it means it can’t be set by user mode code, only kernel mode code calling FsRtlSetKernelEaFile. Also crucially it’s a purgeable EA which means it will be deleted automatically by the USN journal code if any attempt is made to write to the file after being set.
As far as I can tell the binary data doesn’t need to correspond to anything inside the file itself, so if we replace the contents of the file with a valid cached signing level the kernel is entirely relying on the automatic purging of the kernel EA to prevent spoofing. To test that theory I copied the EA entry from a valid signed file onto an unsigned file with a non-kernel EA name then used a disk editor to modify the name offline. This worked when I rebooted the machine, so I was confident it could work if you could write the kernel EA entry. Of course if this was the only way to exploit it I wouldn’t be sending this report.
As user mode code can’t directly set the kernel EA the facility to write the cache entry is exposed through ZwSetCachedSigningLevel(2). This takes a number of arguments, including flags, a list of associated file handles and the target handle to write the EA to. There seems to be 3 modes which are specified through the flags:
Mode 1 - This is used by mscorsvw.exe and seems to be used for blessing NGEN binaries. Calling this requires the caller to be a PPL so I didn’t investigate this too much. I’m sure there’s probably race conditions in NGEN which could be exploited, or ways to run in a PPL if you’re admin. The advantage here is you don’t need to apply the cache to a signed file. This is what piqued my interesting in the first place.
Mode 2 - Didn’t dig into this one TBH
Mode 5 - This sets a cache on a signed file, the list of files must only have 1 entry and the handle must match the target file handle. This is the one we’ll be exploiting as it doesn’t require any privileges to call.
Looking through the code inside the kernel the handles passed to ZwSetCachedSigningLevel are also passed as handles into CiSetFileCache which is slightly odd on the face of it. My first thought was you could race the handle lookup when ObReferenceObjectByHandle is called for the target handle and when the code is enumerating the list of handles. The time window would be short but it’s usually pretty easy to force the kernel to reuse a handle number. However it turns out in Mode 5 as the handle is verified to be equal the code just uses the looked up FILE_OBJECT from the target handle instead which removes this issue (I believe).
So instead I looked at racing the writing of the cache EA with the signature verification. If you could rewrite the file between the kernel verifying the signature of the file and the writing of the kernel EA you could ensure your USN journal entries from the writes are flushed through before hand and so doesn’t cause the EA to be purged. The kernel code calls FsRtlKernelFsControlFile with FSCTL_WRITE_USN_CLOSE_RECORD to force this flush just before writing the EA so that should work.
The question is can you write to the file while you’re doing this? There’s no locking taking place on the file from what I could tell. There is a check for the target file being opened with FILE_SHARE_WRITE (the check for FileObject->SharedWrite) but that’s not the same as the file handle already being writable. So it looks like it’s possible to write to the file.
The final question is whether there’s a time period between signature verification and applying the EA that we can exploit? Turns out CI maps the file as a read only section and calls HashKComputeImageHash to generate the hash once. The code then proceeds to lookup the hash inside a catalog (presumably unless the file has an embedded signature). Therefore there's a clear window of time between the validation and the setting of the kernel EA to write.
The final piece of the puzzle is how to win that race reliably. The key is the validation against the catalog files. We can use an exclusive oplock to block the kernel opening the catalog file temporarily, which crucially happens after the target file has already been hashed. By choosing a catalog we know the kernel will check we can get a timing signal, modify the target file to be an unsigned, untrusted file then release the oplock and let the kernel complete the verification and writing of the cache.
Almost all files on a locked down system such as Win10S are Microsoft Platform signed and so end up in catalogs such as Microsoft-Windows-Client-Features-Package. This seems like a hot-path file which might always be opened by the kernel and so couldn’t be exploited for an oplock. However another useful feature now comes into play, the fact that there’s also an EA which can specify a hint name for the catalog the file is signed in. This is called $CI.CATALOGHINT and so isn’t a kernel EA which means we can set it. It contains a UTF8 encoded file name (without path information). Importantly while CI will check this catalog first, if it can’t find the hash in that catalog it continues searching everything else, so we can pick a non-hot-path catalog (such as Adobe’s Flash catalogs) which we can oplock on, do the write then release and the verification will find the correct real catalog instead. I don’t think you need to do this, but it makes it considerably more convenient.
Note that to exploit this you’d likely need executable code already running, but we already know there’s multiple DG bypasses and things like Office on Win10S can run macros. Or this could be used from shellcode as I can’t see any obvious limitation on exploiting this from a sandbox as long as you can write a file to an NTFS drive with the USN Change Journal enabled. Running this once would give you an executable or a DLL which bypasses the CI policies, so it could be used as a stage in an attack chain to get arbitrary code executing on a DG system.
In theory it think this would also allow you to specify the signing level for an untrusted file which would allow the DLL to be loaded inside a PPL service so you could use this on a vanilla system to try and attack the kernel through PPL’s such as CSRSS as an administrator. I don’t know how long the cache is valid for, but it’s at least a day or two and might only get revoked if you update your system or replace the file.
Proof of Concept:
I’ve provided a PoC as a C# project. It will allow you to “cache sign” an arbitrary executable. If you want to test this on a locked down system such as Win10S you’ll need to sign the PoC (and the NtApitDotNet.dll assembly) so it’ll run. Or use it via one of my .NET based DG bypasses, in that case you can call the POC.Exploit.Run method directly. It copies notepad to a file, attempts to verify it but uses an oplock to rewrite the contents of the file with the untrusted file before it can set the kernel EA.
1) Compile the C# project. It will need to grab the NtApiDotNet v1.0.8 package from NuGet to work.
2) Execute the PoC passing the path to an unsigned file and to the output “cache signed” file, e.g. poc unsigned.exe output.exe
3) You should see it print the signing level, if successful.
4) You should not be able to execute the unsigned file, bypassing the security policy enforcement.
NOTE: If it prints an exception then the exploit failed. The opened catalog files seemed to be cached for some unknown period of time after use so if the catalog file I’m using for a timing signal is already open then the oplock is never broken. Wait a period of time and try again. Also the output file must be on to a NTFS volume with the USN Change Journal enabled as that’s relied upon by the signature level cache code. Best to do it to the boot drive as that ensures everything should work correctly.
Expected Result:
Access denied or at least an error setting the cached signing level.
Observed Result:
The signing level cache is applied to the file with no further verification. You can now execute the file as if it was signed with valid Microsoft signature.
Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/43162.zip