Neutron, notre moteur d’IA, a obtenu un score de 96,75 % sur le benchmark CyberGym de l’UC Berkeley. En savoir plus

Sécurité

Sécurité

Sécurité de Flutter : rétro-ingénierie et analyse

Article sur les techniques d'analyse statique et dynamique pour la rétro-ingénierie des applications Flutter. Il aborde le patching du runtime, l'interception d'une ABI personnalisée et l'interception du trafic.

Introduction

Flutter, développé par Google, est un framework multiplateforme très répandu pour le développement d'applications mobiles, qui prend aussi en charge les applications web et de bureau. Il connaît une croissance significative, avec une hausse de 340 % et de 270 % sur les marketplaces Android et iOS respectivement.

Le framework est reconnu pour ses hautes performances, attribuées au moteur de rendu Skia. Il offre aussi un système d'interface flexible pour des conceptions d'interface complexes.

Flutter peut s'intégrer à des applications existantes et servir à concevoir des applications de zéro.

La plateforme bénéficie d'une communauté solide et active, qui contribue à sa croissance rapide. En raison de sa nouveauté, Flutter est actuellement peu étudié sur le plan de la sécurité, et peu de travaux ont été menés sur l'analyse de sécurité des applications mobiles.

Même si Flutter est populaire, il n'est pas à l'abri des risques de sécurité, ce qui souligne le besoin de meilleurs outils et d'une meilleure connaissance du framework. Cela passe notamment par l'apprentissage de la décomposition et de l'étude des applications Flutter pour trouver et comprendre d'éventuels problèmes de sécurité.

Outils et techniques

Pour analyser une application Flutter, deux catégories d'outils sont disponibles : les outils d'analyse statique et les outils d'analyse dynamique.

Les outils d'analyse statique, comme Doldrums, analysent le fichier en réimplémentant son format. Doldrums ne prend en charge que partiellement les anciennes versions de Flutter (2.10 et 2.12 ; la dernière version au moment de la rédaction de cet article est la 3.10), et n'est plus maintenu en raison des difficultés liées à l'évolution rapide et continue du runtime.

Les outils d'analyse dynamique, en revanche, comme reFlutter, patchent le runtime Flutter pour collecter des informations pendant l'exécution. Ces outils nécessitent souvent des utilitaires complémentaires comme Frida ou un débogueur comme LLDB pour instrumenter les fonctions identifiées dynamiquement.

Malgré son approche puissante pour analyser les applications Flutter, reFlutter ne prend plus en charge les dernières versions du runtime. Les changements apportés au runtime Flutter ont rendu les patchs inopérants.

Structure d'une application Flutter

Sur toutes les plateformes, les applications Flutter se composent d'une application d'enveloppe (wrapper), du runtime Flutter et de l'application Flutter.

Voici un exemple d'arborescence d'une application Linux. libapp.so est le code de l'application, counter est le wrapper, et libflutter_linux_gtk.so est le runtime.

├── counter
├── data
│   ├── flutter_assets
│   │   ├── AssetManifest.json
│   │   ├── FontManifest.json
│   │   ├── fonts
│   │   │   └── MaterialIcons-Regular.otf
│   │   ├── NOTICES.Z
│   │   ├── packages
│   │   │   └── cupertino_icons
│   │   │       └── assets
│   │   │           └── CupertinoIcons.ttf
│   │   ├── shaders
│   │   │   └── ink_sparkle.frag
│   │   └── version.json
│   └── icudtl.dat
└── lib
    ├── libapp.so
    └── libflutter_linux_gtk.so

L'application wrapper fournit un point d'entrée qui se coordonne avec le système d'exploitation sous-jacent pour accéder à des services comme les surfaces de rendu, l'accessibilité et les entrées, et gère la boucle d'événements de messages.

Le runtime Flutter est chargé de charger l'application Flutter et offre un ensemble de fonctionnalités à l'application. Il comprend la machine virtuelle Dart (VM) et le moteur Flutter, ce dernier étant principalement écrit en C++ et prenant en charge les primitives nécessaires à toutes les applications Flutter.

Voici une liste de fonctions exposées par le runtime Flutter :

