Brancher un disque dur externe sur une XBox : une plongée dans les ACL NTFS
La XBox Series S est, selon moi, une très bonne console à utiliser comme station de rétro-gaming dans son salon. Elle ne coûte pas très cher (elle peut se trouver dans les 200€), et pour un unique paiement de 15€ au diable à Microsoft, on peut la passer en mode développeur, et installer des applications arbitraires dessus, dont RetroArch. De cette manière, c’est facile de brancher un disque dur externe sur le port USB de facade, mettre quelques centaines de Go de ROMs (obtenues légalement si vous le souhaitez) dessus, et jouer des nuits entières aux plus grands classiques du jeu vidéo.
Encore faut-il que le disque dur soit reconnu par la console.
Les différentes étapes de l’article sont :
Un peu de contexte
Après avoir passé la console en mode développeur et installé RetroArch (de nombreux guides existent pour ces étapes) vient le moment de brancher un disque dur ou une clé USB avec ses ROMs sur la console. Il s’agit d’une console Microsoft ; je prends donc un disque formaté en NTFS, le système de fichier privilégié par Windows. Plus qu’à le brancher, et tout fonctionne ! Il apparait sous la lettre E:/, et… est vide ?
Quelques recherches mènent à penser que c’est un problème de métadonnées, probablement du coté des permissions. Il existe même un petit outil, XboxMediaUSB, qui promet de résoudre tous ces problèmes de permissions pour vous, et si vous avez un Windows, cet outil est effectivement la solution. Seulement je n’en ai pas, il est impossible à lancer avec Wine car les disques et partitions sont gérées fondamentalement différemment sous Windows et sous Linux, et aucun outil n’existe sous Linux pour faire la même chose… Mais qu’est-ce qu’il fait exactement ? Et est-ce qu’on ne pourrait pas réussir à le faire à la main ?
Analyse des sources de XboxMediaUSB
Je ne connais pas du tout le .NET, mais une petite lecture du code source de l’application devrait nous permettre de mieux comprendre les étapes nécessaires pour que nos fichiers soient visibles par la console. Le README nous explique que deux options sont possibles : un formatage complet du disque (en créant éventuellement une arborescence de dossiers dessus), ou une simple modification des permissions du disque. Essayons de suivre ce qui se passe quand on clique sur ce deuxième bouton.
En recherchant le texte de ce bouton d’après la capture d’écran du README (“Add permissions to drive”), on arrive sur la ligne Newest/XboxMediaUSB-N/MainWindow.xaml.vb:225
, qui nous indique que c’est le texte de SetPermissionsButton
. En cherchant ce nom dans ce même fichier, on arrive ligne 260, qui nous explique que le bouton s’appelle en fait AddPermissionsButton
. Et je soupçonne que son comportement est défini par la fonction AddPermissionsButton_Click
, définie ligne 329.
Private Sub AddPermissionsButton_Click(sender As Object, e As RoutedEventArgs) Handles AddPermissionsButton.Click
If DriveList2.SelectedItem IsNot Nothing Then
'Set the SelectedDrive
Dim SelectedDriveLetter As String = CType(DriveList2.SelectedItem, String)
SelectedDrive = New DriveInfo(SelectedDriveLetter.Split(CChar(vbTab))(0))
PermissionWorker.RunWorkerAsync()
Else
MsgBox("Please select a drive first", MsgBoxStyle.Exclamation)
End If
End Sub
Tout ce que cette fonction fait, c’est lancer PermissionWorker.RunWorkerAsync()
. Cherchons son code, ligne 359 :
Private Sub PermissionWorker_DoWork(sender As Object, e As DoWorkEventArgs) Handles PermissionWorker.DoWork
Try
Dim AllApplicationPackagesUser As New SecurityIdentifier("S-1-15-2-1")
Dim CurrentDirPermissions As DirectorySecurity = Directory.GetAccessControl(SelectedDrive.Name)
Dim SecurityRule As New FileSystemAccessRule(AllApplicationPackagesUser,
FileSystemRights.FullControl,
InheritanceFlags.ContainerInherit Or InheritanceFlags.ObjectInherit,
PropagationFlags.None,
AccessControlType.Allow)
CurrentDirPermissions.AddAccessRule(SecurityRule)
If Dispatcher.CheckAccess() = False Then
Dispatcher.BeginInvoke(Sub() Directory.SetAccessControl(SelectedDrive.Name, CurrentDirPermissions))
Else
Directory.SetAccessControl(SelectedDrive.Name, CurrentDirPermissions)
End If
Catch ex As Exception
MsgBox(LanguageConfig.ReadValue("Errors", "CouldNotSetPermissions") + " " + SelectedDrive.Name)
End Try
End Sub
Cette fois, on dirait que c’est la bonne ! Cette fonction définit un SecurityIdentifier à partir d’une valeur codée en dur (qui reviendra surement plus tard…), récupère le dossier racine du disque à modifier, et lui ajoute une AccessRule avec comme paramètres :
- le SecurityIdentifier
FileSystemRights.FullControl
: probablement le niveau de droits ?InheritanceFlags.ContainerInherit Or InheritanceFlags.ObjectInherit
: des flags difficiles à interpréter sans plus de contextePropagationFlags.None
: pareilAccessControlType.Allow
: une AccessRule peut donc probablement donner ou retirer des droits d’accès, et ici on en donne
Et c’est tout ce que ce bouton fait. Il suffit donc de reproduire ce comportement sous linux, et c’est gagné ! Où est définie cette classe FileSystemAccessRule
exactement ?
Dans la librairie standard .NET, qui n’est évidemment pas opensource. Oh. Ce n’est pas comme ça qu’on connaitra son comportement.
Essayons d’en savoir un peu plus sur ces ACL NTFS. De mon expérience, la meilleure documentation des API Microsoft est souvent dans les projets opensource qui les utilisent (par exemple, LLVM pour les formats d’exécutables Windows). Allons jeter un coup d’oeil du coté de ntfs-3g
, le driver NTFS pour Linux.
Le driver ntfs-3g
Une petite recherche dans la documentation du driver nous donne un très bon espoir : il permet d’extraire et de modifier les ACL d’un fichier ou d’un dossier ! Essayons cette commande sur un disque NTFS qui traine sur mon bureau, pour voir à quoi ressemblent ces ACL !
> getfattr -h -e hex -n system.ntfs_acl /mnt/file.txt
getfattr : Suppression des « / » en tête des chemins absolus
# file: mnt/file.txt
system.ntfs_acl=0x0100048014000000240000000000000034000000010200000000000520000000200200000102000000000005200000002002000002001c000100000000031400ff011f00010100000000000100000000
… D’accord, peut-être que le code source du driver a plus d’informations sur comment l’interpréter ?
Et la réponse est non. J’ai perdu quelques heures à fouiller le code source, sans succès : ces permissions étant ignorées sous Linux, elles ne sont pas analysées plus que ça. Le driver permet uniquement de les extraire (ce qu’il a fait ici) et de les remplacer par d’autres ACL sur le même format. Il va donc falloir trouver une autre source d’information.
Avec un peu plus de recul, le driver est bel et bien capable d’analyser ces ACL, puisqu’il est capable de vérifier que la nouvelle ACL définie est valide (en vérifiant notamment les Size et Offset). Je n’ai donc juste pas assez cherché dans le code source.
Analyse des ACL d’un fichier NTFS
C’est à ce moment-là que je suis arrivée sur la documentation de l’ancien driver NTFS de Linux : https://flatcap.github.io/linux-ntfs/ntfs/index.html. Cette documentation, basée sur de la rétroingénierie, est une vraie mine d’or pour mieux comprendre le fonctionnement du système de fichier. La page qui va nous intéresser le plus est celle sur le Security Descriptor.
Si le fonctionnement de NTFS vous intéresse, je vous recommande vivement de lire toute cette documentation !
Essayons de décomposer la sortie de getfattr
pour voir si elle correspond bien à ce Security Descriptor.
Celui-ci est composé d’un header, d’une première ACL (facultative) appelée SACL, d’une deuxième ACL, appelée DACL, qui peut elle-même être composée de plusieurs ACE suivies d’un SID, et enfin de deux SID.
Qu’est-ce que tous ces mots veulent dire ?
- ACL : Access Control List, la liste des autorisations et interdictions qui s’appliquent à un fichier ou à un dossier
- ACE : Access Control Entry, une entrée de la liste précédente
- SID : Security IDentifier, on reviendra dessus un petit peu plus bas
Commençons par le header : il est censé occuper les 20 premiers octets.
01 00 0480 14000000 24000000 00000000 34000000
| | | | | | |
| | | | | | Offset to DACL: 52B
| | | | | Offset to SACL: 0B
| | | | Offset to Group SID: 36B
| | | Offset to User SID: 20B
| | Flags: Self Relative, DACL Present
| Padding
Revision
Bingo, tout correspond ! Notons dès maintenant que tout est en little-endian, ce sera important par la suite. Continuons avec les champs suivants, le User SID et le Group SID :
01 020000000000 05 20000000 20020000
| | | |
| | | Subauthorities
| | Authority
| Number of subauthority
Revision
Les deux sont identiques et suivent bien notre notice (en devinant le nombre d’octets que prennent les différents champs…). Enfin, on a ensuite le DACL, l’ACL qui définit les permissions :
02 00 1c00 01 000000 ACL Header
| | | | |
| | | | Padding
| | | Count: 2 ACE
| | Size: 28B
| Padding
Revision
00 03 1400 ff011f00 ACE Header
| | | |
| | | Access mask
| | Size: 20B
| Flags: Object inherits ACE, Container inherits ACE
Type: 0x00 = Allow
01 010000000000 01 00000000 SID
| | | |
| | | Subauthorities
| | Authority
| Number of subauthority
Revision
Dans cet exemple, il n’y a pas de SACL, aussi appelé Audit ACL. Son offset est à 0, et le premier header n’a pas le flag SACL Present
Le drive nous extrait donc bien le Security Descriptor. Essayons de comprendre un petit peu mieux ce que nous avons sous la main. Commençons par le SID. Wikipedia nous aide à comprendre qu’il s’agit d’un identifiant représentant pour Windows un compte, un groupe, etc., et qui se représente sous la forme S-1-5-18
. Dans notre cas, nous avons S-1-5-32-544
comme User SID et Group SID, qui représente d’après Wikipedia le compte et le groupe Administrateur, puis dans notre unique ACE nous avons S-1-1-0
qui représente “Nobody”.
Ainsi, les permissions pour ce fichier sont :
- Son propriétaire est l’Administrateur
- Nobody a le droit de tout faire sur ce fichier
- Et c’est tout
Je ne vais pas détailler ce que contient l’Access Mask. Pour ce blogpost, il suffit de savoir que
ff011f00
représente “tous les droits”
A partir de toutes ces informations, j’ai développé un petit script Python qui permet de décrire les ACL d’un fichier donné. Lançons le sur le fichier précédent :
> ~/Documents/ntfs_acl_editor/main.py `getfattr -h -e hex -n system.ntfs_acl /mnt/file.txt | grep '=' | sed -e 's/^.*=0x//'`
Header:
Revision: 1
Flags: DACL_PRESENT|SELF_RELATIVE
User offset: 20
Group offset: 36
SACL offset: 0
DACL offset: 52
Audit ACL: None
Permission ACL:
Header:
Revision: 2
Size: 28
Count: 1
ACE list:
Header:
Type: ALLOW
Flags: OBJECT_INHERIT|CONTAINER_INHERIT
Size: 20
Access mask: 0x1f01ff
SID: S-1-1-0
User: S-1-5-32-544
Group: S-1-5-32-544
On y voit déjà un peu plus clair.
Première approche : injection d’ACE
Maintenant qu’on comprend mieux comment fonctionnent les permissions NTFS, revenons à notre objectif. Le but serait de modifier les ACL du dossier racine afin de rajouter une entrée à la liste, sur le même modèle que XboxMediaUSB. Avec nos connaissances actuelles, on devine mieux la forme de l’entrée à ajouter :
Header:
Type: ALLOW
Flags: OBJECT_INHERIT|CONTAINER_INHERIT
Size: 24
Access mask: 0x1f01ff
SID: S-1-15-2-1
Il suffit donc d’ajouter cette entrée, et de modifier les champs Size et les Offsets lorsque c’est nécessaire, puis de reconstruire la chaine hexa en respectant les offsets.
Deuxième approche : on jette tout et on recommence
Mais avant de se lancer dans le développement d’un script pour automatiser ça, regardons en arrière et réfléchissons si on peut pas faire un peu plus simplement.
Après tout, notre but est juste d’avoir un disque dur branché à la console, qui fonctionne avec la console, et sur lequel on puisse rajouter des jeux depuis une machine linux. Est-ce qu’on a vraiment besoin que le disque fonctionne sous Windows… On peut peut-être trouver une solution beaucoup plus simple et rapide, quitte à écraser toutes les ACL existantes ?
Essayons de construire l’ACL minimale pour notre besoin :
Header:
Revision: 1
Flags: DACL_PRESENT|SELF_RELATIVE
User offset: 72
Group offset: 84
SACL offset: 0
DACL offset: 20
Audit ACL: None
Permission ACL:
Header:
Revision: 2
Size: 52
Count: 2
ACE list:
Header:
Type: ALLOW
Flags: OBJECT_INHERIT|CONTAINER_INHERIT
Size: 20
Access mask: 0x1f01ff
SID: S-1-1-0
Header:
Type: ALLOW
Flags: OBJECT_INHERIT|CONTAINER_INHERIT
Size: 24
Access mask: 0x1f01ff
SID: S-1-15-2-1
User: S-1-5-32-544
Group: S-1-5-32-544
Mieux vaut laisser l’ACE pour Nobody, étant donnée qu’elle est présente sur toutes les ACL que j’ai pu examiner. Plus qu’à “compiler” ça en hexa :
0x0100048048000000580000000000000014000000020034000200000000031400ff011f0001010000000000010000000000031800ff011f00010200000000000f02000000010000000102000000000005200000002002000001020000000000052000000020020000
Essayons de mettre naïvement cette ACL sur la racine du disque :
$ sudo setfattr -n system.ntfs_acl -v "0x0100048048000000580000000000000014000000020034000200000000031400ff011f0001010000000000010000000000031800ff011f00010200000000000f02000000010000000102000000000005200000002002000001020000000000052000000020020000" /mnt
On branche le disque sur la console, et… Le disque est reconnu, on voit tous les dossiers à la racine, mais ils apparaissent vides. Il va donc falloir l’appliquer sur tous les fichiers et les dossiers. Et en le faisant, les fichiers sont bien reconnus sur la console !
Si vous êtes attentif, vous aurez remarqué que certains flags parlent d’héritage de permission. Alors pourquoi ne peut-on pas juste modifier les permissions du dossier racine et les faire hériter sur tout le reste de l’arborescence ?
Je n’ai malheureusement trouvé aucune documentation sur le comportement de cet héritage, et j’ai pu observer que l’unique appel système que fait XboxMediaUSB injecte bien une ACE sur tous les fichiers et dossiers récursivement… J’ai donc simplement reproduis ce fonctionnement.
Conclusion et tutoriel
Avec tout ce qu’on a vu jusque là, j’ai développé un petit script python qui permet de visualiser les ACL d’un fichier (ou dossier) sur un disque NTFS, et qui permet de soit injecter la règle nécessaire dans les ACL d’un fichier, soit de remplacer brutalement toute l’ACL par celle construite ci-dessus, et ce récursivement. Je m’attendais à ce que la première solution soit beaucoup plus lente que la deuxième, mais à ma grande surprise, les deux sont à peu près aussi rapides sur de grandes quantités de fichiers (je n’ai pas fait de benchmark rigoureux).
Si vous êtes venus jusque là pour des instructions simples pour faire fonctionner un disque dur avec RetroArch sur Xbox Series depuis un ordi linux :
- Installez
ntfs-3g
etpython-xattr
avec votre gestionnaire de paquet favoris - Récupérez mon script
- Partitionnez votre disque en NTFS
- Mettez toutes vos ROMs dessus
- Lancez
python3 ntfs_acl_tool.py edit --inject --recursive /path/to/your/drive
Et votre disque devrait fonctionner parfaitement sur la Xbox. N’oubliez pas de relancer la dernière commande à chaque fois que vous rajoutez de nouveaux fichiers !