In October 2022, I've come across a tweet from 5th July, from @wdormann, who reported a discovery of a new method for bypassing MOTW, using a flaw in how Windows handles file extraction from ZIP files.
This sounded to me like a nice challenge to freshen up my rusty RE skills. The bug was also a 0-day, at the time. It has already been reported to Microsoft, without a fix deployed for more than 90 days.
What I always find the most interesting about vulnerability research write-ups is the process on how one found the bug, what tools were used and what approach was taken. I wanted this post to be like this.
Now that the vulnerability has been fixed, I can freely publish the details.
What I found out, based on public information about the bug and demo videos, was that Windows, somehow, does not append MOTW to files extracted from ZIP files.
Mark-of-the-web is really another file attached as an Alternate Data Stream (ADS), named
Zone.Identifier, and it is only available on NTFS filesystems. The ADS file always contains the same content:
For example, when you download a ZIP file
file.zip, from the Internet, the browser will automatically add
file.zip:Zone.Identifier ADS to it, with the above contents, to indicate that the file has been downloaded from the Internet and that Windows needs to warn the user of any risks involving this file's execution.
This is what happens when you try to execute an executable like a JScript file, through double-clicking, stored in a ZIP file, with MOTW attached.
Clearly the user would think twice before opening it when such popup shows up. This is not the case, though, for specially crafted ZIP files bypassing that feature.
Let's find the cause of the bug.
Identifying the culprit
What I knew already from my observation is that the bug was triggered when
explorer.exe process handles the extraction of ZIP files. I figured the process must be using some internal Windows library for handling ZIP files unpacking and I was not mistaken.
zipfldr.dll module loaded within Explorer process and it looked like a good starting point. I booted up IDA with conveniently provided symbols from Microsoft, to look around.
ExtractFromZipToFile function immediately caught my attention. I created a sample ZIP file with a packaged JScript file, for testing, which had a single instruction:
WScript.Echo("YOU GOT HACKED!!1");
I then added a MOTW ADS file with Notepad and filled it with MOTW contents, mentioned above:
I loaded up
x64dbg debugger, attached it to
explorer.exe and set up a breakpoint on
ExtractFromZipToFile. When I double-clicked the JS file, the breakpoint triggered and I could confirm I'm on the right path.
One of the function calls I noticed nearby, revealed an interesting pattern in IDA. Right after the file is extracted and specific conditions are meet,
CheckUnZippedFile function is called, followed by a call to
_OpenExplorerTempFile, which opens the extracted file.
Having a hunch that
CheckUnZippedFile is the function responsible for adding MOTW to extracted file, I nopped its call and found that I stopped getting the MOTW warning popup, when I tried executing a JScript file from within the ZIP.
It was clear to me that if I managed to manipulate the execution flow in such a way that the branch, executing this function is skipped, I will be able to achieve the desired effect of bypassing the creation of MOTW on extracted files. I looked into the function to investigate further.
I noticed that
CheckUnZippedFile tries to combine the TEMP folder path with the zipped file filename, extracted from the ZIP file, and when this function fails, the function quits, skipping the creation of MOTW file.
Considering that I controlled the filename of the extracted ZIP file, I could possibly manipulate its content to trigger
PathCombineW to fail and as a result achieve my goal.
PathCombineW turned out to be a wrapper around
PathCchCombineExW function with output buffer size limit set to fixed value of
260 bytes. I thought that if I managed to create a really long filename or use some special characters, which would be ignored by the function handling the file extraction, but would trigger the length check in
CheckUnZippedFile to fail, it could work.
I opened 010 Editor, which I highly recommend for any kind of hex editing work, and opened my sample ZIP file with a built-in ZIP template.
I spent few hours testing with different filename lengths, with different special characters, just to see if the extraction function would behave in erratic way. Unfortunately I found out that there was another path length check, called prior to the one I've been investigating. It triggered much earlier and prevented me from exploiting this one specific check. I had to start over and consider this path a dead end.
I looked if there are any controllable branching conditions, that would result in not triggering the call to
CheckUnZippedFile at all, but none of them seemed to be dependent on any of the internal ZIP file parameters. I considered looking deeper into
CheckUnZippedFile function and found out that when
PathCombineW call succeeds, it creates a
CAttachmentServices COM objects, which has its three methods called:
CAttachmentServices::SetReferrer(unsigned short const * __ptr64) CAttachmentServices::SetSource(unsigned short const * __ptr64) CAttachmentServices::SaveWithUI(struct HWND__ * __ptr64)
I realized I am about to go deep down a rabbit hole and I may spend there much longer than a hobby project like that should require. I had to get a public exploit sample to speed things up.
Detonating the live sample
I managed to copy over all relevant ZIP file parameters from the obtained exploit sample into my test sample and I confirmed that MOTW was gone, when I extracted the sample JScript file.
I decided to dig deeper into
SaveWithUI COM method to find the exact place where creation of
Zone.Identifier ADS fails. Navigating through
shdocvw.dll, I ended up in
urlmon.dll with a failing call to
This is the Windows API function for handling the creation of INI configuration files. Considering that
Zone.Identifier ADS file is an INI file containing section
ZoneTransfer, it was definitely relevant. I dug deeper.
The search led me to the final call of
NtCreateFile, trying to create the
Zone.Identifier ADS file, which failed with
ACCESS_DENIED error, when using the exploit sample and succeeded when using the original, untampered test sample.
It looked like the majority of parameters were constant, as you can see on the screenshot above. The only place where I'd expect anything dynamic was in the structure of
ObjectAttributes parameter. After closer inspection and half an hour of closely comparing the contents of the parameter structures from two calls, I concluded that both failing and succeeding calls use exactly the same parameters.
This led me to realize that something had to be happening prior to the creation of the ADS file, which I did not account for. There was no better way to figure that out than to use Process Monitor, which honestly I should've used long before I even opened IDA 😛.
I set up my filters to only list file operations related to files extracted to TEMP directory, starting with
The test sample clearly succeeded in creating the
Zone.Identifier ADS file:
While the exploit sample failed:
Through comparison of these two listings, I could not clearly see any drastic differences. I exported the results as text files and compared them in a text editor. That's when I could finally spot it.
Prior to creating
Zone.Identifier ADS file, the call to
SetBasicInformationFile was made with
FileAttributes set to
I looked up what was that
R attribute, which apparently is not set for the file when extracting from the original test sample and then...
R file attribute stands for
read-only. The file stored in a ZIP file has the read-only attribute set, which is set also on the file extracted from the ZIP. Obviously when Windows tries to attach the
Zone.Identifier ADS, to it, it fails, because the file has a read-only attribute and any write operation on it will fail with
It doesn't even seem to be a bug, since everything is working as expected 😛. The file attributes in a ZIP file are set in
ExternalAttributes parameter of the
ZIPDIRENTRY structure and its value corresponds to the ones, which carried over from MS-DOS times, as stated in ZIP file format documentation I found online.
4.4.15 external file attributes: (4 bytes) The mapping of the external attributes is host-system dependent (see 'version made by'). For MS-DOS, the low order byte is the MS-DOS directory attribute byte. If input came from standard input, this field is set to zero. 4.4.2 version made by (2 bytes) 188.8.131.52 The upper byte indicates the compatibility of the file attribute information. If the external file attributes are compatible with MS-DOS and can be read by PKZIP for DOS version 2.04g then this value will be zero. If these attributes are not compatible, then this value will identify the host system on which the attributes are compatible. Software can use this information to determine the line record format for text files etc. 184.108.40.206 The current mappings are: 0 - MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems) 1 - Amiga 2 - OpenVMS 3 - UNIX 4 - VM/CMS 5 - Atari ST 6 - OS/2 H.P.F.S. 7 - Macintosh 8 - Z-System 9 - CP/M 10 - Windows NTFS 11 - MVS (OS/390 - Z/OS) 12 - VSE 13 - Acorn Risc 14 - VFAT 15 - alternate MVS 16 - BeOS 17 - Tandem 18 - OS/400 19 - OS X (Darwin) 20 thru 255 - unused 220.127.116.11 The lower byte indicates the ZIP specification version (the version of this document) supported by the software used to encode the file. The value/10 indicates the major version number, and the value mod 10 is the minor version number.
Changing the value of external attributes to anything with the lowest bit set e.g.
0x01, would effectively make the file read-only with Windows being unable to create MOTW for it, after extraction.
I honestly expected the bug to be much more complicated and I definitely shot myself in the foot, getting too excited to start up IDA, instead of running Process Monitor first. I started with IDA first as I didn't have an exploit sample in the beginning and I was hoping to find the bug, through code analysis. Bottom line, I managed to learn something new about Windows internals and how extraction of ZIP files is handled.
The patch was clean and reliable as seen in the screenshot from a debugger:
I've been also able to have a nice chat with Will Dormann, who initially discovered this bug, and his story on how he found it is hilarious:
I merely wanted to demonstrate how an exploit in a ZIP was safer (by way of prompting the user) than that *same* exploit in an ISO. So how did I make the ZIP? I:
1) Dragged the files out of the mounted ISO
2) Zipped them. That's it. The ZIP contents behaved the same as the ISO.
Every mounted ISO image is listing all files in read-only mode. Drag & dropping files from read-only partition, to a different one, preserves the read-only attribute set for created files. This is how Will managed to unknowingly trigger the bug.
Will also made me realize that 7zip extractor, even though having announced they began to add MOTW to every file extracted from MOTW marked archive, does not add MOTW by default and this feature has to be enabled manually.
I mentioned it as it may explain why MOTW is not always considered a valid security boundary. Vulnerabilities related to it may be given low priority and be even ignored by Microsoft for 90 days.
I haven't yet analyzed how the patch made by Microsoft works, but do let me know if you did and I will gladly update this post with additional information.
Hope you enjoyed the write-up!