Exploit-DB #43162
Description: Microsoft Windows 10 - CiSetFileCache TOCTOU Security Feature Bypass
Type: local
Platform: windows
Date: 2017-11-20
Author: Google Security Research

Exploit:
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 d