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 apktool
We 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/*
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.solib/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
We can see below three directories lib
Each 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
: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
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 libsqlcipher.so
,
we need a 64-bit ARM build libsqlcipher.so
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/libsqlcipher.so
inflating: jni/armeabi/libsqlcipher.so
inflating: jni/armeabi-v7a/libsqlcipher.so
inflating: jni/x86/libsqlcipher.so
inflating: jni/x86_64/libsqlcipher.so
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.apk
It 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: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
, getBroadcast
either 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 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: 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 Intent
And 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, 0xA000000invoke-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 2new-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:
if (p4 & (FLAG_IMMUTABLE | FLAG_MUTABLE) == 0)
p4
FLAG_IMMUTABLE | FLAG_MUTABLE
is stable 0x6000000
which 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
:cond_0
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/orMODE_WORLD_WRITEABLE
will 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
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 returnsfalse
by default. If your app requires cleartext to be enabled for a specific domain, you must explicitly setcleartextTrafficPermitted
Totrue
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…