Context
SSL Pinning ensures your app only accepts connections with specific certificates. Attackers use proxies (Burp, Charles) or Frida to circumvent this mechanism. This snippet detects bypass signals at runtime.
1. Proxy / Man-in-the-Middle Detector
Checks whether an HTTP proxy is configured on the device — a strong indicator of an interception attempt.
object ProxyDetector {
fun isProxySet(): Boolean {
val proxyHost = System.getProperty("http.proxyHost")
val proxyPort = System.getProperty("http.proxyPort")
if (!proxyHost.isNullOrBlank() && proxyPort != null) {
return true
}
// Check via NetworkInfo (API 29+)
val defaultProxy = ProxyInfo.buildDirectProxy("", 0)
return defaultProxy.host?.isNotBlank() == true
}
}
2. Frida Detector
Frida injects a native library (frida-agent) into the process. We detect it by checking for the presence of known files and by probing ports typically used by Frida Server.
object FridaDetector {
fun isFridaPresent(): Boolean {
return checkFridaFiles() || checkFridaPorts() || checkFridaThreads()
}
private fun checkFridaFiles(): Boolean {
val fridaPaths = listOf(
"/data/local/tmp/frida-server",
"/data/local/tmp/re.frida.server",
"/system/bin/frida-server",
)
return fridaPaths.any { File(it).exists() }
}
private fun checkFridaPorts(): Boolean {
// Frida Server listens on port 27042 by default
return try {
Socket().use { socket ->
socket.connect(InetSocketAddress("127.0.0.1", 27042), 200)
true
}
} catch (e: Exception) {
false
}
}
private fun checkFridaThreads(): Boolean {
return try {
val maps = File("/proc/self/maps").readText()
maps.contains("frida") || maps.contains("gum-js-loop")
} catch (e: Exception) {
false
}
}
}
3. Additional Certificate Validation in OkHttp
Beyond standard pinning, compare the fingerprint of the received certificate with the expected value:
class CertificateValidator : X509TrustManager {
// SHA-256 fingerprint of the expected public certificate
private val expectedFingerprint = "AA:BB:CC:DD:..."
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
val serverCert = chain[0]
val actualFingerprint = serverCert.fingerprint()
if (actualFingerprint != expectedFingerprint) {
throw CertificateException("Invalid certificate: possible MITM detected")
}
}
private fun X509Certificate.fingerprint(): String {
val digest = MessageDigest.getInstance("SHA-256")
val bytes = digest.digest(encoded)
return bytes.joinToString(":") { "%02X".format(it) }
}
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
}
4. Integration at App Startup
class App : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG.not()) {
runSecurityChecks()
}
}
private fun runSecurityChecks() {
when {
FridaDetector.isFridaPresent() -> {
reportThreat("frida_detected")
exitProcess(1)
}
ProxyDetector.isProxySet() -> {
reportThreat("proxy_detected")
// Can terminate or just alert — a product decision
}
}
}
private fun reportThreat(type: String) {
// Send event to your security backend
// Do not block on the main thread
CoroutineScope(Dispatchers.IO).launch {
securityApi.reportThreat(ThreatEvent(type = type, timestamp = System.currentTimeMillis()))
}
}
}
Considerations
What this snippet does NOT cover:
- Bypass via framework patches (Xposed, LSPosed) — requires additional detection
- Advanced rooting with Magisk Delta (DenyList) — use the Play Integrity API
- Emulators — add an
isEmulator()check if needed
Recommendation: Combine this detector with the Play Integrity API (MEETS_DEVICE_INTEGRITY level or higher) for full coverage of compromised environments.