Mi Remote icone

Les codes infrarouges des appareils télécommandés vendus dans le monde constituent-ils un secret industriel ? L'application Mi Remote de Xiaomi semble indiquer que oui étant donné les efforts mis en œuvre pour protéger leur base de données.

À la recherche d'une liste complète de codes pour l'application TVKILL (une version Android du TV-B-Gone), je me suis penché sur Mi Remote qui propose de transformer son téléphone en télécommande universelle.

L'effort est louable (et sans publicité, ça vaut le coup de le mentionner !) mais les données utilisées sont curieusement inaccessibles. Terminons donc le travail.

Le monde a besoin de personnes capables de faire de la rétro-ingénierie. Il s'agit ici de codes infrarouges comme il pourrait s'agir d'une application bancaire trop invasive (Crédit Agricole <3), ou d'une application vérolée imposée aux étudiants français par les CROUS (Izly), ou encore de malwares.

Les exemples sont légion et peu importe leurs origines car : Si un utilisateur ne peut bénéficier des libertés fondamentales inhérentes aux logiciels libres, alors quelqu'un d'autre a le contrôle de son logiciel. Tôt ou tard la personne qui contrôle le logiciel sera en désaccord avec celle qui l'utilise ou abusera de son pouvoir. Ce jour-là l'utilisateur comprendra les conséquences du fait de "nager avec les requins".

Après tout il faut bien se nourrir mais comprenez qu'en tant que développeur, si vous participez à des projets verrouillés, alors vous faites partie du problème. Un problème d'éthique et parfois aussi de sécurité.

Je ne puis que conseiller cet article de Richard Stallman : The Right to Read (1997).

À l'heure où cet article est posté, la majorité de la base de données est publiée, ainsi que le code pour la reproduire et l'exploiter. Ceci permettra je l'espère la création ou l'amélioration d'alternatives libres.

Voir le dépôt GitHub.

Sommaire

Avant-propos

Je ne reviens pas ici sur les méthodes de désassemblage/décompilation des APK déjà abordées dans d'autres articles de ce site (Voir Exemple de crack et suppression des pubs d'une application Android).

Désassemblage vs décompilation

Le désassemblage convertit le bytecode Dalvik en Smali, un langage de bas niveau néanmoins lisible.

La décompilation convertit ou essaie de convertir le bytecode Dalvik en Java, le langage de haut niveau utilisé pour développer l'application. Il s'agit d'une tentative de restauration du code source.

Afin d'éviter les prises de tête, gardez à l'esprit que vous lirez le Java mais modifierez le Smali avant recompilation.

Quelques conseils pour la réalisation d'un crack ou la rétro-ingénierie en général

Imprégniez-vous de l'application cible, testez les fonctionnalités que vous voulez modifier, regardez les fenêtres avec un regard de développeur. Devinez ce qui se passe derrière l'interface. Mémorisez les textes, messages de notifications ou d'erreur.

Une fonctionnalité a l'air d'interroger un serveur ? Coupez le réseau et observez le message d'erreur. Activez le debuggage USB pour lire le logcat du téléphone, les développeurs laissent toujours des routines de debuggage dans leur code. Certains messages seront forcément utiles.

Ce sont autant de mots et d'indices que vous pourrez rechercher dans le code et qui vous aideront à identifier le rôle des routines.

Enfin, si possible, travaillez dans une VM Android si vous testez du code potentiellement dangereux...

Verrous rencontrés dans ce projet

  • Chiffrement des échanges HTTPS.

  • Offuscation du code par l'éditeur de l'application : Il s'agit de la technique la plus ennuyante ; les packages, fonctions, variables sont renommés avec 1 lettre plus quelques chiffres additionnels.

  • Chiffrement des données obtenues depuis une API REST.

  • Usage de code natif pour brouiller les pistes : Potentiellement un des plus ennuyant, sauf si des artefacts de debugging sont oubliés et que le code appelle en fait des fonctions Java...

  • Protection anti-rejeu de l'API REST.

Logiciels utilisés

Outils et liensRemarques
MitmproxyDétournement du trafic HTTPS
jadx-guiExploration du code décompilé, coloration syntaxique, recherche des usages et déclarations.
Features overview


Des logiciels peuvent être utilisés pour décompiler le bytecode Dalvik et permettre de tracer les usages des fonctions, variables etc. jadx-gui sera utilisé car il permet aussi d'explorer le code Smali tout en renommant en un clic les items dans l'ensemble du projet. Mieux que IDA pour le coup, qui ne se base que sur le code Smali ; le renommage des variables y est complexe car en tant que registres elles sont réutilisées plusieurs fois dans chaque méthode et contiennent à tour de rôle des données radicalement différentes.

