Il est utile de savoir comment analyser le flux réseau entier d’un terminal Android. C'est notamment pratique pour déterminer les API consommées par une application inconnue.
Lorsqu’on développe notre propre application, Android Studio nous facilite la vie avec un outil qu’il embarque. Il s’agit d’App Inspection, qui offre la possibilité d’intercepter le réseau d’une application en développement :

En version zoomée :

On a les informations minimales nécessaires : temps de requête, contenu des headers, body, etc.
Le fonctionnement du debug sur Android
ADB (Android Debug Bridge)
En fait, Android Studio nous mâche beaucoup le travail en développement : il met en place seul une redirection des flux réseaux vers notre machine via ADB. Par défaut, une app installée depuis le playstore ne permet pas à Android Studio de s'y brancher. C'est parce qu'elle est normalement en mode release.
Pour que cela fonctionne en développement, il faut d’abord que ADB soit activé. Vous pouvez le faire via les options développeur. ADB est désactivé par défaut pour des raisons de sécurité.
Ensuite, il faut que votre application soit elle-même en mode debug. Là encore, les projets Android cachent un peu de magie dans la mesure où au démarrage de chaque projet nous avons à disposition deux buildTypes : debug et release.
Mais en regardant dans la configuration du projet, dans le fichier build.gradle, on ne voit la définition que du buildType de release.
Là aussi, Android Studio nous mâche le travail et ajoute automatiquement un build type debug dans notre configuration.
Ce build type debug contient une instruction de build particulière : isDebuggable = true.
C’est le déclencheur pour pouvoir attacher un debugger à notre app. En d’autres termes, si vous voulez déclarer un autre build type qui doit aussi être debuggable, voici comment vous le ferez :
android {
buildTypes {
create("customDebugType") {
isDebuggable = true
...
}
}
}
Référez-vous à la documentation officielle pour plus d’informations.
L’ajout de ce paramètre indique qu’il faut ajouter un flag particulier au build dans le fichier AndroidManifest.xml de votre application :
<application android:debuggable="true" />
Ce flag est appliqué dans une sous-étape du build Android, le merge de manifest. Une application peut avoir plusieurs fichiers AndroidManifest.xml dans ses sources. Pensons déjà à celui de l’application et ceux de chaque bibliothèque android qu’elle intègre. À l’issue de cette étape, ils sont fusionnés en un seul fichier AndroidManifest.xml. Ce dernier fichier est embarqué dans l’archive applicative à installer.
La signature de l’application
Lorsque l’application est signée en debug, le certificat utilisé est un certificat générique généré par l’outillage de build. Il est dans le keystore de debug. Vous pouvez le trouver à une adresse générique ici : ~/.android/debug.keystore .
Il utilise aussi un mot de passe par défaut : android.

Au démarrage de l’application
Zygote est un des processus clés d’Android. C’est un processus racine du système et des applications.
En fait, Zygote est initialisé par init, le processus père de tous les processus dans un noyau linux (Android reposant sur ce noyau). Lorsqu’une application démarre, Zygote se fork pour isoler l’application dans un nouveau processus. Le fork présente deux avantages :
- 1) du temps est économisé puisqu’il n’y a pas besoin de recharger certaines ressources.
- 2) Des pages mémoires sont partagées en lecture seule entre les apps pour économiser de la mémoire
Ce qui nous intéresse, c’est lorsque une application est installée, le fichier AndroidManifest.xml est lu par un sous-processus lancé par Zygote : le PackageManager. Des metadatas sont extraites et sauvegardées dont le flag debuggable qui est injecté.
Lorsque l’app doit être lancée, un autre sous-processus, l’ActivityManager, récupère les metadatas et envoie une demande de fork à zygote avec le flag --enable-jdwp, le Java Debug Wire Protocol (JWDP), ce qui permet d’attacher un debugger à l’application.
Si on résume dans un diagramme, voici ce qu’il se passe :

