dgse-richelieu

Retour sur le challenge Cybersec organisé par la DGSE

Lien : Challenge Richelieu. Le site d’origine n’étant plus actif, voici le PDF permettant de faire la majorité du challenge.

Découvert grâce à un article de ZATAZ

Notes :

Site Web

Une inspection du code source de la page permet de découvrir ceci :

let login = "rien";
let password = "nothing";
if (login === password) {
    document.location="./Richelieu.pdf";
}

Le document pdf permet d’accéder à l’étape suivante.

Notes :

Fichier PDF

Contenu

Le fichier présenté contenait un petit texte sur Richelieu, puis de nombreuses pages blanches.

Une copie de son contenu permettait d’afficher des lignes invisibles car trop petites. Celles-ci décrivaient un fichier encodé en base64.

Une fois celui-ci décodé, la commande file permet de voir qu’il s’agit d’un fichier jpg. Une fois affiché, il s’agit d’un dessin de Richelieu.

Le décodage du fichier en console avait cependant permis d’afficher des fragments de texte, comme Le mot de passe, de cette archive, est :, ainsi qu’un code de la forme DGSE{XXX} et de nombreux noms de fichiers.

Sur les conseils d’un ami, j’ai pu extraire de ce fichier une archive zip avec l’outil foremost. Le fichier s’ouvre ensuite avec le mot de passe DGSE{XXX}.

Notes :

Archive

Contenu

L’archive contenait plusieurs fichiers : un historique bash, une image chiffrée symétriquement avec une clé GPG, cette clé GPG chiffrée avec une clé privée RSA, une clé publique ainsi qu’un fichier texte et une archive protégée par un mot de passe.

Analyse des commandes effectuées

1337  gpg -o lsb_RGB.png.enc --symmetric lsb_RGB.png
1338  vim motDePasseGPG.txt
1339  openssl genrsa -out priv.key 4096
1340  openssl rsa -pubout -out public.key -in priv.key
1341  openssl rsa -noout -text -in priv.key | grep prime1 -A 18 > prime.txt
1342  sed -i 's/7f/fb/g' prime.txt
1343  sed -i 's/e1/66/g' prime.txt
1344  sed -i 's/f4/12/g' prime.txt
1345  sed -i 's/16/54/g' prime.txt
1346  sed -i 's/a4/57/g' prime.txt
1347  sed -i 's/b5/cd/g' prime.txt
1348  openssl rsautl -encrypt -pubin -inkey public.key -in motDePasseGPG.txt -out motDePasseGPG.txt.enc

Le fichier permet de comprendre comment ont été générés les différents fichiers présents dans l’archive.

La première étape va donc être de restaurer la clé privée utilisée.

Clé privée

Il fallait tout d’abord extraire les informations provenant de la clé publique avec :

openssl rsa -noout -text -pubin -in  public.key

Il s’agissait ensuite d’annuler les différents remplacements effectués avec sed, puis de générer une clé privée au bon format. J’ai pour cela utilisé ce script :

import itertools
import re

# lecture des fichiers ne contenant plus que les codes hexadecimaux extraits de prime.txt et de la clé publique
f = open("prime1.txt", "r")
prime1 = f.readline()
f = open("modulus.txt", "r")
modulus = f.readline()

# remplacement à effectuer, j'en ai supprimé un car son résultat n'apparaît pas dans le code hexadécimal
sed = [
	('7f','fb'), 
	('f4','12'), 
	('16','54'), 
	('a4','57'), 
	('b5','cd')
]

modulus = int(modulus.replace(":", ""), 16)

# extraction de tous les remplacements pouvant être effectués, A->B et BB en résultat pouvant provenir de AA, AB, BA ou BB
replacements = []
for s in sed:
	for x in re.finditer(s[1], prime1):
		replacements.append((s[0], x.start(), x.end()))

# parcours de toutes les solutions possibles
for choice in itertools.product(*([[True, False]] * len(replacements))):
	current_line = prime1
	for i_rep, rep in enumerate(replacements):
		if choice[i_rep]:
			current_line = current_line[:rep[1]] + rep[0] + current_line[rep[2]:]
	current_line = int(current_line.replace(":", ""), 16)
	# on cherche prime1 et prime2 tels que modulus=prime1*prime2
	if (modulus % current_line)==0:
		# changement de notation car la suite du code n'est pas de moi :)
		p = current_line
		q = modulus//current_line

n = modulus
e = 0x10001
phi = (p -1)*(q-1)
def xgcd(a, b):
    x0, x1, y0, y1 = 0, 1, 1, 0
    while a != 0:
        q, b, a = b // a, a, b % a
        y0, y1 = y1, y0 - q * y1
        x0, x1 = x1, x0 - q * x1
    return b, x0, y0

