Você deve ter 64 bits para andar nesta balsa

Nov 25 2022
Engenharia reversa de um aplicativo NY Waterway atualizado para o Pixel 7 TLDR: Se você tiver um dispositivo Android mais recente que não permite a instalação do NY Waterway, você pode baixar minha versão modificada do aplicativo. Você deve sempre ter cuidado ao instalar aplicativos aleatórios, especialmente de fontes diferentes da Play Store oficial - como esta postagem do Medium de um cara aleatório de quem você nunca ouviu falar.

Engenharia reversa de um aplicativo NY Waterway atualizado para o Pixel 7

Foto de Maxwell Ridgeway no Unsplash

TLDR : Se você possui um dispositivo Android mais recente que não permite a instalação do NY Waterway, você pode baixar minha versão modificada do aplicativo . Você deve sempre ter cuidado ao instalar aplicativos aleatórios, especialmente de fontes diferentes da Play Store oficial - como esta postagem do Medium de um cara aleatório de quem você nunca ouviu falar. Se você quiser ser mais cauteloso, pode ler adiante para ver como o APK foi modificado (e até repetir as etapas se quiser).

Em 2019, o Google tornou o suporte de 64 bits necessário para todos os aplicativos novos e atualizados na Play Store. A partir de agosto de 2021, os aplicativos que não suportam a arquitetura de 64 bits ficaram indisponíveis na Play Store para dispositivos compatíveis com 64 bits. Notavelmente, os novos Pixel 7 e Pixel 7 Pro não suportam a instalação de aplicativos somente de 32 bits .

Para os nova-iorquinos que viajam na balsa do rio Hudson, isso é bastante inconveniente porque o aplicativo que fornece bilhetes eletrônicos em seu telefone, o NY Waterway , é realmente antigo . Foi publicado pela última vez em junho de 2018 e contém bibliotecas nativas apenas para arquiteturas de 32 bits… Portanto, para usuários dos novos dispositivos Pixel, nada de bilhetes eletrônicos na balsa do rio Hudson para você!

Mudei para o iPhone há muitos anos, mas quando eu era um usuário do Android, costumava mexer muito com o sistema operacional e os aplicativos - instalando ROMs personalizados e descompilando aplicativos. Um amigo próximo comprou o novo Pixel 7 Pro e pega a balsa do rio Hudson o tempo todo, então ele me incentivou, brincando, a consertar esse aplicativo para ele. Aqui vamos nós!

Perscrutando o aplicativo

Vamos começar inspecionando o aplicativo NY Waterway para identificar as partes que são apenas de 32 bits, que estão impedindo sua instalação. Usando apktool, podemos extrair o aplicativo Android e inspecionar seu 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

Compatibilidade de 64 bits e bibliotecas nativas

Os aplicativos Android geralmente são escritos em Java ou Kotlin, ambas as linguagens voltadas para a Java Virtual Machine, que é uma abstração de alto nível que geralmente protege você de preocupações sobre compatibilidade específica da plataforma. No entanto, você pode usar a Java Native Interface (JNI) para chamar o código nativo específico da plataforma (geralmente compilado de linguagens de nível inferior, como C ou C++). Se olharmos para o libsdiretório, podemos ver as bibliotecas nativas incluídas no aplicativo 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

Outra observação aqui é que armeabie x86tem quatro bibliotecas enquanto armeabi-v7atem apenas uma. Para que uma biblioteca seja carregada pelo aplicativo Android, ela teria que chamar java.lang.System.loadLibraryou java.lang.Runtime.loadLibrary. Pesquisar o código Smali por “loadLibrary” revela apenas um local onde está carregando 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

Precisamos de uma compilação ARM de 64 bits libsqlcipher.sopara lib/arm64-v8atornar o aplicativo compatível com os novos dispositivos Pixel. Convenientemente, SQLCipher é uma biblioteca de código aberto . Observando o código cola de alto nível para interagir com a biblioteca nativa do sqlcipher, podemos ver a versão da biblioteca que foi usada.

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

Atualizando SQLCipher para v3.5.5

O processo de atualização envolverá a substituição do código SQLCipher Smali e das bibliotecas nativas pelo código da versão mais recente. Isso causaria problemas se a superfície da API pública do SQLCipher mudasse significativamente (por exemplo, se uma função pública usada pelo NY Waterway mudasse de assinatura ou fosse removida, substituí-la pela versão mais recente causaria problemas). Fazendo uma varredura rápida das mudanças de v3.5.4 para v3.5.5, não parece ser um problema que aparecerá aqui. Eu baixei o arquivo AAR para SQLCipher v3.5.5 e usei unzippara extraí-lo.

$ 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

O Android SDK fornece uma ferramenta de linha de comando chamada d8que pode compilar um jararquivo para o código de bytes do Android ( classes.dexarquivo). Depois, há outra ferramenta chamada baksmalique pode descompilar dexarquivos em smali. Combinando as etapas:

$ 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

Agora, podemos recriar o aplicativo e assiná-lo, para que possa ser instalado em um 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

Ele corre! No entanto, temos esse pop-up irritante

Aumentando a versão do SDK de destino

Para se livrar desse pop-up indicando que o aplicativo foi criado para uma versão mais antiga do Android, precisamos aumentar a versão do SDK de destino em apktool.yml. Aplicativos que visam a versão SDK <31 não são mais aceitos na Play Store, então optei por aumentá-lo para isso.

