Récit d’un CTF (partie 1)

Hier, j’ai eu l’occasion de participer pour la première fois à un CTF.

Un CTF (Capture The Flag) est un évènement à durée limitée durant lequel des centaines d’équipes du monde entier s’affrontent pour résoudre une série de challenges pour obtenir les fameux « flags ». La différence avec les sites de challenges traditionnels est justement cette durée limitée (quelques heures à quelques jours), ce qui rajoute une certaine pression aux challengers…

Pour ma première expérience, j’ai donc choisi un CTF « débutant » organisé par une université italienne: m0leCon Beginner CTF. Un CTF de 5 heures, combinant des épreuves de crypto, de web hacking, de reversing etc.

Je me plie maintenant à la tradition du « write-up », c’est-à-dire du partage de quelques-unes de mes solutions après la fin du concours, pour permettre à tout le monde de progresser!

Unguessable

Unguessabe, really?

La première épreuve nous invite à deviner un nombre. A moins d’une chance extraordinaire, la réponse est toujours « Wrong, try again »

Le premier réflexe dans ce genre d’épreuve est de comprendre comment fonctionne l’application. Et pour cela, il faut plonger dans le code…

Les deux fonctions qui nous intéressent dans le code javascript sont les suivantes:

   function update(res) {
      if (res === "wrong") {
        card.style.backgroundColor = "red";
        text.innerText = "Wrong, try again";
      } else {
        card.style.backgroundColor = "green";
        fetch("/vjfYkHzyZGJ4A7cPNutFeM/flag")
          .then((response) => response.text())
          .then((str) => {
            text.innerText = str
          });
      }

      card.removeAttribute("hidden");
    }

    document.getElementById("guessBtn").onclick = function () {
      card.setAttribute("hidden", "");
      load().then(() =>
        fetch("/guess", {
          body: document.getElementById("guess").value,
          method: "POST",
        }))
        .then((response) => response.json())
        .then((json) => update(json["result"]))
        .then(() => loader.parentElement.setAttribute("hidden", ""));

    };

La deuxième fonction est celle qui est déclenchée quand on clique sur le bouton « Guess ». Cette fonction envoie le nombre entré dans le formulaire à une page « /guess », récupère le résultat renvoyé par cette page et le passe à la fonction « update »

La fonction « update » regarde si le résultat est « wrong ». Dans ce cas, elle affiche le message « Wrong, try again! » en rouge. Sinon, elle va chercher une autre page (/vjfYkHzyZGJ4A7cPNutFeM/flag) et affiche son contenu. Et cette page contient le flag…

Bingo!

AND Cipher

Cette épreuve présente une page permettant de générer une chaine cryptée.

Chaque appui sur le bouton « Encrypt!!! » génère une nouvelle chaine. D’après les indices du titre, cette chaine est le résultat de l’opérateur booléen AND entre un message secret (probablement le flag) et une clef aléatoire

pour rappel, voici comment fonctionne l’opérateur AND :

Entrée 1 (bit du flag)Entrée 2 (bit de la clef)Sortie (bit de la chaine cryptée)
000
010
100
111
opérateur AND

Si l’on possède la clef, comment pourrait-on retouver le flag? Essayons d’inverser l’opérateur AND:

bit de la chaine cryptéebit de la clefbit du flag
000 ou 1???
010
10Impossible!
111
inversion de l’opérateur AND

On peut constater deux problèmes dans ce « cryptage » AND:

  • Si les bits de la chaine cryptée et de la clef sont tous les deux 0, impossible de savoir quel était le bit du flag
  • Si le bit de la chaine cryptée est 1, cela veut dire que l’on sait que le bit du flag est 1 même si l’on ne connait pas la clef

C’est cette deuxième propriété que je vais utiliser. En effet, chaque fois qu’un bit de la chaine cryptée est 1, on peut en déduire que le bit correspondant du flag est 1. Et comme on peut générer autant de cryptage que l’on veut, on peut accumuler ces bits pour reconstruire le flag. J’ai automatisé le processus dans un petit programme python:

from Crypto.Util.number import long_to_bytes
url="https://andcipher.challs.m0lecon.it/api/encrypt"
import requests

s=0
while True:
	# get the response from the url
	d=requests.get(url).text.split('"')[3]
	
	# transform it into int
	d=int(d,16)
	
	# accumulate the "1" bits with OR operator
	s|=d
	
	# print the result
	print(long_to_bytes(s))
	input()

Et voici le résultat:

Bingo!