Ceci est un post invité de MrAaaah posté sous licence creative common 3.0 unported.
Salut à toutes et à tous c’est MrAaaah, je suis celui qui réussi à résoudre le plus rapidement les énigmes du coffre secret de Sam et Max, on ma demandé de vous faire un "petit" article expliquant un peu ma démarche et mes méthodes.
Avant-propos
Avant de me lancer dans la résolution des énigmes je tiens à signaler que je n’ai qu’un an de python dans les pattes, de ce fait le code peut paraitre cradoc et peut-être un poil bourrin vu que je ne connais pas encore très bien python, ses librairies et ses subtilités (ça a également été codé très vite). Je n’ai pas retouché le fonctionnement de mes scripts, j’ai juste renommé quelques variables et mis des commentaires, donc c’est du brut, y’a pas de vérif, ça plante à la fin, etc.
Y U NO OPEN?
Première énigme http://game.sametmax.com/ : un message, un champ d’entrée et une image. Comme beaucoup je suppose, je commence par rentrer diverses conneries dans le formulaire, toujours le même résultat : le message “I don’t GET it.”.
Je parcours le code source de la page à la recherche d’un éventuel script où autre indice dans le code, nada.
Si on regarde l’url on peut voir que le code est envoyé via la méthode GET : http://game.sametmax.com/?code=monnezsurtescouilles. En relisant le message de ci-dessus on peut deviner qu’il faut balancer une requête de type POST. Pour faire ça rien de plus simple, on sort Firebug (ou équivalent), on édite le code source en changeant l’attribut method du formulaire et on renvoi avec un code bidon.
Là on obtient un nouveau message : “Error log : areyouhuman”. Bon je retente des codes bidon (genre “yes”, etc.). Je cherche sur le net “areyouhuman”, mais rien de concluant.
Au final rien de bien complexe, mais y’a moyen de tourner un peu en rond, il suffit «juste» d’allez sur l’URL http://game.sametmax.com/areyouhuman
areyouhuman
À partir de là des connaissances en Python (basiques) vont être nécessaires. (j’utilise ici Python 2.7)
La page nous renvoie ce qui ressemble à une définition de classe NextUrlContainer avec un attribut next suivi d’une bouillie de caractères en commentaire.
NextUrlContainer = type("NextUrlContainer", (), {'__init__': (lambda s, n: setattr(s, 'next', n))}) # Y2NvcHlfcmVnCl9yZWNvbnN0cnVjdG9yCnAwCihjX19tYWluX18KTmV4dFVybENvbnRhaW5lcgpw MQpjX19idWlsdGluX18Kb2JqZWN0CnAyCk50cDMKUnA0CihkcDUKUyduZXh0JwpwNgpJMjA5NzM1 MQpzYi4= |
Avec un peu de recherche on découvre que la soi-disant bouillie n’est autre que du texte encodé en base64. Une fois décodé (il y a des utilitaires en ligne qui font ça très bien), on obtient quelque chose qui nous parle un peu plus (quoique).
ccopy_reg _reconstructor p0 (c__main__ NextUrlContainer p1 c__builtin__ object p2 Ntp3 Rp4 (dp5 S'next' p6 I2097351 sb. |
Encore un peu de recherche et je découvre que c’est un fameux pickle, on sort notre Python préféré.
# -*-coding:Utf-8 -* import base64, pickle NextUrlContainer = type("NextUrlContainer", (), {'__init__': (lambda s, n: setattr(s, 'next', n))}) b64 = "Y2NvcHlfcmVnCl9yZWNvbnN0cnVjdG9yCnAwCihjX19tYWluX18KTmV4dFVybENvbnRhaW5lcgpw MQpjX19idWlsdGluX18Kb2JqZWN0CnAyCk50cDMKUnA0CihkcDUKUyduZXh0JwpwNgpJMjA5NzM1 MQpzYi4=" # on décode et on dépickle-ise obj = pickle.loads(base64.urlsafe_b64decode(b64)) print obj.next |
Et on obtient un objet de type NextUrlContainer contenant l’id de la prochaine URL : 2097351.
En ce rendant sur http://game.sametmax.com/areyouhuman/2097351 on se retrouve avec de nouveau un pickle encodé en base64. Après en avoir fait deux URL à la main, on ressort Python pour automatiser tout ça.
Notre script doit :
-
Récupérer le contenu de la page http://game.sametmax.com/areyouhuman/:id. (j’utilise ici la librairie httplib)
-
Décoder le contenu. (base64)
-
Dépickle-iser (je ne connais pas le terme “officiel”). (pickle)
-
Récupérer le prochain id.
-
Recommencer à la première étape jusqu’à… la fin.
Voilà le script utilisé. Il affiche chaque nouvel id et plante quand on arrive au bout.
# -*-coding:Utf-8 -* import httplib import base64, pickle # la classe donnée sur la page http://game.sametmax.com/areyouhuman NextUrlContainer = type("NextUrlContainer", (), {'__init__': (lambda s, n: setattr(s, 'next', n))}) # initialisation de la connection connection = httplib.HTTPConnection("game.sametmax.com:80") # Fait une requête sur l'URL http://game.sametmax.com/areyouhuman/:id # Et retourne le contenu de la page def make_request(id): url = "/areyouhuman/%i" % id connection.request("GET", url) response = connection.getresponse() return response.read() # id de départ id = 2097351 # tant que ça plante pas ça suit les liens while 1: # récupération du pickle en base64 contenant l'id de la prochain URL response_crypt = make_request(id) # on décode et on dépickle-ise obj = pickle.loads(base64.urlsafe_b64decode(response_crypt)) # on remplace l'id courant par le prochain afin de pouvoir recommencer id = obj.next print id |
On le lance et ça tourne un petit bout de temps avant d’en arriver au bout. (le serveur doit adorer)
3245669 2993679 1050294 ....... 9683898 8147905 9664723 wololo.zip Traceback (most recent call last): File "1_requests.py", line 27, inresponse_crypt = make_request(id) File "1_requests.py", line 14, in make_request URL = "/areyouhuman/%i" % id TypeError: %d format: a number is required, not str
On a donc notre prochaine destination : http://game.sametmax.com/wololo.zip
Wololo
Pour pas me prendre la tête j’ai télécharger le .zip dans le même répertoire que mes scripts.
On commence par dézipper wololo.zip, on obtient one_more_time_1.zip que l’on dézippe, on obtient one_more_time_2.zip que l’on dézippe, on obtient one_more_time_3.zip que l’on dézippe, etc.
Bon par besoin de chercher très loin pour savoir quoi faire, l’algo est simple :
-
On dézippe wololo.zip (zipfile)
-
Tant que ça marche
-
On dézippe le fichier venant d’être extrait
Ce qui se traduit en python par :
# -*-coding:Utf-8 -* import zipfile # archive de départ archive_name = "wololo.zip" # tant qu’il y a quelque chose à dézipper, ça tourne while 1: # ouverture de l'archive archive = zipfile.ZipFile(archive_name, 'r') # récupération du nom du fichier contenu dans l'archive file_to_extract = archive.namelist()[0] # extraction archive.extract(file_to_extract) print file_to_extract # le fichier tout fraichement extrait sera le prochain à être dézippé # (ça plantera quand y'aura plus de .zip) archive_name = file_to_extract |
On exécute
one_more_time_1.zip one_more_time_2.zip one_more_time_3.zip ................... one_more_time_998.zip one_more_time_999.zip one_more_time_1000.zip youdiditjonhy.txt Traceback (most recent call last): File "2_zips.py", line 10, inarchive = zipfile.ZipFile(archive_name, 'r') File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/zipfile.py", line 712, in __init__ self._GetContents() File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/zipfile.py", line 746, in _GetContents self._RealGetContents() File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/zipfile.py", line 761, in _RealGetContents raise BadZipfile, "File is not a zip file" zipfile.BadZipfile: File is not a zip file
On se retrouve donc avec un nouveau petit fichier youdiditjonhy.txt contenant le texte
api.json
ainsi que 1000 fichiers zip … un petit rm one_more_time* dans son terminal (sur Windows débrouillez-vous) pour cleaner tout ça et on repart pour l’énigme suivante.
api.json
On se rend donc sur http://game.sametmax.com/api.json…
Et on se prend ça dans la tronche :
{"\f": ["."], " ": ["."], "$": [".", "."], "(": [".", ".", "."], ",": [".", "."], "0": [".", "."], "4": ["."], "8": [".", ".", "."], "<": ["."], "@": [".", ".", "."], "D": [".", ".", "."], "H": [".", "."], "L": [".", "."], "P": ["."], "T": [".", "."], "X": [".", ".", "."], "\\": [".", "."], "`": ["."], "d": ["."], "h": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "l": [".", ".", "."], "p": [".", ".", "."], "t": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "x": [".", ".", "."], "|": [".", ".", "."], "\u000b": [".", ".", "."], "#": [".", ".", "."], "'": [".", ".", "."], "+": [".", "."], "/": [".", ".", "."], "3": [".", "."], "7": [".", ".", "."], ";": [".", "."], "?": ["."], "C": [".", ".", "."], "G": ["."], "K": [".", "."], "O": ["."], "S": [".", ".", "."], "W": ["."], "[": ["."], "_": [".", "."], "c": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "g": [".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "k": ["."], "o": [".", ".", "."], "s": [".", "."], "w": ["."], "{": [".", "."], "\n": [".", "."], "\"": ["."], "&": [".", "."], "*": ["."], ".": [".", ".", "."], "2": ["."], "6": [".", "."], ":": ["."], ">": [".", "."], "B": ["."], "F": [".", "."], "J": ["."], "N": ["."], "R": [".", "."], "V": [".", "."], "Z": [".", ".", "."], "^": [".", "."], "b": [".", ".", "."], "f": [".", ".", "."], "j": [".", "."], "n": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "r": [".", "."], "v": [".", "."], "z": [".", ".", "."], "~": [".", "."], "\t": ["."], "\r": [".", ".", "."], "!": ["."], "%": [".", "."], ")": ["."], "-": [".", "."], "1": [".", ".", "."], "5": [".", "."], "9": [".", ".", "."], "=": [".", "."], "A": ["."], "E": [".", ".", "."], "I": [".", "."], "M": ["."], "Q": [".", ".", "."], "U": ["."], "Y": [".", ".", "."], "]": ["."], "a": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "e": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "i": [".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", ".", "."], "m": [".", ".", "."], "q": [".", "."], "u": [".", "."], "y": [".", ".", "."], "}": [".", ".", "."]} |
Bon déjà on sait que c’est du JSON, on se fait un nouveau script pour voir ce que donne ce json en Python :
# -*-coding:Utf-8 -* import httplib import json # on récupère le json connection = httplib.HTTPConnection("game.sametmax.com:80") url = "/api.json" connection.request("GET", url) response = connection.getresponse() # on transorme en python decoded = json.loads(response.read()) print decode |
Bon c’est pas beaucoup mieux, on à affaire à un dictionnaire avec pour clé un caractère Unicode avec une liste de ‘.’ plus où moins longue associé. En mettant un peu en forme ça donne un truc du genre :
Y : . . . Z : . . . [ : . \ : . . ] : . ^ : . . _ : . . ` : . a : . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . b : . . .
Soit pas grand-chose d’exploitable, je modifie le script pour obtenir le nombre de points associé à chaque caractère :
# -*-coding:Utf-8 -* import httplib import json # on récupère le json connection = httplib.HTTPConnection("game.sametmax.com:80") url = "/api.json" connection.request("GET", url) response = connection.getresponse() # on met ça en python decoded = json.loads(response.read()) # on passe le dico en liste pour pouvoir le trier items = decoded.items() items.sort() for i in items: print "%s : %s" % (i[0], len(i[1])) |
Z : 3 [ : 1 \ : 2 ] : 1 ^ : 2 _ : 2 ` : 1 a : 50 b : 3
À partir de là je tente de convertir des mots en remplaçant les lettres par le nombre de ‘.’ associé, par exemple “Max” => 1503. J’arrête vite mes conneries et me rend compte qu’une grosse partie des caractères n’est associé qu’a 1,2 ou 3 points et quelques autres sont à 50, 30, 60, étrange…
Je garde juste les plus grosses valeurs, les tris dans l’ordre croissant, soit : 10 20 … 80. En reprenant chaque clé associée, on obtient le mot gnitaehc, soit cheating à l’envers. Peu de chance que ce soit là par hasard.
On se rend sur http://game.sametmax.com/cheating, on arrive à la fin.
↑↑↓↓←→←→BA
Retour sur la page d’accueil avec cette fois-ci le Konami code en message (↑↑↓↓←→←→BA). Pas de temps à perdre, on va sur http://game.sametmax.com/konami.
Achievement unlocked
Et c’est là que ce termine la série d’énigmes ! Au final la difficulté était plutôt bien dosée, pas trop hard pour un débutant tout en offrant du challenge et du cassage de tête.
J’espère avoir été clair dans mes explications, si y’a des questions sur certains point allez-y, même si ce n’est pas moi je pense qu’il y’aura toujours quelqu’un pour vous éclairer.
Si vous avez utilisé d’autres techniques ou s’il y a des remarques sur mon code, allez-y. Dans les commentaires de l’article original, il y’a une solution “collaborative” similaire à la mienne (bon leurs codes est quand même un peu plus propre), y’a également Recher qui nous propose d’utiliser un éditeur hexadécimal pour l’énigme du zip.
Je suis assez amateur de ce genre de puzzle/challenge, si ça interesse des gens il y a quelques sites bien sympa pour se prendre la tête :
-
http://www.pythonchallenge.com/ : qui est un grand classique, très orienté python, niveau énigme c’est vraiment dans la même veine que cette ouverture de coffre, pour ma part j’avais testé il y’a quelque temps et je m’étais assez vite retrouvé coincé… (par contre il ne faut pas s’arrêter à l’aspect graphique qui fait un peu saigner les yeux…)
-
http://www.newbiecontest.org/ : ce site est beaucoup plus général en proposant des épreuves de crypto, hacking, prog, logique, crackme, etc. Et y’a largement de quoi péter des plombs. La partie programation est assez interessante pour se faire un peu de python. (par contre il est necessaire de s’inscrire)
-
Y’en a plein d’autre je suppose, mais je n’ai testé que ces deux là, si vous avez quelques bonnes adresses n’hésitez pas à partager.
Sur ce, bravo à ceux qui ont tenté le jeu, bon voyage à Max et encore merci à nos deux taulier !
La classe à Dallas,
Tu nous feras aussi un ptit article quand t’auras reçu le colis ? :)
Je l’ai reçu ce matin et c’est bien chouette. je vais essayer d’écrire un petit truc pour ce week-end !
@mraaaah
“Avant de me lancer dans la résolution des énigmes je tiens à signaler que je n’ai qu’un an de python dans les pattes…”
Tu m’as tué la!
En tout cas félicitation Môôsieur !!
@pirateboxge
1 an de python avec 2 sites en django, mais après je code depuis plusieurs années dans différents langages et je suis étudiant en informatique (dans le jeu vidéo), donc même si je ne connais pas tout dans Python je connais quand même pas mal de trucs via d’autres langages.
Mais merci !
merci bibi
N’empêche un truc comme ça ça vaut tous les CV du monde pour un entretien d’embauche :) .
On voit le côté fouine de suite :)
PS: sois franc dans ton article sur le colis, que t’aime ou pas et si c’est pourri faut dire pourquoi, ici pas de langue de bois :)
Amen
Pfioooooooooooouuuuuuuuuuuuuuu!!!!!!!houhouhou bouh !!
C’est pire que du cracking.
Mais vous êtes complètement cinglés ici…..
@roro toi t’a jamais du voir de cracking ! ^^
Respect (autant pour la conception de l’énigme que pour la soluce).
De ma fenêtre, on dirait bien que tu as le niveau pour devenir auteur invité sur le blog (parce que faut pas changer l’URL non plus faut pas déconner).
Et Sametmaxetmraaaah.com c’est bien plus chiant à se rappeler quand on cherche un vieil article.
Y’a plus qu’à montrer ce que tu sais faire dans la rubrique “Cul”.
PS : merci à vous 2 (et maintenant 3) pour votre blog, c’est très fun (même si je ne comprends qu’un article sur 2.
Pour plus de challenges: +Ma’s Reversing – Bright Shadows – Electrica the Puzzle/Challenge – Hacker’s Games – Mod-X Security Challenge – NetForce – NotPron – Rankk – Try this One
et quelques brainteasers: Brain Bashers – Brain Den – Braingle – Had To Play On – Mind Cipher – Puzzle to me – Pzzls
@mraaaah; Si j’en fais un peu (un tout petit peu),avec Ollydbg et KWdsm.
Mais c’est vrai que je “bricole petit”, pour m’amuser.
Mwah, il est bien fait l’article ! Bien expliqué et tout ! Le challenge était cool aussi, même si j’ai arrêté suite à ma fatigue (j’suis d’ailleurs responsable du “42” et “69” en code pour le coffre !)
Haha, je suis vraiment tout pourri en énigmes. Bien joué.
@Max (un autre Max) : Merci, n’ayant pas de blog pour le moment il est pas exclu que je repropose un (ou des) article(s) si je trouve un truc qui pourrait être intéressant à raconter.
@YP : Wahou génial tout ces liens !
@roro : Ba déjà tu as le courage de plonger dans l’assembleur c’est déjà pas mal ^^ Étant sur OS X c’est un peu désert niveau cracking, y’a bien deux trois tutos mais j’ai jamais eu le courage de poussé vu le peu de ressources.