Research by: Nadav Grossman
In this article, we tell the story of how we found a logical bug using the WinAFL fuzzer and exploited it in WinRAR to gain full control over a victim’s computer. The exploit works by just extracting an archive, and puts over 500 million users at risk. This vulnerability has existed for over 19 years(!) and forced WinRAR to completely drop support for the vulnerable format.
A few months ago, our team built a multi-processor fuzzing lab and started to fuzz binaries for Windows environments using the WinAFL fuzzer. After the good results we got from our Adobe Research, we decided to expand our fuzzing efforts and started to fuzz WinRAR too.
One of the crashes produced by the fuzzer led us to an old, dated dynamic link library (dll) that was compiled back in 2006 without a protection mechanism (like ASLR, DEP, etc.) and is used by WinRAR.
We turned our focus and fuzzer to this “low hanging fruit” dll, and looked for a memory corruption bug that would hopefully lead to Remote Code Execution.
However, the fuzzer produced a test case with “weird” behavior. After researching this behavior, we found a logical bug: Absolute Path Traversal. From this point on it was simple to leverage this vulnerability to a remote code execution.
Perhaps it’s also worth mentioning that a substantial amount of money in various bug bounty programs is offered for these types of vulnerabilities.
Figure 1: Zerodium tweet on purchasing WinRAR vulnerability.
WinRAR is a trialware file archiver utility for Windows which can create and view archives in RAR or ZIP file formats and unpack numerous archive file formats.
According to the WinRAR website, over 500 million users worldwide make WinRAR the world’s most popular compression tool today.
This is what the GUI looks like:
These are the steps taken to start fuzzing WinRAR:
After a short time of fuzzing, we found several crashes in the extraction of several archive formats such as RAR, LZH and ACE that were caused by a memory corruption vulnerability such as Out-of-Bounds Write. The exploitation of these vulnerabilities, though, is not trivial because the primitives supplied limited control over the overwritten buffer.
However, a crash related to the parsing of the ACE format caught our eye. We found that WinRAR uses a dll named unacev2.dll
for parsing ACE archives. A quick look at this dll revealed that it’s an old dated dll compiled in 2006 without a protection mechanism. In the end, it turned out that we didn’t even need to bypass them.
We decided to focus on this dll because it looked like it would be quick and easy to exploit.
Also, as far as WinRAR is concerned, as long as the archive file has a .rar extension, it would handle it according to the file’s magic bytes, in our case – the ACE format.
To improve the fuzzer performance, and to increase the coverage only on the relevant dll, we created a specific harness for unacev2.dll
.
To do that, we need to understand how unacev2.dll
is used. After reverse engineering the code calling unacev2.dll
for ACE archive extraction, we found that two exported functions should be called for extraction in the following order:
ACEInitDll
, with the following signature:INT __stdcall ACEInitDll(unknown_struct_1 *struct_1);
ACEExtract
, with the following signature:INT __stdcall ACEExtract(LPSTR ArchiveName, unknown_struct_2 *struct_2);
•ArchiveName
: string pointer to the path to the ace file to be extractedstruct_2
: pointer to an unknown structBoth of these functions required structs that are unknown to us. We had two options to try to understand the unknown struct: reversing and debugging WinRAR, or trying to find an open source project that uses those structs.
The first option is more time consuming, so we opted to try the second one. We searched github.com for the exported function ACEInitDll
and found a project named FarManager that uses this dll and includes a detailed header file for the unknown structs.
Note: The creator of this project is also the creator of WinRAR.
After loading the header files to IDA, it was much easier to understand the previously “unknown structs” to both functions (ACEInitDll
and ACEExtract
), as IDA displayed the correct name and type for each struct member.
From the headers we found in the FarManager project, we came up with the following signature:
INT __stdcall ACEInitDll(pACEInitDllStruc DllData);
INT __stdcall ACEExtract(LPSTR ArchiveName, pACEExtractStruc Extract);
To mimic the way that WinRAR uses unacev2.dll
, we assigned the same struct member just as WinRAR did.
We started to fuzz this specific harness, but we didn’t find new crashes and the coverage did not expand in the first few hours of the fuzzing. We tried to understand the reason for this limitation.
We started by looking for information about the ACE archive format.
We didn’t find a RFC for that format, but we did find vital information over the internet.
1. Creating an ACE archive is protected by a patent. The only software that is allowed to create an ACE archive is WinACE
. The last version of this program was compiled in November 2007. The company’s website has been down since August 2017. However, extracting an ACE archive is not protected by a patent.
2. A pure Python project named acefile
is mentioned in this Wikipedia page. Its most useful features are:
To understand the ACE file format, let’s create a simple .txt
file (named “simple_file.txt”), and compress it using WinACE
. We will then check the headers of the ACE file using acefile
.
This is simple_file.txt
These are the options we selected in WinACE
to create our example:
This option creates the subdirectories \users\nadavgr\Documents
under the chosen extraction directory and extracts simple_file.txt to that relative path.
simple_file.ace
Running acefile.py
from the acefile
project using headers flags displays information about the archive headers:
This results in:
Notes:
filename
field in the image above as a single slash “\”, this is just python escaping.acefile
.Summary of the important fields:
unacev2.dll
.*Note – The CRC is a modified implementation of the regular CRC-32.filename
is defined by 2 bytes (little endian) marked by a black frame in the hex dump.WinACE
, during the creation of an ACE archive, if the archive is created using an unregistered version of WinACE
.origsize
” – The content’s size. The content itself is positioned after the header that defines the file (“hdr_type” field == 1).hdr_size
” – The header size. Marked by a gray frame in the hex dump.Because the filename field contains the relative path to the file, we did some manual modification attempts to the field to see if it is vulnerable to “Path Traversal.”
For example, we added the trivial path traversal gadget “\..\” to the filename field and more complex “Path Traversal” tricks as well, but without success.
After patching all the structure checks, such as the CRC validation, we once again activated our fuzzer. After a short time of fuzzing, we entered the main fuzzing directory and found something odd. But let’s first describe our fuzzing machine for some necessary background.
To increase the fuzzer performance and to prevent an I\O bottleneck, we used a RAM disk drive that uses the ImDisk toolkit on the fuzzing machine.
The Ram disk is mapped to drive R:\, and the folder tree looks like this:
A short time after starting the fuzzer, we found a new folder named sourbe in a surprising location, in the root of drive R:\
The harness is instructed to extract the fuzzed archive to sub-directories under “output_folders”. For example, R:\ACE_FUZZER\output_folders\Slave_2\
. So why do we have a new folder created in the parent directory?
Inside the sourbe
folder we found a file named RED VERSION_¶
with the following content:
This is the hex dump of the test case that triggers the vulnerability:
Notes:
acefile
.acefile
.These are the first three things that we noticed when we looked at the hex dump and the output from acefile
:
filename
field was used in the extraction process as an “absolute path” instead of a relative path to the destination folder (the backslash is the root of the drive).filename
field was converted to an underscore and the \x14\ (0x14) value represented as “¶” in the extract file name. The other content of the filename
field is ignored because there is a null char which terminates the string, after the \x14\ (0x14) value.To find the constraints that caused it to ignore the destination folder and use the filename
field as an absolute path during the extraction, we did the following attempts, based on our assumptions.
Our first assumption was the first character of the filename
field (the ‘\’ char) triggers the vulnerability. Unfortunately, after a quick check we found out that this is not the case. After additional checks we arrived at these conclusions:
filename
at least once; the location doesn’t matter.Example of a filename
field that triggers the bug: \some_folder\some_file*.exe
will be extracted to C:\some_folder\some_file_.exe
, and the asterisk is converted to an underscore (_).
Now that it worked on our fuzzing harness, it is time to test our crafted archive (e.g. exploit file) file on WinRAR.
At first glance, it looked like the exploit worked as expected on WinRAR, because the sourbe
directory was created in the root of drive C:\
. However, when we entered the “sourbe” folder (C:\sourbe
) we noticed that the file was not created.
These behaviors raised two questions:
We expected that the exploit file would behave the same on WinRAR as it behaved in our harness, for the following reasons:
unacev2.dll
) extracts the files to the destination folder, and not the outer executable (WinRAR or our harness).A deeper look showed that we had a false assumption in our second point. Our harness defines 4 callbacks pointers, and our implemented callbacks differ from WinRAR’s callbacks. Let’s return to our harness implementation.
We mentioned this signature when calling the exported function named ACEInitDll
.
INT __stdcall ACEInitDll(pACEInitDllStruc DllData);
pACEInitDllStruc
is a pointer to the sACEInitDLLStruc
struct. The first member of this struct is tACEGlobalDataStruc
. This struct has many members, including pointers to callback functions with the following signature:
INT (__stdcall *InfoCallbackProc) (pACEInfoCallbackProcStruc Info);
INT (__stdcall *ErrorCallbackProc) (pACEErrorCallbackProcStruc Error);
INT (__stdcall *RequestCallbackProc) (pACERequestCallbackProcStruc Request);
INT (__stdcall *StateCallbackProc) (pACEStateCallbackProcStruc State);
These callbacks are called by the dll (unacev2.dll
) during the extraction process.
The callbacks are used as external validators for operations that about to happen, such as the creation of a file, creation of a directory, overwriting a file, etc.
The external callback/validators get information about the operation that’s about to occur, for example, file extraction, and returns its decision to the dll.
If the operation is allowed, the following constant is returned to the dll: ACE_CALLBACK_RETURN_OK
Otherwise, if the operation is not allowed by the callback function, it returns the following constant: ACE_CALLBACK_RETURN_CANCEL
, and the operation is aborted.
For more information about those callbacks function, see the explanation from the FarManager.
Our harness returned ACE_CALLBACK_RETURN_OK
for all the callback functions except for the ErrorCallbackProc
, where it returned ACE_CALLBACK_RETURN_CANCEL
.
It turns out, WinRAR does validation for the extracted filename
(after they are extracted and created), and because of those validations in the WinRAR callback’s, the creation of the file was aborted. This means that after the file is created, it is deleted by WinRAR.
This is part of the WinRAR callback’s validator pseudo-code that prevents the file creation:
“SourceFileName
” represents the relative path to the file that will be extracted.
The function does the following checks:
\
” or “/
”...\
” or “../
” which are gadgets for “Path Traversal”.\..\
”\../
”/../
”/..\
”The extraction function in unacv2.dll
calls StateCallbackProc
in WinRAR, and passes the filename
field of the ACE format as the relative path to be extracted to.
The relative path is checked by the WinRAR callback’s validator. The validators return ACE_CALLBACK_RETURN_CANCEL
to the dll, (because the filename
field starts with backslash “\”) and the file creation is aborted.
The following string passes to the WinRAR callback’s validator:
“\sourbe\RED VERSION_¶”
Note: This is the original filename with fields “\sourbe\RED VERSION*¶”. “unacev2.dll
” replaces the “*” with an underscore.
Because of a bug in the dll (“unacev2.dll
”), even if ACE_CALLBACK_RETURN_CANCEL
returned from the callback, the folders specified in the relative path (filename
field in ACE archive) will be created by the dll.
The reason for this is that unacev2.dll
calls the external validator (callback) before the folder creation, but it checks the return value from the callbacks too late – after the creation of the folder. Therefore, it aborts the extraction operation just before writing content to the extracted file, before the call to WriteFile API.
It actually creates the extracted file, without writing content to it. It calls to CreateFile API
and then checks the return code from the callback function. If the return code is ACE_CALLBACK_RETURN_CANCEL
, it actually deletes the file that previously created by the call to CreateFile API.
Side Notes:
ACE_CALLBACK_RETURN_CANCEL
, dll tries to delete the Alternate Data Stream of the file instead of the file itself.unacev2.dll
. It enables our harness to extract the file to an arbitrary path, and completely ignore the destination folder, and treats the extracted file relative path as the full path.filename
at least once. The location does not matter.unacev2.dll
doesn’t abort the operation after getting the abort code from the WinRAR callback (ACE_CALLBACK_RETURN_CANCEL
). Due to this delayed check of the return code from WinRAR callback, the directories specified in the exploit file are created.At this point, we wanted to figure out why the destination folder is ignored, and the relative path of the archive files (filename
field) is treated as the full path.
To achieve this goal, we could use static analysis and debugging, but we decided on a much quicker method. We used DynamoRio to record the code coverage in unacev2.dll
of a regular ACE file and of our exploit file which triggered the bug. We then used the lighthouse plugin for IDA and subtracted one coverage path from the other.
These are the results we got:
In the “Coverage Overview” window we can see a single result. This means there is only one basic block that was executed in the first attempt (marked in A) and wasn’t reached on the second attempt (marked in B).
The Lighthouse plugin marked the background of the diffed basic block in blue, as you can see in the image below.
From the code coverage results, you can understand that the exploit file is not going through the diffed basic block (marked in blue), but it takes the opposite basic block (the false condition, marked with a red arrow).
If the code flow goes through the false condition (red arrow) the line that is inside the green frame replaces the destination folder with ""
(empty string), and the later call to sprintf
function, which concatenates the destination folder to the relative path of the extracted file.
The code flow to the true and false conditions, marked with green and red arrows respectively,
is influenced by the call to the function named GetDevicePathLen
(inside the red frame).
If the result from the call to GetDevicePathLen
equals 0, the sprintf
looks like this:
sprintf(final_file_path, "%s%s", destination_folder, file_relative_path);
Otherwise:
sprintf(final_file_path, "%s%s", "", file_relative_path);
The last sprintf
is the buggy code that triggers the Path Traversal vulnerability.
This means that the relative path will actually be treated as a fullpath to the file/directory that should be written/created.
Let’s look at GetDevicePathLen function to get a better understanding of the root cause:
The relative path of the extracted file is passed to GetDevicePathLen
.
It checks if the device or drive name prefix appears in the Path parameter, and returns the length of that string, like this:
C:\some_folder\some_file.ext
\some_folder\some_file.ext
\\LOCALHOST\C$\some_folder\some_file.ext
\\?\Harddisk0Volume1\some_folder\some_file.ext
some_folder\some_file.ext
If the return value from GetDevicePathLen
is greater than 0, the relative path of the extracted file will be considered as the full path, because the destination folder is replaced by an empty string during the call to sprintf
, and this leads to Path Traversal vulnerability.
However, there is a function that “cleans” the relative path of the extract file, by omitting any sequences that are not allowed before the call to GetDevicePathLen
.
This is a pseudo-code that cleans the path “CleanPath
”.
The function omits trivial Path Traversal sequences like “\..\
” (it only omits the “..\
” sequence if it is found in the beginning of the path) sequence, and it omits drive sequence like: “C:\
”, “C:
”, and for an unknown reason, “C:\C:
” as well.
Note that it doesn’t care about the first letter; the following sequence will be omitted as well: “_:\
”, “_:
”, “_:\_:
” (In this case underscore represents any value).
To create an exploit file, which causes WinRAR to extract an archived file to an arbitrary path (Path Traversal), extract to the Startup Folder (which gains code execution after reboot) instead of to the destination folder.
We should bypass two filter functions to trigger the bug.
To trigger the concatenation of an empty string to the relative path of the compressed file, instead of the destination folder:
sprintf(final_file_path, "%s%s", "", file_relative_path);
Instead of:
sprintf(final_file_path, "%s%s", destination_folder, file_relative_path);
The result from GetDevicePathLen
function should be greater than 0.
It depends on the content of the relative path (“file_relative_path”). If the relative path starts the device path this way:
C:\some_folder\some_file.ext
\some_folder\some_file.ext
(The first slash represents the current drive.)The return value from GetDevicePathLen
will be greater than 0.
However, there is a filter function in unacev2.dll
named CleanPath
(Figure 17) that checks if the relative path starts with C:\ and removes it from the relative path string before the call to GetDevicePathLen
.
It omits the “C:\
” sequence from the option 1 string but doesn’t omit “\” sequence from the option 2 string.
To overcome this limitation, we can add to option 1 another “C:\
” sequence which will be omitted by CleanPath
(Figure 17), and leave the relative path to the string as we wanted with one “C:\
”, like:
C:\C:\
some_folder\some_file.ext
=> C:\
some_folder\some_file.ext
However, there is a callback function in WinRAR code (Figure 13), that is used as a validator/filter function. During the extraction process, unacev2.dll
is called to the callback function that resides in the WinRAR code.
The callback function validates the relative path of the compressed file. If the blacklist sequence is found, the extraction operation will be aborted.
One of the checks that is made by the callback function is for the relative path that starts with “\” (slash).
But it doesn’t check for the “C:\
”. Therefore, we can use option 1’ to exploit the Path Traversal Vulnerability!
We also found an SMB attack vector, which enables it to connect to an arbitrary IP address and create files and folders in arbitrary paths on the SMB server.
Example:
C:\\\10.10.10.10\smb_folder_name\some_folder\some_file.ext
=> \\10.10.10.10\smb_folder_name\some_folder\some_file.ext
We change the .ace extension to .rar extension, because WinRAR detects the format by the content of the file and not by the extension.
This is the output from acefile
:
We trigger the vulnerability by the crafted string of the filename
field (in green).
This archive will be extracted to C:\some_folder\some_file.txt
no matter what the path of the destination folder is.
We can gain code execution, by extracting a compressed executable file from the ACE archive to one of the Startup Folders. Any files that reside in the Startup folders will be executed at boot time.
To craft an ACE archive that extracts its compressed files to the Startup folder seems to be trivial, but it’s not.
There are at least 2 Startup folders at the following paths:
C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp
C:\Users\<user name>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
The first path of the Startup folder demands high privileges / high integrity level (in case the UAC is on). However, WinRAR runs by default with a medium integrity level.
The second path of the Startup folder demands to know the name of the user.
We can try to overcome it by creating an ACE archive with thousands of crafted compressed files, any one of which contains the path to the Startup folder but with different <user name>
, and hope that it will work in our target.
We have found a vector which allows us to extract a file to the Startup folder without caring about the <user name>
.
By using the following filename
field in the ACE archive:
C:\C:
C:../AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\some_file.exe
It is translated to the following path by the CleanPath
function (Figure 17):
C:../AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\some_file.exe
Because the CleanPath
function removes the “
C:\C:
” sequence.
Moreover, this destination folder will be ignored because the GetDevicePathLen
function (Figure 16) will return 2 for the last “C:” sequence.
Let’s analyze the last path:
The sequence “C:
” is translated by Windows to the “current directory” of the running process. In our case, it’s the current path of WinRAR.
If WinRAR is executed from its folder, the “current directory” will be this WinRAR folder: C:\Program Files\WinRAR
However, if WinRAR is executed by double clicking on an archive file or by right clicking on “extract” in the archive file, the “current directory” of WinRAR will be the path to the folder that the archive resides in.
For example, if the archive resides in the user’s Downloads folder, the “current directory” of WinRAR will be:
C:\Users\<user name>\Downloads
If the archive resides in the Desktop folder, the “current directory” path will be:
C:\Users\<user name>\Desktop
To get from the Desktop or Downloads folder to the Startup folder, we should go back one folder “../
” to the “user folder”, and concatenate the relative path to the startup directory: AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\
to the following sequence: “C:../
”
This is the end result: C:../AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\some_file.exe
Remember that there are 2 checks against path traversal sequences:
CleanPath
function which skips such sequences.CleanPath
checks for the following path traversal pattern: “\..\
”
The WinRAR’s callback function checks for the following patterns:
“\..\”
“\../”
“/../”
“/..\”
Because the first slash or backslash are not part of our sequence “C:../
”, we can bypass the path traversal validation. However, we can only go back one folder. It’s all we need to extract a file to the Startup folder without knowing the user name.
Note: If we want to go back more than one folder, we should concatenate the following sequence “/../
”. For example, “C:../../
” and the “/../
” sequence will be caught be the callback validator function and the extraction will be aborted.
Toward the end of our research, we discovered that WinACE
created an extraction utility like unacev2.dll
for linux which is called unace-nonfree (compiled using Watcom compiler). The source code is available.
The source code for Windows (which unacev2.dll
was built from) is included as well, but it’s older than the last version of unacev2.dll
, and can’t be compiled/built for Windows. In addition, some functionality is missing in the source code – for example, the checks in Figure 17 are not included.
However, Figure 16 was taken from the source code.
We also found the Path Traversal bug in the source code. It looks like this:
CVEs:
CVE-2018-20250, CVE-2018-20251, CVE-2018-20252, CVE-2018-20253.
WinRAR decided to drop UNACEV2.dll from their package, and WinRAR doesn’t support ACE format from version number: “5.70 beta 1”.
Quote from WinRAR website:
“Nadav Grossman from Check Point Software Technologies informed us about a security vulnerability in UNACEV2.DLL library. Aforementioned vulnerability makes possible to create files in arbitrary folders inside or outside of destination folder when unpacking ACE archives.WinRAR used this third party library to unpack ACE archives. UNACEV2.DLL had not been updated since 2005 and we do not have access to its source code. So we decided to drop ACE archive format support to protect security of WinRAR users. We are thankful to Check Point Software Technologies for reporting this issue.“
Check Point’s SandBlast Agent Behavioral Guard protect against these threats.
Check Point’s IPS blade provides protections against this threat: “RARLAB WinRAR ACE Format Input Validation Remote Code Execution (CVE-2018-20250)”
Many thanks to my colleagues Eyal Itkin (@EyalItkin) and Omri Herscovici (@omriher) for their help in this research.