Vous devez être 64 bits pour monter sur ce ferry

Nov 25 2022
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.

Inverser l'ingénierie d'une application NY Waterway mise à jour pour le Pixel 7

Photo de Maxwell Ridgeway sur Unsplash

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 libsré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 armeabiet x86avoir quatre bibliothèques alors qu'il armeabi-v7an'en a qu'une. Pour qu'une bibliothèque soit chargée par l'application Android, elle doit appeler java.lang.System.loadLibraryou 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.sopour lib/arm64-v8arendre 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é unzippour 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é d8qui peut compiler un jarfichier en code binaire Android ( classes.dexfichier). Ensuite, il existe un autre outil appelé baksmaliqui peut décompiler dexles 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

Ça tourne ! Nous avons cependant ce popup harcelant

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:exportedattribut 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 PendingIntentobjet 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 PendingIntentobjet est créé, il doit spécifier explicitement FLAG_MUTABLEou 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é. PendingIntentles objets sont créés par un ensemble de méthodes statiques sur la classe : getActivity, getActivities, getBroadcastou 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-staticinstruction 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.PendingIntentdans 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;Isont 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 v1pour le Context, v2pour le premier int, v3pour le Intentet v4pour le int. Pour ces PendingIntentAPI, les flagssont toujours le dernier paramètre ( int) donc nous avons juste besoin de nous assurer que la valeur a toujours l'un FLAG_MUTABLEou l'autre FLAG_IMMUTABLE. La documentation du SDK Android révèle que la valeur de FLAG_MUTABLEest 0x02000000et FLAG_IMMUTABLEest 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, 0x8000000ou const/4 v4, 0x0). Dans ces cas, on peut trivialement vérifier si FLAG_MUTABLEouFLAG_IMMUTABLEest 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_READABLEet/ou MODE_WORLD_WRITEABLE, déclenchera un SecurityException.

Il y avait une certaine SharedPreferencesutilisation de l'API qui utilisait MODE_WORLD_READABLEdans 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 falsepar défaut. Si votre application doit activer le texte en clair pour des domaines spécifiques, vous devez définir explicitement cleartextTrafficPermittedsur truepour 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.xmlajout 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 !