CVE: CVE-2019-9133

Tested Versions:

  • KMPlayer 4.2.2.12 KMP Plus

Product URL(s):

Description of the vulnerability

K-Multimedia Player (KMPlayer) is a media player for Windows which can play a large number of formats including VCD, DVD, AVI, MKV, Ogg, OGM, 3GP, MPEG-1/2/4, AAC, WMA 7, 8, WMV, RealMedia, FLV and QuickTime. When processing .sup files, KMPlayer doesn’t check the Object size correctly, which leads to integer overflow then to memory out-of-bound read.

Technical Details

Crash Context

0:026> g
(4018.340c): Access violation - code c0000005 (!!! second chance !!!)
eax=013822fc ebx=03f2fe34 ecx=fff032fc edx=fffffffc esi=0147f000 edi=0dd25eed
eip=69f897be esp=03f2fdf0 ebp=03f2fe14 iopl=0         nv up ei ng nz na po cy
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010283
utils!src_new+0x14d6fe:
69f897be f3a4            rep movs byte ptr es:[edi],byte ptr [esi]

utils.dll Base Address:0x69D40000

There is a DLL named utils.dll in the installation folder of the KMPlayer. When a subtitles file is open (manually or automatically), KMPlayer will use an API of this module to detect the type of the subtitles and process it with a corresponding function. This DLL has several export functions, one of them is OpenSubtitleByFile, this function detects the subtitles type by guessing from the file extension, then by its content. The program will try to parse the subtitles file with the parser of every subtitles type which is supported.

The decompiled function for handling .sup files is as follows:

int __thiscall CPGSSubFile::ParseFile(_BYTE *this, LPCWSTR lpFileName)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  CPGSSubFile = this;
  v29 = 0;
  PathUtils::Exists(fd);
  LOBYTE(v29) = 1;
  if ( open(fd, lpFileName, 32, 0) )
  {
    v19 = 0;
    v20 = 0;
    v21 = 0;
    LOBYTE(v29) = 2;
    while ( !CPGSSubFile->m_bStopParsing )
    {
      if ( read(fd, &Buffer, 0xDu) != 0xD )
        break;
      header_size = 0xD;
      headerBuffer = &Buffer;
      v15 = 0;
      v16 = 0;
      v17 = 0;
      v18 = 0;
      if ( read_a2_bit_from_a1_buffer(&headerBuffer, 0x10u, v3) != 'PG' )
        break;
      rtStart = 1000i64 * read_a2_bit_from_a1_buffer(&headerBuffer, 0x20u, v4) / 9;
      rtStop = 1000i64 * read_a2_bit_from_a1_buffer(&headerBuffer, 0x20u, v5) / 9;
      read_a2_bit_from_a1_buffer(&headerBuffer, 8u, v6);
      wLenSegment_ = read_a2_bit_from_a1_buffer(&headerBuffer, 0x10u, v7);
      wLenSegment = wLenSegment_;
      v25 = wLenSegment_;
      nLenData = wLenSegment_ + 3;
      std::vector(&v19, nLenData);
      segBuff = v19;
      *v19 = v27;
      *(segBuff + 2) = v28;
      if ( wLenSegment )
      {
        if ( read(fd, (segBuff + 3), wLenSegment) != wLenSegment )
          break;
      }
      CPGSSubFile->vtable->ParseSample(
        CPGSSubFile,
        rtStart,
        HIDWORD(rtStart),
        rtStop,
        HIDWORD(rtStop),
        segBuff,
        nLenData);
    }
    CPGSSubFile->byte62670 = 1;
    sub_6C642290(&v19);
  }
  sub_6C6F4A94(fd);
  v29 = -1;
  result = _InterlockedDecrement(lpFileName - 1);
  if ( result <= 0 )
    result = (*(**(lpFileName - 4) + 4))(lpFileName - 8);
  return result;
}

Note that I have declared the function name CPGSSubFile::ParseFile because the original binary was stripped, the offset of the function is 0xc6c20.

Looking into CPGSSubFile->vtable->ParseSample function:

