Cubicle Riddle

Catégorie: Misc Difficulté: easy Flag: HTB{r1ddle_m3_th1s_r1ddle_m3_th4t}

Challenge

Description


Navigate the haunting riddles that echo through the forest, for the Cubicle Riddle is no ordinary obstacle. The answers you seek lie within the whispers of the ancient trees and the unseen forces that govern this mystical forest. Will your faction decipher the enigma and claim the knowledge concealed within this challenge, or will the forest consume those who dare to unravel its secrets? The fate of your faction rests in you.

Analyse du code

On a accès à la partie du code source qui vérifie ce que l’on envoie. Dedans on voit la fonction check_answer qui construit une fonction avec notre réponse et cette fonction doit retourner les mêmes valeurs que (min(self.num_list), max(self.num_list))

def check_answer(self, answer: bytes) -> bool:
        _answer_func: types.FunctionType = types.FunctionType(
            self._construct_answer(answer), {}
        )
        return _answer_func(self.num_list) == (min(self.num_list), max(self.num_list))

Concernant la fonction construct_answer on remarque plusieurs choses :

  • Notre réponse est encadrée par co_code_start et co_code_end

  • Une fonction est construite via CodeType où 18 arguments sont passés

def _construct_answer(self, answer: bytes) -> types.CodeType:
        co_code: bytearray = bytearray(self.co_code_start)
        co_code.extend(answer)
        co_code.extend(self.co_code_end)

        code_obj: types.CodeType = types.CodeType(
            1,
            0,
            0,
            4,
            3,
            3,
            bytes(co_code),
            (None, self.max_int, self.min_int),
            (),
            ("num_list", "min", "max", "num"),
            __file__,
            "_answer_func",
            "_answer_func",
            1,
            b"",
            b"",
            (),
            (),
        )
        return code_obj

En prenant la documentation de CodeType on voit qu’à chaque version de Python, le nombre de paramètre change. Et cela a son importance puisque chaque version compilera notre réponse différement. La plus grande difficulté ici était donc d’identifier que c’était du Python 3.11 grâce aux 18 arguments

#!/usr/bin/python3.11

print(len(inspect.signature(types.CodeType).parameters))
# 18

Toujours avec la documentation, on comprend que :

  • Le premier argument (1) correspond au nombre de paramètres de la fonction

  • ("num_list", "min", "max", "num") correspond aux variables locales

  • (None, self.max_int, self.min_int) sont les constantes utilisées

Donc on a 1 paramètre, c’est num_list puisque c’est le premier définit dans les variables locales. Sa valeur est None

Ensuite on a 3 variables déclarés dans le corps de la fonction : min max puis num (définis dans cet ordre)

Si on transforme le début et la fin en instructions avec dis :

print(dis.dis(b"d\x01}\x01d\x02}\x02")) # co_code_start
#          0 LOAD_CONST               1
#          2 STORE_FAST               1
#          4 LOAD_CONST               2
#          6 STORE_FAST               2

print(dis.dis(b"|\x01|\x02f\x02S\x00")) # co_code_end
#          0 LOAD_FAST                1
#          2 LOAD_FAST                2
#          4 BUILD_TUPLE              2
#          6 RETURN_VALUE

On interprête ce code comme

def _answer_func(num_list=None):
	min = 1000
	max = -1000
	# a nous de construire ici avec notre réponse
	return (min, max)

Il suffit donc de compléter cette fonction puis d’envoyer son co_code sans les 8 premiers et 8 derniers octets sachant que l’on ne peut utiliser que la variable num

def _answer_func(num_list=None):
	min = 1000
	max = -1000
	for num in num_list:
		if min > num:
			min = num
		if max < num:
			max = num
	return (min, max)

print(_answer_func.__code__.co_code)
# b'\x97\x00d\x01}\x01d\x02}\x02|\x00D\x00]\x12}\x03|\x03|\x01k\x00\x00\x00\x00\x00r\x02|\x03}\x01|\x03|\x02k\x04\x00\x00\x00\x00r\x02|\x03}\x02\x8c\x13|\x01|\x02f\x02S\x00'

On voit qu’il y a juste un décalage, notre co_code commence par \x97\x00 , il faut donc penser à les retirer en plus des 8 premiers


Script de résolution

from pwnlib.tubes.remote import remote

def _answer_func(num_list=None):
	min = 1000
	max = -1000
	for num in num_list:
		if min > num:
			min = num
		if max < num:
			max = num
	return (min, max)

answer = ', '.join([str(n) for n in _answer_func.__code__.co_code[10:-8]])
client = remote('x.x.x.x', port)
client.sendlineafter(b'(Choose wisely) > ', b'1')
client.sendlineafter(b'(Answer wisely) >', answer.encode()) 
flag = re.search(r"(HTB{.*})", client.recvall(timeout=1).decode()).group(1)
print(f"Flag: {flag}")

Dernière mise à jour

Cet article vous a-t-il été utile ?