Last active
January 15, 2024 09:20
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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