This blog post was authored by Marcin Noga of Cisco Talos.

Introduction
In 2016 Talos released an advisory for CVE-2016-2334, which was a remote code execution vulnerability affecting certain versions of 7zip, a popular compression utility. In this blog post we will walk through the process of weaponizing this vulnerability and creating a fully working exploit that leverages it on Windows 7 x86 with the affected version of 7zip (x86 15.05 beta) installed.

Analysis
First a quick look at the vulnerable portion of the 7zip code. Additional technical details regarding this vulnerability can be found in the aforementioned advisory report.

The vulnerability manifests during the decompression of a compressed file located on an HFS+ filesystem. It is present within the CHandler::ExtractZlibFile function. As can be observed in Fig. A, on line 1575, the ReadStream_FALSE function gets the number of bytes to read from the `size` parameter and copies them from the file into a buffer called buf. The buf buffer has a fixed size of 0x10000 + 0x10 and is defined in the CHandler::Extract function. The problem is that the size parameter is user controlled, and is read directly from the file (line 1573) without any sanity checks being performed.

A quick summary:

  • size parameter - A 32-bit value fully controlled by the attacker.
  • buf parameter - A fixed buffer with a length of 0x10010 bytes.
  • ReadStream_FALSE - A wrapper function for the ReadFile function, in other words, the content that is overflowing the `buf` buffer is coming directly from the file and is not restricted to any characters. Note: In situations where the heap overflow is triggered by a function like read/ReadFile, generally the part of the code which is finally executed in the kernel, the overflow won't appear if we turn on page heap. Kernel awareness of the unavailable page (free/protected/guarded) causes the system call to simply return an error code. Keep this in mind before turning on page heap.

We need to create a base HFS+ image which we will modify later to trigger the vulnerability. We can do this using either Apple OSX or with the python script available here if using the Windows platform. On OSX Snow Leopard 10.6 and above, you can use the DiskUtil utility with the --hfsCompression option to create the base image. Later we will walk through the technical details of how modify the image to trigger the vulnerability. For now, the modified version of the image should look like this.

c:\> 7z l PoC.img

Scanning the drive for archives:
1 file, 40960000 bytes (40 MiB)
Listing archive: PoC.img
--

Path = PoC.img
Type = HFS
Physical Size = 40960000
Method = HFS+
Cluster Size = 4096
Free Space = 38789120
Created = 2016-07-09 16:41:15
Modified = 2016-07-09 16:59:06

Date      Time    Attr         Size   Compressed  Name
------------------- ----- ------------ ------------  ------------------------
2016-07-09 16:58:35 D....                            Disk Image
2016-07-09 16:59:06 D....                            Disk Image\.fseventsd
2016-07-09 16:41:15 D....                            Disk Image\.HFS+ Private Directory Data
2016-07-09 16:41:16 .....       524288       524288  Disk Image\.journal
2016-07-09 16:41:15 .....         4096         4096  Disk Image\.journal_info_block
2016-07-09 16:41:15 D....                            Disk Image\.Trashes
2014-03-13 14:01:34 .....       131072       659456  Disk Image\ksh
2014-03-20 16:16:47 .....         1164          900  Disk Image\Web.collection
2016-07-09 16:41:15 D....                            Disk Image\[HFS+ Private Data]
2016-07-09 16:59:06 .....          111         4096  Disk Image\.fseventsd\0000000000f3527a
2016-07-09 16:59:06 .....           71         4096  Disk Image\.fseventsd\0000000000f3527b
2016-07-09 16:59:06 .....           36         4096  Disk Image\.fseventsd\fseventsd-uuid
------------------- ----- ------------ ------------  ------------------------

2016-07-09 16:59:06             660838      1201028  7 files, 5 folders

Preparing the Test Environment

Building 7zip 15.05 beta
To make our exploitation analysis easier we can build 7zip from source code and add debugging features to the build. Change the build file (Build.mak) as follows to enable debugging symbols:

Standard:

- CFLAGS = $(CFLAGS) -nologo -c -Fo$O/ -WX -EHsc -Gy -GR-
- CFLAGS_O1 = $(CFLAGS) -O1
- CFLAGS_O2 = $(CFLAGS) -O2
- LFLAGS = $(LFLAGS) -nologo -OPT:REF -OPT:ICF

With debug: 

+ CFLAGS_O1 = $(CFLAGS) -Od
+ CFLAGS_O2 = $(CFLAGS) -Od
+ CFLAGS = $(CFLAGS) -nologo -c -Fo$O/ -W3 -WX -EHsc -Gy -GR- -GF -ZI
+ LFLAGS = $(LFLAGS) -nologo -OPT:REF -DEBUG

Once 7zip has been compiled from source, we can perform a test run using our PoC and see what the heap layout looks like before the overflow occurs.

