Skip to content

Instantly share code, notes, and snippets.

@neckothy
Last active January 15, 2024 09:20
Show Gist options
  • Save neckothy/56b46781bca2c2644956cc73d3fea894 to your computer and use it in GitHub Desktop.
Save neckothy/56b46781bca2c2644956cc73d3fea894 to your computer and use it in GitHub Desktop.
Temporarily inject your mitmproxy cert to the system certificate store on a rooted Android emulator / device. See info / warnings at top of script before use.
"""
Temporarily inject your mitmproxy cert to the system certificate store on a rooted Android emulator / device.
Based on (and heavily borrowed from) HTTP Toolkit's awesome adb interceptor:
https://github.com/httptoolkit/httptoolkit-server/blob/main/src/interceptors/android/android-adb-interceptor.ts
Requires:
adb_shell - https://github.com/JeffLIrion/adb_shell
openssl in path
Many of the features, checks, and failsafes have been excluded because I'm dumb & lazy.
This has only been tested on Android Studio emulators on Linux, but *should* work on other platforms.
I'd recommend using HTTP Toolkit if you're expecting safe/flawless behavior on a wide variety of setups.
Run as follows:
mitmproxy -s inject-system-ca.py (as mitmproxy addon)
OR
python inject-system-ca.py (standalone)
Attempt to inject cert automatically when mitmproxy is started
Attempt to inject cert manually later with :inject
Cert is automatically removed upon device restart (cold boot)
"""
import logging
import subprocess
from io import BytesIO
from mitmproxy import command
from pathlib import Path
from adb_shell.adb_device import AdbDeviceTcp
# a guess at default emulator connection info
# adb_shell doesn't offer a way to enumerate devices that I can see
# could iterate odd numbers 5555-5587
# realistically most users will be running a single emulator which should default to 5555
ADB_HOST = "127.0.0.1"
ADB_PORT = 5555
MITMPROXY_CERT_PATH = Path.home() / ".mitmproxy" / "mitmproxy-ca-cert.cer"
SYSTEM_CA_PATH = "/system/etc/security/cacerts"
ANDROID_TEMP_PATH = "/data/local/tmp"
INJECTION_SCRIPT_PATH = f"{ANDROID_TEMP_PATH}/mitmp-inject-system-cert.sh"
# really don't feel like adding more requirements or figuring out how to do this natively
# https://docs.mitmproxy.org/stable/howto-install-system-trusted-ca-android/#2-rename-certificate
# https://github.com/httptoolkit/httptoolkit-server/blob/main/src/certificates.ts#L13-L25
def get_cert_hash():
return (
subprocess.check_output(
f"openssl x509 -inform PEM -subject_hash_old -in {MITMPROXY_CERT_PATH}".split()
)
.decode()
.split()[0]
)
# https://github.com/httptoolkit/httptoolkit-server/blob/main/src/interceptors/android/adb-commands.ts#L269-L346
def generate_injection_script(cert_temp):
script_content = f"""
set -e # Fail on error
# Create a separate temp directory, to hold the current certificates
# Without this, when we add the mount we can't read the current certs anymore.
mkdir -p -m 700 /data/local/tmp/mitmp-ca-copy
# Copy out the existing certificates
if [ -d "/apex/com.android.conscrypt/cacerts" ]; then
cp /apex/com.android.conscrypt/cacerts/* /data/local/tmp/mitmp-ca-copy/
else
cp /system/etc/security/cacerts/* /data/local/tmp/mitmp-ca-copy/
fi
# Create the in-memory mount on top of the system certs folder
mount -t tmpfs tmpfs /system/etc/security/cacerts
# Copy the existing certs back into the tmpfs mount, so we keep trusting them
mv /data/local/tmp/mitmp-ca-copy/* /system/etc/security/cacerts/
# Copy our new cert in, so we trust that too
mv {cert_temp} /system/etc/security/cacerts/
# Update the perms & selinux context labels, so everything is as readable as before
chown root:root /system/etc/security/cacerts/*
chmod 644 /system/etc/security/cacerts/*
chcon u:object_r:system_file:s0 /system/etc/security/cacerts/*
echo 'System cacerts setup completed'
# Deal with the APEX overrides in Android 14+, which need injecting into each namespace:
if [ -d "/apex/com.android.conscrypt/cacerts" ]; then
echo 'Injecting certificates into APEX cacerts'
# When the APEX manages cacerts, we need to mount them at that path too. We can't do
# this globally as APEX mounts are namespaced per process, so we need to inject a
# bind mount for this directory into every mount namespace.
# First we get the Zygote process(es), which launch each app
ZYGOTE_PID=$(pidof zygote || true)
ZYGOTE64_PID=$(pidof zygote64 || true)
Z_PIDS="$ZYGOTE_PID $ZYGOTE64_PID"
# N.b. some devices appear to have both, some have >1 of each (!)
# Apps inherit the Zygote's mounts at startup, so we inject here to ensure all newly
# started apps will see these certs straight away:
for Z_PID in $Z_PIDS; do
if [ -n "$Z_PID" ]; then
nsenter --mount=/proc/$Z_PID/ns/mnt -- \
/bin/mount --bind /system/etc/security/cacerts /apex/com.android.conscrypt/cacerts
fi
done
echo 'Zygote APEX certificates remounted'
# Then we inject the mount into all already running apps, so they see these certs immediately.
# Get the PID of every process whose parent is one of the Zygotes:
APP_PIDS=$(
echo $Z_PIDS | \
xargs -n1 ps -o 'PID' -P | \
grep -v PID
)
# Inject into the mount namespace of each of those apps:
for PID in $APP_PIDS; do
nsenter --mount=/proc/$PID/ns/mnt -- \
/bin/mount --bind /system/etc/security/cacerts /apex/com.android.conscrypt/cacerts &
done
wait # Launched in parallel - wait for completion here
echo "APEX certificates remounted for $(echo $APP_PIDS | wc -w) apps"
fi
# Delete the temp cert directory & this script itself
rm -r /data/local/tmp/mitmp-ca-copy
rm {INJECTION_SCRIPT_PATH}
echo "System cert successfully injected"
"""
return BytesIO(script_content.encode("utf-8"))
def try_inject():
device = AdbDeviceTcp(ADB_HOST, ADB_PORT)
if device.connect():
cert_temp = f"{ANDROID_TEMP_PATH}/{get_cert_hash()}.0"
device.push(MITMPROXY_CERT_PATH, cert_temp)
injection_script = generate_injection_script(cert_temp)
device.push(injection_script, INJECTION_SCRIPT_PATH)
injection_output = device.shell(
f"su -c sh {INJECTION_SCRIPT_PATH}", decode=True
)
if "System cert successfully injected" not in injection_output:
logging.info("failed to inject system CA")
logging.info(injection_output)
else:
logging.info("System cert successfully injected")
else:
logging.info("No ADB devices found")
return
def running():
try_inject()
@command.command("inject")
def inject() -> None:
try_inject()
if __name__ == "__main__":
try_inject()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment