Analyse d'un décodeur TNT
- 08/04/2025 - dansDe nombreuses personnes possèdent un décodeur TNT, ce qui constitue une surface d'attaque importante souvent insoupçonnée. Certains modèles nécessitant une connexion internet, ils constituent un point d'accès potentiel vers le réseau local. Dans cet article, nous examinerons les protocoles utilisés par la TNT et les vulnérabilités liées à leurs implémentations.
Vous souhaitez améliorer vos compétences ? Découvrez nos sessions de formation ! En savoir plus
Introduction
En nettoyant le garage, nous avons retrouvé un décodeur TNT et avons pensé qu'il constituerait une cible idéale pour une recherche de vulnérabilités. Bien qu'il s'agisse d'un appareil couramment utilisé, nous n'avons trouvé que peu de recherches précédentes sur ce sujet.

Ce modèle est vendu par la société française Metronic et est toujours disponible sur leur site web. Il dispose d'un port Ethernet pour la connectivité Internet et est indispensable pour certaines fonctionnalités telles que les mises à jour météo, la radio web, etc.
L'objectif de cette recherche est d'obtenir une exécution de code sur l'appareil via des ondes radio, ce qui permettrait à un attaquant d'accéder au réseau local auquel l'appareil est connecté.
Cependant, lors de notre session de nettoyage, nous n'avons pas pu retrouver la télécommande de l'appareil. Celui-ci est presque inutilisable sans elle car la première étape lors d'un changement d'antenne sur un tel appareil est la synchronisation des chaînes, ce qui nécessite la télécommande. Les trois boutons sur la face avant sont l'alimentation, l'incrémentation et la décrémentation des chaînes, mais ils ne suffisent pas à accéder aux paramètres de l'appareil.
La documentation utilisateur spécifie qu'il peut ou non fonctionner avec une télécommande universelle. Nous avons donc essayé une méthode de force brute avec la plus grande base de données de télécommandes infrarouges que nous avons pu trouver sur Internet, mais sans succès.
Nous avons donc entamé notre analyse en recherchant le code implémentant la réception des données infrarouges.
Récupération du firmware
La première étape consiste à récupérer le firmware du produit. Comme le fabricant ne propose pas de mises à jour, il nous faudra l'extraire directement depuis l'appareil.

Nous avons identifié deux ports sur le PCB étiquetés CN2 et J11, qui semblent être des ports de débogage. Cependant, ces derniers semblent inactifs, car aucune activité n’a été observée en y connectant un analyseur logique.
Le SoC sous le dissipateur thermique est un ALI M3626. Très peu d’informations sont disponibles en ligne concernant cette puce, et son architecture reste inconnue. Cependant, un détail notable sur cette carte est la présence d’une mémoire flash SPI, qui pourrait contenir le firmware de l’appareil.
Les données extraites de la mémoire flash comprennent quelques chaînes de caractères, mais la majorité de ces données présentent une haute entropie.
En examinant le début du dump, il semble contenir un en-tête.