"C:\Program Files\Windows Kits\10\Debuggers\x86\windbg.exe" -c"!gflag -htc -hfc -hpc" t:\projects\bugs\7zip\src\7z1505-src\CPP\7zip\installed\7z.exe x PoC.hfs

Note: Remember to turn off all heap options for the debugging session using the !gflag command.

Let's check the memory chunks after this buffer :

The heap listing looks promising. We found a couple of objects with a vftable. We can potentially use them to manipulate the control flow of the code. By overwriting the vftables with our data, we can bypass the heap overflow mitigation techniques present in modern operating systems and take over control of the code execution.

Let's do a test without changing the PoC by just overwriting the object inside the debugging session and continue with execution:

It appears that the overwritten object was called after the overflow and it happened quickly enough that no other memory operation (e.g. alloc/free) affected the corrupted heap prior to the call. Had this not been the case the application would have crashed. Now we need to confirm that the heap layout is the same with the standard version of 7zip. It is important to keep in mind that the debug version could have a significantly different heap layout.

Finding the ExtractZLibFile Function
To determine what the heap layout looks like in the standard build of 7zip, we need to find the ExtractZLibFile function where the ReadStream_FALSE function is called.

To localize this function we can look for one of the constants used in its body and search for it in IDA.

0x636D7066

*(Function was renamed in IDA before)

Jumping into the .text1001D9D9 location shows that we found what we were looking for.

We can then set a breakpoint on 0x1001D7AB which contains the call to ReadStream_FALSE in our debugger to analyze the heap layout around `buf`.

Hint: See that edx is pointing to the `buf` buffer address

The heap layout should look like this:

Unfortunately, it appears that using the standard 7zip build results in a different heap layout. For instance, following our `buf` buffer [size 0x10010 ] there is no object containing a vftable.

Note: WinDBG shows objects with a vftable via the !heap -p -h command even when no debugging symbols or RTTI are loaded. For example :

013360b0 0009 0007  [00]   013360b8    0003a - (busy)
013360f8 0007 0009  [00]   01336100    00030 - (busy)     ←-- object with vftable

? 7z!GetHashers+246f4

01336130 0002 0007  [00]   01336138    00008 - (free)
01336140 9c01 0002  [00]   01336148    4e000 - (busy)
* 01384148 0100 9c01  [00]   01384150    007f8 - (busy)

Our goal is to write a real world exploit, so we need to find a way to manipulate the heap and reorder it in a better way to facilitate this.

Building Our Strategy
Our PoC.hfs file contents and its internal data structures have the biggest influence on the structure of the heap. If we want to change the current heap layout we need to create a reasonably reliable HFS+ image file generator, which will allow us to add HFS+ parts into the file image in a way that allows us to reorder heap allocations so that we can ensure that objects with a vtable appear after our `buf` buffer.

There is no need to build a super advanced HFS+ image file generator implementing all possible structures, configurations and functionalities. It simply needs to support the elements that will enable us to reorder the heap and trigger the vulnerability.

For details regarding the HFS+ file format, you can consult the documentation here. A decent understanding of the HFS+ file format will help during this debugging session.

Identifying Elements That Change the Heap Layout
First we need to identify places where the data from our file is written on the heap and its size is variable. We will begin our search in the part of the code that is responsible for parsing the HFS+ format.

Note: Remember that 7zip might execute several instructions before it begins parsing a particular format. An example of this are actions that relate to "dynamic" format detection,etc.

By debugging the code of our PoC.hfs example step by step, we can find all of the functions that are responsible for writing our data to the heap during the file parsing process.

Mapping it to the source code, we start here:

To later dive into:

After some testing, we can identify a perfect candidate inside the following function:

LoadName function body:

Each attribute has a name which is a UTF-16 string with a variable size allocated on the heap. This looks like a perfect candidate. We can add as many attributes as we want using their name as a spray. The only constraint is that the `attr.ID` must be set to anything except the corresponding `file.ID`

Writing the HFS+ Generator
The file which we want to generate is supposed to look like this:

The 7zip author did not directly follow the standard HFS+ documentation, when the HFS+ file system parser was implemented by him. This requires us to first analyse 7zip to determine how HFS+ parsing was specifically implemented in 7zip. We are releasing a file generation script to create the specially crafted file required to exploit this vulnerability. The script can be obtained here.

010 Editor template used during the file format reversing process.

As mentioned above, our generator is limited to only generating the necessary structures in the file to trigger the specific vulnerability covered in this post. By setting the `OVERFLOW_VALUE` (the size of the buffer used to overflow the `buf` buffer) to 0x10040, we can generate a file that triggers the vulnerability and generates the following result in our debugging session:

Let's single step through the code execution and analyze where the overflow occurs:

