Patchdiffing another CVE and some thoughts
Introduction
Had some time this weekend and felt like analysing a CVE, so I went to CVE Details to look for some new Windows 11 CVEs.
Right on top are a pair of CVEs affecting the Windows Overlay Filter
(wof.sys).
CVE-2023-21766 leads to information disclosure through a race condition, while CVE-2023-21767 achieves EOP.
I chose to focus on the information disclosure bug since full EOP exploits tend to require much effort. Also, with the soon removal of NtQuerySystemInformation()
like APIs from Medium IL, information disclosure bugs are getting more valuable.
Spoiler: I did not manage to complete the poc, so this is more of a diary.
Update: I did manage to poc it after consulting the researcher who discovered this bug, @k0shl .
Check the Update section at the end.
Patchdiff
With a lack of public information regarding the bug, we will have to diff the patch to understand the bug.
Microsoft’s official patch is released on Jan 10, 2023
as part of security update KB5022289
.
You can download the update and patchdiff it that way, but my go-to method now is to use Winbindex.
Winbindex is a collection of Windows system binaries across updates.
We can easily find wof.sys
binaries before and after the patch.
To learn patchdiffing using BinDiff and IDA I suggest watching Modern Binary/Patch Diffing! by Off By One Security
.
Comparing between the functions, we see only 2 functions with changes. WimCbEnumOverlay
and WimFSFInitializeOffsetTable
.
WimCbEnumOverlay
The main change is adding a call to hold a FastMutex before proceeding.
WimFSFInitializeOffsetTable
The main change is checking if a field is indeed smaller than another before proceeding.
At this point, I have a rough idea that the information disclosure probably exists due to the lack of locking in WimCbEnumOverlay
, and the EOP happens due to out of bounds operations in WimFSFInitializeOffsetTable
.
Woof Woof?
Wait hold on… wtf does wof.sys
even do, what’s FSF
and Wim
? So many acronyms!
This is when we do reconnaissance, similar to how pentesters map assets before actually attacking.
There’s no better place to start than MSDN Documentation.
A quick search reveals that Wim stands for Windows Image
, which is a backup format to store a set of files, up to an entire partition.
We can inform the OS that we have backed up a partition using Wim by adding a Wim Overlay
to a volume. This along with overlays from other providers are then managed by the Windows Overlay
Filter, wof.sys.
Going a few pages down we find documentation for the WIM_PROVIDER_ADD_OVERLAY_INPUT structure, which very kindly points us to a few FSCTL_*
APIs.
They sound awfully like the functions residing in wof.sys:
1 | __int64 __fastcall WimFSFInitializeProvider(_WORD *a1, __int64 a2) |
Again I’m making a guess that invoking one of these FSCTL_*
operations will eventually lead to the Wim
functions processing our data, in the event that we use Wim
as a backing provider.
Based on the docs I made some wrapper functions to interact with Wim
.
Here’s an example to interact with WimEnumCbOverlay
:
1 | static NTSTATUS EnumOverlay(HANDLE hVolume, WIM_PROVIDER_OVERLAY_ENTRY *outBuf, ULONG outSize) |
Again, all of these are public information available in MSDN Docs. It’s a pure goldmine.
This call will inevitably fail, because we have yet to add any overlays.
Adding Overlay
First we create 2 partitions, E and F.
Then we make some a dummy directory on partition E and invoke the native Dism
tool to backup the entire partition into a Wim.
1 | Dism /Capture-Image /ImageFile:C:\Users\User\Desktop\mywim.wim /CaptureDir:E:\ /Name:"anything" |
Now we wrap the FSCTL_ADD_OVERLAY
code like shown above:
1 | static NTSTATUS AddOverlay(_In_ HANDLE hVolume, _In_ LPCWSTR wimPath, _Out_ PLARGE_INTEGER dataSrc) |
The path must be a full path, like:
1 | \??\C:\Users\User\Desktop\mywim.wim |
If we run the code now we’ll get a 0xC00000BB
error:
1 | C:\Users\User\Desktop>.\exp.exe |
which means The request is not supported.
I’m not sure if this is the correct way to use Wim(it surely isn’t), but I followed the code flow in windbg and ida to find the offending function:
1 | NTSTATUS __fastcall WimFSFValidateWim(struct _FLT_INSTANCE *flt_instance, _FILE_OBJECT *fileobj, char *buf, _QWORD *a4) |
To bypass the check just null out byte 16 in the wim file.
After fix:
1 | C:\Users\User\Desktop>.\exp.exe |
Using native fsutil
we can confirm that the overlay is added:
1 | C:\Users\User\Desktop>fsutil wim enumWims F: |
And we can dump it out using the EnumOverlay
function written above:
1 | C:\Users\User\Desktop>.\exp.exe |
Sweet!
Reversing WimCb*
Up to this point we didn’t really need to touch ida, other than to fix that weird verification.
Unfortunately in order to truly understand the bug, reversing part of wof.sys is inevitable.
Honestly with pdb and no protections I’m not complaining.
Initially you get something like this:
1 | __int64 __fastcall WimCbEnumOverlay(__int64 a1, __int64 a2, __int64 a3, __int64 a4, unsigned int a5, unsigned int *a6) |
By experience anything with a +16 or +8 in a windows driver for loop is PROBABLY a LIST_ENTRY
.
We can also see some global offset being subtracted from a1
to point to a mutex.a1
is therefore probably a global structure storing the head of linked lists and mutexes.
v10
is being indexed and written to? Probably a struct to return back for usermode consumption.
After cleaning up:
1 | __int64 __fastcall WimCbEnumOverlay(GlobalStruct *global, __int64 a2, __int64 a3, WIM_PROVIDER_OVERLAY_ENTRY *nextBuf, unsigned int outbuf_sz, unsigned int *outbytes_written) |
WimCbEnumOverlay
iterates through a linked list looking for non-deleted overlay entries, then copying to usermode.
Knowing it’s a race condition, we can start thinking of possible exploitation scenarios.
What if we remove the entire overlay while the enum is copying? Can we get a UAF style control over unicode pointer?
Can we update the path to a different length for OOB read?
For the first hypothesis, turns out WimCbRemoveOverlay
does not actually release any structures.
As far as we need to be concerned, it just sets a flag to indicate that the entry is not active and should not be parsed by WimCbEnumOverlay
.
As for the second hypothesis, we’ll have to look at WimCbUpdateOverlay
:
1 | __int64 __fastcall WimCbUpdateOverlay(int a1, GlobalStruct *a2, WIM_PROVIDER_UPDATE_OVERLAY_INPUT *overlayInput, unsigned int fileNameLengthAfterBuffer) |
There are checks in place preventing us from updating rogue offsets and performing OOB reads, BUT the old UNICODE_STRING.Buffer
does get freed. We also see an example of proper locking of the FAST_MUTEX
embedded inside every entry(this is the patch to WimCbEnumOverlay
).
Thoughts
Now the path is pretty clear. We gonna call WimCbEnumOverlay
in a few threads, while calling WimCbUpdateOverlay
in another.
At the same time we can spray the heap with objects that enter the Paged Pool of the same size as the string buffer, maybe a WNF object as shown in my CVE-2021-31956
analysis.
If we can get the string buffer freed while another thread is performing a copy, we might be able to allocate new structures on it.
If those structures contain kernel pointers, we can leak them to userspace, completing the attack.
One way to prolong the race window could be to use super long directory names so the copy takes a longer time.
However, this depends on the target to enable LongPathsEnabled
from the registry, which is not enabled by default.
We’ll also require the ability to obtain a handle to a volume(F drive in the example above).
In most scenarios this requires admin privileges already… which is pretty sad.
I had an idea which is to map a network drive(Medium IL) and use a handle to the network drive, but NtFsControlFile
does not like that and denies our handle.
TLDR:
I’m struggling to trigger the exploit from medium/low IL.
Would love a helping hand/a person to collab with!
Update
After reaching out to k0shl, he pointed out that it’s possible to open a volume handle from Medium IL only in one case; If that volume is mapped from a second disk added via VMWare settings.
I’ve tested it on HyperV and it’s not the case. Seems like a VMWare bug maybe?
Anyways, after porting it to VMWare and enabling special pool, I finally got the poc working on the newest build of Windows 11(with wof.sys replaced of course).
see the UAF read above