{"id":130,"date":"2024-04-28T15:23:28","date_gmt":"2024-04-28T15:23:28","guid":{"rendered":"https:\/\/tolva.fr\/?p=130"},"modified":"2025-03-08T22:14:09","modified_gmt":"2025-03-08T22:14:09","slug":"retroconception-dun-vpn-android","status":"publish","type":"post","link":"https:\/\/tolva.fr\/index.php\/2024\/04\/28\/retroconception-dun-vpn-android\/","title":{"rendered":"R\u00e9troconception d&rsquo;un VPN Android"},"content":{"rendered":"\n<p style=\"margin-right:0;margin-left:0;padding-top:0;padding-right:0;padding-bottom:0;padding-left:0\">Un grand nombre de gens utilise un VPN, pour diverses raisons plus ou moins pertinentes.<\/p>\n\n\n\n<p style=\"margin-right:0;margin-left:0;padding-top:0;padding-right:0;padding-bottom:0;padding-left:0\">Si certains acteurs du march\u00e9 donnent des gages de transparence en divulguant leur code source et\/ou en publiant r\u00e9guli\u00e8rement des rapports d&rsquo;audits, de nombreux autres fournisseurs font preuve d&rsquo;une r\u00e9elle opacit\u00e9. <\/p>\n\n\n\n<p style=\"margin-right:0;margin-left:0;padding-top:0;padding-right:0;padding-bottom:0;padding-left:0\">\u00c9tudier attentivement un client VPN pourrait donc r\u00e9v\u00e9ler quelques surprises\u2026<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>L&rsquo;application Secure VPN<\/strong><\/h4>\n\n\n\n<p class=\"has-text-align-left\">Souhaitant \u00e9tudier d&rsquo;un peu plus pr\u00e8s le fonctionnement d&rsquo;une de ces applications, j&rsquo;ai choisi &#8211; un peu au hasard &#8211; de m&rsquo;int\u00e9resser \u00e0 Secure VPN.<\/p>\n\n\n\n<p class=\"has-text-align-left\">Cette application est relativement populaire puisqu&rsquo;elle a \u00e9t\u00e9 t\u00e9l\u00e9charg\u00e9e plus de 100 millions de fois, soit plus que NordVPN et CyberGhost VPN !<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"486\" height=\"1080\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_popularity-486x1080.png\" alt=\"\" class=\"wp-image-131\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_popularity-486x1080.png 486w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_popularity-135x300.png 135w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_popularity-768x1707.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_popularity-691x1536.png 691w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_popularity-922x2048.png 922w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_popularity.png 1080w\" sizes=\"auto, (max-width: 486px) 100vw, 486px\" \/><\/figure>\n\n\n\n<p>La page Google Play de l&rsquo;application pr\u00e9cise qu&rsquo;elle est d\u00e9velopp\u00e9e par la soci\u00e9t\u00e9 Secure Signal inc :<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"486\" height=\"1080\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_securesignal-486x1080.png\" alt=\"\" class=\"wp-image-132\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_securesignal-486x1080.png 486w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_securesignal-135x300.png 135w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_securesignal-768x1707.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_securesignal-691x1536.png 691w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_securesignal-922x2048.png 922w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_securesignal.png 1080w\" sizes=\"auto, (max-width: 486px) 100vw, 486px\" \/><\/figure>\n\n\n\n<p>Contrairement \u00e0 ce que ce nom pourrait sous-entendre, cette soci\u00e9t\u00e9 n&rsquo;a \u00e0 priori aucun lien avec l&rsquo;\u00e9diteur de l&rsquo;application de messagerie instantan\u00e9e, dont le site est signal.org.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Le site de l&rsquo;application<\/strong><\/h4>\n\n\n\n<p>La rubrique \u00ab\u00a0Assistance pour l&rsquo;application\u00a0\u00bb de Google Play pr\u00e9cise une adresse mail (secure-vpn@free-signal.com), une adresse physique et un site internet pour contacter le d\u00e9veloppeur :<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"486\" height=\"1080\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_contact_1-486x1080.png\" alt=\"\" class=\"wp-image-133\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_contact_1-486x1080.png 486w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_contact_1-135x300.png 135w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_contact_1-768x1707.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_contact_1-691x1536.png 691w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_contact_1-922x2048.png 922w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_contact_1.png 1080w\" sizes=\"auto, (max-width: 486px) 100vw, 486px\" \/><\/figure>\n\n\n\n<p>Le site internet en question est securesignal.app.<\/p>\n\n\n\n<p>On peut notamment y lire les t\u00e9moignages \u00e9logieux de Marsha Singer, Tim Shaw et Lindsay Spice :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1789\" height=\"941\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/website1.png\" alt=\"\" class=\"wp-image-134\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/website1.png 1789w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/website1-300x158.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/website1-768x404.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/website1-1536x808.png 1536w\" sizes=\"auto, (max-width: 1789px) 100vw, 1789px\" \/><\/figure>\n\n\n\n<p>Par un hasard extraordinaire, ces trois personnes ont des Doppelg\u00e4ngers ayant donn\u00e9s des t\u00e9moignages tout aussi \u00e9logieux pour une autre application, apptuary.com, \u00e0 laquelle est d\u00e9di\u00e9e un site web d&rsquo;aspect rappelant celui de securesignal.app :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1790\" height=\"941\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/apptuary.png\" alt=\"\" class=\"wp-image-135\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/apptuary.png 1790w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/apptuary-300x158.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/apptuary-768x404.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/apptuary-1536x807.png 1536w\" sizes=\"auto, (max-width: 1790px) 100vw, 1790px\" \/><\/figure>\n\n\n\n<p>Une autre adresse mail de contact est mentionn\u00e9e sur le site (contact@securesignal.app) :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"669\" height=\"251\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securesignalcontact.png\" alt=\"\" class=\"wp-image-136\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securesignalcontact.png 669w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securesignalcontact-300x113.png 300w\" sizes=\"auto, (max-width: 669px) 100vw, 669px\" \/><\/figure>\n\n\n\n<p>Le reste du site donne quelques indications sur les capacit\u00e9s techniques du VPN, avec les promesses habituelles de ce genre d&rsquo;applications (No Log, Safe Protocol, Privacy, etc), sans entrer dans les d\u00e9tails.<\/p>\n\n\n\n<p>Les sites free-signal.com ou secure.free-signal.com ne donnent pas plus d&rsquo;indication.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Premi\u00e8re analyse sur Exodus privacy<\/h4>\n\n\n\n<p>Un utilisateur soucieux de savoir dans quelle mesure une application est intrusive mais ne voulant\/sachant pas extraire le manifeste de l&rsquo;apk peut utiliser le site https:\/\/reports.exodus-privacy.eu.org.<\/p>\n\n\n\n<p>Il s&rsquo;av\u00e8re que Secure VPN utilise 2 pisteurs et n\u00e9cessite 16 permissions :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"977\" height=\"415\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/exodus_rapport1.png\" alt=\"\" class=\"wp-image-137\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/exodus_rapport1.png 977w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/exodus_rapport1-300x127.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/exodus_rapport1-768x326.png 768w\" sizes=\"auto, (max-width: 977px) 100vw, 977px\" \/><\/figure>\n\n\n\n<p>Certaines de ces permissions :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"813\" height=\"953\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/exodus_rapport2.png\" alt=\"\" class=\"wp-image-138\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/exodus_rapport2.png 813w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/exodus_rapport2-256x300.png 256w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/exodus_rapport2-768x900.png 768w\" sizes=\"auto, (max-width: 813px) 100vw, 813px\" \/><\/figure>\n\n\n\n<p>comme <code>QUERY_ALL_PACKAGES<\/code>, <code>ACCESS_ADSERVICES_TOPICS<\/code>, <code>ACCESS_ADSERVICES_ATTRIBUTION<\/code> ou <code>ACCESS_ADSERVICES_AD_ID<\/code> <\/p>\n\n\n\n<p>ne sont pas \u00e0 proprement parler indispensables au fonctionnement d&rsquo;un client VPN et sont uniquement pr\u00e9sentes pour des besoins publicitaires et\/ou de profilage (<code>QUERY_ALL_PACKAGES<\/code> permettant ainsi de lister les applications install\u00e9es sur le t\u00e9l\u00e9phone d&rsquo;une personne, ce qui peut en dire long sur elle).<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Premi\u00e8re utilisation<\/h4>\n\n\n\n<p>L&rsquo;interface de l&rsquo;application est tr\u00e8s simple.<\/p>\n\n\n\n<p>Lorsqu&rsquo;elle est lanc\u00e9e pour la premi\u00e8re fois, l&rsquo;application demande \u00e0 l&rsquo;utilisateur s&rsquo;il consent \u00e0 ce que ses donn\u00e9es personnelles soient utilis\u00e9es \u00e0 diverses fins, publicitaires notamment :<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"486\" height=\"1080\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_approbation_2-486x1080.png\" alt=\"\" class=\"wp-image-139\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_approbation_2-486x1080.png 486w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_approbation_2-135x300.png 135w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_approbation_2-768x1707.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_approbation_2-691x1536.png 691w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_approbation_2-922x2048.png 922w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_approbation_2.png 1080w\" sizes=\"auto, (max-width: 486px) 100vw, 486px\" \/><\/figure>\n\n\n\n<p>L&rsquo;utilisateur a ensuite acc\u00e8s \u00e0 une ic\u00f4ne gris\u00e9e sur laquelle il faut cliquer pour monter le tunnel VPN :<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"486\" height=\"1080\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_main_1-486x1080.png\" alt=\"\" class=\"wp-image-140\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_main_1-486x1080.png 486w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_main_1-135x300.png 135w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_main_1-768x1707.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_main_1-691x1536.png 691w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_main_1-922x2048.png 922w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_main_1.png 1080w\" sizes=\"auto, (max-width: 486px) 100vw, 486px\" \/><\/figure>\n\n\n\n<p>L&rsquo;ic\u00f4ne de connexion devient bleue lorsque le tunnel VPN est mont\u00e9 :<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"486\" height=\"1080\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_connection_1-486x1080.png\" alt=\"\" class=\"wp-image-141\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_connection_1-486x1080.png 486w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_connection_1-135x300.png 135w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_connection_1-768x1707.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_connection_1-691x1536.png 691w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_connection_1-922x2048.png 922w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_connection_1.png 1080w\" sizes=\"auto, (max-width: 486px) 100vw, 486px\" \/><\/figure>\n\n\n\n<p>Une visite de whatismyip.com confirme que l&rsquo;adresse IP observ\u00e9e par un site internet visit\u00e9 est diff\u00e9rente de l&rsquo;adresse IP r\u00e9elle du t\u00e9l\u00e9phone :<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"486\" height=\"1080\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_whatismyip-486x1080.png\" alt=\"\" class=\"wp-image-142\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_whatismyip-486x1080.png 486w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_whatismyip-135x300.png 135w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_whatismyip-768x1707.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_whatismyip-691x1536.png 691w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_whatismyip-922x2048.png 922w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/securevpn_whatismyip.png 1080w\" sizes=\"auto, (max-width: 486px) 100vw, 486px\" \/><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">Le manifeste<\/h4>\n\n\n\n<p>Jeter un \u0153il au fichier AndroidManifest.xml d&rsquo;une application \u00e9tudi\u00e9e est une \u00e9tape incontournable, et nous n&rsquo;y ferons pas exception.<\/p>\n\n\n\n<p>Le manifeste mentionne 24 activities. <\/p>\n\n\n\n<p>Parmi celles-ci, 18 font partie de l&rsquo;application \u00e0 proprement parler (toutes sont membres du package <code>com.signallab.secure.activity<\/code>).<\/p>\n\n\n\n<p>Nous ne nous attarderons pas sur les activities de l&rsquo;application : <\/p>\n\n\n\n<p>En effet, le travail d&rsquo;un client VPN est de chiffrer\/d\u00e9chiffrer le trafic entrant.<\/p>\n\n\n\n<p>Cette t\u00e2che, r\u00e9alis\u00e9e en t\u00e2che de fond, n\u00e9cessite l&rsquo;ex\u00e9cution d&rsquo;un service. Le manifeste d\u00e9clare 12 services. <\/p>\n\n\n\n<p>Deux d&rsquo;entre eux, <code>com.signallab.secure.service.SecureService<\/code> et <code>com.signallab.lib.SignalService<\/code>, font partie de l&rsquo;application \u00e0 proprement parler :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"750\" height=\"754\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/manifest_list_services.png\" alt=\"\" class=\"wp-image-143\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/manifest_list_services.png 750w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/manifest_list_services-298x300.png 298w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/manifest_list_services-150x150.png 150w\" sizes=\"auto, (max-width: 750px) 100vw, 750px\" \/><\/figure>\n\n\n\n<p>Ces deux services h\u00e9ritent de la classe android.net.VpnService, classe utilis\u00e9e pour impl\u00e9menter un client VPN. Un tel service va cr\u00e9er une interface r\u00e9seau virtuelle (<code>\/dev\/tun<\/code>, typiquement).<\/p>\n\n\n\n<p>Lorsque le VPN est lanc\u00e9,<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>les applications du t\u00e9l\u00e9phone envoient leur trafic r\u00e9seau sur cette interface<\/li>\n\n\n\n<li>le service VPN lit les paquets que les applications souhaitent envoyer, les chiffre et les envoie<\/li>\n\n\n\n<li>le service VPN re\u00e7oit les paquets chiffr\u00e9s provenant du serveur VPN, les d\u00e9chiffre et les \u00e9crit sur l&rsquo;interface virtuelle<\/li>\n\n\n\n<li>les applications re\u00e7oivent ces paquets d\u00e9chiffr\u00e9s sur cette interface virtuelle.<\/li>\n<\/ul>\n\n\n\n<p>La classe <code>SignalService<\/code> comporte une m\u00e9thode <code>loop()<\/code>, qui appelle la m\u00e9thode <code>connect()<\/code> de la classe <code>SignalHelper<\/code> :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1345\" height=\"819\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/SignalServiceLoop.png\" alt=\"\" class=\"wp-image-144\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/SignalServiceLoop.png 1345w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/SignalServiceLoop-300x183.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/SignalServiceLoop-768x468.png 768w\" sizes=\"auto, (max-width: 1345px) 100vw, 1345px\" \/><\/figure>\n\n\n\n<p>La m\u00e9thode <code>connect()<\/code> est une m\u00e9thode native :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"993\" height=\"530\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/SignalHelperNativeMethods.png\" alt=\"\" class=\"wp-image-145\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/SignalHelperNativeMethods.png 993w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/SignalHelperNativeMethods-300x160.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/SignalHelperNativeMethods-768x410.png 768w\" sizes=\"auto, (max-width: 993px) 100vw, 993px\" \/><\/figure>\n\n\n\n<p>Cette m\u00e9thode est appel\u00e9e lorsque l&rsquo;utilisateur clique sur l&rsquo;ic\u00f4ne de connexion mentionn\u00e9e auparavant.<\/p>\n\n\n\n<p>On peut tracer l&rsquo;appel \u00e0 cette m\u00e9thode <code>connect()<\/code> avec Frida (c&rsquo;est le script trace_SignalHelper_connect.js dans le github associ\u00e9 <\/p>\n\n\n\n<p><a href=\"https:\/\/github.com\/T0lva\/securevpn\" data-type=\"link\" data-id=\"https:\/\/github.com\/tmalherbe\/securevpn\">https:\/\/github.com\/T0lva\/securevpn<\/a>) :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>Spawned `com.fast.free.unblock.secure.vpn`. Resuming main thread!       \n&#91;Pixel 7a::com.fast.free.unblock.secure.vpn ]-&gt;\nappel de connect - arguments :\ntunfd : 263\nhost : 205.185.127.80\nudpPorts : 53,9981\ntcpPorts : 443,9981\nuserId : 3363728822350923000\nuserToken : 2374040680779317000\nkey : Fh8YUC9uTVv2qJikWbXCHh\nsupportBt : false\nalgo : 1\n\nappel de connect - pile d'appel :\njava.lang.Exception\n    at com.signallab.lib.SignalHelper.connect(Native Method)\n    at com.signallab.lib.SignalService$VpnThread.loop(SignalService.java:144)\n    at com.signallab.lib.SignalService$VpnThread.run(SignalService.java:1)<\/code><\/pre>\n\n\n\n<p>Les arguments de la m\u00e9thode <code>connect()<\/code> sont :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Le descripteur de fichier sur l&rsquo;interface virtuelle <code>\/dev\/tun<\/code>,<\/li>\n\n\n\n<li>L&rsquo;adresse IP du serveur VPN \u00e0 contacter,<\/li>\n\n\n\n<li>les ports UDP et TCP sur lesquels contacter le serveur VPN,<\/li>\n\n\n\n<li>des entiers (<code>userId<\/code> et <code>userToken<\/code>) identifiant l&rsquo;utilisateur,<\/li>\n\n\n\n<li>une cl\u00e9 (valant ici <code>Fh8YUC9uTVv2qJikWbXCHh<\/code>),<\/li>\n\n\n\n<li>un bool\u00e9en ainsi qu&rsquo;une constante indiquant l&rsquo;algorithme support\u00e9.<\/li>\n<\/ul>\n\n\n\n<p>D&rsquo;apr\u00e8s la documentation Android (https:\/\/developer.android.com\/reference\/java\/lang\/Thread#start()), l&rsquo;invocation de la m\u00e9thode <code>start<\/code> d&rsquo;un objet <code>Thread<\/code> provoque l&rsquo;appel de la m\u00e9thode <code>run<\/code> de l&rsquo;objet en question.<\/p>\n\n\n\n<p>La m\u00e9thode <code>start<\/code> de <code>VpnThread<\/code> est justement appel\u00e9e dans la m\u00e9thode <code>onStartCommand<\/code> de <code>SignalService<\/code> :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"517\" height=\"173\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/SignalService_onStartCommand.png\" alt=\"\" class=\"wp-image-146\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/SignalService_onStartCommand.png 517w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/SignalService_onStartCommand-300x100.png 300w\" sizes=\"auto, (max-width: 517px) 100vw, 517px\" \/><\/figure>\n\n\n\n<p>Cette m\u00e9thode <code>onStartCommand<\/code> est le point d&rsquo;entr\u00e9e par laquelle l&rsquo;ext\u00e9rieur peut lancer le service en question.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Les librairies natives de l&rsquo;application<\/h4>\n\n\n\n<p>L&rsquo;application embarque 3 libairies natives, libz.so, liblog.so et libchannel.so.<\/p>\n\n\n\n<p>Cette derni\u00e8re est nettement plus grosse que les deux autres, et c&rsquo;est elle qui impl\u00e9mente la plupart des m\u00e9thodes natives employ\u00e9es par les classes java de l&rsquo;application.<\/p>\n\n\n\n<p>Intuitivement, la fa\u00e7on la plus \u00ab\u00a0facile\u00a0\u00bb de cr\u00e9er un client VPN est \u00ab\u00a0d&#8217;emballer\u00a0\u00bb un client OpenVPN dans un apk : L&rsquo;application contiendra une classe h\u00e9ritant de <code>android.net.VpnService<\/code>, classe qui finira par lancer le client natif.<\/p>\n\n\n\n<p>Aucune des fonctions export\u00e9es par libchannel.so ne comporte dans son nom les mots-cl\u00e9 \u00ab\u00a0openvpn\u00a0\u00bb ou \u00ab\u00a0ovpn\u00a0\u00bb que contiennent un grand nombre de fonctions export\u00e9es par la librairie libovpn utilis\u00e9e dans ce type d&rsquo;applications :<\/p>\n\n\n\n<figure class=\"wp-block-image alignwide size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1089\" height=\"79\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/libovpn.png\" alt=\"\" class=\"wp-image-147\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/libovpn.png 1089w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/libovpn-300x22.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/libovpn-768x56.png 768w\" sizes=\"auto, (max-width: 1089px) 100vw, 1089px\" \/><\/figure>\n\n\n\n<p>Cela laisse penser que Secure VPN n&rsquo;utilise pas la OpenVPN, m\u00eame s&rsquo;il est possible que libchannel.so soit une version customis\u00e9e de libovpn.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Trafic \u00e9chang\u00e9 avec le serveur VPN<\/h4>\n\n\n\n<p>Si l&rsquo;on examine le trafic intercept\u00e9 apr\u00e8s avoir lanc\u00e9 l&rsquo;application et mont\u00e9 le tunnel VPN, on observe, au fil des sessions :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>essentiellement du trafic avec le port tcp 443 du serveur,<\/li>\n\n\n\n<li>ou essentiellement du trafic avec le port tcp 9981 du serveur,<\/li>\n\n\n\n<li>ou bien essentiellement du trafic avec le port udp 53 du serveur.<\/li>\n<\/ul>\n\n\n\n<p>Bien que le port 443 soit d\u00e9di\u00e9 au trafic TLS, le trafic captur\u00e9 n&rsquo;est pas du trafic TLS, un paquet TLS applicatif commen\u00e7ant n\u00e9cessairement par <code>\\x17\\x03<\/code> :<\/p>\n\n\n\n<figure class=\"wp-block-image alignwide size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1496\" height=\"955\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/traffic_443.png\" alt=\"\" class=\"wp-image-148\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/traffic_443.png 1496w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/traffic_443-300x192.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/traffic_443-768x490.png 768w\" sizes=\"auto, (max-width: 1496px) 100vw, 1496px\" \/><\/figure>\n\n\n\n<p>De m\u00eame, le trafic sur le port 53 n&rsquo;est pas du trafic DNS, comme l&rsquo;indique les erreurs remont\u00e9es par wireshark :<\/p>\n\n\n\n<figure class=\"wp-block-image alignwide size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1547\" height=\"955\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/traffic_53.png\" alt=\"\" class=\"wp-image-149\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/traffic_53.png 1547w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/traffic_53-300x185.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/traffic_53-768x474.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/traffic_53-1536x948.png 1536w\" sizes=\"auto, (max-width: 1547px) 100vw, 1547px\" \/><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">Descente jusqu&rsquo;au natif<\/h4>\n\n\n\n<p>L&rsquo;appel de la m\u00e9thode <code>connect()<\/code> de la classe <code>SignalHelper<\/code> se traduit par un appel \u00e0 la fonction du m\u00eame nom dans libchannel.so.<\/p>\n\n\n\n<p>Cette fonction cr\u00e9e un objet de la classe <code>SignalLinkClient<\/code> et positionne diff\u00e9rents champs de cet objet avec les fonctions <code>setSignalRouter<\/code>, <code>enableObscure<\/code>, <code>setUsers<\/code>, <code>setProto<\/code>, <code>setBackupPort<\/code>, <code>connect<\/code> et <code>setTunnel<\/code>, <\/p>\n\n\n\n<p>avant d&rsquo;appeler la fonction <code>runLoop<\/code> qui est la boucle infinie principale du service client VPN.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"718\" height=\"499\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/native_connect.png\" alt=\"\" class=\"wp-image-150\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/native_connect.png 718w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/native_connect-300x208.png 300w\" sizes=\"auto, (max-width: 718px) 100vw, 718px\" \/><\/figure>\n\n\n\n<p>Donnons quelques d\u00e9tails sur les r\u00f4les respectifs de ces fonctions d&rsquo;initialisation :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Le constructeur <code>SignalLinkClient<\/code> initialise un objet <code>SignalPackage<\/code>, vers lequel pointe un des champs du <code>SignalLinkClient<\/code>. Les 8 premiers octets du <code>SignalPackage<\/code> pointent \u00e0 leur tour vers un buffer de 1500 octets, valeur usuelle de la MTU : ce buffer sera probablement utilis\u00e9 pour manipuler des paquets r\u00e9seau.<\/li>\n\n\n\n<li><code>SignalLinkClient::setSignalRouter<\/code> : Cette fonction copie l&rsquo;adresse d&rsquo;un objet statique <code>SignalRouter<\/code> au d\u00e9but du <code>SignalLinkClient<\/code>. L&rsquo;objet <code>SignalRouter<\/code> contient diff\u00e9rents pointeurs de fonction. Comme les fonctions en question n&rsquo;interviennent pas dans le chiffrement\/d\u00e9chiffrement du trafic VPN, nous ne y int\u00e9resserons pas particuli\u00e8rement.<\/li>\n\n\n\n<li>La fonction <code>SignalLinkClient::enableObscure<\/code> modifie le <code>SignalPackage<\/code> adress\u00e9 par l&rsquo;un des champs du <code>SignalLinkClient<\/code> : \u00c0 la fin de l&rsquo;ex\u00e9cution de cette fonction, un des champs du <code>SignalPackage<\/code> pointe vers un <code>SignalObfuscator<\/code>, qui contient essentiellement la cl\u00e9 d&rsquo;obfuscation pass\u00e9e en param\u00e8tre de <code>SignalHelper.connect<\/code>.<\/li>\n\n\n\n<li><code>SignalLinkClient::setUser<\/code> copie les entiers <code>userId<\/code> et <code>userToken<\/code> dans le <code>SignalLinkClient<\/code>.<\/li>\n\n\n\n<li><code>SignalLinkClient::setProto<\/code> et <code>SignalLinkClient::setBackupPort<\/code> ont pour effets respectifs de positionner des bool\u00e9ens sp\u00e9cifiants les protocoles support\u00e9s (UDP et TCP), et de positionner des num\u00e9ros de port.<\/li>\n\n\n\n<li>La fonction <code>SignalLinkClient::connect<\/code> initialise des tableaux de <code>RemoteLink *<\/code>, objets d\u00e9crivant un serveur VPN (adresse IP + port + protocole).<\/li>\n\n\n\n<li>Enfin, la fonction <code>SignalLinkClient::setTunnel<\/code> copie le descripteur de fichier sur l&rsquo;interface virtuelle <code>\/dev\/tun<\/code> dans le <code>SignalLinkClient<\/code>.<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\">La boucle de traitement <code>runLoop()<\/code><\/h4>\n\n\n\n<p>Lorsque toutes les \u00e9tapes d&rsquo;initialisation ont eu lieu, la fonction runLoop` est appel\u00e9e.<\/p>\n\n\n\n<p>Dans les grandes lignes, cette fonction est une boucle infinie \u00e0 l&rsquo;int\u00e9rieur de laquelle on surveille, avec l&rsquo;api <code>epoll<\/code>, des ensembles de descripteurs de fichiers :<\/p>\n\n\n\n<p>Lorsqu&rsquo;une application du t\u00e9l\u00e9phone envoie un paquet r\u00e9seau, ce paquet en clair est \u00e9crit sur <code>\/dev\/tun<\/code>. Le client VPN lit ce paquet, le chiffre et l&rsquo;envoie vers le serveur VPN.<\/p>\n\n\n\n<p>\u00c0 l&rsquo;inverse, un paquet provenant du serveur VPN est d\u00e9chiffr\u00e9 et \u00e9crit sur <code>\/dev\/tun<\/code> afin d&rsquo;\u00eatre relay\u00e9 \u00e0 l&rsquo;application destinataire.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"680\" height=\"877\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/runLoop.png\" alt=\"\" class=\"wp-image-151\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/runLoop.png 680w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/runLoop-233x300.png 233w\" sizes=\"auto, (max-width: 680px) 100vw, 680px\" \/><\/figure>\n\n\n\n<p>Ainsi,<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>les paquets clairs \u00e0 \u00e9mettre sont lus sur <code>\/dev\/tun<\/code>, chiffr\u00e9s et envoy\u00e9 au serveur par la fonction <code>SignalLinkClient::processTunIn<\/code>,<\/li>\n\n\n\n<li>les paquets chiffr\u00e9s en provenance du serveur VPN sont d\u00e9chiffr\u00e9s puis \u00e9crits sur <code>\/dev\/tun<\/code> par la fonction <code>SignalLinkClient::processLinkData<\/code>.<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\">Traitement des paquets sortants<\/h4>\n\n\n\n<p>La fonction <code>SignalLinkClient::processTunIn<\/code> de traitement des paquets sortants<\/p>\n\n\n\n<p>Cette fonction commence par lire, \u00e0 l&rsquo;offset <code>0x468<\/code>du <code>SignalLinkClient<\/code>, depuis <code>\/dev\/tun<\/code>, le paquet \u00e0 envoyer. Ce paquet est trait\u00e9 par la fonction <code>SignalLinkClient::writeToLink<\/code>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"677\" height=\"749\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/processTunIn.png\" alt=\"\" class=\"wp-image-152\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/processTunIn.png 677w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/processTunIn-271x300.png 271w\" sizes=\"auto, (max-width: 677px) 100vw, 677px\" \/><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>La fonction <code>writeToLink<\/code><\/strong><\/h4>\n\n\n\n<p>Cette fonction commence par une op\u00e9ration d&rsquo;initialisation, dont l&rsquo;un des effets est d&rsquo;\u00e9crire le magic word <code>\\x01\\x00_SiG<\/code> dans le buffer dont l&rsquo;adresse est conserv\u00e9e au d\u00e9but du <code>SignalPackage<\/code>.<\/p>\n\n\n\n<p>Le reste du travail est r\u00e9alis\u00e9 par la fonction <code>SignalPackage::setData<\/code>, et le paquet chiffr\u00e9 est ensuite envoy\u00e9 au serveur VPN.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"722\" height=\"649\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/writeToLink.png\" alt=\"\" class=\"wp-image-153\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/writeToLink.png 722w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/writeToLink-300x270.png 300w\" sizes=\"auto, (max-width: 722px) 100vw, 722px\" \/><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>La fonction <code>setData<\/code><\/strong><\/h4>\n\n\n\n<p>Apr\u00e8s quelques v\u00e9rifications pr\u00e9liminaires, la fonction <code>SignalPackage::setData<\/code> calcule deux entiers de 64 bits construits \u00e0 partir des entiers <code>userId<\/code> et <code>userToken<\/code> identifiants l&rsquo;utilisateur.<\/p>\n\n\n\n<p>Ces deux entiers sont \u00e9crits dans un buffer point\u00e9 par un des champs du <code>SignalPackage<\/code>. Le paquet en clair est copi\u00e9 \u00e0 la suite de ces deux entiers.<\/p>\n\n\n\n<p>Le r\u00f4le de ces 128 bits pr\u00e9c\u00e9dants le paquet pourrait \u00eatre de permettre au serveur VPN d&rsquo;identifier l&rsquo;utilisateur \u00e0 l&rsquo;origine du paquet re\u00e7u.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"726\" height=\"800\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/setData.png\" alt=\"\" class=\"wp-image-154\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/setData.png 726w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/setData-272x300.png 272w\" sizes=\"auto, (max-width: 726px) 100vw, 726px\" \/><\/figure>\n\n\n\n<p>La fonction <code>SignalObfuscator::encode<\/code> est finalement appel\u00e9e. C&rsquo;est cette derni\u00e8re qui va invoquer les primitives cryptographiques chiffrant le paquet.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>La fonction <code>encode<\/code><\/strong><\/h4>\n\n\n\n<p>Cette fonction comporte deux blocs conditionn\u00e9s par la valeur de son dernier argument, <code>algo<\/code>. Si il vaut <code>1<\/code>, le paquet est chiffr\u00e9 avec AES GCM. Si il vaut <code>0<\/code>, c&rsquo;est ChaCha20 qui est employ\u00e9.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"724\" height=\"861\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/encode.png\" alt=\"\" class=\"wp-image-155\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/encode.png 724w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/encode-252x300.png 252w\" sizes=\"auto, (max-width: 724px) 100vw, 724px\" \/><\/figure>\n\n\n\n<p>Comme nous le verrons plus tard c&rsquo;est AES qui est utilis\u00e9 par la tr\u00e8s grande majorit\u00e9 des serveurs VPN. <\/p>\n\n\n\n<p>Nous ne nous attarderons donc pas sur le deuxi\u00e8me bloc, qui invoque ChaCha20.<\/p>\n\n\n\n<p>Voici finalement la s\u00e9quence de chiffrement d&rsquo;un paquet clair :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Les 16 premiers octets de la cl\u00e9 d&rsquo;obfuscation sont utilis\u00e9 comme cl\u00e9 AES (positionn\u00e9e par `gcm_setkey`, qui appelle `aes_setkey`, qui invoque `aes_set_encryption_key`).<\/li>\n<\/ul>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Les 12 octets suivants de la cl\u00e9 d&rsquo;obfuscation sont utilis\u00e9s comme nonce GCM, via l&rsquo;appel `gcm_start`. En pratique, la cl\u00e9 d&rsquo;obfucation n&rsquo;est pas assez longue (elle devrait faire 16 + 12 = 28 octets), et les 6 derniers octets du nonce sont toujours nuls.<\/li>\n<\/ul>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Le chiffrement AES-GCM du paquet clair est assur\u00e9 par `gcm_update`, le r\u00e9sultat chiffr\u00e9 \u00e9tant \u00e9crit dans l&rsquo;objet `SignalObfuscator`.<\/li>\n<\/ul>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Ensuite, le tag GCM est g\u00e9n\u00e9r\u00e9 par un appel \u00e0 `gcm_finish`.<\/li>\n<\/ul>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Enfin, le paquet chiffr\u00e9 est copi\u00e9 dans l&#8217;emplacement initialement occup\u00e9 par le paquet clair, l&rsquo;envoi du paquet chiffr\u00e9 \u00e9tant, comme vu auparavant, r\u00e9alis\u00e9 \u00e0 la fin de la fonction `SignalLinkClient::writeToLink`.<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading alignwide\">La fonction <code>processLinkData<\/code> traitant les paquets entrants<\/h4>\n\n\n\n<p>Une fois que le traitement d&rsquo;un paquet sortant est bien compris, l&rsquo;\u00e9tude du traitement des paquets entrants est beaucoup plus facile.<\/p>\n\n\n\n<p>D\u00e9crivons succintement comment un paquet est d\u00e9chiffr\u00e9 :<\/p>\n\n\n\n<p>Les paquets re\u00e7us sont trait\u00e9s par la fonction <code>SignalLinkClient::writeToTun<\/code>. <\/p>\n\n\n\n<p>Dans cette derni\u00e8re, le d\u00e9chiffrement du paquet est r\u00e9alis\u00e9 par <code>SignalPackage::decodePackage<\/code>, via <code>SignalObfuscator::decode<\/code>, \u00e9quivalent de <code>SignalObfuscator::encode<\/code> d\u00e9di\u00e9 \u00e0 la r\u00e9ception.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">L&rsquo;algorithme de chiffrement est-il vraiment AES ?<\/h4>\n\n\n\n<p>M\u00eame si les noms de fonctions parlent d&rsquo;AES, il n&rsquo;est pas \u00e0 exclure que, volontairement ou pas, le d\u00e9veloppeur utilise autre chose qu&rsquo;AES. Il est donc pr\u00e9f\u00e9rable de s&rsquo;assurer que la primitive de chiffrement est bien AES.<\/p>\n\n\n\n<p>La fonction en question, appel\u00e9e dans <code>gcm_setkey<\/code> et <code>gcm_update<\/code>, est la fonction <code>aes_cipher<\/code>.<\/p>\n\n\n\n<p>On pourrait analyser le code d\u00e9compil\u00e9 produit par Ghidra pour s&rsquo;assurer qu&rsquo;on a bien affaire \u00e0 AES, mais cela serait extr\u00e9mement fastidieux, et pourquoi s&#8217;emb\u00eater alors qu&rsquo;on peut hooker <code>aes_cipher<\/code> avec Frida ?<\/p>\n\n\n\n<p>On lance ainsi un script (trace_AesGm.js) qui intercepte une s\u00e9quence <code>gcm_setkey<\/code>, <code>gcm_update<\/code> et <code>gcm_finish<\/code> :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>thomas@ankou:~\/articles\/securevpn$ frida -U -p $(frida-ps -U | grep 'Secure VPN' | awk -F ' ' '{print $1}') -l trace_AesGcm.js \n     ____\n    \/ _  |   Frida 16.2.1 - A world-class dynamic instrumentation toolkit\n   | (_| |\n    &gt; _  |   Commands:\n   \/_\/ |_|       help      -&gt; Displays the help system\n   . . . .       object?   -&gt; Display information about 'object'\n   . . . .       exit\/quit -&gt; Exit\n   . . . .\n   . . . .   More info at https:\/\/frida.re\/docs\/home\/\n   . . . .\n   . . . .   Connected to Pixel 7a (id=3C221JEHN01971)\n\n&#91;Pixel 7a::PID::21108 ]-&gt; Entering into gcm_setkey\ngcm_setkey, input : \\x46\\x68\\x38\\x59\\x55\\x43\\x39\\x75\\x54\\x56\\x76\\x32\\x71\\x4a\\x69\\x6b\ngcm_setkey, key_len : 16\nEntering into gcm_start\ngcm_start, mode : 1\ngcm_start, iv : \\x57\\x62\\x58\\x43\\x48\\x68\\x00\\x00\\x00\\x00\\x00\\x00\ngcm_start, iv_len : 12\nEntering into gcm_update\ngcm_update, input len : 90\ngcm_update, input : \\xd5\\xc2\\xb3\\x50\\x09\\xd8\\xfc\\x6e\\x56\\x9d\\x01\\x17\\x37\\x34\\x01\\x01\\x00\\x00\\x5f\\x53\\x69\\x47\\x2e\\xae\\x5d\\x8a\\xc8\\xf8\\x43\\x9a\\x20\\xf2\\x49\\x6b\\xc4\\x2d\\x80\\xbd\\x45\\x00\\x00\\x34\\x0b\\xe9\\x40\\x00\\x40\\x06\\x2a\\x4e\\xc0\\xa8\\x01\\x86\\x8e\\xfa\\xb3\\x64\\x8e\\x36\\x01\\xbb\\xef\\x16\\x68\\xf5\\x5d\\x8d\\xa5\\x89\\x80\\x11\\x01\\x2c\\x74\\x97\\x00\\x00\\x01\\x01\\x08\\x0a\\x9e\\x0f\\xe7\\xd0\\xb4\\x12\\xd7\\x63\ngcm_update, gcm_mode : 1\ngcm_update, output after : \\x48\\xb7\\xff\\x55\\xd1\\x7c\\x26\\x92\\x5e\\x4d\\x2d\\x5f\\x48\\x65\\xa9\\xa9\\xa8\\x0f\\x4a\\x56\\x3e\\x84\\x32\\xbc\\x61\\xea\\xae\\x06\\xb9\\x87\\x31\\x65\\x33\\xcf\\x3a\\x1d\\x8f\\x49\\x79\\x3f\\x1d\\xe2\\xca\\x33\\xb5\\xe5\\xd6\\xa8\\xd7\\xd9\\x57\\xcc\\x2d\\x67\\x8d\\xfb\\x34\\x35\\x3a\\x74\\x07\\x1c\\x7d\\x44\\x08\\x94\\x97\\xd0\\x3e\\x8e\\x85\\x8d\\x34\\x55\\x79\\x0b\\xba\\xfa\\xb3\\x39\\x28\\xc2\\xe2\\xa0\\x0e\\xd9\\x78\\x4f\\x9b\\x52\nEntering into gcm_finish\ngcm_finish, tag ptr : 0x0\ngcm_finish, tag len : 0\nStopping script\n&#91;Pixel 7a::PID::21108 ]-&gt;<\/code><\/pre>\n\n\n\n<p>Le script affiche l&rsquo;entr\u00e9e et la sortie de <code>gcm_update<\/code>, ainsi que la cl\u00e9 de chiffrement et le nonce. On note au passage que les arguments de <code>gcm_finish<\/code>, \u00e0 savoir le pointeur et la longueur du tag, sont nuls.<\/p>\n\n\n\n<p>Reproduisons le calcul r\u00e9alis\u00e9 avec quelques lignes de python (script decrypt.py) :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>#!\/usr\/bin\/python3\n# -*- coding: utf-8 -*-\n\nfrom Cryptodome.Cipher import AES\n\nkey = b'\\x46\\x68\\x38\\x59\\x55\\x43\\x39\\x75\\x54\\x56\\x76\\x32\\x71\\x4a\\x69\\x6b'\nnonce = b'\\x57\\x62\\x58\\x43\\x48\\x68\\x00\\x00\\x00\\x00\\x00\\x00'\nplaintext = b'\\xd5\\xc2\\xb3\\x50\\x09\\xd8\\xfc\\x6e\\x56\\x9d\\x01\\x17\\x37\\x34\\x01\\x01\\x00\\x00\\x5f\\x53\\x69\\x47\\x2e\\xae\\x5d\\x8a\\xc8\\xf8\\x43\\x9a\\x20\\xf2\\x49\\x6b\\xc4\\x2d\\x80\\xbd\\x45\\x00\\x00\\x34\\x0b\\xe9\\x40\\x00\\x40\\x06\\x2a\\x4e\\xc0\\xa8\\x01\\x86\\x8e\\xfa\\xb3\\x64\\x8e\\x36\\x01\\xbb\\xef\\x16\\x68\\xf5\\x5d\\x8d\\xa5\\x89\\x80\\x11\\x01\\x2c\\x74\\x97\\x00\\x00\\x01\\x01\\x08\\x0a\\x9e\\x0f\\xe7\\xd0\\xb4\\x12\\xd7\\x63'\n\ncipher = AES.new(key, AES.MODE_GCM, nonce)\nciphertext = cipher.encrypt(plaintext)\n\nprint(f\"enc ciphertext : {ciphertext}\")<\/code><\/pre>\n\n\n\n<p>Le script nous donne la m\u00eame sortie que celle observ\u00e9e \u00e0 la fin de <code>gcm_finish<\/code> : le client VPN utilise bien AES en mode GCM !<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>thomas@ankou:~\/articles\/securevpn$ .\/decrypt.py \nenc ciphertext : b'H\\xb7\\xffU\\xd1|&amp;\\x92^M-_He\\xa9\\xa9\\xa8\\x0fJV&gt;\\x842\\xbca\\xea\\xae\\x06\\xb9\\x871e3\\xcf:\\x1d\\x8fIy?\\x1d\\xe2\\xca3\\xb5\\xe5\\xd6\\xa8\\xd7\\xd9W\\xcc-g\\x8d\\xfb45:t\\x07\\x1c}D\\x08\\x94\\x97\\xd0&gt;\\x8e\\x85\\x8d4Uy\\x0b\\xba\\xfa\\xb39(\\xc2\\xe2\\xa0\\x0e\\xd9xO\\x9bR'<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">Identification de l&rsquo;impl\u00e9mentation cryptographique utilis\u00e9e<\/h4>\n\n\n\n<p>Dans AES, chaque tour (comme la cl\u00e9 fait 16 octets, ici il y a 10 tours) applique trois transformations successives &#8211; m\u00eame si le dernier tour est l\u00e9g\u00e8rement diff\u00e9rent :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>La transformation SubBytes, qui associe \u00e0 chacun des 16 octets du bloc d&rsquo;entr\u00e9e un autre octet ;<\/li>\n\n\n\n<li>La transformation ShiftRows. Dans cette transformation, les octets du bloc d&rsquo;entr\u00e9e sont r\u00e9partis en 4 groupes de 4 pour former une matrice carr\u00e9e. La premi\u00e8re ligne de cette matrice n&rsquo;est pas modifi\u00e9e, les \u00e9l\u00e9ments de la deuxi\u00e8me ligne sont d\u00e9cal\u00e9s de 1, les \u00e9l\u00e9ments de la troisi\u00e8me ligne sont d\u00e9cal\u00e9s de 2, et les \u00e9l\u00e9ments de la quatri\u00e8me ligne sont d\u00e9cal\u00e9s de 3.<\/li>\n\n\n\n<li>La transformation MixColumns. Cette transformation interpr\u00e8te elle aussi le bloc d&rsquo;entr\u00e9e comme une matrice carr\u00e9e 4&#215;4, et calcule l&rsquo;image par une transformation matricielle de chacune des 4 colonnes de la matrice d&rsquo;entr\u00e9e.<\/li>\n<\/ul>\n\n\n\n<p>Pour des raisons d&rsquo;efficacit\u00e9, ces transformations sont g\u00e9n\u00e9ralement impl\u00e9ment\u00e9es sous la forme de tables pr\u00e9calcul\u00e9es et stock\u00e9es dans le binaire.<\/p>\n\n\n\n<p>Ce n&rsquo;est pas le cas ici. Si l&rsquo;on examine les tables utilis\u00e9es par la fonction <code>aes_cipher<\/code> impl\u00e9mentant le chiffrement AES :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"732\" height=\"304\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/aes_cipher.png\" alt=\"\" class=\"wp-image-156\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/aes_cipher.png 732w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/aes_cipher-300x125.png 300w\" sizes=\"auto, (max-width: 732px) 100vw, 732px\" \/><\/figure>\n\n\n\n<p>et que l&rsquo;on regarde o\u00f9 est d\u00e9fini la table `DAT_0015ed78` :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1482\" height=\"444\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/sbox.png\" alt=\"\" class=\"wp-image-157\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/sbox.png 1482w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/sbox-300x90.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/sbox-768x230.png 768w\" sizes=\"auto, (max-width: 1482px) 100vw, 1482px\" \/><\/figure>\n\n\n\n<p>On constate que son contenu n&rsquo;est pas initialis\u00e9.<\/p>\n\n\n\n<p>On observe \u00e9galement une r\u00e9f\u00e9rence crois\u00e9e menant \u00e0 une fonction <code>aes_init_keygen_tables<\/code>, dont le r\u00f4le, comme son nom l&rsquo;indique, est d&rsquo;initialiser les tables en question.<\/p>\n\n\n\n<p>Quelques recherches en ligne permettent de retrouver la trace d&rsquo;une impl\u00e9mentation de AES utilisant une fonction <code>aes_init_keygen_tables<\/code> :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"788\" height=\"947\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/aes_init_keygen_tables.png\" alt=\"\" class=\"wp-image-159\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/aes_init_keygen_tables.png 788w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/aes_init_keygen_tables-250x300.png 250w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/aes_init_keygen_tables-768x923.png 768w\" sizes=\"auto, (max-width: 788px) 100vw, 788px\" \/><\/figure>\n\n\n\n<p>L&rsquo;impl\u00e9mentation de AES en question est celle du serveur web embarqu\u00e9 mongoose, \u00e9dit\u00e9 par la soci\u00e9t\u00e9 Cesanta : https:\/\/github.com\/cesanta\/mongoose\/blob\/master\/mongoose.c<\/p>\n\n\n\n<p>Une comparaison du code d\u00e9compil\u00e9 par Ghidra et du code de <code>aes_init_keygen_tables<\/code> pr\u00e9sent sur github confirme que la version pr\u00e9sente dans libchannel.so est probablement issue de ce d\u00e9p\u00f4t.<\/p>\n\n\n\n<p>Par ailleurs, les autres fonctions cryptographiques utilis\u00e9es lors du traitement d&rsquo;un paquet sont aussi pr\u00e9sente dans ce d\u00e9p\u00f4t.<\/p>\n\n\n\n<p>Cet \u00e9l\u00e9ment permet de comprendre les r\u00f4les respectifs des arguments des fonctions cryptographiques <code>gcm_setkey<\/code>, <code>gcm_start<\/code>, <code>gcm_update<\/code> et <code>gcm_finish<\/code> utilis\u00e9es par libchannel.so.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Focus sur les m\u00e9canismes cryptographiques utilis\u00e9s<\/h4>\n\n\n\n<p>GCM est un mode <em>authentifiant<\/em> : Il chiffre un message et calcule un <em>tag<\/em> d&rsquo;authentification garantissant son int\u00e9grit\u00e9. La partie chiffrement du mode GCM est assur\u00e9e par le mode op\u00e9ratoire CTR. Dans le mode CTR, un <em>compteur<\/em> est utilis\u00e9 pour chiffrer le message : Pour chiffrer un bloc du message, on chiffre le compteur, on xor le r\u00e9sultat avec le bloc, puis on incr\u00e9mente le compteur avant de passer au bloc suivant.<\/p>\n\n\n\n<p>On peut r\u00e9sumer cela plus formellement par la formule<\/p>\n\n\n\n<p><code>Y_{i} = X_{i} ^ E(counter + i)<\/code>. <\/p>\n\n\n\n<p>Un message chiffr\u00e9 avec AES en mode CTR est xor\u00e9 avec une suite d&rsquo;octets al\u00e9atoires, un peu comme si l&rsquo;on utilisait un chiffrement par flot.<\/p>\n\n\n\n<p>Il est donc capital, lorsque l&rsquo;on utilise le mode CTR &#8211; et donc si l&rsquo;on emploie le mode GCM &#8211; de ne pas r\u00e9utiliser ce compteur pour chiffrer deux messages. Si l&rsquo;on ignore cette pr\u00e9caution, on se retrouve dans la situation suivante :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Le chiffr\u00e9 <code>C1<\/code> est le r\u00e9sultat du xor du message clair <code>M1<\/code> avec la suite d&rsquo;octets al\u00e9atoires <code>R<\/code>,<\/li>\n\n\n\n<li>Le chiffr\u00e9 <code>C2<\/code> est le r\u00e9sultat du xor du message clair <code>M2<\/code> avec la suite d&rsquo;octets al\u00e9atoires <code>R<\/code>,<\/li>\n\n\n\n<li>Un observateur peut xorer <code>C1<\/code> avec <code>C2<\/code>, ce qui lui donne <code>M1 ^ M2<\/code>, \u00e0 partir duquel il peut extraire de l&rsquo;information sur <code>M1<\/code> et\/ou <code>M2<\/code>.<\/li>\n<\/ul>\n\n\n\n<p>Regardons comment les fonctions de mongoose sont employ\u00e9es par libchannel.so.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Fonction <code>gcm_start<\/code><\/h4>\n\n\n\n<p>Le code de mongoose d\u00e9crit le r\u00f4le des arguments de cette fonction :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"797\" height=\"689\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/gcm_start_mongoose.png\" alt=\"\" class=\"wp-image-160\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/gcm_start_mongoose.png 797w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/gcm_start_mongoose-300x259.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/gcm_start_mongoose-768x664.png 768w\" sizes=\"auto, (max-width: 797px) 100vw, 797px\" \/><\/figure>\n\n\n\n<p>Les deux derniers arguments pr\u00e9cisent une \u00ab\u00a0AEAD data\u00a0\u00bb : lorsque l&rsquo;on utilise un mode authentifiant, ce sont des donn\u00e9es qui sont authentifi\u00e9es en plus des donn\u00e9es chiffr\u00e9es, sans \u00eatre elles-m\u00eames chiffr\u00e9es.<\/p>\n\n\n\n<p>Dans la fonction <code>SignalObfuscator::encode<\/code>, on voit que les deux derniers arguments de <code>gcm_start<\/code> sont nuls (voir capture d&rsquo;\u00e9cran plus haut) : Il n&rsquo;y a donc pas de AEAD data.<\/p>\n\n\n\n<p>R\u00e9utilisons le dernier script Frida employ\u00e9 pour tracer des chiffrements successifs de diff\u00e9rents paquets r\u00e9seau en partance :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>Entering into gcm_setkey\ngcm_setkey, input : \\x46\\x64\\x59\\x57\\x35\\x48\\x54\\x6e\\x65\\x61\\x61\\x70\\x68\\x53\\x4e\\x79\ngcm_setkey, key_len : 16\nEntering into gcm_start\ngcm_start, mode : 1\ngcm_start, iv : \\x4d\\x47\\x5a\\x51\\x62\\x65\\x00\\x00\\x00\\x00\\x00\\x00\ngcm_start, iv_len : 12\nEntering into gcm_update\ngcm_update, input len : 70\ngcm_update, input : \\xe6\\x45\\x94\\x41\\x01\\xb3\\x01\\x01\\x00\\x00\\x5f\\x53\\x69\\x47\\x2e\\xae\\x5d\\x8a\\xc8\\xf8\\x43\\x9a\\x20\\xf2\\x49\\x6b\\xc4\\x2d\\x80\\xbd\\x45\\x00\\x00\\x28\\x00\\x00\\x40\\x00\\x40\\x06\\x11\\xc4\\xc0\\xa8\\x01\\x86\\x8e\\xfb\\xd7\\xe2\\xc1\\xcc\\x01\\xbb\\xe9\\xb4\\x0d\\xd1\\x00\\x00\\x00\\x00\\x50\\x04\\x00\\x00\\xcb\\xc6\\x00\\x00\ngcm_update, gcm_mode : 1\ngcm_update, output after : \\x06\\xea\\x80\\xdc\\x05\\xe6\\x92\\xea\\x6c\\x54\\xad\\x87\\xf5\\x71\\xb6\\x80\\xfb\\x03\\xae\\x3b\\x16\\xe6\\x84\\x58\\x50\\xc7\\xd5\\x01\\x37\\x94\\x6f\\x47\\x7f\\xed\\x76\\x16\\x46\\xb2\\xc7\\x89\\x34\\xbc\\x8e\\x22\\x13\\x0b\\x62\\x8a\\xf4\\xf1\\x88\\x5d\\x81\\xec\\x90\\x12\\x45\\xbb\\x41\\x3f\\xd2\\xd1\\x29\\xcc\\x62\\x2a\\x88\\xa5\\x52\\x53\nEntering into gcm_finish\ngcm_finish, tag ptr : 0x0\ngcm_finish, tag len : 0\nEntering into gcm_setkey\ngcm_setkey, input : \\x46\\x64\\x59\\x57\\x35\\x48\\x54\\x6e\\x65\\x61\\x61\\x70\\x68\\x53\\x4e\\x79\ngcm_setkey, key_len : 16\nEntering into gcm_start\ngcm_start, mode : 1\ngcm_start, iv : \\x4d\\x47\\x5a\\x51\\x62\\x65\\x00\\x00\\x00\\x00\\x00\\x00\ngcm_start, iv_len : 12\nEntering into gcm_update\ngcm_update, input len : 78\ngcm_update, input : \\x1f\\xa6\\x2e\\x1f\\x09\\x1b\\xe4\\x39\\x24\\x58\\x63\\x0c\\x50\\x8f\\x01\\x01\\x00\\x00\\x5f\\x53\\x69\\x47\\x2e\\xae\\x5d\\x8a\\xc8\\xf8\\x43\\x9a\\x20\\xf2\\x49\\x6b\\xc4\\x2d\\x80\\xbd\\x45\\x00\\x00\\x28\\x00\\x00\\x40\\x00\\x40\\x06\\x11\\xc4\\xc0\\xa8\\x01\\x86\\x8e\\xfb\\xd7\\xe2\\xc1\\xcc\\x01\\xbb\\xe9\\xb4\\x0d\\xd1\\x00\\x00\\x00\\x00\\x50\\x04\\x00\\x00\\xcb\\xc6\\x00\\x00\ngcm_update, gcm_mode : 1\ngcm_update, output after : \\xff\\x09\\x3a\\x82\\x0d\\x4e\\x77\\xd2\\x48\\x0c\\x91\\xd8\\xcc\\xb9\\x99\\x2f\\xa6\\x89\\x39\\x90\\x3c\\x3b\\x8a\\x04\\x44\\x26\\xd9\\xd4\\xf4\\xb3\\x0a\\xb5\\x36\\xae\\xb2\\x3b\\x86\\x0f\\xc2\\x8f\\x25\\x50\\x4e\\x8a\\x52\\x8d\\xac\\x77\\x32\\xd7\\x89\\x39\\x81\\xd1\\xf7\\x5d\\x9f\\x88\\x80\\xf3\\xd3\\x6a\\x90\\x7c\\x6f\\xfb\\x43\\x63\\x52\\x53\\x47\\xea\\x33\\x73\\xf8\\x29\\x38\\x2d\nEntering into gcm_finish\ngcm_finish, tag ptr : 0x0\ngcm_finish, tag len : 0<\/code><\/pre>\n\n\n\n<p>Nous observons quelque chose que nous savions d\u00e9j\u00e0 apr\u00e8s \u00e9tude de <code>SignalObfuscator::encode<\/code> : <\/p>\n\n\n\n<p>le nonce est identique (ici <code>\\x4d\\x47\\x5a\\x51\\x62\\x65\\x00\\x00\\x00\\x00\\x00\\x00<\/code>) pour deux paquets diff\u00e9rents !<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Fonction <code>gcm_finish<\/code><\/h4>\n\n\n\n<p>La fonction <code>gcm_finish<\/code> a pour argument :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>un pointeur <code>ctx<\/code> sur le contexte,<\/li>\n\n\n\n<li>un pointeur <code>tag<\/code> indiquant o\u00f9 le tag GCM doit \u00eatre copi\u00e9,<\/li>\n\n\n\n<li>un entier <code>tag_len<\/code> indiquant la longueur du tag GCM.<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"657\" height=\"663\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/gcm_finish.png\" alt=\"\" class=\"wp-image-161\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/gcm_finish.png 657w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/gcm_finish-297x300.png 297w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/gcm_finish-150x150.png 150w\" sizes=\"auto, (max-width: 657px) 100vw, 657px\" \/><\/figure>\n\n\n\n<p>La lecture du code de cette fonction montre que si `tag` et `tag_len` sont nuls, le tag GCM ne fait l&rsquo;objet d&rsquo;aucune copie.<\/p>\n\n\n\n<p>C&rsquo;est pr\u00e9cis\u00e9ment le cas ici (voir capture d&rsquo;\u00e9cran du code de `SignalObfuscator::encode`) : Le tag GCM du paquet n&rsquo;est donc copi\u00e9 nulle part !<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Script de d\u00e9chiffrement du trafic<\/h4>\n\n\n\n<p>\u00c0 ce stade, nous disposons de toute les informations n\u00e9cessaires pour d\u00e9chiffrer le trafic Secure VPN.<\/p>\n\n\n\n<p>On d\u00e9veloppe un script dissect_securevpn_traffic.py capable de d\u00e9chiffrer le trafic Secure VPN captur\u00e9.<\/p>\n\n\n\n<p>Pour fonctionner, le script a besoin :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>de l&rsquo;adresse IP, du port et \u00e9ventuellement du protocole de transport du serveur VPN,<\/li>\n\n\n\n<li>de la cl\u00e9 d&rsquo;obfuscation,<\/li>\n\n\n\n<li>d&rsquo;un pcap \u00e0 d\u00e9chiffrer.<\/li>\n<\/ul>\n\n\n\n<p>Dans l&rsquo;exemple ci-dessous, le trafic passant dans le tunnel VPN est du trafic ICMP \u00e0 destination du serveur 8.8.8.8.<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>$ .\/dissect_securevpn_traffic.py -f traffic_5.pcap -k FdYW5HTneaaphSNyMGZQbe -p 53 -a 51.81.222.204 --proto udp \ndecrypted_payload : b'l\\xe5~\\x0f\\x0b% 4\\xca\\xfb\\x976(bVo\\x01\\x01\\x00\\x00_SiG\\x9aC\\xf8\\xc8\\x8a]\\xae.\\xbd\\x80-\\xc4kI\\xf2 E\\x00\\x00T\\x00\\x00\\x00\\x003\\x01\\xcb\\x88\\x08\\x08\\x08\\x08\\xac\\x10\\x00\\x01\\x00\\x00\\x12u\\x00\\x0e\\x00\\x13\\x1d\\x13\\x1cf\\x00\\x00\\x00\\x00\\xee\\x1d\\x07\\x00\\x00\\x00\\x00\\x00\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f !\"#$%&amp;\\'()*+,-.\/01234567'\nplaintext ip_packet : b'E\\x00\\x00T\\x00\\x00\\x00\\x003\\x01\\xcb\\x88\\x08\\x08\\x08\\x08\\xac\\x10\\x00\\x01\\x00\\x00\\x12u\\x00\\x0e\\x00\\x13\\x1d\\x13\\x1cf\\x00\\x00\\x00\\x00\\xee\\x1d\\x07\\x00\\x00\\x00\\x00\\x00\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f !\"#$%&amp;\\'()*+,-.\/01234567'\n###&#91; IP ]### \n  version   = 4\n  ihl       = 5\n  tos       = 0x0\n  len       = 84\n  id        = 0\n  flags     = \n  frag      = 0\n  ttl       = 51\n  proto     = icmp\n  chksum    = 0xcb88\n  src       = 8.8.8.8\n  dst       = 172.16.0.1\n  \\options   \\\n###&#91; ICMP ]### \n     type      = echo-reply\n     code      = 0\n     chksum    = 0x1275\n     id        = 0xe\n     seq       = 0x13\n###&#91; Raw ]### \n        load      = '\\x1d\\x13\\x1cf\\x00\\x00\\x00\\x00\\xee\\x1d\\x07\\x00\\x00\\x00\\x00\\x00\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c\\x1d\\x1e\\x1f !\"#$%&amp;\\'()*+,-.\/01234567'<\/code><\/pre>\n\n\n\n<p>Le script est relativement \u00e9l\u00e9mentaire puisqu&rsquo;il identifie le d\u00e9but d&rsquo;un paquet IPv4 en recherchant le couple d&rsquo;octets `\\x45\\x00`, ce qui ne marche pas tout le temps mais est suffisant pour un PoC.<\/p>\n\n\n\n<p>Le script dissect_securevpn_traffic.py et les pcap utilis\u00e9s sont disponibles dans le d\u00e9p\u00f4t github associ\u00e9 https:\/\/github.com\/T0lva\/securevpn.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">La cl\u00e9 d&rsquo;obfuscation et le processus d&rsquo;enr\u00f4lement<\/h4>\n\n\n\n<p>Toute la s\u00e9curit\u00e9 du protocole de chiffrement de ce VPN semble reposer sur la cl\u00e9 d&rsquo;obfuscation, dont sont issus la cl\u00e9 de chiffrement et le nonce GCM.<\/p>\n\n\n\n<p>C&rsquo;est le serveur VPN (l&rsquo;infrastructure Secure VPN en r\u00e9alit\u00e9) qui communique cette cl\u00e9 au client lors de la premi\u00e8re utilisation de l&rsquo;application.<\/p>\n\n\n\n<p>Interceptons le trafic HTTPS de l&rsquo;application avec HTTP Toolkit lors de sa premi\u00e8re utilisation :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"708\" height=\"323\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/https_1st_use.png\" alt=\"\" class=\"wp-image-162\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/https_1st_use.png 708w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/https_1st_use-300x137.png 300w\" sizes=\"auto, (max-width: 708px) 100vw, 708px\" \/><\/figure>\n\n\n\n<p>Et lors d&rsquo;une utilisation ult\u00e9rieure :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"757\" height=\"293\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/https_2nd_use.png\" alt=\"\" class=\"wp-image-163\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/https_2nd_use.png 757w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/https_2nd_use-300x116.png 300w\" sizes=\"auto, (max-width: 757px) 100vw, 757px\" \/><\/figure>\n\n\n\n<p>On constate que l&rsquo;application contacte les endpoints <code>\/ip<\/code>, <code>\/v2\/devices<\/code>, <code>\/vip\/v2\/prices<\/code> et <code>\/v2\/server<\/code> de s3.free-signal.com lors de sa premi\u00e8re utilisation, et uniquement les endpoints <code>\/ip<\/code> et <code>\/vip\/v2\/prices<\/code> lors des utilisations ult\u00e9rieures.<\/p>\n\n\n\n<p>Petite diff\u00e9rence suppl\u00e9mentaire, les noms de domaine des serveurs sont utilis\u00e9s lors de la premi\u00e8re utilisation, alors que c&rsquo;est leurs adresses IP qui sont utilis\u00e9es lors des utilisations suivantes.<\/p>\n\n\n\n<p>Examinons les informations \u00e9chang\u00e9es entre l&rsquo;application et le backend Secure VPN lors de ce processus d&rsquo;enr\u00f4lement :<\/p>\n\n\n\n<p><strong>R\u00f4le des requ\u00eates sur .free-signal.com\/ip<\/strong><\/p>\n\n\n\n<p>Lorsque l&rsquo;application contacte ce endpoint, le serveur lui r\u00e9pond l&rsquo;adresse IP du t\u00e9l\u00e9phone o\u00f9 elle est install\u00e9e :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"989\" height=\"710\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/endpoint_ip_whitened.png\" alt=\"\" class=\"wp-image-164\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/endpoint_ip_whitened.png 989w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/endpoint_ip_whitened-300x215.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/endpoint_ip_whitened-768x551.png 768w\" sizes=\"auto, (max-width: 989px) 100vw, 989px\" \/><\/figure>\n\n\n\n<p><strong>R\u00f4le des requ\u00eates sur .free-signal.com\/v2\/devices<\/strong><\/p>\n\n\n\n<p>Cette requ\u00eate <code>POST<\/code> envoie un blob de donn\u00e9e au backend :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"874\" height=\"837\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/endpoint_device.png\" alt=\"\" class=\"wp-image-165\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/endpoint_device.png 874w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/endpoint_device-300x287.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/endpoint_device-768x735.png 768w\" sizes=\"auto, (max-width: 874px) 100vw, 874px\" \/><\/figure>\n\n\n\n<p>L&rsquo;envoi de requ\u00eates HTTP par l&rsquo;application utilise la classe <code>com.signallab.lib.utils.net.HttpClients<\/code>.<\/p>\n\n\n\n<p>Au sein de cette classe, c&rsquo;est la m\u00e9thode <code>request<\/code> qui fait le gros du travail :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1225\" height=\"527\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/HttpClients_request.png\" alt=\"\" class=\"wp-image-166\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/HttpClients_request.png 1225w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/HttpClients_request-300x129.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/HttpClients_request-768x330.png 768w\" sizes=\"auto, (max-width: 1225px) 100vw, 1225px\" \/><\/figure>\n\n\n\n<p>Dans cette requ\u00eate, l&rsquo;en-t\u00eate <code>s-req-token<\/code> est le condensat MD5 d&rsquo;autres champs de la requ\u00eate :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1208\" height=\"723\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/s-req-token.png\" alt=\"\" class=\"wp-image-167\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/s-req-token.png 1208w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/s-req-token-300x180.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/s-req-token-768x460.png 768w\" sizes=\"auto, (max-width: 1208px) 100vw, 1208px\" \/><\/figure>\n\n\n\n<p>Le corps de la requ\u00eate est trait\u00e9 par la m\u00e9thode <code>encode<\/code> de <code>com.signallab.lib.utils.net.HttpClients<\/code> (voir captures d&rsquo;\u00e9cran pr\u00e9c\u00e9dentes)<\/p>\n\n\n\n<p>On utilise le hook frida suivant pour tracer les appels \u00e0 cette m\u00e9thode et r\u00e9cup\u00e9rer le corps de la requ\u00eate avant traitement par <code>encode<\/code> :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>httpClients.request.implementation = function(str, map, bArr, str2)\n{\n    console.log(\"appel de request - arguments :\");\n    console.log(\"request, str : \" + str);\n    console.log(\"request, bArr : \" + hexlify(bArr) + \" (\" + stringify(bArr) + \")\");\n    console.log(\"request, str2 : \" + str2 + \" (\" + hexlify(str2) + \")\");\n\n    var result = request.call(this, str, map, bArr, str2);\n\n    console.log(\"\\nappel de request (Url : \" + str + \" ), r\u00e9sultat : \\n\" + result);\n    console.log(\"appel de request - pile d'appel :\");\n    console.log(stackTraceHere());\n\n    return result;\n};<\/code><\/pre>\n\n\n\n<p>On intercepte aussi les appels \u00e0 `encode` :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>httpClients.encode.implementation = function(bArr)\n{\n    console.log(\"appel de encode - arguments :\");\n    console.log(\"encode, bArr avant : \" + stringify(bArr));\n\n    var result = encode.call(this, bArr);\n\n    console.log(\"encode, bArr apres : \" + hexlify(bArr));\n    console.log(\"appel de encode - r\u00e9sultat : \" + result);\n    console.log(\"appel de encode - pile d'appel :\");\n    console.log(stackTraceHere());\n\n    return result;\n};<\/code><\/pre>\n\n\n\n<p>On constate que le corps de la requ\u00eate sur <code>\/v2\/devices<\/code> est un json contenant diverses informations sur le terminal :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>appel de encode - arguments :\nencode, bArr avant : \n{\n    \"dev_id\":\"c7059ee8ee463482\",\n    \"dev_model\":\"Pixel 7a\",\n    \"dev_manufacturer\":\"Google\",\n    \"dev_lang\":\"fr_FR\",\"dev_os\":\"Android 14\",\n    \"dev_country\":\"fr\",\n    \"app_package\":\"com.fast.free.unblock.secure.vpn\",\n    \"app_ver_name\":\"4.2.5\",\n    \"app_ver_code\":202403071,\n    \"dev_imsi\":\"\"\n}\n\nencode, bArr apres : \n24 fa 9a 30 67 ef e2 c8 ce d2 24 f0 36 18 73 2b 7e 41 c2 2a 78 57 5c 3b d9 2d\n64 0b 8b e1 57 e7 95 cb 2b a1 fa 64 6c c9 49 4f 1f 68 97 a4 6c 59 00 06 07 49\n9c 70 8a 2e 1a 7a e7 26 6b 5d 93 87 9e 43 6a 3a 5f 3f 8e 10 5c 8f 32 3e fb 91\n83 0f c3 1c ca 2a a4 f9 c7 ae 67 28 cd bc ad e9 36 f5 08 9f 2b 0a 5a fe 90 c5\na6 d6 fb c0 6c e2 91 73 6b 91 13 53 d9 22 a2 bd d3 9c 52 f2 68 88 7b bf 53 38\n2b cb ab 31 e6 1d b7 9a e0 a0 00 20 bd e6 f6 70 8c 16 f7 b5 60 20 6a 63 2c 0d\n3b da e2 34 01 bf 10 6d 61 71 92 31 27 1b 20 17 af b0 e3 ab 12 20 ae eb 09 3b\n0d ed 0e 91 83 7b e6 ce ec c4 99 1d f6 5c 37 72 11 70 77 4a 22 dd e4 f1 d5 81\nfc fb 0b 9e 7b 39 95 6b f5 08 2d c0 1e 35 a8 5a 0c d9 fa f3 d9 64 af 74 ab 63\ne2 88 9d e1 ed 71 b2 81 16 c0 a1 e1 cb 4a b3 55 ae<\/code><\/pre>\n\n\n\n<p>Le blob obtenu en sortie de <code>encode<\/code> est identique au corps de la requ\u00eate.<\/p>\n\n\n\n<p>Le json soumis contient deux informations qui pourraient \u00eatre assez discriminantes : le <code>dev_id<\/code> et le <code>dev_imsi<\/code>.<\/p>\n\n\n\n<p>Le nom de cette derni\u00e8re laisse penser qu&rsquo;il pourrait s&rsquo;agir de l&rsquo;identifiant IMSI. Les tests ayant \u00e9t\u00e9 men\u00e9s sur un t\u00e9l\u00e9phone d\u00e9pourvu de carte SIM, le contenu de ce champ est vide ici, si bien qu&rsquo;il n&rsquo;est pas possible d&rsquo;\u00eatre certain de sa valeur.<\/p>\n\n\n\n<p>La r\u00e9ponse du serveur est elle aussi un blob, qui est d\u00e9cod\u00e9 par la m\u00e9thode <code>com.signallab.lib.utils.net.HttpClients.decode<\/code> :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"875\" height=\"742\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/endpoint_device_response.png\" alt=\"\" class=\"wp-image-168\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/endpoint_device_response.png 875w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/endpoint_device_response-300x254.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/endpoint_device_response-768x651.png 768w\" sizes=\"auto, (max-width: 875px) 100vw, 875px\" \/><\/figure>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>appel de decode - arguments :\ndecode, bArr avant : \n5b 0a 52 1c 6e 9f 7c b0 7a 01 b0 b1 75 64 dd 22 8d c4 56 be d2 79 6f 53 67 79\nf8 ea 05 07 a1 1e 30 0f ce 00 5e 56 f8 5b c2 13 c5 a2 9d 2e c0 94 17 1b 63 c5\nd3 cc 3c ec 71 9c b5 03 ed 72 32 c0 34 04 e1 \n\nj7 : 1713962462255568\nEntering into native side of HttpClients::decode\nparam_4 : 1713962462255568\nEnd of native side of HttpClients::decode\n\ndecode, bArr apres :\n{\n    \"auth_id\": 1383550594126135778,\n    \"auth_token\": 3954827927911532616\n}<\/code><\/pre>\n\n\n\n<p>C&rsquo;est donc en r\u00e9ponse \u00e0 cette requ\u00eate que le backend fournit le couple d&rsquo;identifiants <code>auth_id<\/code> et <code>auth_token<\/code>.<\/p>\n\n\n\n<p><strong>R\u00f4le des requ\u00eates sur .free-signal.com\/vip\/v2\/prices<\/strong><\/p>\n\n\n\n<p>Apr\u00e8s d\u00e9codage, la r\u00e9ponse \u00e0 la requ\u00eate sur ce endpoint comporte des informations sur les options d&rsquo;abonnement disponibles :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide has-small-font-size\"><code>appel de request (Url : https:\/\/s3.free-signal.com\/vip\/v2\/prices\/?dev_manufacturer=Google&amp;dev_model=Pixel%207a&amp;dev_lang=fr ),\nr\u00e9sultat : \n{\n    \"product\": &#91;\n        {\"id\": \"se_year_60\", \"type\": 3, \"marked\": true, \"trial\": false, \"trial_days\": 0},\n        {\"id\": \"se_month_10\", \"type\": 2, \"marked\": false, \"trial\": false, \"trial_days\": 0},\n        {\"id\": \"se_week_6\", \"type\": 1, \"marked\": false, \"trial\": false, \"trial_days\": 0}\n    ],\n    \"popup\": \"3days\"\n}<\/code><\/pre>\n\n\n\n<p><strong>R\u00f4le des requ\u00eates sur .free-signal.com\/v2\/server<\/strong><\/p>\n\n\n\n<p>Les valeurs des en-t\u00eates <code>s-auth-id<\/code> et <code>s-auth-token<\/code> de cette m\u00e9thode sont donn\u00e9es par les identifiants <code>auth_id<\/code> et <code>auth_token<\/code> retourn\u00e9s par le endpoint <code>\/v2\/device<\/code>.<\/p>\n\n\n\n<p>Comme auparavant, <code>s-req-token<\/code> est un condensat MD5 portant sur une partie de la requ\u00eate.<\/p>\n\n\n\n<p>L&rsquo;en-t\u00eate <code>s-req-param<\/code>, est calcul\u00e9 en deux \u00e9tapes :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>On encode la cha\u00eene <code>dev_imsi=&amp;dev_lang=fr_FR<\/code> avec la fonction <code>encode<\/code> :<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>encode, bArr avant : dev_imsi=&amp;dev_lang=fr_FR\n(...)\nencode, bArr apres : 85 54 18 60 4a 82 6c 6d 3a 30 93 c0 14 16 14 cf bf a5 be 64 5d 41 59 83<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>On encode le r\u00e9sultat en base64 :<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code alignwide has-small-font-size\"><code>thomas@ankou:~\/articles\/securevpn$ echo -n hVQYYEqCbG06MJPAFBYUz7+lvmRdQVmD | base64 -d | hexdump -C\n00000000  85 54 18 60 4a 82 6c 6d  3a 30 93 c0 14 16 14 cf  |.T.`J.lm:0......|\n00000010  bf a5 be 64 5d 41 59 83                           |...d]AY.|\n00000018<\/code><\/pre>\n\n\n\n<p>Une fois d\u00e9cod\u00e9e, la r\u00e9ponse du backend contient la liste des serveurs VPN disponibles au format json.<\/p>\n\n\n\n<p>Chaque entr\u00e9e de la liste des serveurs contient :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>son adresse IP,<\/li>\n\n\n\n<li>des informations g\u00e9ographiques et de r\u00e9partition de charge (tokens \u00ab\u00a0country\u00a0\u00bb, \u00ab\u00a0area\u00a0\u00bb et \u00ab\u00a0load\u00a0\u00bb),<\/li>\n\n\n\n<li>la cl\u00e9 d&rsquo;obfuscation,<\/li>\n\n\n\n<li>l&rsquo;algorithme support\u00e9 (token \u00ab\u00a0obs_algo\u00a0\u00bb. Ce token vaut g\u00e9n\u00e9ralement 1, c&rsquo;est donc g\u00e9n\u00e9ralement AES GCM qui est employ\u00e9).<\/li>\n\n\n\n<li>un token bool\u00e9en \u00ab\u00a0is_vip\u00a0\u00bb, indiquant un serveur reserv\u00e9 aux utilisateurs ayant pay\u00e9 un abonnement,<\/li>\n\n\n\n<li>un token bool\u00e9en \u00ab\u00a0is_bt\u00a0\u00bb dont le r\u00f4le reste inconnu (le terme bt pourrait d\u00e9signer bittorrent),<\/li>\n\n\n\n<li>un token bool\u00e9en \u00ab\u00a0is_running\u00a0\u00bb,<\/li>\n\n\n\n<li>un token \u00ab\u00a0feature\u00a0\u00bb indiquant le service de VOD compatible avec le serveur.<\/li>\n<\/ul>\n\n\n\n<p>La liste des serveurs VPN est divis\u00e9e en trois sous-liste :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>serveurs VPN gratuits,<\/li>\n\n\n\n<li>serveurs VPN permettant l&rsquo;acc\u00e8s \u00e0 des services de vid\u00e9o \u00e0 la demande (Netflix, Amazon prime, etc)<\/li>\n\n\n\n<li>serveurs VPN payants.<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\">R\u00e9cup\u00e9ration de la liste des serveurs VPN sur diff\u00e9rents terminaux<\/h4>\n\n\n\n<p>Voici la liste compl\u00e8te r\u00e9cup\u00e9r\u00e9e lors d&rsquo;une installation sur t\u00e9l\u00e9phone physique :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\" style=\"font-size:0.8rem\"><code>{\n\t\"config\": {\"udp\": &#91;53, 9981],\"tcp\": &#91;443, 9981], \"tun_mtu\": 1380, \"dns_server\": &#91;\"8.8.8.8\", \"1.1.1.1\"]},\n\t\"server\": &#91;\t\t\n                {\"ip\": \"209.141.47.171\", \"country\": \"US\", \"area\": \"US West\", \"load\": 45, \"obs_key\": \"yUk4jmdCSXcZErGiutxbcc\", \"obs_algo\": 1, \"is_vip\": false, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t{\"ip\": \"205.185.121.163\", \"country\": \"US\", \"area\": \"US West\", \"load\": 18, \"obs_key\": \"VkzH5h6CAJhjp5AsBv9Mv6\", \"obs_algo\": 1, \"is_vip\": false, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t{\"ip\": \"205.185.127.80\", \"country\": \"US\", \"area\": \"US West\", \"load\": 18, \"obs_key\": \"Fh8YUC9uTVv2qJikWbXCHh\", \"obs_algo\": 1, \"is_vip\": false, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t{\"ip\": \"51.81.222.204\", \"country\": \"US\", \"area\": \"US West\", \"load\": 15, \"obs_key\": \"FdYW5HTneaaphSNyMGZQbe\", \"obs_algo\": 1, \"is_vip\": false, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t{\"ip\": \"167.114.3.117\", \"country\": \"CA\", \"area\": \"\", \"load\": 24, \"obs_key\": \"RQxyQKzBew7Qw2ZSBcVvB3\", \"obs_algo\": 1, \"is_vip\": false, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t{\"ip\": \"51.79.68.207\", \"country\": \"CA\", \"area\": \"\", \"load\": 22, \"obs_key\": \"46WxPL9JhoUGhymmzEHDiy\", \"obs_algo\": 1, \"is_vip\": false, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t{\"ip\": \"199.195.249.144\", \"country\": \"US\", \"area\": \"US East\", \"load\": 32, \"obs_key\": \"3M4gTqzzqNgjbxHGHvN5RY\", \"obs_algo\": 1, \"is_vip\": false, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t{\"ip\": \"198.98.48.96\", \"country\": \"US\", \"area\": \"US East\", \"load\": 32, \"obs_key\": \"ADwnMgVnUiSVm5Wu2i3U2D\", \"obs_algo\": 1, \"is_vip\": false, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t{\"ip\": \"15.204.245.155\", \"country\": \"US\", \"area\": \"US East\", \"load\": 31, \"obs_key\": \"Ljj3uwFrFbfWCdRFpvuUfN\", \"obs_algo\": 1, \"is_vip\": false, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t{\"ip\": \"15.204.204.38\", \"country\": \"US\", \"area\": \"US East\", \"load\": 34, \"obs_key\": \"hgsJbhxqSkmZHbfzDSByH6\", \"obs_algo\": 1, \"is_vip\": false, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t{\"ip\": \"107.189.6.242\", \"country\": \"LU\", \"area\": \"\", \"load\": 34, \"obs_key\": \"eQGxShreMX86bMhizYATE5\", \"obs_algo\": 1, \"is_vip\": false, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t{\"ip\": \"104.244.72.70\", \"country\": \"LU\", \"area\": \"\", \"load\": 35, \"obs_key\": \"UcSHeZRbBcGRj5Msfbb99f\", \"obs_algo\": 1, \"is_vip\": false, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t{\"ip\": \"51.89.166.197\", \"country\": \"GB\", \"area\": \"\", \"load\": 30, \"obs_key\": \"EuwFB6mjax2T8BzMxF2XFp\", \"obs_algo\": 1, \"is_vip\": false, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t{\"ip\": \"198.244.148.161\", \"country\": \"GB\", \"area\": \"\", \"load\": 32, \"obs_key\": \"KbxK3Rb7mQZK3JDQdnYLhT\", \"obs_algo\": 1, \"is_vip\": false, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"}\n\t],\n\t\"list\": \"10:5-61:0-8:1\",\n\t\"_features\": &#91;\n\t\t{\"type\": \"netflix\", \"name\": \"Netflix\", \"url\": \"https:\/\/tiny.one\/dmwc2rk\"},\n\t\t{\"type\": \"prime_video\", \"name\": \"Prime Video\", \"url\": \"https:\/\/tiny.one\/j336z7z6\"},\n\t\t{\"type\": \"iplayer\", \"name\": \"BBC iPlayer\", \"url\": \"https:\/\/tiny.one\/3tmkktab\"},\n\t\t{\"type\": \"hulu\", \"name\": \"Hulu\", \"url\": \"https:\/\/tiny.one\/cx2ybpw5\"},\n\t\t{\"type\": \"hbomax\", \"name\": \"HBO Max\", \"url\": \"https:\/\/tiny.one\/ath39vz4\"},\n\t\t{\"type\": \"disney+\", \"name\": \"Disney+\", \"url\": \"https:\/\/tiny.one\/ux4bbhx4\"},\n\t\t{\"type\": \"apple_tv\", \"name\": \"Apple TV+\", \"url\": \"https:\/\/tiny.one\/2mp7kupf\"},\n\t\t{\"type\": \"utorrent\", \"name\": \"\u00b5Torrent\", \"url\": \"https:\/\/tiny.one\/tn6p2cbm\"}\n\t],\n\t\"video\": {\n\t\t\"config\": {\"udp\": &#91;53, 9981], \"tcp\": &#91;443, 9981], \"tun_mtu\": 1380, \"dns_server\": &#91;\"8.8.8.8\", \"1.1.1.1\"]}, \n\t\t\"server\": &#91;\n\t\t\t{\"ip\": \"198.98.55.32\", \"country\": \"US\", \"area\": \"US East\", \"load\": 0, \"obs_key\": \"vqaFKmxyN2BNaWE6TvYg4h\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"hbomax\"}, \t\t\t\t{\"ip\": \"209.141.56.225\", \"country\": \"US\", \"area\": \"US West\", \"load\": 6, \"obs_key\": \"AVxj6k24FCcAvZyZ8XVW63\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"prime_video\"},\n\t\t\t{\"ip\": \"209.141.40.164\", \"country\": \"US\", \"area\": \"US West\", \"load\": 0, \"obs_key\": \"nMyMesqWErSQtghNX7rZkb\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"prime_video\"},\n\t\t\t{\"ip\": \"209.141.51.235\", \"country\": \"US\", \"area\": \"US West\", \"load\": 4, \"obs_key\": \"umj3YibP3Ke2MgZCaPhptR\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"apple_tv\"},\n\t\t\t{\"ip\": \"154.3.37.112\", \"country\": \"HK\", \"area\": \"\", \"load\": 0, \"obs_key\": \"RzSqPnnU9pdSVXZj2JcJhY\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"netflix\"},\n\t\t\t{\"ip\": \"174.136.206.64\", \"country\": \"US\", \"area\": \"San Jose\", \"load\": 0, \"obs_key\": \"C7EqF6Q9GcJ7rR5oLNpvRB\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"netflix\"},\n\t\t\t{\"ip\": \"174.136.206.251\", \"country\": \"US\", \"area\": \"San Jose\", \"load\": 0, \"obs_key\": \"HrmBCUjfgtMEWRAWtijSpX\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"hulu\"},\n\t\t\t{\"ip\": \"209.141.32.52\", \"country\": \"US\", \"area\": \"US West\", \"load\": 0, \"obs_key\": \"rzRdjEdKtpNLUvCa27aVLW\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"disney+\"},\n\t\t\t{\"ip\": \"51.195.201.130\", \"country\": \"GB\", \"area\": \"\", \"load\": 0, \"obs_key\": \"8xGEc6mbmZKr55zuecRVm8\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"iplayer\"}\n\t\t]\n\t},\n\t\"vip\": {\n\t\t\"config\": {\"udp\": &#91;53, 443, 9981], \"tcp\": &#91;443, 9981], \"tun_mtu\": 1380, \"dns_server\": &#91;\"8.8.8.8\", \"1.1.1.1\"]},\n\t\t\"server\": &#91;\n\t\t\t{\"ip\": \"209.141.42.117\", \"country\": \"US\", \"area\": \"Las Vegas\", \"load\": 17, \"obs_key\": \"Q27T92trww5wM8oWF75yUC\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"}, \n\t\t\t{\"ip\": \"205.185.117.71\", \"country\": \"US\", \"area\": \"Las Vegas\", \"load\": 16, \"obs_key\": \"Dj2ffYzCzzd3yTk5XBgb2k\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"}, \n\t\t\t{\"ip\": \"3.24.182.39\", \"country\": \"AU\", \"area\": \"Sydney\", \"load\": 3, \"obs_key\": \"MXMx3nm9JUks7f4Uo7RJwF\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"54.252.149.235\", \"country\": \"AU\", \"area\": \"Sydney\", \"load\": 2, \"obs_key\": \"dTxKnNoMGPMkvypUAe9ZP6\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"3.97.6.58\", \"country\": \"CA\", \"area\": \"Montreal\", \"load\": 5, \"obs_key\": \"4HoHcsidhAFrxWBpAYLofi\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"3.125.9.233\", \"country\": \"DE\", \"area\": \"Frankfurt\", \"load\": 5, \"obs_key\": \"F9Nepq3qEuTd9tUizNiizR\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"141.95.67.250\", \"country\": \"DE\", \"area\": \"Frankfurt\", \"load\": 5, \"obs_key\": \"8aPQsmnnVQ5p9wAPt2PSEL\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"13.36.174.51\", \"country\": \"FR\", \"area\": \"Paris\", \"load\": 4, \"obs_key\": \"PKe4u6NZtMgk6CGKMVQ7oZ\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"51.222.14.147\", \"country\": \"CA\", \"area\": \"Quebec\", \"load\": 9, \"obs_key\": \"enGLSZL9qEymURNZE5e8TQ\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"18.130.167.124\", \"country\": \"GB\", \"area\": \"London\", \"load\": 7, \"obs_key\": \"3j2FgHQXi2KQVueqUkWonN\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"51.195.203.119\", \"country\": \"GB\", \"area\": \"London\", \"load\": 4, \"obs_key\": \"SYtf8ca57fyjBRZEzVD4P7\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"89.31.126.175\", \"country\": \"JP\", \"area\": \"Tokyo\", \"load\": 3, \"obs_key\": \"w5tPPCeV2TP8NUkU9oNa25\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"13.230.159.152\", \"country\": \"JP\", \"area\": \"Tokyo\", \"load\": 5, \"obs_key\": \"eZKxh7cDe26ttin9iyJyAb\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"52.78.142.34\", \"country\": \"KR\", \"area\": \"Seoul\", \"load\": 5, \"obs_key\": \"6Y6kitYTcKUdt8z7hDKXYJ\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"13.212.77.39\", \"country\": \"SG\", \"area\": \"\", \"load\": 6, \"obs_key\": \"8fRwfKNtTFZtZXmwrXyFLw\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"13.250.54.203\", \"country\": \"SG\", \"area\": \"\", \"load\": 5, \"obs_key\": \"3MgARisZt6FQ6n3VFkG3Ci\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"54.179.136.114\", \"country\": \"SG\", \"area\": \"\", \"load\": 4, \"obs_key\": \"EfnnbxxzDqLbSbTpUmqfuJ\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"18.143.94.202\", \"country\": \"SG\", \"area\": \"\", \"load\": 5, \"obs_key\": \"mP6suC8zcCpBECacsT9JhA\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"54.87.9.107\", \"country\": \"US\", \"area\": \"Virginia\", \"load\": 6, \"obs_key\": \"8w8mrQDoVXr7FceTcthaiW\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"135.148.120.195\", \"country\": \"US\", \"area\": \"Virginia\", \"load\": 11, \"obs_key\": \"44yjYGyGBVkooaqCiwptwU\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"}, \t\t\t{\"ip\": \"35.85.155.32\", \"country\": \"US\", \"area\": \"Oregon\", \"load\": 4, \"obs_key\": \"jZPZzSz6QEGdCQw4TNwPPn\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"15.204.58.138\", \"country\": \"US\", \"area\": \"Oregon\", \"load\": 14, \"obs_key\": \"Y2r2vGupoMP8DaguK4aurD\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"13.234.33.201\", \"country\": \"IN\", \"area\": \"Mumbai\", \"load\": 7, \"obs_key\": \"F6YoZybfKnrrAA8nFGAJ5H\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"13.127.78.191\", \"country\": \"IN\", \"area\": \"Mumbai\", \"load\": 7, \"obs_key\": \"drqkS4UwM9X5YmJGYKBYTR\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"13.127.133.124\", \"country\": \"IN\", \"area\": \"Mumbai\", \"load\": 7, \"obs_key\": \"RKmN3NNTnHRZt4HqzFrGAX\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"15.206.82.4\", \"country\": \"IN\", \"area\": \"Mumbai\", \"load\": 5, \"obs_key\": \"4bQo4duR6txesFfGckRwPQ\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"62.216.93.210\", \"country\": \"HK\", \"area\": \"\", \"load\": 3, \"obs_key\": \"YHgVVSq5kLSJPjixFzKz6d\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"198.98.51.234\", \"country\": \"US\", \"area\": \"New York\", \"load\": 12, \"obs_key\": \"n35m6GGYKLjMuizRUWjvjV\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"198.98.62.88\", \"country\": \"US\", \"area\": \"New York\", \"load\": 16, \"obs_key\": \"3biLb3kQT47kHN6A4TXyip\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"51.15.43.251\", \"country\": \"NL\", \"area\": \"Amsterdam\", \"load\": 5, \"obs_key\": \"JvJdL5ctfwxz863WTf2yNm\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"45.90.58.238\", \"country\": \"CH\", \"area\": \"\", \"load\": 5, \"obs_key\": \"SczMbJ2F7uGYYoKpSQ9EzK\", \"obs_algo\": 0, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"45.61.185.191\", \"country\": \"US\", \"area\": \"Miami\", \"load\": 11, \"obs_key\": \"DrHTBo6trF8y4JmMHbHr4X\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"107.189.30.27\", \"country\": \"LU\", \"area\": \"\", \"load\": 10, \"obs_key\": \"kqayakjpQYMSa38AJXVdzm\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"38.54.57.155\", \"country\": \"BR\", \"area\": \"Sao Paulo\", \"load\": 4, \"obs_key\": \"2rfqxMBWxK4YuBVAKemwYL\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"45.142.215.43\", \"country\": \"LV\", \"area\": \"\", \"load\": 0, \"obs_key\": \"Za3EUxX3WqgiegdtzWphVt\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"45.67.229.246\", \"country\": \"MD\", \"area\": \"\", \"load\": 2, \"obs_key\": \"VxEKJcRMME5Yrw24vvdi5b\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"77.91.74.80\", \"country\": \"IL\", \"area\": \"\", \"load\": 1, \"obs_key\": \"FDTDigdsaxxdHAGBbnh8dk\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"193.46.56.59\", \"country\": \"TR\", \"area\": \"Istanbul\", \"load\": 13, \"obs_key\": \"PbrfQZ9GpjEqkLiM9k6sRX\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"45.144.30.124\", \"country\": \"RU\", \"area\": \"Moscow\", \"load\": 2, \"obs_key\": \"Ky6TvcsJyUrj7m8ebT3fEC\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"3.141.197.10\", \"country\": \"US\", \"area\": \"Ohio\", \"load\": 9, \"obs_key\": \"TDDTRWC8ppv4ZFkQYRihKP\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"51.68.138.221\", \"country\": \"PL\", \"area\": \"Warsaw\", \"load\": 10, \"obs_key\": \"5eitF5nVEG2uVYBYR3VFm2\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"151.80.136.62\", \"country\": \"FR\", \"area\": \"Strasbourg\", \"load\": 4, \"obs_key\": \"dL6xAa4JNKWUUfrLL9nQyf\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"},\n\t\t\t{\"ip\": \"141.94.21.110\", \"country\": \"FR\", \"area\": \"Gravelines\", \"load\": 6, \"obs_key\": \"XXYMVSrZMuKTzzyvPZrwKG\", \"obs_algo\": 1, \"is_vip\": true, \"is_bt\": false, \"is_running\": true, \"feature\": \"\"}\n\t\t]\n\t}\n}<\/code><\/pre>\n\n\n\n<p>Deux questions restent en suspens :<\/p>\n\n\n\n<p>Deux utilisateurs diff\u00e9rents verront-ils la m\u00eame liste de serveurs ?<\/p>\n\n\n\n<p>Et si un serveur est utilisable par deux utilisateurs, l&rsquo;est-il avec la m\u00eame cl\u00e9 d&rsquo;obfuscation ?<\/p>\n\n\n\n<p>Plusieurs sc\u00e9narios sont possibles :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>sc\u00e9nario 1 : la liste des serveurs et des cl\u00e9s est fixe ;<\/li>\n\n\n\n<li>sc\u00e9nario 2 : le backend fournit \u00e0 chaque client un ensemble diff\u00e9rent de serveurs ; un serveur est accessible \u00e0 deux clients l&rsquo;est avec la m\u00eame cl\u00e9 ;<\/li>\n\n\n\n<li>sc\u00e9nario 3 : le backend fournit \u00e0 chaque client un ensemble diff\u00e9rent de serveurs ; si un serveur est accessible \u00e0 deux clients, il l&rsquo;est avec des cl\u00e9s diff\u00e9rentes.<\/li>\n<\/ul>\n\n\n\n<p>Pour apporter des \u00e9l\u00e9ments de r\u00e9ponse \u00e0 ces questions, on observe la premi\u00e8re ex\u00e9cution de l&rsquo;application sur :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u00c9mulateur n\u00b01 : un \u00e9mulateur Android 14 utilisant la m\u00eame connexion wifi que le t\u00e9l\u00e9phone android utilis\u00e9 jusqu&rsquo;ici,<\/li>\n\n\n\n<li>\u00c9mulateur n\u00b02 : un \u00e9mulateur Android 14 utilisant une autre connexion wifi.<\/li>\n<\/ul>\n\n\n\n<p>Ces deux \u00e9mulateurs sont physiquement situ\u00e9s sur mon poste de travail, et cr\u00e9\u00e9s avec Android Studio. Tous deux sont des Pixel 6 Pro virtualis\u00e9s.<\/p>\n\n\n\n<p>On observe \u00e9galement la premi\u00e8re utilisation de l&rsquo;application sur trois \u00e9mulateurs Corellium (\u00e9mulateurs n\u00b03, n\u00b04 et n\u00b05). <\/p>\n\n\n\n<p>Chacun de ces trois \u00e9mulateurs utilise Android 14 et virtualise un mod\u00e8le de t\u00e9l\u00e9phone diff\u00e9rent : Huawei P8, Samsung Galaxy S7 et Samsung Galaxy Note 5.<\/p>\n\n\n\n<p>On observe les faits suivants :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Si l&rsquo;on consid\u00e8re les listes des serveurs VPN payants sur deux t\u00e9l\u00e9phones diff\u00e9rents, on constate un nombre significatif de <em>collisions<\/em> (serveur apparaissant la liste de serveur VIP des deux t\u00e9l\u00e9phones).<\/li>\n<\/ul>\n\n\n\n<p>L&rsquo;image ci-dessous montre la comparaison des serveurs payants pour les \u00e9mulateurs n\u00b03 et n\u00b04. Si la majorit\u00e9 des entr\u00e9es diff\u00e8rent, certaines sont partag\u00e9es entre ces deux \u00e9mulateurs.<\/p>\n\n\n\n<figure class=\"wp-block-image alignwide size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1908\" height=\"854\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_vip0.png\" alt=\"\" class=\"wp-image-169\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_vip0.png 1908w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_vip0-300x134.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_vip0-768x344.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_vip0-1536x687.png 1536w\" sizes=\"auto, (max-width: 1908px) 100vw, 1908px\" \/><\/figure>\n\n\n\n<p>Dans certains cas, les listes sont identiques. Ci-dessous on voit la comparaison des serveurs payants pour le t\u00e9l\u00e9phone physique initialement utilis\u00e9 (un Pixel 7) et l&rsquo;\u00e9mulateur Pixel 6 Pro (\u00e9mulateur n\u00b02). <\/p>\n\n\n\n<p>Certaines entr\u00e9es de la liste ne diff\u00e8rent que par le token \u00ab\u00a0load\u00a0\u00bb, qui repr\u00e9sente visiblement la charge \u00ab\u00a0instantan\u00e9e\u00a0\u00bb d&rsquo;un serveur, et sont en r\u00e9alit\u00e9 les m\u00eames.<\/p>\n\n\n\n<figure class=\"wp-block-image alignwide size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"873\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_vip.png\" alt=\"\" class=\"wp-image-170\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_vip.png 1920w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_vip-300x136.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_vip-768x349.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_vip-1536x698.png 1536w\" sizes=\"auto, (max-width: 1920px) 100vw, 1920px\" \/><\/figure>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Si l&rsquo;on prend la liste des serveurs VPN compatibles avec des services VOD sur deux t\u00e9l\u00e9phones diff\u00e9rents, on constate l\u00e0 encore un nombre significatifs de collisions. Dans l&rsquo;exemple ci-dessous (\u00e9mulateur n\u00b02 et \u00e9mulateur n\u00b05), les listes sont identiques, modulo le token \u00ab\u00a0load\u00a0\u00bb.<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image alignwide size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1918\" height=\"299\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_vod.png\" alt=\"\" class=\"wp-image-171\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_vod.png 1918w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_vod-300x47.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_vod-768x120.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_vod-1536x239.png 1536w\" sizes=\"auto, (max-width: 1918px) 100vw, 1918px\" \/><\/figure>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Si l&rsquo;on prend la liste des serveurs VPN gratuits sur deux t\u00e9l\u00e9phones diff\u00e9rents, ces listes sont g\u00e9n\u00e9ralement disjointes (cas des \u00e9mulateurs n\u00b03 et n\u00b04 ici) :<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image alignwide size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"367\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_gratuits1.png\" alt=\"\" class=\"wp-image-172\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_gratuits1.png 1920w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_gratuits1-300x57.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_gratuits1-768x147.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_gratuits1-1536x294.png 1536w\" sizes=\"auto, (max-width: 1920px) 100vw, 1920px\" \/><\/figure>\n\n\n\n<p>Ce n&rsquo;est cependant pas toujours le cas, et le t\u00e9l\u00e9phone physique a la m\u00eame liste de serveurs gratuits que l&rsquo;\u00e9mulateur n\u00b04 :<\/p>\n\n\n\n<figure class=\"wp-block-image alignwide size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1913\" height=\"349\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_gratuits2.png\" alt=\"\" class=\"wp-image-173\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_gratuits2.png 1913w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_gratuits2-300x55.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_gratuits2-768x140.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_gratuits2-1536x280.png 1536w\" sizes=\"auto, (max-width: 1913px) 100vw, 1913px\" \/><\/figure>\n\n\n\n<p>Les \u00e9mulateurs n\u00b01 et n\u00b02 ont eux aussi une liste de serveurs gratuits identique :<\/p>\n\n\n\n<figure class=\"wp-block-image alignwide size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"349\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_gratuits3.png\" alt=\"\" class=\"wp-image-174\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_gratuits3.png 1920w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_gratuits3-300x55.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_gratuits3-768x140.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/04\/comparaison_serveurs_gratuits3-1536x279.png 1536w\" sizes=\"auto, (max-width: 1920px) 100vw, 1920px\" \/><\/figure>\n\n\n\n<ul class=\"wp-block-list\">\n<li>La situation o\u00f9 un serveur VPN est connu de deux t\u00e9l\u00e9phones, mais avec des cl\u00e9s d&rsquo;obfuscation diff\u00e9rentes, n&rsquo;a pas \u00e9t\u00e9 observ\u00e9e.<\/li>\n<\/ul>\n\n\n\n<p>Ces diff\u00e9rentes observations vont dans le sens du sc\u00e9nario n\u00b02 : Le backend fournit \u00e0 chaque client un sous-ensemble des serveurs VPN existants. <\/p>\n\n\n\n<p>Aucune hypoth\u00e8se ne peut \u00eatre faite sur l&rsquo;heuristique utilis\u00e9e par le backend pour choisir ce sous-ensemble pour un client donn\u00e9.<\/p>\n\n\n\n<p>Dans les configurations \u00ab\u00a0captur\u00e9es\u00a0\u00bb, d\u00e8s que deux clients ont un serveur VPN gratuit en commun, alors ils l&rsquo;ont avec la m\u00eame cl\u00e9 d&rsquo;obfuscation : Comme cette cl\u00e9 est l&rsquo;unique \u00e9l\u00e9ment cryptographique intervenant dans le chiffrement du trafic, chacun de ces clients pourra d\u00e9chiffrer le trafic VPN de l&rsquo;autre !<\/p>\n\n\n\n<p>Cependant, du fait du faible nombre de terminaux utilis\u00e9s (un t\u00e9l\u00e9phone + cinq \u00e9mulateurs), il n&rsquo;est pas totalement impossible que la cl\u00e9 d&rsquo;obfuscation soit calcul\u00e9e \u00e0 partir de certains param\u00e8tres provenant du t\u00e9l\u00e9phone, et qu&rsquo;en d\u00e9finitive la situation \u00ab\u00a0clients acc\u00e9dant au m\u00eame serveur gratuit avec des cl\u00e9s diff\u00e9rentes\u00a0\u00bb puisse se produire.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">R\u00e9capitulatif des vuln\u00e9rabilit\u00e9s cryptographiques du VPN<\/h4>\n\n\n\n<p>Des vuln\u00e9rabilit\u00e9s cryptographiques <em>spectaculaires<\/em> sont pr\u00e9sentes dans Secure VPN :<\/p>\n\n\n\n<p>La plus s\u00e9rieuse est le m\u00e9canisme de gestion de cl\u00e9s : L\u00e0 o\u00f9 tout protocole VPN \u00ab\u00a0raisonnable\u00a0\u00bb comme OpenVPN, IPsec ou Wireguard aurait utilis\u00e9 un handshake initial (authentification du serveur via un certificat racine connu de l&rsquo;application + \u00e9laboration d&rsquo;un secret commun via un \u00e9change Diffie-Hellman), Secure VPN n&rsquo;utilise aucun protocole connu et invente un m\u00e9canisme de gestion de cl\u00e9s relativement <em>original<\/em> : Lorsqu&rsquo;elle est install\u00e9e, l&rsquo;application r\u00e9cup\u00e8re un sous-ensemble de la liste des serveurs existants.<\/p>\n\n\n\n<p>Cet \u00e9change est prot\u00e9g\u00e9 par un canal HTTPS et ne peut pas \u00eatre intercept\u00e9 \u00ab\u00a0facilement\u00a0\u00bb. <\/p>\n\n\n\n<p>La situation ne serait donc pas si grave si la cl\u00e9 d&rsquo;obfuscation pour un serveur donn\u00e9 variait d&rsquo;un client \u00e0 l&rsquo;autre : On pourrait imaginer que le backend d\u00e9rive la cl\u00e9 d&rsquo;obfuscation \u00e0 partir d&rsquo;identifiants du t\u00e9l\u00e9phone (<code>dev_imsi<\/code> et <code>dev_id<\/code>, par exemple) et d&rsquo;une cl\u00e9 secr\u00e8te gard\u00e9e au chaud dans le backend. <\/p>\n\n\n\n<p>Il n&rsquo;est pas compl\u00e8tement exclu qu&rsquo;un m\u00e9canisme de \u00ab\u00a0diversification\u00a0\u00bb soit pr\u00e9sent, mais si c&rsquo;est le cas, il est clairement probl\u00e9matique puisqu&rsquo;on observe des terminaux diff\u00e9rents utilisant les m\u00eames serveurs avec les m\u00eames cl\u00e9s d&rsquo;obfuscation.<\/p>\n\n\n\n<p>\u00c0 vrai dire, il est assez peu probable qu&rsquo;un tel m\u00e9canisme existe, car cela n\u00e9cessiterait que chaque serveur VPN conserve un nombre de cl\u00e9s d&rsquo;obfuscation relativement impportant (il faut garder \u00e0 l&rsquo;esprit que l&rsquo;application fait l&rsquo;objet de plus de cent millions de t\u00e9l\u00e9chargements !).<\/p>\n\n\n\n<p>On est donc sur ce qui peut arriver de pire pour un utilisateur d&rsquo;un VPN : \u00c0 peu pr\u00e8s n&rsquo;importe qui, pour peu qu&rsquo;il r\u00e9ussisse \u00e0 obtenir la m\u00eame liste de serveurs VPN que vous, peut d\u00e9chiffrer votre trafic VPN !<\/p>\n\n\n\n<p>M\u00eame si ce probl\u00e8me d&rsquo;utilisation de cl\u00e9s secr\u00e8tes communes \u00e0 plusieurs utilisateurs n&rsquo;existait pas, Secure VPN souffrirait de toute fa\u00e7on de vuln\u00e9rabilit\u00e9s tr\u00e8s probl\u00e9matiques :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>L&rsquo;integrit\u00e9 du trafic n&rsquo;est pas garantie : La fonction <code>SignalObfuscator::encode<\/code> calcule bien le tag GCM du paquet \u00e0 \u00e9mettre\u2026mais ne l&rsquo;utilise pas !<\/li>\n\n\n\n<li>R\u00e9utilisation du nonce GCM : La fonction <code>SignalObfuscator::encode<\/code> chiffre chaque paquet r\u00e9seau ind\u00e9pendamment des autres, en r\u00e9utilisant \u00e0 chaque fois le nonce extrait de la cl\u00e9 d&rsquo;obfuscation. Comme le mode GCM utilise le mode CTR, il est possible de capturer le trafic et d&rsquo;obtenir une quantit\u00e9 significative d&rsquo;information en xorant des paquets deux \u00e0 deux (puisqu&rsquo;ils sont toujours xor\u00e9s avec la m\u00eame suite chiffrante).<\/li>\n\n\n\n<li>Enfin, et c&rsquo;est un d\u00e9tail au vu de tout ce qui pr\u00e9c\u00e8de, aucune garantie en terme de <em>forward secrecy<\/em> n&rsquo;est possible dans la mesure o\u00f9 l&rsquo;on utilise une cl\u00e9 secr\u00e8te : Si j&rsquo;arrive \u00e0 mettre la main sur la cl\u00e9 d&rsquo;obfuscation stock\u00e9e dans votre t\u00e9l\u00e9phone, je suis en mesure de d\u00e9chiffrer votre trafic Secure VPN.<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\">Conclusion<\/h4>\n\n\n\n<p>La morale de cet article et que l&rsquo;on peut faire quelques d\u00e9couvertes surprenantes, y compris dans des applications massivement utilis\u00e9es, d\u00e8s que l&rsquo;on s&rsquo;y int\u00e9resse de pr\u00e8s.<\/p>\n\n\n\n<p>On peut s&rsquo;interroger sur la raison de d\u00e9velopper un VPN comportant des failles aussi s\u00e9rieuses. Une hypoth\u00e8se assez naturelle, quoique l\u00e9g\u00e8rement complotiste, serait d&rsquo;y voir une volont\u00e9 d\u00e9lib\u00e9r\u00e9e d&rsquo;introduire des portes d\u00e9rob\u00e9es dans l&rsquo;application, afin de pouvoir acc\u00e9der au trafic des utilisateurs. En r\u00e9alit\u00e9, cela ne tient pas la route : L&rsquo;administrateur de Secure VPN a par d\u00e9finition acc\u00e8s au trafic utilisateur en clair puisqu&rsquo;il a la main sur les serveurs VPN !<\/p>\n\n\n\n<p>La r\u00e9alit\u00e9 est probablement bien plus prosa\u00efque : il est beaucoup plus probable que l&rsquo;application ait \u00e9t\u00e9 d\u00e9velopp\u00e9e par des d\u00e9veloppeurs ayant quelques lacunes en cryptologie.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Un grand nombre de gens utilise un VPN, pour diverses raisons plus ou moins pertinentes. Si certains acteurs du march\u00e9 donnent des gages de transparence en divulguant leur code source et\/ou en publiant r\u00e9guli\u00e8rement des rapports d&rsquo;audits, de nombreux autres fournisseurs font preuve d&rsquo;une r\u00e9elle opacit\u00e9. \u00c9tudier attentivement un client VPN pourrait donc r\u00e9v\u00e9ler quelques [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-130","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/posts\/130","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/comments?post=130"}],"version-history":[{"count":13,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/posts\/130\/revisions"}],"predecessor-version":[{"id":319,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/posts\/130\/revisions\/319"}],"wp:attachment":[{"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/media?parent=130"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/categories?post=130"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/tags?post=130"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}