Tools & Scripts
Part 8 of the Android Pentest series. These three scripts are the reusable engine the rest of the series drives.
| Script | Language | Purpose |
|---|---|---|
install_cert.sh | Shell | CA cert injection (tmpfs overlay + APEX namespace) |
frida_ssl.js | JavaScript (Frida) | Root detection bypass + Flutter TLS bypass |
LoggingHTTPServer.py | Python 3 | Lightweight HTTP request logger |
⚠️ Authorization & sensitivity. Use only on devices you own or are authorized to test.
LoggingHTTPServer.pywrites captured traffic tolog.txt— treat that file as sensitive and never commit it.
install_cert.sh — Certificate Injection
Injects a CA certificate (e.g. BurpSuite) into the Android system trust
store. It works on both pre-14 (simple tmpfs overlay) and 14+ (APEX
namespace injection via nsenter). See
Certificate Injection — Android 14+
for the end-to-end usage.
What it does
- Pre-flight checks — must be root, cert file must exist, refuses to double-run (detects an existing tmpfs mount).
- Copies existing system certs (handles both
/system/...and the/apex/com.android.conscrypt/cacertspaths) to a temp dir. - Mounts a
tmpfsoverlay on the cacerts dir, moves originals + the new cert in, fixes ownership/permissions/SELinux labels. - On Android 14+: bind-mounts the updated dir into every Zygote namespace
and every running app process via
nsenter(parallelised withwait). - Cleanup trap removes the temp dir on error; on success the script self-deletes.
📌 Not idempotent. It self-deletes and refuses to re-run over an existing mount. Re-push from your host to run again. The default cert filename is
9a5ba575.0(standard BurpSuite hash) — pass yours as$1or editCERTIFICATE_PATH.
#!/system/bin/sh
# Certificate injection script for Android (supports both pre-14 and 14+ with APEX)
# Usage: Push cert to /data/local/tmp/<hash>.0, then run this script.
# Optionally pass the cert filename as $1: ./install_cert.sh 9a5ba575.0
SYSTEM_CA_PATH='/system/etc/security/cacerts'
ANDROID_TEMP='/data/local/tmp'
CERT_FILENAME="${1:-9a5ba575.0}"
CERTIFICATE_PATH="${ANDROID_TEMP}/${CERT_FILENAME}"
INJECTION_SCRIPT_PATH="${ANDROID_TEMP}/install_cert.sh"
HTK_CA_COPY="${ANDROID_TEMP}/htk-ca-copy"
set -e # Fail on error
# Cleanup handler — remove temp files on failure so we don't leave junk behind
cleanup() {
echo "Cleaning up temporary files..."
rm -rf "$HTK_CA_COPY" 2>/dev/null || true
}
trap cleanup EXIT
# ── Pre-flight checks ──────────────────────────────────────────────────────
if [ "$(id -u)" -ne 0 ]; then
echo "ERROR: This script must run as root. Got UID=$(id -u)"
exit 1
fi
if [ ! -f "$CERTIFICATE_PATH" ]; then
echo "ERROR: Certificate not found at $CERTIFICATE_PATH"
echo "Push your cert first: adb push <hash>.0 ${ANDROID_TEMP}/<hash>.0"
exit 1
fi
# Check if we already have a tmpfs mount on the cacerts path (idempotency guard)
if mount | grep -q "tmpfs on ${SYSTEM_CA_PATH}"; then
echo "WARNING: tmpfs is already mounted on ${SYSTEM_CA_PATH}"
echo "If you need to re-inject, reboot first, then re-run this script."
exit 1
fi
printf "\n---\nInjecting certificate: %s\n" "$CERT_FILENAME"
# ── Copy existing certs to temp ────────────────────────────────────────────
mkdir -p "$HTK_CA_COPY"
chmod 700 "$HTK_CA_COPY"
rm -rf "${HTK_CA_COPY:?}/"*
if [ -d "/apex/com.android.conscrypt/cacerts" ]; then
cp /apex/com.android.conscrypt/cacerts/* "$HTK_CA_COPY/"
else
cp "${SYSTEM_CA_PATH}/"* "$HTK_CA_COPY/"
fi
# ── Mount tmpfs and populate ───────────────────────────────────────────────
mount -t tmpfs tmpfs "$SYSTEM_CA_PATH"
mv "$HTK_CA_COPY/"* "$SYSTEM_CA_PATH/"
# Copy our new cert in, so we trust that too
mv "$CERTIFICATE_PATH" "$SYSTEM_CA_PATH/"
# Update the perms & selinux context labels, so everything is as readable as before
chown root:root "$SYSTEM_CA_PATH/"*
chmod 644 "$SYSTEM_CA_PATH/"*
chcon u:object_r:system_file:s0 "$SYSTEM_CA_PATH/"
chcon u:object_r:system_file:s0 "$SYSTEM_CA_PATH/"*
echo 'System cacerts setup completed'
# ── APEX namespace injection (Android 14+) ─────────────────────────────────
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 mount for the shell itself, for completeness and so we can see this
# when we check for correct installation on later runs
mount --bind "$SYSTEM_CA_PATH" /apex/com.android.conscrypt/cacerts
# 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 (!)
if [ -z "$(echo $Z_PIDS | tr -d ' ')" ]; then
echo "WARNING: No Zygote PIDs found — newly launched apps won't inherit the cert"
fi
# 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_CA_PATH" /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_CA_PATH" /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
# ── Cleanup ────────────────────────────────────────────────────────────────
# Remove the temp cert copy directory & this script itself
rm -rf "$HTK_CA_COPY"
trap - EXIT # Disarm the cleanup trap since we cleaned up successfully
rm "$INJECTION_SCRIPT_PATH"
printf "System cert successfully injected\n---\n"
frida_ssl.js — Root + Flutter TLS Bypass
A two-in-one Frida script. Used throughout Root Detection Bypass and SSL Pinning Bypass.
Part 1 — Root detection bypass. Hooks the Java layer
(PackageManager.getPackageInfo, File.exists, Runtime.exec,
SystemProperties.get, BufferedReader.readLine, ProcessBuilder.start)
and the native layer (libc fopen / system), covering 29 known root
packages and the binary/property/command detection families.
Part 2 — Flutter TLS bypass. Patches BoringSSL’s ssl_verify_peer_cert
using architecture-specific byte patterns (arm64 / arm / x64, plus an iOS
arm64 config), so it works against stripped/custom libflutter.so. It
hooks System.loadLibrary, scans memory once the library loads, replaces
the verify function with a NativeCallback returning 0, and retries
after ~20s if the first scan misses.
📌 Completing the exec-family hooks. The original script left the
exec-family (execl/execv/execve/…) as a TODO — apps can shell out tosuthrough these and slip past thesystem()-only native hook. This version adds a sharedhookExecVariant()over all eight bionic front-ends: it reads the path atargs[0], and if the basename is a root-probe binary (su,busybox,magisk,which, …) it rewrites the path to a non-existent one so the call fails withENOENT— exactly how a clean device behaves. Unreadable pointers are left untouched. Reviewed for behaviour; test against your target before relying on it.
⚠️ Pattern fragility. BoringSSL byte patterns change between Flutter engine versions. If the TLS bypass stops matching, update the patterns per NVISO’s guide.
Java.perform(function() {
// Helper: check if a command string matches known root-detection commands
function isRootProbeCmd(cmd) {
return cmd.indexOf("getprop") != -1 ||
cmd == "mount" ||
cmd.indexOf("build.prop") != -1 ||
cmd == "id" ||
cmd == "sh";
}
function isRootProbeInArray(cmdarr) {
for (var i = 0; i < cmdarr.length; i++) {
if (isRootProbeCmd(cmdarr[i])) return true;
if (cmdarr[i] == "su") return true;
}
return false;
}
function isSuCmd(cmd) {
return cmd == "su";
}
var RootPackages = ["com.noshufou.android.su", "com.noshufou.android.su.elite", "eu.chainfire.supersu",
"com.koushikdutta.superuser", "com.thirdparty.superuser", "com.yellowes.su", "com.koushikdutta.rommanager",
"com.koushikdutta.rommanager.license", "com.dimonvideo.luckypatcher", "com.chelpus.lackypatch",
"com.ramdroid.appquarantine", "com.ramdroid.appquarantinepro", "com.devadvance.rootcloak", "com.devadvance.rootcloakplus",
"de.robv.android.xposed.installer", "com.saurik.substrate", "com.zachspong.temprootremovejb", "com.amphoras.hidemyroot",
"com.amphoras.hidemyrootadfree", "com.formyhm.hiderootPremium", "com.formyhm.hideroot", "me.phh.superuser",
"eu.chainfire.supersu.pro", "com.kingouser.com", "com.topjohnwu.magisk"
];
var RootBinaries = ["su", "busybox", "supersu", "Superuser.apk", "KingoUser.apk", "SuperSu.apk", "magisk"];
var RootProperties = {
"ro.build.selinux": "1",
"ro.debuggable": "0",
"service.adb.root": "0",
"ro.secure": "1"
};
var RootPropertiesKeys = [];
for (var k in RootProperties) RootPropertiesKeys.push(k);
var PackageManager = Java.use("android.app.ApplicationPackageManager");
var Runtime = Java.use('java.lang.Runtime');
var NativeFile = Java.use('java.io.File');
var String = Java.use('java.lang.String');
var SystemProperties = Java.use('android.os.SystemProperties');
var BufferedReader = Java.use('java.io.BufferedReader');
var ProcessBuilder = Java.use('java.lang.ProcessBuilder');
var StringBuffer = Java.use('java.lang.StringBuffer');
var loaded_classes = Java.enumerateLoadedClassesSync();
send("Loaded " + loaded_classes.length + " classes!");
var useKeyInfo = false;
var useProcessManager = false;
send("loaded: " + loaded_classes.indexOf('java.lang.ProcessManager'));
if (loaded_classes.indexOf('java.lang.ProcessManager') != -1) {
try {
//useProcessManager = true;
//var ProcessManager = Java.use('java.lang.ProcessManager');
} catch (err) {
send("ProcessManager Hook failed: " + err);
}
} else {
send("ProcessManager hook not loaded");
}
var KeyInfo = null;
if (loaded_classes.indexOf('android.security.keystore.KeyInfo') != -1) {
try {
//useKeyInfo = true;
//var KeyInfo = Java.use('android.security.keystore.KeyInfo');
} catch (err) {
send("KeyInfo Hook failed: " + err);
}
} else {
send("KeyInfo hook not loaded");
}
PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(pname, flags) {
var shouldFakePackage = (RootPackages.indexOf(pname) > -1);
if (shouldFakePackage) {
send("Bypass root check for package: " + pname);
pname = "set.package.name.to.a.fake.one.so.we.can.bypass.it";
}
return this.getPackageInfo.overload('java.lang.String', 'int').call(this, pname, flags);
};
NativeFile.exists.implementation = function() {
var name = NativeFile.getName.call(this);
var shouldFakeReturn = (RootBinaries.indexOf(name) > -1);
if (shouldFakeReturn) {
send("Bypass return value for binary: " + name);
return false;
} else {
return this.exists.call(this);
}
};
var exec = Runtime.exec.overload('[Ljava.lang.String;');
var exec1 = Runtime.exec.overload('java.lang.String');
var exec2 = Runtime.exec.overload('java.lang.String', '[Ljava.lang.String;');
var exec3 = Runtime.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;');
var exec4 = Runtime.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;', 'java.io.File');
var exec5 = Runtime.exec.overload('java.lang.String', '[Ljava.lang.String;', 'java.io.File');
exec5.implementation = function(cmd, env, dir) {
if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id" || cmd == "sh") {
var fakeCmd = "grep";
send("Bypass " + cmd + " command");
return exec1.call(this, fakeCmd);
}
if (cmd == "su") {
var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled";
send("Bypass " + cmd + " command");
return exec1.call(this, fakeCmd);
}
return exec5.call(this, cmd, env, dir);
};
exec4.implementation = function(cmdarr, env, file) {
for (var i = 0; i < cmdarr.length; i = i + 1) {
var tmp_cmd = cmdarr[i];
if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id" || tmp_cmd == "sh") {
var fakeCmd = "grep";
send("Bypass " + cmdarr + " command");
return exec1.call(this, fakeCmd);
}
if (tmp_cmd == "su") {
var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled";
send("Bypass " + cmdarr + " command");
return exec1.call(this, fakeCmd);
}
}
return exec4.call(this, cmdarr, env, file);
};
exec3.implementation = function(cmdarr, envp) {
for (var i = 0; i < cmdarr.length; i = i + 1) {
var tmp_cmd = cmdarr[i];
if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id" || tmp_cmd == "sh") {
var fakeCmd = "grep";
send("Bypass " + cmdarr + " command");
return exec1.call(this, fakeCmd);
}
if (tmp_cmd == "su") {
var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled";
send("Bypass " + cmdarr + " command");
return exec1.call(this, fakeCmd);
}
}
return exec3.call(this, cmdarr, envp);
};
exec2.implementation = function(cmd, env) {
if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id" || cmd == "sh") {
var fakeCmd = "grep";
send("Bypass " + cmd + " command");
return exec1.call(this, fakeCmd);
}
if (cmd == "su") {
var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled";
send("Bypass " + cmd + " command");
return exec1.call(this, fakeCmd);
}
return exec2.call(this, cmd, env);
};
exec.implementation = function(cmd) {
for (var i = 0; i < cmd.length; i = i + 1) {
var tmp_cmd = cmd[i];
if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id" || tmp_cmd == "sh") {
var fakeCmd = "grep";
send("Bypass " + cmd + " command");
return exec1.call(this, fakeCmd);
}
if (tmp_cmd == "su") {
var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled";
send("Bypass " + cmd + " command");
return exec1.call(this, fakeCmd);
}
}
return exec.call(this, cmd);
};
exec1.implementation = function(cmd) {
if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id" || cmd == "sh") {
var fakeCmd = "grep";
send("Bypass " + cmd + " command");
return exec1.call(this, fakeCmd);
}
if (cmd == "su") {
var fakeCmd = "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled";
send("Bypass " + cmd + " command");
return exec1.call(this, fakeCmd);
}
return exec1.call(this, cmd);
};
String.contains.implementation = function(name) {
if (name == "test-keys") {
send("Bypass test-keys check");
return false;
}
return this.contains.call(this, name);
};
var get = SystemProperties.get.overload('java.lang.String');
get.implementation = function(name) {
if (RootPropertiesKeys.indexOf(name) != -1) {
send("Bypass " + name);
return RootProperties[name];
}
return this.get.call(this, name);
};
// NOTE: The native hooks below use Memory.writeUtf8String(args[0], ...) to overwrite
// the original path/command buffer in-place. This is safe ONLY when the replacement
// string is shorter than or equal to the original. Overwriting with a longer string
// risks a heap buffer overflow. The replacements below are chosen to be safe for the
// expected inputs (e.g., "/notexists" is shorter than most absolute paths).
Interceptor.attach(Module.findExportByName("libc.so", "fopen"), {
onEnter: function(args) {
var path = Memory.readCString(args[0]);
path = path.split("/");
var executable = path[path.length - 1];
var shouldFakeReturn = (RootBinaries.indexOf(executable) > -1)
if (shouldFakeReturn) {
Memory.writeUtf8String(args[0], "/notexists");
send("Bypass native fopen");
}
},
onLeave: function(retval) {
}
});
Interceptor.attach(Module.findExportByName("libc.so", "system"), {
onEnter: function(args) {
var cmd = Memory.readCString(args[0]);
send("SYSTEM CMD: " + cmd);
if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id") {
send("Bypass native system: " + cmd);
Memory.writeUtf8String(args[0], "grep");
}
if (cmd == "su") {
send("Bypass native system: " + cmd);
Memory.writeUtf8String(args[0], "justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled");
}
},
onLeave: function(retval) {
}
});
/*
Exec Family
int execl(const char *path, const char *arg0, ..., const char *argn, (char *)0);
int execle(const char *path, const char *arg0, ..., const char *argn, (char *)0, char *const envp[]);
int execlp(const char *file, const char *arg0, ..., const char *argn, (char *)0);
int execlpe(const char *file, const char *arg0, ..., const char *argn, (char *)0, char *const envp[]);
int execv(const char *path, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
All eight variants are glibc/bionic front-ends that ultimately reach
execve(2). The execl* forms take the path as arg0 and a NULL-terminated
varargs list; the execv* forms take the path as args[0] and an argv
array as args[1]. We only need to neutralise the path (args[0]) when it
points at a root-probing binary: redirecting it to a non-existent path
makes the exec fail with ENOENT, which is exactly how a non-rooted
device would behave.
*/
// Path basenames that indicate a root probe (su, busybox, magisk, ...).
function isRootProbePath(p) {
if (!p) return false;
var needles = ["su", "busybox", "magisk", "supersu", "daemonsu",
"ksud", "which", "id", "mount", "getprop"];
for (var i = 0; i < needles.length; i++) {
// match the bare command or any path ending in /<command>
if (p === needles[i] || p.endsWith("/" + needles[i])) return true;
}
return false;
}
// Single shared onEnter: every exec* variant passes the file/path as
// args[0], so the same handler covers all eight entry points.
function hookExecVariant(name) {
var addr = Module.findExportByName("libc.so", name);
if (addr === null) {
send("exec hook: " + name + " not found, skipping");
return;
}
Interceptor.attach(addr, {
onEnter: function(args) {
try {
var path = Memory.readCString(args[0]);
send("EXEC " + name + ": " + path);
if (isRootProbePath(path)) {
send("Bypass native " + name + ": " + path);
// Redirect to a path that cannot exist -> ENOENT,
// identical to the binary simply not being present.
Memory.writeUtf8String(
args[0],
"/nonexistent/blocked_by_frida_ssl"
);
}
} catch (e) {
// Unreadable pointer (rare) — leave the call untouched.
}
},
onLeave: function(retval) {}
});
}
[
"execl", "execle", "execlp", "execlpe",
"execv", "execve", "execvp", "execvpe"
].forEach(hookExecVariant);
BufferedReader.readLine.overload('boolean').implementation = function() {
var text = this.readLine.overload('boolean').call(this);
if (text === null) {
// just pass , i know it's ugly as hell but test != null won't work :(
} else {
var shouldFakeRead = (text.indexOf("ro.build.tags=test-keys") > -1);
if (shouldFakeRead) {
send("Bypass build.prop file read");
text = text.replace("ro.build.tags=test-keys", "ro.build.tags=release-keys");
}
}
return text;
};
var executeCommand = ProcessBuilder.command.overload('java.util.List');
ProcessBuilder.start.implementation = function() {
var cmd = this.command.call(this);
var shouldModifyCommand = false;
var containsSu = false;
for (var i = 0; i < cmd.size(); i = i + 1) {
var tmp_cmd = cmd.get(i).toString();
// Use exact match for short words to avoid false positives
// (indexOf("id") would match "android", "video", etc.)
if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id") {
shouldModifyCommand = true;
}
if (tmp_cmd == "su") {
containsSu = true;
}
}
if (shouldModifyCommand) {
send("Bypass ProcessBuilder " + cmd);
this.command.call(this, ["grep"]);
return this.start.call(this);
}
if (containsSu) {
send("Bypass ProcessBuilder su " + cmd);
this.command.call(this, ["justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"]);
return this.start.call(this);
}
return this.start.call(this);
};
if (useProcessManager) {
var ProcManExec = ProcessManager.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;', 'java.io.File', 'boolean');
var ProcManExecVariant = ProcessManager.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;', 'java.lang.String', 'java.io.FileDescriptor', 'java.io.FileDescriptor', 'java.io.FileDescriptor', 'boolean');
ProcManExec.implementation = function(cmd, env, workdir, redirectstderr) {
var fake_cmd = cmd;
for (var i = 0; i < cmd.length; i = i + 1) {
var tmp_cmd = cmd[i];
if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id") {
fake_cmd = ["grep"];
send("Bypass " + cmd + " command");
}
if (tmp_cmd == "su") {
fake_cmd = ["justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"];
send("Bypass " + cmd + " command");
}
}
return ProcManExec.call(this, fake_cmd, env, workdir, redirectstderr);
};
ProcManExecVariant.implementation = function(cmd, env, directory, stdin, stdout, stderr, redirect) {
var fake_cmd = cmd;
for (var i = 0; i < cmd.length; i = i + 1) {
var tmp_cmd = cmd[i];
if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd == "mount" || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd == "id") {
fake_cmd = ["grep"];
send("Bypass " + cmd + " command");
}
if (tmp_cmd == "su") {
fake_cmd = ["justafakecommandthatcannotexistsusingthisshouldthowanexceptionwheneversuiscalled"];
send("Bypass " + cmd + " command");
}
}
return ProcManExecVariant.call(this, fake_cmd, env, directory, stdin, stdout, stderr, redirect);
};
}
if (useKeyInfo) {
KeyInfo.isInsideSecureHardware.implementation = function() {
send("Bypass isInsideSecureHardware");
return true;
}
}
});
//Bypass SSL Pinning
var config = {
"ios":{
"modulename": "Flutter",
"patterns":{
"arm64": [
"FF 83 01 D1 FA 67 01 A9 F8 5F 02 A9 F6 57 03 A9 F4 4F 04 A9 FD 7B 05 A9 FD 43 01 91 F? 03 00 AA ?? 0? 40 F9 ?8 1? 40 F9 15 ?? 4? F9 B5 00 00 B4",
],
},
},
"android":{
"modulename": "libflutter.so",
"patterns":{
"arm64": [
"F? 0F 1C F8 F? 5? 01 A9 F? 5? 02 A9 F? ?? 03 A9 ?? ?? ?? ?? 68 1A 40 F9",
"F? 43 01 D1 FE 67 01 A9 F8 5F 02 A9 F6 57 03 A9 F4 4F 04 A9 13 00 40 F9 F4 03 00 AA 68 1A 40 F9",
"FF 43 01 D1 FE 67 01 A9 ?? ?? 06 94 ?? 7? 06 94 68 1A 40 F9 15 15 41 F9 B5 00 00 B4 B6 4A 40 F9",
],
"arm": [
"2D E9 F? 4? D0 F8 00 80 81 46 D8 F8 18 00 D0 F8 ??",
],
"x64": [
"55 41 57 41 56 41 55 41 54 53 50 49 89 f? 4c 8b 37 49 8b 46 30 4c 8b a? ?? 0? 00 00 4d 85 e? 74 1? 4d 8b",
"55 41 57 41 56 41 55 41 54 53 48 83 EC 18 49 89 FF 48 8B 1F 48 8B 43 30 4C 8B A0 28 02 00 00 4D 85 E4 74"
]
}
}
};
var TLSValidationDisabled = false;
if (Java.available) {
console.log("[+] Java environment detected");
Java.perform(hookSystemLoadLibrary);
} else if (ObjC.available) {
console.log("[+] iOS environment detected");
}
disableTLSValidation();
setTimeout(disableTLSValidation, 20000, true);
function hookSystemLoadLibrary() {
const System = Java.use('java.lang.System');
const Runtime = Java.use('java.lang.Runtime');
const SystemLoad_2 = System.loadLibrary.overload('java.lang.String');
const VMStack = Java.use('dalvik.system.VMStack');
SystemLoad_2.implementation = function(library) {
try {
const loaded = Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), library);
if (library === 'flutter') {
console.log("[+] libflutter.so loaded");
disableTLSValidation();
}
return loaded;
} catch (ex) {
console.log(ex);
}
};
}
function disableTLSValidation(fallback=false) {
if (TLSValidationDisabled) return;
var platformConfig = config[Java.available ? "android" : "ios"];
var m = Process.findModuleByName(platformConfig["modulename"]);
// If there is no loaded Flutter module, the setTimeout may trigger a second time, but after that we give up
if (m === null) {
if (fallback) console.log("[!] Flutter module not found.");
return;
}
if (Process.arch in platformConfig["patterns"])
{
findAndPatch(m, platformConfig["patterns"][Process.arch], Java.available && Process.arch == "arm" ? 1 : 0, fallback);
}
else
{
console.log("[!] Processor architecture not supported: ", Process.arch);
}
if (!TLSValidationDisabled)
{
if (fallback){
if(m.enumerateRanges('r-x').length == 0)
{
console.log('[!] No memory ranges found in Flutter library. This is either a Frida bug, or the application is using some kind of RASP.');
}
else
{
console.log('[!] ssl_verify_peer_cert not found. Please open an issue at https://github.com/NVISOsecurity/disable-flutter-tls-verification/issues');
}
}
else
{
console.log('[!] ssl_verify_peer_cert not found. Trying again...');
}
}
}
function findAndPatch(m, patterns, thumb, fallback) {
console.log("[+] Flutter library found");
var ranges = m.enumerateRanges('r-x');
ranges.forEach(range => {
patterns.forEach(pattern => {
Memory.scan(range.base, range.size, pattern, {
onMatch: function(address, size) {
console.log('[+] ssl_verify_peer_cert found at offset: 0x' + (address - m.base).toString(16));
TLSValidationDisabled = true;
hook_ssl_verify_peer_cert(address.add(thumb));
}
});
});
});
}
function hook_ssl_verify_peer_cert(address) {
Interceptor.replace(address, new NativeCallback((pathPtr, flags) => {
return 0;
}, 'int', ['pointer', 'int']));
}
LoggingHTTPServer.py — HTTP Request Logger
📥 Download LoggingHTTPServer.py
A minimal stdlib-only HTTP server that logs every GET/POST to log.txt.
Pair it with iptables redirection to
capture raw requests when BurpSuite is overkill or unavailable.
# Default port 444 (ports < 1024 need sudo)
python3 LoggingHTTPServer.py
python3 LoggingHTTPServer.py 8080 # custom port
# Quick test
curl http://localhost:444/api/v1/ping
curl -X POST -d "user=admin&pw=secret" http://localhost:444/login
Output format:
[2026-03-07 14:30:12] [GET] Path: /api/v1/users
[2026-03-07 14:30:15] [POST] Path: /api/v1/login, Data: user=admin&pw=secret
📌 Single-threaded and unrotated by design — fine for short pentest captures, not for load testing. The log is opened in append mode, so repeated runs accumulate.
import atexit
import signal
import sys
from datetime import datetime
from http.server import BaseHTTPRequestHandler, HTTPServer
from os import getcwd
from os.path import isfile
# https://gist.github.com/mdonkers/63e115cc0c79b4f6b8b3a6b797e485c7
# CURL Request: curl -H "Content-Type: application/x-www-form-urlencoded" -X POST http://192.168.204.1:80/ -d "<Post_Data>"
LOG_FILENAME = 'log.txt'
if not isfile(LOG_FILENAME):
file = open(LOG_FILENAME, 'wb')
else:
file = open(LOG_FILENAME, 'ab')
def _cleanup():
"""Flush and close the log file on exit."""
if file and not file.closed:
file.flush()
file.close()
atexit.register(_cleanup)
class LoggingHTTPRequestHandler(BaseHTTPRequestHandler):
def _set_response(self, code=200, content_type='text/plain'):
self.send_response(code)
self.send_header('Content-Type', content_type)
self.end_headers()
def do_GET(self):
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print(f"\n[+] [{timestamp}] GET Request: {str(self.path)}\n")
file.write(f"[{timestamp}] [GET] Path: {self.path}\n".encode())
file.flush()
self._set_response()
self.wfile.write("GET request for {}".format(self.path).encode('utf-8'))
def do_POST(self):
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
content_length_hdr = self.headers.get('Content-Length')
if content_length_hdr is None:
self._set_response(400)
self.wfile.write(b"Missing Content-Length header")
return
try:
content_length = int(content_length_hdr)
except ValueError:
self._set_response(400)
self.wfile.write(b"Invalid Content-Length header")
return
post_data = self.rfile.read(content_length)
file.write(f"[{timestamp}] [POST] Path: {self.path}, Data: ".encode() + post_data + b'\n')
file.flush()
try:
decoded = post_data.decode('utf-8')
except UnicodeDecodeError:
decoded = repr(post_data)
print(f"\n[+] [{timestamp}] POST Request: {str(self.path)}\n{decoded}\n")
self._set_response()
self.wfile.write(f"POST request for {self.path}".encode('utf-8'))
def run(server_class=HTTPServer, handler_class=LoggingHTTPRequestHandler, port=444):
print(f'[+] Server running on port: {port}')
print(f'[+] Log file: {getcwd()}/{LOG_FILENAME}')
server_address = ('', port)
httpd = server_class(server_address, handler_class)
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
print(f'[+] Output file: {getcwd()}/{LOG_FILENAME}')
print('[-] Stopping HTTP Server...\n')
if __name__ == '__main__':
from sys import argv
if len(argv) == 2:
try:
port = int(argv[1])
except ValueError:
print(f"[-] Invalid port: {argv[1]} (must be a number)")
sys.exit(1)
if not (1 <= port <= 65535):
print(f"[-] Port out of range: {port} (must be 1-65535)")
sys.exit(1)
run(port=port)
else:
run()
See also
- Previous: Traffic Interception
- Series start: Android Pentest Overview