Déchiffrer le trafic TLS d’une application Android avec Frida


Déchiffrer le trafic TLS est souvent une étape incontournable lorsque l’on étudie une application Android.

La méthode usuelle est d’utiliser un proxy HTTPS comme BurpProxy ou Http Toolkit.

Cette méthode nécessite généralement d’installer un certificat dans le magasin de certificats du terminal (même s’il est possible d’arriver à ses fins en patchant l’application étudiée, mais c’est une autre histoire), chose qui est de plus en plus compliquée, comme l’explique cet article https://httptoolkit.com/blog/android-14-install-system-ca-certificate/ .

Mais pourquoi s’embêter alors qu’on peut récupérer les clés de la session TLS avec Frida ?

Regardons comment un script frida de quelques lignes permettrait d’arriver à nos fins.

Conscrypt et BoringSSL

À partir de Android 11, Conscrypt devient l’implémentation SSLSocket par défaut (voir fin de https://developer.android.com/privacy-and-security/security-ssl?hl=fr), ce qui signifie qu’une grande partie des applications utilisent Conscrypt.

En interne, Conscrypt utilise à son tour la bibliothèque native BoringSSL, qui est un fork de OpenSSL créé peu après la découverte de Heartbleed en 2014.

La fonction SSL_do_handshake

BoringSSL embarque bssl, outil de test en ligne de commande permettant de se connecter à un serveur TLS.

C’est la fonction SSL_connect(), appelée par la fonction DoConnection, qui fait le travail :

Cette fonction, implémentée dans ssl_lib.cc, se réduit essentiellement à un appel à SSL_do_handshake :

Cette fonction est elle aussi implémentée dans ssl_lib.cc :

Cette fonction est bien exportée par BoringSSL et pourra facilement être hookée avec frida :

$ aarch64-linux-gnu-objdump -T /tmp/libssl64.so | grep SSL_do
0000000000043cd0 g    DF .text    0000000000000160  Base        SSL_do_handshake

Frida permet de modifier le comportement d’une fonction native au tout début et à la toute fin de la fonction :

const func_ptr = Module.getExportByName('librairie.so', 'uneFonction');

Interceptor.attach(func_ptr, {

    onEnter(args) {
        on fait qqch ici
    },

    onLeave(retvar) {
        on fait qqch ici
    }
});

Ici c’est la fin de la fonction qui nous intéresse, puisque à ce stade le handshake est normalement terminé.

La fonction SSL_do_handshake() n’utilise qu’un seul argument, un pointeur ssl sur une structure de type SSL.

Il va donc falloir explorer la structure SSL pour voir si les secrets de handshake y sont bien nichés, directement ou non.

Comme le décrit la section 7 de la rfc 8446 décrivant TLSv1.3, les secrets en question sont à minima :

  • client_handshake_traffic_secret : Chiffrement du trafic de handshake client->serveur
  • server_handshake_traffic_secret : Chiffrement du trafic de handshake serveur->client
  • client_application_traffic_secret_N : Chiffrement du trafic applicatif client->serveur
  • server_application_traffic_secret_N : Chiffrement du trafic applicatif serveur->client

SSL est un alias sur ssl_st :

typedef struct ssl_st SSL;

Cette structure contient elle-même un pointeur vers une structure SSL3_STATE :

  // do_handshake runs the handshake. On completion, it returns |ssl_hs_ok|.
  // Otherwise, it returns a value corresponding to what operation is needed to
  // progress.
  bssl::ssl_hs_wait_t (*do_handshake)(bssl::SSL_HANDSHAKE *hs) = nullptr;

  bssl::SSL3_STATE *s3 = nullptr;   // TLS variables
  bssl::DTLS1_STATE *d1 = nullptr;  // DTLS variables

Cette structure embarque le client_random et le server_random :

struct SSL3_STATE {
  static constexpr bool kAllowUniquePtr = true;

  SSL3_STATE();
  ~SSL3_STATE();

  uint64_t read_sequence = 0;
  uint64_t write_sequence = 0;

  uint8_t server_random[SSL3_RANDOM_SIZE] = {0};
  uint8_t client_random[SSL3_RANDOM_SIZE] = {0};

Ainsi que les secrets applicatifs convoités :

  uint8_t write_traffic_secret[SSL_MAX_MD_SIZE] = {0};
  uint8_t read_traffic_secret[SSL_MAX_MD_SIZE] = {0};
  uint8_t exporter_secret[SSL_MAX_MD_SIZE] = {0};

Pas de trace des clés de handshake cependant.

Par contre, SSL3_STATE contient un UniquePtr sur une structure SSL_HANDSHAKE :

  // hs is the handshake state for the current handshake or NULL if there isn't
  // one.
  UniquePtr<SSL_HANDSHAKE> hs;

et cette dernière structure contient bien tout matériel cryptographique recherché :

struct SSL_HANDSHAKE {
  explicit SSL_HANDSHAKE(SSL *ssl);
  ~SSL_HANDSHAKE();
  static constexpr bool kAllowUniquePtr = true;

  // ssl is a non-owning pointer to the parent |SSL| object.
  SSL *ssl;

  // config is a non-owning pointer to the handshake configuration.
  SSL_CONFIG *config;

  // wait contains the operation the handshake is currently blocking on or
  // |ssl_hs_ok| if none.
  enum ssl_hs_wait_t wait = ssl_hs_ok;

  // state is the internal state for the TLS 1.2 and below handshake. Its
  // values depend on |do_handshake| but the starting state is always zero.
  int state = 0;

  // tls13_state is the internal state for the TLS 1.3 handshake. Its values
  // depend on |do_handshake| but the starting state is always zero.
  int tls13_state = 0;

  // min_version is the minimum accepted protocol version, taking account both
  // |SSL_OP_NO_*| and |SSL_CTX_set_min_proto_version| APIs.
  uint16_t min_version = 0;

  // max_version is the maximum accepted protocol version, taking account both
  // |SSL_OP_NO_*| and |SSL_CTX_set_max_proto_version| APIs.
  uint16_t max_version = 0;

 private:
  size_t hash_len_ = 0;
  uint8_t secret_[SSL_MAX_MD_SIZE] = {0};
  uint8_t early_traffic_secret_[SSL_MAX_MD_SIZE] = {0};
  uint8_t client_handshake_secret_[SSL_MAX_MD_SIZE] = {0};
  uint8_t server_handshake_secret_[SSL_MAX_MD_SIZE] = {0};
  uint8_t client_traffic_secret_0_[SSL_MAX_MD_SIZE] = {0};
  uint8_t server_traffic_secret_0_[SSL_MAX_MD_SIZE] = {0};
  uint8_t expected_client_finished_[SSL_MAX_MD_SIZE] = {0};

Nous avons donc une idée de ce qu’il faut faire :

Dans le hook onLeave de notre script Frida, récupérer le SSL *ssl, aller y chercher s3 au bon offset, et aller chercher hs au bon offset de s3.

On pourra valider cette stratégie en la testant sur une version modifiée de bssl dans un premier temps.

Malheureusement, certaines propriétés du C++ vont nous mettre des batons dans les roues.

En effet, la démarche envisagée est de :

  • récupérer le pointeur SSL *ssl à la fin de SSL_do_handshake,
  • aller au bon offset à l’intérieur de ssl, y lire et déréférencer s3,
  • aller dans s3, y lire et déréférencer hs.

Cette dernière étape ne va pas être possible simplement, comme l’illustre petit exemple ci-dessous :

#include <iostream>
#include <memory>

struct voiture
{
    unsigned short magic = 0xcafe;
    int a = 0;
    long b = 0;
    unsigned char nom[16] = {0};
};

typedef voiture voit;

int main()
{
    std::unique_ptr<voit> v1;
    voit *v2 = nullptr;

    v1 = std::unique_ptr<voit>(new voit);
    v2 = v1.get();

    printf("v2 : 0x%x\n", (void *)v2);
    printf("v1 : 0x%x\n", (void *)v1);

    return 0;
}

Le dernier printf contient un cast d’un unique_ptr en void *.

Un tel cast (nécessaire pour récupérer hs dans s3) est interdit par le compilateur :

$ g++ poc.c -o poc
poc.c: In function ‘int main()’:
poc.c:23:24: error: invalid cast from type ‘std::unique_ptr<voiture>’ to type ‘void*’
   23 |  printf("v1 : 0x%x\n", (void *)v1);
      |                        ^~~~~~~~~~

D’ailleurs, dans le code de SSL_do_handshake :

  // Run the handshake.
  SSL_HANDSHAKE *hs = ssl->s3->hs.get();

On voit que l’objet hs n’est pas obtenu en déréférençant un pointeur, mais en utilisant la méthode hs.get().

Malheureusement, ce getter ne semble pas être exporté par libssl.so :

$ objdump -T /tmp/libssl64.so  | grep '.text' | grep '_Z' | awk -F 'Base' '{print $2}' | tr -d ' ' | c++filt 
bssl::ssl_cipher_is_deprecated(ssl_cipher_st const*)
bssl::SSL_CTX_set_aes_hw_override_for_testing(ssl_ctx_st*, bool)
bssl::CBBFinishArray(cbb_st*, bssl::Array<unsigned char>*)
bssl::SSL_set_aes_hw_override_for_testing(ssl_st*, bool)
bssl::ssl_session_serialize(ssl_session_st const*, cbb_st*)
bssl::SSL_set_handoff_mode(ssl_st*, bool)
bssl::ssl_is_valid_ech_public_name(bssl::Span<unsigned char const>)
bssl::SSL_serialize_handoff(ssl_st const*, cbb_st*, ssl_early_callback_ctx*)
bssl::SSL_decline_handoff(ssl_st*)
bssl::ssl_client_hello_init(ssl_st const*, ssl_early_callback_ctx*, bssl::Span<unsigned char const>)
bssl::SSL_CTX_set_handoff_mode(ssl_ctx_st*, bool)
bssl::SSL_serialize_handback(ssl_st const*, cbb_st*)
bssl::SSL_SESSION_parse(cbs_st*, bssl::SSL_X509_METHOD const*, crypto_buffer_pool_st*)
bssl::ssl_cert_check_key_usage(cbs_st const*, bssl::ssl_key_usage_t)
bssl::SSL_apply_handback(ssl_st*, bssl::Span<unsigned char const>)
bssl::SSL_get_traffic_secrets(ssl_st const*, bssl::Span<unsigned char const>*, bssl::Span<unsigned char const>*)
bssl::ssl_decode_client_hello_inner(ssl_st*, unsigned char*, bssl::Array<unsigned char>*, bssl::Span<unsigned char const>, ssl_early_callback_ctx const*)
bssl::SSL_apply_handoff(ssl_st*, bssl::Span<unsigned char const>)
bssl::SSL_SESSION_dup(ssl_session_st*, int)

Il ne semble donc pas y avoir de moyen simple de récupérer l’objet hs, et donc les secrets qu’il abrite.

Impasse

On se retrouve donc dans une situation où l’on a accès aux secrets chiffrant le trafic, mais pas le handshake.

Comme on a par ailleurs accès au client_random et à l’exporter_secret (via l’objet s3), on peut peut-être construire un fichier keylogfile partiel.

Pour rappel, le fichier keylogfile, que certains logiciels génèrent lorsque la variable d’environnment SSLKEYLOGFILE est positionnée, permet à wireshark de déchiffrer un pcap contenant du trafic TLS.

On peut par exemple dechiffrer sa navigation internet en lançant SSLKEYLOGFILE=/quelque/part firefox et en capturant le trafic en parallèle.

Le fichier keylogfile est structuré ainsi :

# SSL/TLS secrets log file, generated by OpenSSL
SERVER_HANDSHAKE_TRAFFIC_SECRET <client_random> <server_handshake_secret>
EXPORTER_SECRET <client_random>  <exporter_secret>
SERVER_TRAFFIC_SECRET_0 <client_random> <server_traffic_secret>
CLIENT_HANDSHAKE_TRAFFIC_SECRET <client_random> <client_handshake_secret>
CLIENT_TRAFFIC_SECRET_0 <client_random> <client_traffic_secret>

Il nous manque donc les valeurs server_handshake_secret et client_handshake_secret.

Wireshark refuse d’utiliser un keylogfile incomplet. Remplacer les deux valeurs inconnues par des blocs de zéro n’est pas une solution et wireshark refuse également de déchiffrer le trafic dans ces conditions.

Lorsque l’on tente une expérience similaire avec https://github.com/T0lva/tls-dissector/tree/master, on rencontre un problème similaire : le trafic applicatif n’est pas déchiffré correctement.

Déchiffrer le trafic applicatif n’a pourtant rien d’impossible, car les secrets protégeant le handshake ne sont pas utilisés pour déchiffrer le trafic applicatif.

En réalité, dans le cas de tls-dissector, le problème est causé par la transition secrets de handshake -> secrets applicatifs :

Dans le cas où les secrets de handshake sont inconnus (à droite), il n’est pas possible de détecter le message FINISHED après lequel les secrets de handshake sont remplacés par les secrets applicatifs, et les compteurs GCM sont réinitialisés.

Il n’y a là rien d’insurmontable, car on pourrait faire une hypothèse sur la position des messages FINISHED, déchiffrer le trafic suivant ces messages, voir si on obtient quelque chose d’intelligible et passer à la position suivante si ce n’est pas le cas.

L’absence des secrets de handshake nous interdira de déchiffrer les messages chiffrés du handshake, notamment la chaîne de certificats du serveur.

Ce n’est toutefois pas si grave, car, dans le cas de trafic HTTPS on aura accès aux champs HOST des requêtes, et la majorité de l’information se trouve dans le trafic applicatif de toute façon.

Réinventer la roue

La situation n’est toutefois pas idéale : Une (petite) partie du trafic reste inaccessible, et le déchiffrement ne fonctionnera qu’au prix d’une bidouille relativement bancale dans tls-dissector.

En parcourant un peu internet, on réalise qu’on est passé à coté d’une solution beaucoup plus simple et élégante : Cette solution consiste à forcer libssl à logger les secrets avec les fonctions prévues pour cela !

Un peu plus haut, nous avons parlé de la variable d’environnement SSLKEYLOGFILE qui, lorsqu’elle existe, précise le chemin d’un fichier où les secrets doivent être écrits.

Cette variable d’environnement est utilisé dans ce morceau de code :

 const char *keylog_file = getenv("SSLKEYLOGFILE");
  if (keylog_file) {
    g_keylog_file = fopen(keylog_file, "a");
    if (g_keylog_file == nullptr) {
      perror("fopen");
      return false;
    }
    SSL_CTX_set_keylog_callback(ctx.get(), KeyLogCallback);
  }

Lorsqu’elle est présente, la fonction SSL_CTX_set_keylog_callback est appelée et positionne une callback de dump de secret dans l’objet SSL_CTX.

La callback en question, KeyLogCallback, est une fonction qui va tout simplement écrire quelque chose quelque part :

static void KeyLogCallback(const SSL *ssl, const char *line) {
  fprintf(g_keylog_file, "%s\n", line);
  fflush(g_keylog_file);
}

Lorsque les fonctions de dérivation de clefs sont appelées, la fonction ssl_log_secret est invoquée :

bool tls13_derive_handshake_secrets(SSL_HANDSHAKE *hs) {
  SSL *const ssl = hs->ssl;
  if (!derive_secret(hs, hs->client_handshake_secret(),
                     label_to_span(kTLS13LabelClientHandshakeTraffic)) ||
      !ssl_log_secret(ssl, "CLIENT_HANDSHAKE_TRAFFIC_SECRET",
                      hs->client_handshake_secret()) ||
      !derive_secret(hs, hs->server_handshake_secret(),
                     label_to_span(kTLS13LabelServerHandshakeTraffic)) ||
      !ssl_log_secret(ssl, "SERVER_HANDSHAKE_TRAFFIC_SECRET",
                      hs->server_handshake_secret())) {
    return false;
  }

  return true;
}

Cette fonction ne fait rien si aucune keylog_callback n’est positionnée. Dans le cas inverse, elle fait appelle à cette callback pour dumper le secret avec le format attendu :

bool ssl_log_secret(const SSL *ssl, const char *label,
                    Span<const uint8_t> secret) {
  if (ssl->ctx->keylog_callback == NULL) {
    return true;
  }

  ScopedCBB cbb;
  Array<uint8_t> line;
  if (!CBB_init(cbb.get(), strlen(label) + 1 + SSL3_RANDOM_SIZE * 2 + 1 +
                               secret.size() * 2 + 1) ||
      !CBB_add_bytes(cbb.get(), reinterpret_cast<const uint8_t *>(label),
                     strlen(label)) ||
      !CBB_add_u8(cbb.get(), ' ') ||
      !cbb_add_hex_consttime(cbb.get(), ssl->s3->client_random) ||
      !CBB_add_u8(cbb.get(), ' ') ||
      // Convert to hex in constant time to avoid leaking |secret|. If the
      // callback discards the data, we should not introduce side channels.
      !cbb_add_hex_consttime(cbb.get(), secret) ||
      !CBB_add_u8(cbb.get(), 0 /* NUL */) ||
      !CBBFinishArray(cbb.get(), &line)) {
    return false;
  }

  ssl->ctx->keylog_callback(ssl, reinterpret_cast<const char *>(line.data()));
  return true;
}

La démarche à suivre est donc :

  • redéfinir une fonction keylog_callback,
  • trouver la fonction initialisant le SSL_CTX,
  • forcer l’appel à set_keylog_callback_func à la fin de cette fonction, afin d’enregistrer notre fonction de callback.

Le script final sera le suivant :

/* on va hooker les appels au constructeur SSL_CTX_new */
const SSL_CTX_new_ptr = Module.getExportByName('libssl.so', 'SSL_CTX_new');
/* on récupère l'adresse de la fonction positionnant la callback de keylog */
const set_keylog_callback_ptr = Module.getExportByName('libssl.so', 'SSL_CTX_set_keylog_callback');
/* on définit la fonction de positionnement de callback avec son adresse et sa signature */
const set_keylog_callback_func = new NativeFunction(set_keylog_callback_ptr, 'void', ['pointer', 'pointer']);

/* on définit une fonction de callback qui affiche la string passée en argument */
var keylog_callback = new NativeCallback((ssl, line) => {
  send(Memory.readCString(line));
}, 'void', ['pointer', 'pointer']);

Interceptor.attach(SSL_CTX_new_ptr, {

    /* à la fin de SSL_CTX_new() on appelle set_keylog_callback pour positionner notre callback */
    onLeave(ssl_ctx) {
        set_keylog_callback_func(ssl_ctx, keylog_callback);
    }
});

On lance une application en y injectant le script et l’on obtient bien tous les secrets voulus :

$ frida -U -f com.fast.free.unblock.secure.vpn -l tls_keylog.js 
     ____
    / _  |   Frida 16.2.1 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to Pixel 7a (id=3C221JEHN01971)
Spawned `com.fast.free.unblock.secure.vpn`. Resuming main thread!       
[Pixel 7a::com.fast.free.unblock.secure.vpn ]-> message: {'type': 'send', 'payload': 'CLIENT_HANDSHAKE_TRAFFIC_SECRET cf3e0ab582224ff776f36b8c4292f83e2367dce8272a2d6a4708a3b0f8b8b3fa bb98e59d1279313964dc3078260233ac458acbc804d30e0716db772028dd7eb4'} data: None
message: {'type': 'send', 'payload': 'SERVER_HANDSHAKE_TRAFFIC_SECRET cf3e0ab582224ff776f36b8c4292f83e2367dce8272a2d6a4708a3b0f8b8b3fa 2c47469c09079d027e448675f4fbadb165200984e93e05f5cd1da5c3f6332831'} data: None
message: {'type': 'send', 'payload': 'CLIENT_TRAFFIC_SECRET_0 cf3e0ab582224ff776f36b8c4292f83e2367dce8272a2d6a4708a3b0f8b8b3fa 9f25837280e3e9878efe352bf8deafb00bcf4bcb22f897a01385a8a30e2c2f24'} data: None
message: {'type': 'send', 'payload': 'SERVER_TRAFFIC_SECRET_0 cf3e0ab582224ff776f36b8c4292f83e2367dce8272a2d6a4708a3b0f8b8b3fa b0092a784fc0f8f5fded3acc6cdd1c84080615dd6fea6bab13bc190e7b63678a'} data: None
message: {'type': 'send', 'payload': 'EXPORTER_SECRET cf3e0ab582224ff776f36b8c4292f83e2367dce8272a2d6a4708a3b0f8b8b3fa fa8913bed3c0a5f3a08c5825af19fc14c68950778d06b5043f73c61c8000626b'} data: None
message: {'type': 'send', 'payload': 'CLIENT_RANDOM 8d552fb06b13539565a9e52ee008c9d29514da662c8b395fc5aadf052b381834 7d758cb230c58591e24b1a147bff434e826863276536265926df7e3c638cb7d01c4ed766d109851b72563df705a92ee3'} data: None
message: {'type': 'send', 'payload': 'CLIENT_RANDOM be1d9ef8bc91fb097517d61363936d69f70aa0111bafe8396dcab03e811b4d60 59004c8fe302a8fed74fc9ee5add870063cd716414024874398a76a433b295d16770a23bfa14b50e50057cdc3247e7d5'} data: None
message: {'type': 'send', 'payload': 'CLIENT_HANDSHAKE_TRAFFIC_SECRET 489d092d48d9a6cddb16e45b195ca3a6c5a49dbc703bdac0420b41fb9f77d3fb 0ba791322fd4abe7903ef65ea239c3902604bb1c82ddcd8ef025d33dca2dd3b7'} data: None
message: {'type': 'send', 'payload': 'SERVER_HANDSHAKE_TRAFFIC_SECRET 489d092d48d9a6cddb16e45b195ca3a6c5a49dbc703bdac0420b41fb9f77d3fb aef81d648d9d19e514f72d4dcb50ca883203cc56a41a8de11ff7f2cfddac767e'} data: None
message: {'type': 'send', 'payload': 'CLIENT_TRAFFIC_SECRET_0 489d092d48d9a6cddb16e45b195ca3a6c5a49dbc703bdac0420b41fb9f77d3fb 9749dd129388c3b946fc4e96c19ee33c6520e6cfa6025701810050af35c66e11'} data: None
message: {'type': 'send', 'payload': 'SERVER_TRAFFIC_SECRET_0 489d092d48d9a6cddb16e45b195ca3a6c5a49dbc703bdac0420b41fb9f77d3fb ea6e1a671f87086ad814a5a88fb855ea9a55323f72bb9cf4a403251027da7106'} data: None
message: {'type': 'send', 'payload': 'EXPORTER_SECRET 489d092d48d9a6cddb16e45b195ca3a6c5a49dbc703bdac0420b41fb9f77d3fb 59e0e1b6cf6a4257c2a950f882b265628f7b2621d0d8211abb7d9415f9684562'} data: None
message: {'type': 'send', 'payload': 'CLIENT_RANDOM d813b9719d2d4a7f4cbf8e5de7bcfdeea9cfab7a5f74f70123f246d0dc29943b 62b74269a2108379202c9434a7f13a434e9edf37a9408cd43d40e77ff99b4bfe5137a52ef887779091768811e8e20a02'} data: None
message: {'type': 'send', 'payload': 'CLIENT_RANDOM 0f9741ca59a5a47dca1ba35736dee6ea808304487b8013db76ec9a39278fe16e 57b054fe8c832ee3b47470e323b16f17ccea24a035b22b0f1ade44d80c18e52491a238f2be7f2c93e1528c4b361928ac'} data: None
message: {'type': 'send', 'payload': 'CLIENT_RANDOM 26c00e713a14a5092541b3b05cc7e7a1f8b6e49cb07173184f732e059b53e218 f3d74023cdc293d9b5f3a32cf33386871eaec21a5abbeb3bc8ba808e36b015ff735d813cbcec82d4846880fdfc8f2959'} data: None
message: {'type': 'send', 'payload': 'CLIENT_RANDOM 0c5622a7cdfd1a0e0aa9fe926bfd6e91c3a934a68343c89a3445cc92ef627397 c65f78f73bac31e7857adb5e6ce100d9f904884bcaef11d4cd157e0a7dc30e75739719c73beafc582e2506f059182054'} data: None
message: {'type': 'send', 'payload': 'CLIENT_RANDOM 2cabf9b1ca4523b8481f07dc4c2d7ee9b78b386aeda4f98bb6d2e0a17eae3ad3 59004c8fe302a8fed74fc9ee5add870063cd716414024874398a76a433b295d16770a23bfa14b50e50057cdc3247e7d5'} data: None
message: {'type': 'send', 'payload': 'CLIENT_RANDOM ef6d5d44239990c6f7aed7a6518a7845f12b064751ddd778aeceb20798a556e0 59004c8fe302a8fed74fc9ee5add870063cd716414024874398a76a433b295d16770a23bfa14b50e50057cdc3247e7d5'} data: None

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *