Solution partielle du challenge SSTIC 2011

26 mai 2011 – 19:10

Cela fait un petit moment que je planche sur le célèbre challenge de SSTIC 2011. Étant donné que je ne pense pas en venir complétement à bout, voici quelques éléments de réponses qui résument ma progression. Certains m’ayant devancé, je vais tenter d’insister sur les parties non couvertes actuellement.

Extraction des pistes

Le but est d’analyser une vidéo et d’en extraire un mail. Il s’agit d’une vidéo au format MP4. Ne connaissant absolument pas ce format au moment où je me suis penché sur le challenge, j’ai donc préféré me documenter sur le sujet. MP4 est en réalité un conteneur, et hérite de l’ISO Base Media File Format. Les specs de ce dernier sont libres de droits, mais pas celles de MP4. Qu’à cela ne tienne, elles suffisent amplement.

On y apprend que MP4 permet de stocker tout type d’information : vidéo, son, et autre. Un fichier MP4 est décomposé en boxes, qui sont structurées sous la forme d’un arbre. L’outil MP4Box permet de les parser, mais je ne l’ai découvert que tardivement ; aussi je me suis lancé dans l’écriture d’un outil manuel en Python.

E:\Challenges\SSTIC2011>boxes.py challenge
challenge {
  FileTypeBox(24 - ftyp - mp42 - 0),
  Box(4170138 - mdat),
  Box(277450 - mdat),
  Box(178748 - mdat),
  MovieBox(12507 - moov) {
    Box(108 - mvhd),
    TrackBox(5982 - trak - 0 ) {
      Box(92 - tkhd),
      SsticBox(12 - ssti)[   ?ssti    ],
      MediaBox(5822 - mdia) {
        Box(32 - mdhd),
        HandlerBox(33 - hdlr - vide -  ),
        MediaInformationBox(5749 - minf) {
          Box(20 - vmhd),
          DataInformationBox(36 - dinf) {
            DataReferenceBox(28 - dref) {
              1 entries
              DataEntryUrlBox(12 - url  - flags: 000001, url: )
            }
          },
          SampleTableBox(5685 - stbl) {
            SampleDescriptionBox(181 - stsd) {
              1 entries
              VisualSampleEntry(165 - mp4v - index: 1) {
                size: 640x480
                resolution: 0x00480000/0x00480000
                compressorname:
                depth: 0018

              }
            },
            Box(3248 - stts),
            SampleSizeBox(2092 - stsz) {
              sample_size = 0
              sample_count = 518

            },
            SampleToChunkBox(40 - stsc) {
              2 entries
            },
            ChunkOffsetBox(88 - stco) {
              18 entries
            },
            Box(28 - stss)
          }
        }
      },
      Box(48 - edts)
    },
    TrackBox(3492 - trak - 1 ) {
      Box(92 - tkhd),
      MediaBox(3372 - mdia) {
        Box(32 - mdhd),
        HandlerBox(33 - hdlr - soun -  ),
        MediaInformationBox(3299 - minf) {
          Box(16 - smhd),
          DataInformationBox(36 - dinf) {
            DataReferenceBox(28 - dref) {
              1 entries
              DataEntryUrlBox(12 - url  - flags: 000001, url: )
            }
          },
          SampleTableBox(3239 - stbl) {
            SampleDescriptionBox(103 - stsd) {
              1 entries
              Box(87 - mp4a)
            },
            Box(24 - stts),
            SampleSizeBox(2980 - stsz) {
              sample_size = 0
              sample_count = 740

            },
            SampleToChunkBox(40 - stsc) {
              2 entries
            },
            ChunkOffsetBox(84 - stco) {
              17 entries
            }
          }
        }
      },
      UserDataBox(20 - udta) {
        NameBox(12 - name -    ?name    )
      }
    },
    TrackBox(2917 - trak - 2 ) {
      Box(92 - tkhd),
      MediaBox(2817 - mdia) {
        Box(32 - mdhd),
        HandlerBox(45 - hdlr - data - SsticHandler ),
        MediaInformationBox(2732 - minf) {
          Box(12 - nmhd),
          DataInformationBox(36 - dinf) {
            DataReferenceBox(28 - dref) {
              1 entries
              DataEntryUrlBox(12 - url  - flags: 000001, url: )
            }
          },
          SampleTableBox(2676 - stbl) {
            SampleDescriptionBox(32 - stsd) {
              1 entries
              ElfEntry(16 - elf  - index: 1)
            },
            Box(24 - stts),
            SampleToChunkBox(1552 - stsc) {
              128 entries
            },
            SampleSizeBox(532 - stsz) {
              sample_size = 0
              sample_count = 128

            },
            ChunkOffsetBox(528 - stco) {
              128 entries
            }
          }
        }
      }
    }
  }
}

Le fichier est ainsi composé de 3 tracks : une video, une piste son, et une piste apparemment inconnue. On peut extraire chacune de ces pistes en utilisant les méta-données du fichier, et en particulier les tables STSZ, STSC et STCO. L’algorithme d’extraction n’est pas bien complexe et peut être trouvé grâce aux spécifications.

La piste audio est en clair, mais pas très utile – quelque chose qui ressemble à des frappes sur un clavier. La vidéo est illisible, a priori chiffrée grâce à des DRM. Enfin, la piste 3 n’est autre qu’un fichier ELF !

Analyse du fichier ELF

Avec IDA, on s’aperçoit rapidement que le binaire n’est autre qu’un plugin VLC. En effet, il comporte tous les symboles propres à ce type de plugin.On suppose que ce plugin est nécessaire à la lecture de ladite vidéo. Les sources de VLC permettant de se rendre compte qu’il s’agit en fait d’une version modifiée du plugin officiel libmp4.so. Un certain nombre de fonctions ont été ajoutées :

  • sstic_drm_init()
  • sstic_read_secret1(char*dir, char* buf1)
  • sstic_check_secret1(char*buf1)
  • sstic_read_secret2(char*dir, char*buf2)
  • sstic_check_secret2(char*buf2)
  • sstic_lame_derive_key(char* bufKey, char* buf1, char* buf2)

Il est possible de débuger le plugin en le plaçant dans le dossier des librairies de VLC sur une machine Linux, puis en s’attachant à VLC avec GDB.

En gros, sstic_drm_init() est appelée dès l’initialisation du plugin, et est chargée d’appeler les fonctions suivantes. Le but de ce plugin est de déchiffrer la vidéo a l’aide d’une clé générée à partir de deux fichiers, secret1.dat et secret2.dat situés dans le dossier ~/sstic2011/. Voici un schéma expliquant l’algorithme reversé à grand coup d’IDA et Hex Rays :

 

Algorithme de déchiffrement
Algorithme de déchiffrement

Pour résumer :

  • secret1.dat (32 octets) est lu, puis son emprunte MD5 est calculée et comparée avec une constante présente en dur dans le binaire
  • secret2.dat (1024 octets) est lu, puis déchiffré à l’aide d’un algorithme symétrique maison (cf plus bas) et d’une clé fixe stockée. Le plaintext résultant est comparé à un plaintext lui aussi stocké en dur.
  • Si les deux tests précédents réussissent, les 2 secrets originaux sont passés à la fonction sstic_lame_derive_key, qui ne fait que les xorrer pour finalement obtenir un secret de 32 octets (pour secret1.dat, qui fait 1024 octets, tous les blocs de 32 octet sont xorrés)
  • Le secret résultant est utilisé comme une clé RC2 qui servira à déchiffrer chaque sample de la vidéo lors de sa lecture. Au passage, l’implémentation RC2 est la même que celle d’OpenSSL, pas de soucis de ce côté la.

Autrement dit, il nous faut nécessairement secret1 et secret2 si l’on veut déchiffrer la vidéo. C’est là que les choses se compliquent…

Secret2.dat : Crypto + MMX

Pour trouver secret2.dat, on remarque qu’il « suffit » d’inverser l’algorithme de déchiffrement implémenté dans la fonction « decrypt », pour trouver le chiffré qui donnera le bon clair une fois déchiffré. Cependant, il ne s’agit pas directement d’un algorithme standard… Celui-ci utilise massivement les instructions Intel MMX, qui manipulent des entiers sur 128 bits. Le code ASM est une horreur, et je ne parle pas du listing généré par Hex Rays…

 

Pseudo-code de decrypt()
Pseudo-code partiel de decrypt()

Pas moins de 351 variables, plus de 2100 lignes de code, et des opérations logiques dans tous les sens. Miam ! Bon, on prend notre courage à deux mains, et on y va. Premièrement, on remarque l’utilisation de constantes un peu particulières.

for ( sum = 0x9E3779B9 * a3; ; sum += -0x9E3779B9u ) {

Notre ami Google nous indique qu’une boucle faisant intervenir de telles constantes est utilisée par les algorithmes TEA et XTEA. Cependant, ces algorithmes ont une taille de clé de 128 bits, et une taille de bloc de 64 bits, alors qu’ici, la clé fait 2048 octets…

Deuxièmement, on s’aperçoit qu’il y a beaucoup de code redondant dans ce listing. Cela est du à l’utilisation intensive des techniques de loop unrolling, qui consistent en gros à minimiser le nombre de tours dans une boucle en écrivant n fois la même instruction dans un tour (appliqué à des données différentes).

Troisièmement, en supposant qu’il s’agisse de l’algorithme TEA, la logique voudrait qu’on trouve des opérations + et -. Or ce n’est pas le cas ici ; on a affaire à des opérations logiques uniquement. Après on long moment de réflexion, je me suis rendu compte que ces opérations logiques servent en réalité à propager des retenues… utilisées justement lors des opérations d’addition et soustraction. On se rend ainsi compte que chaque addition a été remplacé par une série de xor, not et and équivalente au schéma suivant :

 

Additionneur

Additionneur

Attention, chaque fil ici représente non pas 1 bit, mais 128, soit 16 octets ! En gros, l’auteur du challenge a réimplémenté son propre + et – en recodant ces 2 opérations, prenant comme unité 128 bits de données. Soit !

En utilisant toutes ces information, et une bonne dose de motivation, j’ai pu confirmé qu’il s’agit bien de l’algorithme TEA, qui finalement n’est pas si complexe que ça (cf Wikipédia) :

void decrypt (uint32_t* v, uint32_t* k) {
    uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i;  /* set up */
    uint32_t delta=0x9e3779b9;                     /* a key schedule constant */
    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];   /* cache key */
    for (i=0; i<32; i++) {                         /* basic cycle start */
        v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
        v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
        sum -= delta;
    }                                              /* end cycle */
    v[0]=v0; v[1]=v1;
}

A la différence près que les uint32 de ce code doivent être remplacés par des blocs de 16 octets. Autrement dit, toutes les tailles doivent être multipliées par 4. Cela n’étant pas standard, il n’existe aucune librairie (à ma connaissance) faisant cela. Il est donc nécessaire de recoder l’opération encrypt() à la main !

Pour cela, j’ai copié/collé le code d’Hex Rays dans un fichier .c, puis je l’ai adapté afin qu’il compile dans un premier temps avec Visual Studio. Puis je l’ai optimisé en taille (suppression du loop unrolling). Et enfin, je l’ai inversé afin d’effectuer non pas un déchiffrement, mais un chiffrement.

On lance tout ça avec le plaintext et la clé tous deux stockés en dur dans le programme, et on obtient un secret2.dat valide. J’ai pu le confirmer en debuggant le plugin avec GDB, un breakpoint étant posé au bon endroit dans sstic_check_secret2(). Et une étape de passée !

Secret1.dat : MySQL, UDF and rock’n roll

Ici, contrairement à secret2.dat, nous ne disposons que du MD5 du fichier. Bruteforcer 32 octets étant une perte de temps, il doit donc y avoir un moyen de l’obtenir… J’ai séché pendant pas mal de temps, et c’est là que l’appel à un ami a servi (merci Dad) !

Ceux qui ont regardé le fichier original de la vidéo dans un éditeur auront certainement remarqué le « introduction.txt ». Il s’agit en réalité du nom d’un fichier texte qui a été gzipé et embarqué dans la vidéo. Malheureusement, il n’est pas en un seul bloc, mais en plusieurs, qui ont été éparpillées dans le fichier, aux endroits non utilisés (un fichier MP4 peut contenir du « vide »). Lors de notre opération d’extraction des pistes, nous nous sommes focalisés sur les données utiles du fichier. Et si nous faisions l’inverse ? Le fichier gzip peut être obtenu justement grâce à ce procédé, c’est-à-dire en suppriment tous les chunks non utilisés du fichier.

Une fois décompressé, on obtient la notice suivante :

