int $0x80

Attacking Javascript Engines - The Bug 본문

해킹공부/브라우져를 털자

Attacking Javascript Engines - The Bug

cd80 cd80 2017.01.04 21:20

오늘은 문서의 2장인 버그 자체에 대한 설명을 정리합니다



The Bug

우선 이 버그는 ECMA 표준에 정의 돼있는 Array.prototype.slice(start, end) 메소드의 JavaScriptCore 구현체에서 발생합니다.

JavaScriptCore에서는 Source/JavaScriptCore/runtime/ArrayPrototype.cpp 에 arrayProtoFuncSlice 라는 이름으로 구현돼있습니다


Array.prototype.slice는 php의 substring, python의 "asdf"[a:b] 등과 같이 배열이나 문자열을 slice하는 메소드 입니다


먼저 slice메소드를 사용하는 예시를 문서에 나온 그대로 보겠습니다

a.slice는

첫번째인자 <= N < 두번째 인자인 N번째 요소들의 집합을 만듭니다

그래서 1,3 으로 했을때

a[1], a[2] 두개인 [2,3]이라는 array가 만들어집니다




이 slice의 구현체를 봅시다

slice의 구현체는 

/root/browny/WebKit/320b1fc/webkit/Source/JavaScriptCore/runtime/ArrayPrototype.cpp

여기에 arrayProtoFuncSlice 라는 이름의 함수로 있습니다


아래는 문서에서 정리한 소스코드입니다

    // 1. Obtain the reference object for the method call
    JSObject* thisObj = exec->thisValue().toThis(exec, StrictMode).toObject(exec);
    if (!thisObj)
        return JSValue::encode(JSValue());


    // 2. Retrive the length of the array
    unsigned length = getLength(exec, thisObj);
    if (exec->hadException())
        return JSValue::encode(jsUndefined());

    // 3. Convert the arguments (start and end index) into native integer types and clamp them to the range [0, length)
    unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length);
    unsigned end = argumentClampedIndexFromStartOrEnd(exec, 1, length, length);

    // 4. Check if a species constructor should be used
    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());

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

1. this를 구합니다. 즉 a.slice에서 a를 구합니다

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

3. begin에는 0번째인자, end에는 2번째 인자를 넣습니다. argumentClampedIndexFromStartOrEnd는 a.slice(-2, 4)나 a.slice(1, 5) 등에 대해서도 정상 작동하게 구현돼있습니다

4. species라는건 상속관계를 뜻하는것 같습니다. 어레이의 요소들이 바꼈을 때 새로 생성자를 호출해야되는지를 체크하는거 같기도 하네요 잘 모르겠습니다

5. fast slice를 호출합니다 ( 이 취약점은 fast slice 에서만 발생합니다 )


fast slice에서만 취약점이 발생한 이유는 문서에서 설명하고 있는데

slow slice에서는 요소를 가져올 때 오브젝트에 getProperty를 호출하고 hadExecption을 체크하는데 여기서 바운드 체크를 한다고 합니다



argumentClampedIndexFromStartOrEnd 함수는 이름이 참 긴데

두번째인자 (위의 slice코드에서 begin은 0, end는 1로 설정된 그 값)으로 인자를 가져오고

그 값을 toInteger해서 native 자료형으로 변경합니다


함수 분석을 모두 적기는 제가 힘들어서 대충 쓰면

toInteger에서 toNumber를 호출합니다

toNumber에서는 toPrimitive와, toPrimitive에서 리턴된 오브젝트에서 toNumber를 호출합니다

toPrimitive에서는  callToPrimitiveFunction<TypeHintMode::TakesHint>(exec, this, exec->propertyNames().toPrimitiveSymbol, preferredType); 를 호출합니다


callToPrimitiveFunction에서는 오브젝트의 toPrimitive함수를 호출합니다

primitive라는 용어는 https://developer.mozilla.org/en/docs/Glossary/Primitive 여기에 잘 설명 돼있습니다



이런식으로 인자가 number뿐만 아니라 String, Boolean, Symbol 등 다른 primitive의 경우에도 가능하다면 숫자로 변환시켜줍니다

유연한 형변환을 위한 구현인 것 같습니다

예를 들어서 서버에서 정수를 받아와서 인자에 바로 넣었는데 이런 유연한 형변환을 지원하지 않는다면 

func(a, b); 라고 하면 될것을

func(int(a), int(b)); 와 같이 쓰게 됩니다

이런 편의성을 지원하기 위해 만들어진 개념 같습니다


근데 이런 기능들이 특정 primitive에만 한정돼서 유연하다면 결국 number만 허용하는것과 다를게 없겠죠

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf

이 문서에서는 valueOf라는 메소드를 이용해 값을 조작하는데,

위 링크에 잘 설명이 돼있습니다

한번 따라해보면


이렇게 값을 참조할때 코드를 실행시키게 할 수 있습니다

CVE-2016-4622는 바로 이런 기능을 이용해서 공격하는 취약점입니다



이렇게 간단하게도 사용이 가능합니다


문서에 나와있는걸 똑같이 해보면


이렇게 앞의 0.123, 1.123을 제외하면 원래 있던 값이 사라졌습니다

아무리 length를 0으로 설정했어도 slice에서 10을 리턴했기 때문에

똑같이 a가 있던곳에서 10개만큼을 출력해 줄텐데

뒤에 값이 사라진 이유는 array의 length를 0으로 설정하면 실제 메모리상에서도 줄이면서  realloc을 하기 때문입니다


이게 첫길이와 그다음길이가 64이상 차이가 나야하는데, 그이유는 문서에 쉽게 설명돼있습니다

64바이트 이상 차이가 나지 않으면 배열을 재할당하는것보다 배열 요소를 모두 지우고 그냥 쓰는게 낫다고 합니다



