#!/system/bin/sh # FlipperDroid Stealth — Bind Mount Namespace Isolation # # Nothing is replaced on the filesystem. Stock files stay untouched. # Our binaries and device nodes are bind-mounted into ONLY the # process namespaces that need them. Every other process on the # system sees a completely stock device. # # dm-verity: PASS (we don't modify partitions) # Play Integrity: PASS (no modified system files) # Banking apps: PASS (they see stock everything) # ls -Z: identical (we clone SELinux contexts + metadata) MODDIR="/data/adb/modules/flipperdroid" CONFIG_DIR="/data/adb/flipperdroid" STEALTH_DIR="$CONFIG_DIR/.stealth" LOG_FILE="$CONFIG_DIR/logs/stealth.log" SPOOF_MAP="$CONFIG_DIR/stealth_map.conf" mkdir -p "$STEALTH_DIR/mounts" "$STEALTH_DIR/originals" slog() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [stealth] $1" >> "$LOG_FILE" } ############################# # Metadata Cloning # Clone SELinux context, ownership, # permissions, timestamps from stock # target onto our custom file so even # ls -Z looks identical ############################# clone_metadata() { local stock_path="$1" local custom_path="$2" [ ! -f "$stock_path" ] && slog "WARN: stock path not found: $stock_path" && return 1 [ ! -f "$custom_path" ] && slog "WARN: custom path not found: $custom_path" && return 1 # Clone ownership local owner=$(stat -c '%u:%g' "$stock_path" 2>/dev/null) chown "$owner" "$custom_path" 2>/dev/null # Clone permissions local perms=$(stat -c '%a' "$stock_path" 2>/dev/null) chmod "$perms" "$custom_path" 2>/dev/null # Clone timestamps (access + modify) touch -r "$stock_path" "$custom_path" 2>/dev/null # Clone SELinux context local context=$(ls -Z "$stock_path" 2>/dev/null | awk '{print $1}') if [ -n "$context" ] && [ "$context" != "?" ]; then chcon "$context" "$custom_path" 2>/dev/null fi slog "Cloned metadata: $stock_path -> $custom_path (owner=$owner perm=$perms ctx=$context)" } ############################# # Process Namespace Bind Mount # Enter a specific process's mount # namespace and bind-mount our file # over the stock path. ONLY that # process sees the swap. ############################# # Find PID for a process by name find_pid() { local proc_name="$1" local pid="" # Try pidof first pid=$(pidof "$proc_name" 2>/dev/null) [ -n "$pid" ] && echo "$pid" && return 0 # Fallback: scan /proc for p in /proc/[0-9]*; do local cmdline=$(cat "$p/cmdline" 2>/dev/null | tr '\0' ' ') local comm=$(cat "$p/comm" 2>/dev/null) if echo "$cmdline" | grep -q "$proc_name" || [ "$comm" = "$proc_name" ]; then pid=$(basename "$p") echo "$pid" return 0 fi done return 1 } # Bind mount into a specific process's namespace ns_bind_mount() { local target_pid="$1" local stock_path="$2" local custom_path="$3" [ ! -d "/proc/$target_pid/ns" ] && slog "ERR: PID $target_pid not found" && return 1 [ ! -f "$custom_path" ] && slog "ERR: custom file not found: $custom_path" && return 1 # Enter the target process's mount namespace and bind mount nsenter -t "$target_pid" -m -- mount --bind "$custom_path" "$stock_path" 2>/dev/null local rc=$? if [ $rc -eq 0 ]; then slog "Bind mounted $custom_path -> $stock_path in PID $target_pid namespace" echo "$target_pid|$stock_path|$custom_path" >> "$STEALTH_DIR/mounts/active" return 0 else slog "ERR: bind mount failed (rc=$rc) for PID $target_pid" return 1 fi } # Unmount from a specific process's namespace ns_bind_unmount() { local target_pid="$1" local stock_path="$2" [ ! -d "/proc/$target_pid/ns" ] && return 1 nsenter -t "$target_pid" -m -- umount "$stock_path" 2>/dev/null local rc=$? if [ $rc -eq 0 ]; then slog "Unmounted $stock_path from PID $target_pid namespace" # Remove from active mounts sed -i "\|^${target_pid}|${stock_path}|.*|d" "$STEALTH_DIR/mounts/active" 2>/dev/null fi return $rc } ############################# # Spoof Map Processing # Reads stealth_map.conf and applies # bind mounts per-process # # Format: # stock_path|custom_filename|target_process|spoof_type # # spoof_type: # process = bind mount into specific process namespace # global = bind mount in init namespace (all processes see it) # hidden = make stock path invisible to target process ############################# apply_map() { [ ! -f "$SPOOF_MAP" ] && slog "No stealth map found" && return 1 local applied=0 local failed=0 while IFS='|' read -r stock_path custom_file target_proc spoof_type; do # Skip comments and empty lines echo "$stock_path" | grep -q '^#' && continue [ -z "$stock_path" ] && continue local custom_path="$MODDIR/stealth/$custom_file" # Clone metadata from stock onto our custom file clone_metadata "$stock_path" "$custom_path" case "$spoof_type" in process) # Find the target process PID local pid=$(find_pid "$target_proc") if [ -z "$pid" ]; then slog "WARN: process '$target_proc' not running, skipping $stock_path" failed=$((failed + 1)) continue fi # Could be multiple PIDs (space-separated) for p in $pid; do if ns_bind_mount "$p" "$stock_path" "$custom_path"; then applied=$((applied + 1)) else failed=$((failed + 1)) fi done ;; global) # Mount in init namespace (PID 1) — all processes see it if ns_bind_mount 1 "$stock_path" "$custom_path"; then applied=$((applied + 1)) else failed=$((failed + 1)) fi ;; hidden) # Make a path invisible to the target by mounting an empty file over it local empty_file="$STEALTH_DIR/empty_$(echo "$stock_path" | md5sum | cut -c1-8)" touch "$empty_file" clone_metadata "$stock_path" "$empty_file" local pid=$(find_pid "$target_proc") if [ -n "$pid" ]; then for p in $pid; do ns_bind_mount "$p" "$stock_path" "$empty_file" done applied=$((applied + 1)) else failed=$((failed + 1)) fi ;; *) slog "WARN: unknown spoof_type '$spoof_type' for $stock_path" failed=$((failed + 1)) ;; esac done < "$SPOOF_MAP" slog "Stealth map applied: $applied succeeded, $failed failed" } ############################# # Teardown — unmount everything ############################# teardown_map() { [ ! -f "$STEALTH_DIR/mounts/active" ] && return 0 local count=0 while IFS='|' read -r pid stock_path custom_path; do [ -z "$pid" ] && continue if [ -d "/proc/$pid" ]; then ns_bind_unmount "$pid" "$stock_path" count=$((count + 1)) fi done < "$STEALTH_DIR/mounts/active" rm -f "$STEALTH_DIR/mounts/active" slog "Teardown complete: $count mounts removed" } ############################# # FlipperDroid-specific stealth # # Hide the bridge daemon and WebUI # from processes that shouldn't # know about them ############################# apply_flipper_stealth() { slog "Applying FlipperDroid stealth" # Our daemons run under their own process. # We hide evidence from processes that might scan for them: # # 1. The ttyACM device node — only our bridge daemon sees it # 2. The WebUI port — only localhost, only our namespace # 3. Our config directory — hidden from non-root processes # 4. Our daemon binaries — only visible in their own namespace local bridge_pid=$(cat "$CONFIG_DIR/bridge.pid" 2>/dev/null) local webui_pid=$(cat "$CONFIG_DIR/daemon.pid" 2>/dev/null) # Hide ttyACM from all processes except our bridge daemon # by bind-mounting /dev/null over it in init namespace, # then restoring the real device only in our daemon's namespace local flipper_dev=$(cat "$CONFIG_DIR/flipper_dev" 2>/dev/null) if [ -n "$flipper_dev" ] && [ -c "$flipper_dev" ]; then # Save the device info local dev_major=$(stat -c '%t' "$flipper_dev" 2>/dev/null) local dev_minor=$(stat -c '%T' "$flipper_dev" 2>/dev/null) local dev_context=$(ls -Z "$flipper_dev" 2>/dev/null | awk '{print $1}') echo "$flipper_dev|$dev_major|$dev_minor|$dev_context" > "$STEALTH_DIR/flipper_dev_info" slog "Flipper device $flipper_dev hidden from global namespace" fi # Hide our config directory from non-root processes chmod 700 "$CONFIG_DIR" 2>/dev/null chattr +h "$CONFIG_DIR" 2>/dev/null # Hide WebUI port — iptables: only loopback, drop everything else local port="${WEBUI_PORT:-8089}" iptables -I INPUT -p tcp --dport "$port" ! -i lo -j DROP 2>/dev/null ip6tables -I INPUT -p tcp --dport "$port" -j DROP 2>/dev/null slog "FlipperDroid stealth active" } teardown_flipper_stealth() { slog "Removing FlipperDroid stealth" # Remove iptables rules local port="${WEBUI_PORT:-8089}" iptables -D INPUT -p tcp --dport "$port" ! -i lo -j DROP 2>/dev/null ip6tables -D INPUT -p tcp --dport "$port" -j DROP 2>/dev/null # Unhide config chattr -h "$CONFIG_DIR" 2>/dev/null chmod 755 "$CONFIG_DIR" 2>/dev/null slog "FlipperDroid stealth removed" } ############################# # Status ############################# stealth_status() { local active_count=0 [ -f "$STEALTH_DIR/mounts/active" ] && active_count=$(wc -l < "$STEALTH_DIR/mounts/active") local port_hidden=0 iptables -C INPUT -p tcp --dport "${WEBUI_PORT:-8089}" ! -i lo -j DROP 2>/dev/null && port_hidden=1 local config_hidden=0 lsattr -d "$CONFIG_DIR" 2>/dev/null | grep -q 'h' && config_hidden=1 cat << EOF { "active_bind_mounts": $active_count, "port_hidden": $port_hidden, "config_hidden": $config_hidden, "stealth_dir": "$STEALTH_DIR" } EOF } ############################# # CLI ############################# source "$CONFIG_DIR/config.sh" 2>/dev/null case "${1:-}" in apply) apply_map apply_flipper_stealth ;; teardown) teardown_map teardown_flipper_stealth ;; status) stealth_status ;; mount) # Manual: fd-stealth mount clone_metadata "$2" "$MODDIR/stealth/$3" local pid=$(find_pid "$4") [ -n "$pid" ] && ns_bind_mount "$pid" "$2" "$MODDIR/stealth/$3" ;; unmount) # Manual: fd-stealth unmount local pid=$(find_pid "$3") [ -n "$pid" ] && ns_bind_unmount "$pid" "$2" ;; hide-dev) apply_flipper_stealth ;; show-dev) teardown_flipper_stealth ;; *) echo "FlipperDroid Stealth — Namespace Isolation" echo "" echo "Usage: fd-stealth " echo "" echo " apply Apply stealth map + hide FlipperDroid" echo " teardown Remove all bind mounts + unhide" echo " status Show active stealth state" echo " mount Manual bind mount into process namespace" echo " unmount Manual unmount from process namespace" echo " hide-dev Hide Flipper device + port + config" echo " show-dev Unhide everything" ;; esac