Un petit challenge

Ce challenge a été envoyé comme composant d’un appel d’offre ; il couvrait plusieurs domaines et types d’épreuves basiques de CTF. Un seul fichier nommé Annexe.txt était fourni, sans explication complémentaire.

Niveau 1

Le fichier contient la sortie d’une commande xxd. Cette commande peut être inversée facilement pour retrouver les donnés binaires.

$ < Annexe.txt xxd -r | file -
/dev/stdin: pcapng capture file - version 1.0

Le fichier de capture peut être analysé avec des outils comme wireshark. Plusieurs requêtes HTTP apparaissent en filtrant, et wireshark permet d’avoir une version lisible des échanges.

Deux éléments peuvent être extraits :

  • la sortie d’une nouvelle commande xxd ;
  • une phrase étrange.

 

La phrase étrange

Puisque la phrase comporte une ponctuation, le chiffrement utilisé est probablement une forme de chiffre de César.

# apt-get install bsdgames
$ for i in $( seq 26 ); do \
caesar $i <<< "Ib jlq ab mxppb mlro zbqqb bmobrsb bpq : xslrpabglrboylkzlroxdbofbkafkprojlkqxyib !" ; \
done
Jc kmr bc nyqqc nmsp acrrc cnpcstc cqr : ytmsqbchmscpzmlamspyecpgclbglqspkmlryzjc !
Kd lns cd ozrrd ontq bdssd doqdtud drs : zuntrcdintdqanmbntqzfdqhdmchmrtqlnmszakd !
Le mot de passe pour cette epreuve est : avousdejouerboncourageriendinsurmontable !
Mf npu ef qbttf qpvs dfuuf fqsfvwf ftu : bwpvtefkpvfscpodpvsbhfsjfoejotvsnpoubcmf !
Ng oqv fg rcuug rqwt egvvg grtgwxg guv : cxqwufglqwgtdqpeqwtcigtkgpfkpuwtoqpvcdng !
Oh prw gh sdvvh srxu fhwwh hsuhxyh hvw : dyrxvghmrxhuerqfrxudjhulhqglqvxuprqwdeoh !
Pi qsx hi tewwi tsyv gixxi itviyzi iwx : ezsywhinsyivfsrgsyvekivmirhmrwyvqsrxefpi !
Qj rty ij ufxxj utzw hjyyj juwjzaj jxy : fatzxijotzjwgtshtzwfljwnjsinsxzwrtsyfgqj !
Rk suz jk vgyyk vuax ikzzk kvxkabk kyz : gbuayjkpuakxhutiuaxgmkxoktjotyaxsutzghrk !
Sl tva kl whzzl wvby jlaal lwylbcl lza : hcvbzklqvblyivujvbyhnlyplukpuzbytvuahisl !
Tm uwb lm xiaam xwcz kmbbm mxzmcdm mab : idwcalmrwcmzjwvkwcziomzqmvlqvaczuwvbijtm !
Un vxc mn yjbbn yxda lnccn nyanden nbc : jexdbmnsxdnakxwlxdajpnarnwmrwbdavxwcjkun !
Vo wyd no zkcco zyeb moddo ozboefo ocd : kfyecnotyeoblyxmyebkqobsoxnsxcebwyxdklvo !
Wp xze op alddp azfc npeep pacpfgp pde : lgzfdopuzfpcmzynzfclrpctpyotydfcxzyelmwp !
Xq yaf pq bmeeq bagd oqffq qbdqghq qef : mhagepqvagqdnazoagdmsqduqzpuzegdyazfmnxq !
Yr zbg qr cnffr cbhe prggr rcerhir rfg : nibhfqrwbhreobapbhentrevraqvafhezbagnoyr !
Zs ach rs doggs dcif qshhs sdfsijs sgh : ojcigrsxcisfpcbqcifousfwsbrwbgifacbhopzs !
At bdi st ephht edjg rtiit tegtjkt thi : pkdjhstydjtgqdcrdjgpvtgxtcsxchjgbdcipqat !
Bu cej tu fqiiu fekh sujju ufhuklu uij : qlekituzekuhredsekhqwuhyudtydikhcedjqrbu !
Cv dfk uv grjjv gfli tvkkv vgivlmv vjk : rmfljuvaflvisfetflirxvizveuzejlidfekrscv !
Dw egl vw hskkw hgmj uwllw whjwmnw wkl : sngmkvwbgmwjtgfugmjsywjawfvafkmjegflstdw !
Ex fhm wx itllx ihnk vxmmx xikxnox xlm : tohnlwxchnxkuhgvhnktzxkbxgwbglnkfhgmtuex !
Fy gin xy jummy jiol wynny yjlyopy ymn : upiomxydioylvihwioluaylcyhxchmolgihnuvfy !
Gz hjo yz kvnnz kjpm xzooz zkmzpqz zno : vqjpnyzejpzmwjixjpmvbzmdziydinpmhjiovwgz !
Ha ikp za lwooa lkqn yappa alnaqra aop : wrkqozafkqanxkjykqnwcaneajzejoqnikjpwxha !
Ib jlq ab mxppb mlro zbqqb bmobrsb bpq : xslrpabglrboylkzlroxdbofbkafkprojlkqxyib !

La troisième ligne contient la phrase déchiffrée, contenant un mot de passe pour la prochaine étape.

Récupération de la sortie du xxd

Une possibilité de récupérer le contenu depuis le fichier pcap serait d’utiliser un simple copier/coller. Un autre moyen serait d’extraire la requête POST depuis Wireshark puis de retirer les éléments inutiles à la main.

En ligne de commande, il est possible d’utiliser l’option -z de l’outil tshark appliqué à l’échange entre 10.3.253.93:1259 et 10.3.253.25:80. Avec quelques autres outils, le contenu peut être récupéré élégamment.

$ while read line ; do echo $line | xxd -r -p ; done \
<<< $( tshark -nlr ./network.pcap -qz "follow,tcp,raw,10.3.253.93:1259,10.3.253.25:80" \
| tail -n +7 | head -n -1 | sed 's/^\s\+//g' ) \
| egrep '^[0-9a-z]{8}: ' > form-data.bi

A nouveau, le contenu original peut être reconstruit avec la commande :

$ < form-data.bin xxd -r > form-data.raw

Le fichier n’est pas d’un type connu. Cependant, les premières lignes révèlent sa nature :

$ file form-data.raw
form-data.raw: data
$ strings -a form-data.raw | head -5
CREATED_BY
aescrypt 3.13
/d{B
mrkG
vIFkWs

 

Déchiffrement

Les chaînes de caractères pointent directement vers AESCrypt et la bibliothèque python associée. En utilisant un petit script et le mot de passe précédemment récupéré, les données peuvent être déchiffrées.

$ pip3 install --user pyAesCrypt
$ python3 decrypt.py
Usage: decrypt.py <encrypted file> <password>
$ python3 decrypt.py form-data.raw avousdejouerboncourageriendinsurmontable > decrypted.bin
$ file decrypted.bin
decrypted.bin: gzip compressed data, last modified: Mon Sep 16 12:25:56 2019, from Unix, original size modulo 2^32 133120
$ zcat decrypted.bin | file -
/dev/stdin: POSIX tar archive (GNU)
$ zcat decrypted.bin | tar tv
-rw-r--r-- root/root 149 2019-09-16 14:20 instructions
-rwxr-xr-x root/root 16608 2019-09-16 14:17 challenge
-rw-r--r-- root/root 105316 2019-09-16 14:25 2.tar.gz.aes
-rw-r--r-- root/root 31 2019-09-16 13:48 msg.txt

Une fois déchiffré, le fichier révèle une archive contenant à son tour de nouveaux fichiers.

Niveau 2

Il n’y a toujours pas d’indications de la marche à suivre, et l’exécutable fourni n’est pas d’une grande aide.

$ ./challenge
Segmentation fault

Un détour rapide par Ghidra pour observer une version desassemblée du programme permet de comprendre rapidement son principe.

Les 0x1B = 27 premiers caractères de l’argument fourni par l’utilisateur sont traités octet par octet puis comparés à un tableau de référence. Si tous les caractères correspondent, c’est que l’argument est bien celui attendu. Le traitement peut être facilement inversé en quelques lignes de Python :

base = 0x28
encoded = [
    0x6b,
    0x97,
    0x97,
    0xa9,
    0xb0,
    0xb0,
    0xc0,
    0xc0,
    0xc5,
    0xb0,
    0xdc,
    0xd6,
    0xc6,
    0xf8,
    0xfd,
    0x105,
    0xde,
    0x10e,
    0x118,
    0xf0,
    0x11c,
    0x120,
    0x125,
    0x134,
    0x119,
    0x13d,
    0x12d,
    0x150
]
for e in encoded:
    print(chr(e - base), end='')
    base += 7
print()

Ce programme permet ensuite de récupérer le mot de passe attendu :

$ python3 ./reverse.py
Challenge***JustForCheckIfOk
$
$ ./challenge Challenge***JustForCheckIfOk
C'est le bon mot de passe !!!

Le mot de passe devrait bien aller avec le fichier chiffré présent dans l’archive.

Niveau 3

Comme supposé, le mot de passe peut être utilisé pour déchiffrer le fichier avec le script python créé précédemment.

$ python3 decrypt.py 2.tar.gz.aes ChallengeAbcJustForCheckIfOk > 2.tar.gz
$ tar xfv 2.tar.gz
msg.txt
encrypted1targz
encrypt.php

Le fichier encrypt.php utilise une méthode de chiffrement simple constitué d’un XOR avec une clé d’un seul octet. Les données sont ensuite encodées au format hexdump. Cette dernière étape peut être inversée avec la commande xxd comme précédemment.

$ < encrypted1targz xxd -r -p > 1.tar.gz.enc

Etant donné que le fichier chiffré est appelé encrypted1targz, il semble logique de supposer que son contenu commence avec les Magic Bytes du format GZIP : \x1f\x8b. Puisque le contenu commence par les octets \x6a\xfe, la clé peut être retrouvée avec : 0x6a ^ 0x1f = 0x75. Cette valeur est vérifiable avec le second octet. Avec la clé, quelques lignes de Python permettent de retrouver le fichier original.

hkey = 0x75
with open('1.tar.gz', 'wb') as fout:
    with open('1.tar.gz.enc', 'rb') as fin:
        data = fin.read()
        for d in data:
            fout.write(bytes([ d ^ hkey ])

Les portes du prochain niveau sont ouvertes.

$ zcat 1.tar.gz | tar tv
-rw-r--r-- root/root 31 2019-09-16 13:40 msg.txt
-rw-r--r-- root/root 91430 2019-09-16 13:39 cache.png

 

Niveau 4

Ce niveau se présente avec une image d’un pavillon pirate. En jouant avec GIMP et les couleurs, une curieuse bande apparaît en haut de l’image.

Des données cachées peuvent être recherchées avec l’outil zsteg.

# apt-get install rubygems
$ gem install --user-install zsteg
$ export PATH=$HOME/.gem/ruby/2.5.0/bin:$PATH
$ zsteg -a --no-color cache.png
imagedata .. file: shared library
b1,r,lsb,xy .. text: "Z\"RBZBrG[\"RBZbRW[\"vVZ\"jB["
b1,r,msb,xy .. text: "@djZDnBZ@J"
b1,g,msb,xy .. text: "JYKHLJKHLMK"
b1,rgb,lsb,xy .. text: "0x1f0x8b0x080x000x3b0x560x7f0x5d0x000x030x4b0xcd..."
b2,g,lsb,xy .. file: shared library
...

Puisque l’un des résultats semble contenir des valeurs hexadécimales, il est possible de l’extraire avec ce même outil.

$ zsteg -E 'b1,rgb,lsb,xy' cache.png > hex.txt
$ printf $( < hex.txt sed 's#0x#\\x#g' ) > hex.bin
$ file hex.bin
hex.bin: gzip compressed data, last modified: Mon Sep 16 09:30:35 2019, from Unix, original size modulo 2^32 10240

Et nous voilà au dernier niveau, avec encore une archive.

Niveau 5

L’archive contient deux fichiers. L’un est encore un fichier chiffré en AES tandis que l’autre contient une chaîne hexadécimale.

$ ls
encryptedInstruction msgatrouverbase64.aes
$ cat encryptedInstruction
052b61242135692a24693e203a3d24692b323d6e2d2c6e323c27372820356974610a21342a2134052b112c3a283d06282b21341d3b003f2f2f2a2b320b272427

La première chose à faire est de convertir cette chaîne en ASCII.

$ python3
>>> encr = open('encryptedInstruction').read()
>>> dec = bytes.fromhex(encr)
>>> print(dec)
b"\x05+a$!5i*$i> :=$i+2=n-,n2<'7( 5ita\n!4*!4\x05+\x11,:(=\x06(+!4\x1d;\x00?//*+2\x0b'$'"

Etant donné que ça ne ressemble à rien de lisible, il y a certainement une autre couche de chiffrement. Dans des étapes précédentes, un ROT et un XOR ont été utilisés. Le ROT ne donnant rien ici, le XOR est alors l’option la plus probable.

Même si aucune information n’est disponible sur la clé utilisée, deux méthodes distinctes permettent de réussir à déchiffrer sans utiliser de bruteforce.

Première méthode : trouver la longueur de la clé

En observant la chaîne de caractères décodée, certaines paires de caractères semblent se répéter plusieurs fois comme 5i ou !4.

La majorité de ces répétitions est probablement due au fait d’avoir XORé les mêmes lettres du texte clair et de la clé. Si c’est effectivement le cas, alors la longueur de la clé peut être retrouvée en notant les positions de ces groupes de caractères répétés.

>>> from re import finditer as fi
>>> [i.start() for i in fi(b'5i', dec)]
[5, 29]
>>> [i.start() for i in fi(b'\$i', dec)]
[8, 14]
>>> [i.start() for i in fi(b'!4', dec)]
[34, 37, 49]
>>> [i.start() for i in fi(b'\+2', dec)]
[16, 58]

Avec cette hypothèse, le nombre de caractères entre chaque occurrence d’une répétition doit être un multiple de la longueur de la clé, mis à part certaines éventuelles coïncidences. Ici tout semble cohérent puisque la différence de position entre deux occurrences est toujours un multiple de 3.

29 -  5 = 24 = 3 * 8
14 -  8 =  6 = 3 * 2
49 - 37 = 12 = 3 * 4
...

Une fois la longueur de la clé fixée, il reste très raisonnable de tester toutes les 255 ^ 3 = 16581375 clés possible. Cependant, étant donné le contexte de ce challenge, une clé spécifique peut être privilégiée.

from itertools import cycle
encr = open('encryptedInstruction').read()
dec = bytes.fromhex(encr)
print(''.join( [chr(i ^ j) for i, j in zip(dec, cycle(b'***'))]) )

L’intuition était bonne et le script donne le résultat suivant :

$ python3 method1.py
Le mot de passe est le suivant : CoucouLePetitHibouTuAvancesBien

Et voilà le mot de passe pour déchiffrer le fichier.

Deuxième méthode : deviner une partie du texte chiffré

Même sans avoir identifié les répétitions dans le texte chiffré, la clé pouvait être retrouvée en essayant de deviner des morceaux du texte clair. En se basant sur le message trouvé dans le fichier pcap au premier niveau, le texte mentionne probablement un mot de passe.

En essayant de déchiffrer ce fragment à toutes les position dans le message, la clé qui aurait été utilisée pour chiffrer le fragment à chaque position peut être récupérée. C’est possible grâce aux propriétés de l’opération XOR : clair XOR clé = chiffré donne clair XOR chiffré = clé

Il est ensuite possible de déchiffrer l’intégralité du message en utilisant cette clé extraite, et de vérifier si le message obtenu a du sens.

from itertools import cycle
from string import printable

encr = open('encryptedInstruction').read()
dec = bytes.fromhex(encr)
guess = b'mot de passe'
for k in range(len(dec) - len(guess)):
    test_key = ''.join([chr(i ^ j) for i, j in zip(dec[k:], guess)]).encode()
    test_clear = ''.join([chr(i ^ j) for i, j in zip(dec, cycle(test_key))])
    if test_clear.isprintable():
        print(test_clear)

Le script donne alors le résultat suivant :

$ python3 method2.py
Le mot de passe est le suivant : CoucouLePetitHibouTuAvancesBien
Hq|p$Zhno;y@wg9=.]<*f~)Rq}*|%Zh0*XfTg{)Q.~-~coAHf{)I>o>kdxlRF}9s

Le mot de passe pour AESCrypt est obtenu sans même avoir besoin de connaître la clé de XOR. Evidemment, le résultat peut être obtenu immédiatement car la clé est répétée un nombre entier de fois sur le morceau de texte (la longueur de texte clair deviné est un multiple de la longueur de la clé). Si ça n’avait pas été le cas, il aurait été nécessaire de regarder la valeur des test_keys et voir que dans un cas il s’agissait d’une chaîne de 3 caractères répétée. Le déchiffrement aurait ensuite exactement comme avec la première méthode.
Avec le mot de passe, l’autre fichier peut être déchiffré avec AESCrypt.

$ python3 decrypt.py msgatrouverbase64.aes CoucouLePetitHibouTuAvancesBien > msgatrouverbase64
$ cat msgatrouverbase64 | base64 -d
***{Ceci est le message à trouver ! Si vous avez ceci bravo vous avez réussi ! Verification : ***************************************}

Et avec ceci voici la fin de ce petit challenge.