Solution de ElyKar pour Smart TV Locker

hardware

26 novembre 2023

Smart TV Locker

En cherchant sur Internet les problèmes de sécurité connus liés à la TNT, on tombe rapidement sur le sujet des applications HbbTV. Il y a eu plusieurs présentations sur le sujet, et ce depuis 2013.

Etape 1 : extraction de l’application HbbTV

La première étape est de voir de quoi est constitué le flux TNT. Pour cela, l’outil tsduck est très pratique.

Rapidement, un flux MPEG-2 TS est une suite de paquets de 188 octets, qui servent à multiplexer plusieurs flux ensemble. Chaque paquet contient un PID, tous les paquets du même PID forment le même sous-flux.

Un sous-flux peut être un flux de données (e.g. audio/vidéo) ou de métadonnées (i.e. une “table”, qui contient des informations sur le contenu du flux MPEG-2). En l’occurrence, la table la plus importante est la Program Association Table (PAT), qui est identifiée par le PID 0 et contient les liens (i.e. PIDs) vers les différents programmes contenus dans un flux. Concrètement, un programme c’est une chaîne de télévision.

Pour extraire la PAT:

tsp -I file challenge.ts -P tables --pid 0 --xml-output pat.xml -O drop

L’option “-I” spécifie le type d’entrée : ici le fichier challenge.ts. L’option “-O” spécifie le type de sortie : ici aucune, on ne recopie pas le flux. L’option “-P” appelle un plugin qui spécifie le type d’opérations à appliquer: ici on utilise le plugin “tables” en lui indiquant d’extraire le sous-flux de PID 0 et de le parser dans le fichier pat.xml

<?xml version="1.0" encoding="UTF-8"?>
<tsduck>
  <PAT version="16" current="true" transport_stream_id="0x0004">
    <!-- PID 0x0000 (0) -->
    <service service_id="0x0401" program_map_PID="0x0064"/>
    <service service_id="0x0402" program_map_PID="0x00C8"/>
    <service service_id="0x0407" program_map_PID="0x012C"/>
    <service service_id="0x0415" program_map_PID="0x0190"/>
    <service service_id="0x0416" program_map_PID="0x01F4"/>
  </PAT>
</tsduck>

Le fichier pat.xml indique qu’il y a 5 programmes. L’identifiant de service (service_id) sert à identifier le programme parmi les autres. L’identifiant program_map_PID sert à identifier quel sous-flux contient la structure de description du programme : la Program Map Table (PMT).

(En réalité, pour le challenge tous les programmes sont équivalents, il n’y avait pas de mauvais choix.)

On peut extraire la PMT du service 0x401 ainsi :

tsp -I file challenge.ts -P tables --pid 0x64 --xml-output pmt.xml -O drop
<?xml version="1.0" encoding="UTF-8"?>
<tsduck>
  <PMT version="3" current="true" service_id="0x0401" PCR_PID="0x0078">
    <!-- PID 0x0064 (100) -->
    <component elementary_PID="0x0078" stream_type="0x1B">
      <stream_identifier_descriptor component_tag="0x01"/>
    </component>
    <component elementary_PID="0x0082" stream_type="0x06">
      <stream_identifier_descriptor component_tag="0x02"/>
      <ISO_639_language_descriptor>
        <language code="fra" audio_type="0x00"/>
      </ISO_639_language_descriptor>
      <DVB_enhanced_AC3_descriptor mixinfoexists="false" component_type="0xC4"/>
    </component>
    <component elementary_PID="0x0083" stream_type="0x06">
      <stream_identifier_descriptor component_tag="0x03"/>
      <ISO_639_language_descriptor>
        <language code="qad" audio_type="0x00"/>
      </ISO_639_language_descriptor>
      <supplementary_audio_descriptor mix_type="1" editorial_classification="0x01" language_code="fra"/>
      <DVB_enhanced_AC3_descriptor mixinfoexists="false" component_type="0xD2"/>
    </component>
    <component elementary_PID="0x0084" stream_type="0x06">
      <stream_identifier_descriptor component_tag="0x04"/>
      <ISO_639_language_descriptor>
        <language code="qaa" audio_type="0x00"/>
      </ISO_639_language_descriptor>
      <DVB_enhanced_AC3_descriptor mixinfoexists="false" component_type="0xC2"/>
    </component>
    <component elementary_PID="0x008C" stream_type="0x06">
      <stream_identifier_descriptor component_tag="0x05"/>
      <subtitling_descriptor>
        <subtitling language_code="fra" subtitling_type="0x24" composition_page_id="0x0001" ancillary_page_id="0x0001"/>
      </subtitling_descriptor>
    </component>
    <component elementary_PID="0x008D" stream_type="0x06">
      <stream_identifier_descriptor component_tag="0x06"/>
      <subtitling_descriptor>
        <subtitling language_code="fra" subtitling_type="0x14" composition_page_id="0x0001" ancillary_page_id="0x0001"/>
      </subtitling_descriptor>
    </component>
    <component elementary_PID="0x0100" stream_type="0x05">
      <application_signalling_descriptor>
        <application application_type="0x0010" AIT_version_number="0x01"/>
      </application_signalling_descriptor>
    </component>
    <component elementary_PID="0x0101" stream_type="0x0B">
      <stream_identifier_descriptor component_tag="0xF5"/>
      <carousel_identifier_descriptor carousel_id="0x000000F5">
        <private_data>
          00
        </private_data>
      </carousel_identifier_descriptor>
      <data_broadcast_id_descriptor data_broadcast_id="0x0123">
        <selector_bytes>
          80 10
        </selector_bytes>
      </data_broadcast_id_descriptor>
    </component>
  </PMT>
</tsduck>

La partie de la PMT qui indique qu’une application est transmise par le programme est celle-ci:

<component elementary_PID="0x0100" stream_type="0x05">
  <application_signalling_descriptor>
    <application application_type="0x0010" AIT_version_number="0x01"/>
  </application_signalling_descriptor>
</component>

Elle indique qu’il y a une table de type Application Information Table (AIT) dans le sous-flux référencé par le PID 0x100 (champ “elementary_PID”).

On peut extraire cette table ainsi:

tsp -I file challenge.ts -P tables --pid 0x100 --xml-output ait.xml -O drop

Le contenu de la table est:

<?xml version="1.0" encoding="UTF-8"?>
<tsduck>
  <AIT version="1" current="true" test_application_flag="false" application_type="0x0010">
    <!-- PID 0x0100 (256) -->
    <application control_code="0x01">
      <application_identifier organization_id="0x00000011" application_id="0x0001"/>
      <transport_protocol_descriptor transport_protocol_label="0x00">
        <object_carousel component_tag="0xF5"/>
      </transport_protocol_descriptor>
      <application_name_descriptor>
        <language code="fre" application_name="Softlocker 1"/>
      </application_name_descriptor>
      <application_usage_descriptor usage_type="0x00"/>
      <application_descriptor service_bound="true" visibility="3" application_priority="2">
        <profile application_profile="0x0000" version="1.1.1"/>
        <transport_protocol label="0x00"/>
      </application_descriptor>
      <simple_application_location_descriptor initial_path="locker.html"/>
    </application>
  </AIT>
</tsduck>

La définition de la table est donnée dans la norme ETSI TS 102.809. On repère facilement quelques métadonnées comme le nom de l’application (Softlocker 1) ou le fichier d’entrée de l’application (locker.html).

Pour savoir d’où vient ce fichier, il faut regarder le descripteur suivant :

<application_descriptor service_bound="true" visibility="3" application_priority="2">
  <profile application_profile="0x0000" version="1.1.1"/>
  <transport_protocol label="0x00"/>
</application_descriptor>

Il indique que le mode de transport de l’application est le numéro 0. Celui-ci est défini plus haut :

<transport_protocol_descriptor transport_protocol_label="0x00">
  <object_carousel component_tag="0xF5"/>
</transport_protocol_descriptor>

Deux modes de transports sont principalement utilisés : HTTP (via Internet) ou dans le flux TNT dans un flux de type “carousel” (i.e. une archive qui passe dans un PID). C’est la seconde option qui a été choisie, il faut donc retrouver le carousel identifié par 0xF5 dans le flux. Comme un carousel reste un flux diffusé par un programme, il est référencé dans la PMT. En l’occurrence, il s’agit du bloc suivant :

