Man-in-the-middling Android apps

This is a walk-through of how I go about investigating Android apps. I’m not a subject matter expert, so take everything here with a pinch of salt. As a case-study, I’ll look at the Railcards app which aims to replace physical railcards with ones which can run out of battery.

I have no relationship with the Rail Delivery Group, and I won’t be conducting an unsolicited penetration test. Nor should you. I will however man-in-the-middle the app so I can see all the communication between the app and its server. Combined with reverse-engineering, I can then understand how the app and service work.

Development Environment

In this section we’ll set up a proxy server which will allow you to inspect app communication to and from your Android device (physical or emulator).

Start your proxy

Install ZAP (or mitmproxy or equivalent). In Options > Local Proxies, choose the correct address. This is probably localhost to proxy emulator traffic, or your network adapter IP to proxy traffic from your phone.

Emu

It’s helpful to have an Android emulator to play with. You don’t need to be scared about breaking anything with an emulator and you can choose whichever Android version you want. As a starting point, I recommend the Genymotion Personal Edition.

You want to be able to connect to your device with adb which allows you to e.g. get a shell adb shell. For a physical device, install the Android SDK Platform Tools. Genymotion emulator has it’s own version of adb.

Route Wi-Fi traffic through proxy

You need to modify the advanced options of your device’s Wi-Fi connection. Set the proxy to Manual and enter the hostname and port. For the Genymotion emulator, put the special IP 10.0.3.2 for the hostname. Otherwise you probably want the IP of your ZAP proxy. I often put *.google.com,*.googleapis.com,*.gstatic.com in the bypass section if I need Google services and things are broken.

Android proxy setup
Android proxy setup

Now, when you open your device’s browser and visit http://example.com, you should see the request and response appear in ZAP. When you visit https://caller.xyz or another HTTPS site, the page should load but with a certificate error.

Install ZAP CA certificate

Clients can access HTTPS servers via the proxy using the CONNECT method. A normal proxy should allow creation of a secure client-server tunnel where the proxy can’t read the plaintext communication. ZAP behaves differently: it connects the client and server via a server-ZAP HTTPS connection and a ZAP-client HTTPS connection. Using two separate connections, ZAP can intercept plaintext communication from the server before forwarding it to the client and vice versa.

Since ZAP doesn’t have a valid certificate to pretend to be the server (in the ZAP-client connection), it dynamically generates a certificate. This would be flagged as insecure as it isn’t signed by a trusted CA. To get around this, add your auto-generated ZAP CA certificate to your device’s certificate store.

  1. Export ZAP certificate: Options > Dynamic SSL Certificates > Save
  2. Copy it over to the device with adb: adb push path/to/zap.cer
  3. In your device’s security settings there should be an option like “Install from SD card” which allows the certificate to be installed.

Now the browser should load HTTPS sites without showing a certificate error.

Poking the app

Get the apk on your computer

Search for the apk. There are many sites hosting them. Alternatively, download it to your device from the Play store and pull it via adb. I used v1.1.3 with SHA256 b45a5528922eadfed49e38039cff6365aacd8370e98b3d27d5bab2f53690811a.

Install the apk

Install it with adb install railcards.apk. Run the app. Hmmmm it doesn’t work.

Apps says it's offline
Apps says it’s offline

The internet is working in the browser, but no requests from the Railcards app can be seen in ZAP. Wireshark shows the emulator connecting to ZAP, but the ZAP certificate is being rejected by the app.

Wireshark shows TLS connection between ZAP and emulator
Wireshark shows TLS connection between ZAP and emulator

Tear it apart

Let’s decode the apk with apktool and see what’s going on apktool d --no-res railcards.apk.

Android code is compiled, obfuscated with proguard, and targeted at Dalvik rather than the standard JVM. This means that the tooling for reverse engineering rarely gives you perfect decompilation into Java. A reasonable attempt at browsing the Java decompilation is by running dex2jar railcards.apk and then opening the railcards_dex2jar.jar in jd-gui. I searched around for the code which makes requests, and in com.raildeliverygroup.railcard.app.net.b found:

