Talos Vulnerability Report


Lexmark LibISYSpdf Image Rendering DCTStream::getBlock() Code Execution Vulnerability

August 28, 2017
CVE Number



An exploitable code execution vulnerability exists in the image rendering functionality of Lexmark Perceptive Document Filters A specifically crafted PDF can cause a function call on a corrupted DCTStream to occur, resulting in user controlled data being written to the stack. A maliciously crafted PDF file can be used to trigger this vulnerability.

Tested Versions

Lexmark Perceptive Document Filters

Product URLs


CVSSv3 Score

7.5 CVSS:3.0/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H


CWE-121 - Stack-Based Buffer Overflow


Lexmark Perceptive Document Filters is an SDK used for inspection, conversion, and viewing for a multitude of different file formats. Developers can utilize either the built binaries or shared libraries of this product in order to manipulate common file types including PDFs. It should be noted that Marklogic uses this product for rendering purposes.

Lexmark Perceptive Document Filters statically links various code with a modified version of the open source Poppler library for parsing and rendering PDF files, resulting in a shared library called libISYSpdf6.so. For rendering certain types of images within the PDF, libISYSpdf6 uses a modified version of the DCTStream class, called IGRStream.

An interesting thing to note about the DCTStream class, and by extension the IGRStream class, is that there are two buffers within the class that are used for storing image data, however upon initialization, only one of these buffers is allocated and overwritten, depending on the type of the image (interleaved || progressive). A mini-bug in the class occurs in that one can switch the image type after the allocation has occurred, forcing the DCTStream to interact with the un-allocated buffer.

Due to the above issue and the fact that the IGRStream doesn't clear a few variables correctly, ([IGRStream+0xda8], [IGRStream+0xdb0] respectively designated blockBuf and blockBufEnd), if the heap is manipulated correctly, on initialization of the IGRStream object, the blockBuf and blockBufEnd pointers can be controlled by the user. It should be noted that the image type needs to be set to progressive at first, and then swapped to interleaved after the memory allocation occurs, such that the blockBuf and blockBufEnd pointers do not get overwritten with malloc pointers. This issue will become relevant soon, but in summary, IGRStream+0xda8 and IGRStream+0xdb0 can both be user controlled.

Below is the source code from the poppler-0.53.0 library. It is not 100% accurate with regards to the Lexmark ISYSpdf6.so code, but it serves as a good base. Our corrupted IGR stream is returned from a lookup, and two functions are called upon it:

[0] fontDict->lookup is returning our corrupted IGRStream, which passes isStream() check [1] Something different in the ISYSpdf6.so binary.

Inside of GfxFont.cc:

