Certificate Injection — Android 14+ (APEX)
Part 4 of the Android Pentest series. For Android 13 and below the simpler
/systemremount still works — see Certificate Injection — Android < 14.
The big change at Android 14 is APEX-managed certificate namespaces. The
system CA store moved to /apex/com.android.conscrypt/cacerts, and each
process gets its own mount namespace forked from Zygote. Copying a cert file
into one place no longer works — it has to be injected into every running
namespace.
Why this is harder now
Android ≤ 13 Android ≥ 14 (APEX)
┌──────────────────────┐ ┌────────────────────────────┐
│ /system/etc/security │ │ /apex/com.android.conscrypt │
│ /cacerts/ (one dir) │ │ /cacerts/ (per-namespace) │
└──────────┬───────────┘ └──────────────┬─────────────┘
│ │
write the file Zygote namespace
once │
│ ┌─────────────┼─────────────┐
done ▼ ▼ ▼
app proc 1 app proc 2 new apps
(bind-mount (bind-mount (inherit from
via nsenter) via nsenter) Zygote)
The install_cert.sh script automates all of
this.
Prerequisites
| Tool | Check | Notes |
|---|---|---|
adb | adb version | Platform-Tools 34+ recommended for Android 14 |
openssl | openssl version | For certificate conversion |
| Rooted device | adb shell whoami → root | Magisk or adb root on eng/userdebug |
Get root access
On eng or userdebug builds, root is one command:
adb root
adb shell whoami # expected: root
⚠️ Production builds.
adb rootfails with “adbd cannot run as root in production builds.” You need Magisk or another root solution — and it must grant full shell access. Some partial root setups lackmountornsenter, both of which cert injection requires.
Step 1 — Prepare the certificate
openssl x509 -inform der -in burp.der -out burp.pem
HASH=$(openssl x509 -inform PEM -subject_hash_old -in burp.pem | head -1)
mv burp.pem "$HASH.0"
echo "Certificate filename: $HASH.0"
Step 2 — Push and execute
adb push "$HASH.0" /data/local/tmp/"$HASH.0"
adb push script/install_cert.sh /data/local/tmp/install_cert.sh
adb shell chmod +x /data/local/tmp/install_cert.sh
adb shell /data/local/tmp/install_cert.sh "$HASH.0"
What the script does
- Copies all existing system certs to a temp directory
- Creates a
tmpfsoverlay on/system/etc/security/cacerts/and moves the originals + your new cert into it - Fixes ownership, permissions, and SELinux labels
- If APEX is detected (
/apex/com.android.conscrypt/cacertsexists):- Bind-mounts the updated cert dir into the APEX path for the current shell
- Injects the same bind-mount into every Zygote process via
nsenter - Injects into all currently running app processes (children of Zygote)
⚠️ Important caveats
- The injection uses
tmpfs— it does not survive a reboot. Re-run after every restart.- The script self-deletes after running. Re-push it to run again.
- If the hardcoded cert filename inside the script (
9a5ba575.0) doesn’t match your hash, pass yours as an argument (as shown) or editCERTIFICATE_PATHbefore pushing.- Apps launched after injection inherit the cert from Zygote automatically. Only restart apps that were already running.
Flutter apps — ReFlutter alternative
If the target is a Flutter app and the runtime Frida bypass is fragile against the engine version, ReFlutter re-packages the app with a modified engine instead.
# Identify the package and pull the APK
adb shell pm list packages | grep <app_name>
adb shell pm path <package.name>
adb pull <apk_path> target.apk
# Patch and reinstall
reflutter target.apk
adb uninstall <package.name>
adb install patched_target.apk
📌 Split APKs. Modern apps often ship a
base.apkplussplit_config.*APKs (density, ABI, language). Pull all of them; ReFlutter needs the base, the config splits install alongside. If a patched APK crashes due to engine incompatibility, fall back tofrida_ssl.jsruntime patching.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
adb root fails | Production Android build | Use Magisk or another root method |
| Cert not trusted after script | Namespace injection missed processes | Re-run; confirm Zygote PIDs were found |
install_cert.sh fails on nsenter | nsenter missing on device | Different root method, or push a static nsenter |
| Cert gone after reboot | Expected — tmpfs is volatile | Re-push and re-run the script |
| ReFlutter APK crashes | Flutter engine version mismatch | Use frida_ssl.js instead |
See also
- Previous: Certificate Injection — Android < 14
- Next: Root Detection Bypass
- Tools & Scripts — the
install_cert.shinternals