Cher participant,
Le développeur étourdi d'un nouveau système de gestion de base de données
révolutionnaire a malencontreusement oublié quelques fichiers sur son serveur
web. Une partie des sources et des objets de ce SGBD pourraient se révéler
utile afin d'exploiter une éventuelle vulnérabilité.
Sauras-tu en tirer profit pour lire la clé présente dans le fichier
secret1.dat ?
url      : http://XX.XX.XX.XX/ login    : sstic2011 password : XXXXXXXX
--------------------------------------------------------------------------------
Toute attaque par déni de service est formellement interdite. Les organisateurs
du challenge se réservent le droit de bannir l'adresse IP de toute machine
effectuant un déni de service sur le serveur.
--------------------------------------------------------------------------------

On se connecte sur l’URL fournie, et on récupère trois fichiers :

  • udf.so
  • udf.c
  • lobster_dog.jpg (s’il s’agit d’une blague de l’auteur du challenge, je n’ai pas du la saisir :)

UDF signifie User Defined Functions ; il s’agit de plugins MySQL permettant de rajouter des fonctions sur un serveur. Le port 3306 du serveur hébergeant ces fichiers est ouverts, et les mêmes identifiants fonctionnent. Il y a donc bien quelque chose à exploiter ici. Fouillons un peu :

mysql> select version();
+------------------+
| 1.3.337sstic2011 |
+------------------+
| 1.3.337sstic2011 |
+------------------+
1 row in set (0.56 sec)

mysql> show databases;
+----------+
| Database |
+----------+
|   system |
|    sstic |
+----------+
2 rows in set (0.22 sec)

mysql> use sstic;
Database changed
mysql> show tables;
+--------+
| Tables |
+--------+
|  users |
+--------+
1 row in set (0.20 sec)

mysql> select * from users;
+------+--------+----------------------------------+
| id   | login  | password                         |
+------+--------+----------------------------------+
| 0    | root   | 3e47b75000b0924b6c9ba5759a7cf15d |    => nothing
| 1    | guest  | a76637b62ea99acda12f5859313f539a |    => interesting
| 2    | nobody | 6c92285fa6d3e827b198d120ea3ac674 |    => here
| 3    | *      | 5058f1af8388633f609cadb75a75dc9d |    => .
+------+--------+----------------------------------+
4 rows in set (0.28 sec)

mysql> use system;
Database changed
mysql> show tables;
+-------------+
| Tables      |
+-------------+
| information |
+-------------+
1 row in set (0.23 sec)

mysql> select * from information;
+------------------+----------+
| version          | security |
+------------------+----------+
| 1.3.337sstic2011 | SECCOMP  |
+------------------+----------+
1 row in set (0.22 sec)

Visiblement SECCOMP est activé, ce qui nous empêchera d’effectuer des appels systèmes autres que read() et write() sur des fichiers déjà ouverts. Croisons donc les doigts pour que le serveur ouvre secret1.dat en lecture avant d’activer ce mode :)

Une petite analyse du .c (analysé en partie sur ce blog) montre que celui-ci possède un bug dans la gestion d’un champ union, et est vulnérable à deux failles :

  • Une fuite d’information qui permet d’afficher n’importe quelle adresse mémoire
  • Une exécution de code à distance

Cependant, ces deux failles restents assez complexes à exploiter compte tenu du fait que nous sommes dans l’impossibilité de débugger le serveur distant, et que le udf.so donné ne parvient pas à se lancer sur un serveur MySQL standard. Au moindre problème, la connexion est coupée et il faut tout recommencer.

Toutefois, en exploitant la première faille et son bug de typage, on se rend compte que l’on peut obtenir des adresses mémoires :

mysql> select abs('lala');
+-----------+
| 153315552 |
+-----------+
| 153315552 |
+-----------+
1 row in set (0.59 sec)

mysql> select concat("", 153315552);
+------+
| lala |
+------+
| lala |
+------+
1 row in set (0.20 sec)

En réalité, 153315552 n’est pas directement l’adresse de la chaîne « lala », mais l’adresse de la structure « val » correspondante, qui ressemble à ceci :

struct val {
  int unknown;
  union value {
    int i;                    //4
    void * p;                 //4
  }
  int size;                   //8
  int *(expand)(val*);        //12
}