En analysant les chaînes et les données associées, nous pouvons déduire qu’une structure de données est utilisée.
Nous avons pu déterminer certains champs de cette structure, ce qui nous a permis d’extraire des partitions du dump.
ali_file_hdr = Struct(
"id" / Bytes(4),
"unk" / Int32ub,
"size" / Int32ub,
"crc" / Bytes(4),
"name" / Bytes(0x10),
"version" / Bytes(0x10),
"date" / Bytes(0x10)
)
Voici le nom des différentes partitions récupérées:
- bootloader
- defaultdb
- HDCPKey
- MAC_ADDR
- maincode
- netdata
- osd_string
- Radioback
- seecode
- userdb
La partition maincode semble intéressante, mais son contenu est chiffré, compressé, ou les deux, comme l’indique sa haute entropie. Une phase de rétro-ingénierie du bootloader nous a permis de localiser l'endroit où cette partition est manipulée et avons découvert qu'elle est simplement compressée à l'aide de l'algorithme LZMA.
Cependant, sans la documentation du SoC, nous n'avons pas pu comprendre complètement le code responsable de l'initialisation du matériel. En particulier, nous ne savions pas l'adresse de base où le code principal est exécuté.
Pour trouver cette adresse, nous avons commencé par une phrase de rétro-ingénierie à la recherche d'une fonction qui formate des données, comme printf, sprintf, etc. Une fois identifiée, nous avons cherché des appels à cette fonction contenant au moins deux adresses codées en dur. Les adresses codées en dur sont généralement des chaînes de caractères, ce qui signifie que la première contient %s.
Nous avons ensuite listé toutes les chaînes présentes dans le code et les avons filtrées pour trouver des paires de chaînes avec le bon offset relatif et dont la première contient %s
.
Cette approche nous a donné une vingtaine de résultats potentiels mais un seul était aligné sur 512 octets. L'utilisation de cette adresse de base a parfaitement fonctionné, car toutes les références de chaînes avaient désormais un sens.
Erreurs d'IR
Avec le firmware à la correcte adresse de base, nous pouvons commencer la rétro-ingénierie pour trouver où les codes infrarouges sont traités.
L'OS du SoC est basé sur un système d'exploitation temps réel (RTOS). Les chaînes présentes dans le code suggèrent qu'il suit la spécification du noyau ITRON, bien qu'il ne semble pas utiliser l'implémentation open-source TKernel.
En recherchant certaines chaînes, nous avons trouvé un dépôt Github contenant du code source très similaire au firmware que nous possédons. Il semble concerner le ALI M3711, qui n'est pas exactement notre SoC, mais cela nous a été très utile pour comprendre certaines parties du code liées au matériel.
Du point de vue d'un attaquant, le firmware est particulièrement intéressant car il n'y a pas de traduction d'adresses, ce qui signifie que les adresses virtuelles sont identiques aux adresses physiques. Cela nous permet d'utiliser des gadgets de n'importe quel thread, à condition que nous ayons une vulnérabilité permettant de construire une ROPchain.
L'un des premiers threads lancés lors du démarrage gère l'initialisation du matériel. La fonction irc_m6303irc_init initialise le récepteur infrarouge du SoC et enregistre une callback pour une interruption spécifique.
void irc_m6303irc_init(struct pan_hw_info *hw_info)
{
irc_nec_mode_set(0, 280, 500000);
/* ... */
WRITE_INF_BYTE(INFRA_IRCCFG, 0);
WRITE_INF_BYTE(INFRA_IRCCFG, 0x80 | ((12*VALUE_CLK_CYC) >> 5));
WRITE_INF_BYTE(INFRA_FIFOCTRL, 0xA0); /* 32 bytes */
WRITE_INF_BYTE(INFRA_TIMETHR, (VALUE_TOUT / (VALUE_CLK_CYC << 7) - 1));
WRITE_INF_BYTE(INFRA_NOISETHR, VALUE_NOISETHR / VALUE_CLK_CYC);
/* ... */
osal_interrupt_register_lsr(27, irc_m6303irc_lsr, 0);
/* ... */
}
À chaque fois que la LED infrarouge reçoit une impulsion, la fonction irc_m6303irc_lsr traite l'interruption et appelle generate_code, en lui passant le nombre de ticks écoulés depuis le démarrage en paramètre.
La majorité des protocoles infrarouge utilisent l'encodage par distance d'impulsion pour encoder l'information. Voici un exemple avec le protocole NEC :

Le gestionnaire de l'interruption doit alors enregistrer l'heure de la dernière impulsion reçue et calculer la différence avec l'impulsion actuelle pour pouvoir décoder les bits transmis.
static UINT32 irc_last_tick = 0;
static void generate_code(UINT32 tick)
{
UINT32 code = 0;
/* ... */
code = irc_nec_pulse_to_code((tick - irc_last_tick) * 1000);
irc_last_tick = tick;
/* ... */
}
Le nom de la fonction laisse penser que le protocole NEC est utilisé, mais nous verrons qu'il n'est pas implémenté correctement. Ce protocole est relativement simple, puisqu'il utilise une adresse de périphérique et une commande, chacune encodée sur 8 bits. Voici un exemple complet de transmission NEC :

