Bypassing SSL Pinning in an App with a Custom HTTP Framework and a Non-System SSL Library

About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.

This research is published for educational and defensive purposes. The target sample is anonymized throughout.


Bypassing SSL Pinning in an App with a Custom HTTP Framework and a Non-System SSL Library

The dead ends

I grabbed a random e-book app off the store and it turned out to have more going on than expected. Charles threw this on the first capture:

First instinct: SSL pinning. Straightforward, right? Except nearly every public unpinning script failed, and dumping the certificate didn’t help either.

Unpacking the APK turned up okhttp3, so I assumed it was the stock OkHttp client. Hooking it proved otherwise — nothing matched. So I went deeper, hooking SSLOutputStream.write, and that also never fired. That’s when it clicked: this wasn’t the standard stack at all.

Breaking through at java.net.URL

With the obvious hooks dead, the fallback is to hook something lower and more fundamental in the networking path. java.net.URL finally opened the door:

The call stack shows the app built its own framework — a modified fork of OkHttp3. Some of those class names look familiar:

Those are OkHttp interceptors. The implementation differs from upstream, so I hooked the interceptor chain and dumped every interceptor class to see what it was actually running.

The last interceptor: callServerInterceptor

In stock OkHttp3, TLS lives in the second-to-last interceptor. This app doesn’t follow that layout, so the interesting one was the last interceptor, callServerInterceptor. Its executeCall method is the key — hooking right there already exposes the request body and the response, but since this was a learning exercise I kept going to understand how it worked underneath.

executeCall eventually reaches com.ttnet.org.chromium.net.impl.CronetUrlRequest$1, and from there it drops into native code:

Up to this point the Java layer never touches a certificate, so it’s reasonable to conclude the verification happens natively.

The non-system SSL library

I hooked SSL_write in libssl.so — and it never fired. That was alarming: does this app ship its own SSL library? Searching the loaded modules confirmed it. The app loads BoringSSL, Google’s open-source OpenSSL fork. Hooking SSL_write inside BoringSSL hit immediately, confirming the app talks TLS through its own bundled copy.

Following SSL_write’s call stack back, the caller lives in libsscronet.so. So I searched that library for imports of BoringSSL certificate-verification functions:

Two imports stand out: SSL_CTX_set_custom_verify and SSL_CTX_set_reverify_on_resume. Downloading the BoringSSL source and reading both confirms they’re exactly the certificate-verification hooks, and both get called.

The function signature takes a verification mode as its second argument:

and a callback as the third. The callback’s return value decides whether verification passes. Hook it:

One subtlety: you can’t simply replace the original callback with your own. You have to call the original once first, or things break — I didn’t bother digging into exactly why. With the hook in place, verification:

That’s the pinning bypassed. Since SSL_CTX_set_custom_verify alone did the job, I left SSL_CTX_set_reverify_on_resume for another day.

What the vendor got wrong

The app’s own defense is actually decent — a forked HTTP stack, a bundled BoringSSL, and native-side pinning that defeats generic unpinning scripts. But the weakness is structural: certificate verification is still a client-side callback whose return value the client controls. Once you find the SSL_CTX_set_custom_verify callback and force a passing return, the pin means nothing, because nothing server-side is re-verifying the trust decision. The durable fix is attestation — binding the certificate check to a server-side, hardware-backed decision rather than a return value in a library the user controls.


Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).

Get in touch