TESTED VERSIONS

PRODUCT URLs

DETAILS

While parsing a PDF file with specific /Size element, a memory allocation operation
can fail, returning a NULL pointer due to integer overflow, which is unchecked and leads to a crash
during a memset() call. A carefuly selected size value can also lead to further memory
corruption.

At [1], the value in eax comes straight from the 32bit rounded value from the
/Size element. At [2], it is multiplied by four therefore invalidating the
integer overflow check that was done previously. A malloc wrapper is called
at [3] and the returned pointer (NULL in this case) is saved at [4].
Even though the pointer is checked against NULL at the end, in a subsequent
basic block it is still used as a destination for memset:

The same size derived in the previous basic block is used at [1] as a size
parameter for memset. At [2], saved pointer is retrieved and is NULL in this
case. The application crashes at [3] due to invalid pointer.

If a size value is choosen carefuly, it can lead to an integer
overflow at [2] in the first basic block such that a small value
is passed to SYSNativeAlloc at [3]. In this case, the subsequent
memset call would pass without issue. The problem arises
when, due to rounding, heap allocator returns a pointer
to a bigger heap chunk than requested. In this case, the memset
call will initiallize only the originaly requested size, leaving the
rest of the buffer uninitialized to zero. Later on in the code,
this buffer is treated as a pointer array with checks for NULL pointers,
but the uninitialized portion of the buffer may have non-NULL values
leading to further issues.

As an example, if the size value is specified to be 0x10000001 it will
pass the check before allocation in the first basic block above, but when shifted by 4,
it becomes 0x10, making a small allocation. Depending on an underlying allocator,
the actuall size of the allocated chunk would be bigger. In case of Linux, in
this case, the returned chunk will be 24 bytes long and subsequent memset will
onyl initialize the first 16 bytes.

At [1], a pointer to the previously allocated buffer is moved into esi and used as a starting
position of a loop. At [2], a counter is initialized to 0. At [3], a pointer stored in the
buffer is copied into eax and tested agains being NULL at [4]. If it's not NULL, a usercall
function who's first argument is eax is called at [5]. After the function call, or if the pointer
was NULL, a counter in edi is advanced by one and then compared to the upper bound which is
the original Size value as specified in the file (before the overflow) at [7]. Finally, the code
jumps back to [8], where the pointer into the buffer is increased by 16.

It is now clear that if only first 16 bytes of the buffer are initialized, when the code executes the loop
for the second time, at [3] it will be accessing memory that is uninitialized to zero effectively turning
this into a sort of use-after-free vulnerability. Function called at [5] deals with heap structures and,
if sufficient heap control is achieved, leftover data present in uninitialized part can cause further
memory corruption, potentially leading to code execution.