000000000041a860 g    DF .text  00000000000001e8  Base        fl_binary_messenger_send_response
000000000041f740 g    DF .text  0000000000000043  Base        fl_json_message_codec_new
000000000042a1f0 g    DF .text  000000000000015c  Base        fl_method_channel_invoke_method
000000000042a390 g    DF .text  000000000000012b  Base        fl_method_channel_invoke_method_finish
000000000042b410 g    DF .text  000000000000007c  Base        fl_method_success_response_get_result
0000000000437a00 g    DF .text  0000000000000050  Base        fl_view_new
000000000041b890 g    DF .text  000000000000007c  Base        fl_dart_project_get_icu_data_path
0000000000e8f8d8 g    D  .bss   0000000000000000  Base        _end
000000000041b990 g    DF .text  00000000000000a6  Base        fl_dart_project_set_dart_entrypoint_arguments
00000000004364c0 g    DF .text  000000000000003d  Base        fl_value_get_int
00000000004365f0 g    DF .text  000000000000003d  Base        fl_value_get_int32_list
00000000004133d0  w   DF .text  0000000000000005  Base        operator delete(void*, std::align_val_t)
00000000004192c0 g    DF .text  00000000000001c1  Base        fl_basic_message_channel_new
00000000004198d0 g    DF .text  000000000000015d  Base        fl_basic_message_channel_send
0000000000419a70 g    DF .text  000000000000012b  Base        fl_basic_message_channel_send_finish
000000000042d980 g    DF .text  0000000000000080  Base        fl_plugin_registry_get_type
000000000041be60 g    DF .text  0000000000000045  Base        fl_engine_new_headless
0000000000436480 g    DF .text  000000000000003f  Base        fl_value_get_bool
00000000004297b0 g    DF .text  000000000000007c  Base        fl_method_call_get_args
000000000042ae40 g    DF .text  000000000000003b  Base        fl_method_success_response_get_type
0000000000434f70 g    DF .text  0000000000000160  Base        fl_texture_registrar_mark_texture_frame_available
000000000041f790 g    DF .text  0000000000000176  Base        fl_json_message_codec_encode
000000000042d320 g    DF .text  0000000000000154  Base        fl_plugin_registrar_get_messenger
00000000004368c0 g    DF .text  0000000000000073  Base        fl_value_set
00000000004293f0 g    DF .text  00000000000000be  Base        fl_message_codec_decode_message

Le moteur est chargé de tâches comme la rastérisation des scènes composées, l'implémentation de bas niveau de l'API centrale de Flutter, la gestion des E/S de fichiers et du réseau, la prise en charge de l'accessibilité, l'architecture de plugins, ainsi qu'un runtime Dart et sa chaîne de compilation. Le runtime est fixe et n'inclut aucun code propre à l'application.

L'application Flutter est une bibliothèque autonome, mais elle ne se charge pas comme une bibliothèque standard. Elle suit à la place un format personnalisé contenant la disposition de l'application, son code et ses objets.

readelf -Ws libapp.so 

Symbol table '.dynsym' contains 6 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000001ec000 19904 OBJECT  GLOBAL DEFAULT    7 _kDartVmSnapshotInstructions
     2: 00000000001f0dc0 0x2e0420 OBJECT  GLOBAL DEFAULT    7 _kDartIsolateSnapshotInstructions
     3: 0000000000000200 33728 OBJECT  GLOBAL DEFAULT    2 _kDartVmSnapshotData
     4: 00000000000085c0 0x1dfff0 OBJECT  GLOBAL DEFAULT    2 _kDartIsolateSnapshotData
     5: 00000000000001c8    32 OBJECT  GLOBAL DEFAULT    1 _kDartSnapshotBuildId

Le fichier de l'application contient deux snapshots : un pour l'isolate de la VM et un autre pour l'isolate qui porte le contenu réel. Chacun de ces isolates est divisé en une section de données (qui contient le heap de l'isolate) et une section d'instructions (qui contient le code compilé nativement).

Flutter utilise le modèle d'isolates de Dart, dans lequel chaque morceau de code Dart s'exécute au sein d'un isolate, une structure composée d'un bloc de mémoire appelé son heap. Dans Flutter, les isolates multiples ne sont pas exploités : un seul isolate est utilisé, en dehors de l'isolate de la VM, toujours présent.

L'isolate de la VM est un isolate spécial : il ne gère que des objets immuables et reste accessible aux autres isolates.

Un snapshot Dart, dans le contexte d'une application Flutter, représente l'état sérialisé de la VM Dart à un moment précis de son exécution, y compris tout le code compilé nativement. Dans Flutter, le snapshot de l'isolate correspond à l'état de la VM Dart juste avant l'appel de main.

Mise en place et patching du runtime

Pour analyser une application Flutter, notre objectif est de s'appuyer sur une approche robuste et maintenable. Au lieu de réimplémenter le format de fichier, nous modifierons le SDK Dart afin d'extraire les informations nécessaires de l'application Flutter à l'exécution.

Lorsque vous compilez une application Flutter, deux fichiers sont générés dans le répertoire de votre application :

texte alternatif

  • libFlutter.so : ce fichier est une bibliothèque partagée contenant le moteur Flutter, chargé du rendu de l'interface, de la gestion des événements d'entrée et de la gestion du cycle de vie de l'application.
  • libapp.so : ce fichier contient le code et la logique de l'application proprement dits, qui constituent l'application Flutter.

Les deux fichiers incluent un snapshot_hash qui distingue les versions de build. Le snapshot_hash sert d'identifiant unique pour les versions de build. Si le snapshot_hash diffère entre les fichiers libFlutter.so et libapp.so, une erreur d'incompatibilité de version se produit et empêche l'application de démarrer.

Il est possible de patcher et de construire une version précise de Flutter en suivant ces étapes :

Créer un patch pour une version précise de Flutter.

Pour créer un patch pour une version de Flutter, nous devons déterminer la version précise du SDK Dart utilisée dans cette version. Vous pouvez lister toutes les versions officielles de Flutter avec le lien : https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json

Par exemple, Flutter v3.10.4 utilise le SDK Dart v3.0.3 :

texte alternatif

La première étape consiste à créer un fichier de patch pour cette version précise du SDK Dart :

git clone https://github.com/dart-lang/sdk
cd sdk
git checkout 2.13.4

Nous modifions maintenant le code source du SDK Dart pour extraire des informations de l'application Flutter pendant l'exécution ; dans la section suivante, nous détaillerons ces changements. Une fois tous les changements effectués, créez un fichier de patch avec vos modifications en exécutant :

git diff > patch_2_13_4.patch

Et conservez le fichier de patch pour plus tard.

2 - Construire libFlutter.so avec notre SDK Dart patché.

Clonez le dépôt contenant les outils requis, comme le système de build Ninja et l'outil de gestion des dépendances gclient, et ajoutez-le à votre PATH.

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
cd depot_tools
export PATH=$PATH:$(pwd)/depot_tools

Pour nous assurer de disposer de la bonne version du moteur Flutter pour une version précise de Flutter, nous repérons le fichier /bin/internal/engine.version dans le code de Flutter, au même hash de commit que la version. Ouvrez le fichier et identifiez le hash de commit qu'il mentionne. Ce hash de commit représente la version précise du moteur Flutter utilisée par cette version de Flutter.

Par exemple, prenons la version v3.10.4 de Flutter, dont le hash de commit associé est 682aa387cfe4fbd71ccd5418b2c2a075729a1c66.

Consultez l'URL suivante Engine.version pour obtenir le moteur utilisé avec la version 3.10.4 de Flutter, qui est 2a3401c9bbb5a9a9aec74d4f735d18a9dd3ebf2d.

texte alternatif

Clonez le dépôt du moteur Flutter et faites un checkout vers le même hash de commit que celui de la version de Flutter souhaitée.

git clone https://github.com/flutter/engine.git
cd engine
git fetch origin 2a3401c9bbb5a9a9aec74d4f735d18a9dd3ebf2d
git reset --hard

Préparez un répertoire pour utiliser gclient et créez un fichier de configuration .gclient.

mkdir costum_engine
cd customEngine
echo 'solutions = [{"managed": False,"name": "src/flutter","url": "PATH_TO_ENGINE'","custom_deps": {},"deps_file": "DEPS","safesync_url": "",},]' > .gclient

Exécutez gclient et attendez qu'il termine :

cd customEngine
gclient sync

gclient clonera toutes les dépendances du fichier DEPS dans le moteur Flutter, y compris le SDK Dart depuis le dépôt du SDK Dart, dans notre dossier costum_engine/src/third_party/dart/.

Toute modification du code source du SDK Dart, à la compilation, produira un snapshot_hash différent, ce qui peut conduire à un libflutter.so inutilisable, qui ne fonctionne avec aucun libapp.so.

Le snapshot_hash est généré à la compilation par la méthode MakeSnapshotHashString() dans le script make_version.py.

Avant de modifier le code source, nous devons nous assurer que make_version.py génère toujours le bon snapshot_hash, même si nous avons modifié le code source.