Via JDWP, Android Studio attache le debugger et intercepte les flux de l’application. On a donc les accès sur ce qui nous intéresse : le trafic réseau.
Le réseau ?
Bien que les flux soient redirigés vers Android Studio, comment est-ce que le trafic chiffré en HTTPS est déchiffré ? En fait, il n’y a pas vraiment de man in the middle au sens déchiffrement HTTPS à la volée. Le network inspector va venir se brancher sur des entrées et sorties de certaines classes qui sont dédiées aux échanges réseaux comme HttpsUrlConnection, OkHttp ou Retrofit. Cela peut se faire grâce à des classes spéciales dans java.lang.Instrument, disponibles avec le debugger, puisqu’elles ont des méthodes pour intercepter et réécrire à chaud les classes utilisées. C’est également faisable via l’ajout de hooks au build via des compiler plugins gradle, par exemple en injectant de manière statique un handler dans URL avec setURLStreamHandlerFactory(URLStreamHandlerFactory fac). Finalement, les objets qui transportent les requêtes et réponses https sont lus en mémoire. Il n’y a pas de déchiffrement du flux HTTPS.
Une autre conséquence directe est que si vous utilisez une bibliothèque qui ne s’appuie pas sur ces classes pour vos échanges réseaux, les flux n’apparaîtront pas dans le network inspector.
Résumé
Un autre diagramme plus haut niveau résume toutes les étapes détaillées ci-dessus :

Pourquoi est-ce plus dur d’intercepter ce qu’il se passe sur les applications que nous ne développons pas ?
Tout d’abord, une application compilée pour la production, typiquement une application que l’on installe sur les stores, n’a pas ce flag debugglable = true. La conséquence directe, c’est que les métadatas qui indiquent à Zygote d’initialiser JDWP sont aussi absentes. Pas de JDWP, pas de possibilité d’attacher un debugger. Impossible alors de se placer sur les entrées et sorties des classes comme OkHttp et donc d’intercepter le réseau.
Ensuite, les échanges sont a priori faits en HTTPS. Donc le flux est chiffré même s’il est intercepté.
Intercepter le flux en utilisant un proxy HTTPS
Une solution, c’est de faire un man in the middle avec un proxy HTTPS. L’idée est de rediriger toutes les requêtes sur ce proxy et de déchiffrer cette fois à la volée tout ce qui est intercepté.
Pour cela, il faut que notre échange HTTPS entre l’app, le proxy et le serveur cible inclut le certificat HTTPS du proxy :

Sur Android, on peut embarquer les certificats dans l’application. Nous avons deux choix : dans le magasin de certificats utilisateur ou dans le magasin de certificats système.
Une complexité s’ajoute lorsque nous n’avons pas la main sur l’application à inspecter : depuis Android 7.0, les certificats dans le magasin de certificats utilisateurs ne sont plus reconnus par défaut par les applications. Cette version d’Android introduit le fichier network_security_config.xml. Ce fichier oblige à spécifier plus finement les certificats et clés HTTPS utilisés par l’application :

Dans le détail de la gestion des règles, référez-vous au diagramme ci-dessous :

Hors configuration spécifique via le fichier network_security_config.xml, les applications continuent à reconnaître par défaut les certificats qui sont dans le magasin de certificats système.
Intercepter le réseau avec proxyman
Proxyman est un proxy https disponible sur macOS. On va s’en servir pour intercepter le réseau. La documentation est bien faite alors on va suivre pas à pas ce qui est indiqué.
Installation de Proxyman
Installer le logiciel depuis https://proxyman.com
Installer le certificat root Proxyman sur Mac
Allez dans Certificate > Install Certificate on this Mac

Ensuite, vous pouvez vous référer au guide d’installation de Proxyman. Allez dans Certificate > Install Certificate on Android > Emulators…

Création d’un émulateur
Créez selon votre besoin un émulateur en API inférieure ou supérieure à 7.0. Gardez cependant en tête que Android 7 est désormais une version très ancienne et de nombreuses applications ont une version minimale supportée plus récente. Vous pouvez le vérifier en décompilant l’application cible et en regardant son AndroidManifest.xml. La version minimale est spécifiée

La version minimale ici est 22, soit Android 5.1.
Configuration de l’émulateur
Pour paramétrer le proxy dans l’émulateur, il faut le lancer en ligne de commandes. Le lancement via Android Studio cache ces paramètres. Pour un émulateur appelé “6” et le binaire emulator situé dans ~/Library/Android/sdk/emulator, voici la commande correspondante :
~/Library/Android/sdk/emulator/emulator -avd 6
Si jamais vous ne savez pas quels sont vos émulateurs disponibles, la commande suivante vous les listera :
~/Library/Android/sdk/emulator/emulator -list-avds
Mettez tous les binaires Android dans votre Path pour ne pas devoir saisir leur chemin absolu.
Configuration du proxy
Il faut maintenant configurer proxyman comme proxy de notre émulateur.
Pour cela, aller dans Proxyman → Setting → Advanced Proxy Setting

