Décoder les fichiers de tracé Navionics (.ntf / .nts)
Comment transformer un fichier « illisible » d'application de navigation en un tracé GPS exploitable — récit complet d'une rétro-ingénierie, expliqué pas à pas.
OS
Android / iOS
application
Navionics
Format
.ntf / .nts (Protocol Buffers)
Éditeur
Navionics
Sortie
GPX
À qui s’adresse cet article. À toute personne amenée à exploiter les
données de l’application Navionics, même sans expérience de la
rétro-ingénierie. Chaque notion technique est expliquée en langage simple
avant d’être utilisée. Si vous suivez le tutoriel de bout en bout, vous
obtiendrez le même résultat : le tracé GPS complet, sous une forme
standard et lisible.
Avertissement. Le format de ces fichiers n’est pas documenté
publiquement. Tout ce qui suit a été reconstitué par observation et
analyse — ce n’est pas une spécification officielle de Navionics. Les noms
internes cités proviennent de l’application elle-même. Les emplacements de
fichiers peuvent varier selon les versions.
Sommaire
Le problème de départ
Quelques notions de base (à lire avant tout)
Les fichiers de Navionics et où les trouver
Étape 1 — Identifier le format
Étape 2 — Comprendre comment protobuf range les données
Étape 3 — Décoder le .nts (le résumé)
Étape 4 — Décoder le .ntf (le tracé)
Le découpage en détail — sur les vrais octets
Étape 5 — Donner un sens aux données
Étape 6 — Retrouver le « plan » officiel du format
Étape 7 — Écrire le décodeur final
La leçon : ce que chaque méthode apporte
Tutoriel reproductible — toutes les commandes
Annexes : outils, scripts, schéma, glossaire
1. Le problème de départ
On récupère, dans les données d’un téléphone, deux fichiers produits par
l’application de navigation marine Navionics : un fichier .ntf et un
fichier .nts, portant le même nom (un identifiant unique).
Ouverts dans un éditeur de texte, ils n’affichent que des caractères
incompréhensibles. Impossible de les lire directement. L’objectif : en
extraire le tracé GPS — la liste des positions du bateau, avec l’heure de
chaque point — sous une forme exploitable (par exemple un fichier GPX, le
format standard que lisent tous les logiciels de cartographie).
Cet article raconte toute la démarche, de ce fichier opaque jusqu’au tracé
lisible.
2. Quelques notions de base
Avant de commencer, quelques mots de vocabulaire. Ils reviendront partout.
Un fichier « binaire ». Un fichier texte contient des lettres et des
chiffres lisibles. Un fichier binaire contient des données brutes destinées
à la machine, pas à l’œil humain. Ce n’est pas pour autant « chiffré » (codé
secrètement) : c’est juste écrit dans un format compact que seul le bon
programme sait relire.
Octet, et notation hexadécimale. Un fichier est une suite d’octets
(des nombres de 0 à 255). Pour les afficher, on utilise souvent
l’hexadécimal (base 16) : c’est juste une façon d’écrire ces nombres de
manière compacte. 0x1a est l’écriture hexadécimale du nombre 26.
L’hexadécimal n’est qu’une manière d’afficher — ce ne sont pas des données
différentes.
Protocol Buffers (« protobuf »). C’est une méthode, créée par Google, pour
ranger des données dans un fichier binaire de façon compacte. Beaucoup
d’applications l’utilisent. Un fichier protobuf n’est pas chiffré : il est
juste organisé selon des règles précises. Apprendre ces règles, c’est
apprendre à le lire.
Le « schéma » (.proto). Protobuf sépare deux choses : les données
(le fichier .ntf) et le plan qui décrit ces données (un fichier appelé
.proto). Le plan dit : « le premier renseignement est la latitude, le
deuxième la longitude… ». Sans le plan, on a les données mais pas leurs
étiquettes. Une grande partie du travail consistera, justement, à
reconstituer ce plan.
Rétro-ingénierie. C’est l’art de comprendre comment un système fonctionne
sans en avoir la documentation, en l’observant de l’extérieur. C’est
exactement ce qu’on fait ici.
Une remarque rassurante. La démarche ressemble à une enquête : on part
d’indices, on forme des hypothèses, on les teste, on se trompe parfois, on
corrige. Les impasses font partie du processus — on en rencontrera, et on
expliquera pourquoi elles sont utiles.
3. Les fichiers de Navionics et où les trouver
Navionics manipule trois types de fichiers liés aux déplacements :
Extension
Contenu
Nature réelle
.ndc
une route : un itinéraire planifié à l’avance
un fichier GPX (texte) simplement renommé
.ntf
un tracé : les positions réellement parcourues
un fichier binaire protobuf
.nts
le résumé du tracé
un fichier binaire protobuf
Distinction importante : route ≠ tracé. Une route (.ndc) est un trajet
prévu, dessiné à la main avant de partir — elle ne prouve aucun déplacement
réel. Un tracé (.ntf) est l’enregistrement de positions réellement
relevées par le GPS pendant la navigation. Pour une analyse, les deux n’ont
pas du tout la même valeur.
Le .ndc est facile : c’est du texte, lisible dans n’importe quel éditeur.
Les .ntf et .nts sont le vrai sujet de cet article.
Où les trouver. Ces fichiers sont stockés dans l’espace de données réservé
à l’application Navionics sur le téléphone. À titre indicatif :
sur Android : dans un dossier du type Android/data/ au nom de
l’application ;
sur iPhone : dans le « conteneur » de l’application.
Chaque tracé est identifié par un nom unique (un long identifiant), et ce
même nom est partagé par le trio .ndc / .ntf / .nts du même
enregistrement. Les fichiers eux-mêmes ne contiennent pas de « titre » : c’est
le nom de fichier qui sert d’identité.
4. Étape 1 — Identifier le format
But de l’étape : comprendre à quoi on a affaire.
Premier réflexe devant un fichier inconnu : demander à l’ordinateur de
l’identifier. Sous Linux ou Mac, la commande file examine le début du
fichier et annonce son type :
code
file mon_fichier.ntf
Pour notre .ntf, la réponse est décevante : data. Traduction : « je ne
reconnais pas ». Beaucoup de formats laissent une signature au tout début du
fichier (quelques octets caractéristiques) ; data signifie qu’il n’y en a
pas. Mais attention : « non reconnu » ne veut pas dire « chiffré ».
Pour aller plus loin, on regarde le contenu brut du fichier, octet par octet.
C’est ce qu’on appelle un hexdump : un affichage qui montre, côte à côte,
les octets en hexadécimal et, quand c’est possible, les caractères lisibles
correspondants.
code
od -A x -t x1z mon_fichier.ntf | head
Dans ce hexdump, deux choses sautent aux yeux :
des morceaux de texte lisible noyés dans le binaire — par exemple le nom
d’un fuseau horaire comme Africa/Dar_es_Salaam ;
juste avant ces textes, des petits octets récurrents (08, 12, 1a…).
Ces deux indices, ensemble, sont la signature de protobuf. Le texte lisible
montre qu’il n’y a pas de chiffrement (un fichier chiffré n’aurait aucun
mot lisible). Et les petits octets récurrents sont la marque de fabrique du
rangement protobuf.
Conclusion de l’étape 1 : le fichier n’est pas chiffré. Il est sérialisé
au format Protocol Buffers. Il « suffit » donc d’apprendre à lire ce format.
5. Étape 2 — Comprendre comment protobuf range les données
But de l’étape : apprendre les règles de rangement de protobuf. C’est la
seule partie un peu technique, mais une fois comprise, tout le reste découle.
L’idée générale
Un fichier protobuf est une liste de renseignements mis bout à bout.
Chaque renseignement (protobuf dit un « champ ») est rangé en deux parties :
une étiquette qui dit de quel renseignement il s’agit et quelle
forme il a ;
la valeur elle-même.
Il n’y a pas de sommaire, pas d’en-tête : juste cette succession
étiquette-valeur, répétée.
L’étiquette
L’étiquette est un nombre qui contient, astucieusement compressées, deux
informations :
le numéro du champ (champ n°1, n°2, n°3… — c’est protobuf qui numérote,
il n’y a pas de noms à ce stade) ;
le type de forme de la valeur (appelé wire type, « type sur le fil »).
Il n’existe que quatre formes possibles :
Forme
Nom
Ce que c’est
0
varint
un nombre entier
1
64 bits
un nombre sur 8 octets (souvent un nombre à virgule)
2
longueur + contenu
du texte, ou une sous-liste de champs
5
32 bits
un nombre sur 4 octets
La forme n°2 est la plus riche : elle sert aussi bien pour du texte que pour
ranger une structure à l’intérieur d’une autre. Protobuf est donc
emboîtable : un champ peut contenir une sous-structure, qui contient
elle-même des champs, et ainsi de suite. C’est pour cela que les données
décodées ressemblent à des boîtes dans des boîtes.
Le « varint » : comment protobuf écrit les nombres
Protobuf range les nombres entiers d’une façon astucieuse appelée varint :
un petit nombre prend peu de place, un grand nombre en prend plus. Pour cet
article, retenez simplement que c’est une façon compacte d’écrire un
entier ; les outils savent le relire automatiquement, on n’a pas besoin de
le faire à la main.
Un détail qui aura son importance : le « zigzag »
Quand un nombre peut être négatif (comme une coordonnée dans l’hémisphère
sud), protobuf utilise une variante d’encodage appelée zigzag. Sans entrer
dans le calcul, l’idée est : le zigzag « replie » les nombres pour que les
petits négatifs restent compacts.
La conséquence pratique : un nombre stocké dans le fichier n’est pas
directement la valeur réelle. Il faut lui appliquer une petite
transformation (le « décodage zigzag ») pour retrouver la vraie valeur, qui
peut être négative. On verra que c’est exactement le piège des coordonnées
GPS.
Ce qu’il faut retenir de l’étape 2
Un fichier protobuf = une suite de champs. Chaque champ a un numéro et une
forme. Les champs peuvent s’emboîter. On peut lire la structure d’un
fichier protobuf sans rien connaître d’autre — mais on n’aura que des
numéros (« champ 3 »), pas encore de noms (« latitude »). Donner les noms,
ce sera l’étape 6.
6. Étape 3 — Décoder le .nts (le résumé)
But de l’étape : lire le fichier résumé, plus simple, pour se faire la main.
Il existe un outil officiel pour lire un fichier protobuf : protoc, le
« compilateur » de Protocol Buffers. Il possède une option, --decode_raw,
qui affiche la structure d’un fichier protobuf sans avoir besoin du plan.
C’est exactement ce qu’il nous faut pour commencer.
Le .nts est le plus simple des deux fichiers : il contient un seul bloc
protobuf. On peut donc l’envoyer directement à protoc :
code
protoc --decode_raw < mon_fichier.nts
Note pour les utilisateurs de Windows / PowerShell. Le symbole <
ci-dessus ne fonctionne pas dans PowerShell (il y est interdit). Il faut
écrire à la place :
On voit la structure : un champ 1, un champ 2, un champ 3 qui contient
lui-même des sous-champs, etc. Les accolades { } marquent les structures
emboîtées. Mais à ce stade, on ne sait pas encore ce que signifie « champ
3 ». On y reviendra.
On retiendra déjà que le .nts contient visiblement des coordonnées (les
grands nombres du champ 3), une date (le grand nombre 1763751347 ressemble à
un horodatage) et un nom de fuseau horaire.
Bon à savoir : le .nts est un résumé recalculable. Toutes ses
valeurs (zone géographique, durée, distance…) peuvent être recalculées à
partir du .ntf. C’est un index léger que l’application lit pour afficher
rapidement un aperçu du tracé. Cela donne une vérification gratuite : si le
résumé recalculé depuis le .ntf correspond au .nts fourni, le décodage est
juste.
7. Étape 4 — Décoder le .ntf (le tracé)
But de l’étape : lire le fichier de tracé — plus gros, et organisé
différemment.
Si on tente la même commande sur le .ntf :
code
protoc --decode_raw < mon_fichier.ntf
…on obtient une erreur : Failed to parse input (« impossible d’analyser »).
Pourquoi ? Parce que le .ntf n’est pas un seul bloc protobuf, mais une
longue suite de blocs mis bout à bout. Et chaque bloc est précédé d’un
petit nombre qui indique sa taille.
Une image. Le .ntf est comme un train de wagons accrochés les uns aux
autres, sans séparation visible. Avant chaque wagon, une étiquette indique sa
longueur. On ne peut pas lire le train d’un coup : il faut lire l’étiquette
(« le prochain wagon fait 47 octets »), détacher ce wagon, l’ouvrir, puis
passer au suivant. L’outil protoc, lui, s’attend à recevoir un seul wagon —
d’où l’erreur.
La solution : un petit programme qui fait le travail de découpage. Il lit
la taille, détache un bloc, recommence — et nous donne chaque bloc séparément.
Ce programme est fourni en annexe (ntf_record.py). Une fois le découpage
fait, chaque bloc isolé est, lui, un fichier protobuf normal que protoc sait
lire :
code
# combien de blocs dans le fichier ?python3 ntf_record.py mon_fichier.ntf --count# afficher la structure du bloc n°1python3 ntf_record.py mon_fichier.ntf 1 | protoc --decode_raw
En examinant ces blocs, on découvre que le .ntf contient des dizaines de
milliers de blocs : ce sont les points du tracé, un bloc par point.
Conclusion de l’étape 4 : le .ntf est un flux de blocs ; chaque bloc est
un point du tracé ; il faut d’abord découper, ensuite décoder.
8. Le découpage en détail — sur les vrais octets
Le découpage est la mécanique de base : sans elle, rien n’est possible.
Elle paraît mystérieuse, mais elle est en réalité très simple — c’est de la
lecture, pas du calcul. Détaillons-la sur les vrais octets d’un fichier.
L’image du colis
Imaginez des colis sur un tapis roulant, collés les uns aux autres, sans
séparateur. Sur chaque colis, une étiquette à l’avant indique sa taille :
« ce colis fait 49 cm ». Vous lisez l’étiquette, vous mesurez 49 cm, vous
coupez : le premier colis est isolé. Vous êtes alors pile devant l’étiquette
du suivant. Vous recommencez. Vous n’avez jamais à deviner où couper :
l’étiquette vous le dit à chaque fois.
Bloc 1. Le curseur est à la position 0. L’octet qui s’y trouve est 0x31.
En décimal, 0x31 vaut 49. C’est la longueur : le bloc 1 fait 49 octets.
Le curseur avance d’un cran (position 1), on prélève 49 octets — c’est le
bloc 1 — puis le curseur avance de 49 : il est à la position 50.
Bloc 2. Le curseur est à la position 50. L’octet est 0x35, soit 53.
Curseur +1 (position 51), on prélève 53 octets, curseur +53 : position 104.
Bloc 3. Le curseur est à la position 104. L’octet est 0x26, soit 38.
Curseur +1, on prélève 38 octets, curseur +38 : position 143.
Le tableau résume la mécanique :
Bloc 1
Bloc 2
Bloc 3
curseur au départ
0
50
104
octet de longueur lu
0x31 = 49
0x35 = 53
0x26 = 38
curseur après le bloc
50
104
143
Le « curseur après le bloc » d’une ligne devient le « curseur au départ » de
la suivante : la chaîne s’enchaîne d’elle-même, et continue à l’identique
jusqu’à la fin du fichier.
Le mot juste : la longueur n’est pas calculée, elle est lue. Elle est déjà
écrite dans le fichier, juste devant chaque bloc — l’application l’y a mise
exprès pour qu’on puisse découper. Le terme technique pour ce genre de format
est « préfixé par la longueur » (length-prefixed).
Quand la longueur tient sur deux octets
Dans l’exemple ci-dessus, les longueurs (49, 53, 38) tenaient chacune sur
un seul octet, parce qu’elles sont petites. Mais une longueur peut
dépasser ce qu’un octet peut contenir (un octet ne va que jusqu’à 255). La
longueur s’écrit alors sur plusieurs octets, selon la règle du varint.
Cette règle repose sur un seul bit de chaque octet — le bit le plus à
gauche, qui sert de feu de signalisation :
bit de gauche = 1 → « ça continue, l’octet suivant fait encore partie du
nombre » ;
bit de gauche = 0 → « c’est fini, dernier octet du nombre ».
Le truc pratique pour le repérer sans convertir en binaire : il suffit de
regarder la valeur de l’octet.
octet inférieur à 0x80 (soit 00 à 7f) → bit de gauche à 0 →
dernier octet ;
octet égal ou supérieur à 0x80 (soit 80 à ff) → bit de gauche à 1
→ ça continue.
Règle de poche : un octet ≥ 0x80, le nombre continue ; un octet < 0x80, le
nombre s’arrête là.
Exemples concrets :
Longueur
Nombre d’octets
En hexadécimal
En binaire
49
1
31
00110001
127
1
7f
01111111
128
2
80 01
10000000 00000001
300
2
ac 02
10101100 00000010
Pour la longueur 300 → ac 02 :
code
1er octet : ac = 1010 1100 bit de gauche = 1 → "ça continue"2e octet : 02 = 0000 0010 bit de gauche = 0 → "c'est fini"
Le premier octet 0xac (= 172) est ≥ 0x80 : signal que le nombre n’est pas
terminé. On lit donc le deuxième octet, 0x02, qui est < 0x80 : fin du
nombre. La longueur occupait 2 octets.
C’est exactement ce que fait la fonction read_varint du script (annexe
A) : elle lit les octets un par un et s’arrête au premier octet inférieur à
0x80. C’est aussi pourquoi elle renvoie la nouvelle position du curseur :
elle seule sait si la longueur a occupé un, deux ou trois octets, et fait donc
avancer le curseur du bon nombre de crans.
Le code, relu avec tout ça en tête
Le cœur de ntf_record.py se lit maintenant comme du français :
code
def records(buf): # buf = tout le contenu du fichier pos = 0 # le curseur, au tout début while pos < len(buf): # tant qu'on n'est pas au bout : ln, pos = read_varint(buf, pos) # 1+2 : lire la longueur, avancer le curseur yield buf[pos:pos+ln] # 3 : prélever 'ln' octets = un bloc pos += ln # 4 : avancer le curseur après le bloc # 5 : la boucle 'while' recommence
Quatre lignes. ln est la longueur lue ; buf[pos:pos+ln] désigne les octets
du curseur jusqu’à curseur + longueur (le prélèvement du colis) ; pos += ln
fait avancer le curseur. La boucle reprend toute seule jusqu’à la fin du
fichier.
9. Étape 5 — Donner un sens aux données
But de l’étape : passer des numéros (« champ 3 ») au sens (« latitude »).
À ce stade, on sait lire la structure mais pas interpréter. protoc nous
donne par exemple, pour un point :
Des numéros et des nombres. Pour transformer ça en information utile, quatre
sous-étapes, puis une vérification.
9.1 Deviner le rôle de chaque champ
C’est un travail de détective. On compare beaucoup de points entre eux et on
raisonne sur les valeurs :
un champ qui ne vaut jamais que 0 ou 1 → c’est probablement un
interrupteur (oui/non) ;
un champ qui vaut environ 1,7 milliard → c’est un horodatage (le nombre
de secondes écoulées depuis 1970, une convention informatique universelle) ;
un champ qui augmente régulièrement de point en point → un compteur ;
un champ entre 0 et 36000 → probablement un cap (une direction en
degrés, multipliée par 100).
On recoupe aussi avec ce qu’on connaît : le .nts (qui contient les mêmes
types de valeurs), et la réalité (on convertit des coordonnées candidates et
on vérifie qu’elles tombent bien en mer, dans une zone cohérente).
9.2 Appliquer les bonnes transformations
Les nombres bruts doivent souvent être retravaillés :
les nombres qui peuvent être négatifs sont en zigzag : il faut appliquer
le décodage zigzag pour retrouver la vraie valeur (voir étape 2) ;
les coordonnées ne sont pas en degrés directement : elles sont
multipliées par un million. Le nombre 8 973 097 stocké dans le fichier
signifie en réalité 8,973097 degrés. Il faut donc diviser par 1 000 000 ;
les horodatages (secondes depuis 1970) se convertissent en date et heure
normales.
9.3 Recoller les points entre eux — le point délicat
C’est ici qu’intervient la nuance la plus importante de tout l’article.
Quand on regarde les points du .ntf, on s’aperçoit que le premier point
contient des valeurs complètes (une vraie date, une vraie position), mais
que les points suivants ne contiennent que de tout petits nombres.
Cela veut dire que les points suivants ne stockent pas leur position complète,
mais seulement un écart. C’est une technique de compression classique :
plutôt que de réécrire à chaque fois « 8,973097° de latitude », on écrit juste
« +0,000023° par rapport à la référence », ce qui prend bien moins de place.
Le premier point sert donc de point de référence (on l’appellera
l’ancre). Mais il y a alors deux façons possibles de comprendre les
écarts des points suivants — et c’est là qu’on peut se tromper :
Façon A — « par rapport au point précédent ».
Chaque écart se compte depuis le point d’avant.
« Le bateau a avancé de 3 mètres depuis la note précédente. »
Pour connaître la position réelle d’un point, il faut additionner tous les
écarts depuis le début, dans l’ordre.
Façon B — « par rapport à l’ancre ».
Chaque écart se compte depuis le point de référence fixe (le premier point).
« Le bateau est à 41 mètres du point de départ. »
Chaque point se lit tout seul : il suffit d’ajouter son écart à l’ancre.
Ces deux façons sont visuellement identiques dans le fichier — les mêmes
petits nombres. Rien dans le format ne dit laquelle est la bonne. Il faut
le découvrir en essayant.
Et c’est exactement l’erreur qui a été commise en cours de route : on a
d’abord supposé la façon A (additionner depuis le point précédent).
Résultat : des positions absurdes — une latitude de 159 000 degrés, une date
en l’an 2243. En testant l’autre hypothèse, la façon B (chaque écart
compté depuis l’ancre), tout est rentré dans l’ordre instantanément.
La bonne réponse pour Navionics est donc la façon B : chaque point est un
écart direct par rapport à l’ancre, et non par rapport au point précédent.
Autrement dit, une fois l’ancre connue, chaque point se reconstitue
indépendamment des autres : on prend l’ancre, on ajoute l’écart du point, on a
sa position absolue.
C’est précisément cette règle-là — « écart direct, pas cumulé » — qu’aucun
plan, aucun schéma ne pouvait nous donner : elle ne se trouve qu’en testant
contre un vrai fichier et en regardant si le résultat a un sens.
9.4 Mettre en forme
Une fois chaque point transformé en (date, latitude, longitude, vitesse), on
dispose d’une liste de positions. On l’enregistre dans un format standard,
le GPX — un format de tracé que lisent tous les logiciels de cartographie.
On obtient enfin un fichier ouvrable dans Google Earth, un SIG, ou un logiciel
d’analyse.
9.5 Vérifier
Dernière sous-étape, essentielle : on recalcule, à partir du tracé
reconstitué, des grandeurs globales (la zone géographique couverte, la durée
totale) et on les compare au contenu du .nts (le résumé). Si les deux
concordent, c’est que l’interprétation est juste. C’est ce qu’on appelle une
validation croisée : deux sources indépendantes qui disent la même chose.
10. Étape 6 — Retrouver le « plan » officiel du format
But de l’étape : au lieu de deviner le rôle des champs, retrouver le
plan exact que les développeurs de Navionics ont utilisé.
Jusqu’ici, on a deviné. C’est efficace, mais on aimerait une certitude, et
les vrais noms des champs. Le plan (.proto) n’est pas livré avec
l’application — mais il est peut-être récupérable. Voici la chasse, étape
par étape. C’est la partie la plus « enquête » de l’article.
10.1 Première piste : chercher des fichiers .proto dans l’application
On peut récupérer l’application elle-même (sous Android, c’est un fichier
.apk ; on peut l’obtenir légalement, par exemple sur un site de
téléchargement d’APK, ou depuis un téléphone) et regarder à l’intérieur — un
.apk est en réalité une simple archive compressée.
En cherchant, on trouve effectivement des fichiers .proto. Déception : en
les ouvrant, on constate qu’ils portent tous la mention package google.protobuf. Ce sont les fichiers standards de protobuf lui-même,
livrés avec n’importe quelle application qui utilise cette technologie. C’est
l’équivalent de trouver des pièces standard dans une voiture : ça confirme la
marque du moteur, mais ça n’apprend rien sur ce véhicule en particulier.
Le critère simple pour trier : un .proto marqué package google.protobuf
= standard, sans intérêt. Un .proto avec un autre nom = potentiellement le
plan recherché. Ici, on n’en trouve aucun. Première piste : impasse.
10.2 Deuxième piste : regarder dans le code de l’application
L’application contient son code, dans des fichiers appelés .dex. On peut les
inspecter. On y trouve bien des éléments liés aux tracés — mais aucun ne
contient le plan du format. En revanche, on remarque un détail décisif : le
code mentionne une partie « native ».
Explication. Une application est souvent composée de deux morceaux : une
partie écrite dans le langage habituel du téléphone, et une partie « native »,
écrite dans un langage plus proche de la machine (C++), réunie dans des
fichiers .so. Ici, le code montre que toute la gestion des tracés se passe
dans la partie native. Le plan du format n’est donc pas dans le code
inspecté : il est dans un fichier .so.
Cette piste-là n’a pas donné le plan, mais elle a fait quelque chose
d’important : elle a indiqué où chercher.
10.3 Troisième piste : la bibliothèque native
On récupère donc le bon fichier .so — la bibliothèque native principale de
Navionics (un fichier d’une trentaine de mégaoctets). C’est un programme
compilé : du code transformé en instructions machine, pas du tout lisible
tel quel.
Mais un programme compilé conserve presque toujours une liste de noms
internes : les noms des « structures » et des « fonctions » que les
développeurs ont utilisés. Ces noms sont nécessaires au bon fonctionnement du
programme, et ils survivent dans le fichier. On peut les extraire.
Et là, on touche le but : parmi ces noms, on trouve toute une famille de
structures clairement liées au format du tracé — NavPoint, NavLatLon,
NavLocation, NavTime, NavVelocity, NavMeta, et d’autres. Ce sont les
structures du format Navionics.NavPoint (« point de navigation »), avec son
suffixe au singulier, est manifestement la structure d’un point du tracé.
10.4 Lire le plan dans le programme
Reste à connaître, pour chaque structure, la liste de ses champs. Le programme
contient une fonction qui écrit chaque structure dans le fichier. En
examinant cette fonction — on « désassemble » le programme, c’est-à-dire qu’on
traduit le code machine en instructions lisibles — on voit défiler, dans
l’ordre, l’écriture de chaque champ : son numéro et sa forme.
De plus, le programme contient aussi les noms des champs, rangés à part.
En croisant les deux — la liste des numéros/formes d’un côté, la liste des noms
de l’autre — on reconstitue le plan complet.
Et ce plan est remarquablement clair, car les développeurs de Navionics ont
donné à leurs champs des noms explicites, avec les unités :
LatitudeDegX1000000 → « latitude en degrés, multipliée par 1 000 000 » ;
SpeedMsX100 → « vitesse en mètres par seconde, multipliée par 100 » ;
UTCUnixTimeS → « date, en secondes depuis 1970 » ;
Millisecond → la fraction de seconde ;
et ainsi de suite.
Le nom du champ nous donne gratuitement l’unité et le facteur de conversion.
10.5 Le plan reconstitué
On obtient ainsi le plan complet du format — 22 structures. Les deux plus
importantes :
Un point de tracé (NavPoint) contient :
un interrupteur « point complet ou simple écart », un sous-bloc temps, un
sous-bloc position, et des sous-blocs optionnels profondeur, vitesse,
température de l’eau, événement, poisson.
Une position (NavLocation → NavLatLon) contient :
la latitude et la longitude (en millionièmes de degré), l’altitude, et la
précision de la mesure.
Le plan complet est fourni en annexe C.
10.6 Ce que le plan a confirmé
Le plan officiel a validé toutes les hypothèses faites à l’étape 5 en
devinant :
On avait deviné…
Le plan confirme…
un interrupteur « complet / écart »
le champ IsFull (oui/non)
un champ « position »
le champ Location
coordonnées multipliées par un million
le nom dit …DegX1000000
les coordonnées peuvent être négatives (zigzag)
le type confirme le zigzag
On est passé de « interprétation très probable » à « structure confirmée par
le code de l’application ». Pour un dossier sérieux, c’est la différence entre
une supposition et une preuve.
10.7 Ce que le plan ne dit pas
Point crucial, déjà annoncé : le plan décrit la forme des données (« le
champ 3 est une position »). Il ne dit pas la règle d’interprétation des
écarts — la fameuse « façon A ou façon B » de l’étape 5.3. Cette règle n’est
ni dans le plan, ni dans la fonction d’écriture : elle se vérifie en testant.
Le plan et le test sont donc complémentaires : il faut les deux.
11. Étape 7 — Écrire le décodeur final
But de l’étape : réunir tout ce qui précède dans un programme qui décode le
.ntf automatiquement.
On dispose maintenant de tout :
les règles de protobuf (étape 2) ;
le découpage du flux en blocs (étape 4) ;
le plan du format, avec noms et unités (étape 6) ;
la règle d’interprétation : chaque point est un écart direct par rapport
à l’ancre (étape 5.3).
Le décodeur final (fourni en annexe, navionics_ntf_parser.py) enchaîne :
découper le flux → décoder chaque bloc selon le plan → appliquer les
transformations (zigzag, division par un million, conversion des dates) →
recoller chaque point à l’ancre → écrire le tout en GPX.
Le résultat, sur le fichier analysé :
37 456 points GPS, chacun horodaté ;
une période de navigation de plus de 61 heures ;
un fichier GPX prêt à être ouvert dans n’importe quel logiciel de
cartographie.
La vérification finale. Ce résultat est identique, au chiffre près, à
celui obtenu tout au début par simple devinette (étape 5) : même nombre de
points, mêmes dates, même zone géographique. Deux chemins indépendants — la
devinette du début et le plan extrait du programme — aboutissent exactement au
même tracé. C’est la preuve que le décodage est correct.
12. La leçon : ce que chaque méthode apporte
Cet article a suivi un parcours en zigzag, avec des impasses. Ce n’est pas un
défaut du récit : c’est ainsi que fonctionne réellement une
rétro-ingénierie. Chaque méthode a apporté une pièce :
Deviner (étape 5) a donné la structure générale et la règle des écarts
— mais avec des noms de champs inventés.
Chercher des .proto tout faits (étape 6.1) n’a rien donné — mais a
écarté une hypothèse.
Inspecter le code de l’application (étape 6.2) n’a pas donné le plan non
plus — mais a révélé où il se trouvait (la partie native).
Analyser la bibliothèque native (étapes 6.3 à 6.5) a donné le plan
complet, avec les vrais noms et les unités — mais pas la règle des écarts.
Aucune méthode seule ne suffisait. La devinette de départ et le plan officiel
de la fin sont complémentaires : l’un a donné la logique, l’autre les
noms ; et c’est leur réunion, vérifiée contre le fichier réel, qui produit
un décodeur correct.
Pour quiconque débute : retenez surtout cette idée. On avance par hypothèses,
on accepte les impasses (elles vous disent où chercher ensuite), et on
vérifie toujours le résultat contre quelque chose d’indépendant.
13. Tutoriel reproductible — toutes les commandes
Cette section regroupe toutes les commandes pour refaire la manipulation de
bout en bout et obtenir le tracé GPX. Les scripts mentionnés sont en annexe.
13.1 Outils à installer
Outil
À quoi il sert
Installation
Python 3
exécuter les scripts de décodage
inclus sur Mac/Linux ; sur Windows : winget install Python.Python.3.12
Les deux derniers ne servent que si vous voulez refaire vous-même
l’extraction du plan (étape 6). Pour seulement décoder un .ntf, Python et
les scripts fournis suffisent.
Installer protoc :
Windows : winget install protobuf puis ouvrir un nouveau terminal.
Mac : brew install protobuf.
Linux : sudo apt install protobuf-compiler.
Vérifier que ça marche : protoc --version doit afficher un numéro.
13.2 Décoder le résumé .nts
code
# Linux / Mac :protoc --decode_raw < fichier.nts
code
# Windows (PowerShell) :cmd /c "protoc --decode_raw < fichier.nts"
Pourquoi le cmd /c sous Windows ? PowerShell interdit le symbole <.
La commande cmd /c "..." confie l’opération à l’ancien terminal Windows,
qui, lui, l’accepte. À défaut, on peut faire toute la manipulation sous
Linux, ou dans le sous-système Linux de Windows (WSL).
13.3 Explorer le tracé .ntf
Le .ntf ne se décode pas directement (c’est un flux de blocs). On utilise le
script ntf_record.py (annexe A) pour le découper :
code
# nombre de blocs (points) dans le fichierpython3 ntf_record.py fichier.ntf --count# afficher la structure du bloc n°1python3 ntf_record.py fichier.ntf 1 | protoc --decode_raw
Sous Windows, encadrer la deuxième commande dans cmd /c "..." pour la même
raison que ci-dessus.
13.4 Décoder le tracé complet en GPX
C’est l’étape finale. Le script navionics_ntf_parser.py (annexe B) fait tout
automatiquement :
Le script affiche un résumé (nombre de points, période, zone, vitesses) et
écrit le fichier tracé.gpx. Ce GPX s’ouvre dans Google Earth, QGIS, ou tout
logiciel de cartographie.
13.5 (Optionnel) Refaire l’extraction du plan — étape 6
Réservé à ceux qui veulent reproduire la rétro-ingénierie complète.
code
# 1. ouvrir l'apk de l'application (c'est une archive)unzip base.apk -d apk_extrait# 2. la bibliothèque native est dans un "split" de l'apkunzip split_config.arm64_v8a.apk lib/arm64-v8a/libnavnative.so# 3. extraire la liste des noms internes liés au format# (les structures du tracé apparaissent sous la forme Nav...)aarch64-linux-gnu-readelf -W --dyn-syms libnavnative.so | grep "Nav"# 4. extraire les noms de champs de chaque structureaarch64-linux-gnu-readelf -W --dyn-syms libnavnative.so | grep "FieldNumber"
La lecture précise du plan (numéros + formes des champs) demande de
désassembler la bibliothèque ; cette partie, plus avancée, est décrite à
l’étape 10.4 et utilise l’outil objdump (paquet binutils-aarch64-linux-gnu)
ou un logiciel d’analyse comme Ghidra (gratuit, édité par la NSA). Le plan
déjà reconstitué étant fourni en annexe C, il n’est pas nécessaire de
refaire cette extraction pour décoder un .ntf.
13.6 Récapitulatif du parcours
code
fichier .ntf (illisible) │ │ étape 1 : file + hexdump -> c'est du protobuf, non chiffré │ étape 4 : ntf_record.py -> découpage en blocs (les points) │ étape 3 : protoc --decode_raw -> structure visible (numéros) │ étape 6 : plan extrait du .so -> noms et unités des champs │ étape 5 : règle des écarts -> chaque point = écart depuis l'ancre │ étape 7 : navionics_ntf_parser -> décodage complet ▼tracé.gpx (lisible dans tout logiciel de cartographie)
14. Annexes
Annexe A — ntf_record.py (découpe le flux .ntf en blocs)
code
#!/usr/bin/env python3# Découpe un fichier .ntf en ses blocs successifs.# Le .ntf est une suite de [taille][bloc][taille][bloc]...## Usage :# python3 ntf_record.py fichier.ntf --count -> nombre de blocs# python3 ntf_record.py fichier.ntf N -> envoie le bloc N# python3 ntf_record.py fichier.ntf N | protoc --decode_rawimport sysdef read_varint(buf, pos): result = shift = 0 while True: b = buf[pos]; pos += 1 result |= (b & 0x7f) << shift if not (b & 0x80): return result, pos shift += 7def records(buf): pos = 0 while pos < len(buf): ln, pos = read_varint(buf, pos) yield buf[pos:pos+ln] pos += lnif __name__ == "__main__": data = open(sys.argv[1], "rb").read() if sys.argv[2] == "--count": print(sum(1 for _ in records(data))) else: n = int(sys.argv[2]) for i, rec in enumerate(records(data)): if i == n: sys.stdout.buffer.write(rec) break
Annexe B — navionics_ntf_parser.py (décodeur complet .ntf → GPX)
Le décodeur complet est fourni dans le fichier séparé
navionics_ntf_parser.py. Il est piloté par le plan de l’annexe C et applique
la règle « chaque point est un écart direct par rapport à l’ancre ».
Annexe C — Le plan du format (navionics_track.proto)
Le plan complet reconstitué — les 22 structures, avec numéros, formes et noms
de champs — est fourni dans le fichier séparé navionics_track.proto.
Rappel : il s’agit d’une reconstitution par rétro-ingénierie, pas d’une
spécification officielle de Navionics. Les structures principales sont
NavPoint (un point du tracé), NavLocation et NavLatLon (la position),
NavTime (l’horodatage) et NavMeta (le résumé contenu dans le .nts).
Annexe D — Glossaire
Terme
Explication simple
Binaire
fichier de données brutes, non destiné à l’œil humain (≠ chiffré)
Hexadécimal
manière compacte d’écrire des nombres ; sert à afficher les octets
Octet
la plus petite unité d’un fichier (un nombre de 0 à 255)
Protobuf
méthode de Google pour ranger des données dans un fichier binaire
Schéma / .proto
le « plan » qui décrit ce que contient un fichier protobuf
Champ
un renseignement dans un fichier protobuf (a un numéro et une forme)
Varint
façon compacte d’écrire un nombre entier en protobuf
Zigzag
variante d’encodage pour les nombres pouvant être négatifs
Ancre
le point de référence (le premier point complet du tracé)
Sérialisé
rangé sous forme de fichier selon des règles précises
Rétro-ingénierie
comprendre un système sans sa documentation, en l’observant
.so / bibliothèque native
partie d’une application écrite en langage proche de la machine
Désassembler
traduire un programme compilé en instructions lisibles
GPX
format de fichier standard pour les tracés GPS
Annexe E — Aide-mémoire des commandes
code
# Identifier le fichierfile fichier.ntf# Voir le contenu brut (hexdump)od -A x -t x1z fichier.ntf | head# Décoder le résumé .ntsprotoc --decode_raw < fichier.nts # Linux / Maccmd /c "protoc --decode_raw < fichier.nts" # Windows PowerShell# Explorer le tracé .ntfpython3 ntf_record.py fichier.ntf --countpython3 ntf_record.py fichier.ntf 1 | protoc --decode_raw# Décoder le tracé complet en GPXpython3 navionics_ntf_parser.py fichier.ntf tracé.gpx