Android, SSL master key exportation et déchiffrement

 druide

Les secrets des applications

Tout commence avec une question que je me pose. Comment savoir ce qui transite entre mon natel et le reste du monde lorsque j'utilise telle ou telle application ?

On peut entreprendre le problème de différentes manières :

  • On peut faire du reverse engineering de l'application (fichier apk)
  • On peut sniffer la connexion WiFi durant l'emploi de l'application


J'ai choisi la seconde option car le reverse engineering est souvent laborieux (pour moi) et statique. Avec cette solution, je « vois vivre » l'application.

Bien entendu, la majorité des applications intéressantes utilisent une connexion chiffrée du type TLS et dans ce cas, impossible de voir quoi que ce soit. Pas de problème, je me dis que je vais reproduire ce que j'avais déjà fait pour mon NAS.

Dalvik

Les applications Android sont démarrées dans une machine virtuelle Dalvik. Je débute mon projet avec comme postulat : lorsque l'application chercher à accéder au réseau via une connexion TLS, la demande « sort » de la machine Dalvik via la librairie standard openssl et que tout le code java écrit par google, n'est autre qu'un wrapper sur cette librairie. Est-ce que j'ai raison ? Pour l'instant, mystère ?

Démarche

Ma démarche peut paraître surprenante mais pour savoir si j'ai raison, j'ai commencé par chercher des messages d'erreurs et des appels au secours sur le site StackOverflow.com. En analysant les messages postés, et notamment, les informations de debug devant aider la bonne âme du samaritain, on trouve pleins d'informations utiles. Par exemple, celui-là. On y voit clairement le lien entre le code java :

SSLContext sslCtx = SSLContext.getInstance("TLS");
sslCtx.init(new KeyManager[]{km}, tmf.getTrustManagers(), null);
client.setSslSocketFactory(sslCtx.getSocketFactory());
client.setHostnameVerifier(org.apache.http.conn.ssl.SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);

Et la librairie openssl :
[collapse collapsed title="erreur sur stackoverfow"]


06-02 17:42:01.215  25176-25542/pl.oneapp.sugarloaf A/libc﹕ Fatal signal 11 (SIGSEGV) at 0x00000000 (code=1), thread 25542 (IntentService[P)
    06-02 17:42:01.236      253-253/? I/DEBUG﹕ debuggerd: 2014-06-02 17:42:01
    06-02 17:42:01.236      253-253/? I/DEBUG﹕ *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
    06-02 17:42:01.236      253-253/? I/DEBUG﹕ Build fingerprint: 'tmo_de/ville/ville:4.1.1/JRO03C/148618.10:user/release-keys'
    06-02 17:42:01.236      253-253/? I/DEBUG﹕ pid: 25176, tid: 25542, name: IntentService[P  >>> pl.oneapp.sugarloaf 

[/collapse]

J'ai également parcouru les sites de patch (correctifs d'erreurs) dans lesquels on trouve des informations. Par exemple, ce fichier diff de correction d'erreur du code source même d'Android:

Fixed interruption of blocked SSLSocket via Socket.close()

Bug: 10599593
Change-Id: Iade24eed691756281dfd925abe57740a1ad4145b

diff --git a/NativeCode.mk b/NativeCode.mk
index 42319dc..1a47ee4 100644
--- a/NativeCode.mk
+++ b/NativeCode.mk

@@ -95,11 +95,10 @@
 LOCAL_CFLAGS += -DJNI_JARJAR_PREFIX="com/android/"
 LOCAL_CPPFLAGS += $(core_cppflags)
 LOCAL_SRC_FILES := \
-        crypto/src/main/native/org_conscrypt_NativeCrypto.cpp \
-        luni/src/main/native/AsynchronousSocketCloseMonitor.cpp
+        crypto/src/main/native/org_conscrypt_NativeCrypto.cpp
 LOCAL_C_INCLUDES += $(core_c_includes) \
         libcore/luni/src/main/native
-LOCAL_SHARED_LIBRARIES += $(core_shared_libraries) libcrypto libssl libnativehelper libz
+LOCAL_SHARED_LIBRARIES += $(core_shared_libraries) libcrypto libssl libnativehelper libz libjavacore
 LOCAL_STATIC_LIBRARIES += $(core_static_libraries)
 LOCAL_MODULE_TAGS := optional
 LOCAL_MODULE := libjavacrypto
La pêche semblait me donner raison 😎. En effet, on trouve souvent des liens à la libssl.so. En creusant encore un peu, je tombe sur des messages (sur divers sites d'actualités informatique) indiquant que google a créé son propre fork d'openssl sous le nom de boringssl. Il ne restera donc plus qu'à modifier le code source et recompiler le tout ✌. Bien sûr, ça n'est jamais aussi facile...

Compilation d'Android (cyanogenmod 12.1)

La première étape consiste à être en mesure de compiler un système Android.

J'ai dans mes tiroirs un vieux Samsung Galaxy S4 GT-I9505. Je peux donc me baser sur une version cyanogenmod jfltexx et suivre les instructions présentes sur leur site. Tout se passe bien pour autant que la machine soit suffisamment puissante et surtout, qu'elle possède suffisamment de RAM. Au début j'ai tenté l’expérience dans une machine virtuelle avec 12Go de RAM et j'ai obtenu des messages d'erreurs à la compilation qui étaient complètement erronés. Genre, problème d'inclusion de fichier d'entête *.h !!! Pour pouvoir compiler, j'ai dû faire la compilation sur une machine physique 32Go et là, aucun souci.


Je ne détaille pas les étapes pour rooter son smartphone, installer un recovery mode et flasher sa ROM. Il y a suffisamment de sites web qui en parlent.

Me voilà maintenant en possession de "ma" ROM cm-12.1 fraîchement compilée et installée. Il est temps de coder...

BoringSSL

Le code source de la librairie se trouve dans android/system/chromium_org/thirdy_party/boringssl/src/ssl. Mon idée, c'est d'exporter la master_key ainsi que le random généré par le client lors de l'étape Client Hello du protocole TLS. Le code vient se placer dans le fichier source s3_clnt.c. En effet, si à priori, on était tenté de modifier le code dans le fichier t1_clnt.c, un rapide examen au début du code de ce fichier nous apprend que la connexion se fait avec le code présent dans le fichier s3_clnt.c :

# t1_clnt.c
IMPLEMENT_tls_meth_func(TLS1_2_VERSION, TLSv1_2_client_method,
			ssl_undefined_function,
			ssl3_connect,                   // <------- C'EST DANS s3 que se trouve cette fonction
			tls1_get_client_method,
			TLSv1_2_enc_data)
C'est donc bien dans le fichier s3_clnt.c, où se trouve le code source de la méthode
connect
qu'il faut agir. J'ai créé une fonction d'exportation à la « nss like ». Cette fonction exporte les valeurs dans un tableau de
char
avant de l'écrire dans un fichier texte à la racine de /sdcard/. Le fichier se nomme key_boringssl. Je passe par un tableau de
char
avant d'écrire dans le fichier simplement pour limiter au maximum le temps d'écriture.

/*
 * Druide <druide@druid.es>
 * 
 * Export the key to decrypt the data like Firefox
 * s->handle is a pointer to struct ssl_st defined in ssl.h
 */
void export_secret(SSL *s)
{
	char *buf   = NULL;
	char *ptr   = NULL;
	FILE *fkey  = NULL;
	char path[] = "/sdcard/key_boringssl";
	int i       = 0;
	int size    = 14 +                              // "CLIENT_RANDOM "
			SSL3_RANDOM_SIZE * 2 +          // each byte in hex
			SSL3_MASTER_SECRET_SIZE * 2 +   // format xx
			1  +                            // "\n"				
			1;                              // NULL

	buf = (char*) malloc(sizeof(char) * size);
	if(buf != NULL)
	{
		ptr = buf;
		ptr += sprintf(ptr, "CLIENT_RANDOM ");
		for (i=0; i<SSL3_RANDOM_SIZE; i++)
		{	// s3 is an struct ssl3_state_st defined in ssl3.h. This struct contain unsigned char client_random[SSL3_RANDOM_SIZE];
			ptr += sprintf(ptr, "%02x", s->s3->client_random[i]);
		}
		ptr += sprintf(ptr, " ");
		
		for (i=0; i<SSL3_MASTER_SECRET_SIZE; i++)
		{	// session is an SSL_SESSION *session defined in ssl.h. It contain the master_key[SSL3_MASTER_SECRET_SIZE];
			ptr += sprintf(ptr, "%02x", s->session->master_key[i]);
		}
		sprintf(ptr, "\n");

		fkey = fopen(path, "a");
		if(fkey != NULL)
		{         
			fprintf(fkey, "%s", buf);
			fclose(fkey);
		} 
		
		free(buf);
		ptr = NULL;
	}
}
Puis, dans la fonction
int ssl3_connect(SSL *s)
, à la ligne où se trouve le
case SSL_ST_OK:
:

...

case SSL_ST_OK:
      export_secret(s);
      /* clean a few things up */

...
PS: oui, ce n'est pas sûr de faire un
sprintf
, il faudrait faire au minimum un
snprintf
mais bon, vu l'objectif du programme 😁

OpenSSL

Dans l'arborescence des sources d'Android, on trouve également la librairie OpenSSL. Sans doute que d'autres programmes, tel que cURL utilisent la librairie originale. Tant qu'à faire, autant faire également les modifications dans cette librairie 😎. Le chemin d'accès est : android/system/external/openssl/ssl/. Les fichiers sont les mêmes (presque), donc la modification se fait de la même manière. Pour éviter des accès concurrents au fichier, je sauve les valeurs d'openssl dans /sdcard/key_openssl.




Premier test

Une fois la nouvelle ROM installée, je me rends, à l'aide d'adb, dans le répertoire /sdcard/

$ adb shell
shell@jfltexx:/ $ su
root@jfltexx:/ # cd /sdcard/
root@jfltexx:/sdcard # ls -l
...
-rw-rw---- root     sdcard_r      528 2016-03-15 12:20 key_boringssl
-rw-rw---- root     sdcard_r      880 2016-03-16 16:37 key_openssl
...
root@jfltexx:/sdcard # cat key_boringssl                                       
CLIENT_RANDOM fdf52129506e51e141e662133c4fd51d05a38bae277839ec340f9a7f357c75fc ecb0c1d2f1fed6514c8f150651e107a700b56b929290d7313c4fb6c10eb39a62e07be945be9f989e24b23e7b20d05c4f
CLIENT_RANDOM aca750009281d2f8c43529dcc46971c2e8ded774b1f538cf085b20c530af5b6e 1d5192919afed773ef194471cc62572f00b9f6e7bdc5724c1609cbef30a01becad770fd041b27e4cc910c96e3c742709
CLIENT_RANDOM d4c1a7fb74997c66bbfd2cacbd4065917b25d912a7051f308c1612d1d83cd348 dc7b11d86ed83500f261884b2612184e3164fc7117fa207785726aec543b3d24cf38c747f63cb5b0c7260f7c2c36fe31
root@jfltexx:/sdcard #

tcpdump

Il existe une version binaire de tcpdump pour Android mais les développeurs de cyanogenmod ont pensé à le mettre directement dans le système

root@jfltexx:/sdcard # ls -l /system/xbin/ | grep tcpdump                      
-rwxr-xr-x root     shell      833800 2016-03-07 18:42 tcpdump
root@jfltexx:/sdcard # tcpdump -h                                                    
tcpdump version 4.5.1
libpcap version 1.5.2
Usage: tcpdump [-aAbdDefhHIJKlLnNOpqRStuUvxX] [ -B size ] [ -c count ]
		[ -C file_size ] [ -E algo:secret ] [ -F file ] [ -G seconds ]
		[ -i interface ] [ -j tstamptype ] [ -M secret ]
		[ -Q in|out|inout ]
		[ -r file ] [ -s snaplen ] [ -T type ] [ -V file ] [ -w file ]
		[ -W filecount ] [ -y datalinktype ] [ -z command ]
		[ -Z user ] [ expression ]


On peut dès lors sniffer le trafic réseau sur l'interface wifi


root@jfltexx:/sdcard # tcpdump -D                                          
1.wlan0
2.nflog (Linux netfilter log (NFLOG) interface)
3.nfqueue (Linux netfilter queue (NFQUEUE) interface)
4.p2p0
5.any (Pseudo-device that captures on all interfaces)
6.lo
root@jfltexx:/sdcard # tcpdump -v -i wlan0 -w dump.log                        
tcpdump: listening on wlan0, link-type EN10MB (Ethernet), capture size 65535 bytes
^C496 packets captured
511 packets received by filter
0 packets dropped by kernel
root@jfltexx:/sdcard # 
On peut maintenant analyser les trames dans wireshark

Analyse des trames

Avant de débuter l'analyse des trames, il faut récupérer les fichiers dump.log, key_boringssl et key_openssl et réunir toutes les master_key

$ adb pull /sdcard/key_openssl
10 KB/s (880 bytes in 0.085s)
$ adb pull /sdcard/key_boringssl
11 KB/s (880 bytes in 0.076s)
$ adb pull /sdcard/dump1.log
2318 KB/s (431957 bytes in 0.181s)
$cat key_boringssl key_openssl > key


Ensuite, on démarre wireshark, on configure la clé dans les préférences du protocole SSL


Et on obtient


Le cas google

Tout fonctionne à merveille, tout ? Non, un petit village peuplé d'irréductibles... Lors de l'analyse des trames, une partie ne se décode pas. Le contenu reste chiffré !!! Il s'agit des trames qui ont pour destination les adresses XXXXXXXX.1e100.net, c'est-à-dire, google. Ces trames ne se décodent pas et, après plusieurs recherches, j'ai fini par comprendre pourquoi. La réponse se trouve dans la trame Server Hello et plus précisément, sur le choix de la Cipher Suite. Il semble que google à mis en route un nouveau système de chiffrement ainsi qu'une nouvelle méthode d'authentification de message (traduction wikipedia) ou, terminologie plus souvent employée fonctions de hachage. Il s'agit de deux algorithmes mathématiques créés par Daniel J. Bernstein.




Solution

Pour déchiffrer ces trames, j'ai exploré plusieurs pistes. La première étant la compilation d'un wireshark dernière version utilisant la librairie GnuTLS 3.4.0 qui contient le support de ce chiffrement
* Version 3.4.0

** libgnutls: Added support for Chacha20-Poly1305 ciphersuites
following draft-mavrogiannopoulos-chacha-tls-05 and
draft-irtf-cfrg-chacha20-poly1305-10. That is currently provided as
technology preview and is not enabled by default, since there are no
assigned ciphersuite points by IETF and there is no guarrantee of
compatibility between draft versions. The ciphersuite priority string
to enable it is "+CHACHA20-POLY1305".


Mais rien n'y a fait...


Alors je suis retourné à mon code source. Je me suis dit que finalement, vu que c'est l'application qui informe le serveur des combinaisons possibles qu'elle « comprend », autant lui couper l'herbe sous le pied en lui enlevant ces possibilités.

            Cipher Suites (21 suites)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (0xcc14) <-- celle-là
                Cipher Suite: TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcc13)   <-- celle-là
                Cipher Suite: TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (0xcc15)     <-- et celle-là
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)
                Cipher Suite: TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (0x009e)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
                Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0x0039)
                Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
                Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0x0033)
                Cipher Suite: TLS_ECDHE_RSA_WITH_RC4_128_SHA (0xc011)
                Cipher Suite: TLS_ECDHE_ECDSA_WITH_RC4_128_SHA (0xc007)
                Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c)
                Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035)
                Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)
                Cipher Suite: TLS_RSA_WITH_RC4_128_SHA (0x0005)
                Cipher Suite: TLS_RSA_WITH_RC4_128_MD5 (0x0004)
                Cipher Suite: TLS_RSA_WITH_3DES_EDE_CBC_SHA (0x000a)
                Cipher Suite: TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0x00ff)

