Le Port de Cobalt fait face à une alerte de collision maritime entre deux navires.
À vous de remonter la piste du navire intercepteur pour découvrir le mode opératoire des attaquants.
Suite à votre découverte de la compromission d'un serveur, vous soupçonnez que l'alerte de collision maritime survenue à une période similaire aurait pu être une diversion.
Vous disposez d’un fichier de logs des messages AIS collecté le jour de l'attaque, contenant les phrases AIVDM horodatées de rapport d'état et de position des navires sur cette période (MMSI, latitude/longitude, cap, vitesse...).
En tant qu'expert forensic, analysez ces logs pour déterminer l'identité du navire intercepteur et de sa cible. Ces éléments permettront par la suite de consolider la compréhension du mode opératoire de l'attaque, et à terme de remonter jusqu'aux attaquants en recoupant avec d'autres informations.
Comme annoncé dans le sujet, on a l'indication que l'incident survient en début d'après-midi. On va donc réduire la quantité de logs à analyser en ne gardant que ceux entre 11h et 17h (on prend large pour être certain de ne pas passer à côté, car le concept de début d'après-midi est subjectif). De plus, comme on ne va s'intéresser qu'aux messages AIVDM de type 1-3 (signalement de positions), on va également supprimer les phrases AIVDM typiques des messages de type 5 (signalement des informations générales du navire).
Une phrase AIVDM de type 5 est composée de 2 fragments ayant ce format :
!AIVDM,2,1,0,A,53J4Od02F=@=47D?A5a=@5AE@u9V0E<<590lDq@N0`100000010000000000,0*77
!AIVDM,2,2,0,A,00000000000,2*24 # répétitions exactes dans notre simulations, car les bits de poids faible ne sont pas utilisés
Tandis qu'une phrase AIVDM de type 1-3 ressemble à ceci :
!AIVDM,1,1,,A,13IwU1h;iF8m@0hL0=Ie=bWp0000,0*5A
On peut donc distinguer ces 2 types de messages AIVDM et n'en garder qu'un :
lower = '2028-04-30-11:00:00:000000'
upper = '2028-04-30-17:00:00:000000'
AIVDM_type1_sample = "!AIVDM,1,1,,A,13IwU1h;iF8m@0hL0=Ie=bWp0000,0*5A"
with open("AIVDM.logs", 'r') as fIN:
with open("AIVDM.logs.filtered", 'w') as fOUT:
for record in fIN:
timestamp, AIVDM = record.split(' ')
AIVDM = AIVDM.strip()
if lower < timestamp and timestamp < upper:
# Les messages AIVDM sont à taille fixe selon leur type
if AIVDM != "!AIVDM,2,2,0,A,00000000000,2*24" and len(AIVDM) == len(AIVDM_type1_sample):
fOUT.write(f"{record}")
On retrouvera nos logs filtrés dans le fichier AIVDM.logs.filtered.
Il nous faut maintenant un moyen de connaitre le contenu décodé des phrases AIVDM. Il y a pour cela 2 méthodes possibles :
AIVDM_sample = "!AIVDM,1,1,,A,13IwU1h;iF8m@0hL0=Ie=bWp0000,0*5A"
decoded = decode_aivdm(AIVDM_sample).asdict()
print(decoded)
# {'msg_type': 1, 'repeat': 0, 'mmsi': 228582663, 'status':
```
Méthode manuelle : Analyse du format AIVDM et décodage manuel
Extraire le payload
Par exemple pour le message type 1 :
```python
def to_ASCII_6bits(payload: str) -> str:
bitstring = ''
for char in payload:
value = ord(char) - 48
if value > 40:
value -= 8
bits = format(value, '06b')
bitstring += bits
return bitstring
def bits_to_int(bits) -> int:
return int(bits, 2)
def decode_type1(bits) -> dict:
result = {}
result['msg_type'] = bits_to_int(bits[0:6])
result['repeat'] = bits_to_int(bits[6:8])
result['mmsi'] = bits_to_int(bits[8:38])
result['status'] = bits_to_int(bits[38:42])
result['turn'] = bits_to_int(bits[42:50])
if result['turn'] == 128:
result['turn'] = None
else:
if result['turn'] > 127:
result['turn'] -= 256
rot = (abs(result['turn']) / 4.733) ** 2
result['turn'] = rot if result['turn'] >= 0 else -rot
result['speed'] = bits_to_int(bits[50:60]) / 10.0
result['accuracy'] = bits_to_int(bits[60:61])
lon_raw = int(bits[61:89], 2)
if lon_raw & (1 << 27): # C2 pour nombres négatifs
lon_raw -= 1 << 28
result['lon'] = lon_raw / 600000.0
lat_raw = int(bits[89:116], 2)
if lat_raw & (1 << 26):
lat_raw -= 1 << 27
result['lat'] = lat_raw / 600000.0
result['course'] = bits_to_int(bits[116:128]) / 10.0
result['heading'] = bits_to_int(bits[128:137])
result['second'] = bits_to_int(bits[137:143])
result['maneuver'] = bits_to_int(bits[143:145])
result['spare_1'] = bits_to_int(bits[145:148])
result['raim'] = bits_to_int(bits[148:149])
result['radio'] = bits_to_int(bits[149:168])
return result
AIVDM_sample = "!AIVDM,1,1,,A,13IwU1h;iF8m@0hL0=Ie=bWp0000,0*5A"
bits = to_ASCII_6bits(AIVDM_sample.split(',')[5])
decoded = decode_type1(bits)
print(decoded)
```
Pour cette partie, on va émettre quelques hypothèses réalistes nous permettant de réduire la surface de recherche. Ainsi, on considèrera qu'une collision a lieu lorsque deux navires sont à moins de 75 mètres l'un de l'autre, et ce avec moins de 30 secondes de différence (leur élan et capacité giratoire les empêcheraient d'engager une manoeuvre d'évitement).
from pyais import decode as decode_aivdm
from datetime import datetime
from tqdm import tqdm
from geographiclib.geodesic import Geodesic
COLLISION_DETECT_m = 75
COLLISION_WINDOWS_s = 30
TIMESTAMP_FORMAT = "%Y-%m-%d-%H:%M:%S:%f"
trajectories = {}
with open("AIVDM.logs.filtered", 'r') as fIN:
for record in fIN:
timestamp, AIVDM = record.strip().split(' ')
decoded = decode_aivdm(AIVDM).asdict()
if decoded['mmsi'] not in trajectories.keys():
trajectories[decoded['mmsi']] = []
trajectories[decoded['mmsi']].append((timestamp, decoded['lat'], decoded['lon']))
lookup = list(trajectories.keys())
for i in range(len(lookup)):
trajectories[lookup[i]].sort()
candidates = []
for vessel1 in tqdm(range(len(lookup))):
for vessel2 in tqdm(range(vessel1+1, len(lookup)), leave=False):
for timestamp1, lat1, lon1 in trajectories[lookup[vessel1]]:
for timestamp2, lat2, lon2 in trajectories[lookup[vessel2]]:
# Vérifie d'abord la fenêtre de temps pour considérer une collision
if abs((datetime.strptime(timestamp1, TIMESTAMP_FORMAT) - datetime.strptime(timestamp2, TIMESTAMP_FORMAT)).total_seconds()) > COLLISION_WINDOWS_s:
if (timestamp1 < timestamp2):
# On peut s'arrêter ici, la différence de temps ne fera qu'augmenter
break
continue
# Vérifie maintenant la distance pour considérer une collision
if Geodesic.WGS84.Inverse(lat1, lon1, lat2, lon2)['s12'] < COLLISION_DETECT_m:
candidates.append(f"• {lookup[vessel1]} - {lookup[vessel2]} ({timestamp1} ~ {timestamp2})")
print("Found possible collisions:")
for c in candidates:
print(c)
Found possible collisions:
• 228177913 - 228147948 (2028-04-30-16:54:18:535000 ~ 2028-04-30-16:54:26:588000)
• 228445952 - 228850709 (2028-04-30-15:06:28:790000 ~ 2028-04-30-15:06:19:790000)
• 228094429 - 228203838 (2028-04-30-15:32:16:386000 ~ 2028-04-30-15:32:02:207000)
On a donc 3 collisions, dont une seule qui correspond à l'énoncé. On se rendra compte en analysant les messages AIVDM de Type 5 (informations générales sur le navire) que seule une paire ne concerne que des gros navires (de Classe A) :
from pyais import decode as decode_aivdm
lower = '2028-04-30-11:00:00:000000'
upper = '2028-04-30-17:00:00:000000'
AIVDM_type5_sample = "!AIVDM,2,1,0,A,53J;cQ02BV;gEa<MSUU84p@tlU`DB10u@Dq@T4hr48123000050000000000,0*39"
vessels = {}
with open("AIVDM.logs", 'r') as fIN:
for record in fIN:
timestamp, AIVDM = record.split(' ')
AIVDM = AIVDM.strip()
if lower < timestamp and timestamp < upper:
# Les messages AIVDM sont à taille fixe selon leur type
if AIVDM != "!AIVDM,2,2,0,A,00000000000,2*24" and len(AIVDM) == len(AIVDM_type5_sample):
payload_split = AIVDM.split(',')
if payload_split[1] == '2':
# Ignore les seconds fragments des messages AIVDM Type 5
# car ils ne contiennent pas d'information utile
if payload_split[2] != '1':
continue
# Modifie le nombre de fragments attendus pour décoder
# avec pyais sans levée d'exception
payload_split[1] = '1'
AIVDM = ','.join(payload_split)
decoded = decode_aivdm(AIVDM).asdict()
vessels[decoded['mmsi']] = decoded
Class_A_Types = [32, 60, 70, 80]
for mmsi1, mmsi2 in zip([228177913, 228445952, 228094429], [228147948, 228850709, 228203838]):
print(f"{'Class A' if vessels[mmsi1]['ship_type'] in Class_A_Types else 'Class B'} - ", end='')
print(f"{'Class A' if vessels[mmsi2]['ship_type'] in Class_A_Types else 'Class B'}")
print()
Class A - Class B
Class A - Class A
Class B - Class B
On conclut donc que les MMSI recherchés étaient 228445952 et 228850709.
Jusque-là on a raisonné de manière rationnelle et sans trop de prise de risque, mais l'analyse d'autres éléments auraient permis une résolution plus rapide (même si moins certaine), comme :
- Variation anormale de vitesse entre 2 positions (arrêt du brouilleur sur le navire usurpé)
- Changement de status de navigation (dans la simulation, le navire intercepteur est le seul à passer de “ancré” à “en mouvement en utilisant ses moteurs”)
Ces alternatives permettent une résolution plus rapide avec un peu d'instinct mais nécessitent une compréhension plus poussée du contenu des phrases AIVDM.
Un incident entre petites embarcations n'affolerait pas les autorités portuaires, or dans notre contexte une alerte critique avait été émise, on en déduit donc que la collision détectée se produit entre deux gros navires, donc de Classe A. Pour optimiser les calculs de collision, on aurait pu mettre en place un système de filtrage qui écarte les navires de Classe B, mais cela aurait impliqué de ne pas écarter les messages AIVDM de type 5 qui transmettent ces informations.