
À PHDays 9, nous avons organisé un concours pour le piratage d'une usine de pompage de gaz - le concours
Industrial Ninja . Il y avait trois stands sur le site avec des paramètres de sécurité différents (No Security, Low Security, High Security) imitant le même processus industriel: l'air était pompé dans le ballon (puis descendait) sous pression.
Malgré divers paramètres de sécurité, le matériel des stands était le même: Siemens Simatic S7-300 PLC; bouton de purge d'urgence et dispositif de mesure de pression (connectés aux entrées numériques de l'automate (DI)); vannes de pompage et de purge (connectées aux sorties numériques de l'automate (DO)) - voir la figure ci-dessous.

Le PLC, en fonction des relevés de pression et conformément à son programme, a décidé de souffler ou de gonfler la bille (ouvrir et fermer les vannes correspondantes). Cependant, un mode de contrôle manuel était prévu sur tous les stands, ce qui permettait de contrôler l'état des vannes sans aucune restriction.
Les stands se distinguaient par la complexité de l'activation de ce mode: sur un stand non protégé c'était le plus facile à faire, mais sur le stand Haute Sécurité, en conséquence, c'était plus difficile.
En deux jours, cinq des six problèmes ont été résolus; le vainqueur de la première place a obtenu 233 points (il a passé une semaine à préparer la compétition). Trois gagnants: je place - a1exdandy, II - Rubikoid, III - Ze.
Cependant, pendant les PHDays, aucun des participants n'a pu surmonter les trois stands, nous avons donc décidé de lancer un concours en ligne et publié la tâche la plus difficile début juin. Les participants devaient terminer la tâche en un mois, trouver le drapeau, décrire la solution en détail et de manière intéressante.
Sous la coupe, nous publions une analyse de la meilleure solution à la mission envoyée au cours du mois, elle a été trouvée par Alexey Kovrizhnykh (a1exdandy) de la société Digital Security, qui a pris la 1ère place du concours lors des PHDays. Ci-dessous, nous fournissons son texte avec nos commentaires.
Analyse initiale
Donc, dans la tâche, il y avait une archive avec des fichiers:
- block_upload_traffic.pcapng
- DB100.bin
- hints.txt
Le fichier hints.txt contient les informations et les conseils nécessaires pour résoudre la tâche. Voici son contenu:
- Petrovich m'a dit hier que depuis PlcSim, vous pouvez télécharger des blocs à l'étape 7.
- Sur le stand, des automates Siemens Simatic S7-300 ont été utilisés.
- PlcSim est un émulateur d'automate qui vous permet d'exécuter et de déboguer des programmes pour les automates Siemens S7.
Le fichier DB100.bin contient apparemment un bloc de données API DB100: 00000000: 0100 0102 6e02 0401 0206 0100 0101 0102 .... n ........... 00000010: 1002 0501 0202 2002 0501 0206 0100 0102 ...... ......... 00000020: 0102 7702 0401 0206 0100 0103 0102 0a02 ..w ............. 00000030: 0501 0202 1602 0501 0206 0100 0104 0102 ............... 00000040: 7502 0401 0206 0100 0105 0102 0a02 0501 u ............... 00000050: 0202 1602 0501 0206 0100 0106 0102 3402 .............. 4. 00000060: 0401 0206 0100 0107 0102 2602 0501 0202 .......... & ..... 00000070: 4c02 0501 0206 0100 0108 0102 3302 0401 L ........... 3. .. 00000080: 0206 0100 0109 0102 0a02 0501 0202 1602 ............... 00000090: 0501 0206 0100 010a 0102 3702 0401 0206 .......... 7. .... 000000a0: 0100 010b 0102 2202 0501 0202 4602 0501 ...... "..... F ... 000000b0: 0206 0100 010c 0102 3302 0401 0206 0100 ........ 3. ...... 000000c0: 010d 0102 0a02 0501 0202 1602 0501 0206 ............... 000000d0: 0100 010e 0102 6d02 0401 0206 0100 010f ...... m. ........ 000000e0: 0102 1102 0501 0202 2302 0501 0206 0100 ........ # ....... 000000f0: 0110 0102 3502 0401 0206 0100 0111 0102 .... 5. .......... 00000100: 1202 0501 0202 2502 0501 0206 0100 0112 ......% ......... 00000110: 0102 3302 0401 0206 0100 0113 0102 2602 ..3. .......... &. 00000120: 0501 0202 4c02 0501 0206 0100 .... L .......
A en juger par le nom, le fichier block_upload_traffic.pcapng contient un vidage du trafic de chargement de bloc vers l'automate.
Il est à noter que cette décharge de trafic sur le site de compétition lors de la conférence était un peu plus difficile à obtenir. Pour ce faire, il était nécessaire de comprendre le script du fichier de projet pour TeslaSCADA2. De là, il a été possible de comprendre où se trouve le vidage chiffré avec RC4 et quelle clé doit être utilisée pour le déchiffrer. Des vidages de blocs de données sur le site peuvent être obtenus à l'aide du client de protocole S7. J'ai utilisé le client de démonstration du package Snap7 pour cela.
Extraire des unités de traitement du signal d'une décharge de trafic
En regardant le contenu du dump, on peut comprendre que les blocs de traitement du signal OB1, FC1, FC2 et FC3 y sont transmis:

Il est nécessaire d'extraire ces blocs. Cela peut être fait, par exemple, avec le script suivant, après avoir converti le trafic du format pcapng en pcap:
Après avoir étudié les blocs reçus, vous pouvez remarquer qu'ils commencent toujours par les octets 70 70 (pp). Vous devez maintenant apprendre à les analyser. Un indice de la tâche suggère que vous devez utiliser PlcSim pour cela.
Obtention d'instructions lisibles par l'homme à partir de blocs
Tout d'abord, essayons de programmer S7-PlcSim en y chargeant plusieurs blocs avec des instructions répétitives (= Q 0.0) à l'aide du logiciel Simatic Manager, et enregistrons le résultat dans l'émulateur PLC dans le fichier example.plc. En regardant le contenu du fichier, vous pouvez facilement déterminer le début des blocs chargés par la signature 70 70, que nous avons découverte plus tôt. Avant les blocs, apparemment, la taille du bloc est écrite sous la forme d'une valeur little-endian de 4 octets.

Après avoir reçu des informations sur la structure des fichiers plc, le plan d'action suivant est apparu pour la lecture des programmes PLC S7:
- À l'aide de Simatic Manager, nous créons une structure de blocs dans S7-PlcSim similaire à celle que nous avons obtenue du vidage. Les tailles de bloc doivent correspondre (obtenues en remplissant les blocs avec le bon nombre d'instructions) et leurs identifiants (OB1, FC1, FC2, FC3).
- Enregistrez l'automate dans un fichier.
- Nous remplaçons le contenu des blocs dans le fichier reçu par les blocs de la décharge de trafic. Le début des blocs est déterminé par la signature.
- Le fichier résultant est téléchargé sur S7-PlcSim et nous examinons le contenu des blocs dans Simatic Manager.
Les blocs peuvent être remplacés, par exemple, par le code suivant:
with open('original.plc', 'rb') as f: plc = f.read() blocks = [] for fname in ['OB1.bin', 'FC1.bin', 'FC2.bin', 'FC3.bin']: with open(fname, 'rb') as f: blocks.append(f.read()) i = plc.find(b'pp') for block in blocks: plc = plc[:i] + block + plc[i+len(block):] i = plc.find(b'pp', i + 1) with open('target.plc', 'wb') as f: f.write(plc)
Aleksey a continué sur une voie peut-être plus compliquée, mais toujours dans le bon sens. Nous avons supposé que les participants utiliseraient le programme NetToPlcSim pour que PlcSim puisse communiquer sur le réseau, charger des blocs dans PlcSim via Snap7, puis télécharger ces blocs en tant que projet depuis PlcSim en utilisant l'environnement de développement.
En ouvrant le fichier résultant dans S7-PlcSim, vous pouvez lire des blocs remplacés à l'aide de Simatic Manager. Les principales fonctions de gestion des appareils sont enregistrées dans le bloc FC1. La variable # TEMP0 attire une attention particulière, lorsqu'elle est allumée, il semble que la commande PLC passe en mode manuel sur la base des valeurs de la mémoire binaire M2.2 et M2.3. # TEMP0 est défini par FC3.

