
SnatchBox (CVE-2020-27935) is a sandbox escape vulnerability and exploit affecting macOS up to version 10.15.x
SnatchBox (CVE-2020-27935) est une vulnérabilité d'évasion de la sandbox affectant macOS jusqu'à la version 10.15, ainsi que les premières versions bêta de macOS 11.0. L'impact le plus significatif de SnatchBox est qu'il permet à un éditeur malveillant de s'échapper de la sandbox non optionnelle du Mac App Store et d'obtenir un accès complet à tous les fichiers de l'utilisateur, brisant ainsi le modèle de sécurité de l'App Store sous macOS.
Le fait que sous macOS, contrairement à iOS par exemple, une tâche en espace utilisateur se mette volontairement dans une sandbox est vulnérable par conception. Comme il s'agit d'une tâche pour laquelle un auteur potentiellement malveillant a presque un contrôle total sur ses mappages mémoire et leur contenu, et que le code qui s'exécute avant l'initialisation de la sandbox (par ex. dyld lui-même, ou le runtime Objective-C) analyse le contenu de ce binaire potentiellement malveillant, des données soigneusement conçues peuvent être utilisées pour obtenir une exécution de code avant l'initialisation de la sandbox. Si le processus n'exécutait jamais de code, y compris le code dyld, avant d'être contraint dans une sandbox, cela n'aurait pas posé de problème, car une exécution précoce de code n'apporterait rien à l'attaque. Conceptuellement, cela est similaire au contournement de sandbox de Saagar Jha, sauf que cela contourne les nouvelles mitigations et les validations de l'App Store.
Avant macOS 10.15, l'exploitation de ce bogue est assez simple. Il suffit de créer un binaire contenant une catégorie Objective-C pour une classe utilisée avant l'initialisation de la sandbox (comme OS_xpc_object) et de surcharger une méthode (de préférence héritée pour éviter les avertissements du runtime) qui est utilisée avant l'initialisation de la sandbox (comme +initialize, qui est implicitement appelée lors du premier accès à une classe). Comme les catégories sont chargées avant l'initialisation de la sandbox, et que le premier accès à OS_xpc_object (ou à d'autres classes victimes appropriées) a lieu après le chargement des catégories mais avant l'initialisation de la sandbox, la méthode +initialize fournie par l'attaquant (ou une autre méthode victime appropriée) sera appelée avant l'initialisation de la sandbox, ce qui permet par exemple d'accéder à des données en dehors du conteneur. Alternativement, l'attaquant peut remplacer la référence à _libsecinit_initializer (qui initialise la sandbox) par une fonction de type NOP pour désactiver (potentiellement conditionnellement) la sandbox même après la reprise de l'exécution.
Le runtime Objective-C utilisé dans macOS 10.15 n'est pas vulnérable à la technique d'exploitation précédemment détaillée, car les catégories ne sont pas chargées avant que didCallDyldNotifyRegister ne soit défini, ce qui fait que notre méthode +initialize n'est appelée qu'après l'initialisation de la sandbox.
Cependant, map_images est toujours appelé sur notre binaire, ce qui permet de modifier les données du runtime de manière non intentionnée, permettant ainsi d'exécuter du code avant l'initialisation de la sandbox. L'exploitation complète et commentée se trouve dans main.c, mais je vais ici en présenter les bases. Nous fabriquons une structure de classe Objective-C dont le pointeur data pointe vers un emplacement dans libxbc.dylib. Cet emplacement doit être choisi de sorte que flags ait le bit 31 (RW_REALIZED) activé, afin que le runtime ne tente pas de réaliser cette classe invalide et ne plante pas, et que firstSubclass partage son adresse avec l'isa d'une classe que nous voulons surcharger. Une autre classe (méta) héritera de cette classe invalide et fournira sa propre méthode +initialize. Nous ajoutons la sous-classe à __objc_nlclslist pour que le runtime réalise cette classe.
Lorsque le runtime réalise notre sous-classe, ce qui se produit avant l'initialisation de la sandbox, il appelle addSubclass sur notre superclasse invalide et notre sous-classe, ce qui remplace l'isa de la victime par un pointeur vers notre sous-classe, remplaçant ainsi effectivement toutes ses méthodes par notre +initialize. Lorsque notre méthode +initialize est appelée, ce qui se produit avant l'initialisation de la sandbox si nous avons choisi une classe victime appropriée, nous pouvons à nouveau remplacer les références à _libsecinit_initializer par des NOP (conditionnellement ou non) et corriger les modifications du runtime effectuées pour reprendre l'exécution sans planter ultérieurement.
La démo fournie peut être construite en exécutant make, en créant un fichier dans ~/Documents/SecretDocument.txt, et en exécutant SnatchBox.app/Contents/MacOS/SnatchBox depuis le terminal (un bundle est créé car requis par com.apple.security.app-sandbox, mais il s'agit toujours d'un programme en ligne de commande). Le binaire construit est signé avec com.apple.security.app-sandbox, ce qui empêcherait normalement l'accès à ~/Documents/SecretDocument.txt (car il n'est pas dans notre conteneur), mais il pourra quand même lire ses données. En raison des changements de structure du runtime, cette démo ne fonctionnera pas sur macOS 10.14 et versions antérieures non modifiées, mais fonctionnera sur 10.15 et 11.0 (Testé : 10.15.4, 10.15.7 et 11.0 Beta (20A5354i)). Les deux techniques d'exploitation peuvent être combinées pour cibler les deux versions du runtime, mais une telle démonstration n'est pas fournie.
Exemple d'exécution :
CatalinaVM:SnatchBox lior$ make
mkdir -p SnatchBox.app/Contents/MacOS/
clang -O3 -Wall -framework Foundation main.m -o SnatchBox.app/Contents/MacOS/SnatchBox
cp Info.plist SnatchBox.app/Contents/
codesign --force --sign - SnatchBox.app --entitlements ent.xml
CatalinaVM:SnatchBox lior$ echo "Quack"> ~/Documents/SecretDocument.txt
CatalinaVM:SnatchBox lior$ codesign -d --entitlements :- SnatchBox.app/Contents/MacOS/SnatchBox
Executable=/Volumes/SharedFolders/Home/Projects/SnatchBox/SnatchBox.app/Contents/MacOS/SnatchBox
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>
CatalinaVM:SnatchBox lior$ SnatchBox.app/Contents/MacOS/SnatchBox
Found libsecinit_initializer at 0x7fff72309124
Found libSystem.B.dylib at 0x7fff6f0de000
Found __DATA at 0x7fff984eeca0
Replacing libsecinit_initializer reference at 0x7fff984eed48 with a nop
2020-12-18 16:31:48.196 SnatchBox[804:8043] Attempting to read protected file: /Users/lior/Documents/SecretDocument.txt
2020-12-18 16:31:48.197 SnatchBox[804:8043] Escaped sandbox! The contents are: <51756163 6b0a>
Comme mentionné précédemment, cela permet de créer une application du Mac App Store qui ne s'exécute pas dans une sandbox malgré l'obligation de la politique de l'App Store. La vulnérabilité peut également être utilisée dans un framework, qui peut être utilisé par des applications App Store par ailleurs légitimes. Enfin, elle peut même être combinée avec quelque chose de similaire à "Xcode Ghost" pour injecter en masse du code malveillant s'exécutant en dehors de la sandbox dans des applications de l'App Store.
Apple a corrigé l'exploit lors de la phase de test bêta de macOS 11.0 en ajoutant un appel à malloc_size dans realizeClassWithoutSwift. Cela confirme que si une classe est marquée comme réalisée (RW_REALIZED, comme notre fausse classe), elle a bien un pointeur de données valide, alloué par malloc, avec la taille correcte (0x20 octets). Si ce n'est pas le cas, le runtime abandonne avec un message similaire à realized class 0x100002078 has corrupt data pointer 0x7fff88c00948. Le correctif a également été appliqué à iOS, iPadOS, tvOS et watchOS, même s'ils ne sont pas directement affectés.