Old versions of Google Maps show a full-screen page rendered in WebView prompting you to update the app and refuse to run. For years, the fix was to patch the smali and re-sign the APK. However, this changes the signature, and GMS then rejects the app, breaking sign-in and many other functions of the app.
This project removes the kill switch without modifying the app at all. A small mitmproxy script changes one byte in the sever's config response, so the original, unmodified APK keeps working, sign-in included.
Not affiliated with Google. Use your own legitimately obtained copy of the app. This repo does not include any APKs.
- Tested versions
- How the kill switch works
- How it was found
- The fix
- Requirements
- Setup and running
- Checking that it worked
- Troubleshooting
- Limitations and security
- Adapting to other versions
- Reporting test results
- Files and credit
| Version | Date | Result | Notes |
|---|---|---|---|
| 3.1.0 - 6.14.5 | October 2010 - October 2014 | Not Needed | There is no kill switch in these versions. |
| 7.0.0 - 7.7.0 | July 2013 - October 2014 | Works | Everything works perfectly. |
| 8.0.0 - 8.4.1 | May 2014 - October 2014 | Works | Everything works perfectly. |
| 9.0.0 - 9.27.2 | Nov 2014 - June 2016 | Works | Everything works perfectly. |
| 9.31.3+ | July 2016 - Latest | Not Working | Limitation of mitmproxy user certificates, see Requirements. |
Details of the 9.0.0 build, from decompiling it with jadx:
- package 'com.google.android.apps.maps'
minSdkVersion18,targetSdkVersion17- native libraries for
armeabi-v7aonly (32-bit ARM)
I have only tested these versions. Other versions may use a different field layout or response framing, so treat them as untested until you've checked (see the adaptation section).
| Part | What I used |
|---|---|
| Phone | Samsung Galaxy S10 (SM-G973F), not rooted |
| Computer | M1 MacBook Air |
| Proxy | mitmproxy (mitmdump), installed with Homebrew |
Reading the decompiled 9.0.0 app shows the whole mechanism:
- At startup the app fetches a config message from
POST https://clients4.google.com/glm/mmap. - A protobuf message in that response (class
com.google.t.b.a.uyin this build) has four string fields. Field 2 holds the kill-switch page URL, in practicehttps://maps.gstatic.com/gmm/upgrades/index.html. - A handler class (
com.google.android.apps.gmm.terms.b) saves that string in the app's preferences ascurrentKillSwitchContentUrl. If it is non-null, the app showsKillSwitchFragment, which loads the URL in a WebView. - If a later response has no field 2, the handler stores
nulland removes the fragment itself.
In pseudocode:
on config received:
url = config.field2 if present else null
preferences.killSwitchUrl = url
if url != null: show KillSwitchFragment(url)
else: dismiss KillSwitchFragment
The decision is made by the server, not by a local version comparison. The page
URL is not stored as a string anywhere in the APK, which is why searching the
code for gmm/upgrades finds nothing.
Everything here is read-only. The APK is never modified.
# 1. Decompile
jadx -d maps-src Maps.apk
# 2. Check what it targets
grep -E "minSdkVersion|targetSdkVersion|package=" maps-src/resources/AndroidManifest.xml
ls maps-src/resources/lib/
# 3. Fragment class names survive obfuscation, so search for the class
grep -rl KillSwitch maps-src/sourcesThen follow the references:
KillSwitchFragmentonly displays a URL passed to it, so find its callers (terms/b.javaandterms/c.javain 9.0.0).terms/b.javareads the URL from a protobuf object (uy). In that class's parsing constructor, thecasenumbers are protobuf tags (field << 3 | wire type):10= field 1,18= field 2,26= field 3,34= field 4. The kill-switch URL is the one stored when bit 2 is set, which is field 2.- With the phone's traffic going through a proxy, the
/glm/mmapresponse showed the URL inside a gzip-compressed record.
Protobuf parsers skip fields whose numbers they don't know. The tag byte in
front of the kill-switch string is 0x12 (field 2, length-delimited). Change it
to 0x7A (field 15, length-delimited) and the string is parsed as an unknown
field, so the app sees field 2 as absent.
Illustrative bytes (the URL is 48 characters, 0x30):
before: 12 30 68 74 74 70 73 3a 2f 2f ... tag=field 2, length 48, "https://..."
after: 7a 30 68 74 74 70 73 3a 2f 2f ... tag=field 15, length 48, "https://..."
Only one byte changes and no lengths inside the message need fixing. The core of the script is:
tag, length = raw[start - 2], raw[start - 1] # bytes just before "http..."
if tag != 0x12 or MARK not in raw[start:start + length]:
logging.warning("unexpected bytes before URL") # fail safe: change nothing
return None
out = bytearray(raw)
out[start - 2] = 0x7A # field 15, wire type 2/glm/mmap responses are not a bare protobuf. This is my reading of captured
9.0.0 traffic, not a documented format:
2 header bytes (00 17)
then repeated records:
1 byte record type
4 bytes big-endian signed length
(negative = gzip-compressed, abs(length) = compressed size)
N bytes payload
The record sizes add up exactly to the full response length. The kill-switch URL sat in the second record, inside a gzip stream, so the script decompresses each record, changes the tag byte, recompresses, and rewrites that record's length. If anything doesn't match what it expects, it logs a warning and leaves the response untouched.
- A device that can run the app. 9.0.0 ships only 32-bit ARM libraries, so the device must support 32-bit ARM. Android emulators on Apple Silicon can't run it for that reason.
- Android 14 or newer refuses to install apps with a very old
targetSdk. Neweradbhasadb install --bypass-low-target-sdk-block. - An app that targets API 23 or lower, so that Android trusts user-installed CA certificates for it. 9.0.0 targets API 17. Apps targeting API 24 or higher would ignore the certificate, and this method would not work for them.
- A computer on the same Wi-Fi network with mitmproxy installed.
- No root is needed.
brew install mitmproxy # or: pipx install mitmproxy
mitmdump # run once, then Ctrl+C; creates ~/.mitmproxyFind your computer's IP (macOS: ipconfig getifaddr en0), then either set a
manual proxy in the Wi-Fi settings or use adb:
adb shell settings put global http_proxy YOUR_COMPUTER_IP:8080To turn it off again:
adb shell settings put global http_proxy :0With mitmdump running and the proxy set, open http://mitm.it in the phone's
browser and download the Android certificate. Then go to Settings → Security →
Encryption & credentials → Install a certificate → CA certificate. Android
requires a screen lock to be set.
adb install Maps.apk # your own copy of Google Mapsmitmdump -s killswitch.py --allow-hosts '^clients4\.google\.com:443$'The --allow-hosts option matters. It makes mitmproxy decrypt only the one
host Maps needs and pass everything else through untouched. Without it, Google
Play Services and the Play Store, which pin Google's real certificates, fail to
connect through the proxy.
Force-stop and relaunch Maps:
adb shell am force-stop com.google.android.apps.maps
adb shell monkey -p com.google.android.apps.maps -c android.intent.category.LAUNCHER 1The terminal running mitmdump should print:
kill-switch field neutralised
The update screen should not appear. Another check is that the log shows no
GET https://maps.gstatic.com/gmm/upgrades/index.html after the config
response. Before the fix, that request appeared about a quarter of a second
after the config response arrived.
If the screen shows once on the first launch, that is the URL stored by an
earlier run. Once the edited response arrives, the app stores null. Force-stop
and relaunch to confirm.
| Symptom | Likely cause | What to do |
|---|---|---|
| Sign-in or Play Store says "There was a problem connecting to accounts.google.com" | mitmproxy is decrypting hosts it shouldn't. Play Services pins Google's certificates. | Restart with --allow-hosts '^clients4\.google\.com:443$'. |
Client TLS handshake failed for play.google.com |
Same cause. | Same fix. Those hosts are not needed. |
Log says unexpected bytes before URL |
The response layout differs from 9.0.0. | Run inspect_mmap.py and compare. See Adapting to other versions. |
| Log says nothing and the screen still shows | The response didn't contain the URL on this launch, or the field layout is different. | Run inspect_mmap.py to see what the response contains. |
| Install is refused on Android 14+ | Old targetSdk block. |
adb install --bypass-low-target-sdk-block Maps.apk |
| App crashes on launch | Device has no 32-bit ARM support. | Use a device whose ABI list includes armeabi-v7a (adb shell getprop ro.product.cpu.abilist). |
| Screen returns later | The proxy was off when the app refreshed its config. | Keep the proxy running whenever you use Maps. |
- The proxy must be reachable whenever you use Maps. The server sends the kill-switch URL on every config fetch, and the app refreshes at least daily. Without the proxy the screen comes back.
- If the phone's proxy setting is on but the computer is off, other apps lose their connection. Turn the setting off when you're away.
- If Google changes the response layout, the script fails safe (it logs a warning and changes nothing), but the screen returns until the script is updated.
- Tested on one device and one version. A 2014 login flow could stop working for reasons unrelated to the kill switch.
- The mitmproxy CA certificate on the phone lets whoever holds the private key
in
~/.mitmproxyread traffic from apps that trust user certificates. Keep that folder private, and remove the certificate from the phone when you're done (Settings → Security → Encryption & credentials → User credentials). - Don't leave the proxy listening on an untrusted network.
Class names are obfuscated and differ per build, so don't expect terms/b.java
to exist elsewhere. The steps:
- Decompile and find the kill-switch fragment (see How it was found).
- Find who calls it and which protobuf field supplies the URL.
- Note the tag number in that message's parsing constructor.
- Capture a real response with the read-only helper:
mitmdump -s inspect_mmap.pyIt prints the framing, finds the URL inside the gzip records, and shows the
bytes around it. Check that the tag byte before the URL is the one you found
in step 3.
5. If the tag or framing differs, adjust patch_payload() in killswitch.py.
If you try another version, please open an issue or pull request with:
Maps version and date:
Result (works / fails / partly):
Device and Android version:
Did sign-in work:
Did inspect_mmap.py show tag 0x12 before the URL:
Anything you had to change in killswitch.py:
Confirmed versions can then be added to the table above.
| File | Purpose |
|---|---|
killswitch.py |
The mitmproxy addon that edits the response |
inspect_mmap.py |
Read-only helper for exploring responses on other versions |
The idea of looking for KillSwitchFragment came from Kenneth Jenkins, who patched the APK of Google Maps 7.0.2. Many thanks for the very helpful blog post!