Android : Comment consommer un socket ouvert par une autre application ?
Sous Android, pour améliorer la sécurité, est-il possible d'ouvrir un socket dans une application, de s'occuper de l'authentification, puis de confier ce dernier à une autre application ?
Les connections réseaux (sockets) sont ouverts via des handles de fichiers Linux. En consultant les sources d'Android, on constate que ces derniers subissent un traitement spécifique. Via les mécanismes d'AIDL, Android permet d'envoyer un handle de fichier à une autre application.
Concrètement, une application ouvre un fichier dont elle possède les privilèges d'accès puis retourne le handle du fichier à une autre application pour lui permettre d'en lire le contenu. Par exemple, l'application A possède un fichier sensible secret.txt
. L'application B a un besoin ponctuel d’accéder à ce fichier. Soit l'application A modifie les droits du fichier pour permettre à l'application B d'y avoir accès (avec le risque d'ouvrir l'accès à toutes les applications installées), soit l'application A propose une interface AIDL dont une méthode permet de retourner le handle du fichier. Ainsi, l'application B peut lire secret.txt
, mais ne peut pas l'ouvrir directement. B doit le demander à A. A peut alors vérifier les privilèges de B avant de lui donner accès au fichier.
Techniquement, un dup()
est effectué par le module noyau Binder d'Android. dup()
est une fonction du noyau linux permettant de dupliquer le handle du fichier. Ainsi, le fichier est ouvert deux fois. Ce mécanisme est utilisé, entre autres, lors du lancement d'un processus fils. Ce dernier récupère tous les handles des fichiers ouverts par le père (dont les flux stdin
, stdout
et stderr
sur le clavier et l'écran). Le père peut les fermer, ils restent ouverts dans les fils.
Mais, sous Linux, tout est fichier. Tout est traité par un handle de fichier. Il est donc possible d'ouvrir un socket dans une application et l'utiliser dans une autre ! Ouvrir un socket, c'est récupérer un handle. Appliquer un dup()
sur le handle permet d'avoir deux accès au même socket.
Dans Java, le handle d'un socket est rarement utilisé directement. Une classe Socket
se charge de l'encapsulation. La difficulté réside alors dans la création d'une instance Socket
java propre, simplement à partir du handle du socket. Il faut analyser comment la classe est construite pour reconstruire une instance utilisable, depuis un autre processus.
Nous sommes dans une architecture avec plusieurs APK. L'un se charge d'ouvrir une connexion avec le back-end et d'initier l'authentification. L'autre souhaite récupérer le socket pour continuer la communication sans avoir à s'authentifier. Elle ne souhaite pas ouvrir une nouvelle communication mais continuer avec le même socket.
Côté application qui ouvre le socket, il faut extraire le numéro du handle du socket. Nous avons la méthode cachée getFileDescriptor$()
pour cela. Elle sert normalement à faire la liaison avec les IO asynchrones. La méthode getInt$()
du FileDescriptor
permet alors de récupérer la valeur du handle.
Socket socket=new Socket("10.0.2.2",8080); FileDescriptor fd=(FileDescriptor)socket.getClass() .getMethod("getFileDescriptor@@ARTICLE_CONTENT@@quot;).invoke(socket); int fdi=(Integer)fd.getClass().getMethod("getInt@@ARTICLE_CONTENT@@quot;).invoke(fd);
Il faut alors construire un ParcelFileDescriptor
à partir de ce handle.
ParcelFileDescriptor fd=ParcelFileDescriptor.adoptFd(fdi);
Une instance de ce type est traitée de façon spéciale par le module kernel binder ajouté par Android à Linux. Le module sait qu'il s'agit d'un handle de fichier, il peut alors le dupliquer avant d'envoyer le message à une autre application. Notez qu'au passage, ce module injecte le numéro du processus et le numéro de l'utilisateur de l'appelant. La couche AIDL d'Android, via le module kernel, se charge d'effectuer un dup()
sur le handle de fichier, lors de la transmission des données entre les processus. Pour cela, nous déclarons une interface AIDL.
package com.example.sharesocket; import android.os.ParcelFileDescriptor;
interface ShareSocket { ParcelFileDescriptor openSocket(); }
Cette dernière a besoin d'indiquer que ParcelFileDescriptor
doit être envoyé par valeur. Cela s'effectue en ajoutant un autre fichier AIDL dans le package android.os
de votre projet.
package android.os; parcelable ParcelFileDescriptor;
Et un petit service pour permettre le binding sur l'implémentation de l'AIDL :
public class ShareSocketService extends Service { @Override public IBinder onBind(Intent intent) { return new ShareSocketImpl(); } }
Côté consommateur du socket ouvert par l'autre processus, il faut construire une instance PlainSocketImpl
avec un FileDescriptor
, puis le donner au constructeur du socket. Il reste à placer le drapeau isConnected
à true
et le socket est utilisable. Le code théorique est celui-ci :
int fdi=shareSocket.openSocket().getFd(); FileDescriptor fd=new FileDescriptor(); fd.descriptor=fdi; Socket socket=new Socket(new java.net.PlainSocketImpl(fd)); socket.isConnected=true;
Comme les méthodes ne sont pas toutes accessibles, nous utilisons l'introspection.
ParcelFileDescriptor pfd=shareSocket.openSocket(); int fdi=pfd.getFd(); FileDescriptor fd=new FileDescriptor(); Field field; field=fd.getClass().getDeclaredField("descriptor"); field.setAccessible(true); field.set(fd, fdi); Class<?> clPlainSocketImpl=Class.forName("java.net.PlainSocketImpl"); Object psimpl=clPlainSocketImpl.getConstructor(FileDescriptor.class).newInstance(fd); constructor=Socket.class.getDeclaredConstructor(SocketImpl.class); constructor.setAccessible(true); Socket socket=(Socket)constructor.newInstance(psimpl); field=Socket.class.getDeclaredField("isConnected"); field.setAccessible(true); field.setBoolean(socket, true);
Nous pouvons alors utiliser le socket comme s'il avait été ouvert par notre processus !
PrintWriter out=new PrintWriter(socket.getOutputStream()); BufferedReader in=new BufferedReader(new InputStreamReader(socket.getInputStream())); out.println("GET / HTTP/1.0\n\n"); out.flush(); String line=in.readLine(); System.out.println(line);
Et voilà ! Un socket ouvert par une application est consommé par une autre.