Inshall'hack
Security if God wills it
Inshall'hack

[FR] Mission Impossible 1 Writeup (Santhacklaus CTF 2018)

Description du challenge

Il s'agit du chall donnant le plus de points sur ce CTF (800 pts). C'est un chall de forensics classique, basé sur l'analyse d'un memory dump.

Pour commencer, il nous est donné un zip :

$ unzip -l MI1_fix01.zip 
Archive:  MI1_fix01.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
1094804832  2018-12-16 17:18   challenge.elf
        48  2018-12-16 17:33   challenge.md5
---------                     -------
1094804880                     2 files

Préparation de l'aventure

En l'extrayant, on obtient un memorydump de 1.1 Go Premier réflexe, regarder si c'est un dump de RAM d'un OS Windows ou Linux :

$ strings challenge.elf | grep -i linux | head 
/build/linux-luZh6p/linux-3.16.57/drivers/scsi/pm8001/pm8001_init.c
/build/linux-luZh6p/linux-3.16.57/arch/x86/include/asm/dma-mapping.h
/build/linux-luZh6p/linux-3.16.57/include/asm-generic/dma-mapping-common.h
/build/linux-luZh6p/linux-3.16.57/drivers/scsi/pm8001/pm8001_hwi.c
/build/linux-luZh6p/linux-3.16.57/include/linux/netdevice.h
/build/linux-luZh6p/linux-3.16.57/drivers/net/ethernet/fujitsu/fmvj18x_cs.c
/home/michael/vbox/branches/VBox-5.2/out/linux.amd64/release/obj/VBoxVgaBios386/VBoxVgaBios386.sym
/build/linux-luZh6p/linux-3.16.57/drivers/ata/libata-core.c
/build/linux-luZh6p/linux-3.16.57/include/asm-generic/dma-mapping-common.h
/build/linux-luZh6p/linux-3.16.57/drivers/ata/libata-scsi.c

On voit directement que c'est la deuxième option.

On va donc utiliser l'outil classique pour l'analyse de dump mémoire, à savoir volatility (https://github.com/volatilityfoundation/volatility). Volatility travaille avec des profils, et sous Linux les paramètres d'un profil changent en fonction de la version du kernel de la machine dont la RAM a été dumpée.

$ strings challenge.elf | grep "Linux version"
Linux version %d.%d.%d
Linux version 3.16.0-6-amd64 (debian-kernel@lists.debian.org) (gcc version 4.9.2 (Debian 4.9.2-10+deb8u1) ) #1 SMP Debian 3.16.57-2 (2018-07-14)
Linux version 3.16.0-6-amd64 (debian-kernel@lists.debian.org) (gcc version 4.9.2 (Debian 4.9.2-10+deb8u1) ) #1 SMP Debian 3.16.57-2 (2018-07-14)
Dec 16 11:14:09 virtual-debian kernel: [    0.000000] Linux version 3.16.0-6-amd64 (debian-kernel@lists.debian.org) (gcc version 4.9.2 (Debian 4.9.2-10+deb8u1) ) #1 SMP Debian 3.16.57-2 (2018-07-14)
Dec 16 11:14:09 virtual-debian kernel: [10295.865806] intel_idle: does noual-debian kernel: [    0.000000] Linux version 3.16.0-6-amd64 (debian-kernel@lists.debian.org) (gcc version 4.9.2 (Debian 4.9.2-10+deb8u1) ) #1 SMP Debian 3.16.57-2 (2018-07-14)
Dec 16 11:14:09 virtual-debian kernel: [    0.000000] Linux version 3.16.0-6-amd64 (debian-kernel@lists.debian.org) (gcc version 4.9.2 (Debian 4.9.2-10+deb8u1) ) #1 SMP Debian 3.16.57-2 (2018-07-14)
2018-12-16T11:14:09.150996-05:00 virtual-debian kernel: [    0.000000] Linux version 3.16.0-6-amd64 (debian-kernel@lists.debian.org) (gcc version 4.9.2 (Debian 4.9.2-10+deb8u1) ) #1 SMP Debian 3.16.57-2 (2018-07-14)
MESSAGE=Linux version 3.16.0-6-amd64 (debian-kernel@lists.debian.org) (gcc version 4.9.2 (Debian 4.9.2-10+deb8u1) ) #1 SMP Debian 3.16.57-2 (2018-07-14)

On récupère la version du kernel utilisé : 3.16.0-6-amd64.

On peut également repérer la string deb8u1, qui nous indique qu'on est sur du Debian 8.

L'étape d'après, c'est donc de monter une VM Debian 8 et d'y installer le bon kernel.

Par chance, la version du kernel à installer est celle utilisée sur l'ISO de Debian 8 par défaut.

Il nous reste donc simplement à installer les headers du kernel, et les outils de volatility.

$ apt-cache search $(uname -r)

Pour trouver le bon paquet, puis

$ apt-get install linux-headers-3.16.0-6-amd64 volality-tools make

On va maintenant build le profil

$ dd /usr/src/volatility-tools/linux
$ make
$ zip debian_3.16.0-6-amd64.zip /usr/src/volatility-tools/linux/module.dwarf /boot/System.map-3.16.0-6-amd64

Et maintenant, sur notre OS hôte, on met le profil dans le répertoire où volatility stocke les profils :

$ cp debian_3.16.0-6-amd64.zip usr/lib/python2.7/site-packages/volatility/plugins/overlays/linux

On vérifie que volatility le reconnaît bien :

$ volatility --info | grep debian
Volatility Foundation Volatility Framework 2.6
Linuxdebian_3_13_0-6-amd64x64 - A Profile for Linux debian_3.13.0-6-amd64 x64

C'est tout bon, on est prêts pour l'aventure!

Analyse du dump

Sur les dumps mémoire Linux, je regarde toujours l'historique de bash en premier, c'est généralement le plus croustillant :

➜  mi1 volatility -f challenge.elf --profile=Linuxdebian_3_13_0-6-amd64x64 linux_bash
Volatility Foundation Volatility Framework 2.6
Pid      Name                 Command Time                   Command
-------- -------------------- ------------------------------ -------
    1867 bash                 2018-12-16 16:17:45 UTC+0000   rm flag.txt 
    1867 bash                 2018-12-16 16:17:45 UTC+0000   ls
    1867 bash                 2018-12-16 16:17:45 UTC+0000   ls
    1867 bash                 2018-12-16 16:17:45 UTC+0000   sudo reboot
    1867 bash                 2018-12-16 16:17:45 UTC+0000   zip -r -e -s 64K backup.zip *
    1867 bash                 2018-12-16 16:17:45 UTC+0000   cat /dev/urandom > flag.txt 
    1867 bash                 2018-12-16 16:17:45 UTC+0000   cd /var/www/a-strong-hero.com/
    1867 bash                 2018-12-16 16:17:45 UTC+0000   sudo reboot
    1867 bash                 2018-12-16 16:17:49 UTC+0000   cd /var/www/a-strong-hero.com/
    1867 bash                 2018-12-16 16:17:49 UTC+0000   ls
    1867 bash                 2018-12-16 16:18:09 UTC+0000   find . -type f -print0 | xargs -0 md5sum > md5sums.txt
    1867 bash                 2018-12-16 16:18:10 UTC+0000   cat md5sums.txt 

On remarque que plusieurs commandes sont faites a 16:17:45, soit au cours de la même seconde. Je ne sais pas si c'est car elles ont été lancées via un script (mais pourquoi ne voit-on pas le lancement de ce script alors ?) ou si c'est un "bug" de volatility.

Voilà ce qu'on peut déduire de ces commandes :

  • le flag se trouve probablement dans un fichier flag.txt ;
  • il y a sûrement des choses intéressantes dans /var/www/a-strong-hero.com ;
  • il y a un fichier backup.zip, qu'on pourra sûrement dump avec volatility.

On regarde maintenant les fichiers que volatility peut retrouver dans notre dump :

$ volatility -f challenge.elf --profile=Linuxdebian_3_13_0-6-amd64x64 linux_find_file -L > files.txt`
$ grep -i backup files.txt 
          261933 0xffff88001e61e4b0 /var/www/a-strong-hero.com/backup.z02
          263120 0xffff88001e61e898 /var/www/a-strong-hero.com/backup.z05
          263122 0xffff88001e61ec80 /var/www/a-strong-hero.com/backup.z07
          263123 0xffff88001e61d0c8 /var/www/a-strong-hero.com/backup.z08
          263125 0xffff88001e61d4b0 /var/www/a-strong-hero.com/backup.zip
          261792 0xffff88001e61d898 /var/www/a-strong-hero.com/backup.z01
          262990 0xffff88001e61dc80 /var/www/a-strong-hero.com/backup.z04
          263121 0xffff88001e61c0c8 /var/www/a-strong-hero.com/backup.z06
          263124 0xffff88001e61c4b0 /var/www/a-strong-hero.com/backup.z09
          262949 0xffff88001e61cc80 /var/www/a-strong-hero.com/backup.z03
----------------                0x0 /etc/resolv.conf.pppd-backup.

Plusieurs choses ici :

  • le fichier backup.zip a été construit avec la commande suivante : zip -r -e -s 64K backup.zip *, donc les fichiers backupés sont ceux de /var/www/a-strong-hero.com/, récursivement ;
  • on remarque les extensions inhabituelles des autres fichiers de backup, peut-être une méthode de versionning ésotérique de la part du dev ?

On dump le fichier backup.zip avec volatility :

$ volatility -f challenge.elf --profile=Linuxdebian_3_13_0-6-amd64x64 linux_find_file -i 0xffff88001e61d4b0 -O backup.zip

L'option -i spécifie l'inode, qu'on a trouvé en listant les fichiers juste au-dessus.

Avant de le dézipper, on peut lister son contenu :

$ unzip -l backup.zip 
Archive:  backup.zip
warning [backup.zip]:  zipfile claims to be last disk of a multi-part archive;
  attempting to process anyway, assuming all parts have been concatenated
  together in order.  Expect "errors" and warnings...true multi-part support
  doesn't exist yet (coming soon).
  Length      Date    Time    Name
---------  ---------- -----   ----
       30  2018-12-16 16:57   flag.txt
        0  2018-12-16 15:51   jcvd-website/
        0  2018-12-16 15:51   jcvd-website/js/
     6148  2018-12-16 15:51   jcvd-website/js/.DS_Store
    36816  2018-12-16 15:51   jcvd-website/js/bootstrap.min.js
    95957  2018-12-16 15:51   jcvd-website/js/jquery-1.11.3.min.js
    68890  2018-12-16 15:51   jcvd-website/js/bootstrap.js
       79  2018-12-16 15:51   jcvd-website/js/custom.js
      641  2018-12-16 15:51   jcvd-website/js/ie10-viewport-bug-workaround.js
     5564  2018-12-16 15:51   jcvd-website/js/jquery.easing.min.js
    12292  2018-12-16 15:51   jcvd-website/.DS_Store
        0  2018-12-16 15:51   jcvd-website/images/
    37682  2018-12-16 15:51   jcvd-website/images/concert.jpg
     6148  2018-12-16 15:51   jcvd-website/images/.DS_Store
    52003  2018-12-16 15:51   jcvd-website/images/microphone.jpg
    49276  2018-12-16 15:51   jcvd-website/images/iphone.jpg
    91733  2018-12-16 15:51   jcvd-website/images/header.jpg
    26267  2018-12-16 15:51   jcvd-website/images/writing.jpg
   133773  2018-12-16 15:51   jcvd-website/images/pencil_sharpener.jpg
     7384  2018-12-16 15:51   jcvd-website/index.html
        0  2018-12-16 15:51   jcvd-website/fonts/
    45404  2018-12-16 15:51   jcvd-website/fonts/glyphicons-halflings-regular.ttf
    18028  2018-12-16 15:51   jcvd-website/fonts/glyphicons-halflings-regular.woff2
    23424  2018-12-16 15:51   jcvd-website/fonts/glyphicons-halflings-regular.woff
    20127  2018-12-16 15:51   jcvd-website/fonts/glyphicons-halflings-regular.eot
   108738  2018-12-16 15:51   jcvd-website/fonts/glyphicons-halflings-regular.svg
        0  2018-12-16 15:51   jcvd-website/css/
     6148  2018-12-16 15:51   jcvd-website/css/.DS_Store
   147430  2018-12-16 15:51   jcvd-website/css/bootstrap.css
     8335  2018-12-16 15:51   jcvd-website/css/custom.css
   122540  2018-12-16 15:51   jcvd-website/css/bootstrap.min.css
---------                     -------
  1130857                     31 files

Ah-ah ! Il y a flag.txt dedans! On essaye de l'unzip :

$ unzip backup.zip 
Archive:  backup.zip
warning [backup.zip]:  zipfile claims to be last disk of a multi-part archive;
  attempting to process anyway, assuming all parts have been concatenated
  together in order.  Expect "errors" and warnings...true multi-part support
  doesn't exist yet (coming soon).

Arf, ça aurait été trop simple. On check les options avec lesquels ce zip a été créé :

$ zip -r -e -s 64K backup.zip *

Bon, -r c'est du classique, c'est juste pour qu'il travaille récursivement. -e va permettre de chiffrer le zip, avec un prompt au moment de la création pour demander le mot de passe (donc on ne peut pas le voir dans la ligne de commande…). Enfin, -s, beaucoup moins courante, permet de split le zip en plusieurs morceaux (aaah, c'est donc ça les extensions chelou de tout-à-l'heure !).

Pour le chiffrement, on verra plus tard, on va déjà essayer de récupérer un zip valide. On commence par dump chacun des backup.z0X. En se renseignant sur l'internet, on trouve facilement une commande pour tout rassembler :

$ zip -s 0 backup.zip --out unsplited-backup.zip`

On essaye de le dézip, pour voir si il y a toujours des erreurs :

$ unzip unsplited-backup.zip 
Archive:  unsplited-backup.zip
[unsplited-backup.zip] flag.txt password: 

Ah super, on est bons de ce côté-là !

Attaque par clair connu

Maintenant, il nous reste ce satané chiffrement. C'est sûrement la partie la plus tricky du challenge. Je connaissais déja le trick, mais il peut se retrouver en ayant une bonne analyse de la situation :

Dans les commandes, on voit que le fichier flag.txt a été remplacé par du contenu de /dev/urandom, donc on ne peut pas le récupérer dans la liste des fichiers.

Par contre, les fichiers du site, eux, sont toujours disponibles sur le disque. Au final, la seule inconnue de notre zip chiffré, c'est notre flag.txt. Ça fait quand même beaucoup penser à une attaque par clair-connu, non ?

Avec quelques recherches duckduckgo, avec des mots-clés du genre "zip known plaintext recovery", on tombe assez rapidement sur l'outil PKCrack (ou au moins des writeups qui en parle).

Sur Archlinux, il est dans les dépots AUR, mais si vous ne faites pas partie de l'élite :^), il est aussi sur github (ici).

Une fois notre outil installé, on vérifie les paramètres nécessaires.

Il a besoin un fichier qui est dans le zip chiffré et dont on connaît le contenu, ainsi qu'un zip non-chiffré contenant ce fichier.

Pour le fichier, on va prendre le jquery-1.11.3.min.js, mais on pourrait en prendre un autre, tant qu'il n'est pas trop petit.

Dans notre files.txt, on récupère son inode :

$ grep jquery files.txt 
          261774 0xffff88001e49ac80 /var/www/a-strong-hero.com/jcvd-website/js/jquery.easing.min.js
          261775 0xffff88001e444c80 /var/www/a-strong-hero.com/jcvd-website/js/jquery-1.11.3.min.js

Puis on le dump :

$ volatility -f challenge.elf --profile=Linuxdebian_3_13_0-6-amd64x64 linux_find_file -i 0xffff88001e444c80 -O jquery-1.11.3.min.js

On le met dans un zip :

$ zip plaintex-backup.zip jquery-1.11.3.min.js

Et enfin on lance notre fameux pkcrack :

$ pkcrack -C unsplited-backup.zip -c "jcvd-website/js/jquery-1.11.3.min.js" -P plaintext-backup.zip -p "jquery-1.11.3.min.js" -d flag.zip -a
Files read. Starting stage 1 on Fri Dec 21 19:16:31 2018
Generating 1st generation of possible key2_33170 values...done.
Found 4194304 possible key2-values.
Now we're trying to reduce these...
Lowest number: 993 values at offset 28371
Lowest number: 968 values at offset 27781
Lowest number: 956 values at offset 27724
Lowest number: 907 values at offset 27722
Lowest number: 857 values at offset 27719
Lowest number: 849 values at offset 27718
Lowest number: 767 values at offset 27716
Lowest number: 741 values at offset 27713
Lowest number: 737 values at offset 27692
Lowest number: 733 values at offset 27634
Lowest number: 705 values at offset 27633
Lowest number: 692 values at offset 27632
Lowest number: 690 values at offset 27622
Lowest number: 680 values at offset 27621
Lowest number: 616 values at offset 27620
Lowest number: 587 values at offset 27590
Lowest number: 561 values at offset 27588
Lowest number: 471 values at offset 27587
Lowest number: 445 values at offset 27565
Lowest number: 411 values at offset 27560
Lowest number: 404 values at offset 27559
Lowest number: 399 values at offset 27558
Lowest number: 378 values at offset 27407
Lowest number: 324 values at offset 27404
Lowest number: 279 values at offset 27334
Lowest number: 273 values at offset 27327
Lowest number: 271 values at offset 27322
Lowest number: 268 values at offset 27316
Lowest number: 265 values at offset 27315
Lowest number: 249 values at offset 27314
Lowest number: 247 values at offset 27313
Lowest number: 234 values at offset 27312
Lowest number: 232 values at offset 27311
Lowest number: 221 values at offset 27310
Lowest number: 220 values at offset 27309
Lowest number: 204 values at offset 27308
Lowest number: 185 values at offset 27306
Lowest number: 178 values at offset 27305
Lowest number: 176 values at offset 27284
Lowest number: 168 values at offset 27283
Lowest number: 148 values at offset 27279
Lowest number: 146 values at offset 27202
Lowest number: 143 values at offset 27197
Lowest number: 141 values at offset 27196
Lowest number: 121 values at offset 27185
Lowest number: 108 values at offset 27172
Lowest number: 99 values at offset 27161
Done. Left with 99 possible Values. bestOffset is 27161.
Stage 1 completed. Starting stage 2 on Fri Dec 21 19:16:39 2018
Ta-daaaaa! key0=751f036a, key1=397078fa, key2=d156dfac
Probabilistic test succeeded for 6014 bytes.
Ta-daaaaa! key0=751f036a, key1=397078fa, key2=d156dfac
Probabilistic test succeeded for 6014 bytes.
Ta-daaaaa! key0=751f036a, key1=397078fa, key2=d156dfac
Probabilistic test succeeded for 6014 bytes.
Ta-daaaaa! key0=751f036a, key1=397078fa, key2=d156dfac
Probabilistic test succeeded for 6014 bytes.
Ta-daaaaa! key0=751f036a, key1=397078fa, key2=d156dfac
Probabilistic test succeeded for 6014 bytes.
Ta-daaaaa! key0=751f036a, key1=397078fa, key2=d156dfac
Probabilistic test succeeded for 6014 bytes.
Stage 2 completed. Starting zipdecrypt on Fri Dec 21 19:16:41 2018
Decrypting flag.txt (91c644af94249dd314b62b57)... OK!
Decrypting jcvd-website/js/.DS_Store (2fe6d64c750f20da2d6b7b4e)... OK!
Decrypting jcvd-website/js/bootstrap.min.js (31beae5a6417af2fcee27b4e)... OK!
Decrypting jcvd-website/js/jquery-1.11.3.min.js (68cffaef64b77eca810f7b4e)... OK!
Decrypting jcvd-website/js/bootstrap.js (172450e6004efe284b507b4e)... OK!
Decrypting jcvd-website/js/custom.js (4038fc0d73419d37a34f7b4e)... OK!
Decrypting jcvd-website/js/ie10-viewport-bug-workaround.js (71f134fe12dcf4d413c17b4e)... OK!
Decrypting jcvd-website/js/jquery.easing.min.js (dd66d46318af5411b24b7b4e)... OK!
Decrypting jcvd-website/.DS_Store (ccc90b8c7a949b1dd0297b4e)... OK!
Decrypting jcvd-website/images/concert.jpg (2531ab52a4c3f2af90017b4e)... OK!
Decrypting jcvd-website/images/.DS_Store (cd53bfa34fee99aade507b4e)... OK!
Decrypting jcvd-website/images/microphone.jpg (e04e73cca1576915c96f7b4e)... OK!
Decrypting jcvd-website/images/iphone.jpg (7d0e3ddec5bb0eb5d5537b4e)... OK!
Decrypting jcvd-website/images/header.jpg (558cd122c491a4c95df47b4e)... OK!
Decrypting jcvd-website/images/writing.jpg (de9b24799ceac1377f317b4e)... OK!
Decrypting jcvd-website/images/pencil_sharpener.jpg (89cbb73d79aa6c0472607b4e)... OK!
Read unknown signature: 0xda11766a
Error: unknown signature (ZIP file may be corrupt)
Finished on Fri Dec 21 19:16:41 2018

L'option -C donne le zip chiffré, -c spécifie la version chiffré du fichier dont on connait le contenue, -p la version en clair, et enfin -P spécifie le zip avec la version en clair.

L'option -a permet simplement de s'arrêter après avoir trouvé une clé valide, et -d de sếcofocer le fichier d'output.

Il réussit bien à casser le chiffrement du zip car on voit qu'il réussit à déchiffrer notre flag : Decrypting flag.txt (91c644af94249dd314b62b57)... OK!

... mais il crashe à la fin car une signature zip déconne.

Et du coup, le fichier flag.zip, censé contenir notre zip déchiffré, est créé, mais est corrompu :(.

La méthode yolo

Bon, on est des hackers, on va bien se démerder avec tout ça quand même.

On pourrait s'amuser à trouver quelle partie du zip multipart déconne et pourquoi, mais ca n'a pas l'air très fun.

En réfléchissant, on se dit que notre pkcrack doit bien mettre ses résultat en mémoire à un moment.

On check rapidement si il est compilé en dynamique (sinon on peut toujours le recompiler comme on veut de toute façon) et on fait la méthode yolo :

$ ltrace pkcrack -C unsplited-backup.zip -c "jcvd-website/js/jquery-1.11.3.min.js" -P plaintext-backup.zip -p "jquery-1.11.3.min.js" -d flag.zip -a > ltrace_output  2>&1 

Pour ceux qui ne connaissent pas, ltrace est un petit outil (plutot utilisé en reverse/pwn d'habitude) qui permet de lister tout les appels à des librairies externes (comme la libc) faits par un binaire (d'où la nécessité de savoir s'il est compilé dynamiquement ou pas. Si il était compilé en statique il n'appellerait aucune librairie externe).

On espère donc intercepter un moment où pkcrack va écrire notre flag dans un fichier temporaire, par exemple.

Le > ltrace_output 2>&1 à la fin, c'est pour rediriger stdout dans un fichier, et comme ltrace écrit surtout dans stderr, on redirige aussi stderr vers stdout. Tout ca nous permet d'avoir tout l'output de ltrace dans notre fichier ltrace_output.

Maintenant, on sait que notre flag commence par "IMTLD" (le flag format du CTF), suffit donc e grep dans l'tas !

$ grep IMTLD ltrace_output 
fwrite("IMTLD{z1p_1s_n0t_alw4y5_s4fe}\n", 1, 30, 0x55e82ce41820) = 30

Flagged

Et bim ! On a de la chance, c'est sale mais osef, on a notre flag !

Flag : IMTLD{z1p_1s_n0t_alw4y5_s4fe}


comments powered by Disqus

Receive Updates

ATOM

Contacts