Petite rétro-ingénierie d'un fichier obfusqué de sauvegarde du jeu Orcs Must Die! sortit en 2011. Un jeu de type Tower Defense développé par des anciens du mythique Ensemble Studio à l'origine des Age of Empires.
Il s'agit plus ici de s'amuser à modifier sa sauvegarde que de jouer au jeu (chacun trouve son amusement là où il veut)...
Sommaire
Deviner ce que contient le fichier : chiffrement, compression ou simple obfuscation ?
Le nom du fichier de sauvegarde profiles.xml
en dit déjà long, le XML est un langage de balisage. On y retrouve des balises encadrées par les chevrons < et >, contenant des données diverses.
On s'attend à avoir une certaine répétition de ces balises. Or l'ouverture du fichier permet précisément d'identifier des séquences répétées.
Ceci permet de supposer que le fichier n'est pas compressé. Par ailleurs, le poids du fichier est de 24Ko, ce qui dans le cas d'un résultat de compression correspondrait à un fichier d'origine plutôt conséquent qui ne cadre pas vraiment avec ce qu'on attend d'une simple sauvegarde.
Étude de l'entropie
En chimie physique ou thermodynamique, l'entropie est la mesure du désordre d'un système. Ainsi l'entropie sera plus importante dans un verre d'eau que dans un verre de glace, les molécules d'oxyde de protium étant libres et mobiles dans le premier, et fixées dans une structure cristalline dans le second.
En informatique, Claude Shannon a apporté une définition similaire proche, mesurant la quantité d'information contenue dans un signal ou un ensemble d'octets.
Nous avons sous les yeux un fichier contenant des séquences répétées, il est attendu que son entropie soit plutôt basse (redondance = moins d'information, moins d'information = entropie basse et entropie basse = mauvais chiffrement).
L'utilitaire binwalk
permet entre autres d'obtenir une courbe de l'entropie en fonction de la position dans le fichier.
Les courbes seraient différentes si le fichier avait été chiffré ou compressé. Le chiffrement tend à maximiser l'entropie sur l'intégralité des données ; le but étant de rendre le contenu chiffré indiscernable d'un contenu purement aléatoire. La compression augmente l'entropie mais ce n'est pas son but premier qui reste la suppression des motifs ; il peut alors y avoir des variations locales importantes. Sur un fichier plus gros la différence est flagrante.
Recherche de chaînes de caractères
L'usage de la commande strings
est toujours très utile avant toute chose. En ciblant la recherche sur les éléments "Profiles", "xml", "</" (marqueur de balise de fermeture) on trouve les chaînes suivantes quasi côte à côte :
profiles.xml
lastlogin
version
Profile
La décompilation avec IDA, puis la recherche de ces quelques mots mène à une section de données où les chaînes statiques sont stockées :
C'est là que la commande strings montre ses limites; en effet les chaînes <Profiles lastlogin="%s" version="%d">
et </Profiles>
n'ont pas été trouvées. Or il s'agit très probablement des balises présentes au début et à la fin du fichier.
lastlogin
pourrait être un timestamp ou le nom du joueur, toutefois le placeholder%s
semble être en faveur de la dernière hypothèse.
Implémentation
Méthode simple : utiliser le programme à son avantage
Dorénavant on sait par quoi le fichier commence et quels sont les premiers octets stockés dans le fichier mystère ; procéder par remplacement permet d'avancer très vite mais de nombreux caractères demeureront inconnus.
Créons un profil de joueur avec un nom facilement reconnaissable (avec séquence répétée) pour que le jeu effectue les conversions manquantes.
Ici, avec un profil nommé ooooo1234567890
:
00000000 16 7A 58 45 4C 43 46 4F 59 0A 46 4B 59 5E 46 45
< P r o f i l e s l a s t l o
00000010 4D 43 44 17 08 4545454545 1B 18 19 1E 1F 1C 1D 12 13 1A
g i n = " o o o o o 1 2 3 4 5 6 7 8 9 0
08 0A 5C 4F 58 59 43 45 44 17 08 1B 08 14
" v e r s i o n = " 1 " >
À partir de là il est simple de faire un programme lisant les octets et effectuant un remplacement automatique pour tous ces caractères.
Le reste des caractères inconnus suivra très facilement.
Nous obtenons rapidement un fichier en clair et sommes capables d'effectuer l'opération inverse.
Opération XOR : faire plus propre et plus rapide avec un peu de mathématiques
Ceci était une idée assez "rapide" et simple à mettre en œuvre, il y avait toutefois une piste encore plus simple à tester que j'aurais pu explorer plus tôt (les fils ne se touchent pas toujours très vite...).
L'opération XOR (OU exclusif) a quelques particularités mathématiques intéressantes. Voici tout d'abord sa table de vérité :
0 ⊕ 0 = 0
0 ⊕ 1 = 1
1 ⊕ 0 = 1
1 ⊕ 1 = 0
Maintenant imaginons A, le caractère ou le texte à chiffrer, et B la clé. C se trouve être le résultat de l'opération XOR :
A ⊕ B = C
Or comme B ⊕ B = 0
, réappliquer B à C permet de retrouver A. Par remplacement on obtient ceci :
C ⊕ B = A ⊕ B ⊕ B = A
C'est une opération fortement utilisée en cryptographie, comme ici en chiffrement symétrique. Elle est tout à fait robuste à condition que la clé soit strictement aléatoire, de taille au moins équivalente au message à chiffrer, et utilisée une seule fois (masque jetable).
En outre, il est possible de récupérer la clé lorsqu'on est en possession du couple de données chiffrées et déchiffrées :
A ⊕ C = A ⊕ A ⊕ B = B
Testons l'hypothèse d'une clé unique de 1 octet avec quelques couples connus :
>>> [hex(ord(clear) ^ cipher) for clear, cipher in [("a", 0x4B), ("b", 0x48), ("c", 0x49)]]
['0x2a', '0x2a', '0x2a']
Curieuse similarité n'est-ce pas ? Bon et puis 0x2a
en décimal correspond au fameux nombre 42. Évidemment ! Trois lettres suffisaient donc pour généraliser.
Deux des trois règles énoncées pour assurer la robustesse de l'opération XOR ne sont pas respectées : réutilisation et taille.
Validation par décompilation
Était-il possible de s'y prendre autrement, par décompilation du binaire par exemple ?
Lors de la décompilation ma version d'IDA identifie bien les fonctions mais ne produit pas de code. Le binaire gérant cette partie du jeu est un plugin et je pense que ça doit jouer puisque tous les segments sont de type .text
. Les versions récentes devraient corriger ce problème.
C'est l'occasion d'utiliser Ghidra à la place. Cette fois le code est correctement traité et le pseudocode C généré est propre.
Comme toujours dans ce cas d'usage il faut avoir une vague idée de ce que l'on cherche :
- L'idéal serait de tomber sur la routine qui sauvegarde le fichier de profils.
- On cherche une fonction qui traite les chaînes connues dans le fichier XML, je pense aux chaînes de header et de footer
<Profiles lastlogin="%s" version="%d">
et</Profiles>
. - Il doit y avoir à proximité au moins une boucle prenant les données du fichier constitué en entrée.
- On cherche donc en priorité les mots clés do/while/for.
Une rapide recherche de la chaîne <Profiles lastlogin=...
, puis de ses références (zones où la chaîne est utilisée dans le code) nous amène à une unique fonction :
On y trouve la routine de création du fichier XML; repérer les balises du header et du footer XML entourant une boucle do/while assez obscure de formatage des données.
Pas question de se lancer dans le reversing de ce code à moins d'y être forcé. Regardons plus loin.
Trouvé ! Une seconde boucle do/while dans la même fonction (ce n'était pas forcément attendu) et une opération XOR avec le nombre 0x2a.
Pas la peine d'aller plus loin, la routine de chiffrement est trouvée !
Conclusion
Nous avons vu plusieurs méthodes pour déobfusquer un fichier inconnu. Il s'agit de méthodes très générales et applicables dans de nombreux cas sur des fichiers malicieux, en CTF, cracking, etc.
Il ne reste qu'à packager tout ça dans un programme Python avec une interface en ligne de commande et quelques options (qui code encore des morceaux de scripts mal foutus qui resteront oubliés dans un coin de disque dur ?), puis à envoyer ça sur GitHub !
Lien vers le dépôt: OMD_Profile_Editor.
Le programme peut modifier à volonté le niveau du joueur, activer/désactiver les améliorations du grimoire et activer le mode nightmare.
Have fun.