Certificate Injection — Android 14+ (APEX)

Part 4 of the Android Pentest series. For Android 13 and below the simpler /system remount 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

ToolCheckNotes
adbadb versionPlatform-Tools 34+ recommended for Android 14
opensslopenssl versionFor certificate conversion
Rooted deviceadb shell whoamirootMagisk 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 root fails 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 lack mount or nsenter, 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

  1. Copies all existing system certs to a temp directory
  2. Creates a tmpfs overlay on /system/etc/security/cacerts/ and moves the originals + your new cert into it
  3. Fixes ownership, permissions, and SELinux labels
  4. If APEX is detected (/apex/com.android.conscrypt/cacerts exists):
    • 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 edit CERTIFICATE_PATH before 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.apk plus split_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 to frida_ssl.js runtime patching.

Troubleshooting

SymptomLikely causeFix
adb root failsProduction Android buildUse Magisk or another root method
Cert not trusted after scriptNamespace injection missed processesRe-run; confirm Zygote PIDs were found
install_cert.sh fails on nsenternsenter missing on deviceDifferent root method, or push a static nsenter
Cert gone after rebootExpected — tmpfs is volatileRe-push and re-run the script
ReFlutter APK crashesFlutter engine version mismatchUse frida_ssl.js instead

See also

References