Debes tener 64 bits para viajar en este ferry

Nov 25 2022
Ingeniería inversa de una aplicación NY Waterway actualizada para Pixel 7 TLDR: si tiene un dispositivo Android más nuevo que no le permite instalar NY Waterway, puede descargar mi versión modificada de la aplicación. Siempre debe tener cuidado al instalar aplicaciones aleatorias, especialmente de fuentes distintas a la Play Store oficial, como esta publicación de Medium de un tipo al azar del que nunca ha oído hablar.

Ingeniería inversa en una aplicación NY Waterway actualizada para Pixel 7

Foto de Maxwell Ridgeway en Unsplash

TLDR : si tiene un dispositivo Android más nuevo que no le permite instalar NY Waterway, puede descargar mi versión modificada de la aplicación . Siempre debe tener cuidado al instalar aplicaciones aleatorias, especialmente de fuentes distintas a la Play Store oficial, como esta publicación de Medium de un tipo al azar del que nunca ha oído hablar. Si desea ser más cauteloso, puede seguir leyendo para ver cómo se modificó el APK (e incluso repetir los pasos usted mismo si lo desea).

En 2019, Google hizo necesaria la compatibilidad con 64 bits para todas las aplicaciones nuevas y actualizadas en Play Store. A partir de agosto de 2021, las aplicaciones que no son compatibles con la arquitectura de 64 bits dejaron de estar disponibles en Play Store para dispositivos compatibles con 64 bits. En particular, los nuevos Pixel 7 y Pixel 7 Pro no admiten la instalación de aplicaciones de solo 32 bits .

Para los neoyorquinos que viajan en el ferry del río Hudson, esto es bastante inconveniente porque la aplicación que brinda boletos electrónicos en su teléfono, NY Waterway , es realmente antigua . Se publicó por última vez en junio de 2018 y contiene bibliotecas nativas solo para arquitecturas de 32 bits... Por lo tanto, para los usuarios de los nuevos dispositivos Pixel, ¡ no hay boletos electrónicos en el ferry del río Hudson para ustedes!

Cambié a iPhone hace muchos años, pero cuando era un usuario de Android, solía piratear mucho con el sistema operativo y las aplicaciones: instalaba ROM personalizadas y descompilaba aplicaciones. Un amigo cercano compró el nuevo Pixel 7 Pro y toma el ferry del río Hudson todo el tiempo, así que en broma me instó a arreglar esta aplicación para él. ¡Aquí vamos!

Mirando en la aplicación

Comencemos inspeccionando la aplicación NY Waterway para identificar las partes que son solo de 32 bits y que impiden que se instale. Con apktool, podemos extraer la aplicación de Android e inspeccionar su código.

$ 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

Compatibilidad de 64 bits y bibliotecas nativas

Las aplicaciones de Android generalmente se escriben en Java o Kotlin, ambos lenguajes que se dirigen a la máquina virtual de Java, que es una abstracción de alto nivel que generalmente lo protege de las preocupaciones sobre la compatibilidad específica de la plataforma. Sin embargo, puede usar la interfaz nativa de Java (JNI) para llamar a código nativo específico de la plataforma (generalmente compilado a partir de lenguajes de nivel inferior como C o C++). Si miramos el libsdirectorio, podemos ver las bibliotecas nativas incluidas en la aplicación 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

Otra observación aquí es que armeabitiene x86cuatro bibliotecas mientras que armeabi-v7asolo tiene una. Para que la aplicación de Android cargue una biblioteca, tendría que llamar a java.lang.System.loadLibraryo java.lang.Runtime.loadLibrary. La búsqueda de "loadLibrary" en el código de Smali revela solo un lugar donde está cargando bibliotecas nativas.

$ 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

Necesitamos una compilación ARM de 64 bits libsqlcipher.sopara lib/arm64-v8aque la aplicación sea compatible con los nuevos dispositivos Pixel. Convenientemente, SQLCipher es una biblioteca de código abierto . Mirando el código de enlace de alto nivel para interactuar con la biblioteca nativa de sqlcipher, podemos ver la versión de la biblioteca que se usó.

$ 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"

Actualización de SQLCipher a v3.5.5

El proceso de actualización implicará reemplazar el código SQLCipher Smali y las bibliotecas nativas con el código de la versión más reciente. Esto causaría problemas si la superficie de la API pública de SQLCipher cambiara significativamente (por ejemplo, si una función pública utilizada por NY Waterway cambiara la firma o se eliminara, entonces reemplazarla con la versión más nueva causaría problemas). Al hacer un escaneo rápido de los cambios de v3.5.4 a v3.5.5, no parece ser un problema que aparezca aquí. Descargué el archivo AAR para SQLCipher v3.5.5 y luego lo extraje unzip.

$ 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

El SDK de Android proporciona una herramienta de línea de comandos llamada d8que puede compilar un jararchivo en el código de bytes de Android ( classes.dexarchivo). Luego hay otra herramienta llamada baksmalique puede descompilar dexarchivos en archivos smali. Combinando los pasos juntos:

$ 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

Ahora, podemos reconstruir la aplicación y firmarla, ¡para que pueda instalarse en 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! Sin embargo, tenemos esta molesta ventana emergente

Aumento de la versión del SDK de destino

