Talos Vulnerability Report

TALOS-2017-0321

Poppler PDF library JPEG 2000 levels Code Execution Vulnerability

July 7, 2017
CVE Number

CVE-2017-2820

Summary

An exploitable integer overflow vulnerability exists in the JPEG 2000 image parsing functionality of freedesktop.org Poppler 0.53.0. A specially crafted PDF file can lead to an integer overflow causing out of bounds memory overwrite on the heap resulting in potential arbitrary code execution. To trigger this vulnerability, a victim must open the malicious PDF in an application using this library.

Tested Versions

Poppler 0.53

Product URLs

https://poppler.freedesktop.org/

CVSSv3 Score

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

CWE

CWE-190: Integer Overflow or Wraparound

Details

Poppler is a popular open source PDF parser library. It is used by default in many open source PDF viewers. The library itself implements a decoder for JPEG 2000 encoded images instead of relying on a more complete implementation (such as OpenJPEG), although it does warn about this at compile time and strongly suggests OpenJPEG be used. By default, this internal implementation will be used by applications. That is the case with libpoppler binary shipped by latest Ubuntu version which is used by the default PDF viewer, Evince.

When processing a PDF file with an embedded JPEG 2000 image (specified with a JPXDecode stream) inside, the JPXStream.cc source file will be used to render the image. Eventually, the method readTilePart will be invoked and it will process the image tile parts according to data in the SOT , SIZ and COD elements. When multiple levels are specified inside a COD element, the code will loop that many times starting at line 1961:

