
BLOG
BLOG
In the first blog of the KMM series, we introduced Kotlin Multiplatform Mobile (KMM) and its cross-platform advantages.
In this part, we go deeper into mobile security in KMM apps, focusing on:
But, before that, let’s quickly recap what KMM is.
Kotlin Multiplatform Mobile (KMM) enables developers to write shared code for both Android and iOS, while still maintaining platform-specific implementations where necessary.
For the sake of simplicity, we have divided this blog into two sections:
🔑 Key takeaway: Security is not a one-time implementation. It’s an ongoing cycle of hardening → monitoring → defending against new bypass techniques.
Security is a critical concern in KMM applications, especially for protecting sensitive data and preventing unauthorised access.
While the two essential security mechanisms used in mobile applications, i.e., Root/Jailbreak detection and SSL pinning, help improve security, attackers often attempt to bypass them using various methods.
Risk |
Impact if exploited |
Example |
Root/Jailbreak |
Attackers gain elevated privileges |
Extract sensitive data, alter API calls |
Weak SSL pinning |
Man-in-the-middle (MITM) attacks |
User credentials intercepted |
Missing security layers |
Non-compliance with security standards |
Banking apps failing PCI DSS audits |
Section 1: Implementation
Root detection helps prevent attackers from running the application on a compromised device. A rooted or jailbroken device provides elevated privileges that allow attackers to modify app behaviour, extract sensitive information, or manipulate API requests.
Since KMM supports both Android and iOS, root detection requires platform-specific implementations.
Root detection in Android can be implemented using various checks:
The presence of su (Superuser) binary indicates that the device is rooted.
Apps like Magisk, SuperSU, and KingoRoot are often installed on rooted devices.
If an application can write to system directories, the device is likely rooted.
Modifications in system properties can indicate root access.
override fun isDeviceRooted(): Boolean {
return checkForSuBinary() || checkForDangerousProps() || checkForRWPaths() || checkForRootPackages()
}
private fun checkForSuBinary(): Boolean {
val paths = arrayOf(
"/system/app/Superuser.apk",
"/sbin/su",
"/system/bin/su",
"/system/xbin/su",
"/data/local/xbin/su",
"/data/local/bin/su",
"/system/sd/xbin/su",
"/system/bin/failsafe/su",
"/data/local/su",
"/su/bin"
)
private fun checkForRootPackages(): Boolean {
val packages = arrayOf(
"com.noshufou.android.su",
"com.thirdparty.superuser",
"eu.chainfire.supersu",
"com.koushikdutta.superuser",
"com.zachspong.temprootremovejb",
"com.ramdroid.appquarantine"
)
return packages.any { packageName ->
try {
val pm = context.packageManager
pm.getPackageInfo(packageName, 0)
true
} catch (e: Exception) {
false
}
}
}
}
iOS jailbreak detection can be done using:
override val jailbreakDetectionResult: JailbreakDetectionResult
get() = checkJailbreak()
private fun checkJailbreak(): JailbreakDetectionResult {
val jailbreakPaths = mapOf(
"/Applications/Cydia.app" to "Cydia app detected",
"/Library/MobileSubstrate/MobileSubstrate.dylib" to "MobileSubstrate detected",
"/bin/bash" to "Bash shell detected",
"/usr/sbin/sshd" to "SSH daemon detected",
"/etc/apt" to "APT package manager detected"
)
}
SSL pinning prevents man-in-the-middle (MITM) attacks by ensuring that only trusted certificates are accepted, even if an attacker has installed a custom certificate authority (CA) on the device.
Android uses OkHttpClient with a custom certificate pinning configuration.
val certificatePinner = CertificatePinner.Builder()
.add(hostname, "sha256/47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=") // Original pin
.add(hostname, "sha256/Cl7dc6nofBuxRWuGgnZc9Fi/VYDPg608JSN91g/wQXA=")
// Your pin
.add(hostname, "sha256/wMSCtagZr+ada2dTKz2S7x2fU6YgXGn/a9pBqyEHu7U=") // Burp Pin
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
val request = Request.Builder()
.url("https://$hostname/users")
.build()
val response = client.newCall(request).execute()
OkHttpClient: Configured with the certificate pinning logic.
Request: A simple HTTP GET request to fetch user data.
Response: Captures the response body as a string.
In iOS, SSL pinning is typically implemented using a custom URLSessionDelegate that intercepts and validates server trust during network requests.
Below is a real-world implementation that combines SSL pinning with proxy detection. This ensures the app not only trusts specific certificates but also terminates execution if a network proxy is detected (a common method used by attackers to intercept traffic).
static let shared = BlockProxyAndSSLPinning()
private let pinnedKey = "sha256/47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=" // jsonplaceholder.typicode.com
static func setup() {
// 1. Check for proxy immediately
if hasProxy() {
fatalError("Proxy detected! Disable proxy to use the app.")
}
// 2. Configure SSL pinning for all requests
URLProtocol.registerClass(BlockAllRequestsProtocol.self)
}
static func hasProxy() -> Bool {
guard let settings = CFNetworkCopySystemProxySettings()?.takeRetainedValue() as? [String: Any] else {
return false
}
return !(settings["HTTPProxy"] as? String ?? "").isEmpty
}
// SSL Pinning Validation
func validate(challenge: URLAuthenticationChallenge) -> Bool {
guard let trust = challenge.protectionSpace.serverTrust else { return false }
// Verify certificate
var error: CFError?
guard SecTrustEvaluateWithError(trust, &error) else { return false }
// Get server's public key
guard let serverKey = SecTrustCopyKey(trust),
let keyData = SecKeyCopyExternalRepresentation(serverKey, nil) else {
return false
}
// Calculate SHA256
let data = keyData as Data
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes { _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash) }
let serverKeyHash = "sha256/" + Data(hash).base64EncodedString()
return serverKeyHash == pinnedKey
}
}
Uses SecTrustEvaluateWithError() to verify the server’s certificate.
Extracts the public key from the certificate and hashes it using SHA256.
Compares the resulting hash with a predefined pinned value (pinnedKey).
SSL pinning ensures your app only trusts specific certificates, reducing MITM risk.
val certificatePinner = CertificatePinner.Builder()
.add(hostname, "sha256/47DEQpj8HBSa+/TIm...")
.build()
func validate(challenge: URLAuthenticationChallenge) -> Bool { // verify SSL certs & compare SHA256 hash }
Section 2: Bypass techniques
Frida is a powerful dynamic instrumentation tool that allows attackers to hook into running applications and modify their behaviour at runtime.
adb push frida-server /data/local/tmp/
adb shell chmod 755 /data/local/tmp/frida-server
adb shell ./data/local/tmp/frida-server &
frida -U -n com.example.kmm_test -e "console.log('Frida attached');"
Java.perform(function() {
var rootCheck = Java.use("com.example.kmm_test.RootDetector");
rootCheck.isDeviceRooted.implementation = function() {
console.log("Bypassing root detection");
return false;
};
});
Objection is a runtime mobile exploitation framework built on Frida, allowing security researchers to bypass security mechanisms easily.
objection -g com.example.kmm_test explore
android root disable
Successfully bypassed ✅
ios jailbreak disable
Successfully bypassed ✅
Tool |
Platform |
Method |
Outcome |
Frida |
Android/iOS |
Hook root/jailbreak checks |
Rooted device appears “safe” |
Objection |
Both |
Disable detection at runtime |
Bypasses checks instantly |
SSL pinning ensures that only specific certificates are trusted by the application, preventing Man-in-the-Middle (MITM) attacks. Attackers often try to bypass SSL pinning to intercept and modify API requests.
When you try the objection tool to bypass SSL pinning, you will get error messages and will not be able to bypass.
Not able to bypass using Objection ❌
Now we use a Frida script to bypass SSL pinning and target a particular OkHttp3 function and change the return value.
adb shell ./data/local/tmp/frida-server &
frida -U -f org.example.project -e "
Java.perform(function () {
try {
var CertificatePinner = Java.use('okhttp3.CertificatePinner');
// Hook the overloaded method: check$okhttp(String, Function0)
CertificatePinner.check$okhttp.overload('java.lang.String', 'kotlin.jvm.functions.Function0').implementation = function (host, certChainCleaner) {
console.log('[+] Bypassing OkHTTPv3 CertificatePinner check$okhttp');
console.log(' Host: ' + host);
// You can return without throwing any exceptions to bypass the check
return;
};
console.log('[*] OkHTTPv3 pinning bypass hooked successfully');
} catch (err) {
console.log('[!] Exception: ' + err.message);
}
}); "
This Frida script is designed to bypass SSL pinning in Android applications that use OkHttp3. Specifically, it targets the CertificatePinner class and hooks the check$okhttp method, which is responsible for validating the server’s SSL certificate against a predefined pin.
By overriding this method’s implementation, the script ensures that no exceptions are thrown during certificate validation, effectively disabling the pinning mechanism. This allows traffic interception tools like Burp Suite to inspect HTTPS traffic without being blocked by certificate mismatches. The use of Frida enables this modification dynamically at runtime without the need to recompile the APK.
Successfully bypassed SSL pinning ✅
objection -g com.example.kmm_test explore
ios sslpinning disable
Successfully bypassed iOS SSL pinning ✅
Tool |
Platform |
Method |
Outcome |
Frida |
Android |
Hook CertificatePinner |
Disables SSL checks |
Objection |
iOS |
Command: ios sslpinning disable |
SSL pinning bypassed |
Mechanism |
Implementation method |
Common bypass tools |
Mitigation |
Root/Jailbreak detection |
System checks (binaries, directories, packages) |
Frida, Objection |
Obfuscation, RASP |
SSL pinning |
Certificate hash verification |
Frida, Objection |
Dynamic pinning, Cert transparency logs |
✅ Combine root/jailbreak detection + SSL pinning for layered defense
✅ Regularly update detection logic against new bypass scripts
✅ Use runtime application self-protection (RASP) for stronger enforcement
✅ Always conduct penetration testing before app releases
KMM streamlines cross-platform mobile development, but robust security depends on platform-specific implementations. Root detection and SSL pinning are vital tools in your defence arsenal, but they must be implemented carefully and continuously updated.
While no system is foolproof, layering security features and understanding how attackers bypass them puts you a step ahead.
🚧 “Defence isn’t just building walls, it’s knowing where attackers climb them.”
Stay secure and always test your apps like an attacker would!
💡Final tip: Always conduct penetration testing to identify vulnerabilities before attackers do!
Frequently asked questions (FAQs)
No. Attackers can still bypass with Frida or Objection. That’s why layered security and continuous testing are essential.
Not all apps need jailbreak/root detection, but apps handling sensitive data (banking, fintech, healthcare) should implement it.
Use tools like Frida and Objection yourself during penetration testing to validate defenses and test your implementation.