In April, Steven Seeley of Source Incite published a report of a vulnerability in Foxit Reader and PhantomPDF versions up to 9.0.1 that could allow for remote code execution on a target system. Public release of this report was coordinated with an official vendor fix included in the April's Foxit Reader and PhantomPDF 9.1. release.
According to our analysis the PoC attached to the report triggers a heap-based buffer overflow in a Bitmap image data copy operation inside ConvertToPDF_x86.dll module using an overlong biWidth attribute.
When dropping SRC-2018-0009.bmp into Foxit Reader we immediately got a crash and inspected it by hooking WinDbg with Page Heap enabled.
(3250.3480): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Program Files (x86)\Foxit Software\Foxit Reader\Plugins\Creator\x86\ConvertToPDF_x86.dll -
eax=00000004 ebx=00000000 ecx=00000008 edx=15db5ef8 esi=15db9f38 edi=15dc2000
eip=5f5f9d17 esp=1866f904 ebp=1866f920 iopl=0 nv up ei pl nz na po cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010203
5f5f9d17 8807 mov byte ptr [edi],al ds:002b:15dc2000=??
It first looked like a typical buffer overflow, where a missing boundary check allows data to be written over the edge of destination buffer addressed by edi. (Note: the crash offset is marked in red.)
But the loop of the copy operation is constrained by checking biWidth (at esi+54h) which is read from Bitmap image header. So why is there an access violation despite this check?
When inspecting that buffer's properties something stuck out: an unusually small buffer size was reported by !heap to have been allocated, specifically just 4 bytes (UserSize in the WinDbg output below).
0:012> !heap -p -a edi
address 15dc2000 found in
_DPH_HEAP_ROOT @ 157e1000
in busy allocation ( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize)
15ce264c: 15dc1ff8 4 - 15dc1000 2000
Inspecting SRC-2018-0009.bmp in a hex editor revealed that biWidth is set to a huge value of 40000001h - a remotely controllable attribute in case the image used in conversion to PDF was obtained from the attacker. So if biWidth was used in calculation of destination buffer size before buffer allocation, that calculation was probably prone to integer overflow in order to result in so much lower a value (4).
Besides heap block properties, !heap also printed out the buffer allocation call stack. We inspected that and indeed we found an overflow-prone buffer size calculation right before the orange-marked return address in the above call stack. The code graph below shows only a part of the vulnerable execution path, but the omitted code is very similar so it suffices for our explanation. There are three code blocks; edi represents biWidth read from image data in the first block, in the third block eax is the destination buffer size to be allocated, ecx represents biBitCount (number of bits per pixel).
There are 3 instructions that can overflow in the last code block:
- imul eax, ecx - in case of SRC-2018-0009.bmp, eax=40000001h and ecx=4 so this is the operation that overflows (result is 00000001`00000004h but eax can only hold the lower DWORD - 00000004h)
- add eax,ebx - addition of 1Fh to a potentially huge number held by eax could overflow in case previous multiplication didn't overflow
- add eax,edx - addition of up to 1Fh (edx is and-ed to 1Fh beforehand) to a potentially huge number held by eax could overflow
As already said, this is not the only vulnerable code block before the destination buffer allocation. Depending on biBitCount value that can hold at most 20h two other similar buffer size calculations can occur. In order to fix all of them, many checks would have to be inserted so we decided for a more compact solution. Given that all constraints to the buffer size calculation are known - buffer size can not theoretically exceed 0xffffffff, biBitCount can be at most 20h and two potentially added values are at most 1Fh -, the maximum valid biWidth could be calculated beforehand as follows:
(0xffffffff-0x1f-0x1f)/0x20 = 0x07fffffe
However, one of the vulnerable blocks does not properly handle signed values so this also needs to be taken into account by halving the maximum buffer size to 0xffffffff/2 = 0x7fffffff. Once we do that, the add eax,edx instruction can't overflow because edx is the sign extension (mind the cdq instruction) and will always be 0. So the final constraint calculation goes like this:
(0x7fffffff-0x1f)/0x20 = 0x03ffffff
Knowing this, a single check can be placed right before biWidth is first used - at the first block of the code graph above - that makes sure only biWidth values lower than 0x03ffffff can pass. If this condition is not met, we can set biWidth to 0 so the subsequent jle would raise a handled exception and abort further processing of a malformed image. Here is the patch code that does this:
When this patch is applied to Foxit Reader's memory, the first two instructions from the top code block above (mov edi, [esi+54h] and test edi, edi, making up exactly 5 bytes) are replaced by a 5-byte jump to the patch code, forcing us to repeat the first instruction (mov edi, [esi+54h]) at the beginning of our patch code if we want to inject our code between the two instructions. The second instruction (test edi, edi) is automatically placed after the patch code by 0patch Loader because JUMPOVERBYTES 3 directs it to only omit the first instruction (3 bytes) while keeping the remaining 2-byte instruction.
This video shows our micropatch in action.
This micropatch has already been published and distributed to all installed 0patch Agents. If you're using Foxit Reader or Foxit PhantomPDF version 18.104.22.1689, you can download our free 0patch Agent, create a free 0patch account and register the agent to that account to immediately receive this micropatch and have it applied to your Foxit software.
If you're using some other version of Foxit Reader and would like to have micropatches for it, please contact us at firstname.lastname@example.org.