Introspection et analyse de malware via LibVMI

Magazine
Marque
MISC
Numéro
102
Mois de parution
mars 2019
Spécialité(s)


Résumé

Dans cet article, nous allons illustrer ce qu’est l’introspection des machines virtuelles et comment on peut l’utiliser dans le but d’analyser des malwares.


Body

1. Qu’est-ce que l’introspection ?

De façon générale, le terme introspection désigne l’observation et l’examen de son propre état mental et émotionnel. Il est considéré comme l’acte de se regarder soi-même. Le meilleur exemple pour illustrer cela est l’œil de judas qui permet d’observer le monde extérieur sans avoir besoin d’en faire partie. Toutefois, l’introspection des machines virtuelles est l’art de monitorer les machines virtuelles depuis l’hyperviseur et y accéder sans être dedans (aucun agent n’est installé sur la machine virtuelle).

2. Pourquoi l’introspection ?

Grâce à la technologie d’introspection, il n’y a nul besoin de faire partie de l’environnement du malware pour l’analyser. Tout simplement parce que le comportement des processus à surveiller sera réalisé en dehors de la machine virtuelle, depuis l’hyperviseur. De plus, un malware employant des techniques de détection de débogueurne marchera pas étant donné que le système d’introspection interagit uniquement avec la mémoire de la machine virtuelle et ne s’attache en aucun cas aux processus lancés sur la machine. De même, il est possible de tromper un malware employant des techniques de détection d’environnement d’analyse (modifier à la volée l’accès à une clé de registre VirtualBox par exemple). De ce fait, on ne peut nier que l’application de l’introspection à l’analyse de malware est bien meilleure que les technologies traditionnelles d’analyse automatisée.

3. Architecture de l’introspection

3.1 Types d’hyperviseurs

On considère généralement deux types d’hyperviseurs :

  • Hyperviseur natif : connu aussi sous le nom de l’hyperviseur type-1 ou encore « Bare Metal ». Ce type d’hyperviseur fonctionne directement sur le matériel de la machine afin de contrôler et gérer les machines virtuelles. Exemples d’hyperviseurs de type-1 : Xen, KVM...
  • Hyperviseur hébergé : connu aussi sous le nom de l’hyperviseur de type-2 ou encore « Hosted ». Ce type d’hyperviseur s’exécute à l’intérieur d’un autre système d’exploitation. Exemples d’hyperviseurs de type-2 : VirtualBox, VMware...

La figure suivante illustre le fonctionnement de l’introspection sur l’hyperviseur de type-1. Les outils d’introspection installés sur l’hyperviseur permettent de surveiller et de contrôler tout ce qui se passe sur la machine virtuelle (activité système, réseau, applicative…).

 

introspection_dans_analyse_de_malware_figure_01

 

Fig 1 : Exemple d’hyperviseur natif et d’hyperviseur hébergé.

On peut bien imaginer le même fonctionnement pour l’hyperviseur de type-2.

3.2 Mappage mémoire

De façon générale, il existe deux niveaux de mémoire ; mémoire virtuelle et mémoire physique de la machine physique. Et trois niveaux de mémoire quand on parle d’hyperviseur (mémoire virtuelle et mémoire physique de la machine virtuelle, et mémoire physique de la machine hôte. La mémoire virtuelle de la machine hôte est abstraite dans ce cas). Il faut garder en tête que les hyperviseurs allouent uniquement de la mémoire à la machine virtuelle. Les hyperviseurs par défaut n’ont aucune connaissance de ce qui se passe dans la mémoire virtuelle de la machine virtuelle. Pour ce faire, des outils additionnels doivent être installés. Ci-dessous un exemple simplifié du partage mémoire avec la machine virtuelle.

 

introspection_dans_analyse_de_malware_figure_02

 

Fig 2 : Exemple des trois niveaux d’adressage mémoire sous hyperviseur.

L’un des objectifs des outils d’introspection est de réaliser la traduction des adresses mémoire de la mémoire virtuelle de la machine virtuelle en mémoire physique de la machine virtuelle, et ensuite de la mémoire physique de la machine virtuelle en mémoire physique de la machine physique, dans le but d’aider l’hyperviseur à accéder à la bonne zone mémoire pendant son introspection.

4. LibVMI