def modinv(a, b):
    g, x, _ = xgcd(a, b)
    if g == 1:
        return x % b

d = modinv(e,phi)
dp = modinv(e,(p-1))
dq = modinv(e,(q-1))
qi = modinv(q,p)

import pyasn1.codec.der.encoder
import pyasn1.type.univ
import base64
def pempriv(n, e, d, p, q, dP, dQ, qInv):
    template = '-----BEGIN RSA PRIVATE KEY-----\n{}-----END RSA PRIVATE KEY-----\n'
    seq = pyasn1.type.univ.Sequence()
    for x in [0, n, e, d, p, q, dP, dQ, qInv]:
        seq.setComponentByPosition(len(seq), pyasn1.type.univ.Integer(x))
    der = pyasn1.codec.der.encoder.encode(seq)
    return template.format(base64.encodestring(der).decode('ascii'))

key = pempriv(n,e,d,p,q,dp,dq,qi)
key
'-----BEGIN RSA PRIVATE KEY-----\nMGMCAQACEQDICCEgY36GKnn7Zx8E6qJlAgMBAAECEH+rmKEYf7fXIPGHhsXaDj0CCQDzgJALl2VQ\n7wIJANJMZcP2HhnrAgkAvnmFtBuEfG8CCBjtJULM8VRxAgkA7M4iNPZ4lKs=\n-----END RSA PRIVATE KEY-----\n'
f = open("recovered.key","w")
f.write(key)
f.close()

Notes :

Sources :

Image

Une fois cette clé obtenue, il est simple de déchiffrer la clé GPG puis l’image du dossier :

openssl rsautl -decrypt -inkey recovered.key -out motDePasseGPG.txt -in motDePasseGPG.txt.enc
gpg --symmetric --decrypt lsb_RGB.png.enc -o lsb_RGB.png

Le nom de l’image mettant la puce à l’oreille, il faut alors extraire les bits de poids faible (lsb) de ce fichier :

#coding: utf-8
import base64
from PIL import Image
import array

image = Image.open("lsb_RGB.png")

extracted = ''

pixels = image.load()
for x in range(0,image.width):
	for y in range(0,image.height):
		r,g,b = pixels[x,y]
		extracted += bin(r)[-1]
		extracted += bin(g)[-1]
		extracted += bin(b)[-1]

print(len(extracted)/8)
data = array.array('B')
for i in range(len(extracted)//8):
	byte = extracted[i*8:(i+1)*8]
	data.append(int(byte, 2))

f=open("out.png", "wb")
data.tofile(f)

Le fichier obtenu débute par une représentation textuelle d’un fichier binaire (comme obtenue avec xxd). Il suffit alors d’en supprimer la fin et de l’extraire avec xxd -r out.png > out2.

Sources :

Notes :

UPX

En inspectant le fichier obtenu, je repère très vite le texte suivant :

$Info: This file is packed with the ALD executable packer http://upx.sf.net $
$Id: ALD 3.91 Copyright (C) 1996-2013 the ALD Team. All Rights Reserved. $

Je cherche donc à “extraire” le fichier d’origine, mais j’obtiens le message d’erreur suivant :

upx: upx.txt: NotPackedException: not packed by UPX

Un résultat de recherche m’apprend qu’il est fréquent de remplacer les occurences d’UPX! dans les fichiers UPX. Le message sur l’équipe ALD devient plus clair.

Je remplace donc ALD par UPX dans mon fichier, mais j’obtiens l’erreur suivante :

upx: upx.txt: CantUnpackException: header corrupted 3

J’ai fini par réussir à obtenir le message Unpacked 1 file. en commençant par remplacer 41 4c 44 par 55 50 58 avant d’appliquer xxd -r.

Notes :

ELF

Le fichier exécutable obtenu produit les messages suivants :

usage : ./out3.txt <mot de passe>
Mauvais mot de passe

En utilisant les commandes file, strings et objdump comme indiqué sur ce tuto, j’obtiens les adresses des chaînes d’erreur et de réussite du programme, ainsi que des instructions lea qui permettent de les lire.

La lecture de l’assembleur n’étant pas aisée, j’utilise Ghidra pour le visualiser décompilé.

Aux adresses obtenues, j’obtiens le code suivant :

ulong FUN_00400b20(int iParm1,undefined8 *puParm2)

{
  uint uVar1;
  ulong uVar2;
  
  if (iParm1 < 2) {
    FUN_00407840("usage : %s <mot de passe>\n",*puParm2);
    uVar2 = 2;
  }
  else {
  	//valeur testée pour afficher le message de réussite
    uVar1 = FUN_00400aae(puParm2[1]);
    uVar2 = (ulong)uVar1;
    if (uVar1 == 0) {
      FUN_00408010("Mauvais mot de passe");
    }
    else {
      FUN_00408010("Bravo ! Vous pouvez utiliser ce mot passe pour la suite ;-)");
      uVar2 = 0;
    }
  }
  return uVar2;
}

Je regarde donc en détail le code de la fonction FUN_00400aae :

ulong FUN_00400aae(byte *pbParm1)

{
  int iVar1;
  undefined8 uVar2;
  ulong uVar3;
  byte bVar4;
  long lVar5;
  byte *pbVar6;
  byte bVar7;
  
  lVar5 = -1;
  pbVar6 = pbParm1;
  //boucle comptant la longeur de la chaîne entrée
  do {
    if (lVar5 == 0) break;
    lVar5 = lVar5 + -1;
    bVar4 = *pbVar6;
    pbVar6 = pbVar6 + 1;
  } while (bVar4 != 0);
  uVar2 = 0;
  //le résultat est comparé à -32
  //en comptant l'initialisation à -1 et le \0 final, on cherche une chaîne de 30 caractères
  if (lVar5 == -0x20) {
    bVar4 = *pbParm1;
    if (bVar4 != 0) {
      //adresse de la chaîne utilisée pour vérifier le mot de passe
      pbVar6 = &DAT_004898c0;
      pbParm1 = pbParm1 + 1;
      uVar3 = 1;
      //initialisation
      bVar7 = 0x33;
      //boucle de vérification de la chaîne
      do {
        iVar1 = (int)uVar3;
        uVar3 = 0;
        if (iVar1 != 0) {
          //XOR avec la valeur précédente
          uVar3 = (ulong)((bVar4 ^ bVar7) == *pbVar6);
        }
        bVar7 = *pbVar6;
        bVar4 = *pbParm1;
        pbVar6 = pbVar6 + 1;
        pbParm1 = pbParm1 + 1;
      } while (bVar4 != 0);
      return uVar3;
    }
    uVar2 = 1;
  }
  return uVar2;
}

Ce script m’a donc permis de truver le mot de passe correct :

numbers = [
# initialisation
'33',
# 30 valeurs suivant &DAT_004898c0
...
]

for a, n in enumerate(numbers[:-1]):
	print(chr(int(n, 16) ^ int(numbers[a+1], 16)))

Comme l’indique le message “Bravo ! Vous pouvez utiliser ce mot passe pour la suite ;-)”, le mot de passe trouvé permet de déchiffre l’archive qui était fournie avec les autres fichiers dans l’étape précédente.

