2012-10-24

Android et le sentiment d'insécurité SSL

Hier,  Generation-NT a publié un article sur la sécurité d'Android au niveau du protocole SSL.

Si le titre "Android : des experts démontrent une sécurisation défaillante SSL" est racoleur, l'introduction est plus précise "Des experts en sécurité suggèrent que de nombreuses applications Android sécurisent mal les données sensibles malgré les protocoles de protection.".

L'étude

L'article fait écho à un article de Ars Technica basé sur l'étude de Sascha Fahl, Marian Harbach, Thomas Muders, Matthew Smith, Lars Baumgärtner et Bernd Freisleben (ou ici).  L'art d'Ars est de monter en épingle des détails de l'étude : pwn un antivirus en injectant sa propre signature dans sa base de signature de virus, vol de cookies, rejeu de requête...

Aujourd'hui il existe suffisamment d'attaques SSL simples pour ne plus se reposer uniquement sur le chiffrement de la conversation pour se sentir en sécurité.

Gestion par défaut du SSL par Android

Il est important de comprendre que par défaut les API du SDK Android lancent une "exception" si il y a un quelconque problème dans la communication SSL. Le problème peut venir d'une autorité de certification inconnue, un problème d'implémentation du protocole SSL (version, handshake) ...

Le problème le plus fréquent est l'autorité de certification inconnue. Plutôt que de payer 80€/an un certificat SSL, quelques développeurs créent leur propre autorité de certification (self-signed cert).
Comme Android refuse par défaut le certificat self-signed, le développeur adapte l'application pour qu'elle l'accepte malgré le primo-refus d'Android.

Ce mécanisme n'est pas vraiment un problème si le developpeur prend des mesures supplémentaires qu'on verra plus tard.

Détection des apps qui acceptent les self-signed


Prenons un exemple simple pour faire un HTTP GET sur Twitter en https.
Trois lignes de JAVA suffisent :
HttpGet request = new HttpGet("http://search.twitter.com/search.json?q=blue%20angels&rpp=5&include_entities=true&result_type=mixed");
HttpClient httpClient = new DefaultHttpClient();
HttpResponse response = httpClient.execute(request);

Maintenant ce qu'on trouve le plus communément pour un certificat self-signed, ce code est DÉCONSEILLÉ tel quel :
public static HttpClient wrapClient(HttpClient base) {
 try {
  SSLContext ctx = SSLContext.getInstance("TLS");
  X509TrustManager tm = new X509TrustManager() {

   @Override
   public void checkClientTrusted(
     java.security.cert.X509Certificate[] chain,
     String authType)
     throws java.security.cert.CertificateException {
    // TODO Auto-generated method stub

   }
   @Override
   public void checkServerTrusted(
     java.security.cert.X509Certificate[] chain,
     String authType)
     throws java.security.cert.CertificateException {
    // TODO Auto-generated method stub
   }

   @Override
   public java.security.cert.X509Certificate[] getAcceptedIssuers() {
    // TODO Auto-generated method stub
    return null;
   }
  };
  X509HostnameVerifier verifier = new X509HostnameVerifier() {

   @Override
   public boolean verify(String arg0, SSLSession arg1) {
    // TODO Auto-generated method stub
    return true;
   }

   @Override
   public void verify(String arg0, SSLSocket arg1)
     throws IOException {
    // TODO Auto-generated method stub
   }

   @Override
   public void verify(String arg0,
     java.security.cert.X509Certificate arg1)
     throws SSLException {
    // TODO Auto-generated method stub
   }

   @Override
   public void verify(String arg0, String[] arg1, String[] arg2)
     throws SSLException {
    // TODO Auto-generated method stub
    }
  };
  ctx.init(null, new TrustManager[] { tm }, null);
  SSLSocketFactory ssf = new SSLSocketFactory(ctx);
  ssf.setHostnameVerifier(verifier);
  ClientConnectionManager ccm = base.getConnectionManager();
  SchemeRegistry sr = ccm.getSchemeRegistry();
  sr.register(new Scheme("https", ssf, 443));
  return new DefaultHttpClient(ccm, base.getParams());
 } catch (Exception ex) {
  ex.printStackTrace();
  return null;
 }
}
(...)
HttpGet request = new HttpGet("http://search.twitter.com/search.json?q=blue%20angels&rpp=5&include_entities=true&result_type=mixed"
HttpClient httpClient = new DefaultHttpClient();
httpClient = wrapClient(httpClient);
HttpResponse response = httpClient.execute(request);
(...)
Soyons clairs, ce code là ne fait QUE accepter TOUS les certificats, signés ou non. Le développeur doit vérifier ensuite l'IP et/ou l'autorité ce certification et/ou le hash du certificat.

Les auteurs de l'étude ont téléchargé 13500 applications depuis le PlayStore puis les ont dé-compilé (AndroGuard+MalloDroid).
Ils ont recherché dans le code les mots clefs de la gymnastique Java nécessaire à l'acceptation des certificats self-signed (AcceptAllTrustM AllTrustM DummyTrustM EasyX509TrustM  .. page 54).

Les attaques qui marchent

Les auteurs de l'étude font un tour d'horizon des attaques SSL qui aboutissent à coup sûr lorsqu'une application désactive les erreurs SSL et qui peuvent également aboutir dans certains cas. 
  • Attaque mitm, pour man in the middle. Une machine fait proxy SSL entre Android et le serveur par exemple Twitter.com. Cette attaque fonctionne avec toutes les application "normales" et un Android configuré pour l'attaque (= l'autorité de certification utilisée par l'outil de mitm à été ajoutée dans Android).
  • Les attaques réseaux (attaque arp poison ou dns menteur ou iptables ou ...), qui renvoie vers une machine piège plutôt que la légitime. Cette attaque fonctionne dans les mêmes condition qu'une attaque mitm.
  • L'attaque sslstrip. C'est un proxy (attaque arp poison ou dns menteur ou iptables ou ...) qui piège l'utilisateur distrait qui saisi "paypal.com" dans son navigateur. Le proxy propose un faux paypal en http qui communique en httpS avec le vrai serveur "paypal.com" mais en http avec le terminal Android. Cette attaque ne fonctionne qu'avec un navigateur et par défaut avec Android.

Comment "bien" faire ?

Le point commun de toutes ces attaques est qu'une machine intermédiaire lit ce qu'il se passe.

Dans l'idéal :
  • Toute communication doit être faite en SSL
  • L'application doit vérifier l'IP résolue par le nom
  • L'application doit comparer l'autorité de certification du certificat avec une liste interne
  • L'application doit comparer le hash du certificat avec une liste interne.
  • L'application doit utiliser des tokens contre le rejeu
Les auteurs de l'étude préconisent aussi :
  • Que Google inclue un "vérificateur" d'APK basé sur MalloDroid avant de l'installer (analyse statique heuristique ou par signature)
  • HTTPS EveryWhere
  • Une distinction entre INTERNET_PLAIN et INTERNET_SSL pour informer les utilisateurs d'une probable communication "en-clair".

Conclusion

Comme écrit sur Twitter l'application analysée par mes soins la plus sérieuse est le client officiel Twitter qui, comme les autres, prête au moins un flanc aux attaques et fini par livrer en clair ses communications … et ses tokens.

Aucun commentaire: