このフェリーに乗るには 64 ビットである必要があります

Nov 25 2022
Pixel 7 TLDR 用に更新された NY Waterway アプリのリバース エンジニアリング: NY Waterway をインストールできない新しい Android デバイスをお持ちの場合は、私の変更したバージョンのアプリケーションをダウンロードできます。特に、公式の Play ストア以外のソースからランダムにアプリケーションをインストールする場合は、常に注意する必要があります。

Pixel 7 向けに更新された NY Waterway アプリのリバース エンジニアリング

UnsplashのMaxwell Ridgewayによる写真

TLDR : NY Waterway をインストールできない新しい Android デバイスをお持ちの場合は、私の修正版のアプリケーションをダウンロードできます。特に、公式の Play ストア以外のソースからランダムにアプリケーションをインストールする場合は、常に注意する必要があります。より慎重になりたい場合は、事前に読んで APK がどのように変更されたかを確認できます (必要に応じて、自分で手順を繰り返すこともできます)。

2019 年、 Googleは Play ストアのすべての新規および更新されたアプリケーションに64 ビットのサポートを必須にしました。2021 年 8 月以降、64 ビット アーキテクチャをサポートしていないアプリは、64 ビット対応デバイスの Play ストアで利用できなくなりました。特に、新しい Pixel 7 および Pixel 7 Pro は、32 ビットのみのアプリケーションのインストールをまったくサポートしていません。

ハドソン川フェリーに乗るニューヨーカーにとって、携帯電話で電子チケットを提供するアプリケーションであるNY Waterwayは非常に古いため、これは非常に不便です。最後に公開されたのは 2018 年 6 月で、32 ビット アーキテクチャ専用のネイティブ ライブラリが含まれています。したがって、新しい Pixel デバイスのユーザーには、ハドソン川フェリーの電子チケットは必要ありません!

私は何年も前に iPhone に切り替えましたが、Android ユーザーだった頃は、カスタム ROM をインストールしたり、アプリケーションを逆コンパイルしたりして、OS とアプリケーションを頻繁にハッキングしていました。私の親しい友人が新しい Pixel 7 Pro を手に入れ、いつもハドソン川のフェリーに乗っていたので、彼は冗談めかしてこのアプリを修正するように勧めました。どうぞ!

アプリケーションへのピアリング

まず、NY Waterway アプリケーションを調べて、インストールを妨げている 32 ビットのみのパーツを特定します。を使用しapktoolて、Android アプリケーションを抽出し、そのコードを調べることができます。

$ 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

64 ビット互換性とネイティブ ライブラリ

通常、Android アプリケーションは Java または Kotlin で作成されます。どちらの言語も Java Virtual Machine を対象としています。これは高レベルの抽象化であり、一般にプラットフォーム固有の互換性に関する懸念から保護されています。ただし、Java Native Interface (JNI) を使用して、プラットフォーム固有のネイティブ コード (通常は C や C++ などの低レベル言語からコンパイルされたもの) を呼び出すことができます。ディレクトリを見るとlibs、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

ここでのもう 1 つの観察結果は、armeabi1つしかないのにx864 つのライブラリがあることです。armeabi-v7aライブラリが Android アプリによって読み込まれるようにするには、java.lang.System.loadLibraryまたはを呼び出す必要がありjava.lang.Runtime.loadLibraryます。Smali コードで「loadLibrary」を検索すると、ネイティブ ライブラリをロードしている場所が 1 つだけ見つかります。

$ 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

アプリケーションを新しい Pixel デバイスと互換性を持たせるにはlibsqlcipher.so、64 ビット ARM ビルドが必要です。lib/arm64-v8a便利なことに、SQLCipher はオープン ソース ライブラリです。ネイティブ sqlcipher ライブラリとやり取りするための高レベルのグルー コードを見ると、使用されたライブラリのバージョンがわかります。

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

SQLCipher を v3.5.5 にアップグレードする

アップグレードのプロセスには、SQLCipher Smali コードとネイティブ ライブラリを新しいバージョンのコードに置き換えることが含まれます。これにより、SQLCipher のパブリック API サーフェスが大幅に変更された場合に問題が発生します (たとえば、NY Waterway で使用されているパブリック関数が署名を変更するか削除された場合、それを新しいバージョンに置き換えると問題が発生します)。v3.5.4 から v3.5.5 への変更を簡単にスキャンすると、ここに表示される問題はないようです。SQLCipher v3.5.5 の AAR ファイルをダウンロードし、それを解凍するために使用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

Android SDK には、ファイルを Android バイト コード ( file) にd8コンパイルできるというコマンド ライン ツールが用意されています。次に、ファイルを逆コンパイルして. 手順を組み合わせる:jarclasses.dexbaksmalidexsmali

$ 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

これで、アプリケーションを再構築して署名できるので、デバイスにインストールできます!

$ 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

走る!ただし、このしつこいポップアップがあります

ターゲット SDK のバージョンを上げる

アプリケーションが古いバージョンの Android 用にビルドされたことを示すこのポップアップを取り除くには、 でターゲット SDK バージョンを増やす必要がありますapktool.yml。SDK バージョン <31 を対象とするアプリケーションは Play ストアで受け入れられなくなったため、それまで増やすことにしました。