4.1 Qu’est-ce que LibVMI ?

LibVMI est une librairie en C permettant de mettre en place un système d’introspection des machines virtuelles sous Linux ou Windows. Elle permet d’accéder à la mémoire d’une machine virtuelle en cours d’exécution. Ceci tout en offrant une panoplie de fonctions permettant d’accéder à la mémoire grâce à l’adressage physique ou virtuel et des symboles Kernel. LibVMI offre aussi un accès à la mémoire à partir d’un Snapshot de mémoire physique, ce qui peut s’avérer intéressant lors de débogage ou d’investigation numérique.

En plus de l’accès mémoire, LibVMI supporte les événements mémoire. Ces événements déclenchent des notifications quand une région mémoire est accédée en lecture, écriture ou en exécution.

Plusieurs niveaux d’abstraction complexes existent quand on parle d’introspection. Ces niveaux sont heureusement gérés par LibVMI [1] et nous sont complètement transparents.

4.2 Les API LibVMI

LibVMI met à disposition une variété de fonctions et d’API intuitives permettant au développeur d’interagir plus facilement avec la mémoire. Il existe des API basiques et d’autres, plus avancées.

Toutes les API peuvent être retrouvées sur le site officiel de LibVMI.

4.3 Exemples d’utilisation

LibVMI fournit par défaut un ensemble d’exemples afin de tester ce concept, voire même reposer sur ces exemples comme base dans le but de créer son propre système d’introspection.

 

Tous les exemples listés ci-après sont à retrouver au complet sur le GitHub de LibVMI [2].

4.3.1 Listing de processus

Le code suivant permet de lister les processus lancés sur la machine virtuelle depuis l’hyperviseur.

On commence par initialiser les contextes de fonctionnement de LibVMI et donc l’instance LibVMI qui correspondra à la machine virtuelle sur laquelle nous lancerons notre programme.

...
vmi_init_complete(&vmi, name, VMI_INIT_DOMAINNAME, NULL, VMI_CONFIG_GLOBAL_FILE_ENTRY, NULL, NULL)

...

Ensuite, suivant le système d’exploitation sur lequel nous souhaitons lister les processus, l’initialisation des champs sera différente (dans l’exemple fourni, on considère trois systèmes d’exploitation différents : Windows, Linux et FreeBSD).

...

if (VMI_OS_LINUX == vmi_get_ostype(vmi)) {
   if (VMI_FAILURE == vmi_get_offset(vmi, "linux_tasks", &tasks_offset))
      goto error_exit;

   if (VMI_FAILURE == vmi_get_offset(vmi, "linux_name", &name_offset))

      goto error_exit;

   if (VMI_FAILURE == vmi_get_offset(vmi, "linux_pid", &pid_offset))

      goto error_exit;

} else if (VMI_OS_WINDOWS == vmi_get_ostype(vmi)) {

   if (VMI_FAILURE == vmi_get_offset(vmi, "win_tasks", &tasks_offset))

      goto error_exit;

   if (VMI_FAILURE == vmi_get_offset(vmi, "win_pname", &name_offset))

      goto error_exit;

   if (VMI_FAILURE == vmi_get_offset(vmi, "win_pid", &pid_offset))

      goto error_exit;

} else if (VMI_OS_FREEBSD == vmi_get_ostype(vmi)) {

   tasks_offset = 0;

   if (VMI_FAILURE == vmi_get_offset(vmi, "freebsd_name", &name_offset))

      goto error_exit;

   if (VMI_FAILURE == vmi_get_offset(vmi, "freebsd_pid", &pid_offset))

      goto error_exit;

}

...

Après avoir récupéré les données nous intéressant, nous allons ensuite boucler sur la liste des processus et les afficher (PID, nom du processus et son adresse mémoire) un par un, jusqu’à la fin de la chaîne des processus.

...

