0x00 摘要
本文基于saelo在 phrack上发表的Attacking JavaScript Engines一文,记录了调试的过程。
0x01 环境搭建
这个漏洞可以在OS X 10.11.5
上的safari 9.1.1
重现。
1 | [*] Setting up container object [*] Fake JSObject @ 0x000000010d81e180 [*] Float64Array structure ID found: 00001000 [<] Reading 8 bytes from 0x000000010d9da1c0 [>] Writing 8 bytes to 0x000000010d81e170 [<] Reading 16 bytes from 0x000000010d816580 [>] Writing 16 bytes to 0x000000010d81e180 [>] Writing 8 bytes to 0x000000010d81e198 [+] All done! |
但是通过lldb
直接调试safari
是没有webkit
相关的调试符号的。
1 | * thread #1: tid = 0x0a16, 0x00007fff90f9aac0 JavaScriptCore`JSC::arrayProtoFuncSlice(JSC::ExecState*), queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 frame #0: 0x00007fff90f9aac0 JavaScriptCore`JSC::arrayProtoFuncSlice(JSC::ExecState*) JavaScriptCore`JSC::arrayProtoFuncSlice: -> 0x7fff90f9aac0 <+0>: pushq %rbp 0x7fff90f9aac1 <+1>: movq %rsp, %rbp 0x7fff90f9aac4 <+4>: pushq %r15 0x7fff90f9aac6 <+6>: pushq %r14 (lldb) p (JSC::ExecState*) $rdi error: use of undeclared identifier 'JSC' error: expected expression |
苹果官方虽然开源了JavaScriptCore
的代码,但是没有编译脚本。
在不熟悉JSC
的情况下,想要对POC
进行调试的话需要下载webkit
,并编译存在相应漏洞的版本,就可以在有调试符号的情况下进行调试。具体的源码下载与编译参考WebKit官网的相关链接。
1.1 编译参数
Webkit
官方的编译脚本将所有的warning
视为error
,估计是我们调试漏洞的版本不是发布版本,所以存在一些warning
会导致编译失败,我的做法是手动在配置文件中关闭这个设置。具体方法是将WebKit
源码文件夹中所有的Base.xcconfig
中的GCC_TREAT_WARNINGS_AS_ERRORS
设置为NO
。
1 | GCC_TREAT_WARNINGS_AS_ERRORS = NO; |
1.2 漏洞版本
将代码版本切换到320b1fc
这个版本。
1 | 320b1fc 2016-05-03 Geoffrey Garen <ggaren@apple.com> |
切换到目标版本之后直接编译就可以了。
编译过程中在WebCore
的Link
时应该会有一个报错,修改源码处理一下就好了。
1.3 通过JSC调试
在WebKit/WebKitBuild/Debug
中会有一个jsc
,就是JavaScriptCore
,单独调试这个JavaScript
的引擎就会简单很多。
1 | ➜ Debug git:(CVE-2016-4622) ✗ export DYLD_FRAMEWORK_PATH=/Users/XXX/GitHub/WebKit/WebKitBuild/Debug ➜ Debug git:(CVE-2016-4622) ✗ lldb ./jsc ./poc.js (lldb) target create "./jsc" Current executable set to './jsc' (x86_64). (lldb) settings set -- target.run-args "./poc.js" (lldb) b arrayProtoFuncSlice Breakpoint 1: where = JavaScriptCore`JSC::arrayProtoFuncSlice(JSC::ExecState*), address = 0x00000000002a9640 (lldb) r Process 55066 launched: './jsc' (x86_64) 1 location added to breakpoint 1 Process 55066 stopped * thread #1: tid = 0x2606d6, 0x000000010034300f JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfd910) + 15 at ArrayPrototype.cpp:851, queue = 'com.apple.main-thread', stop reason = breakpoint 1.2 frame #0: 0x000000010034300f JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfd910) + 15 at ArrayPrototype.cpp:851 848 EncodedJSValue JSC_HOST_CALL arrayProtoFuncSlice(ExecState* exec) 849 { 850 // http://developer.netscape.com/docs/manuals/js/client/jsref/array.htm#1193713 or 15.4.4.10 -> 851 JSObject* thisObj = exec->thisValue().toThis(exec, StrictMode).toObject(exec); 852 if (!thisObj) 853 return JSValue::encode(JSValue()); 854 unsigned length = getLength(exec, thisObj); (lldb) |
这样就可以带符号的调试JSC
的漏洞了。
0x02 调试过程
2.1 JSC漏洞
漏洞的POC代码如下,
1 | var a = []; |
执行poc
脚本后可以发现漏洞确实存在。
1 | ➜ Debug git:(CVE-2016-4622) ✗ ./jsc ./poc.js 0.123,1.123,2.12199579146e-313,0,0,0,0,0,0,0 |
具体的漏洞原因在saelo的文章中已经说的很清楚了,大致流程如图所示,细节这里就不赘述了,只记录调试过程。
通过在这三个函数下断点,分析漏洞触发流程。
1 | (lldb) b arrayProtoFuncSlice Breakpoint 1: where = JavaScriptCore`JSC::arrayProtoFuncSlice(JSC::ExecState*), address = 0x00000000002a9640 (lldb) b JSArray::setLength Breakpoint 2: where = JavaScriptCore`JSC::JSArray::setLength(JSC::ExecState*, unsigned int, bool), address = 0x0000000000089170 (lldb )b ArrayPrototype.cpp:861 Breakpoint 3: where = JavaScriptCore`JSC::arrayProtoFuncSlice(JSC::ExecState*) + 211 at ArrayPrototype.cpp:861, address = 0x00000001003430d3 |
1 | Process 55532 stopped * thread #1: tid = 0x26497e, 0x0000000100343040 JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfd910) + 64 at ArrayPrototype.cpp:852, queue = 'com.apple.main-thread', stop reason = step over frame #0: 0x0000000100343040 JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfd910) + 64 at ArrayPrototype.cpp:852 849 { 850 // http://developer.netscape.com/docs/manuals/js/client/jsref/array.htm#1193713 or 15.4.4.10 851 JSObject* thisObj = exec->thisValue().toThis(exec, StrictMode).toObject(exec); -> 852 if (!thisObj) 853 return JSValue::encode(JSValue()); 854 unsigned length = getLength(exec, thisObj); 855 if (exec->hadException()) (lldb) p *thisObj (JSC::JSObject) $15 = { JSC::JSCell = { m_structureID = 77 m_indexingType = '\a' m_type = ObjectType m_flags = '\b' m_cellState = NewWhite } m_butterfly = { JSC::CopyBarrierBase = (m_value = 0x00000001069e4170) } } (lldb) memory read --format x --size 8 --count 10 0x00000001069e4170 0x1069e4170: 0x3fbf7ced916872b0 0x3ff1f7ced916872b 0x1069e4180: 0x4000fbe76c8b4396 0x4008fbe76c8b4396 0x1069e4190: 0x40107df3b645a1cb 0x40147df3b645a1cb 0x1069e41a0: 0x40187df3b645a1cb 0x401c7df3b645a1cb 0x1069e41b0: 0x40203ef9db22d0e5 0x40223ef9db22d0e5 |
通过对thisObj
的m_butterfly
的的内存可以看到浮点数存储在内存中相应的位置。
1 | Process 55532 stopped * thread #1: tid = 0x26497e, 0x0000000100343076 JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfd910) + 118 at ArrayPrototype.cpp:855, queue = 'com.apple.main-thread', stop reason = step over frame #0: 0x0000000100343076 JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfd910) + 118 at ArrayPrototype.cpp:855 852 if (!thisObj) 853 return JSValue::encode(JSValue()); 854 unsigned length = getLength(exec, thisObj); -> 855 if (exec->hadException()) 856 return JSValue::encode(jsUndefined()); 857 858 unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length); (lldb) p length (unsigned int) $17 = 100 |
数组长度是100
符合脚本执行的预期结果。
在执行这一行代码,获取slice
的第二个参数时会对a
执行setLength
函数。
1 | -> 859 unsigned end = argumentClampedIndexFromStartOrEnd(exec, 1, length, length); |
调用栈如下,
1 | * thread #1: tid = 0x26497e, 0x0000000100c36f81 JavaScriptCore`JSC::JSArray::setLength(this=0x0000000106fc7ed0, exec=0x00007fff5fbfd190, newLength=0, throwException=false) + 33 at JSArray.cpp:397, queue = 'com.apple.main-thread', stop reason = breakpoint 2.2 frame #0: 0x0000000100c36f81 JavaScriptCore`JSC::JSArray::setLength(this=0x0000000106fc7ed0, exec=0x00007fff5fbfd190, newLength=0, throwException=false) + 33 at JSArray.cpp:397 394 395 bool JSArray::setLength(ExecState* exec, unsigned newLength, bool throwException) 396 { -> 397 Butterfly* butterfly = m_butterfly.get(); 398 switch (indexingType()) { 399 case ArrayClass: 400 if (!newLength) (lldb) bt * thread #1: tid = 0x26497e, 0x0000000100c36f81 JavaScriptCore`JSC::JSArray::setLength(this=0x0000000106fc7ed0, exec=0x00007fff5fbfd190, newLength=0, throwException=false) + 33 at JSArray.cpp:397, queue = 'com.apple.main-thread', stop reason = breakpoint 2.2 * frame #0: 0x0000000100c36f81 JavaScriptCore`JSC::JSArray::setLength(this=0x0000000106fc7ed0, exec=0x00007fff5fbfd190, newLength=0, throwException=false) + 33 at JSArray.cpp:397 frame #1: 0x0000000100c362ef JavaScriptCore`JSC::JSArray::put(cell=0x0000000106fc7ed0, exec=0x00007fff5fbfd190, propertyName=PropertyName @ 0x00007fff5fbfced0, value=JSValue @ 0x00007fff5fbfcec8, slot=0x00007fff5fbfcf28) + 511 at JSArray.cpp:206 frame #2-#18 省略。。。 frame #19: 0x00000001003430d0 JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfd910) + 208 at ArrayPrototype.cpp:859 |
观察this
指针,
1 | (lldb) p *this (JSC::JSArray) $20 = { JSC::JSNonFinalObject = { JSC::JSObject = { JSC::JSCell = { m_structureID = 77 m_indexingType = '\a' m_type = ObjectType m_flags = '\b' m_cellState = NewWhite } m_butterfly = { JSC::CopyBarrierBase = (m_value = 0x00000001069e4170) } } } } |
可以看到这里的this
就是这钱的thisOBj
也就是脚本中的var a
。
1 | Process 55532 stopped * thread #1: tid = 0x26497e, 0x0000000100c37277 JavaScriptCore`JSC::JSArray::setLength(this=0x0000000106fc7ed0, exec=0x00007fff5fbfd190, newLength=0, throwException=false) + 791 at JSArray.cpp:434, queue = 'com.apple.main-thread', stop reason = step over frame #0: 0x0000000100c37277 JavaScriptCore`JSC::JSArray::setLength(this=0x0000000106fc7ed0, exec=0x00007fff5fbfd190, newLength=0, throwException=false) + 791 at JSArray.cpp:434 431 unsigned lengthToClear = butterfly->publicLength() - newLength; 432 unsigned costToAllocateNewButterfly = 64; // a heuristic. 433 if (lengthToClear > newLength && lengthToClear > costToAllocateNewButterfly) { -> 434 reallocateAndShrinkButterfly(exec->vm(), newLength); 435 return true; 436 } 437 |
在setLength
里面就会触发内存的Shrink
。
1 | * thread #1: tid = 0x26497e, 0x00000001003430d3 JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfd910) + 211 at ArrayPrototype.cpp:861, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1 frame #0: 0x00000001003430d3 JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfd910) + 211 at ArrayPrototype.cpp:861 858 unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length); 859 unsigned end = argumentClampedIndexFromStartOrEnd(exec, 1, length, length); 860 -> 861 std::pair<SpeciesConstructResult, JSObject*> speciesResult = speciesConstructArray(exec, thisObj, end - begin); 862 // We can only get an exception if we call some user function. 863 if (UNLIKELY(speciesResult.first == SpeciesConstructResult::Exception)) 864 return JSValue::encode(jsUndefined()); |
argumentClampedIndexFromStartOrEnd
执行完了之后,thisObj
的m_butterfly
的地址发生了变化。
1 | (JSC::JSObject) $26 = { JSC::JSCell = { m_structureID = 77 m_indexingType = '\a' m_type = ObjectType m_flags = '\b' m_cellState = NewWhite } m_butterfly = { JSC::CopyBarrierBase = (m_value = 0x00000001069e4768) } } (lldb) memory read --format x --size 8 --count 10 0x00000001069e4768 0x1069e4768: 0x3fbf7ced916872b0 0x3ff1f7ced916872b 0x1069e4778: 0x0000000000000000 0x0000000000000000 0x1069e4788: 0x0000000000000000 0x0000000000000000 0x1069e4798: 0x0000000000000000 0x0000000000000000 0x1069e47a8: 0x0000000000000000 0x0000000000000000 |
在fastSlice
的数据拷贝完成之后,再看一下thisObj
的m_bufferfly
。
1 | Process 55532 stopped * thread #1: tid = 0x26497e, 0x000000010034319a JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfd910) + 410 at ArrayPrototype.cpp:868, queue = 'com.apple.main-thread', stop reason = step over frame #0: 0x000000010034319a JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfd910) + 410 at ArrayPrototype.cpp:868 865 866 if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj))) { 867 if (JSArray* result = asArray(thisObj)->fastSlice(*exec, begin, end - begin)) -> 868 return JSValue::encode(result); 869 } 870 871 JSObject* result; (lldb) memory read --format x --size 8 --count 10 0x00000001069e4768 0x1069e4768: 0x3fbf7ced916872b0 0x3ff1f7ced916872b 0x1069e4778: 0x0000000a0000000a 0x3fbf7ced916872b0 0x1069e4788: 0x3ff1f7ced916872b 0x0000000a0000000a 0x1069e4798: 0x0000000000000000 0x0000000000000000 0x1069e47a8: 0x0000000000000000 0x0000000000000000 |
可以看到拷贝到了一些奇怪的东西,读过writeup就会知道,这是在存储区域相邻的一个object
的数据。
0x03 小结
理解了漏洞的成因之后,writeup中介绍更高级的利用技巧。
- 地址泄露
- 伪造object
- 任意地址读写
理解了本文的内容,读者自行调试后面的利用技巧并不困难。这里就详细记录了。
引用
[1] http://phrack.org/papers/attacking_javascript_engines.html
[2] https://webkit.org/building-webkit/
[3] https://webkit.org/blog/6411/javascriptcore-csi-a-crash-site-investigation-story/
PS
Thanks to saelo for answering my doubts :)