signed int __thiscall PGS::ParseSample(struct_pObject *this, int a2, size_t a3, int a4, int a5, int a6, unsigned int a7)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  if ( !a6 )
    return -2147467261;
  v36 = &this->CPGSSub:OBJECT[24];
  EnterCriticalSection(&this->CPGSSub:OBJECT[24]);
  v37 = 0;
  v10 = 0;
  v30 = a6;
  v11 = a7;
  v38 = a7;
  v31 = a7;
  v32 = 0;
  v33 = 0;
  v34 = 0;
  v35 = 0;
  if ( a7 )
  {
    while ( *&this->CPGSSub:OBJECT[64] != 0xFFFF )
    {
LABEL_12:
      v16 = *&this->CPGSSub:OBJECT[76];
      v17 = *&this->CPGSSub:OBJECT[80];
      if ( v16 < v17 )
      {
        v18 = v17 - v16;
        if ( v38 < v18 )
          v18 = v38;
        v19 = v11 - v10;
        v39 = v18;
        v20 = v18;
        if ( v19 < v18 )
          v20 = v19;
        memmove_0((*&this->CPGSSub:OBJECT[76] + *&this->CPGSSub:OBJECT[68]), (v10 + v30), v20);
        v10 = v20 + v32;
        *&this->CPGSSub:OBJECT[76] += v39;
        v32 = v10;
      }
      m_nSegSize = *&this->CPGSSub:OBJECT[80];
      if ( *&this->CPGSSub:OBJECT[76] >= m_nSegSize )
      {
        pGBuffer = *&this->CPGSSub:OBJECT[68];
        m_nCurSegment = *&this->CPGSSub:OBJECT[64];
        v25 = m_nSegSize;
        v26 = 0;
        v27 = 0;
        v28 = 0;
        v29 = 0;
        switch ( m_nCurSegment )
        {
          case 0x14:
            ParsePalette(this->CPGSSub:OBJECT, &pGBuffer, m_nSegSize);
            goto LABEL_31;
          case 0x15:
            ParseObject(this->CPGSSub:OBJECT, &pGBuffer, m_nSegSize);
            goto LABEL_31;
          case 0x16:
            m_nSegSize = a3;
            if ( !a2 && a3 == 0x80000000 )
              break;
            if ( this->m_width )
            {
              v22 = *&this->gap56[6];
              if ( !v22 )
                error(-2147467259);
              v23 = *(v22 + 8);
              if ( *(v23 + 8) == -1 && *(v23 + 12) == 0x7FFFFFFF )
              {
                *(v23 + 8) = a2;
                *(v23 + 12) = a3;
              }
            }
            ParsePresentationSegment(this, a2, a3, &pGBuffer);
            goto LABEL_31;
          case 0x80:
            EnqueuePresentationSegment(this);
LABEL_31:
            v10 = v32;
            break;
          default:
            break;
        }
        *&this->CPGSSub:OBJECT[64] = 0xFFFF;
      }
LABEL_33:
      v11 = v31;
      if ( v10 >= v31 )
        goto LABEL_34;
    }
    v12 = read_a2_bit_from_a1_buffer(&v30, 8u, m_nSegSize);
    v14 = read_a2_bit_from_a1_buffer(&v30, 0x10u, v13);
    v38 -= 3;
    m_nSegSize = v14;
    switch ( v12 )
    {
      case 0x14:
      case 0x15:
      case 0x16:
      case 0x80:
        *&this->CPGSSub:OBJECT[64] = v12;
        v15 = v14;
        if ( v14 > *&this->CPGSSub:OBJECT[72] )
        {
          j_j___free_base(*&this->CPGSSub:OBJECT[68]);
          *&this->CPGSSub:OBJECT[68] = mem_alloc_helper(v15);
          *&this->CPGSSub:OBJECT[72] = v15;
        }
        v10 = v32;
        *&this->CPGSSub:OBJECT[76] = 0;
        *&this->CPGSSub:OBJECT[80] = v15;
        goto LABEL_10;
      case 0x17:
      case 0x18:
      case 0x81:
      case 0x82:
        v10 = v14 + v32;
        v33 = 0;
        v32 += v14;
        v34 = 0;
        v35 = 0;
LABEL_10:
        if ( *&this->CPGSSub:OBJECT[64] == 0xFFFF )
          goto LABEL_33;
        v11 = v31;
        goto LABEL_12;
      default:
        LeaveCriticalSection(&this->CPGSSub:OBJECT[24]);
        result = -2147220949;
        break;
    }
  }
  else
  {
LABEL_34:
    LeaveCriticalSection(&this->CPGSSub:OBJECT[24]);
    result = 0;
  }
  return result;
}

The bug occurs in ParseObject(this->CPGSSub:OBJECT, &pGBuffer, m_nSegSize) function, where the program set or append data to a RLE Object in the Object list:

size_t __thiscall ParseObject(char *this, void *pGBuffer, size_t nUnitSize)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  m_compositionObjects = this;
  i = read_a2_bit_from_a1_buffer(pGBuffer, 16u, this);
  if ( (i & 0x8000u) == 0 && i < 0x40u )
  {
    pObject = &m_compositionObjects[1160 * i + 0x50470];
    pObject->m_version_number = read_a2_bit_from_a1_buffer(pGBuffer, 8u, i);
    if ( (read_a2_bit_from_a1_buffer(pGBuffer, 8u, v6) & 0x80u) == 0 )// Append Data
    {
      pos = pObject->position;
      i = pos + nUnitSize - 4;
      if ( i <= pObject->buffer_size )
      {
        i = memmove_0(pObject->buffer + pos, (*pGBuffer + *(pGBuffer + 2)), nUnitSize - 4);
        pObject->position += nUnitSize - 4;
      }
    }
    else                                        // Set Data
    {
      object_data_length = read_a2_bit_from_a1_buffer(pGBuffer, 0x18u, v7);
      width = read_a2_bit_from_a1_buffer(pGBuffer, 16u, v9);
      pObject->m_width = width;
      height = read_a2_bit_from_a1_buffer(pGBuffer, 16u, width);
      v12 = pObject->buffer;
      nUnitSizea = nUnitSize - 11;
      pObject->m_height = height;
      pGBuffera = (*pGBuffer + *(pGBuffer + 2));
      i = j_j___free_base(v12);
      v13 = object_data_length - 4;
      if ( v13 && nUnitSizea <= v13 )
      {
        buffer = mem_alloc_helper(v13);
        pObject->buffer = buffer;
        pObject->buffer_size = v13;
        pObject->position = nUnitSizea;
        i = memmove_0(buffer, pGBuffera, nUnitSizea);
      }
      else
      {
        pObject->buffer = 0;
        pObject->position = 0;
        pObject->buffer_size = 0;
      }
    }
  }
  return i;
}

The pGBuffer array is the content of the subtitles file, so we totally control the array value. When appending data to an existed object, the following check is performed:

      pos = pObject->position;
      i = pos + nUnitSize - 4;
      if ( i <= pObject->buffer_size )
      {
        i = memmove_0(pObject->buffer + pos, (*pGBuffer + *(pGBuffer + 2)), nUnitSize - 4);
        pObject->position += nUnitSize - 4;
      }

It checks if the new position after the appending is larger than the size of the object, but the size of the new data is our choice then we can pick a number that overflow the expression pos + nUnitSize - 4 and bypass the buffer_size check:

  • The pos variable is the current position of the project that initialized by the size of the segment.
  • The nUnitSize variable is from the subtitles content, so we can set this variable to a value smaller than 4, then the result of pos + nUnitSize - 4 is pretty small and pass the check, but the size when do the memmove is result of nUnitSize - 4, its so huge and crash the program.

Timeline:

  • 2019-03-07 Vendor disclosure via KrCERT
  • 2019-03-08 Vendor acknowledged, via KrCERT
  • 2019-03-28 Vendor patched

Vendor Response

The vendor has acknowledged the issue and released an update to address it.