Initial commit — FlipperDroid v0.1.0-poc
KernelSU module + Flipper Zero FAP that bridges both devices into a unified pentesting platform over USB CDC serial / BT rfcomm. Android side: bridge daemon, WebUI (:8089), bind mount namespace isolation stealth engine. Flipper side: proper FAP with 4-view GUI, GPIO/SubGHz/IR/file command handlers, async event streaming.
This commit is contained in:
376
system/bin/fd-stealth
Normal file
376
system/bin/fd-stealth
Normal file
@@ -0,0 +1,376 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user