Générez le snapshot_hash d'origine avec le script make_version.py.

cd costum_engine/src/third_party/dart/tool
ipython
In [1]: import make_version
In [2]: make_version.MakeSnapshotHashString()
Out[2]: 'e4a09dbf2bb120fe4674e0576617a0dc'

Ouvrez le script make_version.py dans un éditeur de texte et modifiez-le pour qu'il renvoie le snapshot_hash d'origine.

code make_version.py

texte alternatif

Nous pouvons maintenant modifier le code source sans craindre une incohérence de snapshot_hash. Copiez votre fichier de patch dans le dossier du SDK Dart et appliquez-le :

git apply patch_2_13_4.patch

Vous pouvez maintenant construire Flutter pour n'importe quel système d'exploitation ou n'importe quelle architecture pris en charge :

costum_engine/src/flutter/tools/gn --no-goma --android --android-cpu=arm64 --runtime-mode=release 

ninja -C costum_engine/src/out/android_release_arm64

Une fois terminé, vous trouverez le libFlutter.so patché dans le dossier custom_engine/src/out/android_release_arm64/lib.stripped/libflutter.so.

Extraction des fonctions, des classes et des objets

Pour extraire des informations sur la structure de l'application Flutter, la liste des bibliothèques, les fonctions et leur emplacement, reFlutter patche un objet de table de classes afin de les lister. Cet objet ne peut plus être utilisé, car la liste des fonctions est élaguée dans les versions récentes.

Bien que l'information soit élaguée dans cette classe, elle reste présente dans l'application au moment du chargement. Avec un astérisque, mais nous y reviendrons dans un autre article.

Dans les cas normaux, nous pouvons toujours lister les fonctions Flutter, les classes et leur offset à partir du parseur Flutter. Pour cela, nous devrons patcher la fonction PostLoad de la classe FunctionDeserializationCluster.

