{"id":189,"date":"2024-05-01T11:22:13","date_gmt":"2024-05-01T11:22:13","guid":{"rendered":"https:\/\/tolva.fr\/?p=189"},"modified":"2025-03-08T22:13:44","modified_gmt":"2025-03-08T22:13:44","slug":"reverse-engineering-of-an-android-vpn","status":"publish","type":"post","link":"https:\/\/tolva.fr\/index.php\/2024\/05\/01\/reverse-engineering-of-an-android-vpn\/","title":{"rendered":"Reverse engineering of an Android VPN"},"content":{"rendered":"\n<p>Nowadays a large number of people use a VPN, for various more or less relevant reasons.<\/p>\n\n\n\n<p>While some VPN providers give guarantees of transparency by disclosing their source code and\/or periodically publishing technical audits reports, many others providers are really opaque.<br>Carefully study one of these VPN client could therefore reveal some surprises\u2026<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">The Secure VPN application<\/h4>\n\n\n\n<p>I wanted to study the behaviour of such a VPN, so i&rsquo;ve (quite randomly !) chosen to study Secure VPN.<\/p>\n\n\n\n<p>This application is quite popular since it has been downloaded more than 100 millions times, more than NordVPN and 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\/05\/securevpn_popularity-486x1080.png\" alt=\"\" class=\"wp-image-190\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_popularity-486x1080.png 486w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_popularity-135x300.png 135w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_popularity-768x1707.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_popularity-691x1536.png 691w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_popularity-922x2048.png 922w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_popularity.png 1080w\" sizes=\"auto, (max-width: 486px) 100vw, 486px\" \/><\/figure>\n\n\n\n<p>The Google Play page of the application specifies it is developed by 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\/05\/securevpn_securesignal-486x1080.png\" alt=\"\" class=\"wp-image-191\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_securesignal-486x1080.png 486w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_securesignal-135x300.png 135w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_securesignal-768x1707.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_securesignal-691x1536.png 691w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_securesignal-922x2048.png 922w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_securesignal.png 1080w\" sizes=\"auto, (max-width: 486px) 100vw, 486px\" \/><\/figure>\n\n\n\n<p>Contrary to what this name could lead to believe, this society has nothing to do with the editor of the Signal application.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Secure VPN app website<\/h4>\n\n\n\n<p>On the Google Play page, we can find an email address (secure-vpn@free-signal.com), a physical address and a website to contact the developer:<\/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\/05\/securevpn_contact_1-486x1080.png\" alt=\"\" class=\"wp-image-192\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_contact_1-486x1080.png 486w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_contact_1-135x300.png 135w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_contact_1-768x1707.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_contact_1-691x1536.png 691w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_contact_1-922x2048.png 922w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_contact_1.png 1080w\" sizes=\"auto, (max-width: 486px) 100vw, 486px\" \/><\/figure>\n\n\n\n<p>This website is securesignal.app. <\/p>\n\n\n\n<p>We can see the laudatory testimony of Marsha Singer, Tim Shaw and 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\/05\/website1.png\" alt=\"\" class=\"wp-image-193\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/website1.png 1789w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/website1-300x158.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/website1-768x404.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/website1-1536x808.png 1536w\" sizes=\"auto, (max-width: 1789px) 100vw, 1789px\" \/><\/figure>\n\n\n\n<p>By a quite extraordinary chance, these three people has Doppelg\u00e4ngers who gave equally laudatory testimonials for another application, apptuary.com. <\/p>\n\n\n\n<p>This application has a dedicated website whose appearance is quite similar to 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\/05\/apptuary.png\" alt=\"\" class=\"wp-image-194\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/apptuary.png 1790w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/apptuary-300x158.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/apptuary-768x404.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/apptuary-1536x807.png 1536w\" sizes=\"auto, (max-width: 1790px) 100vw, 1790px\" \/><\/figure>\n\n\n\n<p>Another contact email appears on the 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\/05\/securesignalcontact.png\" alt=\"\" class=\"wp-image-195\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securesignalcontact.png 669w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securesignalcontact-300x113.png 300w\" sizes=\"auto, (max-width: 669px) 100vw, 669px\" \/><\/figure>\n\n\n\n<p>The rest of the site gives some indications regarding the technical capabilities of the VPN, with the usual promises (No Log, Safe Protocol, Privacy, etc), without giving any detail.<\/p>\n\n\n\n<p>The free-signal.com and secure.free-signal.com websites don&rsquo;t give any clue either.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">First analysis with Exodus privacy<\/h4>\n\n\n\n<p>A user who wants to know whether an application is intrusive but doesn&rsquo;t know how\/doesn&rsquo;t want to extract the manifset can use the website https:\/\/reports.exodus-privacy.eu.org.<\/p>\n\n\n\n<p>It turns out that Secure VPN uses 2 trackers and requires 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\/05\/exodus_rapport1.png\" alt=\"\" class=\"wp-image-196\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/exodus_rapport1.png 977w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/exodus_rapport1-300x127.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/exodus_rapport1-768x326.png 768w\" sizes=\"auto, (max-width: 977px) 100vw, 977px\" \/><\/figure>\n\n\n\n<p>Some of these 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\/05\/exodus_rapport2.png\" alt=\"\" class=\"wp-image-197\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/exodus_rapport2.png 813w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/exodus_rapport2-256x300.png 256w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/exodus_rapport2-768x900.png 768w\" sizes=\"auto, (max-width: 813px) 100vw, 813px\" \/><\/figure>\n\n\n\n<p>such as <\/p>\n\n\n\n<p><code>QUERY_ALL_PACKAGES<\/code>, <code>ACCESS_ADSERVICES_TOPICS<\/code>, <code>ACCESS_ADSERVICES_ATTRIBUTION<\/code> or <code>ACCESS_ADSERVICES_AD_ID<\/code> <\/p>\n\n\n\n<p>are not required by a VPN app, and are here only for adverstisement\/profiling needs (<code>QUERY_ALL_PACKAGES<\/code> allows to list the applications installed on a phone, which can say a lot about its owner).<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">First use<\/h4>\n\n\n\n<p>Secure VPN interface is very simple.<\/p>\n\n\n\n<p>When launched for the first time, the application asks the user if s.he consent to their personal data being used for various purposes, including advertising:<\/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\/05\/securevpn_approbation_2-486x1080.png\" alt=\"\" class=\"wp-image-198\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_approbation_2-486x1080.png 486w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_approbation_2-135x300.png 135w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_approbation_2-768x1707.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_approbation_2-691x1536.png 691w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_approbation_2-922x2048.png 922w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_approbation_2.png 1080w\" sizes=\"auto, (max-width: 486px) 100vw, 486px\" \/><\/figure>\n\n\n\n<p>The user then has access to a grayed icon which must be clicked on to mount the VPN tunnel:<\/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\/05\/securevpn_main_1-486x1080.png\" alt=\"\" class=\"wp-image-199\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_main_1-486x1080.png 486w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_main_1-135x300.png 135w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_main_1-768x1707.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_main_1-691x1536.png 691w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_main_1-922x2048.png 922w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_main_1.png 1080w\" sizes=\"auto, (max-width: 486px) 100vw, 486px\" \/><\/figure>\n\n\n\n<p>The connection icon turns blue when the VPN tunnel is mounted:<\/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\/05\/securevpn_connection_1-486x1080.png\" alt=\"\" class=\"wp-image-200\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_connection_1-486x1080.png 486w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_connection_1-135x300.png 135w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_connection_1-768x1707.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_connection_1-691x1536.png 691w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_connection_1-922x2048.png 922w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_connection_1.png 1080w\" sizes=\"auto, (max-width: 486px) 100vw, 486px\" \/><\/figure>\n\n\n\n<p>A visit to whatismyip.com confirms that the IP address observed by a visited website is different from the actual IP address of the phone:<\/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\/05\/securevpn_whatismyip-486x1080.png\" alt=\"\" class=\"wp-image-201\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_whatismyip-486x1080.png 486w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_whatismyip-135x300.png 135w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_whatismyip-768x1707.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_whatismyip-691x1536.png 691w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_whatismyip-922x2048.png 922w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/securevpn_whatismyip.png 1080w\" sizes=\"auto, (max-width: 486px) 100vw, 486px\" \/><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">The manifest<\/h4>\n\n\n\n<p>Having a look at the AndroidManifest.xml file of a studied application is an essential step, and we will be no exception.<\/p>\n\n\n\n<p>The manifest declares 24 activities. Among these, 18 are part of the application itself (all are members of the package <code>com.signallab.secure.activity<\/code>).<\/p>\n\n\n\n<p>We will not dwell on the activities of the application: Indeed, the purpose of a VPN client is to encrypt\/decrypt incoming traffic.<\/p>\n\n\n\n<p>This task, carried out in the background, requires the execution of a service. The manifest declares 12 services. <\/p>\n\n\n\n<p>Two of them, <code>com.signallab.secure.service.SecureService<\/code> and <code>com.signallab.lib.SignalService<\/code>, <\/p>\n\n\n\n<p>are part of the application itself:<\/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\/05\/manifest_list_services.png\" alt=\"\" class=\"wp-image-202\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/manifest_list_services.png 750w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/manifest_list_services-298x300.png 298w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/manifest_list_services-150x150.png 150w\" sizes=\"auto, (max-width: 750px) 100vw, 750px\" \/><\/figure>\n\n\n\n<p>These two services inherit from the android.net.VpnService class, a class used to implement a VPN client. Such a service will create a virtual network interface (<code>\/dev\/tun<\/code>, typically).<\/p>\n\n\n\n<p>Once the VPN is launched,<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>phone applications send their network traffic over this interface,<\/li>\n\n\n\n<li>the VPN service reads the packets that applications want to send, encrypts them and sends them,<\/li>\n\n\n\n<li>the VPN service receives encrypted packets from the VPN server, decrypts them and writes them to the virtual interface,<\/li>\n\n\n\n<li>applications receive these decrypted packets on this virtual interface.<\/li>\n<\/ul>\n\n\n\n<p>The class <code>SignalService<\/code> contains a method <code>loop()<\/code>, who calls a method <code>connect()<\/code> from <code>SignalHelper<\/code> class:<\/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\/05\/SignalServiceLoop.png\" alt=\"\" class=\"wp-image-203\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/SignalServiceLoop.png 1345w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/SignalServiceLoop-300x183.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/SignalServiceLoop-768x468.png 768w\" sizes=\"auto, (max-width: 1345px) 100vw, 1345px\" \/><\/figure>\n\n\n\n<p>The <code>connect()<\/code> method is a native one:<\/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\/05\/SignalHelperNativeMethods.png\" alt=\"\" class=\"wp-image-204\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/SignalHelperNativeMethods.png 993w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/SignalHelperNativeMethods-300x160.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/SignalHelperNativeMethods-768x410.png 768w\" sizes=\"auto, (max-width: 993px) 100vw, 993px\" \/><\/figure>\n\n\n\n<p>This method is called when the user clicks on the login icon mentioned before.<\/p>\n\n\n\n<p>We can trace the call to this <code>connect()<\/code> method with Frida (it&rsquo;s the trace_SignalHelper_connect.js script in the associated github https:\/\/github.com\/T0lva\/securevpn):<\/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>The <code>connect()<\/code> method arguments are:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The file descriptor on the virtual interface <code>\/dev\/tun<\/code>,<\/li>\n\n\n\n<li>the IP address of the VPN server to reach,<\/li>\n\n\n\n<li>the UDP and TCP ports on which to contact the VPN server,<\/li>\n\n\n\n<li>some integers (<code>userId<\/code> et <code>userToken<\/code>) who identify the user,<\/li>\n\n\n\n<li>a key (which value is <code>Fh8YUC9uTVv2qJikWbXCHh<\/code> here),<\/li>\n\n\n\n<li>a boolean value and a constant who indicated the cryptographic algorithm to use.<\/li>\n<\/ul>\n\n\n\n<p>According to Android documentation, (https:\/\/developer.android.com\/reference\/java\/lang\/Thread#start()), calling the <code>start<\/code> method of a <code>Thread<\/code> object induces the call of the <code>run<\/code> method of the object in question.<\/p>\n\n\n\n<p>The <code>start<\/code> method of <code>VpnThread<\/code> is precisely called in the <code>onStartCommand<\/code> method of <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\/05\/SignalService_onStartCommand.png\" alt=\"\" class=\"wp-image-205\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/SignalService_onStartCommand.png 517w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/SignalService_onStartCommand-300x100.png 300w\" sizes=\"auto, (max-width: 517px) 100vw, 517px\" \/><\/figure>\n\n\n\n<p>This method <code>onStartCommand<\/code> is the entry point by which this service can be launched.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">The application&rsquo;s native libraries<\/h4>\n\n\n\n<p>The application embeds 3 native libraries, libz.so, liblog.so and libchannel.so.<\/p>\n\n\n\n<p>The latter is significantly larger than the two other, and it is this one who implements most of the native methods used by the application&rsquo;s java classes.<\/p>\n\n\n\n<p>Intuitively, the \u00ab\u00a0easiest\u00a0\u00bb way to create a VPN client is to \u00ab\u00a0wrap\u00a0\u00bb an OpenVPN client into an apk: The application will contain a class inheriting from <code>android.net.VpnService<\/code>, which will eventually launch the client native.<\/p>\n\n\n\n<p>None of the functions exported by libchannel.so contain in their name the keywords \u00ab\u00a0openvpn\u00a0\u00bb or \u00ab\u00a0ovpn\u00a0\u00bb, contained by a large number of functions exported by the libovpn library used in this type of applications:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1089\" height=\"79\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/libovpn.png\" alt=\"\" class=\"wp-image-206\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/libovpn.png 1089w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/libovpn-300x22.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/libovpn-768x56.png 768w\" sizes=\"auto, (max-width: 1089px) 100vw, 1089px\" \/><\/figure>\n\n\n\n<p>This suggests that Secure VPN does not use OpenVPN, although it is possible that libchannel.so is a customized version of libovpn.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Traffic exchanged with the VPN server<\/h4>\n\n\n\n<p>If we examine the traffic intercepted after launching the application and setting up the VPN tunnel, we observe, over the sessions:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>mainly traffic with tcp port 443 of the server,<\/li>\n\n\n\n<li>or mainly traffic with tcp port 9981 of the server,<\/li>\n\n\n\n<li>or mainly traffic with udp port 53 of the server.<\/li>\n<\/ul>\n\n\n\n<p>Although port 443 is dedicated to TLS traffic, the captured traffic is not TLS traffic, an application TLS packet necessarily begins with <code>\\x17\\x03<\/code>:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1496\" height=\"955\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/traffic_443.png\" alt=\"\" class=\"wp-image-207\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/traffic_443.png 1496w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/traffic_443-300x192.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/traffic_443-768x490.png 768w\" sizes=\"auto, (max-width: 1496px) 100vw, 1496px\" \/><\/figure>\n\n\n\n<p>In the same way, traffic on port 53 is not DNS traffic, as indicated by the errors reported by wireshark:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1547\" height=\"955\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/traffic_53.png\" alt=\"\" class=\"wp-image-208\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/traffic_53.png 1547w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/traffic_53-300x185.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/traffic_53-768x474.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/traffic_53-1536x948.png 1536w\" sizes=\"auto, (max-width: 1547px) 100vw, 1547px\" \/><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">Descent to native layer<\/h4>\n\n\n\n<p>Calling the <code>connect()<\/code> method of the <code>SignalHelper<\/code> class results in a call to the function of the same name in libchannel.so.<\/p>\n\n\n\n<p>This function creates an object of the <code>SignalLinkClient<\/code> class and positions different fields of this object with the functions <code>setSignalRouter<\/code>, <code>enableObscure<\/code>, <code>setUsers<\/code>, <code>setProto<\/code>, <code>setBackupPort<\/code>, <code>connect<\/code> and <code>setTunnel<\/code>, before to call the <code>runLoop<\/code> function which is the main infinite loop of the VPN client service.<\/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\/05\/native_connect.png\" alt=\"\" class=\"wp-image-209\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/native_connect.png 718w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/native_connect-300x208.png 300w\" sizes=\"auto, (max-width: 718px) 100vw, 718px\" \/><\/figure>\n\n\n\n<p>Let&rsquo;s give some details on the respective roles of these initialization functions:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The <code>SignalLinkClient<\/code> initializes an object <code>SignalPackage<\/code>. One of the fields of the <code>SignalLinkClient<\/code> points toward this <code>SignalPackage<\/code> object. The 8 first bytes of the <code>SignalPackage<\/code> then point to a 1500 bytes buffer. 1500 is a common MTU value: This buffer will probably be used to handle network packets.<\/li>\n\n\n\n<li><code>SignalLinkClient::setSignalRouter<\/code>: This function copies the address of a static object <code>SignalRouter<\/code> at the beginning of the <code>SignalLinkClient<\/code>. The <code>SignalRouter<\/code> object contains several function pointers. As those functions are not used in packet encryption\/decryption, we won&rsquo;t give much attention to them.<\/li>\n\n\n\n<li>The function<code>SignalLinkClient::enableObscure<\/code> modifies the <code>SignalPackage<\/code> adressed by one of the field of the <code>SignalLinkClient<\/code>: At the end of this function, one of the fields of the <code>SignalPackage<\/code> points toward a <code>SignalObfuscator<\/code>, who essentially contains the obfuscation key. This obfuscation key is one of the parameters of the <code>SignalHelper.connect<\/code> function.<\/li>\n\n\n\n<li><code>SignalLinkClient::setUser<\/code> copies the intergers <code>userId<\/code> and <code>userToken<\/code> into the <code>SignalLinkClient<\/code>.<\/li>\n\n\n\n<li><code>SignalLinkClient::setProto<\/code> and <code>SignalLinkClient::setBackupPort<\/code> respective effects is 1\/to set some booleans who indicate the supported protocols (UDP and TCP) 2\/to set port numbers.<\/li>\n\n\n\n<li>The <code>SignalLinkClient::connect<\/code> function initializes the <code>RemoteLink *<\/code> arrays, which are objects who describe a VPN server (IP address + port + protocol).<\/li>\n\n\n\n<li>Finally, the function <code>SignalLinkClient::setTunnel<\/code> copies the file descriptor on the <code>\/dev\/tun<\/code> virtual interface into the <code>SignalLinkClient<\/code>.<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\">The infinite processing loop, <code>SignalLinkClient::runLoop()<\/code><\/h4>\n\n\n\n<p>Once all these initialization steps are done, the <code>runLoop<\/code> function is called.<\/p>\n\n\n\n<p>Basically, this function is an infinite loop in which some files descriptors are monitored through the <code>epoll<\/code> api:<\/p>\n\n\n\n<p>When an application wants to send a network packet, this plaintext packet is written on <code>\/dev\/tun<\/code>. The VPN client reads this packet, encrypts it and send it to the VPN server.<\/p>\n\n\n\n<p>Conversely, an packet incoming from the VPN server is decrypted and written on <code>\/dev\/tun<\/code> in order to be delivered to the recipient application.<\/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\/05\/runLoop.png\" alt=\"\" class=\"wp-image-210\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/runLoop.png 680w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/runLoop-233x300.png 233w\" sizes=\"auto, (max-width: 680px) 100vw, 680px\" \/><\/figure>\n\n\n\n<p>Thus,<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The unencrypted packet to send are read on <code>\/dev\/tun<\/code>, encrypted and sent to the server by the <code>SignalLinkClient::processTunIn<\/code> function,<\/li>\n\n\n\n<li>The encrypted packets coming from the VPN server are decrypted and written on <code>\/dev\/tun<\/code> by the <code>SignalLinkClient::processLinkData<\/code> function.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Outgoing packets processing<\/h3>\n\n\n\n<h5 class=\"wp-block-heading\">The outgoing packets <code>SignalLinkClient::processTunIn<\/code> processing function<\/h5>\n\n\n\n<p>Firstly, this function reads the packet to send from <code>\/dev\/tun<\/code> into the <code>0x468<\/code> offset of the <code>SignalLinkClient<\/code> object. <\/p>\n\n\n\n<p>The packet is then processed by the <code>SignalLinkClient::writeToLink<\/code> subfunction.<\/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\/05\/processTunIn.png\" alt=\"\" class=\"wp-image-211\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/processTunIn.png 677w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/processTunIn-271x300.png 271w\" sizes=\"auto, (max-width: 677px) 100vw, 677px\" \/><\/figure>\n\n\n\n<h5 class=\"wp-block-heading\">The <code>SignalLinkClient::writeToLink<\/code> function<\/h5>\n\n\n\n<p>Firstly, this function performs initialization steps. <\/p>\n\n\n\n<p>One of these steps is to write to magic word `\\x01\\x00_SiG` in the buffer whose address is stored at the beginning of the `SignalPackage`.<\/p>\n\n\n\n<p>The rest of the work is done by the `SignalPackage::setData`, and the encrypted packet is then sent to the VPN server.<\/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\/05\/writeToLink.png\" alt=\"\" class=\"wp-image-212\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/writeToLink.png 722w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/writeToLink-300x270.png 300w\" sizes=\"auto, (max-width: 722px) 100vw, 722px\" \/><\/figure>\n\n\n\n<h5 class=\"wp-block-heading\">The `SignalPackage::setData` function<\/h5>\n\n\n\n<p>After some preliminary checks, the <code>SignalPackage::setData<\/code> function computes two 64-bits integers from the <code>userId<\/code> and <code>userToken<\/code> integers who identify the user.<\/p>\n\n\n\n<p>These two computed integers are written in a buffer whose address is stored in the <code>SignalPackage<\/code> object. The plaintext packet is copied just after these two integers.<\/p>\n\n\n\n<p>The purpose of these 128 bits could be to give a way to the VPN server to identify which user has sent a given packet.<\/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\/05\/setData.png\" alt=\"\" class=\"wp-image-213\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/setData.png 726w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/setData-272x300.png 272w\" sizes=\"auto, (max-width: 726px) 100vw, 726px\" \/><\/figure>\n\n\n\n<p>The <code>SignalObfuscator::encode<\/code> is finally called. This is this very function who will call all the cryptographic functions who encrypt the packet.<\/p>\n\n\n\n<h5 class=\"wp-block-heading\">The <code>SignalObfuscator::encode<\/code> function<\/h5>\n\n\n\n<p>This function contains two logical blocks conditioned by the value of its last argument, <code>algo<\/code>.<\/p>\n\n\n\n<p>If <code>algo<\/code> is set to <code>1<\/code>, then the packet is encrypted with AES GCM. If it is set to <code>0<\/code>, encryption is done using ChaCha20.<\/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\/05\/encode.png\" alt=\"\" class=\"wp-image-214\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/encode.png 724w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/encode-252x300.png 252w\" sizes=\"auto, (max-width: 724px) 100vw, 724px\" \/><\/figure>\n\n\n\n<p>As we will see later, most of VPN servers use AES. We therefore won&rsquo;t dwell on the second block, the one who calls ChaCha20.<\/p>\n\n\n\n<p>The heuristic used to encrypt a plaintext packet is the following:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The 16 first bytes of the obfuscation key are used as an AES key (via <code>gcm_setkey<\/code>, who calls <code>aes_setkey<\/code>, who in its turn calls <code>aes_set_encryption_key<\/code>).<\/li>\n\n\n\n<li>The next 12 bytes of the obfuscation key are used as the GCM nonce, via <code>gcm_start<\/code> function. In practice, the obfuscation key is not long enough (it should have at least 16 + 12 = 28 bytes), and the 6 last bytes of the nonce are always set to zero.<\/li>\n\n\n\n<li>The packet is AES-GCM encrypted using <code>gcm_update<\/code>. The encrypted packet is written inside the <code>SignalObfuscator<\/code> object.<\/li>\n\n\n\n<li>Then the GCM tag is generated using <code>gcm_finish<\/code> function.<\/li>\n\n\n\n<li>Finally, the encrypted packet is then copied in the location initially occupied by the plaintext packet. As seen before, the sending of the encrypted packet is done at the end of the <code>SignalLinkClient::writeToLink<\/code> function.<\/li>\n<\/ul>\n\n\n\n<h5 class=\"wp-block-heading\">The incoming packets <code>SignalLinkClient::processLinkData<\/code> processing function<\/h5>\n\n\n\n<p>Once we understand well enough how outgoing packets are processed, studying the incoming packets processing chain is much more easy !<\/p>\n\n\n\n<p>Let&rsquo;s briefly describe how a packet is decrypted:<\/p>\n\n\n\n<p>The received packets is processed by the <code>SignalLinkClient::writeToTun<\/code> function. <\/p>\n\n\n\n<p>Inside it, the packet decryption is done by <code>SignalPackage::decodePackage<\/code>, via the <code>SignalObfuscator::decode<\/code> function, the counterpart of <code>SignalObfuscator::encode<\/code> for decryption side.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Is it really AES ?<\/h4>\n\n\n\n<p>Even if libchannel contains some AES-something functions, it cannot be excluded that, voluntarily or not, the developer used something else than AES. <\/p>\n\n\n\n<p>It is therefore preferable to ensure that the encryption is actually done by AES.<\/p>\n\n\n\n<p>The encryption function, called in <code>gcm_setkey<\/code> and <code>gcm_update<\/code>, is the <code>aes_cipher<\/code> function.<\/p>\n\n\n\n<p>We could analyze the Ghidra-decompiled code to ensure that this function really perform an AES encryption, but encryption algorithm are complicated animals. Moreover, we can hook <code>aes_cipher<\/code> with Frida, so why bother ?<\/p>\n\n\n\n<p>We therefore launch a script (trace_AesGm.js) who intercepts <code>gcm_setkey<\/code>, <code>gcm_update<\/code> and <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>The script prints the input and the output of <code>gcm_update<\/code>, and also the nonce and the encryption key.<\/p>\n\n\n\n<p> By the way, we can see that the arguments of <code>gcm_finish<\/code>, namely the pointer and the length of the tag, are null.<\/p>\n\n\n\n<p>Let&rsquo;s reproduce the AES GCM computation with a few lines of python (decrypt.py script):<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>```\n#!\/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}\")\n```<\/code><\/pre>\n\n\n\n<p>This script produces the same output as the one observed at the end of <code>gcm_finish<\/code>: The VPN client really uses AES 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 of the cryptographic implementation used<\/h4>\n\n\n\n<p>In AES, each round (the key is a 16 bytes one, so there is 10 rounds) applies three successive transformations &#8211; even if the last round is a little bit different:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The SubBytes transformation who associates to each of the 16 bytes of the input block another byte ;<\/li>\n\n\n\n<li>The ShiftRows transformation. In this transformation, the bytes of the input block are divided into 4 groups of 4 to form a square matrix. The first line of this matrix is untouched, the elements of the second row are shifted by 1, the elements of the third row are shifted by 2, and the elements of the fourth row are shifted by 3.<\/li>\n\n\n\n<li>The MixColumns transformation. This transformation also interprets the input block as a 4&#215;4 square matrix, and calculates the image by a matrix transformation of each of the 4 columns of the input matrix.<\/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>For efficiency reasons, these transformations are generally implemented in the form of tables precomputed and stored in binary.<\/p>\n\n\n\n<p>This is not what happens here. If we look the tables used by the <code>aes_cipher<\/code> function:<\/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\/05\/aes_cipher.png\" alt=\"\" class=\"wp-image-215\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/aes_cipher.png 732w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/aes_cipher-300x125.png 300w\" sizes=\"auto, (max-width: 732px) 100vw, 732px\" \/><\/figure>\n\n\n\n<p>and if we look where is defined this table <code>DAT_0015ed78<\/code> :<\/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\/05\/sbox.png\" alt=\"\" class=\"wp-image-216\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/sbox.png 1482w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/sbox-300x90.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/sbox-768x230.png 768w\" sizes=\"auto, (max-width: 1482px) 100vw, 1482px\" \/><\/figure>\n\n\n\n<p>We see that its content is not initialized.<\/p>\n\n\n\n<p>We also observe a cross-reference leading to a function <code>aes_init_keygen_tables<\/code>, whose role, as its name suggests, is to initialize the tables in question.<\/p>\n\n\n\n<p>A few online searches allow us to find traces of an AES implementation using an <code>aes_init_keygen_tables<\/code> function:<\/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\/05\/aes_init_keygen_tables.png\" alt=\"\" class=\"wp-image-217\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/aes_init_keygen_tables.png 788w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/aes_init_keygen_tables-250x300.png 250w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/aes_init_keygen_tables-768x923.png 768w\" sizes=\"auto, (max-width: 788px) 100vw, 788px\" \/><\/figure>\n\n\n\n<p>The AES implementation in question is that of the mongoose embedded web server, published by the company Cesanta: https:\/\/github.com\/cesanta\/mongoose\/blob\/master\/mongoose.c<\/p>\n\n\n\n<p>A comparison of the code decompiled by Ghidra and the code of <code>aes_init_keygen_tables<\/code> present on github confirms that the version present in libchannel.so probably comes from this repository.<\/p>\n\n\n\n<p>Furthermore, the other cryptographic functions used when processing a packet are also present in this repository.<\/p>\n\n\n\n<p>This element allows us to understand the respective roles of the arguments of the cryptographic functions <code>gcm_setkey<\/code>, <code>gcm_start<\/code>, <code>gcm_update<\/code> and <code>gcm_finish<\/code> used by libchannel.so.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Focus on the cryptographic mechanisms used<\/h4>\n\n\n\n<p>GCM is an <em>authenticated<\/em> mode: <\/p>\n\n\n\n<p>It encrypts a message and computes a <em>tag<\/em> who ensures the message integrity. The encryption part of the GCM mode is done using the CTR operating mode. <\/p>\n\n\n\n<p>In CTR mode, a <em>counter<\/em> is used to encrypt the message: <\/p>\n\n\n\n<p>To encrypt a message block, <\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>we encrypt the counter, <\/li>\n\n\n\n<li>we xor the result with the plaintext block, <\/li>\n\n\n\n<li>we increment the counter before to process the next block.<\/li>\n<\/ul>\n\n\n\n<p>This can be summarized more formally by the relationship <\/p>\n\n\n\n<p><code>Y_{i} = X_{i} ^ E(counter + i)<\/code>. <\/p>\n\n\n\n<p>A message encrypted with AES_CTR is xored with a sequence of random bytes, as if a stream cipher was used.<\/p>\n\n\n\n<p>It is therefore essential, when CTR is used (and therefore when GCM is used !) not to reuse this counter to encrypt two messages. <\/p>\n\n\n\n<p>If we ignore this precaution, we find ourselves in the following situation:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The cipher <code>C1<\/code> is the result of xoring the plain message <code>M1<\/code> with the sequence of random bytes <code>R<\/code>,<\/li>\n\n\n\n<li>The cipher <code>C2<\/code> is the result of xoring the plain message <code>M2<\/code> with the sequence of random bytes <code>R<\/code>,<\/li>\n\n\n\n<li> An observer can xor <code>C1<\/code> with <code>C2<\/code>, giving it <code>M1 ^ M2<\/code>, from which it can extract information about <code>M1<\/code> and\/or <code>M2<\/code>.<\/li>\n<\/ul>\n\n\n\n<p>Let&rsquo;s look at how mongoose functions are used by libchannel.so.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">The <code>gcm_start<\/code> function<\/h4>\n\n\n\n<p>The mongoose code describes the role of the arguments to this function:<\/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\/05\/gcm_start_mongoose.png\" alt=\"\" class=\"wp-image-218\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/gcm_start_mongoose.png 797w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/gcm_start_mongoose-300x259.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/gcm_start_mongoose-768x664.png 768w\" sizes=\"auto, (max-width: 797px) 100vw, 797px\" \/><\/figure>\n\n\n\n<p>The last two arguments specify an \u00ab\u00a0AEAD data\u00a0\u00bb: when an authenticated mode is used, those \u00ab\u00a0AEAD data\u00a0\u00bb are authenticated without being encrypted.<\/p>\n\n\n\n<p>In the <code>SignalObfuscator::encode<\/code> function, we see that the two last arguments of the <code>gcm_start<\/code> function are null (see in the screenshot above): There is therefore no AEAD data.<\/p>\n\n\n\n<p>Let&rsquo;s reuse the last Frida script to trace the encryption of the outgoing network packets:<\/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>We observe something that we already knew after studying the <code>SignalObfuscator::encode<\/code> function: <\/p>\n\n\n\n<p>The nonce is the same (<code>\\x4d\\x47\\x5a\\x51\\x62\\x65\\x00\\x00\\x00\\x00\\x00\\x00<\/code> here) for two different packets !<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">The <code>gcm_finish<\/code> function<\/h4>\n\n\n\n<p>The arguments of the <code>gcm_finish<\/code> are the following:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>a <code>ctx<\/code> pointer on the context,<\/li>\n\n\n\n<li>a <code>tag<\/code> pointer who specifies where the GCM tag shall be copied,<\/li>\n\n\n\n<li>a <code>tag_len<\/code> integer who indicates the length of the GCM tag.<\/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\/05\/gcm_finish.png\" alt=\"\" class=\"wp-image-219\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/gcm_finish.png 657w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/gcm_finish-297x300.png 297w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/gcm_finish-150x150.png 150w\" sizes=\"auto, (max-width: 657px) 100vw, 657px\" \/><\/figure>\n\n\n\n<p>Reading the code of this function shows that if <code>tag<\/code> and <code>tag_len<\/code> are null, no copy of the GCM tag is done.<\/p>\n\n\n\n<p>This is exactly what happens here (see the screenshot of <code>SignalObfuscator::encode<\/code> code): The GCM tag is not copied at all !<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Traffic decryption script<\/h4>\n\n\n\n<p>At this point, we have all the information needed to decrypt Secure VPN traffic.<\/p>\n\n\n\n<p>We develop a dissect_securevpn_traffic.py script who can decrypt the intercepted Secure VPN traffic.<\/p>\n\n\n\n<p>This script needs the following to work:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>the IP address, the port number and (optionally) the transport protocol of the VPN server,<\/li>\n\n\n\n<li>the obfuscation key,<\/li>\n\n\n\n<li>the pcap to decrypt.<\/li>\n<\/ul>\n\n\n\n<p>In the example below, the traffic going into the VPN tunnel is ICMP traffic to 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>The script is relatively basic since it identifies the start of an IPv4 packet by looking for the pair of bytes <code>\\x45\\x00<\/code>, which won&rsquo;t always work but is well enough for a PoC.<\/p>\n\n\n\n<p>The dissect_securevpn_traffic.py script and the pcap are available in the associated github repository <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<h4 class=\"wp-block-heading\">The obfuscation Key and the enrollment process<\/h4>\n\n\n\n<p>All the security of the encryption protocol of this VPN seems to be based on the obfuscation key, from which the encryption key and the GCM nonce come.<\/p>\n\n\n\n<p>It is the VPN server (the Secure VPN infrastructure in reality) which communicates this obfuscation key to the client when the application is used for the first time.<\/p>\n\n\n\n<p>Let&rsquo;s intercept the application&rsquo;s HTTPS traffic with HTTP Toolkit the first time it is used:<\/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\/05\/https_1st_use.png\" alt=\"\" class=\"wp-image-220\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/https_1st_use.png 708w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/https_1st_use-300x137.png 300w\" sizes=\"auto, (max-width: 708px) 100vw, 708px\" \/><\/figure>\n\n\n\n<p>And when used later:<\/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\/05\/https_2nd_use.png\" alt=\"\" class=\"wp-image-221\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/https_2nd_use.png 757w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/https_2nd_use-300x116.png 300w\" sizes=\"auto, (max-width: 757px) 100vw, 757px\" \/><\/figure>\n\n\n\n<p>We observe that the application reaches the endpoints <code>\/ip<\/code>, <code>\/v2\/devices<\/code>, <code>\/vip\/v2\/prices<\/code> and <code>\/v2\/server<\/code> of s3.free-signal.com during its first use, and only the endpoints <code>\/ip<\/code> et <code>\/vip\/v2\/prices<\/code> during subsequent uses.<\/p>\n\n\n\n<p>Small additional difference, the domain names of the servers are used during the first use, while it is their IP addresses which are used during subsequent uses.<\/p>\n\n\n\n<p>Let&rsquo;s look at the information exchanged between the app and the Secure VPN backend during this enrollment process:<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Requests on .free-signal.com\/ip<\/h4>\n\n\n\n<p>When the application contacts this endpoint, the server responds with the public IP address of the phone:<\/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\/05\/endpoint_ip_whitened.png\" alt=\"\" class=\"wp-image-222\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/endpoint_ip_whitened.png 989w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/endpoint_ip_whitened-300x215.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/endpoint_ip_whitened-768x551.png 768w\" sizes=\"auto, (max-width: 989px) 100vw, 989px\" \/><\/figure>\n\n\n\n<h4 class=\"wp-block-heading\">Requests on .free-signal.com\/v2\/devices<\/h4>\n\n\n\n<p>This <code>POST<\/code> sends a data blob to the 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\/05\/endpoint_device.png\" alt=\"\" class=\"wp-image-223\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/endpoint_device.png 874w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/endpoint_device-300x287.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/endpoint_device-768x735.png 768w\" sizes=\"auto, (max-width: 874px) 100vw, 874px\" \/><\/figure>\n\n\n\n<p>Sending HTTP requests by the application uses the <code>com.signallab.lib.utils.net.HttpClients<\/code> class. <\/p>\n\n\n\n<p>Within this class, it&rsquo;s the <code>request<\/code> method that does most of the work:<\/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\/05\/HttpClients_request.png\" alt=\"\" class=\"wp-image-224\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/HttpClients_request.png 1225w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/HttpClients_request-300x129.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/HttpClients_request-768x330.png 768w\" sizes=\"auto, (max-width: 1225px) 100vw, 1225px\" \/><\/figure>\n\n\n\n<p>In this request, the <code>s-req-token<\/code> header is the MD5 digest of other fields in the request:<\/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\/05\/s-req-token.png\" alt=\"\" class=\"wp-image-225\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/s-req-token.png 1208w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/s-req-token-300x180.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/s-req-token-768x460.png 768w\" sizes=\"auto, (max-width: 1208px) 100vw, 1208px\" \/><\/figure>\n\n\n\n<p>The request body is processed by the <code>encode<\/code> method of <code>com.signallab.lib.utils.net.HttpClients<\/code> (see previous screenshots)<\/p>\n\n\n\n<p>We use the following frida hook to trace calls to this method and retrieve the body of the request before processing by <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>We also intercept calls to <code>encode<\/code>:<\/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>We see that the body of the request on <code>\/v2\/devices<\/code> is a json containing various information about the terminal:<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>```\nappel de encode - arguments :\nencode, bArr avant : \n{\n\t\"dev_id\":\"c7059ee8ee463482\",\n\t\"dev_model\":\"Pixel 7a\",\n\t\"dev_manufacturer\":\"Google\",\n\t\"dev_lang\":\"fr_FR\",\"dev_os\":\"Android 14\",\n\t\"dev_country\":\"fr\",\n\t\"app_package\":\"com.fast.free.unblock.secure.vpn\",\n\t\"app_ver_name\":\"4.2.5\",\n\t\"app_ver_code\":202403071,\n\t\"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\n```<\/code><\/pre>\n\n\n\n<p>The blob obtained as the output of <code>encode<\/code> is identical to the body of the request.<\/p>\n\n\n\n<p>The submitted json contains two pieces of information that could be quite discriminating: the <code>dev_id<\/code> and the <code>dev_imsi<\/code>.<\/p>\n\n\n\n<p>The name of the latter suggests that it could be the IMSI identifier. <\/p>\n\n\n\n<p>The tests having been carried out on a phone without a SIM card, the content of this field is empty here, so that it is not possible to be certain of its value.<\/p>\n\n\n\n<p>The server response is also a blob, which is decoded by the <code>com.signallab.lib.utils.net.HttpClients.decode<\/code> method:<\/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\/05\/endpoint_device_response.png\" alt=\"\" class=\"wp-image-226\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/endpoint_device_response.png 875w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/endpoint_device_response-300x254.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/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>```\nappel 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\t\"auth_id\": 1383550594126135778,\n\t\"auth_token\": 3954827927911532616\n}\n```<\/code><\/pre>\n\n\n\n<p>It is therefore in response to this request that the backend provides the pair of <code>auth_id<\/code> and <code>auth_token<\/code> identifiers.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Requests on .free-signal.com\/vip\/v2\/prices<\/h4>\n\n\n\n<p>After decoding, the response to the request on this endpoint includes information on the available subscription options:<\/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<h4 class=\"wp-block-heading\">Requests on .free-signal.com\/v2\/server<\/h4>\n\n\n\n<p>The values \u200b\u200bof the <code>s-auth-id<\/code> and <code>s-auth-token<\/code> headers of this method are given by the <code>auth_id<\/code> and <code>auth_token<\/code> identifiers returned by the <code>\/v2\/device<\/code> endpoint.<\/p>\n\n\n\n<p>As before, <code>s-req-token<\/code> is an MD5 digest of part of the request.<\/p>\n\n\n\n<p>The <code>s-req-param<\/code> header is calculated in two steps:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>We encode the string <code>dev_imsi=&amp;dev_lang=fr_FR<\/code> with the <code>encode<\/code> function:<\/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>The result is base64-encoded:<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code alignwide\"><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>Once decoded, the backend response contains the list of available VPN servers in json format.<\/p>\n\n\n\n<p>Each server list entry contains:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>its IP address,<\/li>\n\n\n\n<li>geographic and load distribution information (\u201ccountry\u201d, \u201carea\u201d and \u201cload\u201d tokens),<\/li>\n\n\n\n<li>the obfuscatation key,<\/li>\n\n\n\n<li>the supported algorithm (\u00ab\u00a0obs_algo\u00a0\u00bb token. This token is generally set to 1, so AES GCM is generally used).<\/li>\n\n\n\n<li>a boolean token \u00ab\u00a0is_vip\u00a0\u00bb, indicating a server reserved for users who have paid a subscription,<\/li>\n\n\n\n<li>a boolean token \u00ab\u00a0is_bt\u00a0\u00bb whose role remains unknown (the term bt could refer to bittorrent),<\/li>\n\n\n\n<li>a boolean token \u00ab\u00a0is_running\u00a0\u00bb,<\/li>\n\n\n\n<li>a \u201cfeature\u201d token indicating the VOD service compatible with the server.<\/li>\n<\/ul>\n\n\n\n<p>The list of VPN servers is divided into three sub-lists:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Free VPN servers,<\/li>\n\n\n\n<li>VPN servers allowing access to video on demand services (Netflix, Amazon prime, etc.)<\/li>\n\n\n\n<li>VIP VPN servers.<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\">Retrieving the list of VPN servers on different terminals<\/h4>\n\n\n\n<p>Here is the complete list retrieved during an installation on a physical phone:<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide has-small-font-size\"><code>{\n    \"config\": {\"udp\": &#91;53, 9981],\"tcp\": &#91;443, 9981], \"tun_mtu\": 1380, \"dns_server\": &#91;\"8.8.8.8\", \"1.1.1.1\"]},\n    \"server\": &#91;\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        {\"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        {\"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        {\"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        {\"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        {\"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        {\"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        {\"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        {\"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        {\"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        {\"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        {\"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        {\"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        {\"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    ],\n    \"list\": \"10:5-61:0-8:1\",\n    \"_features\": &#91;\n        {\"type\": \"netflix\", \"name\": \"Netflix\", \"url\": \"https:\/\/tiny.one\/dmwc2rk\"},\n        {\"type\": \"prime_video\", \"name\": \"Prime Video\", \"url\": \"https:\/\/tiny.one\/j336z7z6\"},\n        {\"type\": \"iplayer\", \"name\": \"BBC iPlayer\", \"url\": \"https:\/\/tiny.one\/3tmkktab\"},\n        {\"type\": \"hulu\", \"name\": \"Hulu\", \"url\": \"https:\/\/tiny.one\/cx2ybpw5\"},\n        {\"type\": \"hbomax\", \"name\": \"HBO Max\", \"url\": \"https:\/\/tiny.one\/ath39vz4\"},\n        {\"type\": \"disney+\", \"name\": \"Disney+\", \"url\": \"https:\/\/tiny.one\/ux4bbhx4\"},\n        {\"type\": \"apple_tv\", \"name\": \"Apple TV+\", \"url\": \"https:\/\/tiny.one\/2mp7kupf\"},\n        {\"type\": \"utorrent\", \"name\": \"\u00b5Torrent\", \"url\": \"https:\/\/tiny.one\/tn6p2cbm\"}\n    ],\n    \"video\": {\n        \"config\": {\"udp\": &#91;53, 9981], \"tcp\": &#91;443, 9981], \"tun_mtu\": 1380, \"dns_server\": &#91;\"8.8.8.8\", \"1.1.1.1\"]}, \n        \"server\": &#91;\n            {\"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\"},                 {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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        ]\n    },\n    \"vip\": {\n        \"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        \"server\": &#91;\n            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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\": \"\"},          {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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            {\"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        ]\n    }\n}<\/code><\/pre>\n\n\n\n<p>Two questions remain unanswered:<\/p>\n\n\n\n<p>Will two different users see the same server list?<\/p>\n\n\n\n<p>And if a server can be used by two users, can it be used with the same obfuscation key?<\/p>\n\n\n\n<p>Several scenarios are possible:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>scenario 1: the list of servers and keys is fixed;<\/li>\n\n\n\n<li>scenario 2: the backend provides each client with a different set of servers; if a server is accessible to two clients, it is accessible with the same key;<\/li>\n\n\n\n<li>scenario 3: the backend provides each client with a different set of servers; if a server is accessible to two clients, it is accessible with different keys.<\/li>\n<\/ul>\n\n\n\n<p>To provide some answers to these questions, we observe the first execution of the application on:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Emulator #1: an Android 14 emulator using the same wifi connection as the Android phone used until now,<\/li>\n\n\n\n<li>Emulator #2: an Android 14 emulator using another wifi connection.<\/li>\n<\/ul>\n\n\n\n<p>These two emulators are physically located on my workstation, and created with Android Studio. Both are virtualized Pixel 6 Pro.<\/p>\n\n\n\n<p>We also observe the first use of the application on three Corellium emulators (emulators #3, #4 and #5). <\/p>\n\n\n\n<p>Each of these three emulators uses Android 14 and virtualizes a different phone model: Huawei P8, Samsung Galaxy S7 and Samsung Galaxy Note 5.<\/p>\n\n\n\n<p>The following facts are observed:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>If we consider the lists of paid VPN servers on two different phones, we see a significant number of <em>collisions<\/em> (server appearing the VIP server list of both phones).<\/li>\n<\/ul>\n\n\n\n<p>The image below shows the comparison of paid servers for emulators #3 and #4. While the majority of entries differ, some are shared between these two emulators.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1908\" height=\"854\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_vip0.png\" alt=\"\" class=\"wp-image-227\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_vip0.png 1908w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_vip0-300x134.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_vip0-768x344.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_vip0-1536x687.png 1536w\" sizes=\"auto, (max-width: 1908px) 100vw, 1908px\" \/><\/figure>\n\n\n\n<p>In some cases the lists are identical. Below we see the comparison of paid servers for the physical phone initially used (a Pixel 7) and the Pixel 6 Pro emulator (emulator #2). <\/p>\n\n\n\n<p>Some entries in the list differ only in the \u00ab\u00a0load\u00a0\u00bb token, which visibly represents the \u00ab\u00a0instantaneous\u00a0\u00bb load of a server, and are actually the same.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"873\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_vip.png\" alt=\"\" class=\"wp-image-228\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_vip.png 1920w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_vip-300x136.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_vip-768x349.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/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>If we take the list of VPN servers compatible with VOD services on two different phones, we again see a significant number of collisions. In the example below (emulator #2 and emulator #5), the lists are identical, modulo the \u00ab\u00a0load\u00a0\u00bb token.<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1918\" height=\"299\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_vod.png\" alt=\"\" class=\"wp-image-229\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_vod.png 1918w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_vod-300x47.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_vod-768x120.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/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>If we take the list of free VPN servers on two different phones, these lists are generally disjointed (case of emulators #3 and #4 here):<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"367\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_gratuits1.png\" alt=\"\" class=\"wp-image-230\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_gratuits1.png 1920w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_gratuits1-300x57.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_gratuits1-768x147.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_gratuits1-1536x294.png 1536w\" sizes=\"auto, (max-width: 1920px) 100vw, 1920px\" \/><\/figure>\n\n\n\n<p>This is not always the case, however, and the physical phone has the same list of free servers as emulator #4:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1913\" height=\"349\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_gratuits2.png\" alt=\"\" class=\"wp-image-231\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_gratuits2.png 1913w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_gratuits2-300x55.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_gratuits2-768x140.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_gratuits2-1536x280.png 1536w\" sizes=\"auto, (max-width: 1913px) 100vw, 1913px\" \/><\/figure>\n\n\n\n<p>Emulators #1 and #2 also have an identical list of free servers:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"349\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_gratuits3.png\" alt=\"\" class=\"wp-image-232\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_gratuits3.png 1920w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_gratuits3-300x55.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/comparaison_serveurs_gratuits3-768x140.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/05\/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>The situation where a VPN server is known to two phones, but with different obfuscation keys, has not been observed.<\/li>\n<\/ul>\n\n\n\n<p>These different observations are in line with scenario no. 2: The backend provides each client with a subset of existing VPN servers.<\/p>\n\n\n\n<p>No assumptions can be made about the heuristics used by the backend to choose this subset for a given client.<\/p>\n\n\n\n<p>In \u00ab\u00a0captured\u00a0\u00bb configurations, as soon as two clients have a free VPN server in common, then they have it with the same obfuscation key: As this key is the only cryptographic element involved in the encryption of traffic, each of these clients will be able to decrypt each other&rsquo;s VPN traffic!<\/p>\n\n\n\n<p>However, due to the small number of terminals used (one phone + five emulators), it is not completely impossible that the obfuscation key is calculated from certain parameters coming from the phone, and that ultimately the situation \u00a0\u00bb clients accessing the same free server with different keys\u00a0\u00bb may occur.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Summary of Secure VPN cryptographic vulnerabilities<\/h4>\n\n\n\n<p><em>Spectacular<\/em> cryptographic vulnerabilities are present in Secure VPN:<\/p>\n\n\n\n<p>The most serious is the key management mechanism: Where any \u00ab\u00a0reasonable\u00a0\u00bb VPN protocol such as OpenVPN, IPsec or Wireguard would have used an initial handshake (server authentication via a root certificate known to the application + generation of a common secret via a Diffie-Hellman exchange), Secure VPN uses no known protocol and invents a relatively <em>original<\/em> key management mechanism: When installed, the application retrieves a subset of the list of existing servers.<\/p>\n\n\n\n<p>This exchange is protected by an HTTPS channel and cannot be intercepted \u00ab\u00a0easily\u00a0\u00bb. <\/p>\n\n\n\n<p>So the situation wouldn&rsquo;t be so bad if the obfuscation key for a given server varied from one client to another: One could imagine that the backend derives the obfuscation key from phone identifiers (<code>dev_imsi<\/code> and <code>dev_id<\/code>, for example) and a secret key kept warm in the backend. <\/p>\n\n\n\n<p>It is not completely excluded that a \u00ab\u00a0diversification\u00a0\u00bb mechanism is present, but if this is the case, it is clearly problematic since we observe different terminals using the same servers with the same obfuscation keys.<\/p>\n\n\n\n<p>In fact, it is quite unlikely that such a mechanism exists, because it would require each VPN server to maintain a relatively large number of obfuscation keys (it should be kept in mind that the application is subject to more than a hundred million downloads!).<\/p>\n\n\n\n<p>So here we are at the worst that can happen for a VPN user: Almost anyone, as long as they manage to obtain the same list of VPN servers as you, can decrypt your VPN traffic!<\/p>\n\n\n\n<p>Even if this problem of using secret keys shared by several users did not exist, Secure VPN would still suffer from very problematic vulnerabilities:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Traffic integrity is not guaranteed: The <code>SignalObfuscator::encode<\/code> function calculates the GCM tag of the packet to send\u2026but does not use it!<\/li>\n\n\n\n<li>GCM nonce reuse: The <code>SignalObfuscator::encode<\/code> function encrypts each network packet independently of the others, each time reusing the nonce extracted from the obfuscation key. As GCM mode uses CTR mode, it is possible to capture traffic and obtain a significant amount of information by xoring packets two by two (since they are always xored with the same cipher sequence).<\/li>\n\n\n\n<li>Finally, and this is a detail in view of all the above, no guarantee in terms of <em>forward secrecy<\/em> is possible to the extent that we use a secret key: If I manage to get my hands on the key obfuscation stored in your phone, I am able to decrypt your Secure VPN traffic.<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\">Conclusion<\/h4>\n\n\n\n<p>The conclusion of this article is that we can make some surprising discoveries, even in widely used applications, as soon as we take a close look.<\/p>\n\n\n\n<p>One might question the reason for developing a VPN with such serious flaws. A fairly natural hypothesis, although slightly conspiratorial, would be to see a deliberate desire to introduce a backdoor into the application, in order to be able to access user traffic.<\/p>\n\n\n\n<p>In reality, this does not hold: The Secure VPN administrator, by definition, has access to user traffic in the clear since he has control over the VPN servers!<\/p>\n\n\n\n<p>The reality is probably much more prosaic: it is much more likely that the application was developed by developers with some gaps in cryptology.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Nowadays a large number of people use a VPN, for various more or less relevant reasons. While some VPN providers give guarantees of transparency by disclosing their source code and\/or periodically publishing technical audits reports, many others providers are really opaque.Carefully study one of these VPN client could therefore reveal some surprises\u2026 The Secure VPN [&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-189","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/posts\/189","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=189"}],"version-history":[{"count":3,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/posts\/189\/revisions"}],"predecessor-version":[{"id":317,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/posts\/189\/revisions\/317"}],"wp:attachment":[{"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/media?parent=189"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/categories?post=189"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/tags?post=189"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}