Etape 1 : reconnaissance du terrain.
Il s’agit d’un challenge avec un automate industriel Siemens S7-300.
Ce n’est pas mon domaine du tout. Je sens que le challenge sera soit très pointu soit sera raisonnable avec pour difficulté de travailler sur des protocoles inhabituels, des formats inhabituels, des outils spécifiques inhabituels.
On est dans un contexte SCADA. Il y a eu beaucoup de développement sur le domaine suite à Stuxnet.
Etape 2 : Analyse du PCAP.
Le PCAP fourni s’ouvre sans problème avec WIRESHARK. On voit de nombreuses frames du protocole S7COMM. WIRESHARK a en effet maintenant grâce à Stuxnet un dissecteur de protocole S7COMM.
L’énoncé indique :
Les analystes réseaux ont pu récupérer une capture réseau incluant la dernière mise à jour du bloc FC1 contenant le programme de validation.
A priori, on doit donc chercher un upload de firmware dans le PCAP. Initialement, je pensais voir passer un « gros » firmware mais les tailles de frame sont globalement toutes petites. Donc ca ne saute pas immédiatement aux yeux. On va devoir chercher des paquets spécifiques au protocole S7COMM.
En se déplaçant dans les frames on trouve tout d’un coup ce qui nous intéresse vers la fin du fichier.
Lancement du download :
Puis on apperçoit le download du chunk 1 de FC1 au paquet 606.
Download du chunk 2 de FC1 au paquet 612.
Fin de download au paquet 616.
Activation du firmware au paquet 619.
La frame 599 est de type « Request download ».
Il y est bien question de « FC1 » puisqu’on voit un « Block type »
de valeur FC et un « Block number » de valeur 00001.
On annonce un « Length of load memory » de 376 bytes.
Les 4 frames suivantes 604 - 606 - 609 - 612 (2 allers, 2 retours) sont de type « Download Block ». On se rapproche.
Le firmware est en fait tout petit et est envoyé en 2 chunks :
- frame 606 : payload de 222 bytes
- frame 612 : payload de 154 bytes
Au total 376 bytes. Cette taille est cohérente avec ce que l’on attendait.
A ce niveau on a donc un beau fichier binaire qui ne ressemble à rien :
00000000: 7070 0101 020c 0001 0000 0178 0000 0000 pp.........x....
00000010: 03fa ec23 3c30 046a 9e18 3c2a 0008 001a ...#<0.j..<*....
00000020: 0000 010e ba00 2001 00c0 0002 2001 00c1 ...... ..... ...
00000030: 0002 2001 00c2 0002 2001 00c3 0002 cf01 .. ..... .......
00000040: bf00 ff98 0009 2001 7e42 0000 5386 681d ...... .~B..S.h.
00000050: 682c 681c ffe0 2001 09c0 0002 2001 09c1 h,h... ..... ...
00000060: 0002 2001 09c2 0002 2001 09c3 0002 c400 .. ..... .......
00000070: e500 e600 e700 ba00 5202 3003 1234 2180 ........R.0..4!.
00000080: bf00 ff98 0008 3003 0002 5386 681d 682c ......0...S.h.h,
00000090: 681c ffe0 2001 0940 0002 e400 c500 e600 h... ..@........
000000a0: e700 ba00 5202 3003 4849 2180 bf00 2001 ....R.0.HI!... .
000000b0: 0040 0002 ff98 0008 3003 0003 5386 681d .@......0...S.h.
000000c0: 682c 681c ffe0 2001 0941 0002 e400 e500 h,h... ..A......
000000d0: c600 e700 ba00 5202 3003 1394 2180 bf00 ......R.0...!...
000000e0: 2001 0041 0002 ff98 0008 3003 0004 5386 ..A......0...S.
000000f0: 681d 682c 681c ffe0 2001 0942 0002 e400 h.h,h... ..B....
00000100: e500 e600 c700 ba00 5202 3003 7452 2180 ........R.0.tR!.
00000110: bf00 2001 0042 0002 ff98 0008 3003 7777 .. ..B......0.ww
00000120: 5386 681d 682c 681c ffe0 2001 0943 0002 S.h.h,h... ..C..
00000130: 6500 0101 0000 0000 0020 0600 4a00 2c00 e........ ..J.,.
00000140: 3200 3200 3200 0000 0500 0f00 2f00 4800 2.2.2......./.H.
00000150: 6100 7a00 0000 0000 0000 0000 0000 0000 a.z.............
00000160: 0000 0000 0000 0000 0000 0000 0100 0251 ...............Q
00000170: 0000 0000 0000 0000 ........
Etape 2 alternative : Analyse du PCAP.
En fait, je n’ai pas exactement procédé comme vu ci-dessus.
En cherchant de la doc sur le protocole S7COMM, j’ai trouvé le site https://networkforensic.dk/Tools/tools.html qui fournit un jeu de préréglages pour WIRESHARK pour faciliter le traitement de trafic S7COMM.
En s’aidant de ces filtres préconçus, on voit tout de suite mieux les choses :
Etape 3 : Analyse du firmware FC1.
Là, il faut se renseigner sur la programmation des automates Siemens.
On trouve les informations suivantes en cherchant à droite, à gauche :
- les outils officiels sont des produits Siemens « Step 7 » ou « TIA »
- en gros le compilateur « Step 7 » convertit divers langages spécialisés de programmation en du byte-code « MC7 »
- il y a donc un language assembleur pour ce byte-code ; voir https://www.pnfsoftware.com/jeb/simatic-s7-stl-opcodes.html
- il y a un décompilateur commercial appelé « JEB » pouvant décompiler entre autre du code « MC7 » fait par la société PNL ; on trouve beaucoup de documentation intéressante sur le format MC7 sur leur site https://www.pnfsoftware.com/jeb/
- il y a un décompilateur open source de code « MC7 » sous forme d’un module pour RIZIN ; voir https://github.com/rizinorg/rizin et https://github.com/rizinorg/rz-libmc7 (il y a aussi un module pour RADARE2).
Je commence donc par le module rz-libmc7 pour RIZIN. Malheureusement le
code du module ne se compile pas avec la dernière version 0.9.1 de RIZIN.
Je tente le coup d’ouvrir un
issue
sur le repo du module et là, coup de chance, l’auteur répond dans la demi
journée et pousse un gros commit qui rattrape le retard dans le développement
du module par rapport au moteur RIZIN. Au final, le module se compile,
fonctionne et me donne un code désassemblé lisible du firmware mais avec
une bizarrerie sur un JUMP vers une adresse qui n’existe pas ce qui est
anormal.
...
0x0000003e O I 1.7
0x00000040 )
+─< 0x00000042 JNB 0x4b
│ ;-- section.data:
│ 0x00000046 OPN DB 1 ; [02] -rw- section size 270 named data
│ 0x00000048 L DBW 0
Ca va où ? 0x0000004c T QW 6
0x0000004e SET
...
En parallèle, je tente la version d’évaluation de la version commerciale de JEB (car seule la version commerciale embarque le désassembleur MC7, rien dans la community edition). Le firmware se désassemble sans souci mais ne montre pas d’erreur de JUMP. Les JUMPS y semblent logiques au contraire. Je soupçonne donc un petit bug dans le module pour RIZIN. La version d’évaluation ne permet pas de sauver le code désassemblé. En voici donc un screenshot :
Je fusionne les 2 décompilations…
Je rappelle l’URL expliquant les instructions assembleur de MC7 : https://www.pnfsoftware.com/jeb/simatic-s7-stl-opcodes.html
Globalement on a maintenant un code désassemblé que voici (avec juste quelques commentaires mineurs et j’ai mis des labels plus parlants pour les jumps) :
; func_FC1 proc
0x00000024 A(
0x00000026 OPN DB 1
0x00000028 AN DBX 2.0
0x0000002c OPN DB 1
0x0000002e AN DBX 2.1
0x00000032 OPN DB 1
0x00000034 AN DBX 2.2
0x00000038 OPN DB 1
0x0000003a AN DBX 2.3
0x0000003e O I 1.7
0x00000040 )
+----- 0x00000042 JNB loc_CODE_1
| 0x00000046 OPN DB 1
| 0x00000048 L DBW 0
| 0x0000004c T QW 6
| 0x0000004e SET
| 0x00000050 SAVE
| 0x00000052 CLR
| loc_CODE_1:
+----> 0x00000054 A BR
0x00000056 OPN DB 1
0x00000058 R DBX 2.0
0x0000005c OPN DB 1
0x0000005e R DBX 2.1
0x00000062 OPN DB 1
0x00000064 R DBX 2.2
0x00000068 OPN DB 1
0x0000006a R DBX 2.3
0x0000006e A I 0.4 -- ( %I0.4 == 1 ) code 1
0x00000070 AN I 0.5 -- ( %I0.5 == 0 )
0x00000072 AN I 0.6 -- ( %I0.6 == 0 )
0x00000074 AN I 0.7 -- ( %I0.7 == 0 )
0x00000076 A( --
0x00000078 L IW 2
0x0000007a L 4660
0x0000007e ==I
0x00000080 )
+----- 0x00000082 JNB loc_CODE_2
| 0x00000086 L 2
| 0x0000008a T QW 6 -- affiche 2
| 0x0000008c SET
| 0x0000008e SAVE
| 0x00000090 CLR
| loc_CODE_2:
+----> 0x00000092 A BR
0x00000094 OPN DB 1
0x00000096 S DBX 2.0
0x0000009a AN I 0.4 -- ( %I0.4 == 0 )
0x0000009c A I 0.5 -- ( %I0.5 == 1 ) code 2
0x0000009e AN I 0.6 -- ( %I0.6 == 0 )
0x000000a0 AN I 0.7 -- ( %I0.7 == 0 )
0x000000a2 A(
0x000000a4 L IW 2
0x000000a6 L 18505
0x000000aa ==I
0x000000ac )
0x000000ae OPN DB 1
0x000000b0 A DBX 2.0
+----- 0x000000b4 JNB loc_CODE_3
| 0x000000b8 L 3
| 0x000000bc T QW 6 -- affiche 3
| 0x000000be SET
| 0x000000c0 SAVE
| 0x000000c2 CLR
| loc_CODE_3:
+----> 0x000000c4 A BR
0x000000c6 OPN DB 1
0x000000c8 S DBX 2.1
0x000000cc AN I 0.4 -- ( %I0.4 == 0 )
0x000000ce AN I 0.5 -- ( %I0.5 == 0 )
0x000000d0 A I 0.6 -- ( %I0.6 == 1 ) code 3
0x000000d2 AN I 0.7 -- ( %I0.7 == 0 )
0x000000d4 A(
0x000000d6 L IW 2
0x000000d8 L 5012
0x000000dc ==I
0x000000de )
0x000000e0 OPN DB 1
0x000000e2 A DBX 2.1
+----- 0x000000e6 JNB loc_CODE_4
| 0x000000ea L 4 -- affiche 4
| 0x000000ee T QW 6
| 0x000000f0 SET
| 0x000000f2 SAVE
| 0x000000f4 CLR
| loc_CODE_4:
+----> 0x000000f6 A BR
0x000000f8 OPN DB 1
0x000000fa S DBX 2.2
0x000000fe AN I 0.4 -- ( %I0.4 == 0 )
0x00000100 AN I 0.5 -- ( %I0.5 == 0 )
0x00000102 AN I 0.6 -- ( %I0.6 == 0 )
0x00000104 A I 0.7 -- ( %I0.7 == 1 ) code 4
0x00000106 A(
0x00000108 L IW 2
0x0000010a L 29778
0x0000010e ==I
0x00000110 )
0x00000112 OPN DB 1
0x00000114 A DBX 2.2
+----- 0x00000118 JNB loc_100114
| -- Si les 4 codes sont corrects alors on affiche "7777".
| 0x0000011c L 30583
| 0x00000120 T QW 6
| 0x00000122 SET
| 0x00000124 SAVE
| 0x00000126 CLR
| loc_100114:
+----> 0x00000128 A BR
0x0000012a OPN DB 1
0x0000012c S DBX 2.3
0x00000130 BE
; func_FC1 endp
On identifie clairement 4 blocs qui se ressemblent. Justement on a 4 codes de lancement à entrer successivement donc chacun des blocs traite donc un de ces codes.
J’avoue ne pas avoir tout bien compris du code, on comprend globalement ce qui suffit pour un challenge, inutile de perdre du temps sur des petits détails.
La roue codeuse est memory-mapped dans la variable « IW 2 » (l’énoncé
annonçait %IW2).
On voit successivement :
0x00000076 A(
0x00000078 L IW 2
0x0000007a L 4660
0x0000007e ==I
0x00000080 )
0x000000a2 A(
0x000000a4 L IW 2
0x000000a6 L 18505
0x000000aa ==I
0x000000ac )
0x000000d4 A(
0x000000d6 L IW 2
0x000000d8 L 5012
0x000000dc ==I
0x000000de )
0x00000106 A(
0x00000108 L IW 2
0x0000010a L 29778
0x0000010e ==I
0x00000110 )
Donc on loade (« L ») la valeur de la roue codeuse et on la compare avec une « immediate value » (respectivement 4660, 18505, 5012, 29778).
Là, je ne comprends plus trop ce qui se passe parce que la roue codeuse ne permet d’entrer que des chiffres entre 0 et 9. Donc je m’attendais à voir des immediate valeurs entre 0000 et 9999 or deux valeurs dépassent 9999 ???
La solution viendra de ce qui se passe quand les 4 codes entrés sont
corrects. L’afficheur (memory mapped sous la forme de la variable
QW 6) affiche alors 7777 d’après l’énoncé. Or ici on voit
0x0000011c L 30583
0x00000120 T QW 6
Je ne sais plus pourquoi mais j’ai l’idée de convertir en hexa la valeur
30583 pour voir que cela donne 0x7777. Donc l’afficheur fonctionne en
hexa…
Je pense du coup que ce qui est écrit sur la roue codeuse est en fait vu de l’automate comme étant de l’hexadécimal aussi et non du décimal. Si l’on reprend les valeurs trouvées 4660, 18505, 5012, 29778 et qu’on les convertit en hexa, cela donne : 0x1234, 0x4849, 0x1394, 0x7452, valeurs qui ont le bon goût de ne comporter que des chiffres décimaux.
Le flag doit donc être :
FCSC{1234-4849-1394-7452}
Bingo. Ce flag se valide.
QED.
🐒