Voici un exemple du patch à appliquer pour extraire les offsets des fonctions au format JSON.

   void PostLoad(Deserializer* d, const Array& refs, bool primary) {
+    OS::Print("Patch: Function List START\n");
     if (d->kind() == Snapshot::kFullAOT) {
       Function& func = Function::Handle(d->zone());
       for (intptr_t i = start_index_, n = stop_index_; i < n; i++) {
         func ^= refs.At(i);
         auto const code = func.ptr()->untag()->code();
         ASSERT(code->IsCode());
+
+        auto& rCode = Code::Handle(code);
+        auto& rClass = Class::Handle(func.Owner());
+        auto& rLib = Library::Handle(rClass.library());
+        auto& rlibName = String::Handle(rLib.url());
+
+        JSONWriter js;
+        // Open empty object so output is valid/parsable JSON.
+        js.OpenObject();
+
+        js.PrintProperty("method_name", func.UserVisibleNameCString());
+        js.PrintProperty("offset", offset);
+
+        auto& sig = String::Handle(func.InternalSignature());
+        js.PrintProperty("library_url", rlibName.ToCString());
+        js.PrintProperty("class_name", rClass.UserVisibleNameCString());
+        js.CloseObject();
+
+        char* buffer = nullptr;
+        intptr_t buffer_length = 0;
+        js.Steal(&buffer, &buffer_length);

Le patch lit l'URL de la bibliothèque, le nom de la classe et le nom de la méthode. D'autres informations peuvent aussi être extraites, comme la signature de la méthode, mais elle n'est pas toujours présente.

L'offset renvoyé sera une adresse absolue qui varie selon l'adresse de chargement de la bibliothèque.

Pour n'afficher que l'offset correspondant dans le binaire, il est possible d'appliquer le patch suivant :

   code->untag()->monomorphic_entry_point_ = monomorphic_entry_point;
-  code->untag()->monomorphic_unchecked_entry_point_ =
-      monomorphic_entry_point + unchecked_offset;
+
+  auto& offset =
+      instructions_table_.rodata()
+          ->entries()[instructions_table_.rodata()->first_entry_with_code +
+                      instructions_index_ - 1]
+          .pc_offset;
+  code->untag()->monomorphic_unchecked_entry_point_ = offset;
+  //  OS::Print("Patch: Offset 0x%016lx\n", monomorphic_entry_point + unchecked_offset);

Une fois les offsets extraits au démarrage, il est possible de les intercepter. Les offsets peuvent être affichés dans les journaux ; cela peut toutefois poser problème, car les journaux ont une limite de taille qui entraîne souvent la troncature des données.

Écrire les fichiers sur le disque est une solution plus stable. Pour écrire dans un fichier externe, vous pouvez ajouter le patch suivant, qui tente d'extraire vers différents emplacements de fichiers afin de s'adapter aux dispositions du système de fichiers de différents téléphones.

+        for (const auto& path : PATHS) {
+          OS::Print("Using Path %s\n", path);
+          std::FILE* file;
+          // Write to the file
+          file = std::fopen(path, "a");
+          if (file != NULL) {
+            std::fwrite(buffer, sizeof(char), buffer_length, file);
+            std::fwrite("\n", sizeof(char), 1, file);
+            std::fclose(file);
+            OS::Print("Successfully wrote to the file '%s'.\n", path);
+          } else {
+            OS::Print("Failed to open the file '%s' for writing.\n", path);
+          }
+        }
+

Une fois la liste des fonctions et de leurs offsets collectée, nous pouvons commencer à les intercepter.

Analyse dynamique et ABI personnalisée de Flutter

Pour intercepter les appels de fonctions sur Flutter, un obstacle majeur est son utilisation d'une implémentation d'ABI personnalisée.

L'ABI dicte la compatibilité binaire et couvre la convention d'appel, les types de données, la taille et l'alignement, l'interface d'appels système, le name mangling, la gestion des exceptions et le format de fichier.

Dart utilise une convention d'appel personnalisée. La convention d'appel pour arm64, par exemple, est définie dans le fichier sdk/runtime/vm/constants_arm64.h.

// Register aliases.
const Register TMP = R16;  // Used as scratch register by assembler.
const Register TMP2 = R17;
const Register PP = R27;  // Caches object pool pointer in generated code.
const Register DISPATCH_TABLE_REG = R21;  // Dispatch table register.
const Register CODE_REG = R24;
const Register FPREG = FP;          // Frame pointer register.
const Register SPREG = R15;         // Stack pointer register.
const Register ARGS_DESC_REG = R4;  // Arguments descriptor register.
const Register THR = R26;           // Caches current thread in generated code.
const Register CALLEE_SAVED_TEMP = R19;
const Register CALLEE_SAVED_TEMP2 = R20;
const Register BARRIER_MASK = R28;
const Register NULL_REG = R22;  // Caches NullObject() value.
const Register HEAP_BASE = R23;

Pour intercepter une fonction et lire ses arguments avec Frida ou avec un débogueur comme LLDB, nous devons d'abord obtenir le pointeur d'argument puis, selon le type de l'argument, que nous devons connaître au préalable ou que nous pouvons parfois extraire des métadonnées de signature de la fonction, déterminer la disposition mémoire de l'objet argument à lire.

Voici le code JS de Frida qui prend en charge l'extraction des arguments et la lecture de la valeur d'une chaîne. Les booléens et les entiers sont des valeurs simples, qui peuvent facilement être récupérées de la même manière :

...
                    const argPointer = dartGetArguments(this.context, index)
                    const stringValue = getDartStringData(argPointer)

...

/**
 * Get Dart Arguments.
 *
 * Arguments are passed to the custom function stack pointed at by register X15 on ARM.
 *
 * @param context Frida context giving access to register values.
 * @param argIndex Argument index to determine which offset is the arg pointer.
 * @returns {*} Argument pointer.
 */
function dartGetArguments(context, argIndex) {
    // RSP on x64, see constants_x64.h at Dart VM repo SPREG value.
    const x15 = context.x15;
    return x15.add(8 * argIndex).readPointer();
}

/**
 * Read SMI (Small Integer) from pointer.
 * @param smiPtr SMI Pointer,
 * @returns {*|null} Value.
 */
function readSMI(smiPtr) {
    let smi_data = smiPtr.readU64();
    if (parseInt(smi_data & 0x1, 10) === 0) {
        return smi_data >> 1;
    }
    console.log(
        `Invalid SMI pointer ${smiPtr} -> 0x${smi_data.toString(16)}: Smi LSB should be 0`)
    return null
}

/**
 * Parse String Dart object to extract value.
 * @param dartStringPtr Dart String pointer.
 * @returns {null|*[]} Tuple of parsed data.
 */
function parseDartString(dartStringPtr) {
    if (dartStringPtr.and(0x1).toInt32() === 1) {
        dartStringPtr = dartStringPtr.sub(1)
    }
    const tag = dartStringPtr.readU32();
    const classId = (tag >> 16) & 0xffff;
    if (classId === 0x5 || classId === 0x55) {
        let stringLength = readSMI(dartStringPtr.add(8));
        let stringDataStr = dartStringPtr.add(16)
        let stringData = stringDataStr.readCString(stringLength);
        return [stringDataStr, stringLength, stringData]
    }
    return null
}

/**
 * Read Dart string at pointer.
 * @param dartStringPtr Dart string pointer.
 * @returns {*|null} String value.
 */
function getDartStringData(dartStringPtr) {
    let dartStringInfo = parseDartString(dartStringPtr);
    if (dartStringInfo != null) {
        return dartStringInfo[2];
    }
    return null;
}

Maintenant que nous avons créé un patch, construit une version patchée du runtime et disposons d'un script capable de lire les fonctions et leurs arguments, la dernière étape consiste à reconditionner notre application cible, qu'il s'agisse d'Android ou d'iOS, à la démarrer et à y attacher notre outil d'instrumentation préféré.

Savoir quelles fonctions instrumenter est un vaste sujet qui sera traité dans un prochain article. Nous aborderons alors le SDK Dart et ses plus de 120 k méthodes, ainsi que certains des risques observés dans ses 1000 packages les plus populaires.

Interception du trafic

Le trafic Flutter est un cas à part qui demande un traitement particulier.

Dans les applications Android ou iOS classiques, il est possible d'intercepter le trafic et de contourner le TLS pinning avec diverses méthodes, comme modifier la politique de sécurité réseau pour accepter le certificat personnalisé, hooker SSL_read et SSL_write pour intercepter le trafic avant son chiffrement, ou extraire la clé de session TLS pour déchiffrer le trafic.

Flutter n'utilise pas la pile TLS native ; il n'est donc pas possible de définir un proxy ni d'intercepter le trafic. La bibliothèque TLS est compilée statiquement dans le runtime Flutter, et il est donc difficile de l'identifier et de la patcher dynamiquement.

Pour intercepter le trafic sur Flutter, la solution la plus robuste est, là encore, de patcher le runtime Flutter pour désactiver la validation des certificats TLS et définir un proxy personnalisé.

Cette approche est déjà utilisée dans l'outil reFlutter et continue de fonctionner dans les versions actuelles du runtime Flutter.

Dans Socket.cc, nous forçons l'adresse IP et le port vers ceux utilisés pour intercepter le trafic :

        void FUNCTION_NAME(Socket_CreateConnect)(Dart_NativeArguments args) {
            RawAddr addr;
            SocketAddress::GetSockAddr(Dart_GetNativeArgument(args, 1), &addr);
            Dart_Handle port_arg = Dart_GetNativeArgument(args, 2);
            int64_t port = DartUtils::GetInt64ValueCheckRange(port_arg, 0, 65535);
+ if (port > 50) {
+     port = 8083;
+     addr.addr.sa_family = AF_INET;
+     addr.in.sin_family = AF_INET;
+     inet_aton("192.168.10.5", &addr.in.sin_addr);
+ }
+ ...

Nous désactivons ensuite les contrôles de certificat dans ssl_crypto_x509_session_verify_cert_chain :

static bool ssl_crypto_x509_session_verify_cert_chain(SSL_SESSION *session,
                                          SSL_HANDSHAKE *hs,
                                          uint8_t *out_alert) {
+ return true;

Après ces changements, nous utilisons le proxy pour intercepter le trafic sans installer aucun certificat sur l'appareil. Nous devons toutefois nous assurer que le mode proxy invisible est utilisé.

Conclusion

Dans cette première partie de l'article, nous avons passé en revue la disposition d'une application Flutter, la façon de patcher le runtime, comment intercepter les appels de fonctions et lire leurs arguments, et enfin comment patcher le runtime pour intercepter le trafic.

Les 2 prochains articles aborderont les problèmes de sécurité à évaluer dans l'application Dart ; les problèmes notables sont les vulnérabilités de corruption de mémoire avec dart:ffi, les problèmes d'interopérabilité avec dart:jni ou dart:js, la réflexion, et les problèmes de sérialisation, que ce soit dans le code natif ou dans le code Dart avec Reflectables.

Nous aborderons aussi les problèmes de sécurité connus dans les packages dart populaires, dont il faut tenir compte lors du développement de votre application.