« back to boringssl »

Cette fois ça se passe dans le fichier android/system/chromium_org/thirdy_party/boringssl/ssl/src/s3_lib.c. En effet, c'est dans ce fichier qu'on trouve les « ciphers » disponibles dans la librairie.

/* list of available SSLv3 ciphers (sorted by id) */
const SSL_CIPHER ssl3_ciphers[]={
   ...


J'ai donc simplement déplacé l'ouverture d'un commentaire 😎


/*

	{
	1,
	TLS1_TXT_ECDHE_RSA_WITH_CHACHA20_POLY1305,
	TLS1_CK_ECDHE_RSA_CHACHA20_POLY1305,
	SSL_kEECDH,
	SSL_aRSA,
	SSL_CHACHA20POLY1305,
	SSL_AEAD,
	SSL_TLSV1_2,
	SSL_HIGH,
	SSL_HANDSHAKE_MAC_SHA256|TLS1_PRF_SHA256|SSL_CIPHER_ALGORITHM2_AEAD|FIXED_NONCE_LEN(0),
	256,
	0,
	},

	{
	1,
	TLS1_TXT_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
	TLS1_CK_ECDHE_ECDSA_CHACHA20_POLY1305,
	SSL_kEECDH,
	SSL_aECDSA,
	SSL_CHACHA20POLY1305,
	SSL_AEAD,
	SSL_TLSV1_2,
	SSL_HIGH,
	SSL_HANDSHAKE_MAC_SHA256|TLS1_PRF_SHA256|SSL_CIPHER_ALGORITHM2_AEAD|FIXED_NONCE_LEN(0),
	256,
	0,
	},

	{
	1,
	TLS1_TXT_DHE_RSA_WITH_CHACHA20_POLY1305,
	TLS1_CK_DHE_RSA_CHACHA20_POLY1305,
	SSL_kEDH,
	SSL_aRSA,
	SSL_CHACHA20POLY1305,
	SSL_AEAD,
	SSL_TLSV1_2,
	SSL_HIGH,
	SSL_HANDSHAKE_MAC_SHA256|TLS1_PRF_SHA256|SSL_CIPHER_ALGORITHM2_AEAD|FIXED_NONCE_LEN(0),
	256,
	0,
	},

 end of list */
Rien à faire dans OpenSSL qui n'intègre pas ces chiffrements dans la version présente avec cm-12.1




C̶H̶A̶C̶H̶A̶2̶0̶-̶P̶o̶l̶y̶1̶3̶0̶5̶

Nouveau test sans les chiffrements spécifiques à la google

Client Hello

Cipher Suites (18 suites)
    Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
    Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)
    Cipher Suite: TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (0x009e)
    Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (0xc014)
    Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (0xc00a)
    Cipher Suite: TLS_DHE_RSA_WITH_AES_256_CBC_SHA (0x0039)
    Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (0xc013)
    Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (0xc009)
    Cipher Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA (0x0033)
    Cipher Suite: TLS_ECDHE_RSA_WITH_RC4_128_SHA (0xc011)
    Cipher Suite: TLS_ECDHE_ECDSA_WITH_RC4_128_SHA (0xc007)
    Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256 (0x009c)
    Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035)
    Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)
    Cipher Suite: TLS_RSA_WITH_RC4_128_SHA (0x0005)
    Cipher Suite: TLS_RSA_WITH_RC4_128_MD5 (0x0004)
    Cipher Suite: TLS_RSA_WITH_3DES_EDE_CBC_SHA (0x000a)
    Cipher Suite: TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0x00ff)

