v8 엔진에서의 힙오버
- 힙오버는 특정한 8바이트를 읽거나 덮어쓰는 행위를 하는 부분에서 발생한다. 결국 임의의 읽기와 쓰기 를 가능하게 한다. (*v8 의 변수는 힙 영역에 존재)
V8 Object and Their Structures
- V8 Objects List
- Objects 는 JS engine 에서 광범위하게 사용된다. v8 엔진에서는 v8 objects 가 있는데 이는 v8의 버전에 따라 조금씩 수정될 수 있다.
- V8 Objects List 보기 - 특정한 v8 버전에선 v8/src/objects.h, v8/src/objects/objects.h 에서 볼 수 있다. src/objects/objects.h - v8/v8.git - Git at Google
2. Pointer Tagging
- v8 objects 는 힙에 저장된다. 그리고 포인터를 통해 가르켜진다. (포인트는 8바이트의 사이즈를 가진다.)
- 그런데 Smi(immediate small integer) 라는 8바이트를 차지하는 특정한 object 가 존재한다.
*이 두 가지의 object를 구분하기 위해 Pointer Tagging 이라는 구조를 도입해 구분한다.
1. LSB(least significant bit)를 Pointer object 의 경우 1로 세팅 SMI의 경우 0으로 세팅해준다.
(*Pointer Tagging 된 Addr 은 -1 해야 정상적인 메모리를 참조할 수 있다.)
2. SMI의 경우 상위 4바이트만 점유하기 때문에 하위 4바이트는 0으로 세팅된다. 0x900( 0x0000090000000000)
3. JSArray-related Objects
* JSArray, FixedArray, FixedDoubleArray 의 관계를 설명하겠다.
: 우리가 JS source code 로 ‘var arr =[];’ or ‘var arr = new Array();’ 할때 내부적으론 JSArray 와 FixedArray 과 FixedDoubleArray 중 하나 가 생성된다.
- JSArray: 배열의 헤더와 같은 역활은 한다.
- FixedArray: integers 요소를 가진 배열이 생성될 때 이를 사용한다.
- FixedDoubleArray: 부정 소수점을 가진 요소를 가진 배열이 생성될 때 사용된다.
- FixedArray와 FixedDoubleArray는 둘 다 배열의 Body 역활을 한다. ( 배열의 요소를 저장)
세개의 각 Object 는 위 테이블에 나온 것과 같은 구조체를 가진다.
JSArray
각 값은 JSArray의 8바이트를 차지한다.
첫번째 값은 Map object를 가르키는 포인터다. Map object는 배열의 타입을 저장하고 있다.
… 예로 정수형 배열인지 소수형(Double) 배열인지 가르켜준다.
두번째 값은 배열의 개요 속성에 대한 포인터 다.
세번째 값은 FixedArray or FixedDoubleArray object 를 가르키는 포인터이다. 이때 Pointer Tagging 되어 있기때문에 해당 값 -1 한 값이 실제 object Addr 이다.
네번째 값은 요소의 갯수를 나타내며, SMI 이다. (Ex.. 0x 00000002 00000000)
FixedArray and FixedDoubleArray
두 객체는 같은 구조를 가지고 있으며 위와 같이 각 값은 8바이트를 차지한다.
해당 구조체의 첫번째와 두번째 값은 JSArray와 똑같은 의미의 포인터와 값이 저장되어있다. (이하 생략)
….
세번째(0x10) 에는 배열의 첫번째 요소(값) 가 들어 있다.
4. JSArrayBuffer-related Objects
JSArrayBuffer 와 JSTypedArray, JSDataview 의 관계에 대해 설명하겠다.
JavaScript Code 로 ‘var buf = new ArrayBuffer(8);’ 로 JSArrayBuffer object 를 생성할 수 있다.
이는 밑의 테이블과 같은 구조체를 가진다.
0x00 : Pointer to Map 0x08 : Pointer to Outline Properties 0x10 : Pointer to Elements 0x18 : Length 0x20 : Pointer to Backing Store 0x28 : ...
JSArrayBuffer
0x00 ~ 0x18 까지는 JSArray와 같은 값을 가진다. 여기서 중요한 것은 0x20 —> Pointer to Backing Store 이다. Backing Store 에는 배열의 요소들의 영역과 같은 유저 데이터가 저장된다. JSArray와의 차이점은 backing store는 타입이 지정되지 않은 순수한 바이트 값을 가진다는 것이다. 하지만 배열의 요소들은 “flaot64”와 같은 타입을 가지고 있다. 참고로 여기서 Length 가 갖는 값은 Backing store 의 바이트 길이이다.
결국, JSArrayBuffer 자체는 Backing Store 를 통해 Untyped 상태기 때문에 Access 할 때 이를 해결해야 한다. 그래서 JSTypedArray와 JSDataView 가 사용된다. (아래 코드 참고)
5. JSFunction Object
익스를 짜다보면 쉘코드를 메모리 영역에 삽입하고 이를 실행하기를 원할때가 생긴다. 하지만 쉘코드를 삽입하고 실행할 수 있는 곳을 어떻게 찾을 수 있을까? 이는 RWX 권한의 페이지를 이용하는 것이 일반적이다.
v8 에서 RWX 권한 페이지를 찾을 수있는 하나의 방법을 제시한다. 이는 JSFucntion의 구조체를 이용하는 것이다. 예로 v8(6.1.534.32)에서 다음과 같은 코드를 실행한 후의 JSFunction 의 구조체이다.
v8이 원시 코드를 해석하면 JSFucntion Object를 생성한다. 그 구조체는 위와 같고 이때 함수를 가르키는 포인터를 가지고 있기 떄문에 (여기서는 0x38 부분 ) 해당 페이지의 주소(RWX) 값을 볼 수 있다. 구할 수 있다.
하지만 단점은 v8의 JSFunction Object 의 구조체는 자주 바뀌기 때문에 항상 같은 위치 (0x38) 에 있지 않다.
하지만 구조체를 역추적 해봄으로써 해당 주소와 위치를 발견할 수 있다.
6. Wasm-related Objects
이 또한 RWX 페이지 주소를 구하는 방법을 설명한다. 이는 아래 표에서 알 수 있듯이, 먼저 wasm code를 Array에 넣고 WasmModuleObject를 생성한다. 그 다음 이를 사용해 WasmInstanceObject 를 생성하고 이를 통해 함수의 entry 포인트를 알아내 변수 f에 넣고 이를 실행한다.
v8(7.2.502.3) 이후의 버전은 RWX page 에 해당 함수를 정의한다. 그리고 WasmInstanceObject’s structure 에 해당 주소를 저장해 포인터를 만든다. 즉 우리는 이를 통해 주소를 알아낼 수 있다.
위 표에선 Offset 0xe8 에서 0x2e641ceef000 가 RWX page에 존재한다. 하지만 이 또한 항상 같은 위치에 존재하지 않는다. 이는 위와 같은 이유로 v8 버전에 따라 구조체가 자주 변한다. 그러나 역추적을 통해 알아낼 수 있다.
7. Pointer Compression ( After . version 8)
v8에서의 힙은 일반적으로 표시되는 [Heap] 영역과는 다른 영역에 할당된다. 이때 V8 힙은 상위 32비트가 0x00001f08로 표시되는 모든 메모리 영역으로, 보통 프로그램의 최하위 메모리 주소에 매핑된다.
V8에서 메모리는 모두 격리(isolate)된다고 한다. 위에서 보다시피 최하위 메모리 주소에 매핑되며, 바이너리의 텍스트 세그먼트가 바로 다음에 매핑된다.
V8 힙의 주소는 실행될 때마다 변경되지만, 상위 32비트(0x00001f08)는 실행 중에 변경되지 않는다.
이를 포인터 압축이라고 하며, 포인터 압축을 통해 힙 메모리 사용량을 40%까지 절약하였다고 한다.
V8 힙 주소의 상위 32비트를 격리 루트(isolate root)라고 부른다.
격리 루트는 특정 레지스터(R13)에 저장하며, 이 레지스터를 루트 레지스터라고 부른다.
포인터 압축의 단점은 V8 힙의 사이즈가 최대 4GB(32비트만 주소로 사용)로 제한된다는 것이다.
이런 단점 때문에, node.js는 포인터 압축을 사용하지 않는다.
포인터 압축과 관련된 코드는 아래의 위치에 구현되어 있다.
- v8/src/common/prt-compr.h
- v8/src/common/ptr-compr-inl.h
우회 및 익스 방법
자바 스크립트를 통해 격리 루트를 알아내는 건 어렵다. 하지만, 격리 루트를 굳이 알아야할 필요는 없다.
addrof 또는 fakeobj가 가능하다면, 가짜 JSArray를 만든 후 엘리먼트 포인터를 수정하여 임의의 주소에 데이터를 쓰고, 읽을 수 있다.
JSArray의 엘리먼트 포인터에는 32비트로 압축된 포인터가 저장된다.
따라서, V8 힙 내부의 주소에만 데이터를 쓰고, 읽을 수 있다.
이를 극복하기 위한 기본적인 방법은 V8 힙에 ArrayBuffer를 할당하는 것이다.
ArrayBuffer의 엘리먼트 포인터는 PartitionAlloc를 사용해서 할당되기 때문이다.
PartitionAlloc은 V8 힙이 아닌 메모리 영역에 할당한다.
즉, ArrayBuffer의 엘리먼트 포인터에 압축되지 않은 64비트 포인터를 저장하여, V8 힙 이외의 메모리 영역에 접근할 수 있다.
Pointer Compression in V8 - xiphiasil ver 2.1
'Browser > Chrome' 카테고리의 다른 글
Chromium - 크로미움 프레임 (0) | 2023.02.18 |
---|