Devi essere a 64 bit per guidare questo traghetto
Reverse engineering di un'app NY Waterway aggiornata per Pixel 7
TLDR : se disponi di un dispositivo Android più recente che non ti consente di installare NY Waterway, puoi scaricare la mia versione modificata dell'applicazione . Dovresti sempre fare attenzione a installare applicazioni casuali, soprattutto da fonti diverse dal Play Store ufficiale, come questo post di Medium di un ragazzo a caso di cui non hai mai sentito parlare. Se vuoi essere più cauto, puoi leggere in anticipo per vedere come è stato modificato l'APK (e anche ripetere tu stesso i passaggi se lo desideri).
Nel 2019, Google ha reso necessario il supporto a 64 bit per tutte le applicazioni nuove e aggiornate nel Play Store. A partire da agosto 2021, le applicazioni che non supportano l'architettura a 64 bit non sono più disponibili nel Play Store per i dispositivi a 64 bit. In particolare, i nuovi Pixel 7 e Pixel 7 Pro non supportano affatto l'installazione di sole applicazioni a 32 bit .
Per i newyorkesi che viaggiano sul traghetto del fiume Hudson, questo è abbastanza scomodo perché l'applicazione che fornisce i biglietti elettronici sul telefono, NY Waterway , è davvero vecchia . È stato pubblicato l'ultima volta a giugno 2018 e contiene librerie native solo per architetture a 32 bit... Pertanto, per gli utenti dei nuovi dispositivi Pixel, niente biglietti elettronici per il traghetto sul fiume Hudson per te!
Sono passato a iPhone molti anni fa ormai, ma quando ero un utente Android, ero solito hackerare molto con il sistema operativo e le applicazioni, installando ROM personalizzate e decompilando applicazioni. Un mio caro amico ha preso il nuovo Pixel 7 Pro e prende sempre il traghetto sul fiume Hudson, quindi mi ha scherzosamente spronato a riparare questa app per lui. Eccoci qui!
Peering nell'applicazione
Iniziamo ispezionando l'applicazione NY Waterway per identificare le parti che sono solo a 32 bit, che ne impediscono l'installazione. Usando apktool
, possiamo estrarre l'applicazione Android e ispezionarne il codice.
$ 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à a 64 bit e librerie native
Le applicazioni Android sono in genere scritte in Java o Kotlin, entrambi i linguaggi che prendono di mira la Java Virtual Machine, che è un'astrazione di alto livello che generalmente ti protegge dalle preoccupazioni sulla compatibilità specifica della piattaforma. Tuttavia, puoi utilizzare Java Native Interface (JNI) per richiamare codice nativo specifico della piattaforma (in genere compilato da linguaggi di livello inferiore come C o C++). Se guardiamo la libs
directory, possiamo vedere le librerie native incluse nell'app 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
Un'altra osservazione qui è che armeabi
e x86
ho quattro librerie mentre armeabi-v7a
ne ha solo una. Affinché una libreria venga caricata dall'app Android, dovrebbe chiamare in java.lang.System.loadLibrary
o java.lang.Runtime.loadLibrary
. La ricerca nel codice Smali per "loadLibrary" rivela solo un punto in cui sta caricando le librerie native.
$ 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
libsqlcipher.so
Abbiamo bisogno di una build ARM a 64 bit lib/arm64-v8a
per rendere l'applicazione compatibile con i nuovi dispositivi Pixel. Convenientemente, SQLCipher è una libreria open source . Osservando il codice collante di alto livello per l'interazione con la libreria sqlcipher nativa, possiamo vedere la versione della libreria utilizzata.
$ 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"
Aggiornamento di SQLCipher alla versione 3.5.5
Il processo di aggiornamento comporterà la sostituzione del codice SQLCipher Smali e delle librerie native con il codice della versione più recente. Ciò causerebbe problemi se la superficie dell'API pubblica di SQLCipher cambiasse in modo significativo (ad esempio, se una funzione pubblica utilizzata da NY Waterway cambiasse la firma o venisse rimossa, sostituirla con la versione più recente causerebbe problemi). Facendo una rapida scansione delle modifiche dalla v3.5.4 alla v3.5.5, non sembra un problema che apparirà qui. Ho scaricato il file AAR per SQLCipher v3.5.5 e poi l'ho usato unzip
per estrarlo.
$ 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
Android SDK fornisce uno strumento da riga di comando chiamato d8
che può compilare un jar
file in bytecode Android ( classes.dex
file). Poi c'è un altro strumento chiamato baksmali
che può decompilare dex
i file in smali
. Combinando i passaggi insieme:
$ 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
Ora possiamo ricostruire l'applicazione e firmarla, in modo che possa essere installata su un dispositivo!
$ 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
Aumento della versione dell'SDK di destinazione
Per eliminare questo popup che indica che l'applicazione è stata creata per una versione precedente di Android, dobbiamo aumentare la versione dell'SDK di destinazione in apktool.yml
. Le applicazioni che hanno come target la versione dell'SDK <31 non sono più accettate nel Play Store, quindi ho scelto di aumentarlo.
Scegliere come target una versione più recente di Android SDK potrebbe richiedere modifiche al codice perché le API obsolete non sono più disponibili nelle versioni più recenti dell'SDK. NY Waterway richiede diverse modifiche per indirizzare l'SDK v31.
Esportazione di componenti più sicura
Se la tua app ha come target Android 12 o versioni successive e contiene attività, servizi o ricevitori di trasmissione che utilizzano filtri di intent, devi dichiarare in modo esplicito l' android:exported
attributo per questi componenti dell'app.
Ci sono un paio di attività e un ricevitore che hanno se <intent-filter>
richiedono l' android:exported="true"
aggiunta di un attributo in AndroidManifest.xml
.
In attesa di mutabilità degli intenti
Se la tua app ha come target Android 12, devi specificare la mutabilità di ogni PendingIntent
oggetto creato dall'app. Questo requisito aggiuntivo migliora la sicurezza dell'app.
Questo è più complicato, perché ci richiede di modificare il codice effettivo (al contrario della configurazione del progetto o della copia di una versione aggiornata della libreria).
Ogni volta PendingIntent
che viene creato un oggetto, è necessario specificare esplicitamente FLAG_MUTABLE
o FLAG_IMMUTABLE
. Nelle versioni precedenti dell'SDK, FLAG_MUTABLE
era l'impostazione predefinita se non veniva specificato alcun flag. PendingIntent
gli oggetti vengono creati da un insieme di metodi statici sulla classe: getActivity
, getActivities
, getBroadcast
o getService
. Possiamo iniziare cercando le invocazioni di quelle funzioni.
$ 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;
Capire Smali
L' invoke-static
istruzione bytecode accetta un elenco di registri da passare come parametri nella funzione statica. Il simbolo della funzione statica ha l'aspetto di Landroid/app/PendingIntent;->getBroadcast(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
una traduzione diretta dal nome completo della classe e dalla firma della funzione. Inizia con il nome della classe Landroid/app/PendingIntent;
(o android.app.PendingIntent
nella normale sintassi Java). Quindi il nome della funzione ( ->getBroadcast
) insieme ai parametri e al tipo restituito. Landroid/content/Context;ILandroid/content/Intent;I
sono i parametri, che possono essere suddivisi in quattro parametri: Landroid/content/Context;
( android.content.Context
), I
( int
), Landroid/content/Intent;
( android.content.Intent
) e I
( int
). Infine, dopo la parentesi di chiusura c'è il tipo restituito: Landroid/app/PendingIntent;
.
Pertanto, invoke-static {v1, v2, v3, v4}
della funzione precedente passerebbe v1
come Context
, v2
come prima int
, v3
come Intent
, e v4
come int
. Per queste PendingIntent
API, flags
sono sempre l'ultimo parametro ( int
), quindi dobbiamo solo assicurarci che il valore abbia sempre FLAG_MUTABLE
o FLAG_IMMUTABLE
impostato. La documentazione dell'SDK di Android rivela che il valore di FLAG_MUTABLE
è 0x02000000
ed FLAG_IMMUTABLE
è 0x04000000
. Nella maggior parte dei casi, l'ultimo parametro è specificato come una variabile locale register ( v#
) che è stata inizializzata con un valore costante (come const/high16 v3, 0x8000000
o const/4 v4, 0x0
). In questi casi, possiamo banalmente verificare se FLAG_MUTABLE
oFLAG_IMMUTABLE
è impostato e aggiorna la costante se non lo è.
- 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
Modifiche alle autorizzazioni del file system
Le autorizzazioni sui file dei file privati non dovrebbero più essere allentate dal proprietario e un tentativo di farlo utilizzando MODE_WORLD_READABLE
e/o MODE_WORLD_WRITEABLE
, attiverà un file SecurityException
.
C'era un SharedPreferences
utilizzo dell'API che veniva utilizzato MODE_WORLD_READABLE
in com/google/android/gms/ads/identifier/AdvertisingIdClient.smali
. Questo è stato molto semplice da risolvere, poiché si trattava di passare da MODE_WORLD_READABLE
( 0x1
) a 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;
Con Android 6.0, abbiamo rimosso il supporto per il client HTTP Apache. A partire da Android 9, quella libreria viene rimossa dal bootclasspath e non è disponibile per le app per impostazione predefinita.
NY Waterway utilizzava la versione Android del client HTTP Apache, ma la soluzione è piuttosto semplice: solo un'altra modifica al file 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>
Se la tua app ha come target Android 9 o versioni successive, il isCleartextTrafficPermitted()
metodo viene restituito false
per impostazione predefinita. Se la tua app deve abilitare il testo non crittografato per domini specifici, devi impostarlo cleartextTrafficPermitted
in modo esplicito true
per tali domini nella configurazione della sicurezza di rete della tua app.
Le richieste di rete non riuscivano a causa di questa nuova funzionalità di sicurezza. Il modo più semplice per rendere compatibile l'applicazione era solo un'altra modifica AndroidManifest.xml
per aggiungere l' android:usesCleartextTraffic="true"
attributo.
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">
Dopo aver apportato tutte le modifiche di cui sopra, l'applicazione viene eseguita correttamente senza fastidiosi popup che è stata creata per una versione precedente di Android!
In qualche modo inaspettatamente, farlo funzionare con la versione più recente dell'SDK di destinazione è stato molto più complicato che risolvere effettivamente il problema a 64 bit, ma alla fine, tutto è solo codice e il codice non è niente di cui aver paura...
Vuoi connetterti? Inviami un messaggio su Twitter o LinkedIn !