V8 Heap Sandbox

Pwnable
Chrome
V8

라온화이트햇 핵심연구팀 이영주

V8 Heap Sandbox

소개

아래 글은 V8 heap sandbox에 관하여 이 링크를 이해한대로 번역한 내용입니다. 이번 샌드박스가 릴리즈 빌드에 적용되게 된다면 힙 메모리를 조작할 수 있더라도 지금처럼 간단하게 exploit할 수 없을것이라 예상됩니다. 완벽하게 모든 힙 공격을 막을 수는 없겠지만 이 샌드박스에 맞춰서 다른 모든 힙 오브젝트들을 관리하게 되면 결국 나중에는 힙 공격에 완전히 안전한 샌드박스를 만들 수 있을것이라고 생각합니다.

목적

V8엔진에 취약점이 발생하여 힙 내부에 있는 객체를 조작할 수 있을 때, 힙 외부에 있는 메모리를 손상시킬 수 없도록 샌드박스를 구축합니다. 성능 오버헤드는 최소한으로 줄여야하며 약 1%정도가 되어야 합니다.

동기

많은 exploit에 사용되는 취약점들은 2nd order vulnerability이며 예시로 JIT 컴파일러가 있습니다. JIT 컴파일러는 중복되는 런타임 검사들을 줄이는게 주요 목적이기 때문에 어쩔 수 없이 많은 취약점이 발생할 수 밖에 없습니다.

취약점의 예시로 JIT 컴파일러가 operation의 side effect를 잘못 모델링해서 타입 체크를 잘못하는 기계어를 생성하도록 할 수 있습니다. (컴파일러가 연산 중 객체의 타입이 변경되지 않는다고 가정합니다) 따라서 이러한 코드는 type confusion을 발생시키며 메모리 커럽션을 발생시킬 수 있습니다. 이를 통해 공격자는 빠르고 안정적인 exploit을 만들 수 있습니다.

Memory safe language들은 이러한 문제로부터 보호할 수 없으며 Memory Tagging 같은 하드웨어 보안 기능들도 CPU 사이드채널과 V8 취약점들로 우회할 수 있습니다.

이러한 취약점의 특성과 JavaScript 엔진의 고유성으로 인해 V8용 샌드박싱 구조를 구축하는게 좋을 것 같습니다.

공격 모델

이것은 공격자가 V8 힙 내부에서 임의의 읽기/쓰기를 반복적으로 수행할 수 있을 뿐만 아니라 speculative side-channel같은 공격으로 메모리를 읽을 수 있다고 가정합니다. 또한 V8 힙 외부 메모리를 손상시킬 수 있거나 임의 코드 실행이 가능하면 샌드박스를 벗어났다고 간주합니다.

설계

2020년 초부터 V8은 힙에 포인터 압축(pointer compression)을 적용했습니다. 이것을 통해 V8 외부에 있는 힙을 가르키는 몇개의 객체를 제외한 나머지 객체들은 32비트 오프셋만 힙에 남게 되었습니다. 이런 압축된 포인터는 4GB 가상 메모리내에서만 유효합니다. 다음은 포인터 압축이 되어있지 않은 ArrayBuffer 객체의 메모리 레이아웃을 보여줍니다.

Untitled

포인터 압축이 적용되면 아래와 같습니다.

온힙 포인터들이 32bit 압축 포인터로 변경된것을 확인할 수 있습니다.

Untitled

V8의 대부분의 취약점들은 V8 힙 메모리를 덮어서 exploit합니다. 하지만 포인터 압축이 활성화 된 경우, 공격자는 압축된 포인터를 덮는것으로는 exploit을 할 수 없기 때문에 ArrayBuffer같은 압축되지 않은 포인터를 노립니다. 기본적인 아이디어는 남아있는 오프힙 포인터들을 공격자로부터 지키는것이며 이것을 위해 두가지 중심 메커니즘이 있습니다.

  1. V8 힙 영역과 ArrayBuffer의 backing store같은 데이터들을 위한 큰 영역의 가상 메모리 케이지.
  2. 타입 정보와 함께 저장되는 오프힙 객체의 외부 포인터 테이블. 이 테이블의 항목은 인덱스를 통해 v8 힙에서 참조됩니다.

이것을 통해 ArrayBuffer는 다음처럼 표현될 수 있습니다.

Untitled

가장먼저 변경된 backing store 포인터는 (보라색) 40비트로 된 오프셋으로 대체되었습니다. (0x45c00, 항상 topbit가 0임을 보장하기위해 왼쪽으로 24만큼 쉬프트됩니다) 또한 ArrayBufferExtension 객체(주황색)는 외부 포인터 테이블의 32비트 인덱스로 변경되었습니다.

이 샌드박스 내에서는 공격자가 케이지 내부 메모리를 자유롭게 손상시킬 수 있다고 해도 샌드박스 외부 메모리를 손상시켜 코드를 실행할 수 있는 취약점이 추가로 필요합니다. 하지만 샌드박스의 공격 벡터는 V8보다 훨씬 덜 복잡하기 때문에 더 쉽게 방어할 수 있을 것입니다.

마지막으로 이 설계에서 명시적 목표는 아니지만 메모리를 손상시키는 것 뿐만 아니라 케이지 외부 메모리를 읽는것을 막을 수 있다는것이 중요합니다.

가상 메모리 케이지

가상 메모리 케이지는 현재 공유 포인터 압축 케이지를 가정합니다. 이것은 모든 V8 힙이 같은 4GB 가상 메모리 영역을 공유하는것을 말합니다. 이 4GB 메모리는 양쪽에 매우 큰 (약 1TB) 가드 영역으로 둘러싸인 곳에 위치합니다.

ArrayBuffer의 backing stores같은 데이터들은 케이지안에 할당되며 V8 힙에서 40비트 오프셋을 통해 접근할 수 있습니다. 특히 케이지의 베이스주소가 레지스터에 저장되기 때문에 더욱 효과적입니다. 이 오프셋은 왼쪽으로 쉬프트된 상태로 존재하기 때문에 위치를 불러오려면 가져온 값에서 오른쪽으로 쉬프트하고 베이스 레지스터에 더해주기만 하면 됩니다. 이것은 64비트에서 2개의 명령어로 가능하며 arm에서는 1개의 명령어로 가능합니다.

공격자가 V8 힙에 있는 데이터를 손상시킬 수 있으면 케이지 내의 모든 데이터를 손상시킬 수 있다고 가정해야 합니다.

자세한건 https://docs.google.com/document/d/17IW7LKiHrG3ZrtbS-EI8dJHD8-b6vpqCXnP3g315K2w/edit 여기서 확인할 수 있습니다.

외부 포인터 테이블

가상 메모리 케이지 외부에 있는 객체에 대한 참조는 외부 포인터 테이블에 저장됩니다. 이 섹션에서는 이런 객체들을 보호하는 방법을 설명합니다.

Temporal Memory Safety

외부 포인터 테이블은 GC에 의해 관리됩니다. 포인터가 V8힙에서 더이상 사용되지 않으면 free됩니다. 이후 다시 접근했을때 그 값이 free된 상태이면 유효하지 않은 포인터로 체크하고 재사용 되어 살아있다면 유효한 포인터로 간주합니다. 후자의 시나리오에서 새 객체가 이전 객체와 동일한 유형이라면 접근은 안전합니다. 그렇지 않다면 접근이 실패합니다.

Spatial Memory Safety

ExternalStrings같은 일부 객체들은 주어진 길이의 데이터 버퍼를 참조합니다. 이 샌드박스는 이러한 외부 버퍼들에 대한 접근이 할당된 메모리 내에서 이루어지도록 해야합니다. 두가지 방법을 통해 할 수 있습니다.

  1. 테이블이나 외부 객체에 길이 정보를 저장하고 경계 검사.
  2. 해당 버퍼를 가상 메모리 케이지로 이동

Thread Safety

샌드박스는 쓰레드상 안전하지 않은 외부 객체에 대한 이중 접근을 방지해야합니다. 이상적으로는 Isolate나 mutator별 별도의 외부 포인터 테이블을 가지고 쓰레드에 안전하지 않은 객체는 최대 한개의 테이블에서만 참조할 수 있게 함으로써 달성할 수 있습니다.

Trusted Data Structures

지금 V8 힙 내부에는 포인터가 존재하지 않지만 공격자가 샌드박스를 벗어날 수 있는 구조들이 많이 존재합니다. 예시로 인터프리터 바이트 코드, JIT 컴파일된 기계 코드 같은 것들이 있습니다. 이것들은 일반적으로 손상에 강하지 않으므로 샌드박스 탈출이 가능합니다. 다른 예로 오프-힙 데이터 구조에 대한 인덱스를 포함하는 힙 할당자 메타데이터나 V8객체가 포함될 수 있습니다.

일반적으로 샌드박스가 강력해지기 위해서는 이러한 데이터 구조가 V8 힙에서 벗어난 외부 객체가 되어야 하고, 손상에 강해야 하며, 신뢰할 수 없는 것으로 취급되어야 합니다(그리고 액세스 시 무결성을 검사할 수 있는 방법이 있어야 함). 또는 읽기 전용으로 표시되어야 합니다.

Summary

힙 샌드박스는 아래 사진으로 요약할 수 있습니다.

Untitled