-
V8 Sandbox - High-level Design (Kor)Etc 2025. 3. 18. 16:09
소개
이 글은 V8 Sandbox 프로젝트의 일부이자 Sandbox의 전반적인 설계를 다루는 이 글을 이해한 바에 따라 번역한 내용을 다룹니다.
한국어를 사용하는 취약점 연구자들에게 조그마한 도움이 되길 바랍니다.
요약
목표
- 낮은 OverHead를 지니는 V8용 In-process sandbox의 구축
개발 동기
V8의 Bug는 일반적으로 비정상적인 수준의 강력하고 안정적인 Exploit을 구축할 수 있게끔 한다. 게다가 이러한 Bug는 Memory-Safe language나 MTE나 CFI와 같이 Hardware 수준에서 지원하는 보안 기능으로도 완화될 가능성이 낮다. 따라서 이를 고려할 때 V8은 Real-World에 존재하는 공격자들에게 높은 매력을 지닌 공격 표적이다.
설계
이 문서의 제안은 공격자가 JavaScript 객체가 존재하는 V8 heap의 Memory를 임의로 손상시킬 수 있다고 가정한다. 또한 앞서 언급한 악용에 사용되는 Primitive는 일반적인 V8 취약점에서 구성될 수 있음을 가정한다. 동일한 Process 내의 타 Memory를 공격자가 가하는 손상으로부터 보호하고, 더 나아가 임의 코드 실행을 방지하기 위하여 V8 heap은 가상 주소 공간의 사전 예약된 영역인 Sandbox로 이동된다. 이후 V8이 수행하는 모든 Memory Access는 참조 객체에 대한 Raw pointer를 사용하는 대신 Offset을 사용하는 등 그 대상이 Sandbox 내의 주소 공간으로 제한되거나, 혹은 어떠한 방식으로든 유효성을 검사받아야 한다. (예: Pointer Table 간접 사용) 특히 V8 heap에서의 64bit Full-pointer는 그 사용이 완전히 배제되어야만 한다.
목표
V8 취약점의 악용에 성공하여 V8 heap 내부의 객체를 손상시킬 수 있는 공격자가 Process 내 타 Memory를 손상시켜 임의코드실행을 달성하는 것을 방지하기 위해 V8용 In-process sandbox를 구축한다. 본질적으로 이는 V8의 취약점으로 인해 발생하는 arbitrary write를 V8 Sandbox 경계 내의 write로 전환한다. 허나 V8 heap 외부에의 직접적인, 혹은 Speculative한 Read를 방지하는 것은 V8 Sandbox의 목표가 아니다. Performance Overhead는 최소화되어야 하며, 실제 Workload에서 전체적으로 약 1%를 대략적인 목표로 하여야만 한다. 이 Sandbox는 결국 안정적으로 지원되는 보안 경계가 되는 것을 목표로 한다.
동기
실제 공격자가 악용하는 많은 수의 V8 취약점은 사실상 2nd order의 범주에 속한다. 근본적으로 이러한 취약점의 원인은 꽤 높은 빈도로 Just-In-time compiler (JIT compiler) 중 하나의 Logic이며, 이를 악용하여 Runtime에서의 Safety check가 누락된 경우와 같은 취약한 Machine code를 생성하는 데에 악용할 수 있다. 이와 같이 생성된 Machine code는 Runtime에 Memory corruption을 일으키는 데에 악용될 수 있으나, Dynamic language용 JIT compiler의 주요한 목적 중 하나가 Interpreter가 수행하는 Runtime check에 대해 redundant-elimination하는 것이기에 이러한 취약점은 자연스레 발생하는 것으로도 볼 수 있다.
예를 들어, 일반적인 JIT compiler incorrect side effect modeling vulnerability의 경우를 따져보자. 이 경우에서 공격자는 Compiler로 하여금 연산의 부작용을 잘못 Modeling한 다음, 해당 연산 이후 Runtime Type Check가 부재한 Machine code를 출력하도록 속일 수 있다 (이는 Compiler가 연산 중 객체의 유형을 변경할 수 없다는 것을 신뢰하기 때문이다.) 따라서 Compiler에 의해 출력된 Machine code는 Type Confusion에 취약해지며, 공격자는 이를 악용하여 Runtime에 Memory corruption을 발생시킬 수 있다. 이렇듯 Type에 관련한 문제는 하기에서 확인할 수 있는 몇가지 이유로 인해 공격자에게 있어 매우 매력적이다.
- Type 관련 문제가 존재할 경우 공격자는 Memory corruption 관련 Primitive에 대한 상당한 수준의 제어권을 지니게 되며, 종종 이와 같은 Bug를 매우 빠르게 안정적으로 동작하는 Exploit으로 가공할 수 있다.
- 이와 같은 문제는 근본적으로 Logic bug이기에 Memory safe language로 보호할 수 없다.
- CPU Side channel과 V8 취약점이 지니는 위력으로 인해 Memory tagging과 같은 하드웨어 보안 기능은 대부분 우회 가능할 것으로 보인다.
상술한 취약점의 특성과 JavaScript engine의 고유성으로 인해 V8에 대한 맞춤형 Sandboxing mechanism을 구축하는 것이 바람직해보인다.
공격자 모델 (Attacker Model)
이 제안은 공격자가 V8 heap 내부에서 Arbitrary reads / Arbitrary write를 반복적으로 수행할 수 있을 뿐만 아니라 Speculative side channel attack 등으로 V8 heap 외부에서 Arbitrary reads를 수행할 수 있다고 가정한다, 이는 JIT compiler, Runtime, Garbage Collector에서 발생해온 많은 V8 취약점으로부터 획득된 통상적이고 일반적인 초기 Exploit Primitive를 반영한 결과이다. V8 Heap 외부에서 Memory corruption을 발생시키는 것은 이 Sandbox에서 탈출하는 것으로 간주되며, 이에는 arbitrary code execution 또한 포함된다.
디자인 (Design)
2020년 초부터 V8은 Heap에 Pointer compression을 구현했다. Pointer compression을 사용하면 V8 heap의 객체에서 타 객체 ("On-Heap")로의 모든 참조는 Heap의 Base에 기준하는 32bit offset가 되며, V8 heap 외부의 객체에 대한 Raw pointer를 가진 객체 ("Off-Heap")은 소수만 남게 된다. Compressed pointer는 'Pointer Compression Cage'라고 하는 4GB 크기의 가상 메모리 영역 내에서만 유효하다. Pointer compression은 메모리 상의 ArrayBuffer instance로 시각화할 수 있다. 하기 <image 1>는 Pointer compression이 적용되지 않은 임의 ArrayBuffer 객체의 In-memory layout이다.
<Image 1>. Pointer compression이 적용되지 않은 임의 ArrayBuffer 객체의 In-Memory layout 상기 이미지에 대한 상세 사항은 하기와 같다.
- Map (pink) : 64 bit on-heap pointer
- JS Properties array (blue) : 64 bit on-heap pointer
- JS Elements array (red) : 64 bit on-heap pointer
- Size in bytes (magenta) : 64 bit unsigned integer
- Backing storage (purple) : 64 bit off-heap pointer
- ArrayBufferExtension(orange), 64 bit off-heap pointer
Pointer compression을 활성화하면 <Image 1>의 것과 동일한 ArrayBuffer 객체는 하기 <Image 2>와 같은 In-memory layout을 지닌다.
<Image 2>. Pointer compression이 적용된 임의 ArrayBuffer 객체의 In-Memory layout Pointer compression이 적용된 <Image 2>에서는 <Image 1>과 달리 모든 On-Heap Pointer가 32bit compressed pointer로 변환된 것을 확인할 수 있다. <Image 1>과 <Image 2>에서 Heap base address는 '0x250700000000' 이므로 Compressed map pointer(0x08281181)는 Absolute pointer (Non-Compressed map pointer) 기준 '0x250708281181'로 해석된다.
대부분의 V8 취약점은 V8의 Heap을 대상으로 하는 Memory corruption (Out-Of-Bound access, Type Confusion 등)을 발생시키는 행위에 악용될 수 있다. Pointer compression이 적용되지 않은 환경에서 V8 heap의 data를 손상시킬 수 있는 공격자는 Pointer compression이 활성화된 경우 Compressed pointer를 공격하게 됨으로써 충분한 capabilities를 획득할 수 없다. 대신 공격자는 V8 heap 외부에 도달하기 위해 heap에 잔존해있는 raw pointer 중 하나 (일반적으로 ArrayBuffer 또는 TypeArray backing store pointer)를 표적으로 삼는다. 이 프로젝트의 근간에 있는 아이디어는 공격자에 의한 악용을 방지하고자 하는 목적 하에 모든 Raw pointer를 보호("Sandboxify")하는 것이다. 이를 고수준에서 달성하는 방법은 하기와 같다.
- V8을 초기화하는 동안 가상 주소 공간의 넓은 영역 (예: 1TB)를 Sandbox를 위해 예약한다. 이 영역에는 Pointer Compression Cage가 포함되어있으므로 모든 V8 heap과 ArrayBuffer backing store 및 이와 유사한 객체가 포함된다.
- Sandbox 내부에 있으나 V8 heap 외부에 있는 모든 객체는 Raw pointer 대신 고정 크기를 지니는 offset (예: 1TB Sandbox의 경우 40bit offset)을 사용하여 주소를 지정한다.
- 위의 경우에 속하지 않는 모든 Off-heap 객체는 Type Confusion의 악용에 의한 공격을 방지하기 위하여 Type 관련 정보와 함께 객체에 대한 pointer를 지니는 Point Table을 통해 참조되어야 한다. 이후 이 Pointer Table의 각 항목은 Index를 통해 V8 heap의 객체에서 참조된다.
상기의 3개 사항이 Pointer Compression과 함께 적용되었을 경우 <Image 1> 및 <Image 2>의 ArrayBuffer 객체는 하기의 <Image 3>과 같은 In-Memory layout을 지닌다.
<Image 3>. Pointer compression과 Sandbox가 적용된 임의 ArrayBuffer 객체의 In-Memory layout <Image 2>의 Off-heap backing storage에 대한 Raw pointer (purple)는 <Image 3>에서 확인할 수 있듯 Sandbox의 Base에서 40bit 크기의 offset으로 대체되었다. (Offset은 0x45c00이며 최상위 bit가 0으로 설정되는 것을 보장하기 위해 좌측으로 24만큼 shift됨.) 반면 ArrayBufferExtension 객체에 대한 Raw pointer (orange)는 Pointer Table에 대한 32bit index로 대체되었다. <Image 3
>에서 크기(magenta)는 변경되지 않으나, 실제 Access 시 ArrayBuffer의 최대 허용 크기보다 작은지 확인하는 검사를 거킨다, 혹은 좌측으로 shift하며 저장할 수도 있다.
이러한 조치에도 불구하고, 공격자는 Sandbox 내부의 memory를 다수의 Thread에서 임의로 손상시킬 수 있다. 허나 Sandbox 외부의 Memory를 손상시킴으로써 arbitrary code execution을 달성하기 위해서는 추가적인 취약점이 필요할 것으로 추정된다, 또한 Embed
der <-> V8 interface 의 복잡성이 상대적으로 낮기 때문에 Sandbox가 지니는 공격 표면은 V8 자체보다 월등히 복잡성이 낮을 것이며, Sandbox의 Bug는 대부분의 V8 bug와 달리 '고전적인' Memory corruption으로 보인다. 이러한 이유로 V8 Sandbox는 V8 자체보다 방어하기 용이한 보안 경계로 간주된다.
마지막으로, 명시적 목표는 아니나 이와 같은 설계는 공격자가 Sandbox 외부에서 data를 쓰는 행위 뿐만 아니라 직접적으로 (Speculati
ve는 해당되지 않음) 읽으려는 시도 또한 방지한다는 점에 주목할 필요가 있다. 이 문서의 나머지 부분은 Sandboxing machanism에 대해 보다 자세히 설명한 다음, 설계에 대한 간략한 요약을 담은 도식으로 마무리된다.
샌드박스 주소 공간 (Sandbox Address Space)
Sandbox는 현재 모든 V8 isolate 및 heap 간에 공유되는 Shared pointer compression cage를 가정한다. 이 4GB 크기의 영역은 Sandbox를 위한 훨씬 더 넓은 (예: 1TB) 가상 주소 예약 공간의 시작 부분에 배치되며, 양쪽에 위치한 큰 크기의 Guard region으로 둘러쌓여 Indexing된 access가 Sandbox 외부에 도달하는 것을 방지한다. Sandbox의 나머지 공간은 ArrayBuffer backing storage 및 10GB의 크기를 지니는 WASM memory cage와 같이 V8에서 직접 참조하는 타 객체를 할당하는 데에 사용된다. Sandbox를 지원하기 위한 주소 공간의 예약에 관한 상세 사항은 이 문서에서 다룬다.
'샌드박스화'된 포인터 (Sandboxed Pointer)
Sandbox 내부에 존재하는 모든 객체는 Sandbox base에서 40bit offset (1TB 크기의 Sandbox의 경우)을 통해 V8 heap의 객체에서 참조 가능하다, 이를 'Sandboxed Pointer'라고 한다. Raw pointer 대신 Offset을 사용할 경우 공격자가 Sandbox 외부 memory에 주소를 지정하는 것이 불가하다.
Sandboxing된 Pointer는 Sandbox의 base address가 일반적으로 register에서 이미 사용 가능하기 때문에 높은 성능을 지닌다. 이후 Offset을 좌측으로 shift하여 저장할 수 있으며, 이 경우 Sandboxed pointer를 불러오기 위해서는 load된 값을 우측으로 shift하여 기본 register에 추가하기만 하면 된다. 이는 x64에서 2개의 추가 명령어 (shift + add)와 arm에서 1개의 추가 명령어 (arm에서는 add intruction으로도 이동을 수행할 수 있음)만으로 가능하다.
V8 heap 내부의 data를 손상시킬 수 있는 공격자는 Arraybuffer 객체의 Offset 및 size를 손상시킬 수 있으므로 공격자가 Sandbox 내부의 모든 Data를 손상시킬 수 있다 가정해야한다. 허나 Sandboxed pointer는 언제나 Sandbox를 가리키도록 보장된다. Sandbox가 적용된 포인터에 관련한 상세 사한은 이 문서에서 다룬다.
포인터 테이블 (Pointer Table)
Sandbox 외부에 있는 모든 객체 ('External entities')는 Pointer Table을 통해 참조되며, Pointer Table 자체 또한 Sandbox 외부에 위치한다. 이는 개념적으로 운영체제의 커널에서 사용하는 File Descriptor Table이나 WASM table과 유사하다. 일반적으로 Sandbox는 세 가지 유형의 External entities를 구분한다.
- 실행 가능한 코드
- V8의 Garbage Collector에 의해 관리되지만, Sandbox 외부에 위치한 전용 코드 공간에 할당된 실행 코드
- 이와 같은 External entities는 이 문서에서 자세하게 설명하는 CodePointerTable(CPT)를 통해 참조된다.
- V8의 Garbage Collector에 의해 관리되지만 보안 상의 이유로 Sandbox 외부에 위치한 V8 객체
- 이와 같은 객체는 TrustedPointerTable(TPT)에 의해 참조된다.
- 자세한 내용은 본 문서 하단의 Trusted Object section과 TPT 문서에서 자세하게 설명한다.
- V8의 Garbage Collector에 의해 직접 관리되지 않는 Non-V8 객체
- 이에는 <Image1> ~ <Image 3>에서 다룬 ArrayBufferExtension 객체 뿐만 아니라 DOM node와 같은 모든 Embedder 관리 객체 및 이에 속하지 않는 여러 유형의 객체가 포함된다.
- 이러한 객체는 이 문서에서 더 자세히 설명하는 External Pointer Table (EPT)를 통해 참조된다.
이제 이 Section에서는 이러한 객체가 어떻게 보호되는지, 특히 메모리 안전성이 어떻게 달성되는지에 대해 간략하게 설명한다. 상세한 내용은 상기에 링크한 설계문서에서 관련한 깊이 있는 내용을 다룬다.
시간적 측면에서의 메모리 안전성 (Temporal Memory Safety)
Pointer Table의 항목은 Grabage Collector에 의해 관리된다. V8 heap의 임의 객체에서 항목이 더 이상 참조되지 않으면 해당 항목은 지워지고, 다른 위치에서 다른 참조가 없으며 V8에 의해 관리되는 경우 가리키는 객체가 해제된다. 이후 해당 항목에 대한 모든 후속 Acce
ss는 항목이 해제된 상태를 유지하는 경우 유효하지 않은 Pointer를 관측하게 되거나, 혹은 재사용되었을 경우 Live Object에 대해 유효한 pointer를 관측하게 된다, 후자의 경우 새 객체가 이전 객체와 동일한 Type일 경우 설계상 안전하며, 그렇지 못한 경우 이 문서의 하단에서 설명하는 Type Safety mechanism에 의해 Access는 실패하게 된다.
공간적 측면에서의 메모리 안전성 (Spatial Memory Safety)
ExternalStrings와 같은 일부 객체는 지정된 길이의 Data의 buffer를 참조한다. Sandbox는 이와 같은 External buffer에 대한 모든 Access가 할당된 memory 범위 내에 유지되도록 하여야 한다. 이는 일반적으로 하기의 두 가지 방법을 통해 달성 가능하다.
- Table 또는 External object 자체에 스스로의 크기에 대한 Metadata를 저장하고 이에 대한 경계 검사를 수행하도록 함.
- 해당 External Buffer를 Sandbox 내부로 이동시킴.
타입 안전성 (Type Safefy)
Sandbox 외부에 위치한 객체의 타입 안전성을 보장하기 위해, Table의 항목은 Pointer와 Type으로 구성된 1개 쌍으로 구성된다. 단, 효율성을 위해 사용하지 않는 Pointer bit에 타입이 저장될 수 있다.) 이를 활용하여 외부 객체에 Access하기 이전에 Table의 타입 정보를 확인하거나, 잘못된 타입으로 인해 접근 불가한 주소가 발생하는지 확인하는 방식으로 호출자가 제공한 예상 유형과 비교하여 확인해야 한다.
스레드 안전성 (Thread Safety)
Sandbox는 스레드 안전성을 보장받지 않는 외부 객체에 대한 동시 접근을 차단해야한다. 이를 위해서는 Isolate 및 Mutator 스레드 전용 Pointer Table을 사용해야하며, 스레드 안전성을 보장받지 않는 외부 객체가 최대 1개의 Table에서만 참조되도록 하는 것이 이상적이다.
신뢰할 수 있는 객체 (Trusted Object)
Pointer를 지니는 객체, 혹은 참조하거나 참조의 대상이 되는 객체 이외에도 공격자가 Sandbox를 돌파하기 위해 활용할 수 있는 Sandbo
x 내부 객체는 다수 존재한다. Interpreter bytecode, JIT compile된 machine code 및 관련 Metadata와 같이 실행 가능한 코드와 유사한 객체가 대표적인 예시이다. 또 다른 예시로는 Heap Allocator metadata나 Off-heap data structure에 index를 포함하는 V8 객체 등이 있다. 이들은 일반적으로 오염 및 손상에 강하지 않으므로 공격자가 Sandbox escape에 활용할 수 있다.
일반적인 관점에서, Sandbox가 견고해지려면 상술한 객체들이 V8 heap에서 '신뢰 가능한' heap 공간으로 이동하거나, 오염 및 손상에 대한 저항성을 갖추거나, 이들을 적절한 무결성 확인이 배제된 상태에 기준하여 신뢰할 수 없는 객체로 간주하거나, 읽기 전용으로 관리하여야 한다. 이러한 객체들을 Sandbox 외부로 이동시키고 Pointer Table을 통해 참조하는 일반적인 mechanism은 이 문서에서 설명한다.
요약
개념적 관점에서 V8 heap Sandbox design은 하기의 다이어그램과 같다. (단, Sandbox 주변의 Gaurd region은 생략되어있음)
<Image 4>. V8 heap Sandbox design 'Etc' 카테고리의 다른 글
Best of the Best 12기 취약점 분석 트랙 회고 (0) 2024.03.26