Mise à jour [16/02/2011] - TF1 a modifié son lecteur Flash et remplacé la méthode incriminée par une version propre, nommée "hasRedirect". La version du player analysée dans cet article était la 4.0.80, celle actuellement en place sur les sites de WAT et TF1 est désormais la 4.0.81 .
Article original du 13/02/2011:
Je me suis (encore) penché sur la sécurité des applications Flash, notamment à cause de la médiatisation du streaming et du tintouin que cela a causé vis-à-vis de l'Hadopi, en espérant trouver quelque-chose du côté de TF1 et M6 (ces deux sociétés ont misé il y a quelques années sur le streaming, avec leur service de "catch-up" TV).
Et j'ai trouvé un truc bien cocasse.
Le player flash de WAT + une méthode bizarre
La plateforme WAT est une plateforme de diffusion de vidéo, propriété de TF1 qui s'en sert aussi de socle pour la diffusion des vidéos de "catch-up", via le protocole RTMPE (pur streaming). Il n'y a donc plus de problème de fuites de vidéos au format FLV, comme j'ai pu l'aborder dans un pécédent post.
Cette plateforme s'articule autour d'un lecteur vidéo développé maison, et intégré notamment dans les sites de WAT (http://www.wat.fr) et de TF1 vidéos (http://videos.tf1.fr). C'est en plongeant les mains dans le code de ce lecteur maison que j'ai pu identifier une routine peu commune:
override public function get hasPiegeACouillon() : Boolean
*
if (isEmbedMode)
{
return PlayerDataManager.getInstance().flashVars.hasRedirect == "1";
*
return super.hasPiegeACouillon;
}
Et là je suis resté coincé entre le rire et l'étouffement. Je me suis ensuite demandé à quoi servait cette méthode particulière, car des fois on trouve ce genre de choses dans des objets Flash sans qu'elles soient réellement fonctionnelles.
Investigation
J'ai donc fouillé dans le code désassemblé, et j'ai pu identifier la référence à cette méthode:
case "BAR_CLICKED":
*
if (super.playerState.currentErrorType == PlayerState.ERROR_TYPE_RSYND)
{
PlayerUtil.openPage(PlayerContextManager.getInstance().currentContextRules.targetCouillon);
return;
*
if (PlayerContextManager.getInstance().currentContextName == PlayerContext.CONTEXT_PLAYER_PUB || PlayerContextManager.getInstance().currentContextRules.hasPiegeACouillon && !(super.playerDataManager.flashVars.playerType == "watPlayer" && super.playerState.currentMediaObject.media.visibleOnWAT != false))
*
if (PlayerContextManager.getInstance().currentContextName == PlayerContext.CONTEXT_PLAYER_PUB || super.playerDataManager.flashVars.playerType == "watPlayer" && super.playerState.currentMediaObject.media.visibleOnWAT != false || super.playerDataManager.flashVars.playerType == "tf1Player" && super.playerDataManager.flashVars.permalink != null)
{
PlayerUtil.openPage(PlayerContextManager.getInstance().currentContextRules.targetCouillon);
*
}
else if (super.playerState.currentPlayerState == PlayerState.CURRENT_PLAYER_STATE_WAITING)
*
super.playerState.currentPlayerState = PlayerState.CURRENT_PLAYER_STATE_PAUSING;
*
else
*
super.playerState.wishPlayerState = PlayerState.WISH_PLAYER_STATE_PLAY;
*
break;
}
Traduisez par: "Si un couillon clique sur la pub, alors on le redirige sur le site concernant la pub" (targetCouillon). On voit clairement que pour les développeurs, la pub est un simple piège à couillons.
Le mot de la fin
J'espère que cette bourde ne traduit pas une considération en place chez TF1, qui consiste à considérer les internautes comme des "couillons", parce que sinon ils sont bien mal partis. De plus, je pense sincèrement que les annonceurs de TF1 et de WAT apprécieront leur humour (ou pas).
<article43|flattr>
Numerama.fr, le site d'actualités publié par PressTIC, s'est associé avec Flattr afin de fournir à ses lecteurs la possibilité de rétribuer Numerama mais aussi les membres participant à la vie du site. Flattr est un service de micro-paiement communautaire innovant, lancé par l'ancien porte-parole de The Pirate Bay, Peter Sunde en avril 2010 (source Numerama).
J'avais alors créé un compte pour me rendre compte de ce que c'était, sans pour autant accrocher (on ne pouvait faire des transferts qu'avec paypal). Or depuis peu, le transfert via le paiement en ligne est disponible, et j'ai donc réactivé mon compte afin d'une part de pouvoir rétribuer Numérama (que je consulte allègrement) et d'autres trucs comme le très apprécié Babozor et sa grotte du barbu, ou mon ami Korben. Le principe du micro-paiement mis en oeuvre par Flattr est super simple: vous créditez votre compte Flattr d'un montant fixe (vos "Means"), et vous décidez d'un montant à verser par mois aux "trucs" que vous aimez, et qui sont référencés sur Flattr. Rien de plus simple pour les rétribuer, il suffit de cliquer sur le bouton Flattr apposé sur le site de Korben par exemple ou sur celui de la grotte du barbu, et Flattr se charge de calculer la part reversée à chacun des trucs que vous avez apprécié (et "Flattré").
J'en ai donc profité pour ajouter un bouton Flattr à ce site, afin de voir si ca peut au moins rembourser l'hébergement à l'année (en supposant que vous trouviez ce site un tant soit peu utile). Pour les amateurs de iJaw (l'extension Firefox pour downloader les zics de jiwa -- PoC) ou ceux qui aiment DShark (mon projet de sniffer sur Nintendo DS), vous pouvez supporter le développement en "Flattrant" ;). Rien d'obligatoire, surtout en cette période de crise économique.
Cette initiative est tout de même à saluer, car elle est désormais abordable (bien que le site de flattr soit toujours en anglais), et peut vous permettre de vous faire rétribuer et de rétribuer les initiatives ou les personnes que vous jugez méritantes. Numerara espère aussi via ce moyen récupérer un brin de financement, et favoriser les articles reflétant une recherche autant dans le fond que dans la forme plutôt que de la reprise de dépêche pure et simple.
<article42|flattr>
Je suis tombé il y a quelques mois sur un supayre tool de RE dédié à Android. Ayant moi-même développé une ou deux applis pour ce système d'exploitation, je me suis intéressé aux possibilités offertes par cet outil grandiose qu'est Smali. Et je dois avouer que c'est bluffant, à la limite du désassemblage/réassemblage que permet IDA.
La plate-forme Android est de plus en plus en vogue, certainement grâce à son ouverture et aux nombreux systèmes l'utilisant, mais il faut avouer que l'Android Market offre quand même plein d'applications, dont les plus intéressantes sont en version payante. Ceci dit, les personnes ayant migré sur Android l'ont choisi en partie pour le côté ouvert, qui en l'occurence est souvent synonyme de gratuit, et la conversion des utilisateurs gratuit en clients du market est donc difficile à réaliser. C'est pourquoi des versions "Lite" ou "Trial" sont disponibles sur l'Android Market, avec bien sûr des fonctionnalités limitées mais complètement implémentées. Et là se trouve un excellent terrain de jeu pour les apprentis reversers: nous avons à disposition un bon nombre de programmes téléchargeables directement du Market, et nous sommes en mesure de les customiser, voire même de modifier le code du programme, et ce beaucoup plus facilement que sur des plateformes x86.
Android et Dalvik
Le système d'exploitation Android est basé sur Dalvik, qui est a priori une version custom de VM basée sur Java, et développée spécialement pour Android.
“Every Android application runs in its own process, with its own instance of the Dalvik virtual machine. Dalvik has been written so that a device can run multiple VMs efficiently. The Dalvik VM executes files in the Dalvik Executable (.dex) format which is optimized for minimal memory footprint. The VM is register-based, and runs classes compiled by a Java language compiler that have been transformed into the .dex format by the included "dx" tool.
The Dalvik VM relies on the Linux kernel for underlying functionality such as threading and low-level memory management.”
Chaque application tourne dans une VM Dalvik, et a accès aux composants systèmes. Dalvik n'est pas Java, et on ne reverse pas du bytecode Dalvik de la même façon.
Smali & Baksmali
Jesus Freke a pour ce faire développé deux outils (en Java), nommés baksmali et smali [JFSMALI]. Le premier est un désassembleur dédié à Dalvik, tandis que le second n'est rien de moins ... qu'un assembleur ! Grosso modo, nous avons les clefs en main pour désassembler des APK (format de paquetage pour Android, en réalité de simples JARs signés renommés), les triturer de toute part, et les réassembler, afin de reconstruire des APKs patchés, customisés, adaptés à nos besoins.
Le problème principal de ces outils, c'est la syntaxe du code produit/utilisé: des instructions spécifiques, orientées objet (un peu du style de celles utilisées pour du .Net), bien différentes de l'assembleur que nous connaissons, notre bon vieux Intel x86. Alors oui, il faut faire quelques efforts pour lire ce type de code, mais sur le principe (analyse, identification de protections) ça ne change pas grand chose, les structures de contrôle étant quasi les mêmes. Un exemple de code produit par Smali:
.method public DialogID()I
.registers 2
.prologue
.line 126
const/4 v0, 0x0
return v0
.end method
Bon, on a quand même [DALOPCODE] sur le net pour comprendre les opcodes, mais c'est loin de valoir les manuels Intel...
Utilisation de Smali & Baksmali
Un bon document existe en français [SMARTFR], explicitant l'utilisation de cet outil, je ne vais pas m'étendre sur le sujet. J'ai pris comme sujet d'étude le programme BeyondPod, en version Trial, histoire de jauger les possibilités de cet outil.
L'application peut être récupérée directement via le market, et récupérée grâce au SDK d'Android [ANDSDK]. Les commandes suivantes, exécutées dans le dossier tools du SDK, permettent de récupérer l'APK installé sur le mobile, et de le décompresser dans un dossier dédié.
$ ./adb pull /data/app/mobi.beyondpod.apk $ mkdir ~/beyondpod && mv mobi.beyondpod.apk ~/beyondpod/ $ cd ~ $ mkdir ~/beyondpod/apk $ unzip mobi.beyondpod.apk -d ~/beyondpod/apk/ $ rm ~/beyondpod/apk/META-INF -rf
La dernière ligne de commande supprime les informations de signature, afin que nous puissions reconstruire l'APK avec une clef que l'on aura générée. Mais laissons cela pour plus tard, voyons voir ce que peut produire Smali comme sortie. Pour cela, on prend soin de copier les JARs de baksmali et smali dans le dossier créé, et on lance la commande suivante:
$ cd ~/beyondpod/ $ java -Xmx512m -jar baksmali-1.2.4.jar mobi.beyondpod.apk -o disass
Smali va désassembler le paquetage, et créer des fichiers de code source ayant l'extension .smali. Ces fichiers sources peuvent être modifiés, et correspondent au classes et code associés stockés dans le fichier classes.dex stocké dans le package APK. Ciblons le code gérant la licence d'utilisation, cela est loin d'être facilement compréhensible, mais néanmoins nous pouvons identifier certaines routines (méthodes) par leur nom, telle IsUnlocked():
.method public static IsUnlocked()Z
.registers 1
.prologue
.line 84
invoke-static {}, Lmobi/beyondpod/rsscore/rss/LicenseManager;->IsKeyInstalled()Z
move-result v0
if-nez v0, :cond_e
invoke-static {}, Lmobi/beyondpod/rsscore/rss/LicenseManager;->PokeKey()Z
move-result v0
if-nez v0, :cond_e
const/4 v0, 0x0
:goto_d
return v0
:cond_e
const/4 v0, 0x1
goto :goto_d
.end method
Ce qui pourrait se traduire par un code C++ comme celui-ci:
int LicenseManager::IsUnlocked()
*
if (!LicenseManager::IsKeyInstalled())
if (!LicenseManager::PokeKey())
return 0;
return 1;
*
Il est alors aisé de modifier le code smali pour altérer le comportement de cette routine, en la résumant à sa plus simple expression:
.method public static IsUnlocked()Z
.registers 1
.prologue
const/4 v0, 0x1
return v0
.end method
De cette manière, la fonction renverra toujours vrai. Ici, plus de problème d'alignement, de NOPs, ou même d'agrandissement de section, car on travaille directement sur un code source, et aussi parce que l'on dispose d'un outil permettant de le recompiler. En analysant le code source smali de cette application, j'ai pu aisément reconstruire les méthodes de la classe LicenseManager:
#define LICENSE_TYPE_IN_TRIAL 0x00
#define LICENSE_TYPE_ANDROID_MARKET 0x1
#define LICENSE_TYPE_BETA 0x2
#define LICENSE_TYPE_PAYPAL 0x3
#define LICENSE_TYPE_OTHER 0x4
#define LICENSE_TYPE_EXPIRED 0x5
? LicenseManager::GetLicenseKey()
{
v7 = "\\|";
v4 = LicenseManager->ProVersionLicence;
v5 = LicenseManager->_CheckKeyUri;
v2 = LicenseManager$License->GetLicenseKey(v4, v5);
if (v2 != 0)
{
v4 = "\\|";
v4 = v2.split(v7);
return v4;
}
else*
try {
v4 = BeyondPodApplication::getInstance();
v0 = v4->getContentResolver(v4);
if (v0!=null)
{
v3 = v0->getType(v4);
if (v3 != null)
{
v4 = "\\|";
v3.split(v4);
return v4;
}
}
}
catch(Exception e)
{
move-exception v4; /* Dur à traduire en C++ =) */
v1 = v4;
v4 = "LicenseManager";
v5 = "Failed to get License Key";
CoreHelper::WriteTraceEntry(v5);
}
}
v4 = v6;
return v4;
}
int LicenseManager::IsKeyInstalled()
{
try {
v3 = BeyondPodApplication::GetInstance();
v1 = BeyondPodApplication::getPackageManager();
if(PackageManager->checkSignatures(v1, "mobi.beyondpod", "mobi.beyondpod.unlockkey")
return 1;
else
return 0;
}
catch ()
{
}
return 0;
}
int LicenseManager::PokeKey()
{
v3 = 0;
v0 = LicenseManager::GetLicenseKey();
if (v0 != 0)
{
v1 = v0.length();
v2 = 0x2;
if (v1==v2)
{
v1 = LicenseManager->_Key;
v2 = v0;
if (v1 <equals(v2))
return 1;
}
}
return 0;
}
int LicenseManager::IsUnlocked()
{
if (IsKeyInstalled())
return 1;
if (LicenseManager::PokeKey())
return 1;
return 0;
}
int LicenseManager::CheckAndNotifyIfInRestrictedMode()
{
if (IfInRestrictedMode())
{
/* affiche un message Toast */
return 1;
}
return 0;
}
int LicenseManager::CurrentLicenseKind()
{
if (IsUnlocked())
{
v0 = LicenseManager::GetLicenseTypeString(0);
if (StringUtils::IsNullOrEmpty(v0))
{
v1 = LICENSE_TYPE_UNKNOWN;
return v1;
}
else if (v0.equals("Beta Tester"))
{
v1 = LICENSE_TYPE_BETA;
return v1;
}
else if ( v0.equals("Android Market"))
{
v1 = LICENSE_TYPE_ANDROID_MARKET;
return v1;
}
else if (v0.equals("PayPal"))
{
v1 = LICENSE_TYPE_PAYPAL;
return v1;
}
else if (IfInRestrictedMode())
{
v1 = "";
Configuration::setLicenseTypeString(v1);
v1 = LicenseManager->LICENSE_TYPE_EXPIRED;
return v1;
}
}
else
{
v1 = new Date();
v2 = LicenseManager->TrialExpirationDate;
v1 = v1.compareTo(v2);
if (v1<=0)
{
v1 = LICENSE_TYPE_TRIAL;
return v1;
}
else
{
v1 = LICENSE_TYPE_OTHER;
return v1;
}
}
}
Le contournement de cette protection est dès lors trivial: Il suffit de patcher quelques routines pour retourner les bonnes valeurs, en l'occurence:
.method public static CurrentLicenseKind()I
.registers 3
.prologue
const/4 v1, 0x1
return v1
.end method
.method public static IfInRestrictedMode()Z
.registers 3
.prologue
const/4 v2, 0x0
return v2
.end method
.method public static IsUnlocked()Z
.registers 1
.prologue
const/4 v0, 0x1
return v0
.end method
De cette manière, le programme va croire que la licence est valide, et l'on peut observer que le menu "Purchase Unlock Key" a disparu.
Mais il reste toutefois dans le About une mention au sujet d'une date d'expiration "Expires ...". Il s'agit ici d'un problème purement graphique, la boîte de dialogue appelant simplement la méthode GetLicenseKey de la classe LicenseManager.
Voici le code smali incriminé:
.line 94
invoke-static {}, Lmobi/beyondpod/rsscore/rss/LicenseManager;->GetLicenseKey()[Ljava/lang/String;
move-result-object v4
.line 95
.local v4, license:[Ljava/lang/String;
if-eqz v4, :cond_b2
array-length v8, v4
if-le v8, v10, :cond_b2
.line 96
new-instance v8, Ljava/lang/StringBuilder;
const-string v9, "Licensed to: "
invoke-direct *v8, v9*, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
[...]
.line 98
:cond_b2
new-instance v8, Ljava/lang/StringBuilder;
const-string v9, "Expires "
invoke-direct *v8, v9*, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-static {}, Lmobi/beyondpod/rsscore/rss/LicenseManager;->TrialExpirationDate()Ljava/util/Date;
move-result-object v9
const-string v10, "MMM d yyyy"
invoke-static *v9, v10*, Lorg/apache/http/impl/cookie/DateUtils;->formatDate(Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;
move-result-object v9
invoke-virtual *v8, v9*, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v8
invoke-virtual *v8*, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v8
invoke-virtual *v6, v8*, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V
Nous pouvons donc le patcher correctement, voire même l'améliorer afin d'afficher le texte "Licensed To" suivi d'un texte customisé:
.line 94
invoke-static {}, Lmobi/beyondpod/rsscore/rss/LicenseManager;->GetLicenseKey()[Ljava/lang/String;
move-result-object v4
.line 95
.local v4, license:[Ljava/lang/String;
.line 96
new-instance v8, Ljava/lang/StringBuilder;
const-string v9, "Licensed to: "
invoke-direct *v8, v9*, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
const-string v9, "Virtualabs.fr =)"
invoke-virtual *v8, v9*, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v8
invoke-virtual *v8*, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v8
invoke-virtual *v6, v8*, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V
De cette manière, la boite de dialogue affichera "Licensed To: Virtualabs.fr =)", au lien de "Android Market" (texte qui s'affiche normalement quand vous enregistrez l'application via le market).
Comme vous pouvez le remarquer, il est vraiment aisé de modifier le fonctionnement du programme, d'ajouter une chaîne de caractères, et de la concaténer à une chaîne existante.
Compilation
Nous avons vu comment analyser le code source smali, et comment le modifier de manière à changer le comportement d'un programme. Il est donc temps de "compiler" notre code source modifié de manière à intégrer nos modifications. Pour cela, nous utilisons smali:
$ java -Xmx512m -jar smali-1.2.4.jar ~/beyondpod/disass/ $ cp ~/beyondpod/out.dex ~/beyondpod/apk/classes.dex $ cd ~/beyondpod/apk/ $ zip mobi.beyondpod_patched.apk -r ./* $ jarsigner -keystore virtualabs.keystore -alias virtualabs mobi.beyondpod_patched.apk
Ces quelques commandes patchent le fichier classes.dex avec le DEX recréé par Smali, puis reconstruisent le package APK, et enfin le signent ]avec une clef du keystore Java (cf. [APKSIGN]). De là, on peut aisément déployer l'application sur un téléphone connecté:
$ ./adb push ~/beyondpod/apk/mobi.beyondpod_patched.apk /sdcard/
Il ne reste plus qu'à l'installer (via un FileManager), et utiliser la version patchée, qui n'est plus limitée dans le temps, et cela sans licence régulière.
Mots de la fin
Smali et Baksmali sont des outils très puissants, qui permettent de manipuler en profondeur les APKs utilisés par Google. Non seulement le cracking simple est possible, mais la customisation/correction de bug est tout à fait accessible, et ne nécessite pas de grosse bidouille. Certes, il faut se familiariser avec le code assembleur produit par smali (code assembleur relatif à dalvik), mais les possibilités sont énormes. Les plate-formes embarquées sont assez souvent oubliées des reversers, et ces deux outils pourraient peut-être ouvrir certains horizons aux reversers fous (n'est-ce pas Baboon, tu pourrais tronçonner de l'APK :), pour ma part j'avoue que je me suis pas mal amusé sur ce petit exemple, bien que l'aboutissant soit relativement simple.
Bibliographie