Kenneth Jenkins

Android APK patching

Resurrecting vintage builds of Google Maps

Android Google Maps 7.0 launcher icon

Recently I’ve been job hunting, and so I’ve been reflecting on my past work experience. One position I applied to had a required “portfolio” link, and at first I thought that must have been a mistake in the application form. But upon further reflection, I thought, for a front-end engineer, why not? This got me wondering if I could dig up screenshots/recordings of any of the features I’ve worked on in previous roles.

Specifically, I was thinking about my time working on the Android Google Maps app. During my time on the team, we went through not one but two major app redesigns, so a lot of the UI I had worked on back then has since been completely replaced.

Archived builds

Fortunately, APKMirror has tons of archived Google Maps APKs. Their coverage gets pretty spotty before 2014, but still includes a decent selection. Most relevant for my nostalgia, they have a build of Google Maps 7.0. This was the major redesign that was underway when I first joined the team.

I wasn’t sure if these old builds would work with modern Android device images, but I was glad to see that Android Studio still provides emulator system images for devices from around the same era.

After I had downloaded Android Studio, fired up Device Manager, selected what I thought to be an appropriately-ancient Pixel 2 device image with Android 5.0 (Lollipop), installed adb, and then finally installed the old Google Maps APK, I was greeted with this:

“Update Google Maps to continue: This app is out of date.”

D’oh!

I had completely forgotten about the server-controlled kill switch built in to the app. The Google Maps team put a strong emphasis on backwards-compatibility, but also realized there would be cases where we might need to force users to stop using a particular released version.

Is there any way to bypass this update prompt?

apktool to the rescue

We can disassemble the archived Google Maps APKs using the open-source apktool:

$ apktool d -r com.google.android.apps.maps_7.0.2-700022123_minAPI18\(320dpi\)_apkmirror.com.apk -o maps-7.0
I: Using Apktool 2.7.0 on com.google.android.apps.maps_7.0.2-700022123_minAPI18(320dpi)_apkmirror.com.apk
I: Copying raw resources...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

(The -r option avoids decoding resources, as apktool seemed to have some issues with the resources.)

The resulting output includes a folder named “smali” containing all of the disassembled Java class bytecode. Many of the class names have been obfuscated, but we’re in luck: there’s a class named KillSwitchFragment.

We can see one of its methods finds and initializes a WebView, so it certainly looks like we’re on the right track:

.method private d()Landroid/view/View;
    .locals 2

    .prologue
    .line 83
    sget v0, Lcom/google/android/apps/gmm/i;->dv:I

    const/4 v1, 0x0

    invoke-virtual {p0, v0, v1}, Lcom/google/android/apps/gmm/terms/KillSwitchFragment;->a(ILandroid/view/ViewGroup;)Landroid/view/View;

    move-result-object v0

    .line 84
    sget v1, Lcom/google/android/apps/gmm/g;->gN:I

    invoke-virtual {v0, v1}, Landroid/view/View;->findViewById(I)Landroid/view/View;

    move-result-object v0

    check-cast v0, Landroid/webkit/WebView;

    .line 86
    new-instance v1, Lcom/google/android/apps/gmm/terms/a;

    invoke-direct {v1, p0}, Lcom/google/android/apps/gmm/terms/a;-><init>(Lcom/google/android/apps/gmm/terms/KillSwitchFragment;)V

    invoke-virtual {v0, v1}, Landroid/webkit/WebView;->setWebViewClient(Landroid/webkit/WebViewClient;)V

    .line 98
    iget-object v1, p0, Lcom/google/android/apps/gmm/terms/KillSwitchFragment;->b:Ljava/lang/String;

    invoke-virtual {v0, v1}, Landroid/webkit/WebView;->loadUrl(Ljava/lang/String;)V

    .line 100
    return-object v0
.end method

This isn’t the easiest to read. A quick primer to help make sense of it:

  • Method parameter values are referred to as p0, p1, p2, etc.
  • For an instance method, parameter p0 is the this reference.
  • Local variables are referred to as v0, v1, v2, etc.
  • Java classes are referenced with an L, followed by their fully-qualified class name (with all . characters replaced by / characters), followed by a ; (e.g. android.webkit.WebView becomes Landroid/webkit/WebView;)
  • Other types include I (int), Z (boolean), and V (void).
  • The invoke- mnemonics represent a method call.
  • Method references consist of the class name followed by ->, the method name, the types of the method parameters (in parentheses), and the method return type.
  • Constructors are referred to like a void method with the name <init>.

