{"id":238,"date":"2024-08-26T20:43:00","date_gmt":"2024-08-26T20:43:00","guid":{"rendered":"https:\/\/tolva.fr\/?p=238"},"modified":"2025-03-08T22:12:46","modified_gmt":"2025-03-08T22:12:46","slug":"dechiffrer-le-trafic-tls-dune-application-android-avec-frida","status":"publish","type":"post","link":"https:\/\/tolva.fr\/index.php\/2024\/08\/26\/dechiffrer-le-trafic-tls-dune-application-android-avec-frida\/","title":{"rendered":"D\u00e9chiffrer le trafic TLS d&rsquo;une application Android avec Frida"},"content":{"rendered":"\n<p>D\u00e9chiffrer le trafic TLS est souvent une \u00e9tape incontournable lorsque l&rsquo;on \u00e9tudie une application Android.<\/p>\n\n\n\n<p>La m\u00e9thode usuelle est d&rsquo;utiliser un proxy HTTPS comme BurpProxy ou Http Toolkit.<\/p>\n\n\n\n<p>Cette m\u00e9thode n\u00e9cessite g\u00e9n\u00e9ralement d&rsquo;installer un certificat dans le magasin de certificats du terminal (m\u00eame s&rsquo;il est possible d&rsquo;arriver \u00e0 ses fins en patchant l&rsquo;application \u00e9tudi\u00e9e, mais c&rsquo;est une autre histoire), chose qui est de plus en plus compliqu\u00e9e, comme l&rsquo;explique cet article <a href=\"https:\/\/httptoolkit.com\/blog\/android-14-install-system-ca-certificate\/\" data-type=\"link\" data-id=\"https:\/\/httptoolkit.com\/blog\/android-14-install-system-ca-certificate\/\">https:\/\/httptoolkit.com\/blog\/android-14-install-system-ca-certificate\/<\/a> .<\/p>\n\n\n\n<p>Mais pourquoi s&#8217;emb\u00eater alors qu&rsquo;on peut r\u00e9cup\u00e9rer les cl\u00e9s de la session TLS avec Frida ?<\/p>\n\n\n\n<p>Regardons comment un script frida de quelques lignes permettrait d&rsquo;arriver \u00e0 nos fins.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Conscrypt et BoringSSL<\/strong><\/h4>\n\n\n\n<p>\u00c0 partir de Android 11, Conscrypt devient l&rsquo;impl\u00e9mentation <code>SSLSocket<\/code> par d\u00e9faut (voir fin de <a href=\"https:\/\/developer.android.com\/privacy-and-security\/security-ssl?hl=fr\" data-type=\"link\" data-id=\"https:\/\/developer.android.com\/privacy-and-security\/security-ssl?hl=fr\">https:\/\/developer.android.com\/privacy-and-security\/security-ssl?hl=fr<\/a>), ce qui signifie qu&rsquo;une grande partie des applications utilisent Conscrypt.<\/p>\n\n\n\n<p>En interne, Conscrypt utilise \u00e0 son tour la biblioth\u00e8que native BoringSSL, qui est un fork de OpenSSL cr\u00e9\u00e9 peu apr\u00e8s la d\u00e9couverte de Heartbleed en 2014.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>La fonction <code>SSL_do_handshake<\/code><\/strong><\/h4>\n\n\n\n<p>BoringSSL embarque <code>bssl<\/code>, outil de test en ligne de commande permettant de se connecter \u00e0 un serveur TLS.<\/p>\n\n\n\n<p>C&rsquo;est la fonction <code>SSL_connect()<\/code>, appel\u00e9e par la fonction <code>DoConnection<\/code>, qui fait le travail :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"648\" height=\"194\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/08\/SSL_connect.png\" alt=\"\" class=\"wp-image-239\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/08\/SSL_connect.png 648w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/08\/SSL_connect-300x90.png 300w\" sizes=\"auto, (max-width: 648px) 100vw, 648px\" \/><\/figure>\n\n\n\n<p>Cette fonction, impl\u00e9ment\u00e9e dans ssl_lib.cc, se r\u00e9duit essentiellement \u00e0 un appel \u00e0 <code>SSL_do_handshake<\/code> :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"479\" height=\"213\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/08\/SSL_connect1.png\" alt=\"\" class=\"wp-image-240\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/08\/SSL_connect1.png 479w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/08\/SSL_connect1-300x133.png 300w\" sizes=\"auto, (max-width: 479px) 100vw, 479px\" \/><\/figure>\n\n\n\n<p>Cette fonction est elle aussi impl\u00e9ment\u00e9e dans ssl_lib.cc :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"733\" height=\"670\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/08\/SSL_do_handshake.png\" alt=\"\" class=\"wp-image-241\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/08\/SSL_do_handshake.png 733w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/08\/SSL_do_handshake-300x274.png 300w\" sizes=\"auto, (max-width: 733px) 100vw, 733px\" \/><\/figure>\n\n\n\n<p>Cette fonction est bien export\u00e9e par BoringSSL et pourra facilement \u00eatre hook\u00e9e avec frida :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>$ aarch64-linux-gnu-objdump -T \/tmp\/libssl64.so | grep SSL_do\n0000000000043cd0 g    DF .text    0000000000000160  Base        SSL_do_handshake<\/code><\/pre>\n\n\n\n<p>Frida permet de modifier le comportement d&rsquo;une fonction native au tout d\u00e9but et \u00e0 la toute fin de la fonction :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>const func_ptr = Module.getExportByName('librairie.so', 'uneFonction');\n\nInterceptor.attach(func_ptr, {\n\n    onEnter(args) {\n        on fait qqch ici\n    },\n\n    onLeave(retvar) {\n        on fait qqch ici\n    }\n});<\/code><\/pre>\n\n\n\n<p>Ici c&rsquo;est la fin de la fonction qui nous int\u00e9resse, puisque \u00e0 ce stade le handshake est normalement termin\u00e9.<\/p>\n\n\n\n<p>La fonction <code>SSL_do_handshake()<\/code> n&rsquo;utilise qu&rsquo;un seul argument, un pointeur <code>ssl<\/code> sur une structure de type <code>SSL<\/code>.<\/p>\n\n\n\n<p>Il va donc falloir explorer la structure <code>SSL<\/code> pour voir si les secrets de handshake y sont bien nich\u00e9s, directement ou non.<\/p>\n\n\n\n<p>Comme le d\u00e9crit la section 7 de la rfc 8446 d\u00e9crivant TLSv1.3, les secrets en question sont \u00e0 minima :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>client_handshake_traffic_secret<\/code> : Chiffrement du trafic de handshake client-&gt;serveur<\/li>\n\n\n\n<li><code>server_handshake_traffic_secret<\/code> : Chiffrement du trafic de handshake serveur-&gt;client<\/li>\n\n\n\n<li><code>client_application_traffic_secret_N<\/code> : Chiffrement du trafic applicatif client-&gt;serveur<\/li>\n\n\n\n<li><code>server_application_traffic_secret_N<\/code> : Chiffrement du trafic applicatif serveur-&gt;client<\/li>\n<\/ul>\n\n\n\n<p><code>SSL<\/code> est un alias sur <code>ssl_st<\/code> :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>typedef struct ssl_st SSL;<\/code><\/pre>\n\n\n\n<p>Cette structure contient elle-m\u00eame un pointeur vers une structure <code>SSL3_STATE<\/code> :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>  \/\/ do_handshake runs the handshake. On completion, it returns |ssl_hs_ok|.\n  \/\/ Otherwise, it returns a value corresponding to what operation is needed to\n  \/\/ progress.\n  bssl::ssl_hs_wait_t (*do_handshake)(bssl::SSL_HANDSHAKE *hs) = nullptr;\n\n  bssl::SSL3_STATE *s3 = nullptr;   \/\/ TLS variables\n  bssl::DTLS1_STATE *d1 = nullptr;  \/\/ DTLS variables<\/code><\/pre>\n\n\n\n<p>Cette structure embarque le <code>client_random<\/code> et le <code>server_random<\/code> :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>struct SSL3_STATE {\n  static constexpr bool kAllowUniquePtr = true;\n\n  SSL3_STATE();\n  ~SSL3_STATE();\n\n  uint64_t read_sequence = 0;\n  uint64_t write_sequence = 0;\n\n  uint8_t server_random&#91;SSL3_RANDOM_SIZE] = {0};\n  uint8_t client_random&#91;SSL3_RANDOM_SIZE] = {0};<\/code><\/pre>\n\n\n\n<p>Ainsi que les secrets applicatifs convoit\u00e9s :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>  uint8_t write_traffic_secret&#91;SSL_MAX_MD_SIZE] = {0};\n  uint8_t read_traffic_secret&#91;SSL_MAX_MD_SIZE] = {0};\n  uint8_t exporter_secret&#91;SSL_MAX_MD_SIZE] = {0};<\/code><\/pre>\n\n\n\n<p>Pas de trace des cl\u00e9s de handshake cependant.<\/p>\n\n\n\n<p>Par contre, <code>SSL3_STATE<\/code> contient un <code>UniquePtr<\/code> sur une structure <code>SSL_HANDSHAKE<\/code> :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>  \/\/ hs is the handshake state for the current handshake or NULL if there isn't\n  \/\/ one.\n  UniquePtr&lt;SSL_HANDSHAKE&gt; hs;<\/code><\/pre>\n\n\n\n<p>et cette derni\u00e8re structure contient bien tout mat\u00e9riel cryptographique recherch\u00e9 :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>struct SSL_HANDSHAKE {\n  explicit SSL_HANDSHAKE(SSL *ssl);\n  ~SSL_HANDSHAKE();\n  static constexpr bool kAllowUniquePtr = true;\n\n  \/\/ ssl is a non-owning pointer to the parent |SSL| object.\n  SSL *ssl;\n\n  \/\/ config is a non-owning pointer to the handshake configuration.\n  SSL_CONFIG *config;\n\n  \/\/ wait contains the operation the handshake is currently blocking on or\n  \/\/ |ssl_hs_ok| if none.\n  enum ssl_hs_wait_t wait = ssl_hs_ok;\n\n  \/\/ state is the internal state for the TLS 1.2 and below handshake. Its\n  \/\/ values depend on |do_handshake| but the starting state is always zero.\n  int state = 0;\n\n  \/\/ tls13_state is the internal state for the TLS 1.3 handshake. Its values\n  \/\/ depend on |do_handshake| but the starting state is always zero.\n  int tls13_state = 0;\n\n  \/\/ min_version is the minimum accepted protocol version, taking account both\n  \/\/ |SSL_OP_NO_*| and |SSL_CTX_set_min_proto_version| APIs.\n  uint16_t min_version = 0;\n\n  \/\/ max_version is the maximum accepted protocol version, taking account both\n  \/\/ |SSL_OP_NO_*| and |SSL_CTX_set_max_proto_version| APIs.\n  uint16_t max_version = 0;\n\n private:\n  size_t hash_len_ = 0;\n  uint8_t secret_&#91;SSL_MAX_MD_SIZE] = {0};\n  uint8_t early_traffic_secret_&#91;SSL_MAX_MD_SIZE] = {0};\n  uint8_t client_handshake_secret_&#91;SSL_MAX_MD_SIZE] = {0};\n  uint8_t server_handshake_secret_&#91;SSL_MAX_MD_SIZE] = {0};\n  uint8_t client_traffic_secret_0_&#91;SSL_MAX_MD_SIZE] = {0};\n  uint8_t server_traffic_secret_0_&#91;SSL_MAX_MD_SIZE] = {0};\n  uint8_t expected_client_finished_&#91;SSL_MAX_MD_SIZE] = {0};<\/code><\/pre>\n\n\n\n<p>Nous avons donc une id\u00e9e de ce qu&rsquo;il faut faire :<\/p>\n\n\n\n<p>Dans le hook <code>onLeave<\/code> de notre script Frida, r\u00e9cup\u00e9rer le <code>SSL *ssl<\/code>, aller y chercher <code>s3<\/code> au bon offset, et aller chercher <code>hs<\/code> au bon offset de <code>s3<\/code>.<\/p>\n\n\n\n<p>On pourra valider cette strat\u00e9gie en la testant sur une version modifi\u00e9e de <code>bssl<\/code> dans un premier temps.<\/p>\n\n\n\n<p>Malheureusement, certaines propri\u00e9t\u00e9s du C++ vont nous mettre des batons dans les roues.<\/p>\n\n\n\n<p>En effet, la d\u00e9marche envisag\u00e9e est de :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>r\u00e9cup\u00e9rer le pointeur <code>SSL *ssl<\/code> \u00e0 la fin de <code>SSL_do_handshake<\/code>,<\/li>\n\n\n\n<li>aller au bon offset \u00e0 l&rsquo;int\u00e9rieur de <code>ssl<\/code>, y lire et d\u00e9r\u00e9f\u00e9rencer <code>s3<\/code>,<\/li>\n\n\n\n<li>aller dans <code>s3<\/code>, y lire et d\u00e9r\u00e9f\u00e9rencer <code>hs<\/code>.<\/li>\n<\/ul>\n\n\n\n<p>Cette derni\u00e8re \u00e9tape ne va pas \u00eatre possible simplement, comme l&rsquo;illustre petit exemple ci-dessous :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>#include &lt;iostream&gt;\n#include &lt;memory&gt;\n\nstruct voiture\n{\n    unsigned short magic = 0xcafe;\n    int a = 0;\n    long b = 0;\n    unsigned char nom&#91;16] = {0};\n};\n\ntypedef voiture voit;\n\nint main()\n{\n    std::unique_ptr&lt;voit&gt; v1;\n    voit *v2 = nullptr;\n\n    v1 = std::unique_ptr&lt;voit&gt;(new voit);\n    v2 = v1.get();\n\n    printf(\"v2 : 0x%x\\n\", (void *)v2);\n    printf(\"v1 : 0x%x\\n\", (void *)v1);\n\n    return 0;\n}<\/code><\/pre>\n\n\n\n<p>Le dernier <code>printf<\/code> contient un cast d&rsquo;un <code>unique_ptr<\/code> en <code>void *<\/code>.<\/p>\n\n\n\n<p>Un tel cast (n\u00e9cessaire pour r\u00e9cup\u00e9rer <code>hs<\/code> dans <code>s3<\/code>) est interdit par le compilateur :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>$ g++ poc.c -o poc\npoc.c: In function \u2018int main()\u2019:\npoc.c:23:24: error: invalid cast from type \u2018std::unique_ptr&lt;voiture&gt;\u2019 to type \u2018void*\u2019\n   23 |  printf(\"v1 : 0x%x\\n\", (void *)v1);\n      |                        ^~~~~~~~~~<\/code><\/pre>\n\n\n\n<p>D&rsquo;ailleurs, dans le code de <code>SSL_do_handshake<\/code> :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>  \/\/ Run the handshake.\n  SSL_HANDSHAKE *hs = ssl-&gt;s3-&gt;hs.get();<\/code><\/pre>\n\n\n\n<p>On voit que l&rsquo;objet <code>hs<\/code> n&rsquo;est pas obtenu en d\u00e9r\u00e9f\u00e9ren\u00e7ant un pointeur, mais en utilisant la m\u00e9thode <code>hs.get()<\/code>.<\/p>\n\n\n\n<p>Malheureusement, ce getter ne semble pas \u00eatre export\u00e9 par libssl.so :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>$ objdump -T \/tmp\/libssl64.so  | grep '.text' | grep '_Z' | awk -F 'Base' '{print $2}' | tr -d ' ' | c++filt \nbssl::ssl_cipher_is_deprecated(ssl_cipher_st const*)\nbssl::SSL_CTX_set_aes_hw_override_for_testing(ssl_ctx_st*, bool)\nbssl::CBBFinishArray(cbb_st*, bssl::Array&lt;unsigned char&gt;*)\nbssl::SSL_set_aes_hw_override_for_testing(ssl_st*, bool)\nbssl::ssl_session_serialize(ssl_session_st const*, cbb_st*)\nbssl::SSL_set_handoff_mode(ssl_st*, bool)\nbssl::ssl_is_valid_ech_public_name(bssl::Span&lt;unsigned char const&gt;)\nbssl::SSL_serialize_handoff(ssl_st const*, cbb_st*, ssl_early_callback_ctx*)\nbssl::SSL_decline_handoff(ssl_st*)\nbssl::ssl_client_hello_init(ssl_st const*, ssl_early_callback_ctx*, bssl::Span&lt;unsigned char const&gt;)\nbssl::SSL_CTX_set_handoff_mode(ssl_ctx_st*, bool)\nbssl::SSL_serialize_handback(ssl_st const*, cbb_st*)\nbssl::SSL_SESSION_parse(cbs_st*, bssl::SSL_X509_METHOD const*, crypto_buffer_pool_st*)\nbssl::ssl_cert_check_key_usage(cbs_st const*, bssl::ssl_key_usage_t)\nbssl::SSL_apply_handback(ssl_st*, bssl::Span&lt;unsigned char const&gt;)\nbssl::SSL_get_traffic_secrets(ssl_st const*, bssl::Span&lt;unsigned char const&gt;*, bssl::Span&lt;unsigned char const&gt;*)\nbssl::ssl_decode_client_hello_inner(ssl_st*, unsigned char*, bssl::Array&lt;unsigned char&gt;*, bssl::Span&lt;unsigned char const&gt;, ssl_early_callback_ctx const*)\nbssl::SSL_apply_handoff(ssl_st*, bssl::Span&lt;unsigned char const&gt;)\nbssl::SSL_SESSION_dup(ssl_session_st*, int)<\/code><\/pre>\n\n\n\n<p>Il ne semble donc pas y avoir de moyen simple de r\u00e9cup\u00e9rer l&rsquo;objet <code>hs<\/code>, et donc les secrets qu&rsquo;il abrite.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Impasse<\/strong><\/h4>\n\n\n\n<p>On se retrouve donc dans une situation o\u00f9 l&rsquo;on a acc\u00e8s aux secrets chiffrant le trafic, mais pas le handshake.<\/p>\n\n\n\n<p>Comme on a par ailleurs acc\u00e8s au <code>client_random<\/code> et \u00e0 l&rsquo;<code>exporter_secret<\/code> (via l&rsquo;objet <code>s3<\/code>), on peut peut-\u00eatre construire un fichier keylogfile partiel.<\/p>\n\n\n\n<p>Pour rappel, le fichier keylogfile, que certains logiciels g\u00e9n\u00e8rent lorsque la variable d&rsquo;environnment <code>SSLKEYLOGFILE<\/code> est positionn\u00e9e, permet \u00e0 wireshark de d\u00e9chiffrer un pcap contenant du trafic TLS.<\/p>\n\n\n\n<p>On peut par exemple dechiffrer sa navigation internet en lan\u00e7ant <code>SSLKEYLOGFILE=\/quelque\/part firefox<\/code> et en capturant le trafic en parall\u00e8le.<\/p>\n\n\n\n<p>Le fichier keylogfile est structur\u00e9 ainsi :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># SSL\/TLS secrets log file, generated by OpenSSL\nSERVER_HANDSHAKE_TRAFFIC_SECRET &lt;client_random&gt; &lt;server_handshake_secret&gt;\nEXPORTER_SECRET &lt;client_random&gt;  &lt;exporter_secret&gt;\nSERVER_TRAFFIC_SECRET_0 &lt;client_random&gt; &lt;server_traffic_secret&gt;\nCLIENT_HANDSHAKE_TRAFFIC_SECRET &lt;client_random&gt; &lt;client_handshake_secret&gt;\nCLIENT_TRAFFIC_SECRET_0 &lt;client_random&gt; &lt;client_traffic_secret&gt;<\/code><\/pre>\n\n\n\n<p>Il nous manque donc les valeurs <code>server_handshake_secret<\/code> et <code>client_handshake_secret<\/code>.<\/p>\n\n\n\n<p>Wireshark refuse d&rsquo;utiliser un keylogfile incomplet. Remplacer les deux valeurs inconnues par des blocs de z\u00e9ro n&rsquo;est pas une solution et wireshark refuse \u00e9galement de d\u00e9chiffrer le trafic dans ces conditions.<\/p>\n\n\n\n<p>Lorsque l&rsquo;on tente une exp\u00e9rience similaire avec <a href=\"https:\/\/github.com\/T0lva\/tls-dissector\/tree\/master\" data-type=\"link\" data-id=\"https:\/\/github.com\/tmalherbe\/tls-dissector\/tree\/master\">https:\/\/github.com\/T0lva\/tls-dissector\/tree\/master<\/a>, on rencontre un probl\u00e8me similaire : le trafic applicatif n&rsquo;est pas d\u00e9chiffr\u00e9 correctement.<\/p>\n\n\n\n<p>D\u00e9chiffrer le trafic applicatif n&rsquo;a pourtant rien d&rsquo;impossible, car les secrets prot\u00e9geant le handshake ne sont pas utilis\u00e9s pour d\u00e9chiffrer le trafic applicatif.<\/p>\n\n\n\n<p>En r\u00e9alit\u00e9, dans le cas de tls-dissector, le probl\u00e8me est caus\u00e9 par la transition secrets de handshake -&gt; secrets applicatifs :<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1879\" height=\"541\" src=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/08\/decrypt_ok_vs_ko.png\" alt=\"\" class=\"wp-image-242\" srcset=\"https:\/\/tolva.fr\/wp-content\/uploads\/2024\/08\/decrypt_ok_vs_ko.png 1879w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/08\/decrypt_ok_vs_ko-300x86.png 300w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/08\/decrypt_ok_vs_ko-768x221.png 768w, https:\/\/tolva.fr\/wp-content\/uploads\/2024\/08\/decrypt_ok_vs_ko-1536x442.png 1536w\" sizes=\"auto, (max-width: 1879px) 100vw, 1879px\" \/><\/figure>\n\n\n\n<p>Dans le cas o\u00f9 les secrets de handshake sont inconnus (\u00e0 droite), il n&rsquo;est pas possible de d\u00e9tecter le message <code>FINISHED<\/code> apr\u00e8s lequel les secrets de handshake sont remplac\u00e9s par les secrets applicatifs, et les compteurs GCM sont r\u00e9initialis\u00e9s.<\/p>\n\n\n\n<p>Il n&rsquo;y a l\u00e0 rien d&rsquo;insurmontable, car on pourrait faire une hypoth\u00e8se sur la position des messages <code>FINISHED<\/code>, d\u00e9chiffrer le trafic suivant ces messages, voir si on obtient quelque chose d&rsquo;intelligible et passer \u00e0 la position suivante si ce n&rsquo;est pas le cas.<\/p>\n\n\n\n<p>L&rsquo;absence des secrets de handshake nous interdira de d\u00e9chiffrer les messages chiffr\u00e9s du handshake, notamment la cha\u00eene de certificats du serveur.<\/p>\n\n\n\n<p>Ce n&rsquo;est toutefois pas si grave, car, dans le cas de trafic HTTPS on aura acc\u00e8s aux champs <code>HOST<\/code> des requ\u00eates, et la majorit\u00e9 de l&rsquo;information se trouve dans le trafic applicatif de toute fa\u00e7on.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>R\u00e9inventer la roue<\/strong><\/h4>\n\n\n\n<p>La situation n&rsquo;est toutefois pas id\u00e9ale : Une (petite) partie du trafic reste inaccessible, et le d\u00e9chiffrement ne fonctionnera qu&rsquo;au prix d&rsquo;une bidouille relativement bancale dans tls-dissector.<\/p>\n\n\n\n<p>En parcourant un peu internet, on r\u00e9alise qu&rsquo;on est pass\u00e9 \u00e0 cot\u00e9 d&rsquo;une solution beaucoup plus simple et \u00e9l\u00e9gante : Cette solution consiste \u00e0 forcer libssl \u00e0 logger les secrets avec les fonctions pr\u00e9vues pour cela !<\/p>\n\n\n\n<p>Un peu plus haut, nous avons parl\u00e9 de la variable d&rsquo;environnement <code>SSLKEYLOGFILE<\/code> qui, lorsqu&rsquo;elle existe, pr\u00e9cise le chemin d&rsquo;un fichier o\u00f9 les secrets doivent \u00eatre \u00e9crits.<\/p>\n\n\n\n<p>Cette variable d&rsquo;environnement est utilis\u00e9 dans ce morceau de code :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code> const char *keylog_file = getenv(\"SSLKEYLOGFILE\");\n  if (keylog_file) {\n    g_keylog_file = fopen(keylog_file, \"a\");\n    if (g_keylog_file == nullptr) {\n      perror(\"fopen\");\n      return false;\n    }\n    SSL_CTX_set_keylog_callback(ctx.get(), KeyLogCallback);\n  }<\/code><\/pre>\n\n\n\n<p>Lorsqu&rsquo;elle est pr\u00e9sente, la fonction <code>SSL_CTX_set_keylog_callback<\/code> est appel\u00e9e et positionne une callback de dump de secret dans l&rsquo;objet <code>SSL_CTX<\/code>.<\/p>\n\n\n\n<p>La callback en question, <code>KeyLogCallback<\/code>, est une fonction qui va tout simplement \u00e9crire quelque chose quelque part :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>static void KeyLogCallback(const SSL *ssl, const char *line) {\n  fprintf(g_keylog_file, \"%s\\n\", line);\n  fflush(g_keylog_file);\n}<\/code><\/pre>\n\n\n\n<p>Lorsque les fonctions de d\u00e9rivation de clefs sont appel\u00e9es, la fonction <code>ssl_log_secret<\/code> est invoqu\u00e9e :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>bool tls13_derive_handshake_secrets(SSL_HANDSHAKE *hs) {\n  SSL *const ssl = hs-&gt;ssl;\n  if (!derive_secret(hs, hs-&gt;client_handshake_secret(),\n                     label_to_span(kTLS13LabelClientHandshakeTraffic)) ||\n      !ssl_log_secret(ssl, \"CLIENT_HANDSHAKE_TRAFFIC_SECRET\",\n                      hs-&gt;client_handshake_secret()) ||\n      !derive_secret(hs, hs-&gt;server_handshake_secret(),\n                     label_to_span(kTLS13LabelServerHandshakeTraffic)) ||\n      !ssl_log_secret(ssl, \"SERVER_HANDSHAKE_TRAFFIC_SECRET\",\n                      hs-&gt;server_handshake_secret())) {\n    return false;\n  }\n\n  return true;\n}<\/code><\/pre>\n\n\n\n<p>Cette fonction ne fait rien si aucune <code>keylog_callback<\/code> n&rsquo;est positionn\u00e9e. Dans le cas inverse, elle fait appelle \u00e0 cette callback pour dumper le secret avec le format attendu :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>bool ssl_log_secret(const SSL *ssl, const char *label,\n                    Span&lt;const uint8_t&gt; secret) {\n  if (ssl-&gt;ctx-&gt;keylog_callback == NULL) {\n    return true;\n  }\n\n  ScopedCBB cbb;\n  Array&lt;uint8_t&gt; line;\n  if (!CBB_init(cbb.get(), strlen(label) + 1 + SSL3_RANDOM_SIZE * 2 + 1 +\n                               secret.size() * 2 + 1) ||\n      !CBB_add_bytes(cbb.get(), reinterpret_cast&lt;const uint8_t *&gt;(label),\n                     strlen(label)) ||\n      !CBB_add_u8(cbb.get(), ' ') ||\n      !cbb_add_hex_consttime(cbb.get(), ssl-&gt;s3-&gt;client_random) ||\n      !CBB_add_u8(cbb.get(), ' ') ||\n      \/\/ Convert to hex in constant time to avoid leaking |secret|. If the\n      \/\/ callback discards the data, we should not introduce side channels.\n      !cbb_add_hex_consttime(cbb.get(), secret) ||\n      !CBB_add_u8(cbb.get(), 0 \/* NUL *\/) ||\n      !CBBFinishArray(cbb.get(), &amp;line)) {\n    return false;\n  }\n\n  ssl-&gt;ctx-&gt;keylog_callback(ssl, reinterpret_cast&lt;const char *&gt;(line.data()));\n  return true;\n}<\/code><\/pre>\n\n\n\n<p>La d\u00e9marche \u00e0 suivre est donc :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>red\u00e9finir une fonction <code>keylog_callback<\/code>,<\/li>\n\n\n\n<li>trouver la fonction initialisant le <code>SSL_CTX<\/code>,<\/li>\n\n\n\n<li>forcer l&rsquo;appel \u00e0 <code>set_keylog_callback_func<\/code> \u00e0 la fin de cette fonction, afin d&rsquo;enregistrer notre fonction de callback.<\/li>\n<\/ul>\n\n\n\n<p>Le script final sera le suivant :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>\/* on va hooker les appels au constructeur SSL_CTX_new *\/\nconst SSL_CTX_new_ptr = Module.getExportByName('libssl.so', 'SSL_CTX_new');\n\/* on r\u00e9cup\u00e8re l'adresse de la fonction positionnant la callback de keylog *\/\nconst set_keylog_callback_ptr = Module.getExportByName('libssl.so', 'SSL_CTX_set_keylog_callback');\n\/* on d\u00e9finit la fonction de positionnement de callback avec son adresse et sa signature *\/\nconst set_keylog_callback_func = new NativeFunction(set_keylog_callback_ptr, 'void', &#91;'pointer', 'pointer']);\n\n\/* on d\u00e9finit une fonction de callback qui affiche la string pass\u00e9e en argument *\/\nvar keylog_callback = new NativeCallback((ssl, line) =&gt; {\n  send(Memory.readCString(line));\n}, 'void', &#91;'pointer', 'pointer']);\n\nInterceptor.attach(SSL_CTX_new_ptr, {\n\n    \/* \u00e0 la fin de SSL_CTX_new() on appelle set_keylog_callback pour positionner notre callback *\/\n    onLeave(ssl_ctx) {\n        set_keylog_callback_func(ssl_ctx, keylog_callback);\n    }\n});<\/code><\/pre>\n\n\n\n<p>On lance une application en y injectant le script et l&rsquo;on obtient bien tous les secrets voulus :<\/p>\n\n\n\n<pre class=\"wp-block-code alignwide\"><code>$ frida -U -f com.fast.free.unblock.secure.vpn -l tls_keylog.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)\nSpawned `com.fast.free.unblock.secure.vpn`. Resuming main thread!       \n&#91;Pixel 7a::com.fast.free.unblock.secure.vpn ]-&gt; message: {'type': 'send', 'payload': 'CLIENT_HANDSHAKE_TRAFFIC_SECRET cf3e0ab582224ff776f36b8c4292f83e2367dce8272a2d6a4708a3b0f8b8b3fa bb98e59d1279313964dc3078260233ac458acbc804d30e0716db772028dd7eb4'} data: None\nmessage: {'type': 'send', 'payload': 'SERVER_HANDSHAKE_TRAFFIC_SECRET cf3e0ab582224ff776f36b8c4292f83e2367dce8272a2d6a4708a3b0f8b8b3fa 2c47469c09079d027e448675f4fbadb165200984e93e05f5cd1da5c3f6332831'} data: None\nmessage: {'type': 'send', 'payload': 'CLIENT_TRAFFIC_SECRET_0 cf3e0ab582224ff776f36b8c4292f83e2367dce8272a2d6a4708a3b0f8b8b3fa 9f25837280e3e9878efe352bf8deafb00bcf4bcb22f897a01385a8a30e2c2f24'} data: None\nmessage: {'type': 'send', 'payload': 'SERVER_TRAFFIC_SECRET_0 cf3e0ab582224ff776f36b8c4292f83e2367dce8272a2d6a4708a3b0f8b8b3fa b0092a784fc0f8f5fded3acc6cdd1c84080615dd6fea6bab13bc190e7b63678a'} data: None\nmessage: {'type': 'send', 'payload': 'EXPORTER_SECRET cf3e0ab582224ff776f36b8c4292f83e2367dce8272a2d6a4708a3b0f8b8b3fa fa8913bed3c0a5f3a08c5825af19fc14c68950778d06b5043f73c61c8000626b'} data: None\nmessage: {'type': 'send', 'payload': 'CLIENT_RANDOM 8d552fb06b13539565a9e52ee008c9d29514da662c8b395fc5aadf052b381834 7d758cb230c58591e24b1a147bff434e826863276536265926df7e3c638cb7d01c4ed766d109851b72563df705a92ee3'} data: None\nmessage: {'type': 'send', 'payload': 'CLIENT_RANDOM be1d9ef8bc91fb097517d61363936d69f70aa0111bafe8396dcab03e811b4d60 59004c8fe302a8fed74fc9ee5add870063cd716414024874398a76a433b295d16770a23bfa14b50e50057cdc3247e7d5'} data: None\nmessage: {'type': 'send', 'payload': 'CLIENT_HANDSHAKE_TRAFFIC_SECRET 489d092d48d9a6cddb16e45b195ca3a6c5a49dbc703bdac0420b41fb9f77d3fb 0ba791322fd4abe7903ef65ea239c3902604bb1c82ddcd8ef025d33dca2dd3b7'} data: None\nmessage: {'type': 'send', 'payload': 'SERVER_HANDSHAKE_TRAFFIC_SECRET 489d092d48d9a6cddb16e45b195ca3a6c5a49dbc703bdac0420b41fb9f77d3fb aef81d648d9d19e514f72d4dcb50ca883203cc56a41a8de11ff7f2cfddac767e'} data: None\nmessage: {'type': 'send', 'payload': 'CLIENT_TRAFFIC_SECRET_0 489d092d48d9a6cddb16e45b195ca3a6c5a49dbc703bdac0420b41fb9f77d3fb 9749dd129388c3b946fc4e96c19ee33c6520e6cfa6025701810050af35c66e11'} data: None\nmessage: {'type': 'send', 'payload': 'SERVER_TRAFFIC_SECRET_0 489d092d48d9a6cddb16e45b195ca3a6c5a49dbc703bdac0420b41fb9f77d3fb ea6e1a671f87086ad814a5a88fb855ea9a55323f72bb9cf4a403251027da7106'} data: None\nmessage: {'type': 'send', 'payload': 'EXPORTER_SECRET 489d092d48d9a6cddb16e45b195ca3a6c5a49dbc703bdac0420b41fb9f77d3fb 59e0e1b6cf6a4257c2a950f882b265628f7b2621d0d8211abb7d9415f9684562'} data: None\nmessage: {'type': 'send', 'payload': 'CLIENT_RANDOM d813b9719d2d4a7f4cbf8e5de7bcfdeea9cfab7a5f74f70123f246d0dc29943b 62b74269a2108379202c9434a7f13a434e9edf37a9408cd43d40e77ff99b4bfe5137a52ef887779091768811e8e20a02'} data: None\nmessage: {'type': 'send', 'payload': 'CLIENT_RANDOM 0f9741ca59a5a47dca1ba35736dee6ea808304487b8013db76ec9a39278fe16e 57b054fe8c832ee3b47470e323b16f17ccea24a035b22b0f1ade44d80c18e52491a238f2be7f2c93e1528c4b361928ac'} data: None\nmessage: {'type': 'send', 'payload': 'CLIENT_RANDOM 26c00e713a14a5092541b3b05cc7e7a1f8b6e49cb07173184f732e059b53e218 f3d74023cdc293d9b5f3a32cf33386871eaec21a5abbeb3bc8ba808e36b015ff735d813cbcec82d4846880fdfc8f2959'} data: None\nmessage: {'type': 'send', 'payload': 'CLIENT_RANDOM 0c5622a7cdfd1a0e0aa9fe926bfd6e91c3a934a68343c89a3445cc92ef627397 c65f78f73bac31e7857adb5e6ce100d9f904884bcaef11d4cd157e0a7dc30e75739719c73beafc582e2506f059182054'} data: None\nmessage: {'type': 'send', 'payload': 'CLIENT_RANDOM 2cabf9b1ca4523b8481f07dc4c2d7ee9b78b386aeda4f98bb6d2e0a17eae3ad3 59004c8fe302a8fed74fc9ee5add870063cd716414024874398a76a433b295d16770a23bfa14b50e50057cdc3247e7d5'} data: None\nmessage: {'type': 'send', 'payload': 'CLIENT_RANDOM ef6d5d44239990c6f7aed7a6518a7845f12b064751ddd778aeceb20798a556e0 59004c8fe302a8fed74fc9ee5add870063cd716414024874398a76a433b295d16770a23bfa14b50e50057cdc3247e7d5'} data: None<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>D\u00e9chiffrer le trafic TLS est souvent une \u00e9tape incontournable lorsque l&rsquo;on \u00e9tudie une application Android. La m\u00e9thode usuelle est d&rsquo;utiliser un proxy HTTPS comme BurpProxy ou Http Toolkit. Cette m\u00e9thode n\u00e9cessite g\u00e9n\u00e9ralement d&rsquo;installer un certificat dans le magasin de certificats du terminal (m\u00eame s&rsquo;il est possible d&rsquo;arriver \u00e0 ses fins en patchant l&rsquo;application \u00e9tudi\u00e9e, mais [&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-238","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/posts\/238","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=238"}],"version-history":[{"count":3,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/posts\/238\/revisions"}],"predecessor-version":[{"id":315,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/posts\/238\/revisions\/315"}],"wp:attachment":[{"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/media?parent=238"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/categories?post=238"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolva.fr\/index.php\/wp-json\/wp\/v2\/tags?post=238"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}