while (1) {

   current_process = cur_list_entry – tasks_offset;

   vmi_read_32_va(vmi, current_process + pid_offset, 0, (uint32_t*)&pid);

   procname = vmi_read_str_va(vmi, current_process + name_offset, 0);

 

   if (!procname) {

      printf("Failed to find procname\n");

      goto error_exit;

   }

 

   printf("[%5d] %s (struct addr:%"PRIx64")\n", pid, procname, current_process);

 

   if (procname) {

      free(procname);

      procname = NULL;

   }

 

   if (VMI_OS_FREEBSD == os && next_list_entry == list_head) {

      break;

   }

 

   cur_list_entry = next_list_entry;

   status = vmi_read_addr_va(vmi, cur_list_entry, 0, &next_list_entry);

 

   if (status == VMI_FAILURE) {

      printf("Failed to read next pointer in loop at %"PRIx64"\n", cur_list_entry);

      goto error_exit;

   }

 

   if (VMI_OS_WINDOWS == os && next_list_entry == list_head) {

      break;

   } else if (VMI_OS_LINUX == os && cur_list_entry == list_head) {

      break;

   }

};

...

Et enfin, nous allons appeler la fonction qui va détruire l’instance LibVMI créée sur la machine virtuelle et libérer la mémoire concernée.

...

vmi_destroy(vmi);
...

4.3.2 Monitoring des événements

Mis à part son utilisation classique comme démontré dans l’exemple précédent, LibVMI permet aussi de monitorer des « événements » à la volée. Cela en notifiant l’hyperviseur de tout accès mémoire qui peut avoir lieu (lecture, écriture, exécution).

Ci-dessous, un exemple tiré aussi des exemples LibVMI permettant de créer des événements d’interruption.

Contrairement à la phase d’initialisation dans l’exemple du listing de processus, une petite modification est nécessaire. Le flag VMI_INIT_EVENTS doit être ajouté pour permettre à l’instance de monitorer les événements à la volée.

...
vmi_init(&vmi, VMI_XEN, (void*)name, VMI_INIT_DOMAINNAME | VMI_INIT_EVENTS, NULL, NULL)

...

Après avoir initialisé l’instance LibVMI, il est possible de créer un callback afin de traquer le type d’événement souhaité. Dans notre exemple, le choix a été fait sur l’événement d’interruption INT3 (breakpoint).

...
memset(&interrupt_event, 0, sizeof(vmi_event_t));
interrupt_event.version = VMI_EVENTS_VERSION;
interrupt_event.type = VMI_EVENT_INTERRUPT;
interrupt_event.interrupt_event.intr = INT3;

vmi_register_event(vmi, &interrupt_event);

...

On déclare ensuite une fonction de callback, qui sera exécutée à chaque fois qu’une interruption a lieu. Cette fonction de callback suppose que tous les événements INT3 sont causés par un débogueur par exemple, et va tout simplement les réinjecter sans rien faire.

event_response_t int3_cb(vmi_instance_t vmi, vmi_event_t *event)

{

   event->interrupt_event.reinject = 1;

 

   if (!event->interrupt_event.insn_length)

      event->interrupt_event.insn_length = 1;

 

   return 0;

}

5. Analyse de malware avec LibVMI

LibVMI permet de manipuler la mémoire, mais en aucun cas cela ne permet de faire de l’analyse de malware tel qu’il est. Afin de profiter du système d’introspection de LibVMI, il faudra développer son propre système pour analyser les malwares en reposant sur les API fournis par LibVMI.

Dans cette partie, nous allons nous focaliser sur le monitoring des API Kernel Windows, afin de lister les différents comportements du malware. Ci-dessous, quelques possibilités fournies par LibVMI pour monitorer la mémoire d’un exécutable.

5.1 Monitoring via des breakpoints

L’idée est donc de placer des breakpoints (points d’arrêt d’exécution, souvent utilisés dans le reverse engineering) sur les API Kernel qu’on souhaite monitorer sur la machine virtuelle lors de l’initialisation du système d’introspection. Ceci, tout en maintenant une table de correspondance des offsets des breakpoints placés et les octets modifiés. Après la mise en place et le lancement du système d’introspection, un malware sollicitant une API Kernel en passant par une API Usermode (l’appel à l’API Usermode CreateFile requiert l’appel à l’API Kernel NtCreateFile) sera interrompu si l’API Kernel figure parmi les API monitorées. Une notification est donc remontée au niveau de l’hyperviseur, nous permettant de retrouver quel exécutable dans l’espace utilisateur a atteint le breakpoint dans l’espace Kernel. Une fois que les informations souhaitées (la fonction, ses arguments…) sont récupérées, l’exécution du malware continue (surtout pour ne pas modifier son comportement) jusqu’au prochain breakpoint. Grâce à ces informations, il nous est possible de comprendre le comportement du malware analysé.

