TL;DR
The plan: start from absolute zero on mobile security, end up with a 2,500-line unified Frida script that bypasses SSL pinning, root detection, and anti-instrumentation checks across 40+ libraries, all for a final degree project analyzing privacy in free Android apps. We’re learning together, mistakes and all.
Background
If you know exactly 0 about Frida, dynamic instrumentation, or why app developers make your life difficult with certificate pinning, congrats! This post is made for you. This was my first deep dive into mobile security research, so we’re basically learning together. By the end of this journey, you’ll understand why some of the “cleverness” in apps exists, and how to poke holes in it.
The context: my final degree project required analyzing privacy practices in free Android apps. But there was a problem. The apps wouldn’t let me see the traffic. They’d reject my proxy certificate, check if the device was rooted (which it needed to be), and hide if Frida was even present. So I built a solution that evolved from a scattered collection of snippets into something actually useful. The goal was a single script that works across many apps with zero or very few changes, making the approach scalable to a much larger set.
What We Need
- A rooted Android device (Xiaomi Redmi 6 and Samsung Galaxy A52 in my case).
- Frida installed on both the device and your computer.
- SSL pinning bypasses, because some apps don’t trust random CAs and in my case, Magisk module to install certs as system wasn’t working.
- Root detection evasion.
- A way to hide Frida itself.
- Patience. Lots of it.
Part 1: The Starting Point
The journey began where every researcher begins: with existing code. I grabbed scripts from Frida CodeShare:
@fdciabdul’s multi-bypass (root detection fundamentals)@akabe1’s unpinning (OkHttp and Conscrypt hooks)@pcipolloni’s universal SSL pinning bypass (generic TrustManager tricks)@TheDauntless’s Flutter TLS disabler (native BoringSSL patterns)
The problem? They were fragments. Each one handled a few libraries. Each one required manual selection depending on what the target app used. And none of them combined everything into one coherent solution.
The initial script was ~900 lines. It worked for some apps, but try it on a different one and you’d get different failures:
[!] Failed to hook TrustManager
[!] Connection rejected by host
[!] Device detected as rooted - app closing
[!] Frida detected - terminating
The realization: This wasn’t going to scale for analyzing 3+ apps. I needed to consolidate, extend, and actually understand what I was hooking.
Part 2: The First Major Expansion
This is where things got serious. I went from 900 lines to 2,652 lines. The commit added attribution to every technique, detailed implementation notes, and massive new coverage.
The structure became:
PHASE 1: Recognize which libraries are loaded
PHASE 2: Bypass SSL pinning for each one
PHASE 3: Evade root detection (both Java and native layers)
PHASE 4: Hide Frida itself
PHASE 5: Dynamic scanning for unknown implementations
Here’s what expanded:
SSL Pinning Bypasses: Java Layer
Started with basic SSLContext.init hooking. Then added:
- OkHttp3 variants: CertificatePinner.check, OkHostnameVerifier.verify, RealConnection.connectTls
- Conscrypt implementations: Added 3 package variants because Google, Samsung, and standalone Conscrypt all exist
- Third-party SDKs: TrustKit, Fabric, Appcelerator, PhoneGap/Cordova, IBM MobileFirst, Netty, Squareup
- Google Play Services: Hooks targeting obfuscated class names (because GMS obfuscates everything)
- Cronet: Google’s networking library used in play services
The pattern was: for each library, hook the verification methods and either return success or inject our proxy CA certificate. But here’s the thing, different libraries throw different exception types when verification fails. OkHttp throws SSLPeerUnverifiedException, Conscrypt throws CertificateException, transport layers throw SSLHandshakeException. Miss one and the app crashes.
SSL Pinning Bypasses: Native Layer
Then came the real complexity. TikTok, Instagram, Facebook, and Flutter apps all ship their own BoringSSL implementation inside native libraries:
- libliger.so (Instagram/Meta)
- libcoldstart.so (Facebook)
- libflutter.so (Flutter apps)
- libsscronet.so (Social Snap)
- libcronet.so (Google Cronet)
- libjavacrypto.so (APEX Conscrypt on Android 12+)
For these, Java hooks don’t work. You have to intercept the native functions directly:
SSL_CTX_set_custom_verify → nullify callback
SSL_CTX_set_verify → disable verification
X509_verify_cert → always return success
ssl_send_alert → suppress certificate errors
This required using Interceptor.attach() on these functions, reading the native calling conventions, and understanding BoringSSL’s error codes (42=bad_certificate, 48=certificate_unknown, etc.).
Root Detection: A Two-Layer Problem
Apps check for root in Java and at the native level. Java-side checks:
- File existence: /system/bin/su, /system/xbin/su, Magisk paths, etc.
- Package enumeration: checking if root management apps are installed
- Process inspection: running su and catching errors
- RootBeer library: checks build properties, system properties, file permissions
Native-side checks:
- stat/lstat on suspicious files
- open/openat attempting to read /system/build.prop
- access checking file permissions
- system() calling shell commands
The solution was a two-layer approach:
Capa nativa: Hook libc.so exports to hide root-related paths and files. When the app tries to stat /system/xbin/su, we return “doesn’t exist.”
Capa Java: Hook PackageManager.getPackageInfo() to hide root apps, hook Runtime.exec() to prevent shell commands, and critically, hook Process.killProcess() and System.exit() to prevent the app from closing itself when it detects root anyway.
The last part was essential. Many apps have a “if root detected, kill yourself” safety. We had to prevent that or the whole analysis falls apart.
Frida Detection
Apps check for Frida by:
- Reading /proc/self/maps and looking for frida strings
- Checking thread names for frida-agent
- Using dlopen to try loading frida libraries
- Hooking Frida’s public API methods
The fixes:
- Hook fgets() to sanitize “frida” strings from proc reads
- Hook strstr() similarly
- Hook pthread_create() to watch thread creation
- Hook /proc/self/maps reads entirely
This was surprisingly effective. No app in my research detected Frida after these hooks.
The Unified Class Scanner
By commit v2, I realized manual hook lists were incomplete. Different apps load different SDKs. Some have Firebase, some don’t. So I added dynamic enumeration:
Java.enumerateLoadedClasses({
onMatch: function(className) {
// Scan for patterns: anything with "Pinner", "Trust", "Certificate", "SSL"
// Check if it's a GMS class, apply signature matching
// Apply hooks automatically
}
})
This was a game-changer. It meant the script could adapt to unknown libraries by looking for suspicious method signatures rather than hardcoding class names.
The final version of v2 had 2,652 lines and detailed attribution comments (sometimes 3-4 lines explaining which researcher documented which technique).
Part 3: The Cleanup
After building the script, analyzing three apps, and understanding what actually worked, I revisited the code.
What v3 did: Removed bloat, cleaned documentation, and simplified without losing functionality.
- Removed verbose header comments (kept content, ditched attribution cruft)
- Deleted blank line spam
- Removed debug console.log statements like “Trying libliger”
- Simplified method comments from multi-line explanations to one-liners
- Removed redundant “NEW” tags used during development
- Consolidated the final summary message
Important: The script went from 2,652 → 2,571 lines (slight cleanup), but the core logic remained intact. You don’t optimize while building; you optimize when you know what works.
This commit represented maturity: knowing what was essential and cutting the rest. The script still covered 40+ libraries, 60+ hooks, and 4 major protection mechanisms, but it was cleaner.
Part 4: What Actually Worked (and What Didn’t)
Successes
The script successfully captured network traffic from:
- Jetpack Joyride (Halfbrick): 8 SDKs identified, including Facebook SDK, IronSource, Vungle, AppLovin, Mintegral
- Null’s Clash (unofficial Clash of Clans mod): Bypassed custom pinning
- Subway Surfers (SYBO): IronSource, Vungle, Unity Ads, and more
For Jetpack Joyride alone, we identified:
- Device fingerprinting by Mintegral
- Location data collection by Halfbrick’s own GeoIP service
- Battery/RAM telemetry by IronSource
- AAID and advertising ID collection by multiple SDKs
- Discrepancies between declared privacy policies and actual behavior
The script worked on both Android 9 (Xiaomi) and Android 14 (Samsung), handling different Conscrypt versions, GMS configurations, and native library layouts.
What Broke
Some protections remained unsolved:
- Obfuscated RASP solutions: Apps using commercial runtime app self-protection frameworks change class names on every build. Our dynamic scanner catches some, but not all.
- Certificate Transparency (CT): Android 14+ enforces CT verification at the platform level. We patched Conscrypt’s CT checker, but the platform-level checks still slip through in some cases.
- Generic hooks: Hooking widely-used methods like File.exists() or String.contains() caused crashes because too many legitimate code paths use them.
These weren’t failures, they were learning moments (xD). You can’t hook everything.
The Technical Journey: Six Phases of Development
If you look at my thesis, the actual development followed six phases on Jetpack Joyride before expanding to other apps:
- Phase 1: Basic SSL Pinning Bypass - Get any traffic at all
- Phase 2: OkHttp3 and Third-Party SDKs - Stop getting rejected by ad networks
- Phase 3: Root Detection Evasion - Let the app actually run
- Phase 4: Static Analysis + Specific Hooks - JADX + Frida cycle to find missing pieces
- Phase 5: Native SSL Bypasses - Handle BoringSSL in TikTok, Instagram, Flutter
- Phase 6: Auto-Patchers and Unified Scanner - Make it work for any app
Each phase took time. Phase 2 alone required understanding OkHttp’s architecture, learning how CertificatePinner works, and testing against real traffic. Phase 5 required reading BoringSSL documentation and understanding ARM calling conventions.
How to Actually Use This
The script works like this:
# Push Frida server to device
adb push frida-server /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/frida-server"
adb shell "su -c /data/local/tmp/frida-server &"
# Push our proxy CA certificate
adb push cacert.crt /data/local/tmp/
# Run the app with the script loaded
frida -U -f com.halfbrick.jetpackjoyride -l root-sslpining.js
Then configure Burp Suite as an HTTP proxy on the device, and all traffic flows through unencrypted. The script handles everything else: injecting certificates, hiding root, suppressing Frida, bypassing pinning.
The magic is in the parallelization. Instead of hooking one thing at a time and testing, the script hooks everything simultaneously. If 30 hooks are active and one fails, the app usually still works because another hook succeeded upstream.
Why This Matters
This wasn’t just “let’s see what apps do.” Privacy regulations like GDPR require transparency. Apps must declare what data they collect. Our analysis compared:
- Declared: “We collect analytics for performance”
- Actual: “We’re sending your AAID, device fingerprint, battery status, RAM usage, and your IMEI to 6 different ad networks”
The Frida script was the tool that revealed the discrepancy. Without it, we’d be guessing based on static analysis. With it, we had proof.
Challenges and Lessons
Lesson 1: Composition beats perfection. My script didn’t invent anything new, it integrated existing techniques. The value was in combining them coherently.
Lesson 2: Dynamic > Static. Enumerating classes at runtime found SDKs that static analysis missed because they’re loaded conditionally.
Lesson 3: Test against real apps. I wrote hooks in a vacuum, then tested on Jetpack Joyride. It failed. Then I jumped to Null’s Clash and Subway Surfers. Different apps, different libraries, different failures. That’s what forced the evolution.
Lesson 4: Understand the why. Why does Instagram ship BoringSSL? Because they control it, avoid OS bugs, and can ensure consistent behavior across versions. Why do apps check for root? Because rooted devices are easier to compromise. Understanding the “why” helps you find the “what to hook.”
Lesson 5: Document as you go. v2 had detailed comments. v3 cleaned them up. But without v2’s documentation, I’d have forgotten why each hook existed.
Wrap Up
We went from scattered script fragments to a unified, 2,500-line Frida instrumentation framework that:
- Bypasses 40+ libraries’ SSL pinning implementations
- Evades root detection at both Java and native layers
- Hides Frida’s presence
- Auto-discovers unknown implementations
- Works across Android 9 and 14 with different architectures
It’s not magic. It’s persistence. It’s understanding that app security is often a series of small checks, and each check can be bypassed. It’s knowing that perfect security doesn’t exist, only layers of friction.
The script is available in the TFG repository, and the full analysis (what we found in each app, privacy policy mismatches, GDPR compliance assessment) is in the thesis.
My next step? Building similar tools for iOS, which is significantly harder because iOS apps are often native-only and don’t have a unified instrumentation framework like Frida. But that’s another story.
Stay curious. And always, always test your security assumptions on real apps.