La puce Titan M ou Citadel est une puce sécurisée sur laquelle repose en grande partie la sécurité des terminaux Android de Google, la gamme Pixel. Dans cet article, nous détaillerons le fonctionnement interne et les usages de ce composant pour lequel peu d’information publique est disponible à ce jour. Nous donnerons également plusieurs pistes pour aider le rétro-ingénieur à travailler sur ce projet.
La puce Titan M (a.k.a Citadel) a été créée par Google et intégrée dans les smartphones de la gamme Pixel à compter du Pixel 3. D’après Google, cette puce améliore la sécurité de ses terminaux en réduisant leur surface d’attaque et en limitant l’impacte de vulnérabilités matérielles telles que Rowhammer ou Spectre.
Bien que la plupart des smartphones Android reposent aujourd’hui sur la ARM TrustZone pour garantir les fonctionnalités de sécurité critiques du système, Google a fait le choix, depuis le Pixel 2 avec une puce sécurisée de NXP, de s’abstraire de la TrustZone et d’utiliser une puce externe. En effet, plusieurs attaques ont démontré qu’il était possible d’extraire des secrets de la TrustZone, depuis une application Android non privilégiée via des fuites par canaux auxiliaires. On peut citer l’attaque TruSpy [1] qui permet d’extraire une clé AES de la TrustZone via une attaque sur le cache CPU.
Cette nouvelle puce utilise sa propre mémoire et son propre cache CPU, limitant ainsi l’impact de ces attaques. Elle embarque aussi plusieurs protections contre les attaques matérielles ce qui la rend résistante à la rétro-ingénierie matérielle et aux injections de fautes.
Lors de l’annonce de la puce, Google a annoncé que le firmware serait open source. Deux ans après, les sources n’ont toujours pas été publiées et ce composant reste bien mystérieux.
1. Spécification matérielle
La puce Titan M est un SoC (pour System on Chip) reposant sur un microprocesseur ARM Cortex-M3, disposant de 64 ko de RAM et d’une mémoire flash interne dont la taille reste inconnue. Elle dispose aussi d’accélération matérielle pour les algorithmes de cryptographie courants (AES, SHA, HMAC), d’un composant pour la manipulation des grands nombres, d’un générateur de nombres aléatoires, et d’un module permettant de générer et de gérer des clés cryptographiques. On note aussi la présence de protections contre les attaques matérielles telles qu’un shield (un maillage qui recouvre la puce) et des capteurs, notamment de température et de tension (Google n’a pas présenté de liste exhaustive) probablement destinés à détecter des attaques par glitch.
Cette puce peut communiquer sur plusieurs bus de données classiques. Sur le Pixel 3, le bus SPI est utilisé pour communiquer avec Android. Sur le bus UART, on va retrouver les logs ainsi qu’une console minimaliste. Le bus UART n’est pas accessible depuis Android, mais il est possible de le rediriger sur le bus USB via un câble de debug appelé SuzyQable [SUZYQ].
Le Titan M n’est pas la première puce sécurisée créée par Google. En effet, il en existe au moins une autre, le Titan H1. Ce composant a d’ailleurs été présenté par l’un de ses développeurs [2] et dispose d’une architecture matérielle très similaire à celle de Citadel. On apprend notamment qu’il dispose d’un processeur ARM SecurCore SC300, qui combine un Cortex-M3 et des fonctionnalités d’anti-tampering de la gamme ARM SecurCore. On notera d’ailleurs que ce type de processeurs est utilisé dans de nombreuses puces sécurisées du marché.
2. Usages dans Android
Le CPU principal du Pixel 3 communique avec Citadel via le bus SPI. Le pilote au niveau du noyau Linux est très simple et va envoyer les données reçues sur le périphérique /dev/citadel0 vers le bus SPI à destination de Citadel (voir partie 4).
Côté userland, le démon CitadelProxy gère la communication avec le pilote. Différents services HAL implémentent les fonctionnalités reposant sur Citadel. Tous ces services communiquent avec CitadelProxy via le périphérique /dev/vndbinder. La partie applicative du protocole permettant de communiquer entre un service Android et une tâche Citadel est définie avec protobuf [PROTOBUF], un outil bien connu développé par Google. La définition de ces protocoles ainsi que les sources des services HAL se trouvent dans le projet external/nos (pour NuggetOS ?) dans les sources d’Android. Ainsi le parseur de paquets est généré par protobuf, limitant les risques d’introduire une erreur dans la manipulation des données.
2.1 Format des images
Sur le Pixel 3, Android utilise le programme citadel_updater pour mettre à jour le firmware de Citadel. On trouve d’ailleurs ce firmware dans le répertoire /vendor/firmware/citadel/ec.bin sur le téléphone. citadel_updater implémente un parseur des en-êtes du firmware, ce qui est bien utile pour comprendre comment celui-ci est structuré.
La définition de l’en-tête est présente dans les sources d’Android dans le fichier external/nos/host/generic/nugget/include/signed_header.h.
Cet en-tête comporte des informations intéressantes parmi lesquelles les adresses de deux zones mémoire ro_base et rx_base ou encore la taille de l’image.
2.2 Version et dépendances du firmware
citadel_updater permet aussi de récupérer différentes informations du firmware du Titan M telles que la version du firmware, la version des images ainsi qu’une liste des dépendances logicielles du projet.
On apprend notamment que la bibliothèque nanopb [NANOPB] est utilisée pour générer des parseurs à partir de définitions protobuf.
2.3 Format de la mémoire flash
Les sources d’Android comportent aussi des informations sur l’organisation de la flash interne dans le fichier external/nos/host/generic/nugget/include/flash_layout.h.
Ainsi, un firmware comporte quatre images, deux de type RO et deux de type RW, chacune comportant un SignedHeader. Les images RO contiennent le bootloader chargé de vérifier puis de démarrer l’OS principal contenu dans les images RW. Il existe deux versions de chaque image, une version A et une version B. C’est le résultat de l’implémentation des mises à jour de type « A / B », permettant de mettre à jour le système sans coupure que l’on trouve aussi dans Android ou ChromeOS. Après analyse, il apparaît que le fichier de firmware ec.bin comporte ces quatre images et respecte le formatage de la mémoire flash.
Avec ces informations, il nous est donc possible de charger le firmware aux adresses indiquées dans notre désassembleur préféré.
3. Analyse du firmware
3.1 Nugget OS
Il s’agit de l’OS principal exécuté par Citadel (présent dans les images RW). Il repose sur Chrome EC (Chromium Embedded Controller) [ECSOURCES], un OS open source pour microcontrôleur, développé par Google, aussi utilisé par le Titan H1. Dans les sources d’EC, il y a d’ailleurs la définition d’un équipement cr50 qui correspond à la puce Titan H1. Dans l’implémentation du cr50, on trouve la définition associée à une mystérieuse puce g (pour Google ?). En comparant cette implémentation avec le firmware de Citadel, on note de nombreuses similitudes, mais aussi quelques différences. Par exemple, les registres de différents contrôleurs de bus de données (UART ou USB) sont mappés à des adresses différentes en mémoire. La majorité des tâches et l’implémentation de la communication sur le bus SPI présenté plus loin ne se trouvent pas dans les sources.
Il n’y a pratiquement pas d’allocation dynamique dans EC. Une implémentation minimaliste de malloc et free repose sur un mécanisme de mémoire partagée, mais est très peu utilisé dans les sources de EC (et ne semble pas utilisé dans le Titan M).
3.2 Les interruptions
L’architecture Cortex-M3 définit un registre dédié à l’initialisation du vecteur d’interruptions. Le registre VTOR (pour Vector Table Offset Register) est mappé sur l’espace mémoire à l’adresse 0xE000ED08. On retrouve ainsi la déclaration du vecteur d’interruptions dans notre désassembleur. Dans les images étudiées, il se trouve juste après l’en-tête de chaque image.
La table des vecteurs d’interruptions commence par l’adresse initiale de la pile (0x10400 pour RW et 0x20000 pour RO), puis l’adresse du point d’entrée des routines d’exceptions dont une partie est définie par l’architecture et les autres sont définies par le fabricant de la puce. On trouve ainsi l’adresse de la routine reset qui est appelée au démarrage de la puce, et qui va initialiser le système puis appeler la fonction main.
Cette table comprend des interruptions pour la gestion des bus SPI, USB et UART, la gestion des boutons (les boutons physiques du Pixel sont reliés au Titan M) et d’autres encore non identifiées.
L’interruption logicielle est déclenchée via l’instruction SVC. Ce mécanisme est utilisé pour changer le contexte d’exécution en passant du mode Thread au mode Handle. Les tâches sont exécutées en mode Thread et utilisent ce mécanisme pour donner la main à l’ordonnanceur qui s’exécute donc en mode Handle. L’ordonnanceur correspond à la routine des interruptions logicielles, svc_handler dont une implémentation se trouve dans les sources d’EC (voir le fichier core/cortex-m/task.c). Avant chaque changement de contexte, il va vérifier la valeur d’un canari au pied la pile de la tâche courante.
Il existe donc un stack canari dans EC. Cependant, cette protection est assez limitée : en effet, elle ne protège que d’un dépassement complet de la pile d’une tâche. Par ailleurs, la valeur du canari est fixe comme on peut le voir dans le pseudo-code ci-dessus, obtenu avec un peu de reverse.
3.3 Les tâches
La majeure partie du code est exécutée dans des tâches. Une tâche dispose de sa propre pile de taille fixe et est associée à une priorité utilisée par l’ordonnanceur pour choisir la prochaine tâche à exécuter.
Dans les sources, les tâches sont définies par le fichier ec.tasklist. Dans le firmware, on peut facilement retrouver cette liste en recherchant le nom de la tâche « << idle >> ».
En remontant les références vers ce tableau, on retrouve la définition initiale de ces tâches ainsi qu’une fonction de journalisation appelée task_print_list. Il est aussi possible de retrouver cette dernière fonction en recherchant la chaîne « Task Ready Name ». Cette fonction va utiliser deux structures qui nous intéressent : le tableau de structures tasks_init qui est stocké dans la flash et contient des valeurs d’initialisation des tâches, et le tableau de structure tasks qui est stocké en RAM et est utilisé pour la gestion des tâches.
Chacune des structures du tableau tasks_init comprend les valeurs initiales pour les registres r0 (qui semble toujours valoir 0), pc ainsi qu’une taille maximale de pile. La valeur de pc nous intéresse particulièrement puisqu’elle correspond au point d’entrée d’une tâche.
On identifie ainsi neuf tâches présentes dans Nugget OS :
- << idle >> : cette tâche est exécutée lorsqu’aucune autre tâche ne l’est ;
- HOOKS : cette tâche gère les évènements et les timers ;
- NUGGET : ici sont implémentées les vérifications du mot de passe utilisateur, les mises à jour du firmware, et d’autres commandes pouvant être appelées depuis le programme citadel_updater d’Android ;
- AVB : implémente la fonctionnalité AVB (Android Verified Boot, les dernières étapes du démarrage sécurisé) d’Android ;
- KEYMASTER : implémente la fonctionnalité Keymaster, aussi appelée Strongbox Keymaster et que l’on trouve derrière le Keystore d’Android. Elle permet de réaliser des opérations cryptographiques dans le Titan ;
- WEAVER : implémente la fonctionnalité Weaver d’Android qui permet de stocker des secrets dans le Titan ;
- CONSOLE : implémente une console minimaliste accessible depuis l’UART ou l’USB en utilisant le câble de débogage SuzyQable [SUZYQ].
Dans les versions récentes de Citadel, deux nouvelles tâches ont été introduites : IDENTITY et FACEAUTH. Elles semblent être liées à la fonctionnalité de reconnaissance faciale introduite dans les smartphones Pixel 4, mais n’ont pas encore été étudiées.
4. La communication entre Citadel et Android
4.1 Côté Android
4.1.1 Les commandes au niveau userland
Un service HAL qui souhaite envoyer une commande va encoder les données avec le parseur généré à l’aide de protobuf. Il va ensuite les envoyer au service CitadelProxy, accompagnées de l’identifiant de l’application (ou tâche) ciblée dans Citadel, et le numéro de commande.
Pour une commande reçue, CitadelProxy va envoyer plusieurs requêtes au driver noyau, via des appels IOCTL. Les données vont être découpées en buffers d’une taille maximale de 2044 octets.
Une requête est représentée par une structure appelée citadel_ioc_tpm_datagram comprenant l’adresse du buffer de données, sa taille ainsi qu’un mot de 32 bits appelé command.
Ce mot contient l’identifiant de l’application Citadel encodé sur un octet, la taille des données encodée sur deux octets et des flags de signalétiques.
Une dernière requête est envoyée au driver. Le mot command contiendra alors l’identifiant de tâche ainsi que le numéro de commande (provenant de la définition protobuf). La requête sera accompagnée d’une structure command_info contenant, entre autres une somme de contrôle CRC16 de l’ensemble de la commande telle que reçue depuis le service HAL.
Pour recevoir la réponse associée à la commande, CitadelProxy va envoyer une nouvelle requête au noyau, toujours en utilisant la structure citadel_ioc_tpm_datagram. Le champ command contiendra un flag indiquant qu’il s’agit d’une lecture. Le champ buf sera alors rempli par le noyau avec la réponse.
4.1.2 Les commandes au niveau noyau
Au niveau du driver, la structure citadel_ioc_tpm_datagram est reçue depuis le userland. La fonction citadel_tpm_datagram se charge de l’envoyer à Citadel en deux temps.
Un premier message comprenant la commande est envoyé sur le bus. Lorsque Citadel est en mesure de recevoir la suite, un message spécifique (0xdfdfdfde) est envoyé au noyau. Le driver envoie alors le buffer provenant du userland sur le bus SPI.
4.2 Côté Citadel
Deux interruptions sont utilisées pour la communication avec Android : une interruption rx pour les requêtes et une interruption tx pour les réponses. Lorsqu’un message est reçu, la routine d’interruption rx va appeler une fonction dédiée au traitement des messages reçus. Elle lui donne en paramètre la variable de commande, les adresses d’un buffer (ici appelé rx_buf) et sa taille.
Cette fonction va itérer sur un tableau de structures (ici appelé struct_spi_msg_task_ARRAY) correspondant à la liste des tâches pouvant recevoir des messages via SPI. Il apparaît donc que cinq tâches peuvent envoyer et recevoir des données de cette façon : les tâches Nugget Core, AVB, Keymaster, Weaver et Identity.
Les structures de struct_spi_msg_task_ARRAY comportent notamment un identifiant de tâche qui est comparé à celui provenant de la commande, une callback pour l’envoi et une autre pour la réception de message, ainsi que l’adresse d’une autre structure appelée ici spi_msg_info.
Cette dernière structure contient entre autres l’adresse d’une zone en RAM où sera reconstituée la commande complète en provenance d’Android et qui sera utilisée par la tâche correspondante pour parser le message reçu. La structure spi_msg_info comporte aussi une fonction qui est utilisée pour générer un évènement afin d’alerter la tâche lorsqu’une commande est reçue.
4.2.1 Réception d’une commande SPI dans une tâche
Cet extrait de la tâche KEYMASTER nous montre que la tâche attend l’arrivée d’un évènement avant d’effectuer la vérification du CRC de la commande reçue, alors stockée dans la structure KM_SPI_CMD. Si la vérification est valide, la fonction nugget_app_keymaster_Keymaster est appelée et va identifier le numéro de commande.
Cette fonction prend en paramètre le pointeur d’un tableau de fonctions situé en RAM. En typant correctement ce tableau de fonctions, la fonction de parsing devient plus lisible.
Le numéro de commande est utilisé dans un switch pour identifier la commande (l’identifiant « 0 » correspond à la commande AddRngEntropy) et appeler ensuite la fonction correspondante.
Toutes les fonctions correspondant aux commandes ont la signature suivante :
4.2.2 Implémentation des commandes
La définition des protocoles protobuf nous donne quelques indications sur le rôle fonctionnel de chacune des fonctions de la liste passée en argument à nugget_app_keymaster_Keymaster. Le numéro de commande nous permettra d’identifier exactement chaque fonction et le format des données échangées.
L’analyse de la définition du protocole de keymaster (le fichier nos/host/generic/nugget/proto/nugget/app/keymaster/keymaster.proto) met en évidence l’ensemble des commandes pour lesquelles une fonction est implémentée dans la tâche.
Les implémentations de ces fonctions sont assez similaires. L’extrait suivant provient de la fonction AddRngEntropy.
La fonction pb_decode_ex du projet nanopb va décoder les données provenant de la commande et les placer dans une structure dest_struct. Cette dernière est ensuite donnée en argument à km_add_rng_entropy qui va réaliser l’action de la commande. Les données en sortie sont placées dans la structure src_struct qui sera utilisée pour générer une réponse. La fonction pb_encode va convertir cette structure en réponse qui sera envoyée à Android lors de la prochaine requête de lecture.
Conclusion
Il est curieux qu’à ce jour si peu de recherches aient été publiées sur ce composant alors même que Google propose d’offrir jusqu’à un million de dollars en bug bounty pour des vulnérabilités sur ce projet. On se demande aussi pourquoi les sources du projet n’ont pas été ouvertes alors que Google avait annoncé que ce projet serait open source.
Bien que ce composant dispose d’une armada impressionnante de protection contre les attaques matérielles, on peut aussi s’étonner de ne trouver qu’un simple canari comme protection logicielle.
Bien entendu, l’architecture semble faite pour limiter l’introduction de bugs : le logiciel est simple, la plupart des buffers sont alloués statiquement en mémoire, et les parseurs sont générés avec protobuf. On imagine aussi que Google a fait auditer ce produit, au moins en interne. On note d’ailleurs la présence de fichiers dédiés aux tests et au fuzzing dans les sources d’EC. Faut-il en conclure que ce projet est exempt de bugs ? L’avenir le dira…
Remerciements
Je tiens à remercier d33d34rt et doegox pour leur relecture attentive et leurs conseils avisés.
Références
[1] N. Zhang, K. Sun, D. Shands, W. Lou, Y. T. Hou, « TruSpy: Cache Side-Channel Information Leakage from the Secure World on ARM Devices », IACR Cryptology, 2016
[2] V. Bendeb, « Google Security Chip H1, A member of the Titan family », Open Source Firmware Conference, 2018, https://2018.osfc.io/uploads/talk/paper/7/gsc_copy.pdf
[ECSOURCES] https://chromium.googlesource.com/chromiumos/platform/ec/
[SUZYQ] https://chromium.googlesource.com/chromiumos/third_party/hdctools/+/HEAD/docs/ccd.md#suzyq-suzyqable
[PROTOBUF] https://developers.google.com/protocol-buffers
[NANOPB] https://github.com/nanopb/nanopb