5.2 Monitoring via des pages mémoire

Avec cette technique, il n’y a nul besoin de placer des breakpoints et donc d’altérer la mémoire. L’idée consiste à placer une surveillance avec des événements mémoire sur chaque page mémoire exécutable et de calculer l’adresse des API qu’on souhaite monitorer. À chaque fois qu’une région de la page marquée en exécution est accédée, l’information est remontée et une comparaison est faite avec l’adresse de l’API monitorée.

5.3 Monitoring via des breakpoints + altp2m

Comme décrit précédemment, l’utilisation des breakpoints seule ne fonctionnera pas, c’est pourquoi les développeurs de Xen ont élaboré une technique intitulée altp2m. Cette technique a pour but de créer un shadow copy des pages mémoire souhaitées. L’idée est donc de créer une vue altérée (contenant les breakpoints sur les API Kernel qu’on souhaite monitorer) et garder une vue intacte qui sera retournée à chaque fois qu’il y a un accès en lecture ou écriture à ces pages mémoire.

Grâce à cette méthode, on résout les deux problèmes cités dans 5.1 Monitoring via des breakpoints.

6. Étude de cas

6.1 Préparation de l’environnement

Après avoir choisi le nom du domaine XEN de la machine, créé le fichier contenant le dictionnaire et déterminé le nom de fichier de malware à analyser, on lance depuis l’hyperviseur la commande suivante :

./monitor_api w6164-1 /tmp/w6164-1.json malware.exe

Notre script va prendre en argument plusieurs entrées :

  • monitor_api : un script que nous avons développé et qui repose sur les fonctions de LibVMI ;
  • w6164-1 : le nom du domaine XEN correspondant à de la machine virtuelle ;
  • /tmp/w6164-1.json : le fichier contenant les fonctions/structures et les offsets correspondants ;
  • malware.exe : le malware que nous souhaitons analyser.

Ci-dessous, le code permettant l’initialisation de notre système d’introspection :

...

vmi_init(&vmi, VMI_XEN | VMI_INIT_COMPLETE | VMI_INIT_EVENTS, argv[1]);

...

On crée ensuite la vue altp2m qui sera altérée :

...

xc_altp2m_set_domain_state(xch, domain_id, 1);

...
xc_altp2m_create_view(xch, domain_id, 0, &shadow_view);

...

6.2 Insertion des breakpoints

On définit dans un premier temps les API que nous souhaitons monitorer. Prenons par exemple les trois API suivantes :

  • NtCreateFile : permet la création/ouverture de fichier ;
  • NtSetValueKey: permet de créer ou remplacer la valeur d’une clé de registre ;
  • NtDelayExecution : permet de retarder l’exécution (cette API est exécutée quand l’appel à l’API Sleep est effectué). Elle peut être utilisée à des fins d’évasion.

Pour plus d’informations sur ces API, vous pouvez vous référer à la documentation MSDN [3].

Ensuite, on insère nos breakpoints dans la vue altp2m pour chacune des API.

...
uint8_t trap = 0xcc;

vmi_write_8_pa(vmi, (shadow << 12) + shadow_offset, &trap)

...

6.3 Déclaration des callbacks

6.3.1 Callback des événements d’interruption

Ce callback est déclenché à chaque fois qu’un breakpoint est atteint. Il va permettre de récupérer les informations souhaitées grâce à la fonction process_event(). Cette fonction va nous permettre d’identifier le nom de l’API qui a atteint le breakpoint ainsi que ses arguments.

...
SETUP_INTERRUPT_EVENT(&trap_event, 0, interrupt_event_cb);

vmi_register_event(vmi, &trap_event);
...
event_response_t interrupt_event_cb(vmi_instance_t vmi, vmi_event_t *event)

{

   ...

   process_event(...);

   event->slat_id = 0;

   event->interrupt_event.reinject = 0;

   return VMI_EVENT_RESPONSE_TOGGLE_SINGLESTEP | VMI_EVENT_RESPONSE_VMM_PAGETABLE_ID;

}

6.3.2 Callback des événements de pas-à-pas