Server Hello

Cette fois, le serveur n'a pas le choix, il devra prendre un « ancien » chiffrement

Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)

SPDY

On y est


En fait, je ne suis pas au bout de mes surprises. En effet, ce que je vois n'est pas du HTTP/1.1 classique mais du spdy (prononcer speedy) version 3



Comme quoi, on ne chôme pas chez google. Normalement, ils devraient abandonner leur protocole d'expérimentation SPDY au profit d'HTTP 2.0 (annonce). En attendant, pour trouver le contenu d'une trame, il faut procéder ainsi:
  1. On commence par chercher SYN_STREAM qui correspond à la demande
  2. On trouve ensuite la réponse du serveur dans SYN_REPLY
  3. Le contenu se trouve dans les DATA's. Il peut y en avoir plusieurs. On sait que c'est la fin grâce au flag FIN correspondant
        Flags: 0x01 (FIN)
            .... ...1 = FIN: Set
    

Exemple

SYN_STREAM contient dans un des header le chemin d'accès de la ressource souhaitée.

    Header block: 38eae3c6a7c202a52a5076b2821608d2e9f9f9e939c042b9...
    Header: :host: www.google.ch
    Header: :method: GET
    Header: :path: /webhp?client=android-google&source=android-home&gws_rd=cr&ei=-O_nVorQN-mP6ATS3YWIDA
        Name: :path
        Value: /webhp?client=android-google&source=android-home&gws_rd=cr&ei=-O_nVorQN-mP6ATS3YWIDA
    Header: :scheme: https
    Header: :version: HTTP/1.1
    Header: accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
    Header: accept-encoding: gzip, deflate
    Header: accept-language: fr-CH,en-US;q=0.8
    Header: user-agent: Mozilla/5.0 (Linux; Android 5.1.1; GT-I9505 Build/LMY49G) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/39.0.0.0 Mobile Safari/537.36
    Header: x-requested-with: com.android.browser
Le serveur renvoie un SYN_REPLY indiquant « que c'est ok » et les DATA suivent. Il faut prendre toutes les trames DATA jusqu'à ce que l'on trouve la trame DATA ayant le FLAG FIN: Set c'est-à-dire à 1



On peut alors exporter les Data via le menu File → Export Packet Bytes. On voit d'ailleurs dans l'image ci-dessus que le magic number (1f b8) correspond à un flux gzip. On peut donc décompresser le flux exporté simplement en le renommant



Conclusion

Me voilà « armé » d'un smartphone équipé pour le sniffing réseau. Au passage j'ai découvert un nouveau chiffrement, une nouvelle fonction de hash et un nouveau protocole... rien que ça... Autant dire que j'ai du "pain sur la planche".

That's all folks

  • 3 years 8 months before
  • |