CharCodeToUnicode *GfxFont::readToUnicodeCMap(Dict *fontDict, int nBits,
CharCodeToUnicode *ctu) {
GooString *buf;
Object obj1;l

if (!fontDict->lookup("ToUnicode", &obj1)->isStream()) { // [0]
return NULL;
buf = new GooString();
obj1.getStream()->fillGooString(buf);  // [1]

Although the above poppler source is not 100% what occurs within the Lexmark code, it's a good approximation. The actual assembly is listed below, in which our unintended object has two methods called upon it, that result in the functions DCTStream::reset [0] and DCTStream::getBlock [1] being called, the latter of which is a Lexmark-only function.

 0x7f9b7da5f2bf     <_ZN7GfxFont17readToUnicodeCMapEP4DictiP17CharCodeToUnicodePb+95>:
 mov    rdi,QWORD PTR [rsp+0x1008]
 0x7f9b7da5f2c7     <_ZN7GfxFont17readToUnicodeCMapEP4DictiP17CharCodeToUnicodePb+103>:
 mov    rax,QWORD PTR [rdi]
 0x7f9b7da5f2ca     <_ZN7GfxFont17readToUnicodeCMapEP4DictiP17CharCodeToUnicodePb+106>:
 call   QWORD PTR [rax+0x18] // [0]

// Ö

0x7f9b7da5f2f0  <_ZN7GfxFont17readToUnicodeCMapEP4DictiP17CharCodeToUnicodePb+144>:
 call   QWORD PTR [rax+0x40] // [1]

Guessed arguments:
arg[0]: 0xff2130 --> 0x7f9b7dd76870 --> 0x7f9b7dab2d10 --> 0x5c8948f8246c8948 
arg[1]: 0x7ffe879972c0 --> 0x0 
arg[2]: 0x1000 

The DCTStream::getBlock function seems to be a Lexmark replacement for the implementation of reading non-progressive and interleaved JPEG images. It reads the current stream in chunks <= 0x1000 bytes for the image data and copies it onto the stack, with a call to memcpy. The arguments to memcpy end up being as such:

Destination : Buffer on the stack, size 0x1018, (rsp before call to DCTStream::getBlock())
Source: *[DCTStream+0xdb0]
Size : *[DCTStream+0xdb0] ñ *[DCTStream+0xda8]  

Since DCTStream+0xdb0 and DCTStream+0xda8 are user controlled (remember IGRStream::blockBuf and IGRStream::blockBufEnd?), the end result is a call to memcpy with a user-controlled source, count, and a destination on the stack. The only thing protecting from an easy mode return address overwrite is a cmovg instruction as shown below:

[0] Corrupted Qword (blockBuf) loaded into RSI
[1] Corrupted Qword (blockBufEnd) loaded into rax
[2] Under flow possible here
[3] EBX == 0x1000, and the buffer for this memcpy is > 0x1000.  
[4] The sign extend move makes underflow even larger

0x00007fbe4b9251b8 <+120>:   mov    rsi,QWORD PTR [rbp+0xda8] // [0]
0x00007fbe4b9251bf <+127>:   cmp    rsi,QWORD PTR [rbp+0xdb0] 
=> 0x00007fbe4b9251c6 <+134>:   jne    0x7fbe4b925180 <_ZN9DCTStream8getBlockEPci+64>
// Ö 
0x00007fbe4b925180 <+64>:    mov    rax,QWORD PTR [rbp+0xdb0]  // [1]
0x00007fbe4b925187 <+71>:    mov    ebx,r13d
0x00007fbe4b92518a <+74>:    movsxd rdi,r14d
0x00007fbe4b92518d <+77>:    sub    ebx,r14d
0x00007fbe4b925190 <+80>:    sub    eax,esi  // [2]
0x00007fbe4b925192 <+82>:    cmp    ebx,eax
=> 0x00007fbe4b925194 <+84>:    cmovg  ebx,eax  // [3]
0x00007fbe4b925197 <+87>:    add    rdi,r15
0x00007fbe4b92519a <+90>:    movsxd r12,ebx // [4]
0x00007fbe4b92519d <+93>:    add    r14d,ebx
0x00007fbe4b9251a0 <+96>:    mov    rdx,r12
0x00007fbe4b9251a3 <+99>:    call   0x7fbe4b8356c0 <[email protected]>

It should be noted that an integer underflow can occur when calculating the distance between IGRStream::blockBuf and IGRStream::blockBufEnd, which bypasses the cmovg check with 0x1000, forcing the size of the memcopy to be larger than the static buffer of 0x1000 bytes, resulting in a stack-based overflow.

Crash Information

RAX: 0x7ffe879972c0 --> 0x0 
RBX: 0xd8f0047d 
RCX: 0x200 
RDX: 0xffffffffd8f0047d 
RSI: 0xf1093ede1d170e06 
RDI: 0x7ffe879972c0 --> 0x0 
RBP: 0xff2130 --> 0x7f9b7dd76870 --> 0x7f9b7dab2d10 --> 0x5c8948f8246c8948 
RSP: 0x7ffe87997278 --> 0x7f9b7dab91a8 --> 0x4500000da8a5014c 
RIP: 0x7f9b809964a0 --> 0x48f88949066f0ff3 
R8 : 0x1059a80 --> 0xe6166ad5556c558e 
R9 : 0x104d670 --> 0xaa2a74d82e7a6c4f 
R10: 0x10 
R11: 0x1 
R12: 0xffffffffd8f0047d 
R13: 0x1000 
R14: 0xd8f0047d 
R15: 0x7ffe879972c0 --> 0x0
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
0x7f9b80996498 <__memmove_ssse3_back+72>:    lea    rdx,[r11+rdx*1]
0x7f9b8099649c <__memmove_ssse3_back+76>:    jmp    rdx
0x7f9b8099649e <__memmove_ssse3_back+78>:    ud2    
=> 0x7f9b809964a0 <__memmove_ssse3_back+80>:    movdqu xmm0,XMMWORD PTR [rsi]
0x7f9b809964a4 <__memmove_ssse3_back+84>:    mov    r8,rdi
0x7f9b809964a7 <__memmove_ssse3_back+87>:    and    rdi,0xfffffffffffffff0
0x7f9b809964ab <__memmove_ssse3_back+91>:    add    rdi,0x10
0x7f9b809964af <__memmove_ssse3_back+95>:    mov    r9,rdi
0000| 0x7ffe87997278 --> 0x7f9b7dab91a8 --> 0x4500000da8a5014c 
0008| 0x7ffe87997280 --> 0x7ffe879982c0 --> 0x8 
0016| 0x7ffe87997288 --> 0x7f9b7db18625 ("ydieresis")
0024| 0x7ffe87997290 --> 0xff1fa0 --> 0x0 
0032| 0x7ffe87997298 --> 0xfef940 --> 0x0 
0040| 0x7ffe879972a0 --> 0x7ffe879982c0 --> 0x8 
0048| 0x7ffe879972a8 --> 0x8 
0056| 0x7ffe879972b0 --> 0x7ffe879987cf --> 0xfec16001 
Legend: code, data, rodata, value
Stopped reason: SIGSEGV


2017-04-24 - Vendor Disclosure
2017-08-28 - Public Release


Discovered by Marcin Noga and Lilith Wyatt of Cisco Talos.