CVE-2017-2822
An exploitable code execution vulnerability exists in the image rendering functionality of Lexmark Perceptive Document Filters 11.3.0.2400. 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.
Lexmark Perceptive Document Filters 11.3.0.2400
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]
obj1.free();
return NULL;
}
buf = new GooString();
obj1.getStream()->fillGooString(buf); // [1]
obj1.streamClose();
obj1.free();
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 <memcpy@plt>
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.
[----------------------------------registers-----------------------------------]
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)
[-------------------------------------code-------------------------------------]
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
[------------------------------------stack-------------------------------------]
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.