Bonjour à tous !

Je présente dans cet article le challenge SSTIC 2016 ainsi que la méthodologie que j’ai employé pour résoudre les épreuves qui m’ont permis de passer les diffèrents niveaux.

Cette année, le challenge ést organisé sous la forme de 3 niveaux de jeu. Chaque niveau contient plusieurs épreuves, chaque épreuve donne 1 clé, et il faut 2 clés pour valider le niveau et passer au suivant. Certaines épreuves étant plus dures que les autres du même niveau, leur clé en valent 2. Génial, ça ira plus vite ! (ou pas)

Voici un récapitulatif des différentes épreuves:
  • Niveau 1:

    • (1) calc.zip: CrackMe sous la forme d’un programme d’une calculatrice TI-83

    • (1) SOS-Fant0me.zip: Capture réseau du RAT Gh0st

    • (2) radio.zip: C’est 2 barbus qui rentrent dans un bar. L’un dit à l’autre: "Hey! Tu sais c’est quoi la diffèrence entre la radio et la peste ?"

  • Niveau 2:

    • (1) foo.zip: CrackMe DLL32 EFI

    • (1) huge.zip: CrackME ELF64

    • (1) loader.zip

  • Niveau 3:

    • (1) screensaver

    • (1) FeSes.zip

    • (2) strange.zip: CrackME en IA-64 bits faisant de l’OCR à partir d’un réseau neuronal multi-couches

    • (2) ring3

Contexte

On commence avec un PCAP récupéré sur http://static.sstic.org/challenge2016/challenge.pcap.

yfrancou@panda$ ll challenge.pcap
    53448495  challenge.pcap
yfrancou@panda:$ file challenge.pcap
    challenge.pcap: tcpdump capture file (little-endian) - version 2.4 (Ethernet, capture length 32767)

On l’ouvre avec WireShark, on extrait les fichiers (File > Export Objects > HTTP). Un fichier challenge.zip sauvage apparaît.

yfrancou@panda$ ll challenge.zip
    52331069  challenge.zip

Une fois dézippé, on voit que le challenge est sous la forme d’une plateforme 2D développée avec rpgjs. On le lance donc dans notre navigateur préféré.

L’aventure commence dans une auberge. On va parler à la tenancière blonde de l'établissement qui nous indique les règles du challenge.

"Salut voyageur ! Pour arriver au bout de challenge, il te faudra résoudre des épreuves. Tu auras le choix entre plusieurs épreuves à chaque niveau, donc choisis bien celles que tu préfères. Il suffit d’atteindre le nombre de points requis pour passer au niveau suivant. Chaque épreuve te fournira une clé de 16 octets, qu’il faudra donner au garde du niveau sous forme héxadécimale pour la faire prendre en compte. Si tu lui donnes suffisamment de clés, le garde te laissera passer. Tu rencontreras peut-être d’autres personnages bizarres en cours de route (toute ressemblance avec des personnes existantes serait purement fortuite). Ne t’y attarde pas trop, sauf si tu aimes troller. Bonne chance !"

start01

Niveau 1

calc.zip

"Quand j'étais encore jeune, j’ai caché ma clé dans un instrument que j’ai ramené du Texas. Malheureusement, j’ai oublié comment la récupérer depuis…"

On commence par dézipper le fichier et on y trouve un fichier SSTIC16.8xp. Un file nous indique qu’il s’agit d’un programme fait pour une calculatrice Texas Instrument TI-83+:

file SSTIC16.8xp
  SSTIC16.8xp: TI-83+ Graphing Calculator (program)

"Cool ça à l’air marrant, mais j’ai pas de TI-83 sous la main moi .. Remarque, même si j’en avais une .. "

Bon. Google, premier lien. On tombe sur le site cemetech qui émule des TI-83+ pour peu qu’on lui fournisse la bonne ROM. Donc on lui donne le fichier SSTIC16.8xp à manger, on appuie sur la touche "PRGM" pour voir les programmes chargés et on lance le programme SSTIC.

N1 calc start

On a un émulateur qui fonctionne, mais ça serait bien de pouvoir regarder le code aussi. Tiens ça tombe bien, il y a un compilateur / décompilateur pour TI-83+ sur le même site.

Le code source étant chiant à lire et n’apportant pas grand chose, voici l'équivalent en python:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

keys = [
    0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832,
    0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2,
    0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A,
    0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
    0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3,
    0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423,
    0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB,
    0xB6662D3D, 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
    0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4,
    0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950,
    0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074,
    0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0,
    0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525,
    0x206F85B3, 0xB966D409, 0xCE61E49F, 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81,
    0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615,
    0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
    0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, 0xFED41B76,
    0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E,
    0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6,
    0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
    0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7,
    0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F,
    0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7,
    0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
    0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278,
    0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC,
    0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 0xCABAC28A, 0x53B39330,
    0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
    0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
]

user_input = 0x05571C16  # Le password passé en INPUT à la calculatrice
a = 0xFFFFFFFF

for i in range(4):
    input_i = ((user_input >> (i * 8)) & 0xFF)
    key_index = input_i ^ (a & 0xFF)
    key = keys[key_index]
    a = (a >> 8) ^ key


if a ^ 0xFFFFFFFF == 0xC49AB257:
    print "GAGNE!"
else:
    print "PERDU"

Comme on veut aller vite et qu’on préfère quand c’est dégueu, on résoud ce problème à la main.

[1]
A = 0xFFFFFFFF
KEY_INDEX = (INPUT[3] ^ A[3]) = (0x?? ^ 0xFF) = 0x??
A = ((A >> 8) ^ KEYS[KEY_INDEX]) = (0x00FFFFFF ^ 0x????????) = 0x????????

[2]
A = 0x????????
KEY_INDEX = (INPUT[2] ^ A[3]) = (0x?? ^ 0x??) = 0x??
A = ((A >> 8) ^ KEYS[KEY_INDEX]) = (0x00?????? ^ 0x????????) = 0x????????

[3]
A = 0x????????
KEY_INDEX = (INPUT[1] ^ A[3]) = (0x?? ^ 0x??) = 0x??
A = ((A >> 8) ^ KEYS[KEY_INDEX]) = (0x00?????? ^ 0x????????) = 0x????????

[4]
A = 0x????????
KEY_INDEX = (INPUT[0] ^ A[3]) = (0x?? ^ 0x??) = 0x??
A = ((A >> 8) ^ KEYS[KEY_INDEX]) = (0x00?????? ^ 0x????????) = 0x3B654DA8

Il faut que A soit égal à 0x3B654DA8 (0x0xC49AB257 ^ 0xFFFFFFFF) à la fin. Donc on part de la fin, et on remonte.

Pour que A soit égal à 0x3B654DA8, il faut que la clé soit de la forme 0x3B??????. La seule clé qui match dans la liste est la clé 0x3B6E20C8 en position 0x20. On peut maintenant calculer (A>>8) qui fait 0x000b6d60. Ca nous donne:

[1]
A = 0xFFFFFFFF
KEY_INDEX = (INPUT[3] ^ A[3]) = (0x?? ^ 0xFF) = 0x??
A = ((A >> 8) ^ KEYS[KEY_INDEX]) = (0x00FFFFFF ^ 0x????????) = 0x????????