Il ne manque à jadx-gui que le graphe d'appel des fonctions de IDA (bien que pas très convaincant sur le Java a priori ?), et la possibilité d'ajouter des commentaires dans le code. Il s'agit néanmoins d'un excellent logiciel.

Vous pouvez aussi tester jd-gui qui fonctionne sur le même principe mais son développement est moins actif. Pensez aussi à Ghidra (NSA) en alternative libre à IDA.

Voici sur stackoverflow, une liste de décompilateurs/déoffuscateurs récente (08/2020). Citant notamment CFR (support de Java 9, 12 et 14), et FernFlower.

Objectifs

Captures d'écran des fonctionnalités essentielles de l'application :

Captures d'écran Mi Remote

Que cherche-t-on ?

  • Comment les codes infrarouges sont récupérés ?
  • Sous quelle forme sont-ils échangés ?
  • Quand sont-ils envoyés ?
  • Quels traitements subissent t'ils avant leur envoi ?

Que sait-on ?

  • Les codes infrarouge sont téléchargés (l'app affiche une erreur en l'absence de réseau).
  • L'application va devoir utiliser l'API d'Android pour émettre les codes IR : android.hardware.ConsumerIrManager ; c'est une chaine de caractères que l'on peut rechercher dans le projet.

Contournement HTTPS - Mitmproxy/addSecurityException

L'APK est obtenue sur Apkpure.

On utilisera le projet Mitmproxy (écrit en Python) afin de s'interposer entre l'application et son serveur (attaque Man In The Middle).

Depuis Android 7.0 il faut intégrer une exception de sécurité pour contraindre une application à accepter un certificat SSL personnalisé.

Je vous invite à suivre l'excellent tuto à ce sujet sur borntocode.fr : Mitmproxy - Analyser le trafic de vos applications mobiles.

Voici le résultat obtenu :

Requêtes capturées

PS : Pour sauvegarder une requête placez-vous dessus, puis appuyez sur 'e' pour spécifier un fichier d'export.

On identifie clairement les requêtes faites ainsi que de nombreux envois vers une API de traçage comportemental (pour simplifier, une requête est faite pour chaque appui sur un bouton de l'application ; étonné ? Toutes les applications font cela aujourd'hui.).

Fragment de réponse au format JSON :

/controller/match/tree/1?version=6034&country=FR&ts=1615096902836&nonce=-1546760834&devid=1&miyk=1&brandid=64&power=1&opaque=c17fd4ce8b4b2d070786448a25344636326efbbc

"ir_zip_key": QJPmll3+SCgpSE73bTO9hni9upbSpKrS73cugR4FZSMT2VGtMTkEIsegm1kjFy3bCLQJsJZKAXxjDF7hGaYIolNzR+qo5f2H3C\/PqsSK2Q8kaQaJAycytxhqhVgnwnOUZ6gj0xXscdkPK3MBzr6HH5yEOGDtocCXKP8qEXZdvctnCmFZaZwubXf1Cscf\/rlVkAz53JacxfUkCiDqw8M27g==
"keysetids":[
    "xm_1_199",   => id interne de la télécommande en bdd
    "xm_1_2425"
],
"children_index":[7,8],
"frequency":37990, => frequence
"level":1,
"parent_index":0,
"index":6,         => position dans la liste des codes proposés
"keyid":"power"    => type de code correspondant à ir_zip_key

ir_zip_key est vraisemblablement encodé en base64 mais son décodage n'est pas plus informatif :

>>> list(base64.b64decode(st))
[64,147,230,150,93,254,72,40,41,72,78,247,109,51,189,134,120,189,186,150,210,164,170,210,239,119,46,129,30,5,101,35,19,217,81,173,49,57,4,34,199,160,155,89,35,23,45,219,8,180,9,176,150,74,1,124,99,12,94,225,25,166,8,162,83,115,71,234,168,229,253,135,220,47,207,170,196,138,217,15,36,105,6,137,3,39,50,183,24,106,133,88,39,194,115,148,103,168,35,211,21,236,113,217,15,43,115,1,206,190,135,31,156,132,56,96,237,161,192,151,40,255,42,17,118,93,189,203,103,10,97,89,105,156,46,109,119,245,10,199,31,254,185,85,144,12,249,220,150,156,197,245,36,10,32,234,195,195,54,238]

Il n'y a pas de motif répété dans cette liste, c'est pourtant ce que l'on observe dans le même code non chiffré récupéré dans une base de données publique (irdb.tk) :

347,173,22,22,22,22,22,22,22,22,22,22,22,22,22,65,22,22,22,65,22,65,22,65,22,65,22,65,22,65,22,22,22,65,22,22,22,65,22,22,22,22,22,65,22,22,22,22,22,22,22,65,22,22,22,65,22,65,22,22,22,65,22,65,22,65,22,4892

Les valeurs a priori aléatoires traduisent une entropie élevée, donc potentiellement du code chiffré.