Dans l'implémentation du protocole NEC de ce produit, les 32 bits reçus sont regroupés dans un entier qui est ensuite transmis à l'utilisateur.
static UINT32 code = 0;
static UINT32 irc_nec_state = 0;
UINT32 irc_nec_pulse_to_code(UINT32 pulse_width)
{
UINT8 got_full_status = 32;
accum_width += pulse_width;
/* ... */
if ((pulse_width > IRC_NEC_PULSE_UNIT) &&
(pulse_width < IRC_NEC_PULSE_UNIT * 5))
{
code = (code << 1) + (pulse_width > IRC_NEC_PULSE_UNIT * 3);
/* check if received 32 bits */
if (got_full_status == irc_nec_state)
{
last_code = code;
irc_nec_state = 0;
return code;
}
}
else
{
irc_nec_state = 0;
return PAN_KEY_INVALID;
}
/* ... */
irc_nec_state++;
return PAN_KEY_INVALID;
}
La variable code
stocke l'adresse dans l'octet de poids fort (MSB) puis son inverse logique, puis la commande et son inverse dans l'octet de poids faible (LSB).
Plus tard dans le programme, cette valeur est convertie en une autre structure.
#define SET_IRCODE(a) ((((a) >> 24) << 8) | ((a) & 0xff))
UINT32 scan_code_to_msg_code(struct pan_key *key_struct)
{
IR_KEY_INFO msg_code;
/* ... */
msg_code.ir_code = SET_IRCODE(key_struct->code);
return *(UINT32 *) (&msg_code);
}
Les MSB et LSB de la variable code
ont extraits, le premier correspond à l'adresse mais le second ne correspond pas à la commande. Au lieu de cela, il correspond à son inverse logique, ce qui est une première erreur d'implémentation.
De plus, comme visible dans la description du protocole NEC ci-dessus, l'adresse et la commande sont envoyées en little-endian (bit de poids faible en premier). Or, la variable code
stocke les octets reçus en big-endian et l'endianness n'est jamais changée, il s'agit de la deuxième erreur d'implémentation.
La valeur de ir_code
correspondant à la touche "menu" est 0x10B7. Pour entrer dans le menu en utilisant le Flipper Zero (qui implémente correctement NEC), nous devons envoyer l'adresse 0x08 (changement d'endianness de 0x10) et la commande 0x12 (changement d'endianness et inverse logique de 0xB7).
Après avoir effectué toutes ces étapes, nous avons enfin pu utiliser l'appareil et procéder à la synchronisation des chaînes.
DVB internals
La norme DVB (Digital Video Broadcasting) est un ensemble de spécifications destinées à la transmission de télévision numérique, principalement en Europe. Dans les premières versions de ces normes, aucune mesure d'authentification n'avait été prévue. Aujourd'hui encore, en France, la plupart des chaînes n'implémentent pas cette sécurité.
Cela implique que, grâce à un signal plus puissant que l'original, il est possible de prendre le contrôle d'une chaîne et de contrôler le flux qui est décodé et affiché sur le décodeur. Cette manipulation peut être aisément réalisée à l'aide d'un HackRF.

Après démodulation, les données forment un flux de transport MPEG, qui est un flux incluant plusieurs flux élémentaires, tels que la vidéo, l'audio et les métadonnées.
Les métadonnées comprennent plusieurs tables, parmi lesquelles :
- PAT (Program Association Table) liste tous les programmes contenus dans le flux
- PMT (Program map specific data) contient des informations a propos des différents programmes
- CAT (Conditional access specific data) fournit un contrôle d'accès conditionnel, c'est ce qui est utilisé par les chaînes payantes
- EIT (Event Information Table) contient le planning des programmes
- etc.
Le décodeur analyse en permanence toutes ces tables. La complexité de ces normes induis une large surface d'attaque qui est susceptible de contenir des bugs.
Vulnerabilités trouvées
Lors de notre étude, nous avons identifié plusieurs vulnérabilités dans l'implémentation DVB du produit. Toutefois, aucune d'entre elles ne semble pouvoir être exploitée.
Le BAT signal
Lors de la phase de synchronisation des chaînes, le décodeur recherche la Bouquet Association Table (BAT) dans le flux MPEG. Cette table permet de relier plusieurs flux entre eux et est surtout utilisée par les chaînes payantes pour offrir l'accès à plusieurs chaînes avec un seul abonnement.
La fonction bat_event
est appelée chaque fois qu'une table BAT est reçue. Elle vérifie si l'ID du bouquet est connu et le stocke si ce n'est pas le cas. Ensuite, la table est parsée et la fonction bat_on_bouqname_desc
est appelée si la table contient une entrée spécifiant le nom de bouquet. Dans ces deux fonctions, l'attaquant peut contrôler le contenu de la variable data
.
static BOOL bat_event(UINT16 pid, struct si_filter_t *filter,UINT8 reason, UINT8 *data, INT32 len)
{
INT16 i = 0;
struct bat_hitmap *hitmap = NULL;
struct bat_section_info *pinfo = bat_info;
/* ... */
// check if bouquet ID is known
for(i = 0; i < pinfo->bouq_cnt; i++)
{
if(((data[3]<<8)|data[4])==pinfo->bouq_id[i])
{
break;
}
}
if(i == pinfo->bouq_cnt)
{
pinfo->bouq_id[i] = (data[3]<<8)|data[4];
pinfo->bouq_cnt++;
}
/* ... */
}
static INT32 bat_on_bouqname_desc(UINT8 tag, UINT8 len, UINT8 *data, void *priv)
{
INT32 i = 0;
struct bat_section_info *b_info = (struct bat_section_info *)priv;
for(i=0; i<b_info->bouq_cnt; i++)
{
// match the current bouquet ID
if(b_info->b_id==b_info->bouq_id[i])
{
if(len <= (2*(MAX_BOUQ_NAME_LENGTH + 1)))
{
MEMCPY(b_info->bouq_name[i], data, len);
}
break;
}
}
if(i == b_info->bouq_cnt)
{
return ERR_FAILUE;
}
return SUCCESS;
}
La valeur de bouq_cnt
n'est jamais vérifiée et bouq_id
possède une taille fixe de 16, ce qui pourrait conduire à un dépassement de tampon. Toutefois, la longueur maximale de l'opération memcpy
est de 32, et la structure est suivie d'un large tampon inutilisé. Bien que nous ayons pu déclencher ce dépassement, nous n'avons pas réussi à provoquer un dépassement suffisamment important pour en faire une vulnérabilité exploitable.
Un bug béNIT
Egalement lors de la phase de synchronisation des chaînes, le décodeur recherche la Network Information Table (NIT). Cette table sert à associer les flux vidéo et audio. Elle peut contenir plusieurs entrées de type extension, et la fonction t2_delivery_system_descriptor_parser
est exécutée pour chaque entrée. Elle vérifie si une entrée correspondant aux paramètres fournis existe déjà, et l'ajoute si ce n'est pas le cas.
INT32 t2_delivery_system_descriptor_parser(UINT8 tag, UINT8 length, UINT8 *data, void *priv)
{
UINT8 min_length = 4;
UINT16 i = 0;
UINT16 t2_system_id = 0;
struct nit_section_info *n_info = NULL;
t2_delivery_system_descriptor *t2_desc = NULL;
if((data == NULL) || (priv == NULL))
{
return !SI_SUCCESS;
}
n_info = (struct nit_section_info *)priv;
t2_desc = (t2_delivery_system_descriptor *)data;
t2_system_id = (t2_desc->t2_system_id[0]<<8) | (t2_desc->t2_system_id[1]);
for(i=0; i<n_info->t2_info_num; ++i)
{
if( n_info->t2_info[i].plp_id == t2_desc->plp_id && n_info->t2_info[i].t2_system_id == t2_system_id )
{
break; // The mapping already exists
}
}
if(i == n_info->t2_info_num)
{
n_info->t2_info_num++; //Append a mapping.
}
n_info->t2_info[i].plp_id = t2_desc->plp_id;
n_info->t2_info[i].t2_system_id = t2_system_id;
n_info->t2_info[i].onid = n_info->onid;
n_info->t2_info[i].tsid = n_info->tsid;
return SI_SUCCESS;
}
La valeur de t2_info_num
n'est jamais vérifiée et t2_info
possède une taille fixe, ce qui peut entraîner un dépassement de tampon. Toutefois, la structure affectée se trouve dans la section .data
, et aucune donnée importante n'est placée juste après.
D'autres vulnérabilités ont été découvertes lors de l'analyse de diverses autres tables, suivant un schéma similaire. Néanmoins, ces vulnérabilités ne sont pas exploitables, car les tailles sont souvent encodées sur un seul octet, ce qui empêche les vulnérabilités liées aux manipulations d'entiers. De plus, lorsque le dépassement est possible, il n'est pas suffisant pour accéder à des données sensibles.
Conclusion
En résumé, notre étude des vulnérabilités sur le récepteur DVB a mis en évidence plusieurs failles potentielles dans le code, principalement causées par l'absence de contrôles suffisants. Toutefois, ces vulnérabilités ne sont pas facilement exploitables, en raison des contraintes du protocole et de la taille relativement petite des données manipulées.
Il est important de souligner que les récepteurs DVB modernes intègrent souvent la fonctionnalité HBBTV, ce qui augmente la surface d'attaque en ajoutant un navigateur et en permettant des interactions web. Cela ouvre de nouveaux vecteurs d'exploitation potentiels. Toutefois, le produit que nous avons analysé ne supportait pas HBBTV.