A few days ago I got the idea to do some traffic analysis to determine how an unnamed Android app handles user authentication and chains it to ticket verification. From unpacking the APK, I already found the GraphQL endpoints and some hardcoded basic auth credentials I was after, but didn’t know for sure how the values were passed to these endpoints and in what order these endpoints were called in. Thus, I decided to dive deeper by sideloading the app into a rooted emulator (AVD) and intercepting the traffic with a MitM proxy.
In theory, intercepting HTTP(s) traffic is simple: make sure the device trusts your CA, configure it to route all traffic through your proxy, and simply wait for the results. In practice, especially with mobile apps, the bar is set slightly higher as apps employ a “TLS hardening” mechanism called certificate pinning, meaning the app embeds the expected certificate or public key directly into its code, rather than solely relying on the device’s trust store. Notably desktop apps like Spotify employ certificate pinning too, but it’s way more prevalent in the mobile app ecosystem.
A practical and relatively flexible solution to this is to use Frida to inject JavaScript payloads into the running app process and hook relevant methods at runtime. In my case, this meant bypassing OkHttp’s CertificatePinner, i.e. hooking the check(hostname, peerCertificates) function. Luckily, there’s already a comprehensive set of scripts for this purpose available on the internet, for example httptoolkit/frida-interception-and-unpinning.
So, I pushed Frida’s server component to the emulator, set up port forwarding, and injected the scripts into the target app:
adb push frida-server-17.7.0-android-arm64 /data/local/tmp/frida-server
adb shell chmod 755 /data/local/tmp/frida-server
adb reverse tcp:8080 tcp:8080 # assuming the traffic is routed to 127.0.0.1:8080
adb shell /data/local/tmp/frida-server &
frida -U \
-l ./config.js \
-l ./native-connect-hook.js \
-l ./native-tls-hook.js \
-l ./android/android-proxy-override.js \
-l ./android/android-system-certificate-injection.js \
-l ./android/android-certificate-unpinning.js \
-l ./android/android-certificate-unpinning-fallback.js \
-l ./android/android-disable-root-detection.js \
-f <PKG_ID>
So far, so good, I thought. Turns out mitmproxy (which has been my go-to tool for reversing APIs) wasn’t going to cut it in this case. I stumbled upon some unhandled certificate parsing/injection issues, as the CA certificates generated by mitmproxy are not “fully valid”. This can also be verified with crt.sh/lintcert:
cablint ERROR CA certificates must include countryName in subject
cablint NOTICE CA certificates without Digital Signature do not allow direct signing of OCSP responses
cablint INFO CA certificate identified
x509lint ERROR CA root certificate with Extended Key Usage
x509lint ERROR Issuer without countryName
x509lint ERROR Subject with organizationName, givenName or surname but without countryName
x509lint INFO Checking as root CA certificate
zlint ERROR Root and Subordinate CA certificates MUST have a countryName present in subject information
zlint ERROR Root CA Certificate: extendedKeyUsage MUST NOT be present
zlint NOTICE Root and Subordinate CA Certificates that wish to use their private key for signing OCSP responses will not be able to without their digital signature set
Knowing the set of scripts I was using was actually just a part of another proxying tool called HTTP Toolkit, I decided to see what the linter output would look like with a certificate generated by that tool. The difference was noticeable, to say the least, and could understandably lead to some compatibility issues:
cablint INFO CA certificate identified
x509lint INFO Checking as root CA certificate
I tried to research this issue further, but couldn’t find any unambiguous causes or solutions besides a few related discussions (e.g. #56).
Now, whether the issue was caused by this incompatibility between the tools I was using or some other minor issue in my setup that I had overlooked previously, I knew the base configuration was solid and would work for my simple use case (no obfuscation or additional anti-debugging measures).
As it seemed like the issue could be solved by using a single tool, in this case HTTP Toolkit, to do both the certificate handling on the device and the proxying on my host machine, I decided to give it a go despite the sluggish Electron app. And just like that, suddenly everything worked without any additional tinkering. This conclusion was partially why I ended up writing this post, as I wanted to remind my future self (or preferably anyone stumbling upon this same problem) to not waste time trying to work around this issue, but instead stick to the solution that achieves the same goal with drastically less effort.