Android SDK の新しいバージョンをターゲットにするには、新しいバージョンの SDK では非推奨の API が使用できなくなるため、コードの変更が必要になる場合があります。NY Waterway では、SDK v31 をターゲットにするためにいくつかの変更が必要です。

より安全なコンポーネントのエクスポート

アプリが Android 12 以降を対象とし、インテント フィルターを使用するアクティビティ、サービス、またはブロードキャスト レシーバーを含む場合、android:exportedこれらのアプリ コンポーネントの属性を明示的に宣言する必要があります。

があり、に属性を追加する<intent-filter>必要があるいくつかのアクティビティと 1 つのレシーバーがあります。android:exported="true"AndroidManifest.xml

保留中のインテントの可変性

アプリが Android 12 を対象とする場合、アプリが作成する各オブジェクトの可変性を指定する必要があります。PendingIntentこの追加要件により、アプリのセキュリティが向上します。

これは、実際のコードを変更する必要があるため (プロジェクトの構成やアップグレードされたバージョンのライブラリをコピーするのではなく)、よりトリッキーです。

オブジェクトを作成するときはいつでも、またはPendingIntentを明示的に指定する必要があります。以前の SDK バージョンでは、どちらのフラグも指定されていない場合は がデフォルトでした。オブジェクトは、クラスの一連の静的メソッド( 、、、または) によって作成されます。これらの関数の呼び出しを検索することから始めることができます。FLAG_MUTABLEFLAG_IMMUTABLEFLAG_MUTABLEPendingIntentgetActivitygetActivitiesgetBroadcastgetService

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

スマリ語を理解する

invoke-staticバイトコード命令は、パラメーターとして静的関数に渡されるレジスターのリストを取ります。Landroid/app/PendingIntent;->getBroadcast(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;静的関数のシンボルは、完全修飾クラス名と関数の署名からの直接の翻訳のように見えます。Landroid/app/PendingIntent;クラス名(またはandroid.app.PendingIntent通常の Java 構文) で始まります。次に、関数の名前 ( ->getBroadcast) とパラメーターおよび戻り値の型。( )、( )、( ) 、 ( ) のLandroid/content/Context;ILandroid/content/Intent;I4 つのパラメータに分割できます。最後に、閉じ括弧の後に戻り値の型があります: .Landroid/content/Context;android.content.ContextIintLandroid/content/Intent;android.content.IntentIintLandroid/app/PendingIntent;

したがって、上記の関数のうち、 、最初の、、およびとしてinvoke-static {v1, v2, v3, v4}渡さv1れます。これらのAPI では、は常に最後のパラメーター ( ) であるため、値が常にいずれかまたは設定されていることを確認する必要があります。Android SDKのドキュメントでは、 isとisの値が明らかにされています。ほとんどの場合、最後のパラメータは、定数値 (またはなど) で初期化されたローカル変数レジスタ ( ) として指定されます。これらの場合、私たちは自明にチェックすることができますContextv2intv3Intentv4intPendingIntentflagsintFLAG_MUTABLEFLAG_IMMUTABLEFLAG_MUTABLE0x02000000FLAG_IMMUTABLE0x04000000v#const/high16 v3, 0x8000000const/4 v4, 0x0FLAG_MUTABLEFLAG_IMMUTABLE設定されていない場合は定数を更新します。

-    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

ファイル システムのアクセス許可の変更

プライベート ファイルのファイル パーミッションを所有者が緩和することはできなくなりました。また、MODE_WORLD_READABLEおよび/またはを使用して緩和しようとするとMODE_WORLD_WRITEABLESecurityException.

SharedPreferencesで使用していた APIMODE_WORLD_READABLEの使用法がいくつかありましたcom/google/android/gms/ads/identifier/AdvertisingIdClient.smaliMODE_WORLD_READABLEこれは( 0x1) から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;

Android 6.0 では、Apache HTTP クライアントのサポートを削除しました。Android 9 以降、そのライブラリは bootclasspath から削除され、デフォルトではアプリで使用できなくなりました。

NY Waterway は Apache HTTP クライアントの Android バージョンを使用していましたが、これに対する修正は非常に簡単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>

アプリが Android 9 以降をターゲットにしている場合、isCleartextTrafficPermitted()メソッドはfalseデフォルトで戻ります。アプリで特定のドメインに対してクリアテキストを有効にする必要がある場合は、アプリのネットワーク セキュリティ構成でそれらのドメインに対して明示的に設定cleartextTrafficPermittedする必要があります。true

この新しいセキュリティ機能が原因で、ネットワーク リクエストが失敗していました。アプリケーションに互換性を持たせる最も簡単な方法は、属性AndroidManifest.xmlを追加するための変更です。android:usesCleartextTraffic="true"

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

上記の変更をすべて行った後、アプリケーションは、古いバージョンの Android 用にビルドされたというしつこいポップアップなしで正常に実行されます。

意外なことに、より新しいターゲット SDK バージョンで動作させることは、実際に 64 ビットの問題を修正することよりもはるかに複雑でしたが、結局のところ、すべては単なるコードであり、コードは恐れるものではありません…

接続したいですか?TwitterまたはLinkedInでメッセージを送ってください