Ce callback est déclenché directement après l’exécution du callback d’interruption. Cela permet d’exécuter en pas-à-pas (comme dans un débugueur) l’instruction du breakpoint depuis la vue non altérée. Ensuite, on reprend l’exécution depuis la vue altérée. Dans le cas où plusieurs vcpus sont attribués à la machine virtuelle, il est nécessaire de boucler sur le nombre de vcpus et de créer un callback pour chaque vcpu.

...

int vcpus = vmi_get_num_vcpus(vmi);

for (i = 0; i < vcpus; i++)

{

   SETUP_SINGLESTEP_EVENT(&singlestep_event[i], 1u << i, singlestep_event_cb, 0);

   vmi_register_event(vmi, &singlestep_event[i]);

}

...

event_response_t singlestep_event_cb(vmi_instance_t vmi, vmi_event_t *event)

{

   event->slat_id = shadow_view;

   return VMI_EVENT_RESPONSE_TOGGLE_SINGLESTEP | VMI_EVENT_RESPONSE_VMM_PAGETABLE_ID;

}

6.3.3 Callback des événements lecture/écriture mémoire

Ce callback est déclenché quand une lecture ou écriture est faite sur l’une des pages mémoire du shadow copy. Si tel est le cas, un changement de vue est instantanément fait sur la vue non altérée. Ceci permet d’éviter un BSOD lorsque le système ou un malware effectue une vérification d’intégrité.

...

SETUP_MEM_EVENT(&mem_event, ~0ULL, VMI_MEMACCESS_RW, memory_event_cb, 1);

vmi_register_event(vmi, &mem_event);

...

event_response_t memory_event_cb(vmi_instance_t vmi, vmi_event_t *event)

{

   event->slat_id = 0;

   return VMI_EVENT_RESPONSE_TOGGLE_SINGLESTEP | VMI_EVENT_RESPONSE_VMM_PAGETABLE_ID;

}

Pour plus d’informations sur le fonctionnement de ces callbacks, veuillez consulter le site de LibVMI.

6.4 Récupération des informations

Quand un breakpoint est atteint, on le confronte au dictionnaire d’offsets et d’API, on récupère les informations souhaitées (arguments de l’API) et on poursuit l’exécution.

Prenons exemple d’un breakpoint sur NtCreateFile (ci-dessous son prototype).

NTSTATUS NtCreateFile(

   OUT PHANDLE                       FileHandle,

   IN ACCESS_MASK                    DesiredAccess,

   IN POBJECT_ATTRIBUTES             ObjectAttributes,

   OUT PIO_STATUS_BLOCK              IoStatusBlock,

   IN PLARGE_INTEGER AllocationSize  OPTIONAL,

   IN ULONG                          FileAttributes,

   IN ULONG                          ShareAccess,

   IN ULONG                          CreateDisposition,

   IN ULONG                          CreateOptions,

   IN PVOID                          EaBuffer OPTIONAL,

   IN ULONG                          EaLength

);

Le nom du fichier et les droits d’accès sont principalement les informations intéressantes de cette API. Le nom de fichier se trouve dans la structure OBJECT_ATTRIBUTES. Afin de parser cette structure, on utilisera le dictionnaire généré précédemment et qui contient les offsets des différentes entrées de la structure :

