Depuis ma dernière présentation publique concernant Android (c'est-à-dire depuis la Nuit du Hack 2011, pfiou), beaucoup de personnes m'ont demandé s'il était possible d'envoyer des SMS de classe 0 sur les dernières versions de cet OS. Personnes auxquelles j'avais répondu à l'époque que la démonstration avait été réalisée sur la version 2.1, et que l'astuce ne fonctionnait plus sur les versions récentes. Néanmoins, l'idée de chercher un moyen de le faire sur les version supérieures à 2.1 me trottait en tête depuis ce temps, mais j'avoue ne pas avoir pris le temps de m'y intéresser. Jusqu'à très récemment.
SMS de classe 0: rappels
Lors de ma précédente présentation, j'avais démontré la facilité avec laquelle on pouvait créer des SMS à partir du PDU (Protocol Description Unit) et comment on pouvait se servir de cette fonctionnalité pour transformer un SMS classique en SMS de classe 0. Pour rappel, un SMS de classe 0 est un message particulier qui est destiné à s'afficher "de force" par le téléphone qui le reçoit, généralement utilisé par les opérateurs pour envoyer des notifications critiques aux utilisateurs de leur réseau ou tout simplement pour communiquer directement avec la SIM (via des systèmes peu documentés).
Ainsi, pour envoyer un SMS à partir du PDU correspondant, il fallait modifier la visibilité d'une méthode du SmsManager (sendRawPdu) afin de pouvoir l'appeler, sous réserve de posséder les bonnes permissions. Il s'agissait en fait d'une fonctionnalité offerte par le SDK Android, qui n'était pas directement exposée mais qui était toutefois présente. Cela était valable jusqu'à la version 2.1 du système. A partir de la version 2.2, le SDK a radicalement changé (du moins en ce qui concerne l'envoi de SMS), et cette méthode a disparu: impossible d'envoyer des SMS "craftés" à partir de PDUs ! Damned. Rien sur Internet expliquant comment le faire, et le code est un brin indigeste.
Services systèmes d'Android
Un jour, j'ai pris mon courage à deux mains et décidé de plonger dans le code du SDK [2], qui de fait est opensource et librement consultable, afin de trouver un moyen d'envoyer (de nouveau) ce type de message. Et par extension d'être capable de faire joujou avec les PDUs, comme au bon vieux temps. Et là, le drame: plus aucune référence de "PDU" dans les éléments offerts par le SDK. Le vide. Vraiment rien ? Eh bien après quelques recherches plus poussées, j'ai pu identifier une classe offrant une méthode nommée sendRawPdu, mais appartenant au package com.android.internal.telephony. En cherchant à savoir où elle était appelée, je suis remonté jusqu'à la classe com.android.internal.telephony.IccSmsInterfaceManager, qui propose des méthodes permettant l'envoi de messages (mais pas de PDU) et qui hérite de ISms.Stub. Attendez, "ISms", serait-ce une interface ? Jetons-y un œil.
Il s'agit effectivement d'une interface munie d'un Stub qui sert principalement de wrapper de service, tel que précisé dans la documentation:
<quote>Interface for applications to access the ICC phone book. The following code snippet demonstrates a static method to retrieve the ISms interface from Android:
private static ISms getSmsInterface()
throws DeadObjectException *
IServiceManager sm = ServiceManagerNative.getDefault();
ISms ss;
ss = ISms.Stub.asInterface(sm.getService("isms"));
return ss;
*
</quote>
Il y est fait mention d'un ServiceManagerNative, qui gère des services. En réalité, c'est un service natif qui gère l'envoi des SMS, appelé au travers d'une couche bien définie de classes permettant de vérifier les permissions et de structurer le message à envoyer. Il existe d'ailleurs d'autres services que ce "isms": "phone","content", etc ... L'utilisation de services permet de cloisonner les fonctionnalités mais surtout de limiter les fonctions exportées: l'interface restreint leur usage et complique l'implémentation d'autres fonctionnalités.
Là où ça devient intéressant, c'est que la classe SMSDispatcher est instanciée par la classe `IccSmsInterfaceManager <http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.3.7_r1/com/android/internal/telephony/IccSmsInterfaceManager.java?av=f>`_, et il semble donc possible de pouvoir récupérer une instance de cette classe. J'ai de suite pensé l'instancier via l'application, mais un problème est survenu: le contexte d'instanciation n'est pas bon. Seule une instance de la classe com.android.internal.telephony.Phone peut retourner l'instance unique et utilisable d'IccSmsInterfaceManager, via la méthode getIccSmsInterfaceManager. Et c'est là que ça se complique.
Récupération de l'instance système Phone
Le problème reste entier: sans une instance de la classe Phone, impossible d'atteindre une instance de SMSDispatcher, on peut donc oublier l'envoi de SMS via PDU. Cependant, une classe offre la possibilité de récupérer l'instance Phone courante: PhoneFactory. Cette classe est encore une fois interne et n'est pas exposée via le SDK "standard" offert par Google. Elle offre une méthode getDefaultPhone() qui retourne une instance de Phone, avec toutefois un bémol: un appel à cette méthode ne réussit que si le code appelant est exécuté au sein du processus com.android.phone, le processus dédié (et unique) à la gestion de la téléphonie sur le smartphone. Et un souci de plus.
Pour pouvoir exécuter du code dans le contexte du processus com.android.phone, il faut que l'application respecte les contraintes suivantes: * elle doit être signée avec le même certificat que l'application créant le processus com.android.phone * le manifest doit spécifier le processus dans lequel une application, activité ou service va s'exécuter
Par conséquence, notre application doit être une application système (com.android.phone étant mis en place par le système, et signée avec le certificat système) et doit forcer son code à être exécuté dans le contexte de com.android.phone. Heuu, ça s'annonce difficile.
Plusieurs questions se posent: * Comment créer une application système ? * Comment exécuter une activité dans le contexte du processus com.android.phone ?
Une fois ces questions résolues, l'appel de sendRawPdu sera un jeu d'enfant.
Création d'une application système
Nous allons devoir créer une application système, cela implique plusieurs choses: * être capable d'utiliser tous les éléments du SDK Android, et notamment les classes internes (com.android.internal.*) * être capable de signer l'application de manière à ce qu'elle soit reconnue comme application système * être capable de l'installer sur un smartphone
L'élément le plus bloquant pour la création d'application système reste la phase de signature de l'APK: celui-ci doit être signé avec le même certificat que les autres applications systèmes. Sur une version Android standard, c'est quasi impossible à moins d'avoir la clef privée. Par contre sur des mods d'Android comme CyanogenMod, la clef privée du certificat est publique et peut être utilisée pour signer une application et l'installer comme application système sur une rom CyanogenMod.
Le développement avec Eclipse est plus sport, la version du framework Android (android.jar) fournie dans le SDK ne contient pas les APIs internes. A moins d'utiliser la réflexion Java à gogo, s'interfacer avec les APIs internes est impossible en l'état. C'est ce que je pensais jusqu'à ce que je tombe sur ce tutorial très bien fait, expliquant comment intégrer dans le fichier android.jar les classes internes récupérées grâce à jd-gui, et comment patcher le plugin ADT afin d'autoriser l'utilisation des APIs internes. Il est assez bien fait, mais je vais vous donner une méthode plus rapide pour arriver au même résultat.
Premièrement, il vous faudra télécharger les différentes versions du SDK Android qui incluent les classes internes, qui sont disponibles sur github. Ensuite, pour chaque dossier dans le SDK, copier le contenu dans un dossier avec pour nom android-X-internals, et insérer dans chacun d'entre eux les fichiers issus de github:
# cd <SDK>/platforms # cp -rf ./android-7 ./android-7-internals # cp -rf ./android-8 ./android-8-internals # cp -rf ./android-9 ./android-9-internals # cp -rf ./android-10 ./android-10-internals # wget "https://github.com/inazaruk/android-sdk/tarball/master" -O /tmp/platforms.tgz # tar xvzf /tmp/platforms.tgz
Il ne reste plus qu'à patcher le plugin ADT, tel qu'indiqué sur cette page. Une fois cela fait, relancer Eclipse et vous devriez voir apparaître de nouvelles versions du SDK d'android, avec un numéro de SDK négatif ...
... ainsi que la restriction sur les APIs internes, qui doit être patchée (com/andoird/internax/* à la place de com/android/internal/*):
Une fois notre application développée, nous allons pouvoir la signer et l'installer sur notre smartphone (utilisant CyanogenMod si vous avez suivi):
# signapk.sh MonApp.apk /tmp/MonApp-signed.apk # adb shell mount -o rw,remount /system # adb push /tmp/MonApp-signed.apk /system/app
J'utilise signapk.sh, un script basé sur signapk.jar et les certificats de CyanogenMod (Gingerbread, CM 7.2). Ce script effectue basiquement la commande suivante:
java -jar signapk.jar platform.x509.pem platform.pk8 Application.apk Application-signed.apk
Après cette dernière étape, notre application système sera installée et reconnue par Android en tant que tel. Bon, reste à développer une application d'envoi de SMS classe 0 maintenant que l'on possède un IDE et un SDK améliorés.
Utilisation de la réflexion pour l'envoi de SMS via PDU
Faisons le point. On a un éditeur Eclipse avec ADT patché et au moins un SDK qui intègre les classes com.android.internal.*. On sait aussi déployer une application système. Au boulot.
Pour commencer, on récupère l'instance de Phone (supposons que cela soit exécuté dans le bon processus, ce point sera abordé par la suite):
Phone phone = PhoneFactory.getDefaultPhone();
Puis on récupère une instance de IccSmsInterfaceManager:
f = IccSmsInterfaceManager.class.getDeclaredField("mDispatcher");
f.setAccessible(true);
SMSDispatcher sms_disp = (SMSDispatcher)f.get(ismsm);
Et on utilise un brin de réflexion Java pour appeler la bonne méthode, en transformant au passage un message SMS classique en SMS de classe 0 (via la manipulation des PDUs)
byte[] b = new byte[0];
SmsMessage.SubmitPdu pdus = SmsMessage.getSubmitPdu(
null, phoneNumber, message,false
);
/* change class to Class 0 */
size = (int)pdus.encodedMessage[2];
size = (size/2) + (size%2);
pdus.encodedMessage[size+5] = (byte)0xF0;
/* send raw pdu */
Method m = SMSDispatcher.class.getDeclaredMethod("sendRawPdu", b.getClass(), b.getClass(), PendingIntent.class, PendingIntent.class);
m.setAccessible(true);
m.invoke(sms_disp, pdus.encodedScAddress, pdus.encodedMessage, null, null);
Et voilà, rien de bien compliqué niveau code. Une fois le tout assemblé avec la gestion des exceptions:
/* Sends class 0 SMS */
private boolean sendSMS(String phoneNumber, String message)
*
int size;
Field f;
SMSDispatcher sms_disp;
Phone phone = PhoneFactory.getDefaultPhone();
/* Get IccSmsInterfaceManager */
IccSmsInterfaceManager ismsm = phone.getIccSmsInterfaceManager();
try {
f = IccSmsInterfaceManager.class.getDeclaredField("mDispatcher");
f.setAccessible(true);
sms_disp = (SMSDispatcher)f.get(ismsm);
if (sms_disp != null)
{
byte[] b = new byte[0];
SmsMessage.SubmitPdu pdus =
SmsMessage.getSubmitPdu(
null, phoneNumber, message,false
);
/* change class to Class 0 */
size = (int)pdus.encodedMessage[2];
size = (size/2) + (size%2);
pdus.encodedMessage[size+5] = (byte)0xF0;
/* send raw pdu */
Method m = SMSDispatcher.class.getDeclaredMethod("sendRawPdu", b.getClass(), b.getClass(), PendingIntent.class, PendingIntent.class);
m.setAccessible(true);
m.invoke(sms_disp, pdus.encodedScAddress, pdus.encodedMessage, null, null);
return true;
*
else
return false;
} catch (SecurityException e) *
return false;
* catch (NoSuchFieldException e) *
return false;
* catch (IllegalArgumentException e) *
return false;
* catch (IllegalAccessException e) *
return false;
* catch (NoSuchMethodException e) *
return false;
* catch (InvocationTargetException e) *
return false;
*
}
Exécution dans le contexte de com.android.phone
Le cœur du système d'envoi est développé, il ne reste plus qu'à préciser le manifeste de manière à ce que l'application soit lancée dans le contexte du processus com.android.phone. J'ai fait beaucoup de tests, et la seule configuration qui semble passer est la suivante, basée sur un identifiant d'utilisateur partagé android.uid.system et sur la définition du processus dans lequel l'activité ZeroSMS doit s'exécuter:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.zerosms" android:versionCode="1" android:versionName="1.0" android:sharedUserId="android.uid.system"> <uses-permission android:name="android.permission.SEND_SMS"/> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".ZeroSMS" android:label="@string/app_name" android:process="com.android.phone"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Il ne reste plus qu'à compiler et produire un APK non-signé, qui sera ensuite signé avec signapk, et installé avec adb, comme mentionné précédemment. Une fois installée, l'application permet l'envoi de SMS classe 0. J'ai publié le code de l'application ainsi qu'une version compilée et signée sur Github [1]. Je ne vous fait pas de démo, mais je vous laisse essayer l'APK final (ZeroSms-signed.apk, situé dans le dossier apks). L'application ressemble à ceci:
<center><img131><img132></center>
Quid des permissions ?
Durant mes tests (qui m'ont pris plusieurs jours) j'ai eu des difficultés à arriver à la solution décrite dans cet article, et j'ai pu notamment remarquer que le fait de passer par l'envoi de PDU contourne les restrictions (via les permissions) sur l'envoi de SMS. J'ai eu énormément d'erreurs de type "User 1001 has no permission android.permission.SEND_SMS", ce qui m'a bloqué pendant quelque temps, jusqu'à ce que je trouve cette parade (passer par la classe SMSDispatcher). Cela contourne notamment les vérifications faites par la classe IccSmsInterfaceManager sur la permission android.permission.SEND_SMS. Ceci dit, vu que l'application est une application système, cela ne remet pas en cause la sécurité d'Android (l'application étant installée de force via adb), mais offre un moyen de contourner les vérifications effectuées sur certaines permissions.
Conclusion
Je dois avouer que les manipulations à effectuer sur la version 2.1 et celles présentées ici sont franchement différentes et bien plus complexes sur les SDK récents. Cependant, une fois le SDK Android amélioré et le plugin ADT d'Eclipse patché, le développement est très intuitif et permet de jouer pleinement avec les composants système: téléphone, sms, etc ... Je suis sûr que ça peut ouvrir des voies vers d'autres utilisations !
Notez toutefois que l'application peut être signée pour les différents mods Android populaires, bien que dans mon exemple je l'ai fait pour CyanogenMod 7.2. Cette astuce doit fonctionner sur les versions récentes d'Android, toutefois ne disposant pas d'un téléphone sous ICS je n'en ai pas la certitude, si quelqu'un peut confirmer ça serait super.
MAJ - 12/10/2012
Versions compatibles (suites aux retours de lecteurs): * CyanogenMod version 7.2 * CyanogenMod version 9 * CyanogenMod version 10
References
`1] [ZeroSMS sur Github <https://github.com/virtualabs/ZeroSMS>`_
`2] [Code source Android 2.3.7 (Grepcode) <http://grepcode.com/snapshot/repository.grepcode.com/java/ext/com.google.android/android/2.3.7_r1/>`_