[2]
A = 0x????????
KEY_INDEX = (INPUT[2] ^ A[3]) = (0x?? ^ 0x??) = 0x??
A = ((A >> 8) ^ KEYS[KEY_INDEX]) = (0x00?????? ^ 0x????????) = 0x????????

[3]
A = 0x????????
KEY_INDEX = (INPUT[1] ^ A[3]) = (0x?? ^ 0x??) = 0x??
A = ((A >> 8) ^ KEYS[KEY_INDEX]) = (0x00?????? ^ 0x????????) = 0x0B6D60??

[4]
A = 0x0B6D60??
KEY_INDEX = (INPUT[0] ^ A[3]) = (0x?? ^ 0x??) = 0x20
A = ((A >> 8) ^ KEYS[KEY_INDEX]) = (0x000B6D60 ^ 0x3B6E20C8) = 0x3B654DA8

A ce niveau, on ne connait pas la valeur de INPUT[0] ni celle de A[3], mais c’est pas grave. On continue de remonter. On sait maintenant que le A du 3ème tour de boucle doit être égale à 0x0B6D60??. Ce qui fait que la clé doit avoir la forme 0x0B??????. La seule qui match est 0x0BDBDF21.

Bon vous voyez le truc, on remonte jusqu’en haut comme ça. Ce qui nous donne:

[1]
A = 0xFFFFFFFF
KEY_INDEX = (INPUT[3] ^ A[3]) = (0x?? ^ 0xFF) = 0xE9
A = ((A >> 8) ^ KEYS[KEY_INDEX]) = (0x00FFFFFF ^ 0xD9D65ADC = 0xD9??????

[2]
A = 0xD9??????
KEY_INDEX = (INPUT[2] ^ A[3]) = (0x?? ^ 0x??) = 0x3F
A = ((A >> 8) ^ KEYS[KEY_INDEX]) = (0x00D9???? ^ 0xB6662D3D = 0xB6BF????

[3]
A = 0xB6BF????
KEY_INDEX = (INPUT[1] ^ A[3]) = (0x?? ^ 0x??) = 0xCF
A = ((A >> 8) ^ KEYS[KEY_INDEX]) = (0x00B6BF?? ^ 0x0BDBDF21 = 0x0B6D60??

[4]
A = 0x0B6D60??
KEY_INDEX = (INPUT[0] ^ A[3]) = (0x?? ^ 0x??) = 0x20
A = ((A >> 8) ^ KEYS[KEY_INDEX]) = (0x000B6D60 ^ 0x3B6E20C8) = 0x3B654DA8

Une fois arrivé tout en haut, on connaît le A de départ, soit 0xFFFFFFFF. Donc A[3] = 0xFF. Ce qui fait que INPUT[3] = 0xE9 XOR 0xFF = 0x16. On calcule la valeur de A (0x00FFFFFF XOR 0xD9D65ADC) qui est du coup égale à 0xD929A523. Et on redescend tout en bas.

[1]
A = 0xFFFFFFFF
KEY_INDEX = (INPUT[3] ^ A[3]) = (0x16 ^ 0xFF) = 0xE9
A = ((A >> 8) ^ KEYS[KEY_INDEX]) = (0x00FFFFFF ^ 0xD9D65ADC = 0xD929A523

[2]
A = 0xD929A523
KEY_INDEX = (INPUT[2] ^ A[3]) = (0x1C ^ 0x23) = 0x3F
A = ((A >> 8) ^ KEYS[KEY_INDEX]) = (0x00D929A5 ^ 0xB6662D3D = 0xB6BF0498

[3]
A = 0xB6BF0498
KEY_INDEX = (INPUT[1] ^ A[3]) = (0x57 ^ 0x98) = 0xCF
A = ((A >> 8) ^ KEYS[KEY_INDEX]) = (0x00B6BF?? ^ 0x0BDBDF21 = 0x0B6D6025

[4]
A = 0x0B6D6025
KEY_INDEX = (INPUT[0] ^ A[3]) = (0x05 ^ 0x25) = 0x20
A = ((A >> 8) ^ KEYS[KEY_INDEX]) = (0x000B6D60 ^ 0x3B6E20C8) = 0x3B654DA8

Et voilà, on a l’INPUT qu’il faut rentrer dans la calculatrice: 0x05571C16.

N1 calc key
Note
Il s’agissait bien évidemment d’un CRC32, mais c'était plus classe de le résoudre à la main que de faire un bruteforcer. Les bruteforcers, c’est pour les n00bs.

SOS-Fant0me.zip

"On suspecte une machine d'être hantée par un fantôme mais cette fois ce n’est pas Casper. Tiens, voici une capture du champ électromagnétique… en gros une capture réseau? Tu pourras peut-être en tirer quelque chose !"

On dézippe le fichier et on trouve un fichier SOS-Fant0me.pcap de 232ko. En l’ouvrant avec WireShark, on s’aperçoit qu’il s’agit d’une capture réseau de la communication entre un RAT Gh0st et son C&C.

N1 ghost start

"Rhooo les cons ! C’est cadeau :D" se dit-on en se frottant les mains. On lance le tool qu’on a en interne chez Airbus qui nous ressort toutes les infos et on passe au challenge suivant. On lance ChopShop, qui a justement un module pour déchiffrer les communications de Gh0st.

Allez, la commande pour le fun:

  chopshop -f ./SOS-Fant0me.pcap -M modules/ -E ext_libs/ "gh0st_decode" -s CARVED

Le module nous extrait aussi les fichiers échangés.

  2759  C__Users_sstic_Documents_Challenge SSTIC 2016_how_to_rule_the_world.txt
   234  C__Users_sstic_Documents_Challenge SSTIC 2016_Stage 1_sstic2016-stage1-solution.zip
193968  C__Users_sstic_Documents_Challenge SSTIC 2016_visio_stage2.mp4

Le fichier C__Users_sstic_Documents_Challenge SSTIC 2016_Stage 1_sstic2016-stage1-solution.zip est chiffré. On retrouve le mot de passe dans les communications (paquets de type Keylogger) qui est: Cyb3rSSTIC_2016.

Le zip contient la clé du challenge qui est: 368B E8C1 CC7C C70C 2245 0309 3430 1C15

Niveau 2

huge.zip

"C’est vraiment très indigeste ce challenge…"

Ce challenge commence avec un ZIP contenant un TAR.

yfrancou@panda$ unzip huge.zip
    Archive:  huge.zip
        inflating: huge.tar
yfrancou@panda$ tar xvf huge.tar
    Huge
    tar: Huge: Cannot seek to 25297854992384: Invalid argument
    tar: Exiting with failure status due to previous errors

Erreur sur l’extraction du TAR quand il veut se positionner en +25To. C’est sûrement dû à mon FS en ext4 qui est limité à des fichiers de 16To, mais j’ai un peu la flemme de l’ouvrir sur un autre FS.

On regarde rapidement le fichier Huge généré:

yfrancou@panda$ ll Huge
    11168083824640  Huge
yfrancou@panda$ file Huge
    Huge: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), statically linked, corrupted section header size

On a un fichier ELF 64bits de 11To corrompu. Cool.

Le fichier huge.tar contient des sparses. Pour résumé, si un fichier de 100To contient 99.9To de zéros, ca sert à rien de les copier sur le disque. C’est là qu’interviennent les sparses en décrivant où sont les gros blocs de zéros.

En extrayant les sparses headers, on voit que le fichier original fait pas loin de 130To.

24 size=128574140715008
30 ctime=1456948527.914039536
30 atime=1456948548.569742108
22 GNU.sparse.major=1
22 GNU.sparse.minor=0
24 GNU.sparse.name=Huge
39 GNU.sparse.realsize=128574140715008

Bon. Toujours pas envie de l’extraire correctement sur un autre FS ? Bof .. Le fichier entier pourrait nous permettre de le debugger avec gdb, mais … les analyses dynamiques c’est pour les noobs. #TeamAnalyseStatique

Un coup de hexedit sur le huge.tar nous montre qu’il semble y avoir des bouts de code valides. "Un bon point pour moi et ma flemme".

On ouvre huge.tar en RAW dans IDA. Ce qui veut dire qu’on connaît juste les offsets des instructions mais qu’on a aucune idée des adresses virtuelles. Pratique pour tous les sauts.

N2 huge main
"Bah c'est easy ce challenge, il y a les trucs bleus à côté qui disent ce que font les fonctions !"
"Ahah ahah .. PAN !"

Bon, ça résout pas notre problème d’adresses virtuelles ça, mais cette fonction ressemble au point d’entrée.

Un coup de readelf sur le fichier Huge nous donne les infos suivantes:

yfrancou@panda$ readelf -a Huge
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x51466a42e705
  Start of program headers:          64 (bytes into file)
  Start of section headers:          0 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         3
  Size of section headers:           0 (bytes)
  Number of section headers:         0
  Section header string table index: 0

L’entry point se situe à l’adresse virtuelle 0x51466a42e705, et il s’agit probablement de celle de la fonction précédente.

Maintenant, on va s’aider des sparses pour identifier les adresses virtuelles de toutes les autres fonctions. Ce qui nous donne le joli schéma suivant:

                       Adresse Virtuelle    Function                       Parties checkées
                                                                 00010203 04050607 08090A0B 0C0D0E0F
            |----------> 0x06F4B0E0000C   <check_6>            [........ ........ ......8C ........]
            |                         .   <check_6>            [........ ........ ......8C ........]
            |                         .   <check_6>            [........ ........ ......8C ........]
         |--|----------- 0x06F4B0E0F370   <check_6>            [........ ........ ......8C ........]
         |  |            0x06F4B0E0F38F   <jmp_exit_13000>
         |  |                         .   <jmp_exit_13000>
         |  |                         .   <jmp_exit_13000>
         |  |            0x099A38050000   <jmp_exit_13000>
         |  |     |----> 0x099A3805000C   <chall_end>          [........ ........ ........ ........]
         |  |     |                   .   <chall_end>          [........ ........ ........ ........]
         |  |     |                   .   <chall_end>          [........ ........ ........ ........]
         |  |     |      0x099A380575A6   <chall_end>  EXIT    [........ ........ ........ ........]
         |  |     |  |-> 0x10F338CF000C   <decode_hex_min>     [........ ........ ........ ........]
         |  |     |  |                .   <decode_hex_min>     [........ ........ ........ ........]
         |  |     |  |                .   <decode_hex_min>     [........ ........ ........ ........]
         |  |     |  |   0x10F338CF7548   <decode_hex_min> RET [........ ........ ........ ........]
|--------|--|-----|--|-> 0x2A7EE24A000C   <check_7>            [........ D4D68C99 ........ ........]
|        |  |     |  |                .   <check_7>            [........ D4D68C99 ........ ........]
|        |  |     |  |                .   <check_7>            [........ D4D68C99 ........ ........]
|        |  |     |--|-- 0x2A7EE24AAE24   <check_7>            [........ D4D68C99 ........ ........]
|        |  |        |   0x2A7EE24AAE7E   <jmp_exit_1000>
|        |  |        |                .   <jmp_exit_1000>
|        |  |        |                .   <jmp_exit_1000>
|        |  |        |   0x2C7AFFEF0000   <jmp_exit_1000>
|        |  |        |   0x2C7AFFEF000C   <sub_22AC>           [........ ........ ........ ........]
|        |  |        |                .   <sub_22AC>           [........ ........ ........ ........]
|        |  |        |                .   <sub_22AC>           [........ ........ ........ ........]
|        |  |        |   0x2C7AFFEF62AC   <sub_22AC> RET       [........ ........ ........ ........]
|        |  |        |   0x2C7AFFEF630A   <jmp_exit_3000>
|        |  |        |                .   <jmp_exit_3000>
|        |  |        |                .   <jmp_exit_3000>
|        |  |        |   0x42021DA90000   <jmp_exit_3000>
|     |--|--|--------|-> 0x352845AB000C   <check1>             [........ ........ ........ 89341BEC]
|     |  |  |        |                .   <check1>             [........ ........ ........ 89341BEC]
|     |  |  |        |                .   <check1>             [........ ........ ........ 89341BEC]
|  |--|--|--|--------|-- 0x352845AB3BCE   <check1>             [........ ........ ........ 89341BEC]
|  |  |  |  |        |   0x352845AB3BF9   <jmp_exit_5000>
|  |  |  |  |        |                .   <jmp_exit_5000>
|  |  |  |  |        |                .   <jmp_exit_5000>
|  |  |  |  |        |   0x42021DA90000   <jmp_exit_5000>
|  |  |  |  |        |   0x42021DA9000C   <exit>               [........ ........ ........ ........]
|  |  |  |  |        |                .   <exit>               [........ ........ ........ ........]
|  |  |  |  |        |                .   <exit>               [........ ........ ........ ........]
|  |  |  |  |        |   0x43ABDB4A0000   <exit>               [........ ........ ........ ........]
|  |  |  |  |     |--|-> 0x43ABDB4A000C   <check2>             [29...... ........ ........ ........]
|  |  |  |  |     |  |                .   <check2>             [29...... ........ ........ ........]
|  |  |  |  |     |  |                .   <check2>             [29...... ........ ........ ........]
|  |  |  |  |  |--|--|-  0x43ABDB4A0AEE   <check2>             [29...... ........ ........ ........]
|  |  |  |  |  |  |  |   0x43ABDB4A0B0B   <jmp_exit_8000>
|  |  |  |  |  |  |  |                .   <jmp_exit_8000>
|  |  |  |  |  |  |  |                .   <jmp_exit_8000>
|  |  |  |  |  |  |  |   0x49E7E5410000   <jmp_exit_8000>
|  |  |  |--|--|--|--|-> 0x49E7E541000C   <check_3_1>          [..89.... ........ ........ ........]
|  |  |     |  |  |  |                .   <check_3_1>          [..89.... ........ ........ ........]
|  |  |     |  |  |  |                .   <check_3_1>          [..89.... ........ ........ ........]
|  |  |     |  |  |  |   0x49E7E541BE18   <check_3_1>          [..89.... ........ ........ ........]
|  |  |     |  |  |  |                .   <0jmp_check3_0000>
|  |  |     |  |  |  |   0x49E7E541BE38   <check_3_2>          [..89.... ........ ........ ........]
|  |  |     |  |  |  |                .   <check_3_2>          [..89.... ........ ........ ........]
|  |  |     |  |  |  |                .   <check_3_2>          [..89.... ........ ........ ........]
|  |  |-----|--|--|--|-- 0x49E7E541C038   <check_3_2>          [..89.... ........ ........ ........]
|  |        |  |  |  |   0x49E7E541C056   <jmp_exit_B000>
|  |        |  |  |  |                .   <jmp_exit_B000>
|  |        |  |  |  |                .   <jmp_exit_B000>
|  |        |  |  |  |   0x4A1706820000   <jmp_exit_B000>
|  |        |  |--|--|-> 0x4A170682000C   <check_4>            [....7ED1 ........ ........ ........]
|  |        |     |  |                .   <check_4>            [....7ED1 ........ ........ ........]
|  |        |     |  |                .   <check_4>            [....7ED1 ........ ........ ........]
|  |        |-----|--|-- 0x4A170682EDE0   <check_4>            [....7ED1 ........ ........ ........]
|  |              |  |   0x4A170682EE02   <jmp_exit_0D6F9>
|  |              |  |                .   <jmp_exit_0D6F9>
|  |              |  |                .   <jmp_exit_0D6F9>
|  |              |  |   0x51466A42E6F9   <jmp_exit_0D6F9>
|  |              |--|-- 0x51466A42E705   <main> (ENTRY POINT) [........ ........ ........ ........]
|  |-------------------> 0x59CB440C000C   <check_5>            [........ ........ D54EC08C ........]
|                                     .   <check_5>            [........ ........ D54EC08C ........]
|                                     .   <check_5>            [........ ........ D54EC08C ........]
|----------------------- 0x59CB440C4524   <check_5>            [........ ........ D54EC08C ........]
                         0x59CB440C4556   <jmp_exit_11000>
                                      .   <jmp_exit_11000>
                                      .   <jmp_exit_11000>
                         0x06F4B0E00000   <jmp_exit_11000>

                                                 USER INPUT    [29897ED1 D4D68C99 D54EC08C 89341BEC]
                                                   XOR KEY     [CCFDCBC5 B2A9E62B 0D7E8737 0E2E4F19]
                                                                -------- -------- -------- --------
                                                  FINAL KEY    [E574B514 667F6AB2 D83047BB 871A54F5]

Pour résumer, le programme récupère une clé de 16 octets en entrée. Il la met en minuscule et la convertit en hexadécimal.

Il va ensuite appeler plusieurs fonctions qui vont checker différentes parties de la clé de plusieurs manières.

Check1: check_2()

N2 huge check2

Cette fonction check si le premier octet de la clé est bien 0x29. Si oui, elle passe à check_4(), sinon elle exit.

Check2: check_4()

N2 huge check4

Cette fonction check si le 2ème et le 3ème octet de la clé sont bien 0x7E et 0xD1.

Check3: check_3()

N2 huge check3

Cette fonction initialise à 0 un octet X quelque part après la clé en mémoire. Ce octet est pointé par RAX.

Ensuite, le 2ème octet de la clé est utilisé comme index pour faire un saut en 0x9E38 + 2 * INDEX. Entre les fonctions check_3_1 et check_3_2 se situe un bloc de 0x200 zéros. Sauter dans des zéros.. WTF ? Elle est où l’erreur ?

Non, pas d’erreur, l’instruction 0000 existe bien et correspond à un ADD BYTE PTR [rax], al. C’est l’octet X qui est affecté.

La fonction check_3_2 check si l’octet X est égal à 0x65. La question est donc de savoir à combien d’instructions avant check_3_2 il faut sauter pour que X = 0x65.

AL étant initialisé à 3, chaque instruction ajoute 3 à X. Regardons:

    X = 0x0065  =>  Pas divisible pas 3
    X = 0x0165  =>  0x165 / 3.0 = 0x77     => 0x200 - 0x77 * 2 = 0x112
                                              0x112 / 2.0 = 0x89

Il faut donc sauter en 0x89 * 2 pour que X = 0x65. C’est notre clé.

Check4: check_1()

N2 huge check1

RBX vaut 0x352845AB4BD5, l’adresse virtuelle de loc_4BD5.

La fonction check si le 4ème DWORD de la clé est bien égal à 0xA9B00F5C une fois xoré avec 0x45AB4BD5.

Il doit donc être égal à 0xEC1B3489, soit "89341BEC".

Check5: check_5()

N2 huge check5

Encore un xor. La valeur attendue est 0x8CC04ED5, soit "D54EC08C".

Check6: check_7()

N2 huge check7

Cette fonction commence par xorer le second DWORD de la clé avec la valeur 0x24B87838, puis elle va faire des trucs de math avec le FPU. On a pas envie de regarder ça plus en détail (bien que ça ressemble à un cos(x) = x), alors on code un mini-bruteforcer en ASM qui va tester toutes les valeurs qui match entre 0x00000000 et 0xFFFFFFFF.

Parce que l’ASM, c’est cool
; Chall SSTIC2016 - Level2 - Huge

; nasm -f elf64 -o chall_huge.o  chall_huge.asm
; ld -o chall_huge chall_huge.o
; objdump -D -M intel chall_huge
;


BITS 64

SECTION .data       ; data section

;                                |--------------------|
key:  db 0x00, 0x00, 0x00, 0x00, 0xD4, 0xD6, 0x8C, 0x99, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
      db 0xCB, 0x6D, 0x71, 0x1E, 0x38, 0x78, 0xB8, 0x24, 0xFE, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
;        |--------------------|  |--------------------|

table:  db 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07
        db 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
        db 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
        db 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00

endline: db 0x0A, 0x00

SECTION .text       ; code section
global      _start                              ;must be declared for linker (ld)

_start:                                         ;tell linker entry point

    xor rsi, rsi
    jmp check7

check7:
    xor eax, eax
    mov     ebx, [key + 0x04]
    xor     dword [key+0x14], ebx
    fclex
    fld     tword [key + 0x10]
    fld     st0
    fcos
    fcompp
    fstsw   ax
    and     ax, 0x0FFDF
    cmp     ax, 0x4000
    mov     rbx, 1 ; main_end
    mov     r14, 0 ; exit
    cmovnz  rbx, r14

    cmp rbx, 1
    je is_end
    cmp dword [key + 0x04], 0xFFFFFFFF
    je end

next_loop:
    mov dword [key + 0x14], 0x24B87838 ; initial value
    inc dword [key + 0x04]
    jmp check7

printf:

    mov r9, rsp

    sub     rsp, 0x1000
    and     rsp, 0x0FFFFFFFFFFFFFC00

    ; buffer
    xor rax, rax
    mov rax, [key + 0x04]
    mov [rsp], eax

    movdqa xmm0, [rsp]

    ;
    mov rdi, rsp
    add rdi, 0x40

    movdqa  xmm1, xmm0
    psrlw   xmm1, 4
    xor     edx, edx
    xor     ecx, ecx
    mov     dl, 0x0F
    mov     cl, dl
    xchg    dh, cl
    mov     cx, dx
    bswap   ecx
    or      edx, ecx
    movd    xmm2, edx
    pshufd  xmm2, xmm2, 0
    pand    xmm0, xmm2
    pand    xmm1, xmm2
    movdqa  xmm2, xmm1
    punpcklbw xmm1, xmm0
    punpckhbw xmm2, xmm0
    movdqa  [rdi], xmm1
    movdqa  [rdi+0x10], xmm2
    mov     rbx, table
    mov     ecx, 0x8

printfloop:
    mov     al, [rdi]
    xlat
    add     byte [rdi], 0x30
    add     [rdi], al
    inc     rdi
    loop    printfloop

printendloop:

    xor    rax,rax
    inc    rax
    xor    rdi,rdi
    inc    rdi
    mov    rsi, rsp
    add     rsi, 0x40
    mov    edx,0x8
    syscall

    xor    rax,rax
    inc    rax
    xor    rdi,rdi
    inc    rdi
    mov    rsi,endline
    mov    edx,0x1
    syscall


    mov rsp, r9 ; restore old stack
    ret

is_end:

    call printf
    jmp next_loop
end:
    mov     eax, 0x3C
    xor     dil, dil
    syscall          ; sys_exit(0)
Ce qui donne:
yfrancou@panda$ ./chall_huge
    0xD4D68C99

La valeur attendue est donc 0xD4D68C99, soit "998CD6D4".

Fin: chall_end()

N2 huge end

Pour finir, la clé est xorée avec la clé CCFD CBC5 B2A9 E62B 0D7E 8737 0E2E 4F19, puis réencodée en hexadécimal.

Le résultat est la clé finale du challenge: E574 B514 667F 6AB2 D830 47BB 871A 54F5

foo.zip

"Ce crackme vient d’en bas. Il faudra être EFIcace pour relever ce dEFI, car le débuggueur manque de DEFInition pour ce type de ROM arrangée."

On commence par dézipper ce fichier sans avoir trop de doutes sur son contenu.

yfrancou@panda$ file foo.efi
    foo.efi: PE32 executable (DLL) (EFI application) EFI byte code, for MS Windows

Sans surprise, il s’agit d’une DLL 32 bits EFI avec du byte code EFI.

On se renseigne un peu sur les arguments passés à la fonction main (documentation).

N2 foo start

Le programme récupère les BootServices grâce à la SystemTable. Il s’agit d’un ensemble de fonction disponible uniquement sur la phase de démarrage (contrairement aux RuntimeServices).

N2 foo start1

La clé est mise en minuscule, puis décodée en héxadécimal.

N2 foo start2

Puis le programme fait appel à la fonction check_password avec la SystemTable et la clé comme arguments. Cette fonction est déterminante puisque de son résultat dépend la sortie affichée ("Success!" ou "Sorry :(")

N2 foo start3

Dans la fonction check_password, le programme commence par charger le GUID EFI_AUTHENTICATION_INFO_PROTOCOL_GUID, puis s’en sert pour déchiffrer une autre chaîne de caractères. Il s’avère qu’une fois déchiffrée, la chaîne correspond au GUID EFI_DECOMPRESS_PROTOCOL_GUID:

EFI_AUTHENTICATION_INFO_PROTOCOL_GUID: [0x7671d9d0, 0x53db, 0x4173, 0xaa, 0x69, 0x23, 0x27, 0xf2, 0x1f, 0x0b, 0xc7]
EFI_DECOMPRESS_PROTOCOL_GUID:          [0xd8117cfe, 0x94a6, 0x11d4, 0x9a, 0x3a, 0x00, 0x90, 0x27, 0x3f, 0xc1, 0x4d]
N2 foo xor

Le programme se sert du EFI_DECOMPRESS_PROTOCOL_GUID pour charger une fonction de décompression grâce à la fonction BootServices->LocateProtocol

N2 foo loadfunction

Un bloc de données est ensuite décompressé. La fonction de décompression étant codée en python dans la library uefireverse, bah on s’en sert.

Données compressées:
from uefi_firmware import efi_compressor

compressed_data = "3F0000005C000000003C4C8D823302ED6400B717307DA812AF1B021DFAB86124FE3B24F5" + \
                  "1FCCCE8BA7738572726A518F09C1D4B10C7BA3A1B3CF078FCD9D553475DED963832000"

compressed_data = compressed_data.decode("hex")
decompressed_data = efi_compressor.EfiDecompress(compressed_data, len(compressed_data))
print decompressed_data

> secret data: cb41dcb1d89746705a7fe998f11acce7

La clé est maintenant chiffrée grâce à la fonction encrypt_char, puis est comparée à la clé décompressée.

N2 foo chiffr

Je pense que vous verrez mieux sur ce script en Python :)

def encrypt_char(my_char, i):
    c1 = my_char
    c2 = my_char
    c1 = c1 >> (i % 8)
    c2 = c2 << (8 - (i % 8))
    a = c1 | c2
    a = ~a
    return a & 0xFF

def crack_password(key):
    key = key.decode("hex")

    for i in range(0x10):
        for j in range(0x100):
            c = encrypt_char(j, i)
            if c == ord(key[i]):
                print "[%02X] -> %02X" % (i, j)

crack_password("cb41dcb1d89746705a7fe998f11acce7")

> [00] -> 34
> [01] -> 7D
> [02] -> 8C
> [03] -> 72
> [04] -> 72
> [05] -> 0D
> [06] -> 6E
> [07] -> C7
> [08] -> A5
> [09] -> 01
> [0A] -> 58
> [0B] -> 3B
> [0C] -> E0
> [0D] -> BC
> [0E] -> CC
> [0F] -> 0C

La clé finale du challenge est donc: 347D 8C72 720D 6EC7 A501 583B E0BC CC0C

Niveau 3

Boh allez, pour ceux qui n’ont pas pu voir le niveau 3 :)

N3

strange.zip

"Ce matin, rien n’a changé. Reflétant le soleil, les chiffres me regardent. Attention, ce haïku compte double."

On commence par décompresser le ZIP. (Vous devriez être habitué là normalement).

yfrancou@panda$ ll
    16860160 avril 17 20:08 196
    5252104  avril 17 20:08 a.out
yfrancou@panda$ file *
    196:   data
    a.out: ELF 64-bit LSB  executable, IA-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.4.0, stripped

On a donc un ELF 64bits compilé en IA-64 et un gros bloc de data. Déjà, on sent que le truc d’IA-64, ça pue.

A ce stade, j’aimerais citer Wikipédia, qui dit: "The architecture implements 128 integer registers, 128 floating point registers, 64 one-bit predicates, and eight branch registers."

Bon, la tête commence à tourner.

On l’ouvre avec IDA pour voir.

N3 haiku start

On gerbe. On quitte IDA. On sort prendre l’air.

On revient, on réouvre IDA. On voit ça:

N3 haiku func1

Bon la fonction fait à peu près 2800 instructions. La tête tourne. On voit qu’il y a 164 fonctions comme ça. On re-gerbe. On re-ferme IDA. On s’en va faire les courses.

On revient des courses, on re-ouvre IDA. Puis la on se dit: "Y a pas de raisons que j’y arrive pas. Vais me le faire". Alors on commence à pleurer du sang par les yeux et les oreilles, comme à chaque fois qu’on apprend un nouvel assembleur.

Bon je comprends qu'à ce stade, vous ayez pu prendre peur. Mais je tiens à vous dire que maintenant que j’en ai chié, j’adore l’IA-64. Bref, analysons ce binaire.

Analyse !

Le programme commence par ouvrir un fichier donnée en entrée, à le lire, à le parser et à faire plusieurs checks sur son contenu:
  • La 1ère ligne doit être égale à "P2".

  • La 2nd ligne doit commencer par "#".

  • La 3ème ligne doit être au format "%d %d" et contenir respectivement 0x280 et 0x14.

  • La 4ème ligne doit être au format "%d" et contenir 0xFF.

  • Chaque pixel doit être soit noir (0x00), soit blanc (0xFF)

En googlant, on retrouve rapidement le format de fichier attendu en entrée, qui est une image au format PGM ascii.

Header d’un fichier PGM ascii
P2
# Un commentaire
WIDTH HEIGH
GREY_SCALE

L’image est donc en 0x280 * 0x14, ce qui correspond précisement à la concaténation horizontale de 32 blocs de 0x14 * 0x14. Sachant qu’une clé de 16 octets fait 32 caractères …

Le programme lit ensuite le fichier 196 qui fait 0x1014400 octets. Il découpe ses données en 32 segments de 0x80a20 octets, puis il copie les 32 caractères de l’image PGM au début des 32 segments.

Comme on comprend toujours mieux avec un dessin:
INPUT: 32 caractères sous la forme de blocs de 0x14 * 0x14

 0x00      0x14      0x28      0x3C                                    0x280
 +---------+---------+---------+---------+---------+---------+---------+
 | 0000000 | 1111111 | 2222222 | 3333333 | 4444444 | 5555555 | ....... |
 | 0000000 | 1111111 | 2222222 | 3333333 | 4444444 | 5555555 | ....... |
 | 0000000 | 1111111 | 2222222 | 3333333 | 4444444 | 5555555 | ....... |     etc ..
 | 0000000 | 1111111 | 2222222 | 3333333 | 4444444 | 5555555 | ....... |
 +---------+---------+---------+---------+---------+---------+---------+
    CHAR1     CHAR2     CHAR3      ...


DATA du fichier 196:

            0x00                    0xC80 (1 pixel: 1 double)                         0x80A20
            +-----------------------+-------------------------------------------------+
   SEGMENT0 | 000000000000000000000 |  DATA                                           |
            +-----------------------+-------------------------------------------------+
   SEGMENT1 | 111111111111111111111 |  DATA                                           |
            +-----------------------+-------------------------------------------------+
   SEGMENT2 | 222222222222222222222 |  DATA                                           |
            +-----------------------+-------------------------------------------------+
   SEGMENT3 | 333333333333333333333 |  DATA                                           |
            +-----------------------+-------------------------------------------------+
   SEGMENT4 | 444444444444444444444 |  DATA                                           |
            +-----------------------+-------------------------------------------------+
   SEGMENT5 | etc ...               |                                                 |
            +-----------------------+-------------------------------------------------+

Chacun des segments est ensuite passé dans 2 fonctions que j’ai nommé respectivement check_shapes et check_char.

La fonction check_shapes vérifie que les contours de l’image du caractère (copié dans le segment) sont en blancs (première et dernière ligne, première et dernière colonne).

N3 haiku checkshapes

La fonction check_char est un peu plus compliquée.

N3 haiku checkchar

Dans un premier temps, check_char fait appel aux 160 fonctions que l’on a vu au début en prenant chacune le segment comme argument.

N3 haiku neuron1

On a cru pouvoir y échapper, mais il y a des sadiques sur Terre qui nous en empêche. On s’y colle donc.

Après plusieurs heures et des excès de rage, on voit qu’il y a un schéma récurrent. La fonction est en faite une boucle aplatie (C’est à dire qu’au lieu de faire une boucle, le compilo a répété les instructions les unes après les autres. Et perso, je crois pas qu’il en ait eu l’idée tout seul #SadiqueSurTerre).

Chacune des 160 fonctions correspond en fait à cette fonction mathématique:

N3 haiku function1

avec i allant de 0 à 0x190, les wi étant des coefficients présents dans la section DATA de chaque segment et les xi étant chacun des pixels de l’image.

Cette fonction est entre autres utilisée dans le cas des réseaux neuronaux (documentation très bien faite en français).

Les 160 fonctions représentent donc 160 neurones.

Chaque neurone utilise une fonction d’activation (cf doc).

N3 haiku neuron1 fin

La fonction d’activation utilisée ici est une sigmoïde. (Vous me croirez sur parole je pense). Elle permet de retourner un résultat variant de 0.0 à 1.0.

N3 haiku sigmoid

Bon. Une bonne chose de faite. On a 160 résultats variant entre 0 et 1.

On revient sur la fonction qui apelle les 160 neurones. A la fin se trouve la fonction func_final. Après analyse, il s’agit de 4 autres neurones prenant chacun en entrée les sorties des 160 premiers neurones. Même fonctionnement: pondération des données d’entrée, fonction d’activation, puis résultat entre 0 et 1.

L’analyse est maintenant quasiment terminée. La fonction check_char prend donc les 0x190 pixels, les donne à manger aux 160 premiers neurones, puis donne les 160 résultats aux 4 derniers neurones, qui resortent 4 valeurs entre 0 et 1.

Le programme finit par checker si ces 4 valeurs sont toutes inférieures à 0.15. Si c’est le cas, le caractère est validé.

N3 haiku end

Cracking

Passons maintenant à la phase de cracking.

On sait qu’il s’agit d’un réseau neuronal à 2 couches qui sert à faire de l’OCR.

Nous avons à notre disposition les coefficients de chacun de 164 neurones (présents dans les DATA de chaque segment du fichier 196)

Ce qu’il nous manque à présent, c’est de savoir à quoi doivent ressembler les caractères en image. L’OCR doit être plutôt sensible et bruteforcer sur des formes d’images … Très peu pour nous !

En repensant au tout début, les pixels des caractères sont écrits au début de chaque segment, en écrasant les données précédentes. A quoi bon avoir mis des données ici si c’est pour les supprimer dès le début.

Après les avoir extrait un par un, on s’aperçoit qu’il s’agit d’images PGM ascii de chiffres entre 0 et 9.

N3 haiku characters

On peut à présent coder notre bruteforcer. Pour chaque segment, il chargera le réseau neuronal avec les données des 160 neurones de première couche et celles des 4 neurones de deuxième couche. Puis il testera chacun des 10 chiffres jusqu'à ce que le réseau neuronal réponde 4 valeurs inférieures à 0.15 pour l’un d’entre eux.

Des exemples de script en Python sont disponible sur GitHub pour faire des réseaux neuronaux multicouches, notamment ici.

Bruteforcer BackPropagationNN.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import math
import struct
import random
import numpy as np
np.seterr(all='ignore')


def sigmoid(x):
    return 1 / (1 + np.exp(-x))


# derivative of sigmoid
def dsigmoid(y):
    return y * (1.0 - y)


# using tanh over logistic sigmoid is recommended
def tanh(x):
    return math.tanh(x)


# derivative for tanh sigmoid
def dtanh(y):
    return 1 - y * y


class MLP_NeuralNetwork(object):
    """
    Basic MultiLayer Perceptron (MLP) network, adapted and from the book 'Programming Collective Intelligence'
    (http://shop.oreilly.com/product/9780596529321.do)
    Consists of three layers: input, hidden and output. The sizes of input and output must match data
    the size of hidden is user defined when initializing the network.
    The algorithm has been generalized to be used on any dataset.
    As long as the data is in this format: [[[x1, x2, x3, ..., xn], [y1, y2, ..., yn]],
                                           [[[x1, x2, x3, ..., xn], [y1, y2, ..., yn]],
                                           ...
                                           [[[x1, x2, x3, ..., xn], [y1, y2, ..., yn]]]
    An example is provided below with the digit recognition dataset provided by sklearn
    Fully pypy compatible.
    """
    def __init__(self, input, hidden, output, iterations, learning_rate, momentum, rate_decay, wi=None, wo=None):
        """
        :param input: number of input neurons
        :param hidden: number of hidden neurons
        :param output: number of output neurons
        """
        # initialize parameters
        self.iterations = iterations
        self.learning_rate = learning_rate
        self.momentum = momentum
        self.rate_decay = rate_decay

        # initialize arrays
        self.input = input + 1  # add 1 for bias node
        self.hidden = hidden
        self.output = output

        # set up array of 1s for activations
        self.ai = [1.0] * self.input
        self.ah = [1.0] * self.hidden
        self.ao = [1.0] * self.output

        # create randomized weights
        # use scheme from 'efficient backprop to initialize weights
        if wi is not None and wo is not None:
            self.wi = wi
            self.wo = wo
        else:
            input_range = 1.0 / self.input ** (1 / 2)
            output_range = 1.0 / self.hidden ** (1 / 2)
            self.wi = np.random.normal(loc=0, scale=input_range, size=(self.input, self.hidden))
            self.wo = np.random.normal(loc=0, scale=output_range, size=(self.hidden, self.output))

        # create arrays of 0 for changes
        # this is essentially an array of temporary values that gets updated at each iteration
        # based on how much the weights need to change in the following iteration
        self.ci = np.zeros((self.input, self.hidden))
        self.co = np.zeros((self.hidden, self.output))

    def feedForward(self, inputs):
        """
        The feedforward algorithm loops over all the nodes in the hidden layer and
        adds together all the outputs from the input layer * their weights
        the output of each node is the sigmoid function of the sum of all inputs
        which is then passed on to the next layer.
        :param inputs: input data
        :return: updated activation output vector
        """
        if len(inputs) != self.input - 1:
            raise ValueError('Wrong number of inputs you silly goose!')

        # input activations
        for i in range(self.input - 1):  # -1 is to avoid the bias
            self.ai[i] = inputs[i]

        # hidden activations
        for j in range(self.hidden):
            sum = 0.0
            for i in range(self.input):
                # print i, j, len(self.wi)
                sum += self.ai[i] * self.wi[i][j]
            # self.ah[j] = tanh(sum)
            self.ah[j] = sigmoid(sum)

        # output activations
        for k in range(self.output):
            sum = 0.0
            for j in range(self.hidden):
                sum += self.ah[j] * self.wo[j][k]
            self.ao[k] = sigmoid(sum)

        return self.ao[:]

    def backPropagate(self, targets):
        """
        For the output layer
        1. Calculates the difference between output value and target value
        2. Get the derivative (slope) of the sigmoid function in order to determine how much the weights need to change
        3. update the weights for every node based on the learning rate and sig derivative

        For the hidden layer
        1. calculate the sum of the strength of each output link multiplied by how much the target node has to change
        2. get derivative to determine how much weights need to change
        3. change the weights based on learning rate and derivative
        :param targets: y values
        :param N: learning rate
        :return: updated weights
        """
        if len(targets) != self.output:
            raise ValueError('Wrong number of targets you silly goose!')

        # calculate error terms for output
        # the delta tell you which direction to change the weights
        output_deltas = [0.0] * self.output
        for k in range(self.output):
            error = -(targets[k] - self.ao[k])
            output_deltas[k] = dsigmoid(self.ao[k]) * error

        # calculate error terms for hidden
        # delta tells you which direction to change the weights
        hidden_deltas = [0.0] * self.hidden
        for j in range(self.hidden):
            error = 0.0
            for k in range(self.output):
                error += output_deltas[k] * self.wo[j][k]
            hidden_deltas[j] = dtanh(self.ah[j]) * error

        # update the weights connecting hidden to output
        for j in range(self.hidden):
            for k in range(self.output):
                change = output_deltas[k] * self.ah[j]
                self.wo[j][k] -= self.learning_rate * change + self.co[j][k] * self.momentum
                self.co[j][k] = change

        # update the weights connecting input to hidden
        for i in range(self.input):
            for j in range(self.hidden):
                change = hidden_deltas[j] * self.ai[i]
                self.wi[i][j] -= self.learning_rate * change + self.ci[i][j] * self.momentum
                self.ci[i][j] = change

        # calculate error
        error = 0.0
        for k in range(len(targets)):
            error += 0.5 * (targets[k] - self.ao[k]) ** 2
        return error

    def test(self, patterns):
        """
        Currently this will print out the targets next to the predictions.
        Not useful for actual ML, just for visual inspection.
        """
        for p in patterns:
            print(p[1], '->', self.feedForward(p[0]))

    def train(self, patterns):
        # N: learning rate
        for i in range(self.iterations):
            error = 0.0
            random.shuffle(patterns)
            for p in patterns:
                inputs = p[0]
                targets = p[1]
                self.feedForward(inputs)
                error += self.backPropagate(targets)
            with open('error.txt', 'a') as errorfile:
                errorfile.write(str(error) + '\n')
                errorfile.close()
            if i % 10 == 0:
                print('error %-.5f' % error)
            # learning rate decay
            self.learning_rate = (self.learning_rate * (self.learning_rate /
                                  (self.learning_rate + (self.learning_rate * self.rate_decay))))

    def predict(self, x):
        """
        return list of predictions after training algorithm
        """
        predictions = []
        for p in x:
            predictions.append(self.feedForward(p))
        return predictions


def key_str_to_list(s):
    l = list()

    for line in s.split("\n"):
        if line == "":
            continue
        for c in line.split(" "):
            if c == "":
                continue
            if c == "00":
                l.append(1)
            elif c == "01" or c == "FF":
                l.append(0)
            else:
                raise Exception()
    return l


def key_list_to_str(l):

    m = ""
    n = ""
    for (i, c) in enumerate(l):
        if i % 0x14 == 0 and i != 0:
            m += n + "\n"
            n = ""

        if c == 0x00:
            n += "FF "
        elif c == 0x01:
            n += "00 "
        else:
            raise Exception()
    m += n
    return m


def get_coefs(i_seg=0):

    payload = open("196", "r").read()
    seg_len = 0x80A20
    segment = payload[i_seg * seg_len: (i_seg + 1) * seg_len]

    c1_coefs = list()
    start = 0xC90

    for i in range(0xA0):
        coefs = list()
        offset = start + (i * 0xCA8)
        bias = struct.unpack("<d", segment[offset:][:8])[0]

        for j in range(0x190):
            offset = start + (i * 0xCA8) + 8 + (j * 8)
            coef = struct.unpack("<d", segment[offset:][:8])[0]
            coefs.append(coef)
        coefs.append(bias)
        c1_coefs.append(coefs)

    c2_coefs = list()
    start = 0x7F590

    for i in range(0x4):
        coefs = list()
        offset = start + (i * 0x528)
        bias = struct.unpack("<d", segment[offset:][:8])[0]

        for j in range(0xA0):
            offset = start + (i * 0x528) + 8 + (j * 8)
            coef = struct.unpack("<d", segment[offset:][:8])[0]
            coefs.append(coef)
        coefs.append(bias)
        c2_coefs.append(coefs)

    def invert_matrix(m):
        width = len(m[0])
        height = len(m)

        matrix = list()
        for x in range(width):
            matrix.append([0] * height)

        for i in range(height):
            for j in range(width):
                matrix[j][i] = m[i][j]
        return matrix

    c1_coefs = invert_matrix(c1_coefs)
    c2_coefs = invert_matrix(c2_coefs)

    return (c1_coefs, c2_coefs)


def get_keys_from_segments():
    payload = open("196", "r").read()
    seg_len = 0x80A20

    keys = list()

    keys_name = [
        "key_1",
        "key_2",
        "key_3",
        "key_4",
        "key_5",
        "key_6",
        "key_7",
        "key_8",
        "key_9",
        "key_0",
        "key_1",
        "key_2",
        "key_3",
        "key_4",
        "key_5",
        "key_6",
        "key_7",
        "key_8",
        "key_9",
        "key_0",
        "key_1",
        "key_2",
        "key_3",
        "key_4",
        "key_5",
        "key_6",
        "key_7",
        "key_8",
        "key_9",
        "key_0",
        "key_1",
        "key_2",
    ]

    for i_seg in range(0x20):
        segment = payload[i_seg * seg_len: (i_seg + 1) * seg_len]
        img = list()

        for j in range(0x190):

            offset = j * 8
            pixel = struct.unpack("<d", segment[offset: offset + 8])[0]
            img.append(int(pixel))

        keys.append([keys_name[i_seg], key_list_to_str(img)])
    return keys


def crack():

    keys = get_keys_from_segments()
    for i_seg in range(0x20):
        print "[SEGMENT %02X]" % i_seg
        (c1_coefs, c2_coefs) = get_coefs(i_seg)

        all_results = list()

        for (i, key_t) in enumerate(keys):
            nn = MLP_NeuralNetwork(
                400, 160, 4, iterations=1, learning_rate=0.5, momentum=0.5, rate_decay=0.01,
                wi=c1_coefs, wo=c2_coefs
            )
            key = key_str_to_list(key_t[1])
            predict = nn.predict([key])[0]
            all_results.append((key_t[0], predict))

        sorted_results = dict()
        for (name, (a, b, c, d)) in all_results:
            if a < 0.15 and b < 0.15 and c < 0.15 and d < 0.15:

                if a + b + c + d in sorted_results:
                    # sorted_results[a + b + c + d].append((name, a, b, c, d))
                    pass
                else:
                    sorted_results[a + b + c + d] = [(name, a, b, c, d)]

        for k in sorted(sorted_results.keys()):

            for (name, a, b, c, d) in sorted_results[k]:
                print "\t\t[%s] %-18s %-18s %-18s %-18s" % (name, a, b, c, d)


if __name__ == '__main__':
    crack()
Ce qui nous donne comme résultat:
[SEGMENT 00]    [key_2]   0.00397893338914   9.07235902734e-05  0.0107712598283    0.000232385338803
[SEGMENT 01]    [key_3]   0.000150258845603  0.00114426186293   0.000433806215538  0.002511883198
[SEGMENT 02]    [key_4]   0.108925819944     0.00115368702028   0.00129610047726   0.0147632379909
[SEGMENT 03]    [key_2]   0.00065248920998   0.00256627434545   0.0202907449627    0.0401688858947
[SEGMENT 04]    [key_5]   0.000285190456697  6.55283249582e-06  0.0321615691355    0.0015937999122
[SEGMENT 05]    [key_0]   0.0190255676432    0.0196549852386    0.00652014658993   0.0476222710845
[SEGMENT 06]    [key_3]   0.000474280459947  0.00272898099755   0.00651642879153   0.000534862879727
[SEGMENT 07]    [key_8]   0.00107047285023   0.0131861677783    0.00337426300349   0.000104422901887
[SEGMENT 08]    [key_4]   0.0346358091297    0.00930722886713   0.00242711216706   0.011591411228
[SEGMENT 09]    [key_7]   0.000393501107416  7.98020429957e-05  0.0357698452026    0.00136188206515
[SEGMENT 0A]    [key_2]   0.00576687209341   0.00949866623292   0.0989526545005    0.0567218408648
[SEGMENT 0B]    [key_5]   0.000155003778542  0.00131494723523   0.00839285962663   0.00335151235461
[SEGMENT 0C]    [key_0]   0.00224962920516   0.00489149726988   0.000433849175192  0.000329701371296
[SEGMENT 0D]    [key_8]   0.00173092573784   0.0173238728053    0.0127146052958    0.00489559697394
[SEGMENT 0E]    [key_2]   0.00350636399461   0.00638949555104   0.0634907320975    0.0471890435774
[SEGMENT 0F]    [key_8]   0.000278742716932  0.00532683301239   0.0242360245219    0.0358726261742
[SEGMENT 10]    [key_7]   0.000209201047605  0.00029882487981   0.034428313666     0.00349307557916
[SEGMENT 11]    [key_3]   3.73974302239e-05  0.000642477692785  0.00775703231443   0.000918657566987
[SEGMENT 12]    [key_3]   3.76019766248e-05  0.0712536752451    0.0147318138922    9.69598837011e-05
[SEGMENT 13]    [key_5]   0.000380348322377  9.99852640963e-06  0.0126060266585    0.000794662599401
[SEGMENT 14]    [key_7]   0.00569940933423   6.89805165781e-05  0.0579083006042    0.00139462956296
[SEGMENT 15]    [key_7]   0.000277163691104  0.0215380161339    0.00712988966144   0.00191881284308
[SEGMENT 16]    [key_2]   3.69492309925e-05  0.00873378608598   0.00458676123513   0.0435942672486
[SEGMENT 17]    [key_0]   0.00836409404569   0.038251037843     0.000714150023095  0.00697239126548
[SEGMENT 18]    [key_8]   0.0042862025067    0.0529439318063    0.0683031295941    0.00181514514501
[SEGMENT 19]    [key_5]   0.000243845251903  2.48170433659e-06  0.00637746499481   0.00154341735812
[SEGMENT 1A]    [key_5]   0.0187191509367    0.000112259715837  0.00577404118398   0.0420927528949
[SEGMENT 1B]    [key_4]   0.0310279272703    0.0287967340397    0.000263293275743  0.00977839831836
[SEGMENT 1C]    [key_4]   0.0913491223826    0.00510462882831   0.000901543952428  0.0291667409991
[SEGMENT 1D]    [key_0]   7.17584533219e-05  0.0128361970354    0.00444086344897   0.00124566527801
[SEGMENT 1E]    [key_3]   0.000499119736394  3.56956322264e-05  0.00407884000424   0.00216085973974
[SEGMENT 1F]    [key_5]   0.000860633194031  0.00108052584886   0.0148795661128    0.0324703351717

La clé finale du challenge est donc: 2342 5038 4725 0828 7335 7720 8554 4035

Fin

N4

Ce n’est pas encore fini. Gandalf nous donne encore un fichier final.txt sur lequel on peut lire:

Coucou !

Tu as presque réussi le challenge !
I01p1 y'4qe3553 z41y : 8Y6d5j9Vy88HUGHfGSKsJvqA@ffgvp.bet

Dernier tr0ll, il s’agit d’un ROT13 qui, une fois déchiffré, donne l’adresse mail finale de validation.

8L6q5w9Il88UHTUsTFXfWidN@sstic.org

Merci à toute l'équipe de l’ANSSI et au comité du SSTIC, le challenge était super sympa et j’ai appris beaucoup de choses ! (Même si la plupart ne sont plus utilisés dans la vraie vie #IA64 !).

A tout ceux qui ont terminé de lire mon article, merci de m’avoir lu et à bientôt au SSTIC (moi j’ai ma place mouhaha :D)