We have confirmed that our HFS+ generator works. Let's increase the OVERFLOW_VALUE variable to 0x10300 which should be enough to overflow the following free chunk with the size of 0x310 bytes. In other words the chunk that contains an object with a vftable. Let's walk through this below.

What we find is that the free chunk following the `buf` buffer grew up, preventing us from successfully overflowing the next object with a vftable. It appears that there was a memory allocation somehow related to the content of our file. To search for the location where that instruction occurred we can set the following conditional breakpoint:

bp ntdll!RtlAllocateHeap "r $t0=esp+0xc;.if (poi(@$t0) > 0xffff) {.printf \"RtlAllocateHeap hHEAP 0x%x, \", poi(@esp+4);.printf \"Size: 0x%x, \", poi(@$t0);.echo}.else{g}"

To simplify this task we can use the 7zip version with debugging symbols which we built earlier.

The debugger hit the breakpoint where buffer with same size as our file size is allocated. After quick analysis it turned out that we have landed in the portion of the code that is responsible for the heuristic detection of the file format.

7zip allocates a buffer large enough to handle the size of the entire file contents then it attempts to determine the format of the file before finally freeing the previously allocated buffer. The freed buffer memory is later used during the allocation of the `buf` buffer. This is why we see a gap after its chunk which grows when we increase the payload size. Does that mean the exploitation won't be possible? No, did you notice the file extension we used to save the generated file? If we want to avoid the heuristic file detection functions in 7zip, we simply need to use proper file extension, .hfs in this case. If we use this extension, 7zip does not execute the heuristic functions and the heap looks like this:

Building Our Strategy
Let's take a moment to summarize what we now know and try to figure out a strategy we can use to create a working exploit.

  • Our target buffer (`buf`) has a fixed size: 0x10010.
  • Due to this buffer size, it will always be allocated by heap-backend. Additional details regarding this can be found here.
  • We can allocate any number of objects with any size before the overflow occurs.
  • We can't perform or trigger any free action on the heap.
  • We are unable to perform any alloc/free operation following the overflow. Given the situation described above, being limited to the aforementioned operations and considering all of the heap mitigations implemented in Windows 7, a sound approach is described below:
  • We should locate an object with vftable that is called as soon as possible following the overflow. This is important because if the call to vftable that is overflowed by us is far from memory location where overflow took place, the likelihood that the code will call an alloc/free operation increase, causing the program to crash.
  • Spray the heap with attributes (name) with the same size the interesting objects we identified. The assumption is that allocating objects with the same size as the target object with an amount greater than 0x10 and an object size of less than 0x4000 (the Low Fragmentation Heap maximum object size) we will activate LFH and allocate free chunks for objects with that size. This should result in free slots being allocated after the overflowed buffer and the objects will be stored within them.

Identifying Interesting Objects
Now that we have defined our strategy, we need to locate a suitable object to overwrite. To find it, we can use a simple JS script for WinDBG that is responsible for printing an object with vftable as well as its stack trace.

The script that performs these actions is located here.

This should result in the following:

First we will try to look for objects allocated in the same function where overflow occurs, `ExtractZlibFile` because they will likely be used quickly following the overflow. We can identify two candidates based on the previous screenshot.

The aforementioned objects are defined in the following locations:

Line 1504  CMyComPtr<ISequentialInStream> inStream;
(...)
Line 1560  CBufInStream *bufInStreamSpec = new CBufInStream;
Line 1561  CMyComPtr<ISequentialInStream> bufInStream = bufInStreamSpec;

Their destructors (release virtual method) are called as soon as the function exits. The fastest way to trigger this is to set the first byte in our overflowed buffer to `0xF`.

Moving the Objects
Now that we have identified the object we would like to overflow, we need to spray the heap with attribute structures containing `name` strings with the same length as the objects, which are:

0x20 and 0x30.

We can accomplish this using the following:

We can either write a script which will control WinDBG and increase the number of attribute structures until our target objects are allocated after overflowing the buffer or do it manually.

We chose to take a manual approach, simply increasing the numbers by 10, 20, 30 and observing the heap. As the object locations began to reach the buf location, we simply switched to increasing it by one.

A few attempts later we reached the value of 139:

139 * (0x20 + 0x30 + 2* 0x18)

At this point the heap layout looks as follows:

This heap structure looks promising. Subtracting the address of the `buf` buffer, which is 0x12df9c8 subtracted by 8 bytes due to the offset in the call instruction (0x12df9d0) from the address after the object located at 0x12efdf8 will help us determine how many bytes we need to overwrite the targeted object. In order to identify how much space is available for our payload, I maximized this size choosing nearly the last address available on the heap (not visible in the screenshot above). Using that information, we can update the OVERFLOW_VALUE variable with value 0x12618.