그런데 0.123, 1.123은 왜 남아있고 그뒤에부터 삭제돼있는지 알아봅시다

그걸 알아보기 위해선 a라는 변수가 어떻게 변했는지를 알아야겠죠

그래서 a.slice를 하기전과 후에 print(a)를 넣고, print함수의 구현체에 breakpoint를 걸어 확인해봅시다




섹션 5.2 디버깅 예제

우선 더 쉬운 예제가 섹션 5.2에 있습니다

이 문서 쓰신분이 디버깅 방법에 대해서는 좀 대충대충 넘어가셔갖고 혼자 삽질하면서 깨우친것들인데

우선 디버깅하기 제일 쉬운방법은 JavaScriptCore/jsc.cpp 에 있는 functionPrint 함수에다가 exec->argument(0); 을 %p로 찍는 코드를 집어넣고

lldb나 gdb에서 functionPrint에 브레이크포인트를 건다음

실행하다가 브포가 걸리면 fini로 함수를 실행시킵니다

그러면 인자의 주소가 나오는데 그걸 x/16gx로 보시면됩니다(거의 모든 데이터형이 8바이트기 때문에 g로 봅니다)



우선 jsc.cpp의 functionPrint에 디버깅코드 추가




디버깅 대상 오브젝트고, 저는 print함수에 브포를 걸어서 확인할꺼기 때문에

섹션 5.2에서는 선언만했지만 저는 print도 넣었습니다


gdb로 jsc열고 functionPrint에 브레이크포인트 걸음


실행하고 브포걸린 상태에서 fini로 함수를 실행하면

exec->argument(0), 즉 obj의 주소가 나옴



보면은 일단 첫 8바이트는 JSCell이라고 합니다 뭔지는 일단 저도 스킵하고

두번째가 Butterfly 포인터라고 합니다

근데 null로 설정 돼 있는 이유는 문서에서 설명하기로는 모든 프로퍼티들이 인라인으로 저장돼서라는데 먼소린진 모르겠다

근데 그다음 값들은 태그를 이해하고 있으면 이제 잘보인다

0xffff000000001337 == Integer 0x1337

0x0000000000000006 == Pointer ( False )

0x402bbd70a3d70a3d == 태그가 1~0xfffe니까 double,

0x00007fffb1fcfee0 == 태그가 pointer니까 포인터


double

여기서 유일하게 해석이 어려웠던게 double 이였는데

0x402bbd70a3d70a3d 가 double인건 알겠는데 

https://gregstoll.dyndns.org/~gregstoll/floattohex/ 여기서 converting하면 13.37이 아니라 13.87로 나옵니다

그래서 왜그런가 하다가 floating point 태그가 0x0001 부터 시작하니까 0x0001000000000000을 빼주면 되지 않을까 싶어서 빼니까 13.37이 잘 나왔습니다

그러니까

0x402bbd70a3d70a3d 으로 돼있으면

0x402abd70a3d70a3d 로 값을 읽어야합니다

// 아니 근데 빼야 제대로된 값이 나올때가 있고 안빼야 나올때가 있는데 대체 어떨때 빼야되는거지



array

float은 그렇고 우리는 지금 array를 보고 있기 때문에

0x7fffb1fcfee0을 봅시다

보면은 위에서 obj 자체를 봤을때는 butterfly pointer가 null이였는데 여기서는 값이 설정 돼있습니다

그 이유를 생각해보면 butterfly는 포인터 왼쪽에 property, 오른쪽에 elements를 가지는 구조인데

배열은 element의 나열이 필요하기 때문에 butterfly pointer가 설정돼있는 것 같습니다

그래서 확인해보면


array에 있었던 Integer형의 1,2,3,4가 들어있습니다

그럼 이제 우리는 array를 보는 방법도 압니다




문서에 나와있는 예제 코드 분석

그러면 이제 드디어 왜 0.123, 1.123은 남아있고 그다음은 이상한값, 그리고 그 뒤에는 0으로 채워져있었는지 확인해봅시다


먼저 디버깅을 위해 print를 추가한 코드입니다

첫 print에서 array의 주소를 확인하고

두번째 print에서 array의 주소를 다시 확인해봅니다



첫 print에서 본 배열입니다

배열 element들의 시작은 0x7fffb33e4148부터입니다



그다음 slice를 하면서 valueOf에서 length를 0으로 바꿔버렸고

그게 JSArray::setLength에서 정한 threshold보다 차이가 높기 때문에 reallocate가 발생해 배열의 시작주소가 0x7fffb33e4148 에서 0x7fffb33e4740으로 바꼈습니다

값들을 좀 살펴보면


이렇게 돼있고, 이 값은 print(b)에서 나오는 값과 똑같습니다

근데 여기에는 두번나오고 b에서는 한번나온다는점이 다른데, 그 다음 b는 어떻게 돼있나 보죠


우선은 아까 엄청 위에서 했었을때와 같은 값들이 출력이 됐구요

0.123, 1.123 이 있고 그다음 2.121995~~ 가 이제 무슨값이였는지 알게 됐습니다

이렇게 앞에 두개가 남아있는 이유는 아직은 알지 못하겠고

오늘은 여기까지만하고 내일 reallocateAndShrinkButterfly함수를 분석해봐야 알 것 같습니다

그냥 쉽게 생각할 수 있는건 slice의 첫 인자는 0으로 고정돼있으니까 0.123은 무조건 들어갈 거라고 예상할 수 있고, 1.123도 뭐 바로 다음이니까 들어갈 수 있긴 할텐데

정확한 코드 흐름을 내일 더 분석해봐야 알 것 같습니다


오늘은 여기까지

신고
0 Comments
댓글쓰기 폼