Files

377 lines
11 KiB
Plaintext
Raw Permalink Normal View History

#!/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 <stock_path> <custom_file> <process> <type>
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 <stock_path> <process>
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 <command>"
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