<component elementary_PID="0x0101" stream_type="0x0B">
  <stream_identifier_descriptor component_tag="0xF5"/>
  <carousel_identifier_descriptor carousel_id="0x000000F5">
    <private_data>
      00
    </private_data>
  </carousel_identifier_descriptor>
  <data_broadcast_id_descriptor data_broadcast_id="0x0123">
    <selector_bytes>
      80 10
    </selector_bytes>
  </data_broadcast_id_descriptor>
</component>

On voit donc que le carousel est à l’intérieur du sous-flux de PID 257 (elementary_PID = 0x101). Pour l’extraire, il existe la suite d’outils OpenCaster, qui bien que plutôt vieille (pas de mise à jour depuis plus de 7 ans) fonctionne. Après installation, la documentation donne un exemple de récupération du contenu du carousel.

ts2sec challenge.ts 257 > carousel.sec
mkdir carousel
dsmcc-receive carousel 100 257 0xF5 < carousel.sec

La première commande sert à extraire le sous-flux de PID 257 dans le fichier “carousel.sec”. La dernière commande sert à extraire le carousel, porté par le flux de PID 257 et d’identifiant 0xF5.

Une fois cela fait, on peut afficher le contenu du répertoire récupéré :

ls -l carousel/carousels/257/245/srg-1-00000000
carousel/carousels/257/245/srg-1-00000000:
total 0
lrwxrwxrwx 1 user user 54  3 avril 13:14 locker.html -> ../../../../../carousel/carousels/0/245/fil-2-00000001
lrwxrwxrwx 1 user user 54  3 avril 13:14 rc4.js -> ../../../../../carousel/carousels/0/245/fil-2-00000002
lrwxrwxrwx 1 user user 54  3 avril 13:14 victory.png -> ../../../../../carousel/carousels/0/245/fil-3-00000003

On retrouve notre application, composée des fichiers locker.html, rc4.js et victory.png.

Etape 2 : Résolution du crackme

Le flag n’est pas présent sur l’image victory.png, inutile de l’étudier. Le fichier locker.html est une application HbbTV simple, non obfusquée.

L’environnement d’exécution d’une application HbbTV contient des objets spéciaux, qui vont exposer certaines capacités spécifiques des télévisions aux applications : changer de chaîne, gestion des contenus audio/vidéo, accès au guide des programmes…

Par exemple, dans le code HTML de l’application on retrouve ces deux objets, définis par la norme :

<object id="oipfAppMan" type="application/oipfApplicationManager"></object>
<object id="oipfBroadcast" type="video/broadcast"></object>

Pour exécuter des applications HbbTV sans télévision, on peut utiliser des plugins de navigateur. Un exemple avec Hybrid TV Viewer sur Firefox :

Exécution de l&rsquo;application

Le code se rentre avec les boutons rouge, vert, bleu et jaune de la télécommande, ou avec le clavier (r, v, b, j) dans le navigateur.

Voilà la fonction qui vérifie le code (répétition enlevée) :

function verify(input) {
    var vb = document.getElementById("oipfBroadcast");
    vb.bindToCurrentChannel();
    var all_programmes = vb.programmes;
    var key = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""];
    for (var i = 0; i < all_programmes.length; i++) {
        current_prog = all_programmes[i]
        name_hash = hash(current_prog.name);
        if (name_hash == 37) {
            key[15] = current_prog.longDescription;
        }
        if (name_hash == 65) {
            key[29] = current_prog.description;
        }
        if (name_hash == 88) {
            key[30] = current_prog.longDescription;
        }
        // [...]
    }
    input = binDecode(input);
    key = hexDecode(key.join(""));
    check = enc(key, input);

    if (check == hexDecode("00112233445566778899aabbccddeeff")) {
        console.log("VALID");
        ciphertext = hexDecode("ceb21c62048e7324c1d30401f8d8dcbabe029cddf103ca9743f92113d9a89cfc96d49a436dc0e6941241bbb8a3773c7774cea837ca86020a80c04105bb6fc032a4d02d199ecce7352cf617344764d5f8d09b3180c3a2f07815685af1d8ee1d6a6d49ddcf6938487ee3b9477a946860d05c");
        plaintext = enc(input, ciphertext);
        c = document.getElementById("content");
        c.style["background-color"] = "white";
        c.innerHTML = "";
        c.innerHTML = '<div style="text-align: center;top: 10%"><img src="victory.png" style="margin: auto; top: 10%" /></div>';
        c.innerHTML += '<div style="left: 10%; width: 80%; position: relative; background-color: white">';
        c.innerHTML += '<p style="text-align: center; color: #000000; font-size: 1vw;">' + plaintext + "</p>";
        c.innerHTML += "</div>";
    }
};

