Reverse engineering the updated NY Waterway app for the Pixel 7
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
-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/*
-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
-rw-r--r-- 1 joeywatts staff 1303788 Nov 21 18:37 libsqlcipher.so
-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
We can see below three directories
libEach pertains to different platforms:
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
Another observation here is that
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.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” (
libsqlcipher.so, Other library files listed for some architectures are either unused or just transitive dependencies
we need a 64-bit ARM build
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
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
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 \
$ java -jar ../baksmali.jar dis ./classes.dex
it produces a
out Directory containing Smali code for the library. Therefore, we can simply change
out/net/sqlcipher And this
lib with directory
$ 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
./dist/NYWaterway.apkIt shows this screen!
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:exportedattribute for these app components.
have some activities and have a receiver
need s and a
android:exported="true" properties to be added
pending intent variability
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_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:
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.
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
android.app.PendingIntent 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:
int, Finally, the return type after the closing parenthesis is:
invoke-static v1, v2, v3, v4 the above function will pass
v1 In form of
v2 of earlier
v3 In form of
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_IMMUTABLE group. android sdk documentation tells the value of
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_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
flags passed as a parameter (
.method private static a(Landroid/content/Context;ILjava/lang/String;Landroid/content/Intent;I)Landroid/app/PendingIntent;
new-instance v0, Landroid/content/Intent;
const-class v1, Lcom/google/firebase/iid/FirebaseInstanceIdInternalReceiver;
invoke-direct v0, p0, v1, Landroid/content/Intent;->
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;
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:
if (p4 & (FLAG_IMMUTABLE | FLAG_MUTABLE) == 0)
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
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
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
.locals 2 The instruction at the top of the function indicates that two local variable registers must be allocated for this function (
v1, because we used two more (
v3) in the above code, we need to change this
file system permission change
SharedPreferences api usage that was using
com/google/android/gms/ads/identifier/AdvertisingIdClient.smali, It was very easy to fix, because it was all about switching
@@ -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
NY Waterway was using the Android version of the Apache HttpClient, but the fix for that is very simple – just one more change.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 1490d73..39ccbf3 100644
@@ -16,6 +16,7 @@
network TLS is enabled by default
If your app targets Android 9 or higher, then
falseby default. If your app requires cleartext to be enabled for a specific domain, you must explicitly set
truefor 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
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 39ccbf3..69b4aa7 100644
@@ -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…