- PROJEKT: OVERFLOW transforme l’assembleur RISC-V et les buffer overflows en règles de jeu de société, pour faire suivre directement les manipulations de mémoire, de pile et d’adresses de retour dans un cadre d’apprentissage
- Les joueurs partagent la même mémoire et le même programme et s’affrontent avec un ordonnancement préemptif où seulement 10 instructions sont exécutées par tour, sans mémoire virtuelle
- La partie se joue autour de la copie d’instructions existantes pour créer du shellcode, puis de l’écrasement de la return address adverse afin de l’envoyer vers
game_over() - Les accès mémoire invalides, les lectures/écritures non alignées et les instructions illégales provoquent un crash et l’exécution du gestionnaire d’exception, ce qui fait du changement d’adresse de trap et du monkeypatch
nopdes variables stratégiques clés - Une version web, un plateau imprimable et un assistant de jeu pour ESP32 et mobile sont proposés, mais certaines règles sont encore en cours d’ajustement, ce qui rapproche l’ensemble d’un puzzle de hacking expérimental
Objectif du jeu et modèle d’exécution
- PROJEKT: OVERFLOW est un projet qui transpose l’assembleur RISC-V et les buffer overflows en jeu de société sur table
- L’objectif principal est de copier des instructions existantes pour créer un petit shellcode en mémoire, de sauter vers ce code via un buffer overflow, puis d’écraser la return address de l’adversaire afin de lui faire appeler la fonction
game_over() - La stratégie ne se limite pas à l’exécution de code : elle inclut aussi la configuration du gestionnaire d’exception et même le monkeypatch
- Tous les joueurs partagent la même mémoire, le même programme et utilisent le même processeur en partage de temps
- Chaque tour exécute 10 instructions
- Le stack pointer de chaque joueur commence à un emplacement différent
- Il n’y a pas de mémoire virtuelle
Flux de build et génération du plateau
- Le code est compilé avec
riscv64-unknown-elf-gccpour une cible RV32- Les options principales incluent
-march=rv32g,-mabi=ilp32,-ffreestanding,-nostdlib,-nostartfiles,-O0, entre autres - Grâce à
-O0, le code machine est verbeux mais plus facile à suivre
- Les options principales incluent
- Les éléments du plateau sont générés en analysant la sortie de
riscv64-unknown-elf-objdump -S -l -fd game- Les instructions
▲et✎sont modifiées - Les offsets de saut sont convertis de l’hexadécimal au décimal
- L’assembleur est nettoyé et mis en correspondance avec le code source
- Un SVG est généré puis converti en PDF avec Inkscape
- Les instructions
Impression et matériel nécessaire
- Le plateau s’utilise en imprimant un PDF séparé en partie gauche et partie droite
- L’impression en A3 est préférable ; l’A4 fonctionne aussi mais reste petit
- Le matériel requis comprend 1 pion pour l’instruction
nop, 1 pion pour l’adresse de trap, 2 pions par joueur pour le program counter et le stack pointer, ainsi qu’un crayon et une gomme - La version web prend en charge le solo et le jeu entre amis, et un assistant de jeu pour ESP32 et mobile est également fourni
Règles de base et déroulement d’un tour
- L’état initial est le suivant
- Tous les registres démarrent à 0, mais le registre de return address
racommence à 1000 - Le
spdu Player 1 est initialisé à 2244, celui du Player 2 à 3844 - Le
pcdes deux joueurs démarre à l’adresse 1000, au début de la fonctionmain - Le pion de trap est placé à l’adresse 1000
- Toutes les adresses mémoire sont à 0, à l’exception du programme préchargé
- Le pion de l’instruction
nopn’est pas placé sur le plateau au départ
- Tous les registres démarrent à 0, mais le registre de return address
- À chaque tour, il faut exécuter 10 instructions, en suivant aussi les sauts comme
jaloubeq - Un joueur peut interrompre son tour après avoir exécuté au moins 1 instruction, puis reporter ses instructions restantes au tour suivant
- Le maximum cumulable est de 20 instructions
Monkeypatch et conditions de victoire
- Au début de chaque tour, après avoir exécuté exactement 1 instruction, il est possible de déplacer le pion de l’instruction
nopvers une adresse arbitraire d’une fonction qu’aucun joueur n’est en train d’exécuter - Quand le
pcatteint cette adresse, l’instruction se comporte comme une no-operation - Déplacer le pion
nopfait perdre le tour en cours et le suivant, tandis que l’adversaire peut exécuter jusqu’à 20 instructions à son prochain tour - La règle de monkeypatch n’est pas encore équilibrée et évolue encore légèrement tous les quelques jours
- En mode difficile, la partie se termine quand on pirate l’adversaire pour lui faire appeler la fonction
game_over() - Si aucun camp ne peut plus envoyer l’autre vers
game_over(), la partie est déclarée nulle - En mode facile, le premier joueur qui exécute
retdansmainet sort de la boucle principale gagne
Symboles spéciaux et gestion des exceptions
✎permet de choisir une valeur immédiate arbitraire de 12 bits entre 0 et 4095 pour l’instructionli▲permet de choisir, dans une instruction de chargement, une valeur dans une plage de ±128 octets autour de son propre stack pointer- Par exemple, si
spvaut 2180, il est possible de choisir entre 2052 et 2308
- Par exemple, si
- Les actions interdites provoquent un crash du programme
- écraser une adresse mémoire inférieure à 1192
- effectuer une lecture ou écriture non alignée sur une adresse non multiple de 4
- exécuter une instruction illégale
- En cas de crash, le gestionnaire d’exception s’exécute et saute vers l’adresse de trap
- L’adresse de trap vaut 1000 au départ, mais peut être écrasée dans la fonction
set_trap() - Lorsqu’une exception se produit, le program counter est défini à une valeur spécifique et l’exécution continue
- L’adresse de trap vaut 1000 au départ, mais peut être écrasée dans la fonction
- Si une triche ou une erreur est détectée, l’état du programme, la mémoire et les registres du joueur concerné sont réinitialisés
Règles d’extension à 3 ou 4 joueurs
- Le
spdu Player 3 est défini à 2116 - Le
spdu Player 4 est défini à 3716 - À partir de 3 joueurs, le symbole
▲ne peut être utilisé que dans la plage située à -128 octets du stack pointer - À plus de 2 joueurs, le jeu devient assez instable et se dégrade rapidement
- Atteindre une condition de victoire devient plus difficile, mais les parties gagnent en fun et en chaos
Exemples de stratégies de hacking
- Les crashes peuvent servir de stratégie offensive pour bloquer la progression adverse
- Si le trap handler est remplacé par la fonction
game_over, le premier joueur qui subit un crash perd- Dans cette configuration, le pion
nopdevient extrêmement puissant - Si l’adversaire place
nopsur leretd’une fonction que vous exécutez, cela peut vous faire perdre
- Dans cette configuration, le pion
- Dans la fonction
bug(), faire déborder l’index à 400 ou -400 permet d’accéder à la pile adverse et d’écraser sa return address- Par exemple, pour aller de l’adresse 3784 à 2184, il faut
(3784 - 2184) / 4 = 400, donc l’index-400
- Par exemple, pour aller de l’adresse 3784 à 2184, il faut
- La fonction
copy()permet de copier certaines instructions pour créer un shellcode court en mémoire- Un exemple de shellcode est la combinaison
li a4, ✎,li a5, ✎,sw a4, 0(a5),ret, qui effectue une écriture arbitraire - Si l’on copie l’instruction
ret, la return address est définie au point de départ du shellcode, ce qui crée une boucle infinie
- Un exemple de shellcode est la combinaison
- Dans la fonction
bug(), définir l’index à 6 permet d’écrire la variablevaluepar-dessus la return address sauvegardée sur la pile à28(sp)- Au retour de
bug(), la valeur de28(sp)est copiée dans le registre de return address - Si l’on y place l’adresse du shellcode créé, on peut sauter vers la mémoire
- Au retour de
Interprétation des instructions et modifications
- Tous les sauts sont relatifs au program counter courant, même si le désassembleur les affiche comme des adresses absolues
- Par exemple, le code machine 1903 de
jal a4, 0devient en exécution une boucle infinie
- Par exemple, le code machine 1903 de
- La liste des instructions valides du jeu est organisée parmi les instructions RV32 JRI de code machine 0 à 4095, sous des formes utilisant notamment
a0,a4,a5,sp,ra, etc. - Le journal des changements 0.0.6 inclut le remplacement de
while(run)parwhile(*prun)- L’adversaire peut ainsi provoquer de force un crash via un déréférencement non aligné
- La règle du NOP a été modifiée pour n’autoriser son placement que dans une fonction qui n’est pas en cours d’exécution
Design et ressources d’apprentissage
- Les rectangles à gauche et à droite du plateau forment un message binaire encodé en ASCII
- Les rectangles blancs valent 1, les rectangles noirs valent 0
- Les couleurs utilisées sont uniquement le rouge, le bleu, le noir et le blanc, afin de réduire les coûts d’impression et d’assurer une bonne lisibilité en noir et blanc
- Il n’y a pas de coloration syntaxique
- Le but est d’éviter qu’une partie du code paraisse plus importante selon le thème visuel, et de favoriser une lecture attentive avec jugement personnel
- Comme ressources pour apprendre l’assembleur RISC-V, on trouve riscv-programming.org, cs3410 risc-v interpreter, rvcodecjs de luplab
- Pour apprendre le C, le début de Beej's Guide to C Programming est utilisé
- Des PDF imprimables d’exercices d’assembleur couvrant les variables, les appels de fonction, les pointeurs, les chaînes, les structures, les tableaux et la récursivité sont fournis, ainsi qu’une version « assembly hangman » à trous
1 commentaires
Avis de Hacker News
Vraiment impressionnant. Ce qui me paraît le plus remarquable, c’est surtout d’avoir réussi à faire jouer sa fille de 12 ans avec lui.
Quand peut-on espérer une version CHERI ? :-D
Core War est un jeu qui se déroule dans l’arène mémoire d’une machine virtuelle prenant en charge un langage d’assemblage simulé très simple. Je l’ai découvert pour la première fois dans Scientific American en 1984 et, comme je programmais déjà depuis une quinzaine d’années, j’ai compris qu’il s’inspirait de Darwin, un jeu plus ancien des Bell Labs.
Darwin a été créé en 1961 et tournait sur IBM 7090. Les programmes y rivalisaient pour les ressources, et celui qui parvenait à se répliquer dans tout l’espace alloué gagnait. Le jeu n’a pas duré très longtemps après que Robert Morris Sr. a créé un programme imbattable. Voir [2].
Au milieu des années 1970, Software Practice and Experience était l’une de mes revues d’informatique préférées, avec une rubrique Computer Recreations souvent publiée sous le pseudonyme Aleph-Null. Quand j’étais en troisième cycle, je me suis amusé à implémenter plusieurs des jeux présentés dans cette rubrique. La revue est chère, mais si vous êtes étudiant, vous pourrez probablement la trouver en bibliothèque universitaire, comme moi à l’époque. Les numéros des années 1970 étaient faciles et plaisants à lire, avec des sujets comme les compilateurs Pascal, Algol 68 et la programmation concurrente ; c’est via des articles de N. Wirth que j’ai découvert Module[3,4] puis, plus tard, Oberon[5].
[1] https://en.wikipedia.org/wiki/Core_War
[2] https://en.wikipedia.org/wiki/Darwin_(programming_game)
[3] https://onlinelibrary.wiley.com/doi/abs/10.1002/spe.43800701...
[4] https://onlinelibrary.wiley.com/doi/abs/10.1002/spe.43800701...
[5] https://onlinelibrary.wiley.com/doi/abs/10.1002/spe.43801909...
J’avais un ami qui disait aimer les jeux mais ne pas avoir l’esprit fait pour coder ; avec Human Resource Machine, il s’est retrouvé à coder de fait, et certaines de ses solutions étaient meilleures que les miennes malgré mes années d’expérience.
Mon enfant de 12 ans déteste les maths, mais se débrouille étonnamment bien à Human Resource Machine et SpaceChem. Je me demande si les maths du lycée et celles de la programmation ne sont pas fondamentalement différentes.
Très intéressant. Vu la taille de la mémoire des ordinateurs actuels, j’ai toujours eu l’impression que les mnémoniques courts étaient un mauvais choix d’ingénierie.
Ici aussi, la première chose à faire est d’apprendre et de mémoriser ce que font les instructions. Si l’on remplaçait les noms par des formes plus explicites, ce serait beaucoup plus facile à apprendre, à retenir et à lire dans le code. Je trouve suspect que les gens ne le fassent pas plus souvent.
Le fait même que ce type de vulnérabilité soit possible me semble aussi indiquer un échec de conception du système dans son ensemble. Cela ne veut pas dire que ce n’est pas un jeu amusant ni une bonne façon d’apprendre, mais en ingénierie, les problèmes structurels sont acceptés trop facilement. La plupart des gens ne voient même plus ce défaut structurel.
Je pense que les enfants réagissent vraiment bien quand on ne les sous-estime pas. En tout cas, c’était le cas de la mienne.
Pensez-vous vraiment que certains ne voient pas les lectures et écritures arbitraires comme un défaut structurel ? Des milliers de personnes travaillent sur ce problème et ont fait de réels progrès. En même temps, je trouve toujours que peek et poke restent amusants.
C’est vraiment excellent. J’ai envie d’essayer ça au bureau.
Ça a l’air assez amusant. À quelle tranche d’âge pensez-vous que ce soit adapté ?
bug(), me semble accessible à des enfants de 10 à 15 ans.Ma fille a 12 ans et nous nous amusons bien ensemble. La condition de victoire difficile, qui consiste à faire sauter l’adversaire vers la fonction
game_over(), est plus compliquée, mais je pense qu’on peut y arriver en 5 ou 6 mois.Pour les adultes, je ne sais pas trop. Certaines personnes ont une peur de l’assembleur comme si c’était une création du diable, donc il pourrait être plus difficile de les faire jouer que des enfants.
Ce qui est intéressant, c’est que nous avons tendance à voir le monde comme un miroir de nous-mêmes.
Quelle est la probabilité que, parce que je m’intéresse aux buffer overflows et à la programmation, ma fille s’y intéresse forcément beaucoup aussi ? Si c’est mon premier enfant, et que le deuxième est une fille, la probabilité me paraît encore plus faible, mais je vois pourtant beaucoup de pères persister.
Quand on mène ce genre de projet, je me demande si l’on a conscience, au moins dans une certaine mesure, qu’il s’agit d’un projet de vanité. Quoi qu’il en soit, moi, ce sujet m’intéresse, donc je suis content qu’il l’ait publié.
Vous suggérez que le créateur du projet impose cela à sa fille par vanité, mais sur quoi vous fondez-vous ? J’ai parcouru quelques pages du site et je n’ai rien vu qui aille dans ce sens ; au contraire, il y a plusieurs formulations mesurées indiquant que sa fille s’amuse et s’y intéresse beaucoup.
Pourquoi exclure la possibilité que cela ait commencé parce que sa fille se demandait sans cesse ce que son père faisait sur son ordinateur ? Cela a pu commencer modestement puis évoluer en un processus à double sens entre quelqu’un qui partage son intérêt et une jeune coexploratrice.
Je ne sais pas ce qu’il en est réellement, mais vous non plus. Pour avoir participé à l’éducation pendant quelques années, je trouve que les enfants sont des apprenants bien plus capables qu’on ne le croit généralement. La structure de l’école y est sans doute pour quelque chose, mais au fond, il y a peut-être aussi ce genre de croyances limitantes. J’ai envie d’applaudir ce père qui a voulu partager ses centres d’intérêt et sa passion avec sa fille et avec le monde.
Certaines de ces choses auront de la valeur, d’autres non. Les probabilités sont toujours défavorables. C’est la vie.
Quand le chemin de code RISC-V 64 bits sera stabilisé, fonctionnera suffisamment bien et que même les « buffer overflows » auront disparu, comment feront-ils pour l’obsolescence programmée si C/C++ ne change pas sans cesse de syntaxe ? Pauvres âmes…
Attendez un peu.
Un jeu de plateau avec du codage en assembleur ? Pourquoi n’y ai-je pas pensé avant ? :D
PL/I avait fait les choses correctement pour certains aspects : vérification des bornes des chaînes/tableaux, pile qui croît vers le haut plutôt que vers le bas, etc.
https://www.acsac.org/2002/papers/classic-multics.pdf