Pour résoudre le problème, il est nécessaire d'analyser la fonction FC3 et de comprendre ce qui doit être fait pour qu'elle renvoie une unité logique.
Les blocs de traitement du signal PLC sur le stand Low Security sur le site de la compétition étaient disposés de la même manière, mais pour définir la valeur de la variable # TEMP0, il suffisait d'écrire la ligne mon chemin ninja sur le bloc DB1. La vérification de la valeur dans le bloc était organisée clairement et ne nécessitait pas une connaissance approfondie du langage de programmation de bloc. De toute évidence, au niveau de la haute sécurité, il sera beaucoup plus difficile de réaliser un contrôle manuel et il est nécessaire de comprendre les subtilités du langage STL (l'une des façons de programmer l'automate S7).
Reverse Block FC3
Le contenu du bloc FC3 dans la représentation STL: Le code est assez volumineux et pour une personne peu familière avec STL, cela peut sembler compliqué. Cela n'a aucun sens de démonter chaque instruction dans le cadre de cet article, pour des instructions détaillées et les fonctionnalités du langage STL, voir le manuel correspondant:
Liste d'instructions (STL) pour la programmation S7-300 et S7-400 . Ici, je vais donner le même code après le traitement - renommer les étiquettes et les variables et ajouter des commentaires décrivant l'algorithme de travail et certaines constructions du langage STL. Je remarque tout de suite que dans le bloc considéré, une machine virtuelle est implémentée qui exécute un bytecode situé dans le bloc DB100, dont nous connaissons le contenu. Les instructions de la machine virtuelle sont 1 octet de code de fonctionnement et octets d'arguments, un octet pour chaque argument. Toutes les instructions examinées ont deux arguments, j'ai désigné leurs valeurs dans les commentaires comme X et Y.
Ayant une idée des instructions de la machine virtuelle, nous allons écrire un petit désassembleur pour analyser le bytecode dans le bloc DB100:
import string alph = string.ascii_letters + string.digits with open('DB100.bin', 'rb') as f: m = f.read() pc = 0 while pc < len(m): op = m[pc] if op == 1: print('R{} = DB101[{}]'.format(m[pc + 2], m[pc + 1])) pc += 3 elif op == 2: c = chr(m[pc + 1]) c = c if c in alph else '?' print('R{} = {:02x} ({})'.format(m[pc + 2], m[pc + 1], c)) pc += 3 elif op == 4: print('R0 = 0; R{} = (R{} == R{})'.format( m[pc + 1], m[pc + 1], m[pc + 2])) pc += 3 elif op == 5: print('R0 = 0; R{} = R{} - R{}'.format( m[pc + 1], m[pc + 1], m[pc + 2])) pc += 3 elif op == 6: print('CHECK (R{} == R{})\n'.format( m[pc + 1], m[pc + 2])) pc += 3 else: print('unk opcode {}'.format(op)) break
Par conséquent, nous obtenons le code de machine virtuelle suivant:
Code de la machine virtuelle R1 = DB101[0] R2 = 6e (n) R0 = 0; R1 = (R1 == R2) CHECK (R1 == R0) R1 = DB101[1] R2 = 10 (?) R0 = 0; R1 = R1 - R2 R2 = 20 (?) R0 = 0; R1 = R1 - R2 CHECK (R1 == R0) R1 = DB101[2] R2 = 77 (w) R0 = 0; R1 = (R1 == R2) CHECK (R1 == R0) R1 = DB101[3] R2 = 0a (?) R0 = 0; R1 = R1 - R2 R2 = 16 (?) R0 = 0; R1 = R1 - R2 CHECK (R1 == R0) R1 = DB101[4] R2 = 75 (u) R0 = 0; R1 = (R1 == R2) CHECK (R1 == R0) R1 = DB101[5] R2 = 0a (?) R0 = 0; R1 = R1 - R2 R2 = 16 (?) R0 = 0; R1 = R1 - R2 CHECK (R1 == R0) R1 = DB101[6] R2 = 34 (4) R0 = 0; R1 = (R1 == R2) CHECK (R1 == R0) R1 = DB101[7] R2 = 26 (?) R0 = 0; R1 = R1 - R2 R2 = 4c (L) R0 = 0; R1 = R1 - R2 CHECK (R1 == R0) R1 = DB101[8] R2 = 33 (3) R0 = 0; R1 = (R1 == R2) CHECK (R1 == R0) R1 = DB101[9] R2 = 0a (?) R0 = 0; R1 = R1 - R2 R2 = 16 (?) R0 = 0; R1 = R1 - R2 CHECK (R1 == R0) R1 = DB101[10] R2 = 37 (7) R0 = 0; R1 = (R1 == R2) CHECK (R1 == R0) R1 = DB101[11] R2 = 22 (?) R0 = 0; R1 = R1 - R2 R2 = 46 (F) R0 = 0; R1 = R1 - R2 CHECK (R1 == R0) R1 = DB101[12] R2 = 33 (3) R0 = 0; R1 = (R1 == R2) CHECK (R1 == R0) R1 = DB101[13] R2 = 0a (?) R0 = 0; R1 = R1 - R2 R2 = 16 (?) R0 = 0; R1 = R1 - R2 CHECK (R1 == R0) R1 = DB101[14] R2 = 6d (m) R0 = 0; R1 = (R1 == R2) CHECK (R1 == R0) R1 = DB101[15] R2 = 11 (?) R0 = 0; R1 = R1 - R2 R2 = 23 (?) R0 = 0; R1 = R1 - R2 CHECK (R1 == R0) R1 = DB101[16] R2 = 35 (5) R0 = 0; R1 = (R1 == R2) CHECK (R1 == R0) R1 = DB101[17] R2 = 12 (?) R0 = 0; R1 = R1 - R2 R2 = 25 (?) R0 = 0; R1 = R1 - R2 CHECK (R1 == R0) R1 = DB101[18] R2 = 33 (3) R0 = 0; R1 = (R1 == R2) CHECK (R1 == R0) R1 = DB101[19] R2 = 26 (?) R0 = 0; R1 = R1 - R2 R2 = 4c (L) R0 = 0; R1 = R1 - R2 CHECK (R1 == R0)
Comme vous pouvez le voir, ce programme vérifie simplement chaque symbole de DB101 pour une égalité à une certaine valeur. La dernière ligne pour passer tous les contrôles: n0w u 4r3 7h3 m4573r. Si cette ligne est placée dans le bloc DB101, la commande manuelle de l'automate est activée et il sera possible de gonfler ou de dégonfler le ballon.
C'est tout! Alexey a démontré un haut niveau de connaissances digne d'un ninja industriel :) Nous avons envoyé des prix mémorables au gagnant. Un grand merci à tous les participants!