En gros, on peut demander à la fonction concat() de traiter une fausse structure située à une adresse choisie. Selon que l’on passe cette structure en 1er ou 2ème argument à la fonction, cela débouche soit sur une exécution de code (pointeur expand) ou sur une fuite (pointeur p).

Étant donné le caractère « blind » de cette exploitation, j’ai préféré dans un premier temps dumper le binaire du serveur MySQL. Celui-ci est chargé de façon standard à l’adresse 0×08048000. En forgeant une fausse structure et en la passant à substring(), elle aussi vulnérable, et en scriptant tout ça en python, et on récupère ce fameux binaire.

Seul soucis : celui-ci n’a aucun symbole et aucune table de section référencée. Impossible de le lancer, donc. En tout cas, on voit bien qu’il y a une référence à secret1.dat (les noms de fonction ne sont pas d’origine, mais ont été devinés).

int __cdecl my_open_secret1()
{
 int fd; // [sp+1Ch] [bp-Ch]@1

 fd = ((int (__cdecl *)(_DWORD, _DWORD))my_open)("secret1.dat", 0);
 if ( fd == -1 )
 sub_8048C28();
 return fd;
}

L’appel à open() est en réalité un appel à une fonction dans la section .plt du programme, qui utilise elle-même la section .got. Pour récupérer le code de cette fonction, j’ai utilisé toujours cette même faille de fuite d’information. Au final, l’adresse référencée est dans une librairie (adresse 0xb7XXXXXX). Son code est constitué d’instructions mov pour digitaliser des registres généraux, suivies d’un call *%gs:0×10, qui n’est autre qu’un appel système (le code appelé débouche certainement sur l’instruction sysenter ou int 0×80). La valeur d’eax, 5, permet de vérifier qu’il s’agit bien de sys_open. Le file descriptor retourné est stocké à l’adresse 0x0804F18C, est n’est autre que 3 (0, 1 et 2 correspondant respectivement à stdin, stdout et stderr).

Tout ce qu’il reste à faire, c’est donc trouver un moyen de faire exécuter un read() sur le file descriptor de secret1.dat afin de lire ses 32 octets qui nous manquent pour déchiffrer cette fichue vidéo. Seulement, je ne vois qu’une seule technique pour cela, et je n’ai pas eu le temps de la tester. La protection NX étant activée (pour s’en rendre compte il suffit de voire que l’exécution d’un ret situé dans le heap foire, alors qu’elle fonctionne dans la section .text), la seule solution reste à mon avis le Return Oriented Programming. Sauf que ne disposant ni d’une forte expérience dans cet art ancestral, ni d’une quantité de temps suffisante (les RSSIL approchent !), je n’ai pas eu le temps de creuser plus loin. Dommage, car j’ai vraiment l’impression d’être tout près du but… à un appel système près :( . A supposer bien sûr que le déchiffrement de la vidéo est l’étape finale… (là, je sens comme un doute m’envahir)

To be continued

Comme l’année dernière, ce challenge réunit à la fois du parsing, du reverse engineering et de la crypto, mais en plus une partie exploitation de service à distance, sur laquelle j’ai buté. J’attends avec impatience la solution de l’étape manquante, à savoir la récupération de secret1.dat. En tout cas, je ne regrette pas avoir passé de nombreuses heures (jours, en fait) sur cette épreuve, qui m’aura fait quelques nœuds aux neurones.

  1. 5 réponses à “Solution partielle du challenge SSTIC 2011”

  2. Tu es allé bien plus loin que moi, chapeau pour le secret2.dat !

    Par Kevin le 26 mai 2011

  3. Tu n’etais pas loin j’etais exactement au même point une heure avant d’envoyer le mail de validation :)

    Par SoTTo le 26 mai 2011

  4. Sérieux ? Bon ben j’essaierai de trouver du temps la semaine prochaine pour aller jusqu’au bout :)

    Par Emilien Girault le 26 mai 2011

  5. Après tout dépend de comment tu t’y prends: a ce stade deux solutions sont possibles une simple et une plus complexe (j’ai personnellement opté pour la simple n’ayant pas trouvée l’autre) :)

    Par SoTTo le 27 mai 2011

  6. Belle article ! Dommage pour le ROP :)

    Par TaPiOn le 19 septembre 2011

Désolé, les commentaires sont fermés pour le moment.