public class a
{
  CertificatePinner c()
  {
    return new CertificatePinner.Builder()
      .add("prod.digital-railcard.co.uk", new String[] { "sha256/rfNAS9FMyvxACLmHPLTQIHnFNs+MIde1t7Vcym6qMM4=" })
      .add("prod.digital-railcard.co.uk", new String[] { "sha256/5kJvNEMw0KjrCAu7eXY5HZdvyCS13BbA0VJG1RSP91w=" })
      .build();
  }
}
The first reason we’re getting connection errors is due to the use of certificate pinning as described in the OkHttp library docs. The app requires the connection to be made using specific certificates. Since our dynamically-generated ZAP certificate doesn’t have those hashes in its chain of trust, validation fails.

Get rid of the certificate pinning

Apktool created a smali directory. Smali/baksmali is the assembly language for the Dalvik machine code. As such, browsing the smali code is much more reliable than looking at the Java, and it can be reassembled. The smali file at smali/com/raildeliverygroup/railcard/app/net/b/a.smali (originally from NetworkModule.java) contains:

.method c()Lokhttp3/CertificatePinner;
    .locals 6
    .prologue
    const/4 v5, 0x1
    const/4 v4, 0x0
    .line 48
    new-instance v0, Lokhttp3/CertificatePinner$Builder;
    invoke-direct {v0}, Lokhttp3/CertificatePinner$Builder;-><init>()V
    const-string v1, "prod.digital-railcard.co.uk"
    new-array v2, v5, [Ljava/lang/String;
    const-string v3, "sha256/rfNAS9FMyvxACLmHPLTQIHnFNs+MIde1t7Vcym6qMM4="
    aput-object v3, v2, v4
    .line 49
    invoke-virtual {v0, v1, v2}, Lokhttp3/CertificatePinner$Builder;->add(Ljava/lang/String;[Ljava/lang/String;)Lokhttp3/CertificatePinner$Builder;
    move-result-object v0
    const-string v1, "prod.digital-railcard.co.uk"
    new-array v2, v5, [Ljava/lang/String;
    const-string v3, "sha256/5kJvNEMw0KjrCAu7eXY5HZdvyCS13BbA0VJG1RSP91w="
    aput-object v3, v2, v4
    .line 50
    invoke-virtual {v0, v1, v2}, Lokhttp3/CertificatePinner$Builder;->add(Ljava/lang/String;[Ljava/lang/String;)Lokhttp3/CertificatePinner$Builder;
    move-result-object v0
    .line 51
    invoke-virtual {v0}, Lokhttp3/CertificatePinner$Builder;->build()Lokhttp3/CertificatePinner;
    move-result-object v0
    .line 48
    return-object v0
.end method

I kept the method signature the same, but deleted the lines specifying the pins, so the method now returns an empty, impotent CertificatePinner. Removing all 12 command lines after the call to init and before the call to build left code which functions like return new CertificatePinner.Builder().build().

Rebuild and sign the apk

The altered smali can now be reassembled into machine code to make an apk with altered code. Since we don’t have the proper apk signing key:

  • we’ll need to use our own one (keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000)
  • we need to uninstall the original apk before we can install our cooked one
apktool b -f -d com.raildeliverygroup.railcard_2018-05-30 && jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-release-key.keystore com.raildeliverygroup.railcard_2018-05-30/dist/com.raildeliverygroup.railcard_2018-05-30.apk alias_name && adb uninstall com.raildeliverygroup.railcard && adb install com.raildeliverygroup.railcard_2018-05-30/dist/com.raildeliverygroup.railcard_2018-05-30.apk

On my emulator I can now see traffic 🎉, but I can’t get it to work on my phone. This is due to a change since Nougat (7.0 or API level 24). Apps now don’t respect that user CA store we added the ZAP cert to earlier. The apps only use the root CAs, or they can use their own custom request handling.

Patch TLS certificate validation

Since I don’t have the private key of a trusted CA, I instead made the app not care about invalid certificates. It was a bit more involved than the pinning change, so I wrote the Android java code I wanted first. You can play with it at bcaller/unsafe-okhttp3-android. Then I decoded the apk to get the smali code I needed for the railcards project so that the method OkHttpClient.Builder b() returns a builder with a special custom trust model of accepting all certs. If you’re interested in learning smali, writing Android Java code and disassembling the apk to smali is the second best way (after reading the smali docs).

Oops: It may have been easier to just add network security config to AndroidManifest.xml

Profit

It works
It works

Now we have MitM’d the connection, we can see what the app is saying about us and to whom!

More poking

Secret password

If you make a request to https://prod.digital-railcard.co.uk/api/v0.0.2/devices/a/railcards then you get a 401. Browsing the code, I found smali/com/raildeliverygroup/railcard/app/net/c/a.smali (originally AuthorizationInterceptor.java) containing some amusing code.

public class a implements Interceptor
{
  public Response intercept(Interceptor.Chain paramChain)
  {
    return paramChain.proceed(
      paramChain.request().newBuilder()
                .header("Authorization", "Basic YXBwbGljYXRpb25AZXhhbXBsZS5jb206dGVzdHJhaWxhcGk=").build()
    );
  }
}

Hard-coded basic auth creds: application@example.com:testrailapi. It’s not really a security issue since this had to be baked into the client app anyway.

Data

To see and modify the app’s data, try looking for XML and sqlite3 files in /data/data/com.raildeliverygroup.railcard. Nothing particularly interesting to report for this app.

DIY-railcard forgery / Saved by the barcode

We can also use ZAP to rewrite intercepted responses (start by setting break points) to give our app a shiny new railcard:

{
    "data": [
        {
            "cardholders": [
                {
                    "cardholder_forename": "Thomas",
                    "cardholder_id": 1111111,
                    "cardholder_photo_url": "https://crossvale.com/wp-content/uploads/2018/10/ttte.jpg",
                    "cardholder_surname": "Engine",
                    "cardholder_title": "Mr",
                    "cardholder_type": "Primary"
                }
            ],
            "railcard_barcode": "05XTNVW5BYQ 2ABCDEFGHIJKLMNOPQRSTUVWXYZAAAAAAAAAABCDEFGHIJKLMNOPQRSTUVWXYZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCDEFGHIJKLMNOPQRSTUVWXYZAAAAAAAAAAAAAAAAAAABCDEFGHIJKLMNOPQRSTUVWXYZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
            "railcard_id": "b16b00b5-ffff-ffff-ffff-123456789abc",
            "railcard_issued": "Online",
            "railcard_name": "TwentysixToThirty",
            "railcard_number": "05ABC0123456789",
            "railcard_requested_date": "2019-01-19T11:11:11+00:00",
            "railcard_transaction_reference": "55555555555555555555555555555",
            "railcard_type": "TwentySixToThirty",
            "railcard_valid_from": "2019-01-19T00:00:00+00:00",
            "railcard_valid_to": "3000-01-18T00:00:00+00:00",
            "state": "Active",
            "token": "ABCDEF"
        }
    ],
    "http_status_code": 200,
    "message": "Success",
    "status": "success"
}

Choo Choo Motherf****r
Choo Choo Motherf****r

The National Rail logo in the bottom-right corner is the SecurityFeatureView which flips when you tap it, and changes colour to show it isn’t just a screenshot (the anti-screenshot protection can be disabled by removing FLAG_SECURE in android.view.window.setFlags(0x2000)). While this railcard looks legit, there is nothing in the app describing the contents of the railcard barcode. This is the key security feature of the system. Depending on its purpose and contents, a forged railcard will be rejected by any inspector who scans the AZTEC (not QR) barcode. Since I don’t know what data the barcode contains, I am unable to forge my own railcard without risking detection.