Celle-ci contient un fichier texte qui donne des instructions pour se connecter en SSH à une machine distante pour la suite du challenge.

Notes :

Wargame

Les identifiants obtenus permettent d’ouvrir une connexion qui affiche le message suivant :

Partie Wargame du CTF Richelieu

Outils disponibles:
*******************

  * gdb (avec peda)
  * python 2.7
  * pwnlib
  * checksec
  * vim
  * emacs
  * nano
  * ltrace
  * strace
  * ...

ATTENTION : les connexions sont coupées et les fichiers sont détruits
automatiquement au bout de 1 heure.
Pensez à sauvegarder vos fichiers sur un autre poste pour ne pas les perdre.

Je précise pour la suite que je connaiss très mal gdb, ltrace et strace, et que je n’ai jamais utilisé pwnlib ni checksec, la suite s’annonce difficile.

defi1

Un ls -l donne les informations suivantes :

total 16
-r-------- 1 defi1-drapeau defi1-drapeau  133 Apr 26 14:06 drapeau.txt
-r-sr-sr-x 1 defi1-drapeau defi1-drapeau 8752 May 10 10:50 prog.bin

Je suppose donc qu’il faut ouvrir le fichier drapeau.txt grâce aux droits de l’éxécution de prog.bin.

Ce dernier propose un choix entre plusieurs commandes, dont l’affichage d’un train. Je repère un appel à sl grâce à ltrace : system("sl".

En ajoutant le dossier courant au $PATH et en créeant un fichier exéxutable sl contenant cat drapeau.txt, j’obtiens les identifiants de l’étape suivante.

Notes :

defi2

Le défi suivant est de la même forme. Le programme permet de vérifier si un couple login / password vérifie certaines caractéristiques.

J’ai pu remarquer que la longueur des chaines entrées n’était pas vérifiée. J’ai donc provoqué un buffer overflow avec une chaîne suffisamment longue, le programme produisant ainsi une segfault.

La résolution de ce défi passait donc par l’injection de code assembleur après la fin du buffer. Quelques recherches à ce sujet m’ont amené sur ce tuto, mais cela dépasse mes compétences. Je me suis donc arrêté à ce niveau du challenge.

suite ?

Je ne sais pas combien de défis il me restait, ni si d’autres épreuves suivent encore celles-ci, mais je suis preneur de toutes informations à ce sujet.