Suite de la partie 1 sur la rétro-ingénierie de l'application Mi Remote, dans laquelle nous avions trouvé comment retrouver en clair les codes infrarouges provenant de l'API REST de Xiaomi.
Cette partie traitera de la mise en œuvre d'un procédé de récupération complet de la base de données.
Sommaire
Détail d'une requête
Je rappelle quelques exemples de requêtes envoyées au serveur par l'application, selon les choix de l'utilisateur. Nous y voyons la récupération de la liste des appareils, la liste des marques connues pour un type d'appareil, puis la liste des modèles connus pour la marque sélectionnée.
L'ordre des paramètres est fixe et est important :
Préfixe de l'url: Chemin vers le controlleur de l'API interrogée :
- /controller/device/1
- /controller/brand/list/1
- /controller/match/tree/1
Paramètres fixes :
- version: Version de l'application
- country: Identifiant de pays (FR, CN, US, etc.)
- ts: Timestamp en millisecondes
- nonce: Nombre aléatoire
Paramètres "utilisateur" variables :
- devid: Type d'appareil choisi (1: TV, 10: Projecteur, etc.)
- miyk: ? - Fixé à 1
- brandid: Identifiant de la marque sélectionnée (64: Toshiba, etc.)
paramètre fixe de fin :
- opaque: Chaine hexadécimale
Ces connaissances sont déduites de l'étude des données JSON exportées depuis Mitmproxy.
Un problème opaque...
Si vous essayez de réutiliser une requête déjà émise vous obtiendrez de nouveau la réponse (probablement durant un laps de temps modéré ; n'espérez pas pouvoir réutiliser une requête datant d'un an).
Si vous essayez de modifier certains paramètres comme brandid
la réponse du serveur sera vide.
En effet, une requête est un ensemble fixe non altérable de paramètres. Le serveur refuse de traiter une requête forgée arbitrairement. Il s'agit à la fois d'une protection anti-rejeu et d'une obligation d'authentification auprès du serveur.
En clair votre application montre patte blanche pour que sa demande soit honorée.
La plupart du temps la requête contient un jeton de session obtenu par concaténation de divers paramètres envoyés dans une fonction de hachage cryptographique (fonction acceptant une entrée de taille variable et ayant une sortie de taille fixe, empêchant le re-calcul de la donnée d'entrée). SHA-1 est souvent utilisée dans ce contexte. Le jeton est ajouté à la requête avant son envoi. Vous l'avez compris ce jeton certifiant est nommé opaque
dans Mi Remote.
Afin d'éviter que l'url ne puisse être réutilisée, le jeton doit changer à chaque fois même si les paramètres demandés par l'utilisateur ne changent pas. Cette notion d'aléatoire est apportée par le timestamp ts
et par le nonce
(une chaine ou un nombre aléatoire). En effet, le timestamp seul ne suffit pas, car émettre une requête avec une date future assurererait sa validité.
Il faut également noter que parfois un sel cryptographique est ajouté aux paramètres avant hachage. Il est en général utilisé pour que deux informations identiques aboutissent à un résultat différent de la fonction de hachage (par exemple des mots de passe d'utilisateurs identiques donneront des hashs différents avec des sels différents avant stockage en base de données). Le salage n'est évidemment efficace que si personne ne connait le sel utilisé...
L'intégration d'un sel dans les urls forgées d'une application décompilable est douteux, mais nous avons déjà vu dans la partie 1 que les développeurs se donnent du mal pour ralentir l'étude de leur application...
Le but de cet article revient donc à trouver l'algorithme produisant le paramètre opaque
.
Recherche dans le projet et fausses pistes
Comme montré dans la partie 1, faites une recherche du mot opaque
dans le projet. Il sera présent dans quelques fichiers.
Exemple :
// classes2.dex: ./c/k/i/b/b/b1/r/e.java
public static String a(String str, String str2) {
[...]
c2.append(String.format("session_secret=%s", c.k.j.a.c.a.f.a.a.b(encoded_random_str, "UTF-8")));
c2.append(String.format("&data=%s", c.k.j.a.c.a.f.a.a.b(encoded_url_part2, "UTF-8")));
// Ajoute opaque et retourne l'url
c2.append(String.format("&opaque=%s", c.k.j.a.c.a.f.a.a.b(str2, "UTF-8")));
return c2.toString();
}
Fausse alerte : Cette url ne nous concerne pas car elle contient des paramètres inconnus (session_secret
et data
). De même, une autre occurence proposera les bons termes mais dans le désordre. Le programme a l'air d'être truffé de code ancien.
Nous trouvons enfin:
// classes3.dex: c/k/j/a/i/.c.java
// Renommage en str_add_opaque_str2
public static String a(String str, String str2) {
return str + "&" + "opaque" + "=" + str2;
}
À partir de maintenant, jadx-gui va amplement simplifier l'étude en nous permettant de retrouver les fonctions qui utilisent ce code, tout en renommant des items dans tout le projet en un clic.
Assemblage de la requête HTTP
Quatre fichiers importants sont identifiés grâce à la recherche de chaines et à jadx-gui :
com/xiaomi/mitv/phone/remotecontroller/ir/activity/MatchIRActivityV52.java
: Déclenche la requête et récupère les données JSON pour les analyser (cf. Partie 1)../c/k/i/b/b/y0/g.java
: Controleur ; construit l'url en utilisant le préfixe, injecte les paramètres variables../c/k/j/a/i/c.java
: Construit la liste des paramètres fixes, calcule le paramètreopaque
, exécute la requête../c/k/j/a/i/e.java
: Objet Query initialisé progressivement avec tous les constituants de la requête (méthode (POST/GET), url, chemin, headers, paramètres).
L'objet ./c/k/j/a/i/e.java
est aisément compréhensible connu grâce aux messages des exceptions réparties dans les méthodes :
throw new IllegalArgumentException("queries == null");
throw new IllegalArgumentException("headers == null");
throw new IllegalArgumentException("method == null");
throw new IllegalArgumentException("queries == null");
throw new IllegalArgumentException("path == null");
throw new IllegalArgumentException("url == null");
throw new IllegalArgumentException("method == null");
À partir de cela il est facile d'identifier les accesseurs/mutateurs (getters/setters) et attributs de l'objet afin de faire sa rétro-ingénierie complète ; la structure de données est similaire à n'importe quelle librairie de n'importe quel langage qui manipule des requêtes.
--
Dans le fichier ./c/k/i/b/b/y0/g.java
, nous voyons apparaitre, grâce au renommage des méthodes, une fonction construisant les paramètres d'une de nos requêtes d'intérêt :
public QueryObj mo21081a() {
ArrayList arrayList = new ArrayList();
arrayList.add(new KeyValuePair(devid_str, String.valueOf(this.devid))); // devid
arrayList.add(new KeyValuePair(miyk_str, "1")); // miyk
arrayList.add(new KeyValuePair(brandid_str, String.valueOf(this.brandid))); // brandid
[...]
arrayList.add(new KeyValuePair("power", this.power ? "0" : "1")); // power
[...]
return new QueryObj.C6146a()
.set_path(C5386g.controller_match_tree_str)
.set_url(get_server_plus_url(C5386g.controller_match_tree_str)) // https://sg-urc.io.mi.com + /controller/match/tree/1
.set_queries(arrayList)
.set_method("GET")
.get_copy_instance();
}
L'étape finale consiste à ajouter le paramètre opaque
à l'url ; ceci est fait dans ./c/k/j/a/i/c.java
(contenant déjà str_add_opaque_str2()
):
// ./c/k/j/a/i/c.java
public static Response execute_query(QueryObj eVar) {
String str;
Request.Builder builder = new Request.Builder();
Headers a = C6163c.m22947a(eVar.get_headers());
builder.headers(a);
List<KeyValuePair> a2 = build_first_url_params();
C5864a a3 = init_hashes(false);
String a4 = add_query_params_to_url_and_GET_opaque(eVar, C6164d.concat_url_with_params(eVar.get_path(), a2), a3.token, a3.secret_key);
String a5 = C6164d.concat_url_with_params(eVar.get_url(), a2);
[...]
str = C6164d.concat_url_with_params(a5, eVar.get_queries());
builder.url(str_add_opaque_str2(str, a4));
[...]
return m22889b().newCall(builder.build()).execute();
}
ligne 10 :
init_hashes
construit un objet avec deux attributs qui une fois identifiés sont un token interne (qui est en fait le sel mentionné dans l'introduction) et une clé secrète.return C5864a.m21432a("0f9dfa001cba164d7bda671649c50abf", "581582928c881b42eedce96331bff5d3");
Il faut noter que ces 2 chaines sont notées en dur dans le code du projet.
ligne 11 : La fonction
add_query_params_to_url_and_GET_opaque()
appellecompute_opaque()
:/* renamed from: a */ public static String add_query_params_to_url_and_GET_opaque(QueryObj eVar, String str, String str2, String str3) { String str4; StringBuilder sb = new StringBuilder(); if (eVar.get_method().equals("GET")) { sb.append(C6164d.concat_url_with_params(str, eVar.get_queries())); str4 = sb.toString(); } [...] return compute_opaque(str4, str2, str3); }
compute_opaque()
obtient donc les arguments suivants :
- str4: Le préfixe + paramètres fixes + paramètres variables
- str2: Le token secret/sel
- str3: La clé secrète
Sa ligne la plus importante insère le token ; celui-ci ne sert qu'au calcul de la signature. Il n'apparait pas dans l'url finale.
return SignatureUtil.getSignature((str + "&token=" + str2).getBytes(), str3.getBytes());
Voici enfin le hachage de nos paramètres :
// com.xiaomi.mitv.socialtv.common.utils.SignatureUtil
public static String getSignature(byte[] bArr, byte[] bArr2) {
SecretKeySpec secretKeySpec = new SecretKeySpec(bArr2, "HmacSHA1");
Mac instance = Mac.getInstance("HmacSHA1");
instance.init(secretKeySpec);
return IOUtil.byteArray2HexString(instance.doFinal(bArr));
}
Patchs
Essayons de faire en sorte que l'application affiche elle-même dans les logs, les informations que nous recherchons.
Affichage du contenu de arrayList juste avant le retour de
mo21081a()
; observez l'usage d'un objetStringBuilder
pour convertir la variable en String et l'envoyer dans la fonction de debugLog.e()
:# c/k/i/b/b/y0/g$o.smali # g$o: objet companion "o" intégré dans la classe g # Signature Java: # /* renamed from: c.k.i.b.b.y0.g$o */ # public static class AsyncTaskC5408o extends AbstractAsyncTaskC5396e { # virtual methods .method public a()Lc/k/j/a/i/e; .locals 6 # PATCH increment 5 to 6 for v5 var [...] :cond_3 # PATCH HERE # v0: ArrayList # v1, v2, v3 are usable # Equivalent to: Log.e("TAG query params", "ArrayList: " + arrayList); sget-object v1, Ljava/lang/System;->out:Ljava/io/PrintStream; new-instance v2, Ljava/lang/StringBuilder; invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V const-string v3, "ArrayList: " invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v2 invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder; move-result-object v5 invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v5 const-string v3, "TAG query params" invoke-static {v3, v5}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I # END PATCH # Code continuation... # Equivalent to: QueryObj.C6146a().set_path(C5386g.controller_match_tree_str)... new-instance v1, Lc/k/j/a/i/e$a; invoke-direct {v1}, Lc/k/j/a/i/e$a;-><init>()V const-string v2, "/controller/match/tree/1" invoke-virtual {v1, v2}, Lc/k/j/a/i/e$a;->c(Ljava/lang/String;)Lc/k/j/a/i/e$a; move-result-object v1 [...]
Résultat :
E TAG query params: ArrayList: [devid:10, miyk:1, brandid:1, power:1]
Affichage des paramètres de compute_opaque() juste avant son appel et de son résultat :
.method public static a(Lc/k/j/a/i/e;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; [...] # PATCH # v1: libre # p0: url # p2: token # p3: private key # juste avant concaténation de l'un avec l'autre const-string v1, "TAG url" invoke-static {v1, p0}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I const-string v1, "TAG token" invoke-static {v1, p2}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I const-string v1, "TAG private key" invoke-static {v1, p3}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I # END PATCH # Call compute_opaque() invoke-static {p0, p2, p3}, Lc/k/j/a/i/c;->a(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; move-result-object p0 # PATCH const-string v1, "TAG opaque" invoke-static {v1, p0}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I # END PATCH return-object p0 .end method
Résultat :
E TAG url: /controller/device/1?version=6034&country=FR&ts=1615406505554&nonce=1126639370 E TAG token: 0f9dfa001cba164d7bda671649c50abf E TAG private key: 581582928c881b42eedce96331bff5d3 E TAG opaque: f422f7bdc77403414fdd674758af0b39435bb46e
Implémentation en Python
from Crypto.Hash import HMAC, SHA1
def get_opaque_http_param(url, token, secret_key):
"""Get the opaque parameter based on the url, an internal token and a
secret key for the hash algorithm
:return: "opaque" parameter to be inserted at the end of the url path
:rtype: <str>
"""
plain_text = url + "&token=" + token
# Get signature of this concatenation
return get_signature(plain_text, secret_key)
def get_signature(plain_text, secret_key):
"""Get signature/hash of the given plain_text with the secret_key
Use HMAC (Hash-based Message Authentication Code) with SHA1 hash algorithm.
:param plain_text: Clear text to be signed
:param secret_key: Secret key
:type plain_text: <str>
:type secret_key: <str>
:return: Hex digest of the signature
:rtype: <str>
"""
cipher = HMAC.new(secret_key.encode(), digestmod=SHA1)
return cipher.update(plain_text.encode()).hexdigest()
Conclusion
Nous sommes dorénavant capables de forger des requêtes valides auprès de l'API REST de Mi Remote, et pourquoi pas, de réaliser une copie quasi-complète du serveur en quelques requêtes.
Le code nécessaire pour ceci est disponible sur GitHub - Mi Remote Database.
Aria
Hi, thank you for this great article. I am a fellow believer in Free Software too and I follow Richard Stallman closely and he always says that reverse engineering is the biggest contribution in the Free Software community.
Happy hacking!
Lex
Thank you for your comment. I had precisely the words of this great personage in mind while writing this article (as well as others in this section).
The history of Free Software and the GNU Project was born in the early 80's out of the problems with the proprietary drivers of a Xerox 9700 printer that Stallman had in his MIT lab. Despite his requests and error reports he never got an answer or a fix from Xerox.
But you must already know all this :)
It is indeed an unfair power that is imposed on the users. Especially nowadays, closed source code is also an opportunity to accumulate valuable behavioral data (this is the case of the app we are talking about here, and of the Xiaomi OS for example).
Most of today's IT is not based on proprietary code, but most of the users are affected by it.
Challenging this power is necessary but dangerous for companies. This is why copyright rights are so strongly defended in our sick western or westernized societies and why reverse engineering is illegal (I live in France, as you may have noticed...).
Also, I notice that you had trouble posting your message. Indeed, I manually validate messages when I have time to do so to avoid spam and security problems. My system is obviously not explicit enough.
A waiting time is necessary but it's the price to pay for not using privacy invasive systems like Disqus etc.
Happy hacking ;)
French version:
Merci pour votre commentaire. J'avais précisément en tête les dires de ce grand personnage en tête en rédigeant cet article (comme d'autres de cette section).
L'histoire du logiciel libre et du projet GNU sont d'ailleurs nés au début des années 80, des difficultés liées aux drivers devenus propriétaires d'une imprimante Xerox 9700 que Stallman avait dans son labo du MIT. Malgré ses demandes et rapports d'erreurs il n'a jamais obtenu de réponse ou de correctif de la part de Xerox.
Mais vous devez déjà savoir tout cela :)
C'est bel et bien un pouvoir injuste qui est exercé sur les utilisateurs. D'autant plus qu'aujourd'hui le code source fermé est aussi l'occasion d'accumuler de précieuses données comportementales (c'est le cas de l'appli dont il est question ici et des OS Xiaomi par exemple).
L'essentiel de l'informatique d'aujourd'hui ne repose pas sur du code propriétaire pourtant l'essentiel des utilisateurs le subit.
Remettre en question ce pouvoir est nécessaire mais dangereux pour les entreprises. C'est d'ailleurs pourquoi les droits du copyright sont si vivement défendus dans nos sociétés occidentales ou occidentalisées malades et que le reverse ingineering est illégal.
Également, je constate que vous avez eu du mal à poster votre message. En effet, je valide manuellement les messages quand j'en ai le temps pour éviter les spams, problèmes de sécurité. Mon système n'est manifestement pas assez explicite.
Un temps d'attente est nécessaire mais c'est le prix à payer pour ne pas faire appel à des systèmes invasifs pour la vie privée comme Disqus etc.