Now we can regenerate our file again and execute the application to confirm that vftable is successfully overwritten:

Now that we have confirmed that, we can specifically focus on weaponizing our exploit.

Checking Available Mitigations
Further development of our exploit depends on mitigations implemented in the version of 7zip we are analyzing. Below we can see the mitigations implemented in version 10.05 of 7zip:

As identified in the screenshot below, 7zip does not support Address Space Layout Randomization (ASLR) or Data Execution Prevention (DEP). We had hoped that this would change following the publication of an advisory last year related to this vulnerability but this still appears to be the case.

If you are using the 64-bit version of 7zip, then DEP is forced by operating system.

Finding The Payload
Before we start looking for gadgets let's identify all registers and pointers on the stack pointing to our payload.

As you can see in the above screenshot, there are a few places pointing to different parts of our payload :

  • ESI
  • EDX
  • ESP
  • ESP-C
  • ESP+30
  • EBP+40
  • EBP-2C
  • EBP-68 We need to determine the exact offset from our buffer to the vftable object. Since ESI points to the vftable object and EDX points to our buffer, we can simply subtract EDX from ESI to obtain this offset.
0:000> ?esi - edx 

Evaluate expression: 66608 = 00010430

Putting the value that is stored at that offset into our payload results in the following:

The value has changed because `8` has been added. Now we can start identifying gadgets keeping in mind the aforementioned elements.

Pointer on Pointer
Since we will be overwriting the pointer to the vftable we will need to identify both gadgets as well as pointers to this gadgets.

To perform this task you can use the following tools:

  • RopGadgets
  • Mona Using multiple tools is a good way to maximize the number of interesting gadgets that are discovered during this type of analysis.

First using RopGadgets let's generate the list of gadgets for 7z.exe and 7z.dll:

Now using these lists with Mona we can find pointers to these gadget addresses.

Abusing Lack of DEP
Since DEP is not supported in this 7zip version, one of the easiest ways to exploit this vulnerability is to simply redirect code execution to our buffer located on the heap. Reviewing the list of pointers we previously enumerated among the others which will meet these requirements reveals the following candidates:

So there are multiple addresses which contain the same pointer value. They will be very useful because in our gadget we will redirect code execution to our buffer using the pointer stored in the address pointed to by the ESP register. It contains the same value pointed to by ESI which is where we will put the address of our pointer to our fake vftable.

Keeping this in mind, we need to identify what instruction it will disassemble to.

As you can see the `POP ES` instruction causes an exception. Additionally, we do not have any influence on the value on the stack being "popped" to `ES`. Fortunately, one of the additional gadget addresses disassembles to a less problematic instruction:

0x1007c748 - 8  = 0x1007c740

`EDI` points to a writable area of memory, so we should be able to execute these instructions.

Also notice that the bytes we use to fill the buffer (`0xcc`) have been used in this instruction.

With that in mind, we will omit 3 bytes when setting the offset for our shellcode in the buffer.

Adding Shellcode
Now we are ready to add our shellcode which should be located at offset:

fake_vftable_ptr_offset = 0x00010430 + 3 ("0xCC")

To generate the shellcode we can use msfvenom which is included with Metasploit :

The updated script including our shellcode should look like this:

Testing the Exploit
Now that we have everything in place we can generate our HFS file and test our exploit:
Now we have confirmed that our shellcode operates as intended.

Exploit Stability  
We have confirmed that our strategy of spraying the heap with objects with sizes of 0x20 and 0x30 is effective but what about stability?

The same version of 7zip parsing exact same HFS file should contain same heap layout at certain points but we need to consider variable artifacts allocated on the heap like environment variables, command line argument strings, the path to the file containing our payload, etc. These elements could change the heap layout and differ across systems.

Unfortunately those variable artifacts are allocated on the same heap as our overflowed buffer in this case, at least in the case of the command line version of 7zip which we created our exploit to target. Analyzing the heap memory used to allocate our target buffer we can see the following:

Inspecting the heap, we can see a string which is actually the path to the location of the HFS file to unpack. The variable length of this single string can significantly impact the amount of free/allocated space on the heap which can impact the heap spray object composition and result in failed exploitation.

One way to account for the difference in free heap space is to create a large enough allocation to exhaust the potential free space on heap, taking into account system limitations with regards to file path and environment variable length, etc. That exercise as well as investigating how the heap layout in the 7zip GUI version is presented is left for interested readers.

Summary
Heap based buffer overflow vulnerabilities in applications like archive utilities or general file parsers are still exploitable on modern systems, even if we do not have such flexible influence on the heap like during web browser exploitation. Lacking the option to use corruption of heap metadata to successful exploit the vulnerability forces us to overwrite application data and leverage that to take control of code execution flow. Still lack of current standard mitigations in some products makes exploitation significantly easier.