You Must Be 64-Bit to Ride This Ferry | by Joey Watts | Nov, 2022

Reverse engineering the updated NY Waterway app for the Pixel 7

photo by maxwell ridgeway Feather unsplash

TLDR: If you have a newer Android device that won’t let you install NY Waterways, you can download my modified version of the app, You should always be careful about installing random apps, especially from sources other than the official Play Store — like this Medium post by some random person you’ve never heard of. If you want to be extra cautious, you can read further to see how the APK was modified (and repeat the steps yourself if you want).

In 2019, Google made 64-bit support necessary For all new and updated apps in Play Store. From August 2021, applications that do not support 64-bit architecture become unavailable in the Play Store for 64-bit enabled devices. especially, The new Pixel 7 and Pixel 7 Pro don’t support installing only 32-bit apps Surely,

For New Yorkers who ride the Hudson River Ferry, this is quite inconvenient because the application that provides electronic tickets on your phone, NY WaterwaysIs really old, It was last published in June 2018, and only contains native libraries for 32-bit architectures… So, for users of newer Pixel devices, No Electronic Tickets on the Hudson River Ferry for you!

I switched to the iPhone many years ago now, but when I was an Android user, I used to hack around with the OS and applications – installing custom ROMs and decompiling applications. A close friend of mine just got the new Pixel 7 Pro and takes the Hudson River Ferry all the time, so he jokingly asked me to fix this app for him. here is my!

Let’s start by inspecting the NY Waterways application to identify the parts that are 32-bit only that are preventing it from installing. using the apktoolWe can extract the Android application and inspect its code.

$ 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

apktool Will produce a new directory that contains the decompiled application bytecode from the binary into a human-readable text-based format for smily, bundled resources (such as images), native libraries, and application configuration. Smily may sound scary, but it’s more accessible than you might think — more on that later… For now, let’s focus on fixing 64-bit compatibility. To do this, we need to see what native libraries are being used.

Android applications are typically written in Java or Kotlin, both languages ​​target the Java Virtual Machine, which is a high-level abstraction that generally shields you from concerns about platform-specific compatibility. However, you can use java native interface (JNI) To call native, platform-specific code (usually compiled from low-level languages ​​such as C or C++). if we see libs directory, we can see the native libraries included in the NY Waterways app.