A segmentação de uma versão mais recente do Android SDK pode exigir alterações de código porque as APIs obsoletas ficam indisponíveis em versões mais recentes do SDK. O NY Waterway requer várias alterações para direcionar o SDK v31.

Exportação de componentes mais segura

Se seu aplicativo for direcionado ao Android 12 ou superior e contiver atividades, serviços ou receptores de transmissão que usam filtros de intenção, você deverá declarar explicitamente o android:exportedatributo para esses componentes do aplicativo.

Existem algumas atividades e um receptor que possuem <intent-filter>s e requerem a inclusão de um android:exported="true"atributo em AndroidManifest.xml.

Mutabilidade de intents pendentes

Se seu aplicativo for direcionado ao Android 12, você deverá especificar a mutabilidade de cada PendingIntentobjeto que seu aplicativo criar. Esse requisito adicional melhora a segurança do seu aplicativo.

Este é mais complicado, porque exige que alteremos o código real (em oposição à configuração do projeto ou à cópia de uma versão atualizada da biblioteca).

Sempre que um PendingIntentobjeto é criado, ele precisa especificar explicitamente FLAG_MUTABLEou FLAG_IMMUTABLE. Em versões anteriores do SDK, FLAG_MUTABLEera o padrão se nenhum sinalizador fosse especificado. PendingIntentos objetos são criados por um conjunto de métodos estáticos na classe: getActivity, getActivities, getBroadcastou getService. Podemos começar procurando invocações dessas funções.

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

Compreendendo Smali

A invoke-staticinstrução de código de byte leva uma lista de registradores a serem passados ​​como parâmetros para a função estática. O símbolo da função estática se parece com Landroid/app/PendingIntent;->getBroadcast(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;uma tradução direta do nome de classe totalmente qualificado e da assinatura da função. Começa com o nome da classe Landroid/app/PendingIntent;(ou android.app.PendingIntentna sintaxe Java normal). Em seguida, o nome da função ( ->getBroadcast) junto com os parâmetros e o tipo de retorno. Landroid/content/Context;ILandroid/content/Intent;Isão os parâmetros, que podem ser divididos em quatro parâmetros: Landroid/content/Context;( android.content.Context), I( int), Landroid/content/Intent;( android.content.Intent) e I( int). Por fim, após o parêntese de fechamento está o tipo de retorno: Landroid/app/PendingIntent;.

Portanto, invoke-static {v1, v2, v3, v4}da função acima passaria v1como o Context, v2como o primeiro int, v3como o Intente v4como o int. Para essas PendingIntentAPIs, flagseles são sempre o último parâmetro ( int), então só precisamos garantir que o valor sempre tenha um FLAG_MUTABLEou FLAG_IMMUTABLEdefinido. A documentação do Android SDK revela que o valor de FLAG_MUTABLEis 0x02000000e FLAG_IMMUTABLEis 0x04000000. Na maioria dos casos, o último parâmetro é especificado como um registro de variável local ( v#) que foi inicializado com um valor constante (como const/high16 v3, 0x8000000ou const/4 v4, 0x0). Nesses casos, podemos verificar trivialmente FLAG_MUTABLEseFLAG_IMMUTABLEestá definido e atualize a constante se não estiver.

-    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

Alterações de permissão do sistema de arquivos

As permissões de arquivo de arquivos privados não devem mais ser relaxadas pelo proprietário, e uma tentativa de fazer isso usando MODE_WORLD_READABLEe/ou MODE_WORLD_WRITEABLE, acionará um SecurityException.

Houve algum SharedPreferencesuso de API que estava usando MODE_WORLD_READABLEno com/google/android/gms/ads/identifier/AdvertisingIdClient.smali. Isso foi muito simples de corrigir, pois era uma questão de mudar para MODE_WORLD_READABLE( 0x1) para 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;

Com o Android 6.0, removemos o suporte para o cliente Apache HTTP. A partir do Android 9, essa biblioteca é removida do bootclasspath e não está disponível para aplicativos por padrão.

O NY Waterway estava usando a versão Android do cliente Apache HTTP, mas a correção para isso é bem simples - apenas outra alteração no arquivo 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 seu aplicativo for direcionado ao Android 9 ou superior, o isCleartextTrafficPermitted()método retornará falsepor padrão. Se seu aplicativo precisar habilitar texto não criptografado para domínios específicos, você deve definir explicitamente cleartextTrafficPermittedpara trueesses domínios na configuração de segurança de rede do seu aplicativo.

As solicitações de rede estavam falhando devido a esse novo recurso de segurança. A maneira mais simples de tornar o aplicativo compatível era apenas mais uma alteração AndroidManifest.xmlpara adicionar o 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">

Depois de fazer todas as alterações acima, o aplicativo é executado com sucesso sem nenhum pop-up irritante que foi criado para uma versão mais antiga do Android!

Um tanto inesperadamente, fazê-lo funcionar com a versão mais recente do SDK de destino foi muito mais complicado do que corrigir o problema de 64 bits, mas no final do dia, tudo é apenas código e código não é nada para se temer…

Quer se conectar? Envie-me uma mensagem no Twitter ou LinkedIn !