Para deshacernos de esta ventana emergente que indica que la aplicación se creó para una versión anterior de Android, debemos aumentar la versión del SDK de destino en apktool.yml. Las aplicaciones destinadas a la versión SDK <31 ya no se aceptan en Play Store, así que opté por aumentarla.

La orientación a una versión más reciente del SDK de Android puede requerir cambios en el código porque las API en desuso dejan de estar disponibles en las versiones más nuevas del SDK. NY Waterway requiere varios cambios para apuntar al SDK v31.

Exportación de componentes más segura

Si su aplicación tiene como objetivo Android 12 o superior y contiene actividades, servicios o receptores de transmisión que usan filtros de intención, debe declarar explícitamente el android:exportedatributo para estos componentes de la aplicación.

Hay un par de actividades y un receptor que tienen <intent-filter>s y requieren android:exported="true"que se agregue un atributo en AndroidManifest.xml.

Mutabilidad de intentos pendientes

Si su aplicación tiene como objetivo Android 12, debe especificar la mutabilidad de cada PendingIntentobjeto que crea su aplicación. Este requisito adicional mejora la seguridad de su aplicación.

Este es más complicado, porque requiere que cambiemos el código real (a diferencia de la configuración del proyecto o la copia de una versión mejorada de la biblioteca).

Cada vez PendingIntentque se crea un objeto, debe especificar explícitamente FLAG_MUTABLEo FLAG_IMMUTABLE. En versiones anteriores del SDK, FLAG_MUTABLEera el valor predeterminado si no se especificaba ningún indicador. PendingIntentlos objetos son creados por un conjunto de métodos estáticos en la clase: getActivity, getActivities, getBroadcasto getService. Podemos comenzar buscando invocaciones de esas funciones.

$ 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;

Entendiendo a Smali

La invoke-staticinstrucción de código de bytes toma una lista de registros para pasar como parámetros a la función estática. El símbolo de la función estática parece Landroid/app/PendingIntent;->getBroadcast(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;una traducción directa del nombre de clase completo y la firma de la función. Comienza con el nombre de la clase Landroid/app/PendingIntent;(o android.app.PendingIntenten la sintaxis normal de Java). Luego el nombre de la función ( ->getBroadcast) junto con los parámetros y el tipo de retorno. Landroid/content/Context;ILandroid/content/Intent;Ison los parámetros, que se pueden dividir en cuatro parámetros: Landroid/content/Context;( android.content.Context), I( int), Landroid/content/Intent;( android.content.Intent) y I( int). Finalmente, después del paréntesis de cierre está el tipo de retorno: Landroid/app/PendingIntent;.

Por tanto, invoke-static {v1, v2, v3, v4}de la función anterior pasaría v1como la Context, v2como la primera int, v3como la Intent, y v4como la int. Para estas PendingIntentAPI, flagssiempre son el último parámetro ( int), por lo que solo debemos asegurarnos de que el valor siempre tenga establecido FLAG_MUTABLEo FLAG_IMMUTABLE. La documentación del SDK de Android revela que el valor de FLAG_MUTABLEes 0x02000000y FLAG_IMMUTABLEes 0x04000000. En la mayoría de los casos, el último parámetro se especifica como un registro de variable local ( v#) que se inicializó con un valor constante (como const/high16 v3, 0x8000000o const/4 v4, 0x0). En estos casos, podemos comprobar trivialmente si FLAG_MUTABLEoFLAG_IMMUTABLEestá configurado y actualice la constante si no lo está.

-    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

Cambios en los permisos del sistema de archivos

El propietario ya no debe relajar los permisos de archivo de los archivos privados, y un intento de hacerlo usando MODE_WORLD_READABLEy/o MODE_WORLD_WRITEABLEactivará un archivo SecurityException.

Hubo algún SharedPreferencesuso de API que estaba usando MODE_WORLD_READABLEen com/google/android/gms/ads/identifier/AdvertisingIdClient.smali. Esto fue muy sencillo de arreglar, ya que era cuestión de cambiar de 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, eliminamos la compatibilidad con el cliente Apache HTTP. A partir de Android 9, esa biblioteca se elimina de bootclasspath y no está disponible para las aplicaciones de forma predeterminada.

NY Waterway estaba usando la versión de Android del cliente Apache HTTP, pero la solución para esto es bastante simple: solo otro cambio en el archivo 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 su aplicación tiene como objetivo Android 9 o superior, el isCleartextTrafficPermitted()método vuelve falsede forma predeterminada. Si su aplicación necesita habilitar texto simple para dominios específicos, debe establecer explícitamente cleartextTrafficPermittedpara trueesos dominios en la Configuración de seguridad de red de su aplicación.

Las solicitudes de red fallaban debido a esta nueva función de seguridad. La forma más sencilla de hacer que la aplicación sea compatible fue simplemente otro cambio en el AndroidManifest.xmlpara agregar el android:usesCleartextTraffic="true"atributo.

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">

¡Después de realizar todos los cambios anteriores, la aplicación se ejecuta con éxito sin ventanas emergentes molestas que se crearon para una versión anterior de Android!

De manera un tanto inesperada, hacer que funcionara con la nueva versión del SDK de destino fue mucho más complicado que solucionar el problema de 64 bits, pero al final del día, todo es solo código y el código no es nada de qué asustarse...

¿Quieres conectarte? ¡Envíame un mensaje en Twitter o LinkedIn !