Você deve ter 64 bits para andar nesta balsa
Engenharia reversa de um aplicativo NY Waterway atualizado para o Pixel 7
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 libs
diretó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 armeabi
e x86
tem quatro bibliotecas enquanto armeabi-v7a
tem apenas uma. Para que uma biblioteca seja carregada pelo aplicativo Android, ela teria que chamar java.lang.System.loadLibrary
ou 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.so
para lib/arm64-v8a
tornar 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 unzip
para 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 d8
que pode compilar um jar
arquivo para o código de bytes do Android ( classes.dex
arquivo). Depois, há outra ferramenta chamada baksmali
que pode descompilar dex
arquivos 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
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:exported
atributo 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 PendingIntent
objeto 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 PendingIntent
objeto é criado, ele precisa especificar explicitamente FLAG_MUTABLE
ou FLAG_IMMUTABLE
. Em versões anteriores do SDK, FLAG_MUTABLE
era o padrão se nenhum sinalizador fosse especificado. PendingIntent
os objetos são criados por um conjunto de métodos estáticos na classe: getActivity
, getActivities
, getBroadcast
ou 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-static
instruçã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.PendingIntent
na 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;I
sã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 v1
como o Context
, v2
como o primeiro int
, v3
como o Intent
e v4
como o int
. Para essas PendingIntent
APIs, flags
eles são sempre o último parâmetro ( int
), então só precisamos garantir que o valor sempre tenha um FLAG_MUTABLE
ou FLAG_IMMUTABLE
definido. A documentação do Android SDK revela que o valor de FLAG_MUTABLE
is 0x02000000
e FLAG_IMMUTABLE
is 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, 0x8000000
ou const/4 v4, 0x0
). Nesses casos, podemos verificar trivialmente FLAG_MUTABLE
seFLAG_IMMUTABLE
está 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_READABLE
e/ou MODE_WORLD_WRITEABLE
, acionará um SecurityException
.
Houve algum SharedPreferences
uso de API que estava usando MODE_WORLD_READABLE
no 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á false
por padrão. Se seu aplicativo precisar habilitar texto não criptografado para domínios específicos, você deve definir explicitamente cleartextTrafficPermitted
para true
esses 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.xml
para 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 !