Bypassing Cert Pinning in the Steam Mobile App

tl;dr: Use apktool and jadx to identify and remove cert pinning code so we can MITM the app to watch its network requests.

Routing a device’s web traffic through a proxy like mitmproxy is a great first step in reverse engineering a mobile app’s API. However, some apps protect against these types of man-in-the-middle attacks (whether the attacker is the user or a shady network admin trying to snoop) using certificate pinning, which enforces a policy rejecting all certificates other than the one hardcoded into the app itself. One such app is Steam‘s mobile app, and in this post, I’ll disassemble and rebuild it with modified code using jadx and apktool.

'chat is offline' and an empty screen

Steam mobile app naively routed through mitmproxy

Obtaining the APK

There are some sketchy websites out there that allow you to directly download an app’s APK, but I’ll pull the file from my phone to be safe.

With ADB shell, find out the name of the Steam app’s APK:

device:/ $ pm list packages | grep valve
package:com.valvesoftware.android.steam.community

Then, find its location:

device:/ $ pm path com.valvesoftware.android.steam.community
package:/data/app/com.valvesoftware.android.steam.community-1/base.apk

From my PC, ADB pull the file from my device and rename it to steam.apk:

$ adb pull /data/app/com.valvesoftware.android.steam.community-1/base.apk steam.apk

Disassembling the APK using apktool

apktool is a great piece of software that can disassemble an APK into editable smali code that’s like assembly/bytecode but not.

I’ve never used it before, but following the sample usage on the site,

$ apktool d steam.apk
$ cd steam
$ ls
assets  original  res  smali  unknown  AndroidManifest.xml  apktool.yml

Finding the certificate pinning code

I don’t know what I’m looking for, but whatever code controls certificate pinning probably contains something about ssl so I’ll start there.

$ grep -ri ssl

I get matches in a bunch of internal android libraries, the web view client, and files called DevHttpsTrustManager.smali and DevHttpsTrustManager$1.smali (the anonymous inner classes of DevHttpsTrustManager). DevHttpsTrustManager? That sounds promising! I’ll pop it open in my favorite text editor, and take a look!

Here are copies of the files:

DevHttpsManager.smali and DevHttpsManager$1.smali

I don’t actually know what I’m looking at here, so I’ll go one level of abstraction up, and use jadx to look at some sweet java code. The catch is that jadx can only view, but not edit the decompiled java.

Looking at the code in jadx

$ jadx-gui steam.apk
a screenshot of jadx displaying a decompiled view of DevHttpsTrustManager

decompiled APK in jadx

Here’s a copy of the decompiled code.

The relevant snippet:

public boolean verify(String arg0, SSLSession arg1) {
    return arg0 != null && (arg0.contains("valvesoftware.com") || arg0.contains("valve.org"));
}

Looks like it’s just checking arg0, whatever that is, for if it contains valvesoftware.com or valve.org (note: this usually isn’t how you do cert pinning, and I’m almost certain this is a vulnerability). I remember seeing those strings in DevHttpsManager$1.smali!

Editing the smali code

I remember that string.contains("") always evaluates to true, so as a quick hack, replacing

const-string v0, "valvesoftware.com"
...
const-string v0, "valve.org"

with

const-string v0, ""
...
const-string v0, ""

should make the conditional evaluate to true, and I’ve bypassed it!

Rebuilding the APK

Now that I’ve edited the code, I’m ready to rebuild an APK to try it out:

$ apktool b steam

The built APK is in steam/dist, but it’s not ready to install yet. All Android apps must be signed, so sign them by following Google’s documentation (I won’t include the steps here since they’re boring).

ADB push the built and signed APK onto my phone:

$ adb push steam.apk /sdcard

Uninstall the official Steam app, install my own modified APK, start up and connect to mitmproxy, and…

mitmproxy successfully showing Steam's network requests

We're in!