So this method may have looked something like this in the source code:

  private View createView() {
    View view = this.inflateLayout(some_layout_id, null);
    WebView webView = (WebView) view.findViewById(some_view_id);
    WebViewClient client = new KillSwitchWebViewClient(this);
    webView.setWebViewClient(client);
    webView.loadUrl(this.url);
    return webView;
  }

(I’m guessing at some of the method names based on the signatures.)

We can search through all the disassembled classes to look for references to KillSwitchFragment, and after tracing a few method call chains, I came to this line in a class named GmmActivity:

    invoke-static {p0, v0}, Lcom/google/android/apps/gmm/terms/KillSwitchFragment;->a(Lcom/google/android/apps/gmm/base/activities/GmmActivity;Z)V

Let’s just comment that line out with a # and see what happens….

Building a patched APK

We can reassemble an APK by running:

$ apktool b maps-7.0
I: Using Apktool 2.7.0
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether resources has changed...
I: Copying raw resources...
I: Building apk file...
I: Copying unknown files/dir...
I: Built apk into: maps-7.0/dist/com.google.android.apps.maps_7.0.2-700022123_minAPI18(320dpi)_apkmirror.com.apk

However in order to install to a device (or emulator), we need to sign the APK. We can generate a dummy signing key for this with keytool (part of the Java runtime):

$ keytool -genkey -keystore dummy.keystore -alias dummy -keyalg RSA -keysize 2048 -validity 100

After following through the prompts, we can then use apksigner (from the Android SDK build tools) to sign our patched APK with the dummy key:

$ apksigner sign --ks dummy.keystore maps-7.0/dist/com.google.android.apps.maps_7.0.2-700022123_minAPI18\(320dpi\)_apkmirror.com.apk

And then we can launch a device emulator and finally install our patched APK using adb:

$ adb install -r maps-7.0/dist/com.google.android.apps.maps_7.0.2-700022123_minAPI18\(320dpi\)_apkmirror.com.apk

And we get this…

Unfortunately, Maps has stopped.

Hmm. Progress?

More patching

Well, at least we can use adb logcat to look for crash information, and what do you know:

FATAL EXCEPTION: main
Process: com.google.android.apps.maps, PID: 3012
java.lang.SecurityException: GoogleCertificatesRslt: not whitelisted: pkg=com.google.android.apps.maps, sha1=9459608ef2083805c6f6075779366c5831df2f8f, atk=true, ver=202414010.true (go/gsrlt)

Well yes, our patched APK would fail whatever hash-based verification it might be subjected to, wouldn’t it.

The logcat output also has a stack trace to work with, and there’s only one stack frame matching the com.google.android.apps.gmm package name:

--------- beginning of crash
E/AndroidRuntime( 3012): FATAL EXCEPTION: main
E/AndroidRuntime( 3012): Process: com.google.android.apps.maps, PID: 3012
E/AndroidRuntime( 3012): java.lang.SecurityException: GoogleCertificatesRslt: not whitelisted: pkg=com.google.android.apps.maps, sha1=9459608ef2083805c6f6075779366c5831df2f8f, atk=true, ver=202414010.true (go/gsrlt)
E/AndroidRuntime( 3012): 	at android.os.Parcel.readException(Parcel.java:1540)
E/AndroidRuntime( 3012): 	at android.os.Parcel.readException(Parcel.java:1493)
E/AndroidRuntime( 3012): 	at com.google.android.gms.internal.v.c(Unknown Source)
E/AndroidRuntime( 3012): 	at com.google.android.gms.internal.aS.a(Unknown Source)
E/AndroidRuntime( 3012): 	at com.google.android.gms.internal.g.b(Unknown Source)
E/AndroidRuntime( 3012): 	at com.google.android.gms.internal.k.onServiceConnected(Unknown Source)
E/AndroidRuntime( 3012): 	at com.google.android.gms.internal.m.a(Unknown Source)
E/AndroidRuntime( 3012): 	at com.google.android.gms.internal.g.c(Unknown Source)
E/AndroidRuntime( 3012): 	at com.google.android.gms.location.reporting.c.a(Unknown Source)
E/AndroidRuntime( 3012): 	at com.google.android.apps.gmm.ulr.UlrPromoFragment.onResume(SourceFile:89)
E/AndroidRuntime( 3012): 	at android.app.Fragment.performResume(Fragment.java:2096)
E/AndroidRuntime( 3012): 	at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:928)
E/AndroidRuntime( 3012): 	at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1067)
E/AndroidRuntime( 3012): 	at android.app.BackStackRecord.popFromBackStack(BackStackRecord.java:1595)
E/AndroidRuntime( 3012): 	at android.app.FragmentManagerImpl.popBackStackState(FragmentManager.java:1504)
E/AndroidRuntime( 3012): 	at android.app.FragmentManagerImpl$2.run(FragmentManager.java:490)
E/AndroidRuntime( 3012): 	at android.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1452)
E/AndroidRuntime( 3012): 	at android.app.FragmentManagerImpl$1.run(FragmentManager.java:447)
E/AndroidRuntime( 3012): 	at android.os.Handler.handleCallback(Handler.java:739)
E/AndroidRuntime( 3012): 	at android.os.Handler.dispatchMessage(Handler.java:95)
E/AndroidRuntime( 3012): 	at android.os.Looper.loop(Looper.java:135)
E/AndroidRuntime( 3012): 	at android.app.ActivityThread.main(ActivityThread.java:5221)
E/AndroidRuntime( 3012): 	at java.lang.reflect.Method.invoke(Native Method)
E/AndroidRuntime( 3012): 	at java.lang.reflect.Method.invoke(Method.java:372)
E/AndroidRuntime( 3012): 	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
E/AndroidRuntime( 3012): 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)
W/ActivityManager( 1178):   Force finishing activity com.google.android.apps.maps/com.google.android.maps.MapsActivity