La vérification est assez simple : l’application fabrique une clé depuis l’objet “video/broadcast”. La clé est utilisée pour chiffrer une valeur dérivée de l’entrée utilisateur. Si le chiffré vaut “\x00\x11..\xff”, alors l’entrée utilisateur est utilisée pour déchiffrer un message et l’afficher, sinon rien ne se passe.

Le chiffrement en lui-même est un RC4 standard, implémenté dans le fichier rc4.js. L’entrée utilisateur est une chaîne binaire : chaque bouton encode deux bits (rouge: 00, vert: 01, … Pour résoudre le challenge, il faut donc récupérer la clé dérivée de l’objet video/broadcast, déchiffrer “\x00\x11…\xff” pour avoir la clé de déchiffrement du flag, puis déchiffrer le flag avec.

Il faut retourner sur la norme HbbTV pour comprendre ce que contient l’objet en question, et notamment les propriétés qu’il contient : “programmes”, “name”, “longDescription”, “description”. La norme HbbTV détaille ces APIs dans l’annexe A, paragraphe 1 de la norme. Dans le cas présent, cette annexe indique que l’objet video/broadcast est défini dans une autre norme : la norme “Declarative Application Environment” (l’ancêtre du standard HbbTV).

Cette norme indique que la propriété “programmes” vient de la table “Event Information Table present/following” (EIT p/f) présente dans le flux : il s’agit simplement du guide des programmes, qui donne le programme actuel et le programme suivant.

Ensuite, chaque programme est composé comme suit :

  • chaque programme est un “event” de l’EIT;
  • chaque programme peut avoir un nom et une description courte (qui deviennent les propriétés “name” et “description” avec HbbTV), qui se trouve dans le “ShortEventDescriptor” d’un événement;
  • chaque programme peut avoir une description longue (qui devient la propriété “longDescription” avec HbbTV), qui se trouve dans le “ExtendedEventDescriptor” d’un événement.

Il faut donc aller parser manuellement la table EIT pour retrouver les différents événements, qui servent à générer la clé. La table EIT est définie comme composante d’un flux TNT, celle-ci a un PID statique qui est le 18.

$ tsp -I file challenge.ts -P tables -p 18 --xml-output eit.xml -O drop
$ cat eit.xml
<tsduck>
  <EIT type="pf" version="7" current="true" actual="true" service_id="0x0401" transport_stream_id="0x0004" original_network_id="0x20FA" last_table_id="0x4E">
    <!-- PID 0x0012 (18) -->
    <event event_id="0x0042" start_time="2021-04-23 00:00:00" duration="23:59:59" running_status="running" CA_mode="false">
      <short_event_descriptor language_code="fre">
        <event_name>FCSC</event_name>
        <text>France Cybersecurity challenge</text>
      </short_event_descriptor>
    </event>
    <event event_id="0xAE47" start_time="2021-04-23 10:00:30" duration="00:01:00" running_status="not-running" CA_mode="false">
      <short_event_descriptor language_code="fre">
        <event_name>vihZ</event_name>
        <text>e</text>
      </short_event_descriptor>
      <extended_event_descriptor descriptor_number="0" last_descriptor_number="0" language_code="fre">
        <text>a</text>
      </extended_event_descriptor>
    </event>
    <event event_id="0x1629" start_time="2021-04-23 10:00:26" duration="00:01:00" running_status="not-running" CA_mode="false">
      <short_event_descriptor language_code="fre">
        <event_name>JML0</event_name>
        <text>2</text>
      </short_event_descriptor>
      <extended_event_descriptor descriptor_number="0" last_descriptor_number="0" language_code="fre">
        <text>8</text>
      </extended_event_descriptor>
    </event>
    <!-- ... -->
  </EIT>
  <EIT type="pf" version="7" current="true" actual="true" service_id="0x0402" transport_stream_id="0x0004" original_network_id="0x20FA" last_table_id="0x4E">
    <!-- ... -->
  </EIT>
  <EIT type="pf" version="7" current="true" actual="true" service_id="0x0407" transport_stream_id="0x0004" original_network_id="0x20FA" last_table_id="0x4E">
    <!-- ... -->
  </EIT>
  <EIT type="pf" version="7" current="true" actual="true" service_id="0x0415" transport_stream_id="0x0004" original_network_id="0x20FA" last_table_id="0x4E">
    <!-- ... -->
  </EIT>
  <EIT type="pf" version="7" current="true" actual="true" service_id="0x0416" transport_stream_id="0x0004" original_network_id="0x20FA" last_table_id="0x4E">
    <!-- ... -->
  </EIT>
</tsduck>

On voit qu’il y a cinq EIT différentes, une par chaîne. Ici on travaille avec celle identifiée par 0x401, donc il faut prendre ces données là (chaque chaîne dérive une clé différente).

Les événements ont tous la même tête. Un nom de quatre caractères, une description courte d’un caractère hexadécimal et une description longue d’un caractère hexadécimal. L’application choisit lequel prendre et à quelle position le placer dans la clé en fonction du hash du nom de l’événement.

Pour résoudre le challenge, on peut simplement faire un objet JSON avec les événements de la chaîne et modifier l’application HbbTV pour utiliser cet objet plutôt que celui qui vient du flux. Ensuite on déchiffre la donnée “\x00\x11…\xff” et on affiche le flag.

Dans le fichier locker.html :

// var all_programmes = vb.programmes;
var all_programmes = [{ "name": "vihZ", "description": "e", "longDescription": "a"},
{ "name": "JML0", "description": "2", "longDescription": "8"},
{ "name": "w5fy", "description": "e", "longDescription": "0"},
{ "name": "9s6e", "description": "5", "longDescription": "6"},
{ "name": "hvqm", "description": "b", "longDescription": "8"},
{ "name": "CfJy", "description": "b", "longDescription": "8"},
{ "name": "jRw0", "description": "1", "longDescription": "a"},
{ "name": "R8WL", "description": "7", "longDescription": "c"},
{ "name": "s7cQ", "description": "c", "longDescription": "d"},
{ "name": "sntz", "description": "d", "longDescription": "6"},
{ "name": "A5eb", "description": "a", "longDescription": "0"},
{ "name": "wjWE", "description": "3", "longDescription": "3"},
{ "name": "n6MY", "description": "2", "longDescription": "f"},
{ "name": "aOgJ", "description": "7", "longDescription": "b"},
{ "name": "C0Ji", "description": "7", "longDescription": "c"},
{ "name": "PoPJ", "description": "c", "longDescription": "6"},
{ "name": "auq8", "description": "7", "longDescription": "0"},
{ "name": "gbqp", "description": "0", "longDescription": "9"},
{ "name": "lnHr", "description": "e", "longDescription": "e"},
{ "name": "PupA", "description": "8", "longDescription": "1"},
{ "name": "TAZX", "description": "0", "longDescription": "b"},
{ "name": "w9QY", "description": "8", "longDescription": "0"},
{ "name": "o5ks", "description": "4", "longDescription": "b"},
{ "name": "c8gS", "description": "a", "longDescription": "8"},
{ "name": "hrJG", "description": "7", "longDescription": "6"},
{ "name": "KaXo", "description": "8", "longDescription": "5"},
{ "name": "WrKS", "description": "7", "longDescription": "4"},
{ "name": "r6xS", "description": "f", "longDescription": "7"},
{ "name": "v7rk", "description": "6", "longDescription": "6"},
{ "name": "iyi8", "description": "2", "longDescription": "0"},
{ "name": "KlK4", "description": "f", "longDescription": "0"},
{ "name": "UzMW", "description": "9", "longDescription": "9"}];

/*
    [...]
*/

    //input = binDecode(input);
    key = hexDecode(key.join(""));
    input = enc(key, hexDecode("00112233445566778899aabbccddeeff"));

/*
    [...]
*/

Une fois exécutée avec n’importe quelle entrée, le flag s’affiche.

Victoire

Flag

FCSC{b2812096b4c3239083fdddd56c77f9e760bad3f488e4852e8acf2a59ceb37e49}