Sur la zone à droite vous voyez votre IP et le port d’écoute de Proxyman. C’est ce qu’on va reporter dans les paramètres de notre émulateur.
Allez dans les autres paramètres de l’émulateur (symbolisés par les trois points en bas), puis dans Settings et enfin dans l’onglet proxy.
Renseignez la configuration manuellement et cliquez sur Apply. Le proxy status doit marquer Success.

Installation du certificat Proxyman dans l’émulateur Android < 7.0
Option 1 : téléchargement sur mac de la machine et dépôt dans l’émulateur
Télécharger le certificat via http://proxy.man/ssl. Note : c’est une adresse purement locale. C’est possible que vous deviez redémarrer votre navigateur pour qu’elle soit prise en compte. Proxyman doit aussi être ouvert.
Ensuite, via adb ou Android Studio et le device explorer, déposer le certificat dans le dossier téléchargement de l’émulateur.

Clic droit -> upload file. Sélectionnez votre certificat.
Ensuite allez dans les téléchargements, cliquez sur votre certificat et installez-le (dans VPN et Apps).
Option 2 : téléchargement via l’adresse IP du proxy
Ouvrez le navigateur web. Téléchargez le certificat via l’IP et le port du proxy :
- Dans l’exemple de ce document il s’agit de http://192.168.1.75:9090/ssl
- Une popup système va vous proposer d’installer le certificat
- Installez-le dans la catégorie VPN et Apps
Installation du certificat Proxyman dans l’émulateur Android >= 7.0
Cette option fonctionne aussi pour l’émulateur plus ancien puisque le certificat sera installé dans le magasin de certificats système.
Points d’attention
Les manipulations qui vont être opérées nécessitent un accès root au système. Vous pouvez soit rooter votre téléphone ou votre émulateur via des outils comme Magisk. Sinon, optez pour un une image d’émulateur qui n’est pas un “production build”.
Dans Android Studio, vous les distinguez à la création d’un nouvel émulateur :
- Google Play → Production build, avec accès au playstore, non rootable
- Google Apis → Non production build, pas de playstore mais google apis installées et rootable
- Android Open Source → Non production build, ni playstore ni google apis et rootable

Pour vérifier si votre image est rootable, vous pouvez lancer un terminal et l’émulateur que vous souhaitez tester. Ensuite testez cette commande :
adb root
Le message vous indiquera si vous pouvez être root ou non (exemple : adbd is already running as root)
Exécution du script proxyman
Aller dans Certificats > Install Certificate on Android > Emulators

Le menu suivant va s’ouvrir :

Cliquer sur Override All Emulators. Un terminal va s’ouvrir et lancer le script d’installation du certificat dans le système. Il faut que adb soit dans votre PATH pour que cela fonctionne.
Vous pouvez ensuite vérifier que le certificat Proxyman est bien installé dans les credentials système du téléphone :

