Vous devez être 64 bits pour monter sur ce ferry
Inverser l'ingénierie d'une application NY Waterway mise à jour pour le Pixel 7
TLDR : Si vous avez un appareil Android plus récent qui ne vous permet pas d'installer NY Waterway, vous pouvez télécharger ma version modifiée de l'application . Vous devez toujours être prudent lorsque vous installez des applications aléatoires, en particulier à partir de sources autres que le Play Store officiel - comme ce message moyen par un gars au hasard dont vous n'avez jamais entendu parler. Si vous voulez être très prudent, vous pouvez lire à l'avance pour voir comment l'APK a été modifié (et même répéter les étapes vous-même si vous le souhaitez).
En 2019, Google a rendu la prise en charge 64 bits requise pour toutes les applications nouvelles et mises à jour du Play Store. À partir d'août 2021, les applications qui ne prennent pas en charge l'architecture 64 bits sont devenues indisponibles dans le Play Store pour les appareils compatibles 64 bits. Notamment, les nouveaux Pixel 7 et Pixel 7 Pro ne prennent pas du tout en charge l'installation d' applications 32 bits uniquement .
Pour les New-Yorkais qui empruntent le ferry de la rivière Hudson, c'est assez gênant car l'application qui fournit des billets électroniques sur votre téléphone, NY Waterway , est vraiment ancienne . Il a été publié pour la dernière fois en juin 2018 et contient des bibliothèques natives pour les architectures 32 bits uniquement… Par conséquent, pour les utilisateurs des nouveaux appareils Pixel, pas de billets électroniques sur le ferry de l'Hudson River pour vous !
Je suis passé à l'iPhone il y a de nombreuses années maintenant, mais à l'époque où j'étais un utilisateur d'Android, j'avais l'habitude de bidouiller avec le système d'exploitation et les applications - en installant des ROM personnalisées et en décompilant des applications. Un de mes amis proches a acheté le nouveau Pixel 7 Pro et prend tout le temps le ferry de la rivière Hudson, alors il m'a poussé en plaisantant à réparer cette application pour lui. Nous y voilà!
Regarder dans l'application
Commençons par inspecter l'application NY Waterway pour identifier les parties 32 bits uniquement qui l'empêchent d'être installée. En utilisant apktool
, nous pouvons extraire l'application Android et inspecter son code.
$ apktool d ./NYWaterway.apk
I: Using Apktool 2.6.1 on NYWaterway.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /Users/joeywatts/Library/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
$ cd ./NYWaterway
$ ls -l
total 72
-rw-r--r-- 1 joeywatts staff 8797 Nov 21 18:37 AndroidManifest.xml
-rw-r--r-- 1 joeywatts staff 21382 Nov 21 18:37 apktool.yml
drwxr-xr-x 14 joeywatts staff 448 Nov 21 18:37 assets
drwxr-xr-x 5 joeywatts staff 160 Nov 21 18:37 lib
drwxr-xr-x 4 joeywatts staff 128 Nov 21 18:37 original
drwxr-xr-x 178 joeywatts staff 5696 Nov 21 18:37 res
drwxr-xr-x 10 joeywatts staff 320 Nov 21 18:37 smali
drwxr-xr-x 10 joeywatts staff 320 Nov 21 18:37 unknown
Compatibilité 64 bits et bibliothèques natives
Les applications Android sont généralement écrites en Java ou en Kotlin, deux langages qui ciblent la machine virtuelle Java, qui est une abstraction de haut niveau qui vous protège généralement des préoccupations concernant la compatibilité spécifique à la plate-forme. Cependant, vous pouvez utiliser Java Native Interface (JNI) pour appeler du code natif spécifique à la plate-forme (généralement compilé à partir de langages de niveau inférieur tels que C ou C++). Si nous regardons le libs
répertoire, nous pouvons voir les bibliothèques natives incluses dans l'application NY Waterway.
$ ls -lR lib/*
lib/armeabi:
total 8352
-rw-r--r-- 1 joeywatts staff 177900 Nov 21 18:37 libdatabase_sqlcipher.so
-rw-r--r-- 1 joeywatts staff 1369284 Nov 21 18:37 libsqlcipher.so
-rw-r--r-- 1 joeywatts staff 2314540 Nov 21 18:37 libsqlcipher_android.so
-rw-r--r-- 1 joeywatts staff 402604 Nov 21 18:37 libstlport_shared.so
lib/armeabi-v7a:
total 2552
-rw-r--r-- 1 joeywatts staff 1303788 Nov 21 18:37 libsqlcipher.so
lib/x86:
total 14616
-rw-r--r-- 1 joeywatts staff 1476500 Nov 21 18:37 libdatabase_sqlcipher.so
-rw-r--r-- 1 joeywatts staff 2246448 Nov 21 18:37 libsqlcipher.so
-rw-r--r-- 1 joeywatts staff 3294132 Nov 21 18:37 libsqlcipher_android.so
-rw-r--r-- 1 joeywatts staff 455740 Nov 21 18:37 libstlport_shared.so
Une autre observation ici est que armeabi
et x86
avoir quatre bibliothèques alors qu'il armeabi-v7a
n'en a qu'une. Pour qu'une bibliothèque soit chargée par l'application Android, elle doit appeler java.lang.System.loadLibrary
ou java.lang.Runtime.loadLibrary
. La recherche dans le code Smali de "loadLibrary" ne révèle qu'un seul endroit où il charge les bibliothèques natives.
$ grep -r loadLibrary smali/
smali//net/sqlcipher/database/SQLiteDatabase.smali: invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
$ grep loadLibrary -A 2 -B 3 smali/net/sqlcipher/database/SQLiteDatabase.smali
:try_start_0
const-string v0, "sqlcipher"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
:try_end_0
.catchall {:try_start_0 .. :try_end_0} :catchall_0
Nous avons besoin d'une version ARM 64 bits libsqlcipher.so
pour lib/arm64-v8a
rendre l'application compatible avec les nouveaux appareils Pixel. Idéalement, SQLCipher est une bibliothèque open source . En regardant le code de colle de haut niveau pour interagir avec la bibliothèque sqlcipher native, nous pouvons voir la version de la bibliothèque qui a été utilisée.
$ grep -ri version smali/net/sqlcipher
smali/net/sqlcipher/database/SQLiteDatabase.smali:.field public static final SQLCIPHER_ANDROID_VERSION:Ljava/lang/String; = "3.5.4"
Mise à niveau de SQLCipher vers la v3.5.5
Le processus de mise à niveau impliquera le remplacement du code SQLCipher Smali et des bibliothèques natives par le code de la version la plus récente. Cela causerait des problèmes si la surface de l'API publique de SQLCipher changeait de manière significative (par exemple, si une fonction publique utilisée par NY Waterway changeait de signature ou était supprimée, la remplacer par la version la plus récente poserait des problèmes). En faisant une analyse rapide des modifications de la v3.5.4 à la v3.5.5, cela ne semble pas être un problème qui apparaîtra ici. J'ai téléchargé le fichier AAR pour SQLCipher v3.5.5 , puis utilisé unzip
pour l'extraire.
$ mkdir ../sqlcipher && cd ../sqlcipher
$ unzip ~/Downloads/android-database-sqlcipher-3.5.5.aar
Archive: /Users/joeywatts/Downloads/android-database-sqlcipher-3.5.5.aar
inflating: AndroidManifest.xml
creating: res/
inflating: classes.jar
creating: jni/
creating: jni/arm64-v8a/
creating: jni/armeabi/
creating: jni/armeabi-v7a/
creating: jni/x86/
creating: jni/x86_64/
inflating: jni/arm64-v8a/libsqlcipher.so
inflating: jni/armeabi/libsqlcipher.so
inflating: jni/armeabi-v7a/libsqlcipher.so
inflating: jni/x86/libsqlcipher.so
inflating: jni/x86_64/libsqlcipher.so
Le SDK Android fournit un outil de ligne de commande appelé d8
qui peut compiler un jar
fichier en code binaire Android ( classes.dex
fichier). Ensuite, il existe un autre outil appelé baksmali
qui peut décompiler dex
les fichiers en smali
. En combinant les étapes :
$ export ANDROID_HOME=/Users/joeywatts/Library/Android/sdk
$ $ANDROID_HOME/build-tools/33.0.0/d8 classes.jar \
--lib $ANDROID_HOME/platforms/android-31/android.jar
$ java -jar ../baksmali.jar dis ./classes.dex
$ rm -r ../NYWaterway/smali/net/sqlcipher ../NYWaterway/lib
$ mv out/net/sqlcipher ../NYWaterway/smali/net/sqlcipher
$ mv jni ../NYWaterway/lib
Maintenant, nous pouvons reconstruire l'application et la signer, afin qu'elle puisse être installée sur un appareil !
$ cd ../NYWaterway
$ apktool b .
$ keytool -genkey -v -keystore my-release-key.keystore -alias alias_name \
-keyalg RSA -keysize 2048 -validity 10000
$ $ANDROID_HOME/build-tools/33.0.0/apksigner sign \
--ks my-release-key.keystore ./dist/NYWaterway.apk
Augmentation de la version cible du SDK
Pour se débarrasser de cette fenêtre contextuelle indiquant que l'application a été conçue pour une ancienne version d'Android, nous devons augmenter la version cible du SDK dans apktool.yml
. Les applications qui ciblent la version SDK <31 ne sont plus acceptées dans le Play Store, j'ai donc choisi de l'augmenter jusqu'à cela.
Le ciblage d'une version plus récente du SDK Android peut nécessiter des modifications de code, car les API obsolètes deviennent indisponibles dans les nouvelles versions du SDK. NY Waterway nécessite plusieurs modifications pour cibler le SDK v31.
Exportation de composants plus sûre
Si votre application cible Android 12 ou version ultérieure et contient des activités, des services ou des récepteurs de diffusion qui utilisent des filtres d'intention, vous devez déclarer explicitement l' android:exported
attribut pour ces composants d'application.
Il y a quelques activités et un récepteur qui ont <intent-filter>
s et nécessitent android:exported="true"
l'ajout d'un attribut dans AndroidManifest.xml
.
Mutabilité des intentions en attente
Si votre application cible Android 12, vous devez spécifier la mutabilité de chaque PendingIntent
objet créé par votre application. Cette exigence supplémentaire améliore la sécurité de votre application.
Celui-ci est plus délicat, car il nous oblige à modifier le code réel (par opposition à la configuration du projet ou à la copie d'une version mise à niveau de la bibliothèque).
Chaque fois qu'un PendingIntent
objet est créé, il doit spécifier explicitement FLAG_MUTABLE
ou FLAG_IMMUTABLE
. Dans les versions précédentes du SDK, FLAG_MUTABLE
était la valeur par défaut si aucun indicateur n'était spécifié. PendingIntent
les objets sont créés par un ensemble de méthodes statiques sur la classe : getActivity
, getActivities
, getBroadcast
ou getService
. Nous pouvons commencer par rechercher les invocations de ces fonctions.
$ grep -r -E "PendingIntent;->(getActivity|getActivities|getBroadcast|getService)" smali
smali/android/support/v4/f/a/ac.smali: invoke-static {p1, v2, v0, v2}, Landroid/app/PendingIntent;->getBroadcast(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
smali/com/google/firebase/iid/r.smali: invoke-static {p0, p1, v0, p4}, Landroid/app/PendingIntent;->getBroadcast(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
smali/com/google/firebase/iid/m.smali: invoke-static {p0, v2, v0, v3}, Landroid/app/PendingIntent;->getBroadcast(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
smali/com/google/firebase/messaging/c.smali: invoke-static {v0, v2, v1, v3}, Landroid/app/PendingIntent;->getActivity(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
smali/com/google/android/gms/common/m.smali: invoke-static {p1, p3, v0, v1}, Landroid/app/PendingIntent;->getActivity(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
smali/com/google/android/gms/common/api/GoogleApiActivity.smali: invoke-static {p0, v0, v1, v2}, Landroid/app/PendingIntent;->getActivity(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
smali/com/google/android/gms/c/cbx.smali: invoke-static {v1, v2, v0, v3}, Landroid/app/PendingIntent;->getActivity(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
smali/com/google/android/gms/c/cbx.smali: invoke-static {v2, v7, v1, v7}, Landroid/app/PendingIntent;->getBroadcast(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
smali/com/google/android/gms/c/v.smali: invoke-static {v0, v1, v2, v3}, Landroid/app/PendingIntent;->getActivity(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
smali/com/google/android/gms/c/bj.smali: invoke-static {v1, p2, v0, v2}, Landroid/app/PendingIntent;->getActivity(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
smali/com/google/android/gms/c/byd.smali: invoke-static {v1, v4, v0, v4}, Landroid/app/PendingIntent;->getBroadcast(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
smali/com/google/android/gms/c/mr.smali: invoke-static {v1, v3, v0, v3}, Landroid/app/PendingIntent;->getBroadcast(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
Comprendre Smali
L' invoke-static
instruction de code d'octet prend une liste de registres à passer en tant que paramètres dans la fonction statique. Le symbole de la fonction statique ressemble à Landroid/app/PendingIntent;->getBroadcast(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
ce qui est une traduction directe du nom de classe complet et de la signature de la fonction. Il commence par le nom de la classe Landroid/app/PendingIntent;
(ou android.app.PendingIntent
dans la syntaxe Java normale). Puis le nom de la fonction ( ->getBroadcast
) avec les paramètres et le type de retour. Landroid/content/Context;ILandroid/content/Intent;I
sont les paramètres, qui peuvent être divisés en quatre paramètres : Landroid/content/Context;
( android.content.Context
), I
( int
), Landroid/content/Intent;
( android.content.Intent
) et I
( int
). Enfin, après la parenthèse fermante se trouve le type de retour : Landroid/app/PendingIntent;
.
Par conséquent, invoke-static {v1, v2, v3, v4}
la fonction ci-dessus passerait v1
pour le Context
, v2
pour le premier int
, v3
pour le Intent
et v4
pour le int
. Pour ces PendingIntent
API, les flags
sont toujours le dernier paramètre ( int
) donc nous avons juste besoin de nous assurer que la valeur a toujours l'un FLAG_MUTABLE
ou l'autre FLAG_IMMUTABLE
. La documentation du SDK Android révèle que la valeur de FLAG_MUTABLE
est 0x02000000
et FLAG_IMMUTABLE
est 0x04000000
. Dans la plupart des cas, le dernier paramètre est spécifié en tant que registre de variable locale ( v#
) qui a été initialisé avec une valeur constante (telle que const/high16 v3, 0x8000000
ou const/4 v4, 0x0
). Dans ces cas, on peut trivialement vérifier si FLAG_MUTABLE
ouFLAG_IMMUTABLE
est défini et mettre à jour la constante si ce n'est pas le cas.
- const/high16 v3, 0x8000000
+ const/high16 v3, 0xA000000
invoke-static {v1, v2, v0, v3}, Landroid/app/PendingIntent;->getActivity(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
# you may need to change from const/4 to const/high16 to specify the flag
# const/4 is a loading a signed 4-bit integer (seen used to load 0x0).
# const/high16 loads the high 16-bits from a value (the low 16-bits must be 0)
- const/4 v4, 0x0
+ const/high16 v4, 0x2000000
.method private static a(Landroid/content/Context;ILjava/lang/String;Landroid/content/Intent;I)Landroid/app/PendingIntent;
.locals 2
new-instance v0, Landroid/content/Intent;
const-class v1, Lcom/google/firebase/iid/FirebaseInstanceIdInternalReceiver;
invoke-direct {v0, p0, v1}, Landroid/content/Intent;-><init>(Landroid/content/Context;Ljava/lang/Class;)V
invoke-virtual {v0, p2}, Landroid/content/Intent;->setAction(Ljava/lang/String;)Landroid/content/Intent;
const-string v1, "wrapped_intent"
invoke-virtual {v0, v1, p3}, Landroid/content/Intent;->putExtra(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;
invoke-static {p0, p1, v0, p4}, Landroid/app/PendingIntent;->getBroadcast(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
move-result-object v0
return-object v0
.end method
if (p4 & (FLAG_IMMUTABLE | FLAG_MUTABLE) == 0) {
p4 |= FLAG_MUTABLE;
}
const/high16 v3, 0x6000000 # v3 = FLAG_IMMUTABLE | FLAG_MUTABLE
and-int v2, p4, v3 # v2 = p4 & v3
if-nez v2, :cond_0 # if (v2 != 0) { goto :cond_0; }
const/high16 v3, 0x2000000 # v3 = FLAG_MUTABLE
or-int p4, p4, v3 # p4 = p4 | v3
:cond_0
Modifications des autorisations du système de fichiers
Les autorisations de fichiers des fichiers privés ne doivent plus être assouplies par le propriétaire, et une tentative de le faire en utilisant MODE_WORLD_READABLE
et/ou MODE_WORLD_WRITEABLE
, déclenchera un SecurityException
.
Il y avait une certaine SharedPreferences
utilisation de l'API qui utilisait MODE_WORLD_READABLE
dans com/google/android/gms/ads/identifier/AdvertisingIdClient.smali
. C'était très simple à corriger, puisqu'il s'agissait de passer de MODE_WORLD_READABLE
( 0x1
) à MODE_PRIVATE
( 0x0
).
--- a/smali/com/google/android/gms/ads/identifier/AdvertisingIdClient.smali
+++ b/smali/com/google/android/gms/ads/identifier/AdvertisingIdClient.smali
@@ -93,7 +93,7 @@
const-string v4, "google_ads_flags"
- const/4 v5, 0x1
+ const/4 v5, 0x0
invoke-virtual {v2, v4, v5}, Landroid/content/Context;->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;
Avec Android 6.0, nous avons supprimé la prise en charge du client HTTP Apache. À partir d'Android 9, cette bibliothèque est supprimée du bootclasspath et n'est pas disponible pour les applications par défaut.
NY Waterway utilisait la version Android du client HTTP Apache, mais le correctif est assez simple - juste une autre modification du fichier AndroidManifest.xml
.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 1490d73..39ccbf3 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -16,6 +16,7 @@
<permission android:name="co.bytemark.nywaterway.permission.C2D_MESSAGE" android:protectionLevel="signature"/>
<uses-permission android:name="co.bytemark.nywaterway.permission.C2D_MESSAGE"/>
<application android:allowBackup="false" android:icon="@drawable/icon" android:label="@string/app_name" android:name="co.bytemark.nywaterway2.core.NYWWApp" android:theme="@style/AppTheme">
+ <uses-library android:name="org.apache.http.legacy" android:required="false" />
<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version"/>
<receiver android:exported="false" android:label="NetworkConnection" android:name="co.bytemark.android.sdk.BytemarkSDK$ConnectionChangeReceiver">
<intent-filter>
Si votre application cible Android 9 ou supérieur, la isCleartextTrafficPermitted()
méthode est renvoyée false
par défaut. Si votre application doit activer le texte en clair pour des domaines spécifiques, vous devez définir explicitement cleartextTrafficPermitted
sur true
pour ces domaines dans la configuration de sécurité réseau de votre application.
Les requêtes réseau échouaient en raison de cette nouvelle fonctionnalité de sécurité. Le moyen le plus simple de rendre l'application compatible était simplement une autre modification de l' AndroidManifest.xml
ajout de l' android:usesCleartextTraffic="true"
attribut.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 39ccbf3..69b4aa7 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -15,7 +15,7 @@
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<permission android:name="co.bytemark.nywaterway.permission.C2D_MESSAGE" android:protectionLevel="signature"/>
<uses-permission android:name="co.bytemark.nywaterway.permission.C2D_MESSAGE"/>
- <application android:allowBackup="false" android:icon="@drawable/icon" android:label="@string/app_name" android:name="co.bytemark.nywaterway2.core.NYWWApp" android:theme="@style/AppTheme">
+ <application android:allowBackup="false" android:icon="@drawable/icon" android:label="@string/app_name" android:name="co.bytemark.nywaterway2.core.NYWWApp" android:theme="@style/AppTheme" android:usesCleartextTraffic="true">
<uses-library android:name="org.apache.http.legacy" android:required="false" />
<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version"/>
<receiver android:exported="false" android:label="NetworkConnection" android:name="co.bytemark.android.sdk.BytemarkSDK$ConnectionChangeReceiver">
Après avoir apporté toutes les modifications ci-dessus, l'application s'exécute avec succès sans aucune fenêtre contextuelle indiquant qu'elle a été conçue pour une ancienne version d'Android !
De manière quelque peu inattendue, le faire fonctionner avec la nouvelle version du SDK cible était beaucoup plus compliqué que de résoudre le problème 64 bits, mais en fin de compte, tout n'est que du code et le code n'a rien à craindre…
Vous voulez vous connecter ? Envoyez-moi un message sur Twitter ou LinkedIn !