for (r = 0; r <= tileComp->nDecompLevels; ++r) {
    resLevel = &tileComp->resLevels[r];
    k = r == 0 ? tileComp->nDecompLevels
                         : tileComp->nDecompLevels - r + 1;
    resLevel->x0 = jpxCeilDivPow2(tileComp->x0, k);
    resLevel->y0 = jpxCeilDivPow2(tileComp->y0, k);
    resLevel->x1 = jpxCeilDivPow2(tileComp->x1, k);
    resLevel->y1 = jpxCeilDivPow2(tileComp->y1, k);
    if (r == 0) {
        resLevel->bx0[0] = resLevel->x0;
        resLevel->by0[0] = resLevel->y0;
        resLevel->bx1[0] = resLevel->x1;
        resLevel->by1[0] = resLevel->y1;
    } else {
        resLevel->bx0[0] = jpxCeilDivPow2(tileComp->x0 - (1 << (k-1)), k); 
        resLevel->by0[0] = resLevel->y0;
        resLevel->bx1[0] = jpxCeilDivPow2(tileComp->x1 - (1 << (k-1)), k); [1]
        resLevel->by1[0] = resLevel->y1;
        resLevel->bx0[1] = resLevel->x0;
        resLevel->by0[1] = jpxCeilDivPow2(tileComp->y0 - (1 << (k-1)), k);
        resLevel->bx1[1] = resLevel->x1;
        resLevel->by1[1] = jpxCeilDivPow2(tileComp->y1 - (1 << (k-1)), k);
        resLevel->bx0[2] = jpxCeilDivPow2(tileComp->x0 - (1 << (k-1)), k);

In the above excerpt, it can be observed that zeroth level will be processed in one way, where the rest is processed in a different way involving more arithmetic. The first integer overflow can happen at [1] (and other lines, but we’ll use this one for example). Specifically, if the value tileComp->x1 is less than 2 to the power of current level being processed, the tileComp->x1 - (1 << (k-1)) can result in a large value, due to integer overflow. Then, when jpxCeilDivPow2 macro is executed, in almost all cases, this will result in 0, but a single value of K lets the overflow persist after jpxCeilDivPow2 call, resulting in a large positive result of the division, instead of 0. This later leads to out of bounds heap access.

subband->x1 = resLevel->bx1[sb]; [2]
subband->y1 = resLevel->by1[sb];
subband->nXCBs = jpxCeilDivPow2(subband->x1,                [3]
                                                                tileComp->codeBlockW)
                                 - jpxFloorDivPow2(subband->x0,
                                                                     tileComp->codeBlockW);

In the above code, when sb is equal to 0, the overflown value from the previous calculation will end up in subband->x1 at [2] and will figure into the calculation at [3], effectively setting subband->nXCBs to a non-zero value. This further leads to a loop being entered when it shouldn’t, leading to further corruption:

for (cbY = 0; cbY < subband->nYCBs; ++cbY) {
    for (cbX = 0; cbX < subband->nXCBs; ++cbX) {        [4]        
        cb->x0 = (sbx0 + cbX) << tileComp->codeBlockW;
        cb->x1 = cb->x0 + tileComp->cbW;
…
…

        for (cbj = 0; cbj < cb->y1 - cb->y0; ++cbj) {
            for (cbi = 0; cbi < cb->x1 - cb->x0; ++cbi) {
                cb->coeffs[cbj * tileComp->w + cbi] = 0;        [5]
            }

        memset(cb->touched, 0,
                     (1 << (tileComp->codeBlockW + tileComp->codeBlockH)));
        cb->arithDecoder = NULL;
        cb->stats = NULL;
        ++cb;

Because subband->nXCBs is positive, a loop at [4] will be entered, ultimately leading to an out of bounds write at [5]. Most of the indices and offsets that figure in the above code come directly from the JPEG 2000 file giving control over the out of bounds write and leaving space for further memory manipulation.

This vulnerability can be triggered with poppler PDF utilities if the library is built to use the internal JPX decoder. As previously mentioned, the official binaries shipped with latest Ubuntu distribution use this decoder, so the vulnerability can be triggered through the evince-thumbnailer application. This means that in order to trigger the vulnerability, it is enough for the victim to view the directory where the malicious file is located.

Crash Information

Valgrind output:

==11527== Invalid write of size 4
==11527==    at 0xFC2DD8C: JPXStream::readTilePart() (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xFC2F0B6: JPXStream::readCodestream(unsigned int) (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xFC312D4: JPXStream::readBoxes() (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xFC31715: JPXStream::reset() (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xF59C4A1: CairoOutputDev::drawImage(GfxState*, Object*, Stream*, int, int, GfxImageColorMap*, bool, int*, bool) (in /usr/lib/x86_64-linux-gnu/libpoppler-glib.so.  8.7.0)
==11527==    by 0xFC7A389: Gfx::doImage(Object*, Stream*, bool) (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xFC7B6A7: Gfx::opXObject(Object*, int) (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xFC7597D: Gfx::go(bool) (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xFC75E1F: Gfx::display(Object*, bool) (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xFCBBF44: Page::displaySlice(OutputDev*, double, double, int, bool, bool, int, int, int, int, bool, bool (*)(void*), void*, bool (*)(Annot*, void*), void*, bool) (in /usr/lib/x86_64-linux-gnu/libpoppler.so.   
58.0.0)
==11527==    by 0xF584791: _poppler_page_render(_PopplerPage*, _cairo*, bool, PopplerPrintFlags) (in /usr/lib/x86_64-linux-gnu/libpoppler-glib.so.8.7.0)
==11527==    by 0xF355400: ??? (in /usr/lib/x86_64-linux-gnu/evince/4/backends/libpdfdocument.so)
==11527==  Address 0x13f6efd4 is 0 bytes after a block of size 4 alloc'd
==11527==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==11527==    by 0xFD040CE: gmallocn (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xFC2D9C9: JPXStream::readTilePart() (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xFC2F0B6: JPXStream::readCodestream(unsigned int) (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xFC312D4: JPXStream::readBoxes() (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xFC31715: JPXStream::reset() (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xF59C4A1: CairoOutputDev::drawImage(GfxState*, Object*, Stream*, int, int, GfxImageColorMap*, bool, int*, bool) (in /usr/lib/x86_64-linux-gnu/
libpoppler-glib.so.8.7.0)
==11527==    by 0xFC7A389: Gfx::doImage(Object*, Stream*, bool) (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xFC7B6A7: Gfx::opXObject(Object*, int) (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xFC7597D: Gfx::go(bool) (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xFC75E1F: Gfx::display(Object*, bool) (in /usr/lib/x86_64-linux-gnu/libpoppler.so.58.0.0)
==11527==    by 0xFCBBF44: Page::displaySlice(OutputDev*, double, double, int, bool, bool, int, int, int, int, bool, bool (*)(void*), void*, bool (*)(Annot*, void*), 
void*, bool) (in /usr/lib/x86_64-linux-gnu/
libpoppler.so.58.0.0)
==11527==
==11527==
==11527== HEAP SUMMARY:
==11527==     in use at exit: 604,867 bytes in 6,752 blocks
==11527==   total heap usage: 206,871 allocs, 200,119 frees, 188,964,491 bytes allocated
==11527==
=11527== LEAK SUMMARY:
==11527==    definitely lost: 0 bytes in 0 blocks
==11527==    indirectly lost: 0 bytes in 0 blocks
==11527==      possibly lost: 2,024 bytes in 25 blocks
==11527==    still reachable: 598,779 bytes in 6,699 blocks
==11527==                       of which reachable via heuristic:
==11527==                         length64           : 3,752 bytes in 29 blocks
==11527==                         newarray           : 1,920 bytes in 40 blocks
==11527==         suppressed: 0 bytes in 0 blocks
==11527== Rerun with --leak-check=full to see details of leaked memory
==11527==
==11527== For counts of detected and suppressed errors, rerun with: -v
==11527== ERROR SUMMARY: 3 errors from 1 contexts (suppressed: 0 from 0)

Mitigation

Mitigation for this vulnerability can involve making sure that the library is compiled to use OpenJPEG library instead of its internal parser.

Timeline

2017-05-16 - Vendor Disclosure
2017-07-07 - Public Release

Credit

Discovered by Aleksandar Nikolic of Cisco Talos.