Let’s see if we can do without the UlrPromoFragment as well. After some more spelunking through the disassembled classes, I found this section in the class com/google/android/apps/gmm/base/activities/d:

    invoke-static {v0}, Lcom/google/android/apps/gmm/ulr/UlrPromoFragment;->a(Lcom/google/android/apps/gmm/base/activities/GmmActivity;)Z

    move-result v0

    if-eqz v0, :cond_0

    .line 566
    iget-object v0, p0, Lcom/google/android/apps/gmm/base/activities/d;->a:Lcom/google/android/apps/gmm/base/activities/GmmActivity;

    new-instance v1, Lcom/google/android/apps/gmm/ulr/UlrPromoFragment;

    invoke-direct {v1}, Lcom/google/android/apps/gmm/ulr/UlrPromoFragment;-><init>()V

    invoke-virtual {v0, v1}, Lcom/google/android/apps/gmm/base/activities/GmmActivity;->a(Lcom/google/android/apps/gmm/base/fragments/GmmActivityFragment;)V

    .line 568
    :cond_0
    iget-object v0, p0, Lcom/google/android/apps/gmm/base/activities/d;->a:Lcom/google/android/apps/gmm/base/activities/GmmActivity;

This is a call to a static method on UlrPromoFragment, and then depending on the return value from that static method, we may or may not initialize a UlrPromoFragment instance and do something with it.

Let’s just make that an unconditional jump to :cond_0 instead, so we never create a UlrPromoFragment instance here:

    invoke-static {v0}, Lcom/google/android/apps/gmm/ulr/UlrPromoFragment;->a(Lcom/google/android/apps/gmm/base/activities/GmmActivity;)Z

    move-result v0

    goto :cond_0

    .line 566
    iget-object v0, p0, Lcom/google/android/apps/gmm/base/activities/d;->a:Lcom/google/android/apps/gmm/base/activities/GmmActivity;

    new-instance v1, Lcom/google/android/apps/gmm/ulr/UlrPromoFragment;

    invoke-direct {v1}, Lcom/google/android/apps/gmm/ulr/UlrPromoFragment;-><init>()V

    invoke-virtual {v0, v1}, Lcom/google/android/apps/gmm/base/activities/GmmActivity;->a(Lcom/google/android/apps/gmm/base/fragments/GmmActivityFragment;)V

    .line 568
    :cond_0
    iget-object v0, p0, Lcom/google/android/apps/gmm/base/activities/d;->a:Lcom/google/android/apps/gmm/base/activities/GmmActivity;

Repeat the same dance as before: apktool b, apksigner sign, uninstall and then adb install, and then…

Searching for restaurants in Mountain View like it's 2013

The map renders! Search works! The Street View thumbnails don’t seem to load anymore, but other than that it seems totally functional.

Reference