Devi essere a 64 bit per guidare questo traghetto

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

Reverse engineering di un'app NY Waterway aggiornata per Pixel 7

Foto di Maxwell Ridgeway su Unsplash

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 libsdirectory, 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 armeabie x86ho quattro librerie mentre armeabi-v7ane ha solo una. Affinché una libreria venga caricata dall'app Android, dovrebbe chiamare in java.lang.System.loadLibraryo 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.soAbbiamo bisogno di una build ARM a 64 bit lib/arm64-v8aper 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 unzipper 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 d8che può compilare un jarfile in bytecode Android ( classes.dexfile). Poi c'è un altro strumento chiamato baksmaliche può decompilare dexi 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

Corre! Tuttavia, abbiamo questo fastidioso popup

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:exportedattributo 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 PendingIntentoggetto 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 PendingIntentche viene creato un oggetto, è necessario specificare esplicitamente FLAG_MUTABLEo FLAG_IMMUTABLE. Nelle versioni precedenti dell'SDK, FLAG_MUTABLEera l'impostazione predefinita se non veniva specificato alcun flag. PendingIntentgli oggetti vengono creati da un insieme di metodi statici sulla classe: getActivity, getActivities, getBroadcasto 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-staticistruzione 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.PendingIntentnella normale sintassi Java). Quindi il nome della funzione ( ->getBroadcast) insieme ai parametri e al tipo restituito. Landroid/content/Context;ILandroid/content/Intent;Isono 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 v1come Context, v2come prima int, v3come Intent, e v4come int. Per queste PendingIntentAPI, flagssono sempre l'ultimo parametro ( int), quindi dobbiamo solo assicurarci che il valore abbia sempre FLAG_MUTABLEo FLAG_IMMUTABLEimpostato. La documentazione dell'SDK di Android rivela che il valore di FLAG_MUTABLEè 0x02000000ed 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, 0x8000000o const/4 v4, 0x0). In questi casi, possiamo banalmente verificare se FLAG_MUTABLEoFLAG_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_READABLEe/o MODE_WORLD_WRITEABLE, attiverà un file SecurityException.

C'era un SharedPreferencesutilizzo dell'API che veniva utilizzato MODE_WORLD_READABLEin 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 falseper impostazione predefinita. Se la tua app deve abilitare il testo non crittografato per domini specifici, devi impostarlo cleartextTrafficPermittedin modo esplicito trueper 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.xmlper 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 !