August 27, 2026
APK Impossible to Intercept: SSLHandshakeException, ProviderInstaller, and How We Got Burp Working…
A TLS debugging guide using Frida, OkHttp, Conscrypt, and GmsCore_OpenSSL for those cases where the problem is not SSL pinning.

By Grupo Oruss Security Research
9 min read
- Grupo Oruss | Division81
Some Android applications take only a few minutes to prepare for dynamic testing: configure Burp Suite, install the CA certificate, deal with certificate pinning if necessary, and start testing.
And then there are the others. This App was a tough one
During an authorized mobile security assessment at Division81 (Grupo Oruss Ethical Hackers Team), we encountered an Android build that worked perfectly on a physical device but became unusable inside our laboratory environment.
The application launched, displayed its splash screen, showed a generic error, and stopped progressing.
At first glance, it looked like one of the usual suspects:
- emulator detection;
- root detection;
- anti-Frida protection;
- SSL pinning;
- Network Security Config restrictions;
- proxy incompatibility;
- or some protection implemented by a third-party SDK.
It was none of them.
The actual problem existed several layers deeper, in the interaction between OkHttp, Google Play Services, Conscrypt, and ProviderInstaller.
This is how we traced it.
Privacy note:_ client names, domains, identifiers, endpoints, package names, and other environment-specific information have been removed or generalized. The application is referred to only as Division81 Mobile._
1. The Initial Symptom
Our test environment included:
- Android Emulator;
- Android 13 / API 33;
- x86_64 architecture;
- Frida;
- JADX;
- ADB;
- Burp Suite Professional;
- a production-equivalent Android build.
The APK installed correctly.
However, shortly after startup, the application displayed a generic dialog during the splash sequence and effectively became unusable.
The exact same APK worked correctly on a physical Android device.
That difference turned out to be extremely important.
The easiest conclusion would have been:
"The application detects the emulator."
That was certainly plausible.
But in mobile pentesting, a plausible hypothesis is not evidence.
So instead of trying random bypass scripts, we started tracing execution.
2. Was the Application Actually Crashing?
Our first observation was that Logcat did not show the usual indicators of a fatal application crash:
FATAL EXCEPTION
SIGSEGV
SIGABRT
Process has diedFATAL EXCEPTION
SIGSEGV
SIGABRT
Process has diedIn fact, parts of the application continued executing after the visible interface disappeared.
We therefore instrumented several termination-related Android methods with Frida:
Activity.finish()
Activity.finishAffinity()
Activity.finishAndRemoveTask()
System.exit()
Runtime.exit()
Process.killProcess()Activity.finish()
Activity.finishAffinity()
Activity.finishAndRemoveTask()
System.exit()
Runtime.exit()
Process.killProcess()The resulting stack trace looked approximately like this:
Activity.finish()
at Division81SplashActivity$Callback.onClick()
at CustomDialog$Listener.onClick()
at View.performClick()Activity.finish()
at Division81SplashActivity$Callback.onClick()
at CustomDialog$Listener.onClick()
at View.performClick()That immediately changed our interpretation.
finish() was not the cause of the failure.
It was merely the consequence of the user dismissing an error dialog.
We needed to move one step backwards.
3. Finding Who Displayed the Error
Next, we intercepted the dialog creation path.
The call stack revealed something similar to:
CustomDialog.show()
↓
SplashActivity.showError()
↓
SplashPresenter
↓
UseCasePriorityLauncherCustomDialog.show()
↓
SplashActivity.showError()
↓
SplashPresenter
↓
UseCasePriorityLauncherNow we had something much more useful.
The dialog was being triggered by the application's splash business logic rather than by a generic anti-tampering or anti-emulation routine.
We then hooked the application very early through:
Application.attach(Context)Application.attach(Context)This allowed us to install our instrumentation before most of the splash logic executed.
That exposed the first meaningful clue:
ApiErrorModel{
type='Error from app',
title='Error because server not respond',
status=555,
detail='Artificial Error because server not respond',
message='...'
}ApiErrorModel{
type='Error from app',
title='Error because server not respond',
status=555,
detail='Artificial Error because server not respond',
message='...'
}The HTTP-looking status code 555 was not being returned by the backend.
It was generated locally by the application.
In other words:
555 ≠ server response
555 = application-side synthetic error555 ≠ server response
555 = application-side synthetic errorThat meant the real exception was still hidden deeper in the networking layer.
4. Retrofit Was Hiding the Real Failure
We followed the construction of the error object.
The call path eventually looked approximately like:
ApiErrorModel()
↓
ApiErrorModel.create()
↓
RetrofitCallbacks.onFailure()
↓
DefaultCallAdapterFactoryApiErrorModel()
↓
ApiErrorModel.create()
↓
RetrofitCallbacks.onFailure()
↓
DefaultCallAdapterFactoryThat was the point we needed.
We instrumented the Retrofit callback receiving the underlying:
ThrowableThrowableThis time, the real exception appeared:
javax.net.ssl.SSLHandshakeException:
Connection closed by peerjavax.net.ssl.SSLHandshakeException:
Connection closed by peerMore importantly, the stack trace told us which TLS implementation was actually being used:
com.google.android.gms.org.conscrypt.NativeCrypto.SSL_do_handshake
com.google.android.gms.org.conscrypt.NativeSsl.doHandshake
com.google.android.gms.org.conscrypt.ConscryptFileDescriptorSocket.startHandshake
okhttp3.internal.connection.RealConnection.connectTls
okhttp3.internal.connection.RealConnection.establishProtocol
okhttp3.internal.connection.RealConnection.connect
...
Retrofitcom.google.android.gms.org.conscrypt.NativeCrypto.SSL_do_handshake
com.google.android.gms.org.conscrypt.NativeSsl.doHandshake
com.google.android.gms.org.conscrypt.ConscryptFileDescriptorSocket.startHandshake
okhttp3.internal.connection.RealConnection.connectTls
okhttp3.internal.connection.RealConnection.establishProtocol
okhttp3.internal.connection.RealConnection.connect
...
RetrofitWe finally had a concrete technical failure.
It was not:
Certificate pinning failureCertificate pinning failureIt was not:
Trust anchor for certification path not foundTrust anchor for certification path not foundAnd it was not:
CertPathValidatorExceptionCertPathValidatorExceptionInstead, the remote peer was closing the connection during the TLS handshake.
5. The Comparison That Eliminated Half Our Hypotheses
At this point we ran an extremely simple test.
From Chrome running inside the same Android Emulator, we accessed the same laboratory backend:
https://api.division81.grupooruss.com/...https://api.division81.grupooruss.com/...It worked.
So we now had:
Same AVD
Same network
Same destination
Chrome
→ TLS OK
Application
→ SSLHandshakeExceptionSame AVD
Same network
Same destination
Chrome
→ TLS OK
Application
→ SSLHandshakeExceptionThat allowed us to significantly reduce the search space.
The problem was unlikely to be:
- DNS;
- routing;
- the emulator's Internet connectivity;
- a global firewall rule;
- backend availability;
- a generally invalid public certificate;
- or a general Android Emulator TLS problem.
The key difference had to be the TLS stack being used by the application.
6. Reviewing the OkHttp Client
Static analysis with JADX showed a fairly conventional Retrofit/OkHttp configuration:
OkHttpClient.Builder builder =
new OkHttpClient.Builder();
builder
.connectTimeout(120, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS);OkHttpClient.Builder builder =
new OkHttpClient.Builder();
builder
.connectTimeout(120, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS);Several application interceptors were present:
AuthorizationInterceptor
RandomHeaderInterceptor
AuditInterceptor
EncryptInterceptorAuthorizationInterceptor
RandomHeaderInterceptor
AuditInterceptor
EncryptInterceptorBut we did not find explicit configuration such as:
sslSocketFactory(...)
certificatePinner(...)
hostnameVerifier(...)
connectionSpecs(...)
protocols(...)sslSocketFactory(...)
certificatePinner(...)
hostnameVerifier(...)
connectionSpecs(...)
protocols(...)One interceptor deserved additional attention because it received an:
OkHttpClient.BuilderOkHttpClient.Builderdirectly.
After reviewing its implementation, however, we found that it only modified timeouts and created an additional Retrofit client.
It was not replacing the TLS stack.
This changed the question entirely:
If OkHttp was not explicitly selecting Google Conscrypt, who was installing it?
7. Enter ProviderInstaller
Static analysis revealed the presence of:
ProviderInstallerProviderInstallerfrom Google Play Services.
Conceptually, the code performs something equivalent to:
ProviderInstaller.installIfNeeded(context);ProviderInstaller.installIfNeeded(context);Internally, Google Play Services can dynamically load:
com.google.android.gms.providerinstaller.dynamitecom.google.android.gms.providerinstaller.dynamiteand invoke:
ProviderInstallerImpl.insertProvider(Context)ProviderInstallerImpl.insertProvider(Context)The application itself was obfuscated, meaning that the runtime method name did not necessarily remain:
installIfNeededinstallIfNeededThis is an important detail when reproducing this type of analysis.
Do not rely exclusively on the documented Java API name.
JADX may display something like:
JADX INFO: renamed from ...JADX INFO: renamed from ...The actual runtime class and method names are what matter when building the Frida hook.
8. The Critical Test: Security.getProviders()
At this stage we decided to inspect Java's active security providers directly.
Before ProviderInstaller executed, the provider list looked roughly like this:
========== SECURITY PROVIDERS ==========
[0] AndroidNSSP
[1] AndroidOpenSSL
[2] CertPathProvider
[3] AndroidKeyStoreBCWorkaround
[4] BC
[5] HarmonyJSSE
[6] AndroidKeyStore========== SECURITY PROVIDERS ==========
[0] AndroidNSSP
[1] AndroidOpenSSL
[2] CertPathProvider
[3] AndroidKeyStoreBCWorkaround
[4] BC
[5] HarmonyJSSE
[6] AndroidKeyStoreThen we traced the ProviderInstaller execution.
Immediately afterward:
========== SECURITY PROVIDERS ==========
[0] GmsCore_OpenSSL
[1] AndroidNSSP
[2] AndroidOpenSSL
[3] CertPathProvider
[4] AndroidKeyStoreBCWorkaround
[5] BC
[6] HarmonyJSSE
[7] AndroidKeyStore========== SECURITY PROVIDERS ==========
[0] GmsCore_OpenSSL
[1] AndroidNSSP
[2] AndroidOpenSSL
[3] CertPathProvider
[4] AndroidKeyStoreBCWorkaround
[5] BC
[6] HarmonyJSSE
[7] AndroidKeyStoreThere it was.
ProviderInstaller had inserted:
GmsCore_OpenSSLGmsCore_OpenSSLas security provider number 0.
And our TLS exception had already shown execution inside:
com.google.android.gms.org.conscrypt.NativeCryptocom.google.android.gms.org.conscrypt.NativeCryptoAt this point the correlation was strong:
ProviderInstaller
↓
GmsCore_OpenSSL
↓
GMS Conscrypt
↓
SSLHandshakeExceptionProviderInstaller
↓
GmsCore_OpenSSL
↓
GMS Conscrypt
↓
SSLHandshakeExceptionBut correlation was still not enough.
We wanted causality.
9. The A/B Test
We did not want to patch the APK permanently.
Instead, we used Frida to temporarily turn the ProviderInstaller call into a no-op.
Conceptually:
Java.perform(function () {
var ProviderInstaller =
Java.use("<runtime ProviderInstaller class>");
var install =
ProviderInstaller
.<runtime_install_method>
.overload("android.content.Context");
install.implementation = function (context) {
console.log(
"[+] ProviderInstaller blocked"
);
// Intentionally do not invoke the original method.
return;
};
});Java.perform(function () {
var ProviderInstaller =
Java.use("<runtime ProviderInstaller class>");
var install =
ProviderInstaller
.<runtime_install_method>
.overload("android.content.Context");
install.implementation = function (context) {
console.log(
"[+] ProviderInstaller blocked"
);
// Intentionally do not invoke the original method.
return;
};
});In our case, the real class and method names were obfuscated and had first been identified through static analysis.
Then we compared both executions.
Test A — ProviderInstaller enabled
ProviderInstaller
↓
GmsCore_OpenSSL
↓
SSLHandshakeException
↓
Application unavailableProviderInstaller
↓
GmsCore_OpenSSL
↓
SSLHandshakeException
↓
Application unavailableTest B — ProviderInstaller blocked
ProviderInstaller blocked
↓
AndroidOpenSSL remains active
↓
TLS succeeds
↓
Login screenProviderInstaller blocked
↓
AndroidOpenSSL remains active
↓
TLS succeeds
↓
Login screenThe authentication screen appeared immediately.
That was our "there it is" moment.
We had demonstrated that the failure was directly associated with the security provider dynamically installed at runtime.
10. Why This Was Not Just SSL Pinning
When an Android application refuses to communicate in a pentesting environment, certificate pinning is often the first suspect.
And sometimes it is the correct one.
But blindly treating every TLS failure as pinning can waste a considerable amount of time.
In our case, we had no evidence of the classic pinning failure patterns.
Instead, the useful evidence was:
SSLHandshakeException:
Connection closed by peerSSLHandshakeException:
Connection closed by peercombined with:
com.google.android.gms.org.conscrypt.NativeCrypto.SSL_do_handshakecom.google.android.gms.org.conscrypt.NativeCrypto.SSL_do_handshakeand finally:
ProviderInstaller
→ GmsCore_OpenSSLProviderInstaller
→ GmsCore_OpenSSLThe lesson was simple:
Always identify the actual TLS implementation and the actual exception before selecting the bypass technique.
11. Building a Pentest-Friendly Android Environment
Solving the TLS failure did not automatically give us HTTPS interception.
We still needed an Android environment that allowed us to trust Burp's CA at the system level.
We created an AVD using:
Android 13 / API 33
x86_64
Google APIsAndroid 13 / API 33
x86_64
Google APIsWe deliberately used a laboratory image that allowed:
adb rootadb rootThe expected result was:
uid=0(root)
gid=0(root)uid=0(root)
gid=0(root)This gave us sufficient control over the emulator to modify the system CA store.
12. Starting the Emulator with a Writable System Partition
To modify the Android system certificate store, the emulator was started with:
emulator @Pentest_AVD \
-writable-system \
-no-snapshotemulator @Pentest_AVD \
-writable-system \
-no-snapshotThen:
adb root
adb disable-verity
adb reboot
adb root
adb remountadb root
adb disable-verity
adb reboot
adb root
adb remountThe important result was:
remount succeededremount succeededAt that point /system could be modified for the laboratory session.
13. Installing the Burp CA as an Android System CA
We exported the Burp Suite CA certificate in DER format and converted it to PEM:
openssl x509 \
-inform DER \
-in burp-ca.der \
-out burp-ca.pemopenssl x509 \
-inform DER \
-in burp-ca.der \
-out burp-ca.pemNext we calculated the legacy subject hash Android expects:
openssl x509 \
-subject_hash_old \
-in burp-ca.pemopenssl x509 \
-subject_hash_old \
-in burp-ca.pemFor example:
9a5ba5759a5ba575The certificate was renamed:
burp-ca.pem
→
9a5ba575.0burp-ca.pem
→
9a5ba575.0and copied into Android's system CA directory:
adb push 9a5ba575.0 \
/system/etc/security/cacerts/9a5ba575.0adb push 9a5ba575.0 \
/system/etc/security/cacerts/9a5ba575.0Then we applied the required permissions:
adb shell chmod 644 \
/system/etc/security/cacerts/9a5ba575.0adb shell chmod 644 \
/system/etc/security/cacerts/9a5ba575.0and ownership:
adb shell chown root:root \
/system/etc/security/cacerts/9a5ba575.0adb shell chown root:root \
/system/etc/security/cacerts/9a5ba575.0Finally:
adb rebootadb reboot14. Routing the Android Emulator Through Burp
When Burp runs on the host machine, the Android Emulator exposes a special address:
10.0.2.210.0.2.2This maps to the host system.
We therefore configured the Android global proxy:
adb shell settings put global \
http_proxy 10.0.2.2:8080adb shell settings put global \
http_proxy 10.0.2.2:8080Verification:
adb shell settings get global http_proxyadb shell settings get global http_proxyExpected result:
10.0.2.2:808010.0.2.2:8080Chrome inside the AVD could now browse HTTPS sites while Burp successfully intercepted the requests.
The final laboratory chain became:
Android Application
↓
ProviderInstaller bypass
↓
AndroidOpenSSL
↓
System-trusted Burp CA
↓
Android proxy
↓
Burp Suite
↓
Application backendAndroid Application
↓
ProviderInstaller bypass
↓
AndroidOpenSSL
↓
System-trusted Burp CA
↓
Android proxy
↓
Burp Suite
↓
Application backendAnd this time:
the application remained fully operational while its HTTPS traffic was visible in Burp.
15. The Investigation Path Matters More Than the Script
The most valuable part of this case was not the final Frida snippet.
It was the investigation sequence.
At different points we had reasonable reasons to suspect:
Emulator detection
Root detection
Anti-Frida
SSL pinning
Network Security Config
Third-party SDK protection
DNS
WAF
Routing
TLS configurationEmulator detection
Root detection
Anti-Frida
SSL pinning
Network Security Config
Third-party SDK protection
DNS
WAF
Routing
TLS configurationBut instead of attempting every bypass we could find online, we progressively followed the evidence.
Our actual path was:
Splash error
↓
Activity.finish()
↓
CustomDialog
↓
SplashPresenter
↓
Synthetic ApiErrorModel 555
↓
Retrofit onFailure()
↓
SSLHandshakeException
↓
GMS Conscrypt
↓
ProviderInstaller
↓
GmsCore_OpenSSLSplash error
↓
Activity.finish()
↓
CustomDialog
↓
SplashPresenter
↓
Synthetic ApiErrorModel 555
↓
Retrofit onFailure()
↓
SSLHandshakeException
↓
GMS Conscrypt
↓
ProviderInstaller
↓
GmsCore_OpenSSLOnly after reaching the bottom of that chain did we modify the runtime behavior.
That distinction matters.
16. Synthetic Error Codes Are Clues, Not Root Causes
Another useful lesson came from the application's error handling.
The user-facing error essentially meant:
Server did not respondServer did not respondBut the real exception was:
SSLHandshakeException:
Connection closed by peerSSLHandshakeException:
Connection closed by peerDuring dynamic mobile testing, application-level error models often hide the original networking exception.
If you encounter proprietary codes such as:
555
999
-1
NETWORK_ERROR
GENERIC_ERROR555
999
-1
NETWORK_ERROR
GENERIC_ERRORdo not stop there.
Trace them until you reach something closer to the transport layer:
Throwable
IOException
SSLException
SocketException
ConnectExceptionThrowable
IOException
SSLException
SocketException
ConnectExceptionThat is usually where the useful investigation begins.
17. Is This a Vulnerability?
Not necessarily.
That distinction is important.
Blocking ProviderInstaller during our test was an environment-enablement technique.
It should not automatically be reported as a security vulnerability.
The behavior we observed resulted from the interaction between:
application build
+
Android Emulator environment
+
Google Play Services provider
+
GMS Conscrypt
+
remote TLS endpointapplication build
+
Android Emulator environment
+
Google Play Services provider
+
GMS Conscrypt
+
remote TLS endpointThe Frida hook allowed us to restore a working analysis environment.
What we subsequently discover through that environment may of course become a legitimate finding, such as:
- missing certificate pinning;
- excessive trust configuration;
- broken authorization;
- sensitive information exposure;
- weak session management;
- BOLA/IDOR;
- business logic vulnerabilities.
But the technique used to reach the attack surface should not be confused with the vulnerability eventually found on that attack surface.
18. A Quick Troubleshooting Checklist
If an Android application works on a physical phone but fails inside an AVD during HTTPS communication:
- Do not immediately assume SSL pinning.
- Test the same backend from Chrome inside the same emulator.
- Capture the real
Throwablereaching Retrofit or OkHttp. - Distinguish between:
SSLPeerUnverifiedException
CertPathValidatorException
SSLHandshakeException
SocketTimeoutException
ConnectExceptionSSLPeerUnverifiedException
CertPathValidatorException
SSLHandshakeException
SocketTimeoutException
ConnectException- Inspect:
Security.getProviders()Security.getProviders()- Check whether:
ProviderInstallerProviderInstallerexecutes during startup.
-
Compare security provider ordering before and after execution.
-
If:
GmsCore_OpenSSLGmsCore_OpenSSLbecomes provider 0, perform a controlled A/B test.
- Prefer runtime instrumentation over permanently patching the APK when possible.
- Once the application communicates correctly, configure your proxy, system CA, and remaining interception environment.
19. Why These Cases Are Worth Documenting
A superficial assessment could easily have ended with:
"The application could not be executed correctly inside an instrumented environment."
But that would have left most of the application's dynamic attack surface unexplored.
A pentest is not about proving that our tools work.
It is about obtaining enough visibility to evaluate the actual security behavior of the target.
Sometimes reaching that visibility means traveling from:
ActivityActivityto:
RetrofitRetrofitthen:
OkHttpOkHttpthen:
Java Security ProviderJava Security Providerand eventually all the way down to:
NativeCrypto.SSL_do_handshake()NativeCrypto.SSL_do_handshake()before the first meaningful request ever reaches Burp.
That is part of the job.
Conclusion
At first, the application appeared to resist dynamic instrumentation.
The real problem turned out to be quite different:
a TLS incompatibility associated with the security provider dynamically installed during application startup.
The critical evidence could eventually be reduced to this:
ProviderInstaller ON
→ GmsCore_OpenSSL
→ SSLHandshakeException
ProviderInstaller OFF
→ AndroidOpenSSL
→ TLS OK
→ Application functional
→ Burp interception availableProviderInstaller ON
→ GmsCore_OpenSSL
→ SSLHandshakeException
ProviderInstaller OFF
→ AndroidOpenSSL
→ TLS OK
→ Application functional
→ Burp interception availableThis is not a revolutionary bypass.
It is not a new Android attack.
But it is exactly the type of problem that can consume hours during a mobile assessment when the investigation begins with the wrong assumption.
The reusable lesson is not the Frida script.
It is the methodology:
observe → isolate → instrument → compare → prove causality → change only what is necessary.
And then, finally:
open Burp and start the real pentest!
Happy Hacking!
Source code & Frida PoC
The sanitized Frida proof-of-concept used in this research, together with troubleshooting notes and provider inspection examples, is available on GitHub:
GitHub → frida-android-providerinstaller-bypass
Research: Division81 Research Observe → isolate → instrument → compare → prove causality.
- Grupo Oruss | Division81 https://grupooruss.com