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
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 extracted
struct_2: pointer to an unknown struct
Both 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
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 (
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
These are the options we selected in
WinACEto 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.
acefile.py from the
acefile project using headers flags displays information about the archive headers:
This results in:
filenamefield in the image above as a single slash “\”, this is just python escaping.
Summary of the important fields:
unacev2.dll.*Note – The CRC is a modified implementation of the regular CRC-32.
filenameis 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
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?
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:
These are the first three things that we noticed when we looked at the hex dump and the output from
filenamefield 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).
filenamefield was converted to an underscore and the \x14\ (0x14) value represented as “¶” in the extract file name. The other content of the
filenamefield 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:
filenameat 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
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
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
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:
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.
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.
filenameat least once. The location does not matter.
unacev2.dlldoesn’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);
sprintf(final_file_path, "%s%s", "", file_relative_path);
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
It checks if the device or drive name prefix appears in the Path parameter, and returns the length of that string, like this:
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
This is a pseudo-code that cleans the path “
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:”, 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);
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:
\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
CleanPath (Figure 17) that checks if the relative path starts with C:\ and removes it from the relative path string before the call to
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 “
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.
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
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:\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
By using the following
filename field in the ACE archive:
It is translated to the following path by the
CleanPath function (Figure 17):
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:
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:
If the archive resides in the Desktop folder, the “current directory” path will be:
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: “
This is the end result:
Remember that there are 2 checks against path traversal sequences:
CleanPathfunction 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:
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.