Comment fait le script proxyman pour installer le certificat dans ce répertoire système ?
Vous pouvez analyser le script en parcourant les ressources locales de l’application :
/Applications/Proxyman.app/Contents/Frameworks/ProxymanCore.framework/Resources/install_certificate_android_emulator.sh
La fonction clé, c’est __inject_root_certificate() {
Au départ, la fonction calcule le hash du certificat. En effet, les certificats sont stockés sous forme de leur hash dans le système. Par exemple : un mon-certificat.pem pourrait être stocké sous le nom c8450d0p.0
# Get the certificate hash
CERT_HASH=$(openssl x509 -inform PEM -subject_hash_old -in "$proxymanCert" | head -1)
if [ -z "$CERT_HASH" ]; then
echo "❌ Error: Could not calculate certificate hash"
exit 1
fi
Ensuite, le certificat est poussé avec le renommage attendu dans un dossier local du terminal.
# Push the certificate to the device
adb -s "$device" push "$proxymanCert" "/data/local/tmp/$CERT_HASH.0"
Puis, un script est poussé dans ce même dossier local. Il commence par créer un dossier local pour les certificats déjà existants. Ensuite, il cherche le magasin de certificats, qui change selon la version d’Android et la version d’Android packagée et distribuée (cela peut varier selon les constructeur) :
# Create a separate temp directory for current certificates
mkdir -p -m 700 /data/local/tmp/tmp-ca-copy
# Determine the source certificate path based on Android version
if [ -d "/apex/com.android.conscrypt/cacerts" ]; then
CERT_SOURCE="/apex/com.android.conscrypt/cacerts"
elif [ -d "/system/etc/security/cacerts" ]; then
CERT_SOURCE="/system/etc/security/cacerts"
elif [ -d "/system/etc/certificates" ]; then
CERT_SOURCE="/system/etc/certificates"
elif [ -d "/etc/security/cacerts" ]; then
CERT_SOURCE="/etc/security/cacerts"
# Some older Android versions or custom ROMs
elif [ -d "/data/misc/keychain/cacerts-added" ]; then
CERT_SOURCE="/data/misc/keychain/cacerts-added"
elif [ -d "/system/ca-certificates/files" ]; then
CERT_SOURCE="/system/ca-certificates/files"
else
echo "❌ Error: Could not find certificate directory"
echo "Searched in:"
echo "- /apex/com.android.conscrypt/cacerts"
echo "- /system/etc/security/cacerts"
echo "- /system/etc/certificates"
echo "- /etc/security/cacerts"
echo "- /data/misc/keychain/cacerts-added"
echo "- /system/ca-certificates/files"
exit 1
fi
Un backup des certificats existants est effectué. Ensuite, un répertoire temporaire pour les certificats systèmes est monté via tmpfs. L’avantage de la procédure c’est que d’une part les dossiers systèmes sont sensibles et cela évite d'y toucher, d’autre part ces mêmes dossiers systèmes sont le plus souvent en lecture seule. L’ensemble des certificats en backup et le nouveau certificat (celui de proxyman) y sont copiés. Notons quand même qu’avec tmpfs un cold boot de l’émulateur conduira à la suppression du répertoire monté. Et donc à la nécessaire réexécution du script de Proxyman.
Enfin, les permissions et les labels SELinux sont mis à jour. SELinux est une couche additionnelle de sécurité utilisée par Android. Mettre les bons labels via chcon est nécessaire pour que les certificats soient bien considérés par Android.
# Copy out the existing certificates
cp $CERT_SOURCE/* /data/local/tmp/tmp-ca-copy/ 2>/dev/null || true
# Create the in-memory mount on top of the system certs folder
mkdir -p /system/etc/security/cacerts
mount -t tmpfs tmpfs /system/etc/security/cacerts
# Copy the existing certs back into the tmpfs
mv /data/local/tmp/tmp-ca-copy/* /system/etc/security/cacerts/ 2>/dev/null || true
# Copy our new cert in
mv /data/local/tmp/*.0 /system/etc/security/cacerts/
# Update the permissions and selinux context labels
chown root:root /system/etc/security/cacerts/*
chmod 644 /system/etc/security/cacerts/*
chcon u:object_r:system_file:s0 /system/etc/security/cacerts/*
Ensuite, via Zygote dont nous parlions plus haut, le script récupère l’id de processus de Zygote et les id de processus de toutes les applications. Grâce à nsenter, il bind le dossier temporaire créé qui contient tous les certificats (dont le nouveau) à $CERT_SOURCE, le dossier originel. Ainsi, zygote et les applications peuvent voir les certificats.
Après avoir injecté le script et ajouté les droits d’exécution, le script est exécuté.
# Handle Zygote processes
ZYGOTE_PID=$(pidof zygote || true)
ZYGOTE64_PID=$(pidof zygote64 || true)
# Inject into Zygote mount namespaces
for Z_PID in "$ZYGOTE_PID" "$ZYGOTE64_PID"; do
if [ -n "$Z_PID" ]; then
nsenter --mount=/proc/$Z_PID/ns/mnt – \
/bin/mount --bind /system/etc/security/cacerts $CERT_SOURCE
fi
done
# Get PIDs of all Zygote child processes
APP_PIDS=$(
echo "$ZYGOTE_PID $ZYGOTE64_PID" | \
xargs -n1 ps -o 'PID' -P | \
grep -v PID
)
# Inject into app mount namespaces
for PID in $APP_PIDS; do
nsenter --mount=/proc/$PID/ns/mnt -- \
/bin/mount --bind /system/etc/security/cacerts $CERT_SOURCE &
done
wait
EOF
# Make the injection script executable
adb -s "$device" shell "chmod +x $INJECTION_SCRIPT"
# Execute the injection script as root
adb -s "$device" shell "su 0 $INJECTION_SCRIPT"
Puis, chrome et webview pouvant aussi présenter des blocages vis-à-vis des certificats, le script fait aussi en sorte de modifier leur configuration. Le SPKI (Simple Public Key Infrastructure) fingerprint est unique pour chrome et lui permet d’identifier le certificat.
L’astuce c’est que pour chaque binaire chrome, android-webview, webview et content-shell, un fichier qui contient chrome --ignore-certificate-errors-spki-list=$SPKI_FINGERPRINT est créé. En d’autres termes, cela indique à ces binaires d’accepter le certificat spécifié.
Ces instructions sont stockées dans le fichier $CHROME_FLAGS_SCRIPT, qui est exécuté. Chrome est finalement stoppé pour que les nouveaux flags soient pris en compte au prochain démarrage.
# Configure Chrome flags
SPKI_FINGERPRINT=$(openssl x509 -in "$proxymanCert" -pubkey -noout | \
openssl pkey -pubin -outform der | \
openssl dgst -sha256 -binary | \
base64)
CHROME_FLAGS_SCRIPT="/data/local/tmp/chrome_flags.sh"
cat << EOF | adb -s "$device" shell "cat > $CHROME_FLAGS_SCRIPT"
#!/system/bin/sh
FLAGS="chrome --ignore-certificate-errors-spki-list=$SPKI_FINGERPRINT"
for variant in chrome android-webview webview content-shell; do
for base_path in /data/local /data/local/tmp; do
FLAGS_PATH=\$base_path/\$variant-command-line
echo "\$FLAGS" > "\$FLAGS_PATH"
chmod 744 "\$FLAGS_PATH"
chcon "u:object_r:shell_data_file:s0" "\$FLAGS_PATH"
done
done
EOF
# Execute Chrome flags script
adb -s "$device" shell "chmod +x $CHROME_FLAGS_SCRIPT"
adb -s "$device" shell "su 0 sh $CHROME_FLAGS_SCRIPT"
adb -s "$device" shell "su 0 am force-stop com.android.chrome"
Pour terminer, les applications sont toutes stoppées pour que les changements soient appliqués.
# Force stop all apps to apply changes
APPS=$(adb -s $device shell dumpsys window a | grep "/" | cut -d "{" -f2 | cut -d "/" -f1 | cut -d " " -f2)
for APP in $APPS; do
adb -s $device shell am force-stop $APP
done
echo "✅ Certificate injection completed for $device_type
Installation de l’application cible
Téléchargez un apk et installez-le :
- Prenez par exemple l’apk sur un store alternatif
- Soit via le device explorer, comme pour le certificat. Il faut ensuite aller dans l’explorateur de fichiers de l’émulateur et cliquer sur le fichier pour installer l’apk
- Soit via la ligne de commandes comme ceci
adb install -r <mon-app>.apk
Activation du déchiffrement HTTPS pour le domaine recherché
Ici, vous devez ouvrir l’application et faire des actions dessus. Normalement vous verrez des entrées s’afficher dans proxyman.
Pour déchiffrer le contenu, cliquez sur Enable only this domain et refaites des actions. Le trafic sera déchiffré.

L’IP ci-dessus correspond au trafic sur l’application google préinstallée. Suite à l’activation du déchiffrement, j’ai le résultat suivant :

Conclusion
La méthode présentée est une méthode parmi d’autres pour inspecter le trafic réseau d’une application dont vous n’avez pas la maîtrise. Il existe d’autres outils comme Frida qui servent à accomplir la même chose. Si votre cas d’usage est plus complexe, je vous invite à y jeter un œil ! Il permet aussi de charger du code à chaud dans l’application et est crossplatform. Merci à Proxyman de fournir un super outil et des méthodes avancées pour le débogage !
Sources
- Fonctionnement d’Android : https://www-igm.univ-mlv.fr/~dr/XPOSE2008/android/fonct.html
- About the Zygote processes : https://source.android.com/docs/core/runtime/zygote
- Network Profiler : https://developer.android.com/studio/debug/network-profiler
- Zygote.java : https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/os/Zygote.java;l=1045
- Android Vitals - Diving into cold start waters : https://dev.to/pyricau/android-vitals-diving-into-cold-start-waters-5hi6?utm_source=chatgpt.com
- Automatic script for Android Emulator : https://docs.proxyman.com/debug-devices/android-device/automatic-script-for-android-emulator
- Install trusted system CA Certificate on Android Emulator : https://docs.mitmproxy.org/stable/howto/install-system-trusted-ca-android
- SELinux in Android : https://source.android.com/docs/security/features/selinux
- Magisk, pour rooter un téléphone : https://docs.mitmproxy.org/stable/howto/install-system-trusted-ca-android/#instructions-when-using-magisk
- Documentation URL Android : https://developer.android.com/reference/java/net/URL
- Documentation setURLStreamHandlerFactory : https://developer.android.com/reference/java/net/URL#setURLStreamHandlerFactory(java.net.URLStreamHandlerFactory)
- JDWP : https://docs.oracle.com/javase/8/docs/technotes/guides/jpda/jdwp-spec.html