Ouverture du coffre : la méthode simple, bourine, mais efficace


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?

Un petit meme : "Code Y U NO WORK?"

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.

Modification de la methode d'envoi d'un formulaire avec Firebug.

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, in 
        response_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

Petit strip humoristique en réference à AOE et ses prètres wololoteur

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, in 
    archive = 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": [".", ".", "."], "}": [".", ".", "."]}
Sir Patrick Stewart jurant à la vu de ce fichier json

Même Sir Patrick Stewart semble contrarié par ce fichier !

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 !

15 thoughts on “Ouverture du coffre : la méthode simple, bourine, mais efficace

  • Max

    La classe à Dallas,

    Tu nous feras aussi un ptit article quand t’auras reçu le colis ? :)

  • mraaaah Post author

    Je l’ai reçu ce matin et c’est bien chouette. je vais essayer d’écrire un petit truc pour ce week-end !

  • pirateboxge

    @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 !!

  • mraaaah Post author

    @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.

  • Max

    Je l’ai reçu ce matin et c’est bien chouette. je vais essayer d’écrire un petit truc pour ce week-end !

    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 :)

  • Max

    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

  • roro

    Pfioooooooooooouuuuuuuuuuuuuuu!!!!!!!houhouhou bouh !!
    C’est pire que du cracking.
    Mais vous êtes complètement cinglés ici…..

  • Max (un autre Max)

    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.

  • roro

    @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.

  • Lyyn

    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 !)

  • mraaaah Post author

    @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.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.