Analyzing an Integer Overflow in Bitdefender AV: Part 2 – The Exploit

June 21, 2018 | The ZDI Research Team

If you haven’t read Part One of this series, check it out here.

Our last blog reviewed the submission from Pagefault detailing an integer overflow in Bitdefender AV products. While that alone was enough information to file a bug with the vendor, Pagefault took it a step further by providing a proof-of-concept (PoC) exploit to go along with the report. Here’s the exploit in action:

Pagefault provided the following details on how the exploit works.

 The Exploit

Due to the terminating condition being a virtual out-of-bounds read, and the fact that the Bitdefender code emulator implements SEH, we are able to retry the overwrite multiple times. We’re also able to tweak the overwritten length and content by modifying the executed code.

The vsserve.exe executable does have ASLR and DEP activated, however the exploit avoids these mitigations by taking advantage of position-independent shellcode within a JIT page. Achieving code execution there requires the manipulation of arbitrary memory addresses.

The Bitdefender emulator provides a virtual address space to an emulated program. One emulator page of 0x1000 bytes has a corresponding real page of 0x1048 bytes, which contains several fields that help the emulator operate on the page:

Multiple VirtualAlloc calls and an associated memory access encountered inside the emulated program will lead to the creation of multiple 0x1048 structures, which subsequently get freed when corresponding VirtualFree calls are encountered.

Internally, the 0x1048 allocations are performed via msvcrt's malloc(), landing in the Low Fragmentation Heap (LFH). For Windows 7 and below, exploitation is continued by allocating multiple virtual pages, freeing at least one, triggering the vulnerable function that allocates and overflows a 0x1048 buffer, then corrupting a virtual page.

For Windows 8 and above, randomization was added to the LFH, making the heap less deterministic. The exploit bypasses randomization by resetting the LFH random table position with 0xFF allocations in between re-allocation attempts. In order to achieve a desired number of memory allocations and avoid extra allocations, JIT code has to be created for the emulated code. This is achieved by executing a piece of code at least 34 times.

Example exploitation steps on Windows 8 and newer:

-       NUMPAGES (e.g. 60) virtual pages are allocated
-       Having a random position inside the LFH bucket, the last page is freed
-       0xFF allocations follow (JIT is triggered to precisely target that number)
-       The vulnerable function is triggered and the vulnerable buffer is allocated in place of the last page
-       A limited overwrite with 8 bytes is triggered, and the other NUMPAGES-1 virtual pages are checked to see if their content was modified
-       If a modification is detected:
        o   A total of 0xFF allocations has to occur between the last allocation of the vulnerable buffer and the next
        o   Another buffer of 0x1048 bytes is allocated in the exact place as the last one and this time a sufficient number of bytes (0x1024) is overwritten, allowing the exploit to proceed
-       If a modification is not detected, the exploit allocates another set of virtual pages and repeats the process.

This is repeated until a modification is detected or the retry limit is reached. The limited overwrite is required in order to avoid hitting guard pages.

Once a virtual page is modified, any attempt to access it will be affected. The dword at offset 0x1020 decides the target read/write address being used to calculate the real address of accessed memory:

real_address = real_address_base + requested_virtual_address - dword[0x1020]

This allows us to write to an arbitrary offset. In this case, offset 0x1020 of a second virtual buffer placed before the corrupted buffer can therefore be modified, allowing repeated control of the write offset for the second virtual buffer. In other words, we’re pointing the corrupted buffer to the previous buffer:

All techniques described so far achieve a reliable read/write primitive with an arbitrary offset. This is however not sufficient for code execution.

If a piece of code is interpreted by the emulator at least 34 times, JIT compilation kicks in, then the emulator interpreting the given opcodes and constructing corresponding dynamic code is executed for the following calls to the emulated code. The constructed code is placed in a writable and executable memory page.

For each JIT segment, a memory structure is created via malloc() when parsing the repeat code. The size of that memory structure can be controlled by placing calculated instructions in the emulated code. A size of 0x1048 bytes is achieved by repeatedly calling a function with multiple push ecx/pop ecx pairs.

An array of functions that are repeatedly called is constructed with the same content, and the 34th call occurs for each function only after arbitrary read write capability is achieved, and the last vulnerable buffer freed.

The space occupied by one of the virtual buffers will be occupied by one of the JIT structures, and we will therefore be able to access it with a read/write offset.The JIT buffer is now placed 0x1108*x bytes (0x1108 = 0x1048 rounded up to the nearest LFH size and 8 added for chunk header) relative to the controlled virtual page.

The JIT structure starts with several useful fields:

By reading the dword at offset 0x2C, we can extract the real address of the corrupted virtual page, thus being further able to read/write to arbitrary addresses and not just to an arbitrary offset. The newly gained ability is next used to modify bytes placed at the address pointed to by the first dword in the JIT structure.

A custom shellcode that dumps an included executable to file and executes it is placed inside the compiled JIT code, which is executed on the next call to the corresponding JIT-compiled function. The shellcode ends with a TerminateProcess(), avoiding potential crashes due to the corrupted heap.

Conclusion

If you want to test this out for yourself, the PoC is here. It should work on Bitdefender versions prior to 73447.

This bug also shows that even when mitigations like ASLR and DEP are enabled, skilled attackers can still find methods to get code execution. If you’re a developer looking to avoid integer overflows in your software, CERT has provided some excellent guidance on avoiding overflow with signed integers for various operations. It’s definitely worth reviewing. Thanks to Pagefault for reporting this bug and thanks to Bitdefender for addressing it in a timely manner.

We’ll be back with other great submissions in the future. Until then, follow the team for the latest in exploit techniques and security patches.