377 lines
11 KiB
Plaintext
377 lines
11 KiB
Plaintext
|
|
#!/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
|