À la recherche du parser JSON

Note

À partir de maintenant j'utilise massivement la recherche récursive de chaines dans le dossier de l'APK décompilée. Mettre ce genre de fonction dans votre fichier .bashrc vous fera gagner un temps monstrueux.

function search() { echo "Recherche de $1 ..."; grep --color=auto -in -R "$1" ./*; }

Nous savons que la clé ir_zip_key est forcément présente dans le code de l'application lors de l'interprétation des données ; on trouve effectivement la fonction responsable :

// ./com/xiaomi/mitv/phone/remotecontroller/ir/dk/model/DKDataParser.java
getMatchTree(..)
    // Accepte un int + JSONObject
    // Retourne un objet de type c.k.i.b.b.b1.p.f (binding objet sur un fabricant de matériel ?) initialisé par les données
    [...]

    if (jSONObject4.has("ir_zip_key")) {
        // Bidouille ir_zip_key avec frequency => Type de retour ??? Probablement un binding objet sur un pattern
        // à quoi sert str ? a priori pas utilisé, initialisé à null ??
        c.k.i.b.b.b1.p.p.e eVar2 = new c.k.i.b.b.b1.p.p.e(
            102,                                                // int encrypt
            new a(str, jSONObject4.getString("ir_zip_key")),    // d   code_ir
            jSONObject4.getInt("frequency")                     // int frequency
        );
        if (jSONObject4.has("ir_zip_key_r")) {       // reverse IR code
            eVar2.a(new a(null, jSONObject4.getString("ir_zip_key_r")));
        }
        if (jSONObject4.has("keyid")) {              // Nom de la commande
            eVar2.a(jSONObject4.getString("keyid")); // set_keyid
        }
        ArrayList arrayList2 = new ArrayList();
        arrayList2.add(eVar2);
        eVar.a(arrayList2);
        eVar.c(0);
        gVar.a(eVar);
    }
    [...]
}

Au premier coup d'œil on remarque que les packages, classes, fonctions et variables sont renommés. Le code est offusqué pour ralentir la rétro-ingénierie en réduisant sa lisibilité. C'est une manœuvre délibérée pour nuire à qui viendrait y mettre son nez.

Certaines fonctions restent néanmoins en clair comme getMatchTree, idem pour les slots/callbacks attachés aux signaux (onFailed, onClick, onClickListener, setPressInterval, etc.), tout comme certains packages utilisés par l'application. On voit que tout n'est pas développé spécifiquement pour ce projet, que tous les packages ne sont pas conçus par les mêmes personnes et que par conséquent l'offuscation est inégale.

L'offuscation est une plaie car tout ce qui guide un développeur se ressemble. Vous ne saurez plus où vous en êtes quand vous aurez parcouru 3 fichiers sans commentaires contenant tous une dizaine de variables et fonctions nommées "a" ou "b".

Notez que de nombreux "développeurs" utilisent déjà "ces astuces" sans le savoir et par incompétence... Si vous travaillez dans un labo de recherche ou en SSII ce genre de code vous paraîtra normal. Spoiler : ça ne l'est pas.

Vous pouvez annoter les fichiers .java décompilés comme je l'ai fait dans l'exemple, mais à terme vous allez devoir utiliser jadx-gui pour gérer le projet dans sa globalité. L'APK décompilée éparpille son code sur des centaines de fichiers, n'espérez pas appréhender cela dans un simple éditeur de texte.

Quand vous identifiez clairement une fonction/variable, n'hésitez pas à la renommer dans le logiciel. Cet outil formidable est fait pour ça ! J'y reviendrai dans la seconde partie de cet article.

La transmission du code infrarouge

getMatchTree() est appelée depuis un objet MatchIRActivityV52 ; son résultat est placé dans un attribut de cette classe.

@Override // c.k.i.b.b.y0.g.w
public void a(JSONObject jSONObject) {
    [...]
    matchIRActivityV52.C = DKDataParser.getMatchTree(this.a, jSONObject);
    [...]
}

On sait que l'interface propose quelques boutons : gauche, droite, envoyer le code, oui, non.

IMAGE

L'activité (fenêtre Android) MatchIRActivityV52 gère les interactions avec l'utilisateur et décide ultimement de l'envoi du code. On identifie assez clairement les routines activées lors d'un clic sur ces boutons :

LongPressButtonWidget J;
LongPressButtonWidget K;
LongPressButtonWidget L;

Traçons leurs usages :

this.L.setLongPressButtonListener(this.j0);
this.J = (LongPressButtonWidget) findViewById(2131296401);
this.K = (LongPressButtonWidget) findViewById(p.i.arrow_right);

Seul L semble attaché à un callback, il est donc probable qu'il soit le bouton d'envoi du code IR.

Un peu plus bas :

private void a(int i) {
    c.a.a.a.a.b("changeState: ", i);
    [...]
    try {
        this.L.setPressInterval(800);
        String d2 = this.C.g().a().d();
        // Test fonction du code à envoyer
        if (d2.equals("power") || d2.equals("off") || d2.equals("on") || d2.equals("poweroff") || d2.equals("poweron")) {
            this.L.setPressInterval(2000); // Désactive le bouton pendant l'envoi du code
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    s();    // Demande d'envoi
    [...]
}

Les chaines sont assez explicites : le programme teste si le code parsé en interne a un rôle acceptable pour le cas d'usage (envoi d'un code d'extinction/allumage); puis appelle la fonction s() qui à son tour appelle la fonction suivante :

public void m() {
    e eVar = this.j;  // notre fameux objet e wrapper JSON...
    if (eVar == null) {
        v.f(o, "sendIr bur mCurMatchData null");
        return;
    }
    try {
        {{ [...]
            } else {
                // Passe l'objet contenant les codes et les envoie
                // Appelle finalement a(eVar, true, false) (send_IR())
                p0.g().a( // send_IR()
                    this.j.a() // c.k.i.b.b.b1.p.e.a(): objet e courant sur l'ui
                );
                return;
            }
        }
        // Passe l'objet contenant les codes et les envoie
        p0.g().a( // send_IR()
            this.j.a(), // c.k.i.b.b.b1.p.e.a(): objet e courant sur l'ui
            true,
            true
        );
    } [...]
}

./c/k/i/b/b/b1/g.java est la classe la plus importante à ce niveau et contient notamment :

  • Une méthode utilisant l'API android.hardware.ConsumerIrManager; Spoiler : Cette fonction n'est jamais appelée :p

    public void a(int i, int[] iArr) {
        if (this.a.hasIrEmitter()) {
            this.a.transmit(i, iArr);
        }
    }
    
  • La méthode a() que j'appelle send_IR(), citée dans le code du paragraphe précédent :

    public void a(e eVar, boolean z, boolean z2) {
        // eVar (structure de données parsée depuis le JSON), bool_vibro, ?? (booléen non utilisé)
        String str;
        d dVar;
        c cVar;
        if (eVar != null) {
            StringBuilder b2 = a.b("sendIR:");
            b2.append(eVar.d());            // get_keyid
            Log.e(v0.a, b2.toString());     // log du rôle du code IR traité "miauto  : sendIR:power"
                                            // à ce moment le code est encore chiffré
            if (z) {
                i0.a().a(v0.a());           // Déclenche vibreur téléphone
            }
    
            if (this.bool_positive_code) {  // Test bool_positive_code
                dVar = eVar.b();            // objet de type c.k.i.b.b.b1.p.p.d
                str = "发正码";              // Envoyer un code positif
            } else {
                dVar = eVar.e();            // objet de type c.k.i.b.b.b1.p.p.d
                if (dVar != null) {
                    str = "发反码";          // Retourne inverted code si présent;
                } else {
                                            // Sinon envoi le code positif
                    dVar = eVar.b();        // objet de type c.k.i.b.b.b1.p.p.d
                    str = "没有反码,发正码"; // Pas de code inverse, envoyer un code positif
                }
            }
            a(str);                         // Fonction de debugging vidée en production
            this.bool_positive_code = !this.bool_positive_code;
            // à la base dVar est de type c.k.i.b.b.b1.p.p.d
            if (dVar instanceof f) {
                // c.k.i.b.b.b1.p.p.f
                // String pulse wrapper: class f extends c
                // public f(String str, String str2) {
                //    super(1001, str, str2); // valeur mystère = 1001
                //}
                cVar = (f) dVar; // cast dvar => f
                if (eVar.a() <= 0) {
                    return;                 // Quitte si frequency négative
                }
            } else if (dVar instanceof c.k.i.b.b.b1.p.p.a) {
                cVar = (c.k.i.b.b.b1.p.p.a) dVar;  // cast dvar => a
            } else if (dVar instanceof b) { // c.k.i.b.b.b1.p.p.b: classe capable de compresser le tableau int[]
                // send_async_clear_pulses
                a(
                    eVar.a(),               // a() = get_frequency
                    ((b) dVar).b(),         // iArr_clear_pulses: int[] c.k.i.b.b.b1.p.p.b.b(): b() = get_int_array()
                    true,                   // bool_dont_process_pulses
                    false                   // bool_vibro
                );
                return;
            } else {
                return;
            }
            // send_async_string_pulses
            // types dvar: f ou a
            c(
                eVar.a(),                   // a() = get_frequency
                cVar.c(),                   // null
                cVar.b()                    // code IR chiffré (string téléchargé)
            );
        }
    }
    

Elle contient une chaine explicite sendIR ainsi que du debuggage en mandarin qui n'apparait pas lors de l'exécution, mais révèle la nationalité de ses auteurs.

Les mentions d'un code "inversé" font allusion à un 2ième code IR (ir_zip_key_r) présent pour certains modèles d'appareils.


Plus important, on découvre que le programme peut traiter 2 types de codes via 2 méthodes à la fin de send_IR() :

  • a()/send_async_clear_pulses() : un non chiffré prêt à l'emploi sous forme de tableau d'entiers (int[]),
  • c()/send_async_string_pulses() : un 2ième code sous forme de String.

L'essentiel est de savoir si notre code chiffré a été déchiffré avant cette fonction ou s'il l'est après.


Les logiciels proposant des diagrammes d'appels comme IDA sont utiles lors de cette étape de recherche. Toutefois IDA se repose sur le désassemblage en Smali, peu lisible. Dans le cas présent je préfère lire directement le Java décompilé dans jadx-gui.

IDA call graph

Ultimement les 2 fonctions suivantes seront appelées dans un thread séparé, selon le type de code (remarquer la surcharge de méthodes) :

public void a(int i, int[] iArr) {              // Tableau d'entiers
    // Depuis send_async_clear_pulses()
    if (this.a.hasIrEmitter()) {
        this.a.transmit(i, iArr); // API Android
    }
}

/* renamed from: b */
public void a(int i, String str, String str2) { // Strings
    // Depuis send_async_string_pulses()
    Miir.b(v0.a(), i, str, str2);
}

Dans les 2 cas il y a un travail supplémentaire à faire :

  • Soit le code est clair et on a raté son déchiffrement
  • Soit le code est encore chiffré et on va devoir trouver les routines de déchiffrement dans les méandres de l'objet Miir...

Analyse des logs Android

Il est plus que temps d'analyser les logs via le logcat d'Android (activons le debuggage USB dans les options développeur du téléphone), puis entrons cette commande dans un terminal pour récupérer tous les logs du système (adb est fournit dans le sdk) :

./adb logcat > android.log

L'activation du bouton "Désactiver" de l'application génère ce texte dans les logs :

E IRLongPressWidget: onSingleTapUp
E miauto  : sendIR:power
D miir    : ==============transmitIR================
D miir    : decrypt
D miir    : uncompress
D miir    : uncompress over
D miir    : jsonStringToInts
D miir    : transmit

Ce debugging indique que :

  • le code est déchiffré avant chaque envoi, après l'appel de send_IR() (cf. la position de la chaine "sendIR:power")
  • la procédure est : déchiffrement/décompression/cast en int[]/émission. L'étape "uncompress over" est une faute d'anglais pour "uncompress done" : il n'y a pas de sur-décompression comme ça le laisse penser...

Miir : Moins offusqué qu'il n'y parait

Allons voir la méthode Miir.b() dans le fichier ./com/xiaomi/mitv/phone/remotecontroller/ir/Miir.java :

public static int b(Context context, int i, String str, String str2) {
    if (str2 == null) {
        return 1;           // Coucou, je suis un code d'erreur jamais testé <3
    }
    a.d("encode: ", str2);  // Debug masqué en release
    try {
        return a().transmitIR(context, i, str, str2); // Envoi IR
    } catch (UnsatisfiedLinkError e) {
        e.printStackTrace();
        return 1;           // Coucou, je suis un code d'erreur jamais testé <3
    } catch (Exception e2) {
        e2.printStackTrace();
        return 1;           // Coucou, je suis un code d'erreur jamais testé <3
    }
}
public native int[] getIRCode(Context context, int i, String str, String str2);
public native String getIRContent(Context context, byte[] bArr);
public native int transmitIR(Context context, int i, String str, String str2);

Le mot-clé "native" indique que transmitIR() ainsi que ces amies toutes aussi intéressantes getIRCode() et getIRContent(), sont implémentées et compilées en code natif dans un langage bas niveau (C), puis importées via la JNI (Java Native Interface).

Note

À ce stade vous auriez pu/dû vous précipiter sur une recherche de strings caractéristiques tels que "==============transmitIR================" sur l'ensemble du projet. Vous auriez vu que du code natif est appelé puisque ces chaines sont localisées dans une librairie :

$ search "==============transmitIR================"
Fichier binaire ./lib/arm64-v8a/libmiir.so correspondant
Fichier binaire ./lib/armeabi-v7a/libmiir.so correspondant

Je détaille la procédure complète car c'est celle que j'ai suivie, mais n'oubliez pas que le but n'est pas de comprendre la moindre ligne de code de ce projet. Il faut aller à l'essentiel c.-à-d. découvrir les routines de déchiffrement. Toutefois, personnellement il n'est pas certain qu'une fois le nez devant, je les aurais comprises sans connaître leur contexte d'usage...

Patch de l'application

Rappels Smali

À partir de maintenant (voire même avant selon les besoins) il peut être intéressant de patcher l'application aux endroits stratégiques.

Les modifications du code Smali peuvent nécessiter l'usage de variables (registres) locales supplémentaires. Leur nombre est déclaré au début de chaque fonction avec la directive .locals x. Pour x ayant une valeur de 2, vous avez accès aux variables v0 et v1. Les variables nommées px correspondent aux arguments de la fonction ; leur nombre est donc fixe. Les méthodes de classes reçoivent l'instance sur le registre p0, les arguments sont dans les registres à partir de p1. Si la méthode est statique, p0 correspond au premier argument.

Gagnez du temps, au lieu de chercher si une variable est libre, créez-en une nouvelle.

Court exemple de code pour afficher une variable dans le logcat (Log.e()) :

.locals 2
# PATCH increment 1 to 2 for v1 var
...
# PATCH
const-string v1, "TAG"
# Display p3 variable
invoke-static {v1, p3}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
# END PATCH

N'hésitez pas à écrire du code Java puis à le convertir en Smali pour l'étudier :

public class HelloWorld {
    public static void main(String[] args) {
        int i = 3;

        String str = String.valueOf(res);
        System.out.println(str);
    }
}

Compilation :

javac  HelloWorld.java
./sdk/build-tools/29.0.2/dx --dex --output=classes.dex HelloWorld.class
baksmali d classes.dex

Résultat :

const/4 v0, 0x3
invoke-static {v0}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v0
sget-object v1, Ljava/lang/System;->out:Ljava/io/PrintStream;
invoke-virtual {v1, v0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V

Patch

  • Premier patch : Afficher la fréquence et le code chiffré dans send_IR() avant l'appel de l'objet Miir :

    Rappel du code Java :

    // send_async_string_pulses
    c(
        eVar.a(),                   // a() = get_frequency
        cVar.c(),                   // null
        cVar.b()                    // code IR chiffré (string téléchargé)
    );
    

    Code Smali :

    .method public a(Lc/k/i/b/b/b1/p/p/e;ZZ)V
        .locals 3
        # PATCH 1 => 3 (need v1, v2)
        [...]
        # frequency is in p1
        invoke-virtual {p1}, Lc/k/i/b/b/b1/p/p/e;->a()I
        move-result p1
        # null in p3
        invoke-virtual {p2}, Lc/k/i/b/b/b1/p/p/c;->c()Ljava/lang/String;
        move-result-object p3
        # IR crypted code in p2
        invoke-virtual {p2}, Lc/k/i/b/b/b1/p/p/c;->b()Ljava/lang/String;
        move-result-object p2
    
        # PATCH
        const-string v1, "TAG string freq"
        # Cast frequency int to String
        invoke-static {p1}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
        move-result-object v2
        invoke-static {v1, v2}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
    
        const-string v1, "TAG string IR code"
        invoke-static {v1, p2}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
        # END PATCH
    
        invoke-direct {p0, p1, p3, p2}, Lc/k/i/b/b/b1/g;->c(ILjava/lang/String;Ljava/lang/String;)V
    
  • Deuxième patch : Forcer le déchiffrement du code par la fonction native non utilisée getIRCode() :

    .method public b(ILjava/lang/String;Ljava/lang/String;)V
        .locals 3
        # PATCH 1 => 3 (need v1, v2)
    
        invoke-static {}, Lc/k/i/b/b/v0;->a()Landroid/content/Context;
        move-result-object v0
        # Call native transmitIR()
        invoke-static {v0, p1, p2, p3}, Lcom/xiaomi/mitv/phone/remotecontroller/ir/Miir;->b(Landroid/content/Context;ILjava/lang/String;Ljava/lang/String;)I
    
        # PATCH
        const-string v1, "TAG"
        const-string v2, "TAG decode int array"
        invoke-static {v1, v2}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
    
        # We add a call to the native getIRCode()
        invoke-static {v0, p1, p2, p3}, Lcom/xiaomi/mitv/phone/remotecontroller/ir/Miir;->a(Landroid/content/Context;ILjava/lang/String;Ljava/lang/String;)[I
        # Put the result of getIRCode in v1
        move-result-object v1
    
        # Convert int array to String and display it in the logger
        invoke-static {v1}, Ljava/util/Arrays;->toString([I)Ljava/lang/String;
        move-result-object v2
        const-string v1, "TAG int pulse decoded"
        invoke-static {v1, v2}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
        # END PATCH
    
        return-void
    .end method
    

Résultat :

E IRLongPressWidget: onSingleTapUp
E miauto  : sendIR:power
E TAG    : code positif
E TAG string freq: 37990
E TAG string IR code: QJPmll3+SCgpSE73bTO9h...
D miir    : ==============transmitIR================
D miir    : decrypt
D miir    : uncompress
D miir    : uncompress over
D miir    : jsonStringToInts
D miir    : transmit
E TAG    : test decode int pulse
D miir    : ==============get IR code================
E TAG int pulse decoded: [9042, 4484, 579, 552, 580, 567, 579, 567, 544, 554, ...

L'application nous donne elle-même le code. Les valeurs du tableau ne sont pas celles attendues mais j'y reviens à la fin.

Décompilation de la librairie native et routines de déchiffrement

IDA va de nouveau nous servir, plus efficacement et sur la librairie native cette fois :

Chargez le fichier, choisissez "ELF for ARM (Shared Object)" comme type de fichier (si non détecté automatiquement), puis "ARM Little-Endian" en type de processeur.

IDA call graphDécompilation de ./lib/armeabi-v7a/libmiir.so avec IDA

On y voit quelques chaines intéressantes car bien connues en Java : java/lang/String, getBytes, utf-8. Puis des appels à JNIEnv::CallObjectMethod. En effet, notre string est convertit en byte[] (grâce à un code du type str.getBytes("utf-8")).


Pour rappel voici la structure d'une fonction typique écrite en C et appelant une fonction Java :

int my_function() {
    // Get JNI environment
    JNIEnv *env = JniGetEnv();

    // Find the Java class via its package's path
    jclass jniTestClass = env->FindClass("com/example/JNITest");

    // Find the Java method
    // "(Z)I;": Arguments inside (): Java env, and the type of the return value (int here)
    jmethodID myMethod = env->GetStaticMethodID(jniTestClass, "myMethod", "(Z)I;");

    // Call the method
    return (int)env->CallStaticObjectMethod(jniTestClass, myMethod, (jboolean)true);
}

Voir les articles à ce sujet :

Extrait du code relatif aux routines de déchiffrement :

.rodata:00002C46 aComXiaomiMitvS DCB "com/xiaomi/mitv/socialtv/common/utils/EncryptUtil",0
.rodata:00002C46                                         ; DATA XREF: transmitIR+98o
.rodata:00002C46                                         ; .text:off_E00o ...
.rodata:00002C78 aDecrypt        DCB "decrypt",0         ; DATA XREF: transmitIR+AAo
.rodata:00002C78                                         ; .text:off_E04o ...
.rodata:00002C80 aBBB            DCB "([B[B)[B",0        ; DATA XREF: transmitIR+B2o           2 taleaux de bytes; retourne bytes
.rodata:00002C80                                         ; .text:off_E08o ...
.rodata:00002C89 aUncompress     DCB "uncompress",0      ; DATA XREF: transmitIR+C6o
.rodata:00002C89                                         ; .text:off_E0Co ...
.rodata:00002C94 aBLjavaLangStri DCB "([B)Ljava/lang/String;",0 ; DATA XREF: transmitIR+C8o    1 tableau de bytes; retourne un string
.rodata:00002C94                                         ; .text:off_E10o ...
.rodata:00002CAB aUncompressOver DCB "uncompress over",0 ; DATA XREF: transmitIR+118o
.rodata:00002CAB                                         ; .text:off_E20o ...
.rodata:00002CBB aComXiaomiMit_0 DCB "com/xiaomi/mitv/socialtv/common/utils/IOUtil",0
.rodata:00002CBB                                         ; DATA XREF: transmitIR+126o
.rodata:00002CBB                                         ; .text:off_E24o ...
.rodata:00002CE8 aJsonstringtoin DCB "jsonStringToInts",0 ; DATA XREF: transmitIR+13Ao
.rodata:00002CE8                                         ; .text:off_E28o ...
.rodata:00002CF9 aLjavaLangStr_0 DCB "(Ljava/lang/String;)[I",0 ; DATA XREF: transmitIR+13Co

Vous l'avez compris, ce code natif ne fait qu'appeler des fonctions déjà définies dans un module rédigé en Java.

Son existence n'est là que pour brouiller les pistes. Dommage de s'être donné autant de mal pour finalement laisser trainer des strings de debuggage franchement explicites attirant l'attention...

Debugging actif ou non la découverte de ce code est inévitable ; appels Java ou non sa décompilation l'est tout autant ; ce n'est qu'une question de minutes à ce niveau.


Nous avons donc le nom des fonctions appelées, leur localisation (com/xiaomi/mitv/socialtv/common/utils/EncryptUtil) et leur contenu :

// Variable très secrète bien ortographiée (spoiler : rien à voir avec l'algo DES)
public static final String DESCRYPTED_KEY = "fd7e915003168929c1a9b0ec32a60788";

public static byte[] decrypt(String str, String str2) {
    [...]
    return decrypt(str.getBytes("UTF-8"), str2.getBytes("UTF-8"));
}

public static byte[] decrypt(byte[] bArr, byte[] bArr2) {
    SecretKeySpec secretKeySpec = new SecretKeySpec(bArr2, "AES");  // Déchiffrement AES avec DESCRYPTED_KEY
    Cipher instance = Cipher.getInstance("AES/ECB/NoPadding");
    instance.init(2, secretKeySpec);
    return instance.doFinal(Base64.decode(bArr, 0));
}

public static String uncompress(byte[] bArr) {                      // Décompression GZip
    GZIPInputStream gZIPInputStream = new GZIPInputStream(new ByteArrayInputStream(bArr));
    [...]
}

Vous pouvez vous faire un programme Java en recopiant ces fonctions afin de tester les entrées/sorties. Ou faire une implémentation Python quand vous voudrez passer à un langage sérieux (:p) :

import base64
import gzip
from Crypto.Cipher import AES

PATTERN_SECRET_KEY = "fd7e915003168929c1a9b0ec32a60788"

def decrypt_pattern(encoded_pattern):
    """Decrypt base64 string with AES ECB mode and a known "secret key"

    :param encoded_pattern: Encrypted text
    :type encoded_pattern: <str>
    :return: Decrypted bytes
    :rtype: <bytes>
    """
    # Base64 decode
    encoded_pattern = base64.b64decode(encoded_pattern)
    cipher = AES.new(PATTERN_SECRET_KEY.encode(), AES.MODE_ECB)
    # Remove trailing padding spaces
    return cipher.decrypt(encoded_pattern).rstrip()


def process_xiaomi_shit(encoded_pattern):
    """Process encoded IR pattern from the API of Xiaomi

    Operations:
        - Decode base64 string
        - Decrypt pattern (AES ECB mode) with a known "secret key"
        - Uncompress GZIP data to recover the plain text as a JSON array
        - Convert JSON array to Python list

    :param encoded_pattern: Base64 encoded, AES ECB crypted, GZip compressed pattern.
        .. seealso:: :meth:`decrypt_pattern`
    :return: List of raw timmings corresponding to the IR code.
        Each value is the time during the transmitter should be stay ON or OFF
        (It's not based on the number of pulses regarding to the frequency used).
    """
    decrypted_pattern = decrypt_pattern(encoded_pattern)
    plain_text = gzip.decompress(decrypted_pattern)
    return json.loads(plain_text)

Idem mais en version "développeur" "moderne"/"chercheur" (supprimez les commentaires et les sauts de ligne ; si c'est moins long c'est plus rapide non ?) :

json.loads(gzip.decompress(AES.new(PATTERN_SECRET_KEY.encode(), AES.MODE_ECB).decrypt(base64.b64decode(encoded_pattern)).rstrip()))

Comparez-les sorties avec celles du logcat suite aux patchs du Smali.

=> c'est bon.

Conversion des valeurs

On sait que les valeurs passées à l'API sont des durées en microsecondes. Ce sont les valeurs que nous venons de déchiffrer. Toutefois certaines bases de données ou formats encodent ces durées en nombre de pulsations pour une fréquence donnée (en Hz).

Cela revient au même mais il faut pourvoir convertir ces valeurs. via un simple produit en croix.

Ainsi, pour une durée de 9042µs à 37990Hz :

9042 µs      => x pulsations
1s (10^6 µs) => 37990 pulsations

x = (37990 * 9042) / 1 000 000 = 343 pulsations

Idem pour (4484*37990)/1000000 = 170 pulsations.

Ainsi :

[9042, 4484, 579, 552, 580, 567, 579, 567, 544, 554, ...

devient :

[347, 173, ...

On retrouve ainsi les valeurs obtenues sur irdb.tk (PS: des écarts de 10% sont tolérés par les appareils).


=> Lire l'excellente suite d'articles sur l'encodage des touches de télécommande dans un signal infrarouge sur remotecentral.com (Codes infrarouges : Pronto's IR Code Format).

Conclusion

Le HTTPS n'a pas été un problème ; l'offuscation et l'usage de code natif sont conçus exclusivement pour décourager la rétro-ingénierie en masquant le détail de la procédure de déchiffrement et de décompression. Toutefois les méthodes utilisées ont été codées en Java et décompilées dès le début de l'étude.

Nous maitrisons donc pleinement le déchiffrement des codes infrarouge obtenus auprès de l'API de Xiaomi.

Toutefois nous souhaiterions dorénavant récupérer les codes sans passer par l'application... La méthode manuelle de sélection d'un appareil, suivie de l'envoi d'un code, puis de sa récupération par le logcat est fastidieuse et irréalisable à plus grande échelle.

Le but est bien entendu de faire un dump complet de la base de données, puis d'en faire un joli projet envoyé sur GitHub...

Sources