$ ls -lR lib/*
total 8352
-rw-r--r-- 1 joeywatts staff 177900 Nov 21 18:37
-rw-r--r-- 1 joeywatts staff 1369284 Nov 21 18:37
-rw-r--r-- 1 joeywatts staff 2314540 Nov 21 18:37
-rw-r--r-- 1 joeywatts staff 402604 Nov 21 18:37

total 2552
-rw-r--r-- 1 joeywatts staff 1303788 Nov 21 18:37

total 14616
-rw-r--r-- 1 joeywatts staff 1476500 Nov 21 18:37
-rw-r--r-- 1 joeywatts staff 2246448 Nov 21 18:37
-rw-r--r-- 1 joeywatts staff 3294132 Nov 21 18:37
-rw-r--r-- 1 joeywatts staff 455740 Nov 21 18:37

We can see below three directories libEach pertains to different platforms: x86, armeabi, armeabi-v7a, All three of these platforms are 32-bit. Most Android devices (basically all phones) use the ARM architecture. “armbee” is the legacy ARM architecture (no longer supported by Android). ARM v7 (“armeabi-v7a”) is a 32-bit ARM architecture. For 64-bit ARM support, we would expect that a arm64-v8a folder.

Another observation here is that armeabi And x86 while there are four libraries armeabi-v7a There is only one. For library to be loaded by Android app, it has to call java.lang.System.loadLibrary either java.lang.Runtime.loadLibrary, Searching the smali code for “loadlibrary” reveals only one place where it is loading native libraries.

$ 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
const-string v0, "sqlcipher"

invoke-static v0, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
.catchall :try_start_0 .. :try_end_0 :catchall_0

The only library directly loaded by the application is “sqlcipher” (, Other library files listed for some architectures are either unused or just transitive dependencies,

we need a 64-bit ARM build In lib/arm64-v8a To make the app compatible with newer Pixel devices. Conveniently, SQLCipher is an open source library, Looking at the high-level glue code for the interaction with the native sqlcipher library, we can see the version of the library that was used.

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

After some quick digging on the open source repo, I can see 64-bit support landed with v3.5.5 (a patched version newer than the one used in NY Waterways). Let’s try to upgrade!

Upgrading SQLCipher to v3.5.5

The upgrading process will involve replacing the SQLCipher Smali code and native libraries with code from the newer version. This will cause problems if the public API surface of SQLCipher changes significantly (for example, if a public function used by NY Waterways has either changed signatures or has been removed, it cannot be updated with newer versions. Replacing with will cause problems). Doing a quick scan of the changes from v3.5.4 to v3.5.5, doesn’t look like it’s going to be an issue here. I downloaded AAR file for SQLCipher v3.5.5 and then used unzip to extract it.

$ 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/
inflating: jni/armeabi/
inflating: jni/armeabi-v7a/
inflating: jni/x86/
inflating: jni/x86_64/

after extracting it we see under it jni The directories are the native libraries. it also outputs a classes.jar File that contains all the Java class files that call the native library. These are not smali files so we have to change this code to get it in one format apktool understands.

Android SDK provides a command line tool called d8 which can compile a jar File for Android byte code (classes.dex file). then another tool is called baksmali that can be decomposed dex in files smali, Linking the steps together:

$ 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

it produces a out Directory containing Smali code for the library. Therefore, we can simply change smali/net/sqlcipher with out/net/sqlcipher And this lib with directory jni,

$ rm -r ../NYWaterway/smali/net/sqlcipher ../NYWaterway/lib
$ mv out/net/sqlcipher ../NYWaterway/smali/net/sqlcipher
$ mv jni ../NYWaterway/lib

Now, we can rebuild and sign the application, so it can be installed on the device!

$ 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

after installing ./dist/NYWaterway.apkIt shows this screen!

Revised NY Waterways application showing a popup that says
It Runs! However we have this annoying popup 😔

To get rid of this popup indicating that the application was built for an older version of Android, we need to increase the target SDK version apktool.yml, Applications targeting SDK version < 31 are no longer accepted in the Play Store, so I chose to extend this to that.

Targeting a newer version of the Android SDK may require code changes as deprecated APIs become unavailable in newer SDK versions. NY Waterways requires several changes to target SDK v31.

Secure Component Export

If your app targets Android 12 or higher and contains activities, services, or broadcast receivers that use intent filters, you must explicitly declare android:exported attribute for these app components.

have some activities and have a receiver need s and a android:exported="true" properties to be added AndroidManifest.xml,

pending intent variability

If your app targets Android 12, you must specify the modality of each PendingIntent The object that your app creates. This additional requirement improves the security of your app.

This one is tricky, because it requires us to change the actual code (as opposed to copying the project configuration or an upgraded version of a library).

at any time a PendingIntent object is created, it needs to be specified explicitly FLAG_MUTABLE either FLAG_IMMUTABLE, In earlier SDK versions, FLAG_MUTABLE Was the default if none of the flags were specified. PendingIntent Objects are created by a set of static methods on the class: getActivity, getActivities, getBroadcasteither getService, We can start by discovering the invocation of those functions.

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

enough! Luckily, most of them are fairly simple changes. We first need to understand a bit about byte code.

understanding smali

invoke-static The byte code instruction takes a list of registers to be passed as a parameter to the static function. What does the symbol of a static function look like? Landroid/app/PendingIntent;->getBroadcast(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent; Which is a direct translation from the fully qualified class name and function signature. It starts with the name of the class Landroid/app/PendingIntent; (either in normal Java syntax). then function name (->getBroadcast) with parameters and return types. Landroid/content/Context;ILandroid/content/Intent;I There are parameters, which can be divided into four parameters: Landroid/content/Context; ,android.content.Context, I ,int, Landroid/content/Intent; ,android.content.Intent), And I ,int, Finally, the return type after the closing parenthesis is: Landroid/app/PendingIntent;,

So, invoke-static v1, v2, v3, v4 the above function will pass v1 In form of Context, v2 of earlier int, v3 In form of IntentAnd v4 In form of int, For these PendingIntent API, The flags are always the last parameter (int) so we only need to ensure that the value is always either FLAG_MUTABLE either FLAG_IMMUTABLE group. android sdk documentation tells the value of FLAG_MUTABLE Is 0x02000000 And FLAG_IMMUTABLE Is 0x04000000, In most cases, the last parameter is specified as a local variable register (v#) which was initialized with a constant value (eg const/high16 v3, 0x8000000 either const/4 v4, 0x0, In these cases, we can trivially check whether FLAG_MUTABLE either FLAG_IMMUTABLE is set and update the constant if it is not.

-    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

There was a case (in com/google/firebase/iid/r.smali) Where flags passed as a parameter (p# register).

.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;->(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

to update p4 to set FLAG_MUTABLE bit easier than following all references to this function if needed as far as flags are specified. To do this, we have to write some byte code by hand! The equivalent Java-like code would be something like this:


FLAG_IMMUTABLE | FLAG_MUTABLE is stable 0x6000000which we can load into a register const/high16 Instructions. we can use and-int bitwise AND instructions with p4, if-nez Allows you to jump to a label if a register is not equal to zero. eventually, or-int Let us do a bit or two of the registers. google has Documentation on Dalvik byte code This is useful for exploring instructions and their syntax. Putting all this together, we get the following code which can be inserted before calling getBroadcast,

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

eventually .locals 2 The instruction at the top of the function indicates that two local variable registers must be allocated for this function (v0 And v1, because we used two more (v2 And v3) in the above code, we need to change this .locals 4,

file system permission change

File permissions of private files should no longer be relaxed by the owner, and should be attempted MODE_WORLD_READABLE and/or MODE_WORLD_WRITEABLEwill trigger aSecurityException,

was something SharedPreferences api usage that was using MODE_WORLD_READABLE In com/google/android/gms/ads/identifier/AdvertisingIdClient.smali, It was very easy to fix, because it was all about switching MODE_WORLD_READABLE ,0x1) To 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;

Apache HTTP Client deprecation

With Android 6.0, we removed support for the Apache HTTP client. Starting with Android 9, that library has been removed from the bootclasspath and is not available to apps by default.

NY Waterway was using the Android version of the Apache HttpClient, but the fix for that is very simple – just one more change. AndroidManifest.xml,

diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 1490d73..39ccbf3 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -16,6 +16,7 @@


network TLS is enabled by default

If your app targets Android 9 or higher, then isCleartextTrafficPermitted() method returns false by default. If your app requires cleartext to be enabled for a specific domain, you must explicitly set cleartextTrafficPermitted To true for those domains in your app’s network security configuration.

This new security feature was causing network requests to fail. The simplest way to make the application compatible was just one more change AndroidManifest.xml to append android:usesCleartextTraffic="true" Speciality

diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 39ccbf3..69b4aa7 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -15,7 +15,7 @@


After making all the above changes, the application runs successfully without any annoying popups that it was built for an older version of Android!

Somewhat unexpectedly, getting it working with the new target SDK version was a lot more involved than actually fixing the 64-bit problem, but at the end of the day, everything is just code and code is nothing to be afraid of. Doesn’t matter…

Leave a Reply