"_OBJECT_ATTRIBUTES": [48, {

   "Attributes": [24, ["unsigned long", {}]],

   "Length": [0, ["unsigned long", {}]],

   "ObjectName": [16, ["Pointer", { "target": "_UNICODE_STRING"}]]

On fera de même pour les autres structures.

Il n’y a nul besoin de modifier les arguments de l’API, même s’il est possible de le faire. Le but est de laisser exécuter le malware sans l’interrompre, récupérer les informations intéressantes à la volée, puis ensuite à la fin de son exécution, analyser les informations remontées et déterminer son comportement.

Ci-dessous un extrait de log des événements générés lors d’analyse de malware. Seules les informations faisant référence à un potentiel comportement malveillant sont gardées.

...

{"target":"C:\\Users\\Francis\\AppData\\Roaming\\Windows\\conhost.exe","process":"C:\\Users\\Francis\\Desktop\\sample.exe","pid":2616,"api":"NtCreateFile","access_mask":1074790528},

...

...

{"target":"\\REGISTRY\\USER\\S-1-5-21-336141597-709016518-532797093-1001\\SOFTWARE\\MICROSOFT\\WINDOWS\\CURRENTVERSION\\RUN\\Persistence","process" : "C:\\Users\\Francis\\Desktop\\sample.exe","pid":2616,"value":"C:\\Users\\Francis\\AppData\\Roaming\\Windows\\conhost.exe","api":"NtSetValueKey"},

...

...

{"value":3600,"process":"C:\\Users\\Francis\\Desktop\\sample.exe","pid":2616,"api":"NtDelayExecution"},

...

Conclusion

Les cyberattaques à base de malware ne cessent d’augmenter et sont de plus en plus sophistiquées, ce qui rend leur détection difficile. L’utilisation de l’introspection peut s’avérer très efficace. Comme démontré dans cet article, elle permet d’aller profondément dans l’analyse de malware et de tirer un maximum d’information sur son fonctionnement, sans avoir besoin d’interrompre son exécution normale.

Références

[1] http://libvmi.com

[2] https://github.com/libvmi/

[3] https://docs.microsoft.com/

 



Article rédigé par

Les derniers articles Premiums

Les derniers articles Premium

PostgreSQL au centre de votre SI avec PostgREST

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Dans un système d’information, il devient de plus en plus important d’avoir la possibilité d’échanger des données entre applications. Ce passage au stade de l’interopérabilité est généralement confié à des services web autorisant la mise en œuvre d’un couplage faible entre composants. C’est justement ce que permet de faire PostgREST pour les bases de données PostgreSQL.

La place de l’Intelligence Artificielle dans les entreprises

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

L’intelligence artificielle est en train de redéfinir le paysage professionnel. De l’automatisation des tâches répétitives à la cybersécurité, en passant par l’analyse des données, l’IA s’immisce dans tous les aspects de l’entreprise moderne. Toutefois, cette révolution technologique soulève des questions éthiques et sociétales, notamment sur l’avenir des emplois. Cet article se penche sur l’évolution de l’IA, ses applications variées, et les enjeux qu’elle engendre dans le monde du travail.

Petit guide d’outils open source pour le télétravail

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Ah le Covid ! Si en cette période de nombreux cas resurgissent, ce n’est rien comparé aux vagues que nous avons connues en 2020 et 2021. Ce fléau a contraint une large partie de la population à faire ce que tout le monde connaît sous le nom de télétravail. Nous avons dû changer nos habitudes et avons dû apprendre à utiliser de nombreux outils collaboratifs, de visioconférence, etc., dont tout le monde n’était pas habitué. Dans cet article, nous passons en revue quelques outils open source utiles pour le travail à la maison. En effet, pour les adeptes du costume en haut et du pyjama en bas, la communauté open source s’est démenée pour proposer des alternatives aux outils propriétaires et payants.

Sécurisez vos applications web : comment Symfony vous protège des menaces courantes

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Les frameworks tels que Symfony ont bouleversé le développement web en apportant une structure solide et des outils performants. Malgré ces qualités, nous pouvons découvrir d’innombrables vulnérabilités. Cet article met le doigt sur les failles de sécurité les plus fréquentes qui affectent même les environnements les plus robustes. De l’injection de requêtes à distance à l’exécution de scripts malveillants, découvrez comment ces failles peuvent mettre en péril vos applications et, surtout, comment vous en prémunir.

Les listes de lecture

11 article(s) - ajoutée le 01/07/2020
Clé de voûte d'une infrastructure Windows, Active Directory est l'une des cibles les plus appréciées des attaquants. Les articles regroupés dans cette liste vous permettront de découvrir l'état de la menace, les attaques et, bien sûr, les contre-mesures.
8 article(s) - ajoutée le 13/10/2020
Découvrez les méthodologies d'analyse de la sécurité des terminaux mobiles au travers d'exemples concrets sur Android et iOS.
10 article(s) - ajoutée le 13/10/2020
Vous retrouverez ici un ensemble d'articles sur les usages contemporains de la cryptographie (whitebox, courbes elliptiques, embarqué, post-quantique), qu'il s'agisse de rechercher des vulnérabilités ou simplement comprendre les fondamentaux du domaine.
Voir les 68 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous