The Acropalypse vulnerability in Windows Snip and Sketch, lessons for developer-centered security (original) (raw)
Acropalypse is a vulnerability first identified in the Google Pixel phone screenshot tool, where after cropping an image, the original would be recoverable. Since the part of the image cropped out might contain sensitive information, this was a serious security issue. The problem occurred because the Android API changed behaviour from truncating files by default to leaving existing content in place. Consequently, the beginning of the resulting image file contains the cropped content, but the end of the original file is still present. Image viewers ignore this data and open the file as usual, but with some clever analysis of the compression algorithm used, the original image can (partially) be recovered.
Shortly after the vulnerability was announced, someone noticed that the Windows default screenshot tool, Snip and Sketch, appeared to have the same problem, despite being an entirely unrelated application on a different operating system. I also found a similar problem back in 2004 relating to JPEG thumbnail images. When the same vulnerability keeps re-occurring, it suggests a systemic problem in how we build software, so I set out to understand more about the reasons for the vulnerability existing in Windows Snip and Sketch.
A flawed API
The first problem I found is that the modern Windows API for saving files had a very similar problem to that in Android. Specifically, existing files would not be truncated by default. Arguably the vulnerability was worse because, unlike Android, there is no option to truncate files. The Windows documentation is, at best, unclear on the need to truncate files and what code is needed to achieve the desired result.
This wasn’t always the case. The old Win32 API for saving a file was (roughly) to show a file picker, get the filename the user selected, and then open the file. To open a file, the programmer must specify whether to overwrite the file or not, and example code usually does overwrite the file. However, the new “more secure” Universal Windows Platform (UWP) sandboxes the file picker in a separate process, allowing neat features like capability-based access control. It creates the file if needed and returns a handle which, if the selected file exists, will not overwrite the existing content.
However, from the documentation, a programmer would understandably assume, however, that the file would be empty.
“The file name, extension, and location of this storageFile match those specified by the user, but the file has no content.”
Unless the programmer explicitly truncates the file, the existing file’s content will be retained. If the data written is smaller than the size of the existing file, the old content will remain, resulting in puzzled StackOverflow posts. The documentation for FileSavePicker doesn’t mention the problem, although the example code avoids the vulnerability by using the simple FileIO API that implicitly truncates files before writing.
However, more sophisticated programs would use DataWriter and examples for these don’t truncate files, nor does the documentation point out this difference between the two APIs. It is no surprise that the default behaviour of not truncating existing files is commonplace, despite not being what most people want.
Documentation could be updated to clarify the risks, and this is always welcome, particularly when accompanied by secure example code (which everyone knows will be copied and pasted directly into applications). However, neither make up for the flawed API in Windows UWP or Android. Writing secure code with those APIs is possible, but the default behaviour is neither secure nor what most people will want. A fundamental principle of developer-centred security is to design APIs where the default behaviour is secure, and it should not be possible to accidentally create insecure programs. A safer API would be for the FileSavePicker to truncate existing files by default. Alternatively, OpenAsync could have the option to open the stream for writing. Currently, it is only Read and ReadWrite, unlike the far richer Win32 CreateFile API.
Should we revisit Postel’s Law?
But why did this flaw persist for so long? Android 10 came out in 2019, and Windows Snip and Sketch appears to have been vulnerable since its release in 2018. Surely because the files these applications produced were corrupt, someone would have complained? Actually, standard practice is to follow Postel’s Law: “be conservative in what you send, be liberal in what you accept”. Image viewer applications can find the end of the valid cropped image and treat the residues of the original file as junk that can safely be ignored. For this reason, nobody identified the problem for a long time, and when someone eventually did, it was initially not recognised as a severe problem.
Perhaps it is time to move on from Postel’s Law. It was important for the growth of the Internet but is now becoming a liability. Rejecting invalid input can help identify problems earlier when they have had less opportunity to cause harm. I’m not the first to point this out, and even Jon Postel argued his principle had been misinterpreted.
What next?
Acropalypse is fixed, both on Android (CVE-2023-21036) and for Windows Snip and Sketch (CVE-2023-28303), but it also has lessons for the future. It has served as a case study for the importance of good documentation and, more importantly, well-designed APIs and secure example code. It also shows that multiple programs to solve the same problem frequently have the same vulnerabilities, so just comparing the results from independent implementations gains you less benefit than you might initially expect. The vulnerability also raises some questions, like how we should teach secure software development.
Also, while I didn’t thoroughly investigate the UWP API, what I initially saw did give me some cause for concern. For example, OpenAsync has a very anaemic API. Win32 CreateFile allows checking for an existing file before opening and setting security parameters. CreateFile does so much to ensure everything happens atomically to avoid race conditions. UWP requires these same steps to be separate. Could some race conditions be hiding in there?
Finally, it seems reasonable to expect that other applications are vulnerable to Acropalypse, given the flawed API. Scanning for such problems isn’t trivial, but I think it is possible to spot the behaviour in Process Monitor. This is what overwriting a file looks like in Snip and Sketch. The File Picker does some checks, the Runtime Broker opens it, and Snip and Sketch writes to the file. Importantly, the NtCreateFile disposition is FILE_OPEN, so does not overwrite the file.
However, what should happen is that the file is truncated before the write. There are several ways to do this in Windows, but this is what happens when I call stream.SetLength(0). Note that the file always will exist because the picker creates it if needed.
So, I think that if there is a sequence of a file being opened, it not being zero length, and there being a write before a truncate, that could be an instance of an Acropalypse-vulnerable application.
I think it could be pretty interesting if someone feels like writing code to search a Process Monitor log for such sequences. Let me know if you try!
Photo by Fotis Fotopoulos on Unsplash