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)
-
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:
-
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 !"
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+:
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.
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
.
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.
"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.
"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()
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()
Cette fonction check si le 2ème et le 3ème octet de la clé sont bien 0x7E
et 0xD1
.
Check3: check_3()
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()
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()
Encore un xor. La valeur attendue est 0x8CC04ED5
, soit "D54EC08C".
Check6: check_7()
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.
; 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)
yfrancou@panda$ ./chall_huge
0xD4D68C99
La valeur attendue est donc 0xD4D68C99
, soit "998CD6D4".
Fin: chall_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).
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
).
La clé est mise en minuscule, puis décodée en héxadécimal.
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 :("
)
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]
Le programme se sert du EFI_DECOMPRESS_PROTOCOL_GUID
pour charger une fonction de décompression grâce à la fonction BootServices->LocateProtocol
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.
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.
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 :)
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.
On gerbe. On quitte IDA. On sort prendre l’air.
On revient, on réouvre IDA. On voit ça:
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 !
-
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.
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.
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).
La fonction check_char
est un peu plus compliquée.
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.
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:
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).
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.
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é.
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.
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.
#!/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()
[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
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)