Sie müssen 64-Bit sein, um mit dieser Fähre zu fahren
Reverse Engineering einer aktualisierten NY Waterway-App für das Pixel 7
TLDR : Wenn Sie ein neueres Android-Gerät haben, auf dem Sie NY Waterway nicht installieren können, können Sie meine modifizierte Version der Anwendung herunterladen . Sie sollten immer vorsichtig sein, zufällige Anwendungen zu installieren, insbesondere aus anderen Quellen als dem offiziellen Play Store – wie diesem Medium-Beitrag von einem zufälligen Typen, von dem Sie noch nie gehört haben. Wenn Sie besonders vorsichtig sein möchten, können Sie im Voraus lesen, wie das APK geändert wurde (und die Schritte sogar selbst wiederholen, wenn Sie möchten).
Im Jahr 2019 hat Google die 64-Bit-Unterstützung für alle neuen und aktualisierten Anwendungen im Play Store erforderlich gemacht. Ab August 2021 sind Anwendungen, die die 64-Bit-Architektur nicht unterstützen, im Play Store für 64-Bit-fähige Geräte nicht mehr verfügbar. Insbesondere das neue Pixel 7 und Pixel 7 Pro unterstützen die Installation von reinen 32-Bit-Anwendungen überhaupt nicht .
Für New Yorker, die mit der Hudson River-Fähre fahren, ist dies ziemlich unpraktisch, da die Anwendung NY Waterway , die elektronische Tickets auf Ihrem Telefon bereitstellt , wirklich alt ist . Es wurde zuletzt im Juni 2018 veröffentlicht und enthält native Bibliotheken nur für 32-Bit-Architekturen ... Daher für Benutzer der neuen Pixel-Geräte keine elektronischen Tickets für die Hudson River-Fähre für Sie!
Ich bin vor vielen Jahren auf das iPhone umgestiegen, aber damals, als ich ein Android-Benutzer war, habe ich viel mit dem Betriebssystem und den Anwendungen rumgespielt – benutzerdefinierte ROMs installiert und Anwendungen dekompiliert. Ein enger Freund von mir hat das neue Pixel 7 Pro und fährt die ganze Zeit mit der Hudson River-Fähre, also hat er mich scherzhaft dazu gedrängt, diese App für ihn zu reparieren. Auf geht's!
Peering in die Anwendung
Beginnen wir damit, die NY Waterway-Anwendung zu untersuchen, um die Teile zu identifizieren, die nur 32-Bit sind und die Installation verhindern. Mit apktool
können wir die Android-Anwendung extrahieren und ihren Code untersuchen.
$ 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
64-Bit-Kompatibilität und native Bibliotheken
Android-Anwendungen sind in der Regel in Java oder Kotlin geschrieben, beides Sprachen, die auf die Java Virtual Machine abzielen, bei der es sich um eine Abstraktion auf hoher Ebene handelt, die Sie im Allgemeinen vor Bedenken hinsichtlich der plattformspezifischen Kompatibilität schützt. Sie können jedoch das Java Native Interface (JNI) verwenden, um nativen, plattformspezifischen Code aufzurufen (normalerweise aus niedrigeren Sprachen wie C oder C++ kompiliert). Wenn wir uns das libs
Verzeichnis ansehen, können wir die nativen Bibliotheken sehen, die in der NY Waterway-App enthalten sind.
$ 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
Eine weitere Beobachtung hier ist, dass armeabi
und x86
vier Bibliotheken haben, während sie armeabi-v7a
nur eine haben. Damit eine Bibliothek von der Android-App geladen werden kann, müsste sie java.lang.System.loadLibrary
oder aufrufen java.lang.Runtime.loadLibrary
. Die Suche im Smali-Code nach „loadLibrary“ zeigt nur eine Stelle, an der native Bibliotheken geladen werden.
$ 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
Wir benötigen einen 64-Bit-ARM-Build libsqlcipher.so
, lib/arm64-v8a
um die Anwendung mit den neuen Pixel-Geräten kompatibel zu machen. Praktischerweise ist SQLCipher eine Open-Source-Bibliothek . Wenn wir uns den High-Level-Glue-Code für die Interaktion mit der nativen sqlcipher-Bibliothek ansehen, können wir die Version der verwendeten Bibliothek sehen.
$ 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"
Upgrade von SQLCipher auf v3.5.5
Der Aktualisierungsprozess umfasst das Ersetzen des SQLCipher-Smali-Codes und der nativen Bibliotheken durch den Code der neueren Version. Dies würde zu Problemen führen, wenn sich die öffentliche API-Oberfläche von SQLCipher erheblich ändert (wenn beispielsweise eine von NY Waterway verwendete öffentliche Funktion entweder die Signatur geändert oder entfernt wurde, würde das Ersetzen durch die neuere Version Probleme verursachen). Wenn Sie einen schnellen Scan der Änderungen von v3.5.4 zu v3.5.5 durchführen, scheint es kein Problem zu sein, das hier auftreten wird. Ich habe die AAR-Datei für SQLCipher v3.5.5 heruntergeladen und dann unzip
zum Extrahieren verwendet.
$ 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
Das Android SDK stellt ein sogenanntes Kommandozeilen-Tool bereit d8
, das eine jar
Datei zu Android-Bytecode ( classes.dex
Datei) kompilieren kann. Dann gibt es noch ein weiteres Tool namens baksmali
, das dex
Dateien in smali
. Kombinieren Sie die Schritte miteinander:
$ 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
Jetzt können wir die Anwendung neu erstellen und signieren, damit sie auf einem Gerät installiert werden kann!
$ 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
Erhöhen der Ziel-SDK-Version
Um dieses Popup loszuwerden, das darauf hinweist, dass die Anwendung für eine ältere Version von Android erstellt wurde, müssen wir die Ziel-SDK-Version in apktool.yml
. Anwendungen, die auf die SDK-Version <31 abzielen, werden im Play Store nicht mehr akzeptiert, daher habe ich mich entschieden, sie darauf zu erhöhen.
Das Targeting einer neueren Version des Android SDK erfordert möglicherweise Codeänderungen, da veraltete APIs in neueren SDK-Versionen nicht mehr verfügbar sind. NY Waterway erfordert mehrere Änderungen am Ziel-SDK v31.
Sicherer Komponentenexport
Wenn Ihre App auf Android 12 oder höher ausgerichtet ist und Aktivitäten, Dienste oder Broadcast-Empfänger enthält, die Absichtsfilter verwenden, müssen Sie das android:exported
Attribut für diese App-Komponenten explizit deklarieren.
Es gibt ein paar Aktivitäten und einen Empfänger, die <intent-filter>
s haben und erfordern, dass ein android:exported="true"
Attribut in hinzugefügt wird AndroidManifest.xml
.
Veränderbarkeit der ausstehenden Absichten
Wenn Ihre App auf Android 12 ausgerichtet ist, müssen Sie die Veränderbarkeit jedes PendingIntent
Objekts angeben, das Ihre App erstellt. Diese zusätzliche Anforderung verbessert die Sicherheit Ihrer App.
Dieser ist kniffliger, da wir den eigentlichen Code ändern müssen (im Gegensatz zur Projektkonfiguration oder zum Kopieren einer aktualisierten Version der Bibliothek).
Jedes Mal, wenn ein Objekt erstellt wird, muss es explizit oder PendingIntent
angeben . In früheren SDK-Versionen war dies der Standardwert, wenn keines der Flags angegeben wurde. Objekte werden durch einen Satz statischer Methoden für die Klasse erstellt: , , , oder . Wir können damit beginnen, nach Aufrufen dieser Funktionen zu suchen.FLAG_MUTABLE
FLAG_IMMUTABLE
FLAG_MUTABLE
PendingIntent
getActivity
getActivities
getBroadcast
getService
$ 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;
Smali verstehen
Der invoke-static
Bytecode-Befehl nimmt eine Liste von Registern, die als Parameter an die statische Funktion übergeben werden. Das Symbol der statischen Funktion sieht aus wie Landroid/app/PendingIntent;->getBroadcast(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
eine direkte Übersetzung aus dem vollständig qualifizierten Klassennamen und der Signatur der Funktion. Es beginnt mit dem Klassennamen Landroid/app/PendingIntent;
(oder android.app.PendingIntent
in normaler Java-Syntax). Dann der Name der Funktion ( ->getBroadcast
) zusammen mit den Parametern und dem Rückgabetyp. Landroid/content/Context;ILandroid/content/Intent;I
sind die Parameter, die in vier Parameter aufgeteilt werden können: Landroid/content/Context;
( android.content.Context
), I
( int
), Landroid/content/Intent;
( android.content.Intent
) und I
( int
). Schließlich steht nach der schließenden Klammer der Rückgabetyp: Landroid/app/PendingIntent;
.
Daher invoke-static {v1, v2, v3, v4}
würde die obige Funktion v1
als Context
, v2
als erste int
, v3
als Intent
, und v4
als int
. Für diese PendingIntent
APIs flags
sind die immer der letzte Parameter ( int
), also müssen wir nur sicherstellen, dass der Wert immer entweder FLAG_MUTABLE
oder FLAG_IMMUTABLE
gesetzt ist. Die Android SDK-Dokumentation zeigt, dass der Wert von FLAG_MUTABLE
is 0x02000000
und FLAG_IMMUTABLE
is 0x04000000
. In den meisten Fällen wird der letzte Parameter als lokales Variablenregister ( ) angegeben, das mit einem konstanten Wert (z. B. oder ) v#
initialisiert wurde . In diesen Fällen können wir trivial prüfen, ob oderconst/high16 v3, 0x8000000
const/4 v4, 0x0
FLAG_MUTABLE
FLAG_IMMUTABLE
gesetzt ist und aktualisieren Sie die Konstante, wenn dies nicht der Fall ist.
- 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
Änderungen der Dateisystemberechtigungen
Die Dateiberechtigungen privater Dateien sollten vom Besitzer nicht mehr gelockert werden, und ein Versuch, dies mit MODE_WORLD_READABLE
und/oder zu tun MODE_WORLD_WRITEABLE
, löst eine SecurityException
.
Es gab eine SharedPreferences
API-Nutzung, die MODE_WORLD_READABLE
in com/google/android/gms/ads/identifier/AdvertisingIdClient.smali
. Dies war sehr einfach zu beheben, da es darum ging, von MODE_WORLD_READABLE
( 0x1
) auf MODE_PRIVATE
( 0x0
) umzuschalten.
--- 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;
Mit Android 6.0 haben wir die Unterstützung für den Apache HTTP-Client entfernt. Ab Android 9 wird diese Bibliothek aus dem Bootclasspath entfernt und steht Apps standardmäßig nicht zur Verfügung.
NY Waterway verwendete die Android-Version des Apache HTTP-Clients, aber die Lösung dafür ist ziemlich einfach – nur eine weitere Änderung an der 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>
Wenn Ihre App auf Android 9 oder höher ausgerichtet ist, gibt die isCleartextTrafficPermitted()
Methode false
standardmäßig zurück. Wenn Ihre App Klartext für bestimmte Domänen aktivieren muss, müssen Sie dies explizit cleartextTrafficPermitted
für true
diese Domänen in der Netzwerksicherheitskonfiguration Ihrer App festlegen.
Netzwerkanfragen schlugen aufgrund dieser neuen Sicherheitsfunktion fehl. Der einfachste Weg, die Anwendung kompatibel zu machen, war nur eine weitere Änderung an , um das Attribut AndroidManifest.xml
hinzuzufügen .android:usesCleartextTraffic="true"
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">
Nachdem Sie alle oben genannten Änderungen vorgenommen haben, läuft die Anwendung erfolgreich ohne nörgelndes Popup, dass sie für eine ältere Version von Android erstellt wurde!
Etwas unerwartet war es viel aufwändiger, es mit der neueren Ziel-SDK-Version zum Laufen zu bringen, als das 64-Bit-Problem tatsächlich zu beheben, aber am Ende des Tages ist alles nur Code und Code ist nichts, wovor man sich fürchten muss …
Möchten Sie sich verbinden? Senden Sie mir eine Nachricht auf Twitter oder LinkedIn !