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:
sssnake
2026-03-31 21:26:58 -07:00
commit be81a92d44
22 changed files with 4191 additions and 0 deletions

376
system/bin/fd-stealth Normal file
View 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

View File

@@ -0,0 +1,365 @@
#!/system/bin/sh
# FlipperDroid WebUI — HTTP server with bridge API endpoints
WEBROOT="/data/adb/modules/flipperdroid/webroot"
CONFIG_DIR="/data/adb/flipperdroid"
CONFIG_FILE="$CONFIG_DIR/config.sh"
CMD_FIFO="$CONFIG_DIR/cmd_fifo"
RESP_DIR="$CONFIG_DIR/responses"
PORT=8089
mkdir -p "$CONFIG_DIR" "$RESP_DIR"
source "$CONFIG_FILE" 2>/dev/null
PORT="${WEBUI_PORT:-$PORT}"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [webui] $1"; }
# Generate unique request ID
req_id() { echo "req_$(date +%s%N | cut -c1-16)_$$"; }
# Send command to bridge daemon via FIFO and wait for response
bridge_cmd() {
local subsystem="$1"
local action="$2"
local params="${3:-}"
local rid=$(req_id)
local timeout=10
echo "${rid}|${subsystem}|${action}|${params}" > "$CMD_FIFO" 2>/dev/null
# Wait for response file
local waited=0
while [ ! -f "$RESP_DIR/${rid}" ] && [ $waited -lt $timeout ]; do
sleep 0.1
waited=$((waited + 1))
done
if [ -f "$RESP_DIR/${rid}" ]; then
cat "$RESP_DIR/${rid}"
rm -f "$RESP_DIR/${rid}"
else
echo "ERR:timeout"
fi
}
# Read config as JSON
config_json() {
source "$CONFIG_FILE" 2>/dev/null
local status=$(cat "$CONFIG_DIR/status" 2>/dev/null || echo "unknown")
local conn_type=$(cat "$CONFIG_DIR/conn_type" 2>/dev/null || echo "none")
local flipper_dev=$(cat "$CONFIG_DIR/flipper_dev" 2>/dev/null || echo "")
local flipper_product=$(cat "$CONFIG_DIR/flipper_product" 2>/dev/null || echo "")
local flipper_serial=$(cat "$CONFIG_DIR/flipper_serial" 2>/dev/null || echo "")
local flipper_version=$(cat "$CONFIG_DIR/flipper_version" 2>/dev/null || echo "")
local flipper_caps=$(cat "$CONFIG_DIR/flipper_caps" 2>/dev/null || echo "")
cat << EOF
{
"status": "$status",
"conn_type": "$conn_type",
"conn_mode": "${CONN_MODE:-auto}",
"device": "$flipper_dev",
"product": "$flipper_product",
"serial": "$flipper_serial",
"firmware_version": "$flipper_version",
"capabilities": "$flipper_caps",
"config": {
"auto_connect": ${AUTO_CONNECT:-1},
"baud_rate": ${BAUD_RATE:-115200},
"webui_port": ${WEBUI_PORT:-8089},
"enable_gpio": ${ENABLE_GPIO:-1},
"enable_subghz": ${ENABLE_SUBGHZ:-1},
"enable_rfid": ${ENABLE_RFID:-1},
"enable_nfc": ${ENABLE_NFC:-1},
"enable_ir": ${ENABLE_IR:-1},
"enable_ibutton": ${ENABLE_IBUTTON:-1},
"enable_badusb": ${ENABLE_BADUSB:-0},
"cpu_share": ${CPU_SHARE:-1},
"cpu_share_threads": ${CPU_SHARE_THREADS:-2},
"log_level": ${LOG_LEVEL:-2}
}
}
EOF
}
# Get recent events as JSON array
events_json() {
local count="${1:-20}"
local events_file="$CONFIG_DIR/events"
[ ! -f "$events_file" ] && echo "[]" && return
echo -n "["
local first=1
tail -n "$count" "$events_file" | while IFS='|' read -r type data1 data2 ts; do
[ $first -eq 1 ] && first=0 || echo -n ","
echo -n "{\"type\":\"$type\",\"data\":\"$data1\",\"extra\":\"$data2\",\"timestamp\":$ts}"
done
echo "]"
}
#############################
# HTTP request handler
#############################
handle_request() {
local method=""
local path=""
local content_length=0
local body=""
# Read request line
read -r line
method=$(echo "$line" | awk '{print $1}')
path=$(echo "$line" | awk '{print $2}')
# Read headers
while read -r header; do
header=$(echo "$header" | tr -d '\r')
[ -z "$header" ] && break
case "$header" in
Content-Length:*|content-length:*)
content_length=$(echo "$header" | awk '{print $2}')
;;
esac
done
# Read body if POST
if [ "$method" = "POST" ] && [ "$content_length" -gt 0 ] 2>/dev/null; then
body=$(dd bs=1 count="$content_length" 2>/dev/null)
fi
# Route
local response=""
local content_type="application/json"
local status="200 OK"
case "$method $path" in
# Static files
"GET /"|"GET /index.html")
content_type="text/html"
response=$(cat "$WEBROOT/index.html" 2>/dev/null)
;;
"GET /css/style.css")
content_type="text/css"
response=$(cat "$WEBROOT/css/style.css" 2>/dev/null)
;;
"GET /js/app.js")
content_type="application/javascript"
response=$(cat "$WEBROOT/js/app.js" 2>/dev/null)
;;
# API: Status & Config
"GET /api/status")
response=$(config_json)
;;
"GET /api/events"*)
response=$(events_json 50)
;;
"GET /api/log")
content_type="text/plain"
response=$(tail -n 100 "$CONFIG_DIR/logs/flipperdroid.log" 2>/dev/null)
;;
# API: System commands
"POST /api/system/ping")
response="{\"result\":\"$(bridge_cmd system ping)\"}"
;;
"POST /api/system/version")
response="{\"result\":\"$(bridge_cmd system version)\"}"
;;
"POST /api/system/status")
response="{\"result\":\"$(bridge_cmd system status)\"}"
;;
# API: GPIO
"POST /api/gpio/init")
local pin=$(echo "$body" | grep -o '"pin":[0-9]*' | grep -o '[0-9]*')
local mode=$(echo "$body" | grep -o '"mode":[0-9]*' | grep -o '[0-9]*')
response="{\"result\":\"$(bridge_cmd gpio init "$pin,$mode")\"}"
;;
"POST /api/gpio/write")
local pin=$(echo "$body" | grep -o '"pin":[0-9]*' | grep -o '[0-9]*')
local val=$(echo "$body" | grep -o '"value":[0-9]*' | grep -o '[0-9]*')
response="{\"result\":\"$(bridge_cmd gpio write "$pin,$val")\"}"
;;
"POST /api/gpio/read")
local pin=$(echo "$body" | grep -o '"pin":[0-9]*' | grep -o '[0-9]*')
response="{\"result\":\"$(bridge_cmd gpio read "$pin")\"}"
;;
"POST /api/gpio/pwm")
local pin=$(echo "$body" | grep -o '"pin":[0-9]*' | grep -o '[0-9]*')
local freq=$(echo "$body" | grep -o '"freq":[0-9]*' | grep -o '[0-9]*')
local duty=$(echo "$body" | grep -o '"duty":[0-9]*' | grep -o '[0-9]*')
response="{\"result\":\"$(bridge_cmd gpio pwm "$pin,$freq,$duty")\"}"
;;
"POST /api/gpio/adc")
local pin=$(echo "$body" | grep -o '"pin":[0-9]*' | grep -o '[0-9]*')
response="{\"result\":\"$(bridge_cmd gpio adc "$pin")\"}"
;;
# API: SubGHz
"POST /api/subghz/set_freq")
local freq=$(echo "$body" | grep -o '"freq":[0-9]*' | grep -o '[0-9]*')
response="{\"result\":\"$(bridge_cmd subghz set_freq "$freq")\"}"
;;
"POST /api/subghz/tx")
local data=$(echo "$body" | grep -o '"data":"[^"]*"' | cut -d'"' -f4)
response="{\"result\":\"$(bridge_cmd subghz tx "$data")\"}"
;;
"POST /api/subghz/rx_start")
response="{\"result\":\"$(bridge_cmd subghz rx_start)\"}"
;;
"POST /api/subghz/rx_stop")
response="{\"result\":\"$(bridge_cmd subghz rx_stop)\"}"
;;
"POST /api/subghz/get_rssi")
response="{\"result\":\"$(bridge_cmd subghz get_rssi)\"}"
;;
"POST /api/subghz/replay")
local slot=$(echo "$body" | grep -o '"slot":[0-9]*' | grep -o '[0-9]*')
response="{\"result\":\"$(bridge_cmd subghz replay "$slot")\"}"
;;
# API: RFID
"POST /api/rfid/read")
response="{\"result\":\"$(bridge_cmd rfid read)\"}"
;;
"POST /api/rfid/emulate")
local data=$(echo "$body" | grep -o '"data":"[^"]*"' | cut -d'"' -f4)
response="{\"result\":\"$(bridge_cmd rfid emulate "$data")\"}"
;;
# API: NFC
"POST /api/nfc/poll")
response="{\"result\":\"$(bridge_cmd nfc poll)\"}"
;;
"POST /api/nfc/read_full")
response="{\"result\":\"$(bridge_cmd nfc read_full)\"}"
;;
"POST /api/nfc/emulate")
local data=$(echo "$body" | grep -o '"data":"[^"]*"' | cut -d'"' -f4)
local type=$(echo "$body" | grep -o '"type":[0-9]*' | grep -o '[0-9]*')
response="{\"result\":\"$(bridge_cmd nfc emulate "${data}${type}")\"}"
;;
"POST /api/nfc/relay_start")
response="{\"result\":\"$(bridge_cmd nfc relay_start)\"}"
;;
"POST /api/nfc/relay_stop")
response="{\"result\":\"$(bridge_cmd nfc relay_stop)\"}"
;;
# API: IR
"POST /api/ir/tx")
local proto=$(echo "$body" | grep -o '"protocol":[0-9]*' | grep -o '[0-9]*')
local addr=$(echo "$body" | grep -o '"address":[0-9]*' | grep -o '[0-9]*')
local cmd=$(echo "$body" | grep -o '"command":[0-9]*' | grep -o '[0-9]*')
response="{\"result\":\"$(bridge_cmd ir tx "$proto,$addr,$cmd")\"}"
;;
"POST /api/ir/rx_start")
response="{\"result\":\"$(bridge_cmd ir rx_start)\"}"
;;
"POST /api/ir/rx_stop")
response="{\"result\":\"$(bridge_cmd ir rx_stop)\"}"
;;
"POST /api/ir/replay")
local slot=$(echo "$body" | grep -o '"slot":[0-9]*' | grep -o '[0-9]*')
response="{\"result\":\"$(bridge_cmd ir replay "$slot")\"}"
;;
# API: iButton
"POST /api/ibutton/read")
response="{\"result\":\"$(bridge_cmd ibutton read)\"}"
;;
"POST /api/ibutton/emulate")
local data=$(echo "$body" | grep -o '"data":"[^"]*"' | cut -d'"' -f4)
response="{\"result\":\"$(bridge_cmd ibutton emulate "$data")\"}"
;;
# API: BadUSB
"POST /api/badusb/start")
response="{\"result\":\"$(bridge_cmd badusb start)\"}"
;;
"POST /api/badusb/exec")
local script=$(echo "$body" | grep -o '"script":"[^"]*"' | cut -d'"' -f4)
response="{\"result\":\"$(bridge_cmd badusb exec "$script")\"}"
;;
"POST /api/badusb/stop")
response="{\"result\":\"$(bridge_cmd badusb stop)\"}"
;;
# API: File operations on Flipper SD
"POST /api/file/list")
local fpath=$(echo "$body" | grep -o '"path":"[^"]*"' | cut -d'"' -f4)
response="{\"result\":\"$(bridge_cmd file list "$fpath")\"}"
;;
# API: Config update
"POST /api/config")
# Write config values back
local key=$(echo "$body" | grep -o '"key":"[^"]*"' | cut -d'"' -f4)
local value=$(echo "$body" | grep -o '"value":"[^"]*"' | cut -d'"' -f4)
if [ -n "$key" ] && [ -n "$value" ]; then
sed -i "s/^${key}=.*/${key}=${value}/" "$CONFIG_FILE"
source "$CONFIG_FILE"
response="{\"ok\":true}"
else
response="{\"ok\":false,\"error\":\"missing key or value\"}"
fi
;;
# API: Reconnect
"POST /api/reconnect")
echo "disconnected" > "$CONFIG_DIR/status"
response="{\"ok\":true,\"message\":\"reconnecting...\"}"
;;
# API: Stealth
"GET /api/stealth/status")
response=$(/system/bin/fd-stealth status 2>/dev/null || echo '{"error":"stealth not available"}')
;;
"POST /api/stealth/apply")
/system/bin/fd-stealth apply >> "$CONFIG_DIR/logs/stealth.log" 2>&1
response="{\"ok\":true,\"message\":\"stealth applied\"}"
;;
"POST /api/stealth/teardown")
/system/bin/fd-stealth teardown >> "$CONFIG_DIR/logs/stealth.log" 2>&1
response="{\"ok\":true,\"message\":\"stealth removed\"}"
;;
"POST /api/stealth/hide")
/system/bin/fd-stealth hide-dev >> "$CONFIG_DIR/logs/stealth.log" 2>&1
response="{\"ok\":true,\"message\":\"device hidden\"}"
;;
"POST /api/stealth/show")
/system/bin/fd-stealth show-dev >> "$CONFIG_DIR/logs/stealth.log" 2>&1
response="{\"ok\":true,\"message\":\"device visible\"}"
;;
*)
status="404 Not Found"
response="{\"error\":\"not found\"}"
;;
esac
# Send HTTP response
local content_length=${#response}
printf "HTTP/1.1 %s\r\n" "$status"
printf "Content-Type: %s\r\n" "$content_type"
printf "Content-Length: %d\r\n" "$content_length"
printf "Access-Control-Allow-Origin: *\r\n"
printf "Connection: close\r\n"
printf "\r\n"
printf "%s" "$response"
}
#############################
# Start HTTP server
#############################
log "FlipperDroid WebUI starting on port $PORT"
# Use busybox nc or toybox for HTTP serving
while true; do
handle_request | busybox nc -l -p "$PORT" -w 30 2>/dev/null || \
handle_request | toybox nc -l -p "$PORT" -W 30 2>/dev/null || \
handle_request | nc -l -p "$PORT" 2>/dev/null
done

481
system/bin/flipperdroidd Normal file
View File

@@ -0,0 +1,481 @@
#!/system/bin/sh
# FlipperDroid Bridge Daemon
# Manages bidirectional communication between Android and Flipper Zero
# Protocol: binary framed messages over USB CDC (ttyACM) or BT rfcomm
CONFIG_DIR="/data/adb/flipperdroid"
CONFIG_FILE="$CONFIG_DIR/config.sh"
LOG_FILE="$CONFIG_DIR/logs/flipperdroid.log"
FIFO_TX="$CONFIG_DIR/bridge_tx"
FIFO_RX="$CONFIG_DIR/bridge_rx"
CMD_FIFO="$CONFIG_DIR/cmd_fifo"
RESP_DIR="$CONFIG_DIR/responses"
MAGIC_HI=$(printf '\xFD')
MAGIC_LO=$(printf '\x01')
log() {
local level="$1"; shift
local min_level=$(cat "$CONFIG_DIR/log_level" 2>/dev/null || echo 2)
[ "$level" -le "$min_level" ] && echo "[$(date '+%Y-%m-%d %H:%M:%S')] [bridge] $*" >> "$LOG_FILE"
}
source "$CONFIG_FILE" 2>/dev/null
mkdir -p "$RESP_DIR"
# CRC8 lookup (Dallas/Maxim polynomial 0x31)
crc8() {
local data="$1"
local crc=0
local i j byte
for i in $(echo "$data" | sed 's/../& /g'); do
byte=$((16#$i))
crc=$((crc ^ byte))
for j in $(seq 1 8); do
if [ $((crc & 0x80)) -ne 0 ]; then
crc=$(( ((crc << 1) ^ 0x31) & 0xFF ))
else
crc=$(( (crc << 1) & 0xFF ))
fi
done
done
printf '%02x' $crc
}
# Build a protocol frame: magic(2) + len(2) + cmd(1) + payload(N) + crc8(1)
build_frame() {
local cmd_hex="$1"
local payload_hex="$2"
local payload_len=$((${#payload_hex} / 2))
local total_len=$((payload_len + 1)) # cmd + payload
local len_hi=$(printf '%02x' $((total_len >> 8)))
local len_lo=$(printf '%02x' $((total_len & 0xFF)))
local frame_data="${len_hi}${len_lo}${cmd_hex}${payload_hex}"
local crc=$(crc8 "$frame_data")
echo "FD01${frame_data}${crc}"
}
# Send raw hex frame to Flipper
send_raw() {
local hex="$1"
local dev=$(cat "$CONFIG_DIR/flipper_dev" 2>/dev/null)
[ -z "$dev" ] || [ ! -c "$dev" ] && return 1
echo "$hex" | xxd -r -p > "$dev"
}
# Send command and wait for response
send_cmd() {
local cmd="$1"
local payload="${2:-}"
local timeout="${3:-5}"
local frame=$(build_frame "$cmd" "$payload")
local dev=$(cat "$CONFIG_DIR/flipper_dev" 2>/dev/null)
[ -z "$dev" ] || [ ! -c "$dev" ] && echo "ERR:no_device" && return 1
log 3 "TX: cmd=0x${cmd} payload=${payload:-<none>}"
# Send frame
echo "$frame" | xxd -r -p > "$dev" 2>/dev/null
if [ $? -ne 0 ]; then
log 1 "TX failed — device gone?"
echo "disconnected" > "$CONFIG_DIR/status"
echo "ERR:tx_failed"
return 1
fi
# Read response with timeout
local response=""
response=$(timeout "$timeout" dd if="$dev" bs=1 count=256 2>/dev/null | xxd -p | tr -d '\n')
if [ -z "$response" ]; then
log 1 "RX timeout for cmd 0x${cmd}"
echo "ERR:timeout"
return 1
fi
log 3 "RX: $response"
# Parse response — extract after magic bytes
local resp_cmd=$(echo "$response" | cut -c9-10)
local resp_payload=$(echo "$response" | cut -c11-)
if [ "$resp_cmd" = "fe" ]; then
echo "OK:${resp_payload}"
elif [ "$resp_cmd" = "ff" ]; then
echo "ERR:${resp_payload}"
else
echo "RAW:${response}"
fi
}
#############################
# Handshake with Flipper
#############################
handshake() {
log 2 "Initiating handshake with Flipper Zero..."
# Send PING
local resp=$(send_cmd "01" "" 3)
if ! echo "$resp" | grep -q "^OK"; then
log 1 "Handshake PING failed: $resp"
return 1
fi
log 2 "PING OK"
# Get version
resp=$(send_cmd "02" "" 3)
if echo "$resp" | grep -q "^OK:"; then
local version_hex=$(echo "$resp" | sed 's/OK://')
echo "$version_hex" | xxd -r -p > "$CONFIG_DIR/flipper_version" 2>/dev/null
log 2 "Flipper version: $(cat "$CONFIG_DIR/flipper_version")"
fi
# Get capabilities
resp=$(send_cmd "03" "" 3)
if echo "$resp" | grep -q "^OK:"; then
echo "$resp" | sed 's/OK://' > "$CONFIG_DIR/flipper_caps"
log 2 "Capabilities: $(cat "$CONFIG_DIR/flipper_caps")"
fi
echo "connected" > "$CONFIG_DIR/status"
log 2 "Handshake complete — bridge active"
return 0
}
#############################
# Command FIFO handler
# WebUI and other tools write commands here
#############################
process_api_command() {
local cmd_line="$1"
local req_id=$(echo "$cmd_line" | cut -d'|' -f1)
local subsystem=$(echo "$cmd_line" | cut -d'|' -f2)
local action=$(echo "$cmd_line" | cut -d'|' -f3)
local params=$(echo "$cmd_line" | cut -d'|' -f4-)
log 3 "API cmd: $req_id $subsystem $action $params"
local result=""
case "$subsystem" in
system)
case "$action" in
ping) result=$(send_cmd "01") ;;
version) result=$(send_cmd "02") ;;
caps) result=$(send_cmd "03") ;;
status) result=$(send_cmd "04") ;;
esac
;;
gpio)
case "$action" in
init)
local pin=$(echo "$params" | cut -d',' -f1)
local mode=$(echo "$params" | cut -d',' -f2)
local pin_hex=$(printf '%02x' "$pin")
local mode_hex=$(printf '%02x' "$mode")
result=$(send_cmd "10" "${pin_hex}${mode_hex}")
;;
write)
local pin=$(echo "$params" | cut -d',' -f1)
local val=$(echo "$params" | cut -d',' -f2)
result=$(send_cmd "11" "$(printf '%02x%02x' "$pin" "$val")")
;;
read)
local pin=$(echo "$params" | cut -d',' -f1)
result=$(send_cmd "12" "$(printf '%02x' "$pin")")
;;
pwm)
local pin=$(echo "$params" | cut -d',' -f1)
local freq=$(echo "$params" | cut -d',' -f2)
local duty=$(echo "$params" | cut -d',' -f3)
local pin_hex=$(printf '%02x' "$pin")
local freq_hex=$(printf '%08x' "$freq")
local duty_hex=$(printf '%02x' "$duty")
result=$(send_cmd "13" "${pin_hex}${freq_hex}${duty_hex}")
;;
adc)
local pin=$(echo "$params" | cut -d',' -f1)
result=$(send_cmd "14" "$(printf '%02x' "$pin")")
;;
esac
;;
subghz)
case "$action" in
set_freq)
local freq=$(echo "$params" | cut -d',' -f1)
result=$(send_cmd "20" "$(printf '%08x' "$freq")")
;;
tx)
local data_hex="$params"
result=$(send_cmd "21" "$data_hex" 10)
;;
rx_start) result=$(send_cmd "22") ;;
rx_stop) result=$(send_cmd "23") ;;
get_rssi) result=$(send_cmd "24") ;;
set_mod)
local mod=$(echo "$params" | cut -d',' -f1)
local bw=$(echo "$params" | cut -d',' -f2)
result=$(send_cmd "25" "$(printf '%02x%02x' "$mod" "$bw")")
;;
replay)
local slot=$(echo "$params" | cut -d',' -f1)
result=$(send_cmd "26" "$(printf '%02x' "$slot")")
;;
esac
;;
rfid)
case "$action" in
read) result=$(send_cmd "30" "" 10) ;;
emulate) result=$(send_cmd "31" "$params") ;;
write) result=$(send_cmd "32" "$params") ;;
esac
;;
nfc)
case "$action" in
poll) result=$(send_cmd "40" "" 10) ;;
read_full) result=$(send_cmd "41" "" 15) ;;
emulate) result=$(send_cmd "42" "$params") ;;
relay_start) result=$(send_cmd "43") ;;
relay_stop) result=$(send_cmd "44") ;;
raw_exchange) result=$(send_cmd "45" "$params" 10) ;;
esac
;;
ir)
case "$action" in
tx)
local proto=$(echo "$params" | cut -d',' -f1)
local addr=$(echo "$params" | cut -d',' -f2)
local cmd=$(echo "$params" | cut -d',' -f3)
result=$(send_cmd "50" "$(printf '%02x%08x%08x' "$proto" "$addr" "$cmd")")
;;
tx_raw) result=$(send_cmd "51" "$params" 10) ;;
rx_start) result=$(send_cmd "52") ;;
rx_stop) result=$(send_cmd "53") ;;
replay) result=$(send_cmd "54" "$(printf '%02x' "$params")") ;;
esac
;;
ibutton)
case "$action" in
read) result=$(send_cmd "60" "" 10) ;;
emulate) result=$(send_cmd "61" "$params") ;;
write) result=$(send_cmd "62" "$params") ;;
esac
;;
badusb)
case "$action" in
start) result=$(send_cmd "70") ;;
exec) result=$(send_cmd "71" "$(echo -n "$params" | xxd -p)") ;;
stop) result=$(send_cmd "72") ;;
esac
;;
file)
case "$action" in
list) result=$(send_cmd "90" "$(echo -n "$params" | xxd -p)" 10) ;;
read) result=$(send_cmd "91" "$(echo -n "$params" | xxd -p)" 10) ;;
write) result=$(send_cmd "92" "$params" 10) ;;
delete) result=$(send_cmd "93" "$(echo -n "$params" | xxd -p)") ;;
esac
;;
*)
result="ERR:unknown_subsystem"
;;
esac
# Write result to response file
echo "$result" > "$RESP_DIR/${req_id}"
log 3 "Result for $req_id: $result"
}
#############################
# Async event listener
# Reads unsolicited events from Flipper
#############################
event_listener() {
local dev=$(cat "$CONFIG_DIR/flipper_dev" 2>/dev/null)
[ -z "$dev" ] || [ ! -c "$dev" ] && return
log 2 "Event listener started on $dev"
while true; do
# Read from device, look for event frames (cmd >= 0xA0)
local raw=$(dd if="$dev" bs=1 count=2 2>/dev/null | xxd -p)
# Check for magic bytes
if [ "$raw" = "fd01" ]; then
# Read length
local len_hex=$(dd if="$dev" bs=1 count=2 2>/dev/null | xxd -p)
local len=$((16#$len_hex))
# Read cmd + payload + crc
local data=$(dd if="$dev" bs=1 count=$((len + 1)) 2>/dev/null | xxd -p)
local event_cmd=$(echo "$data" | cut -c1-2)
local event_payload=$(echo "$data" | cut -c3-$((${#data} - 2)))
case "$event_cmd" in
a0) # GPIO interrupt
local pin=$((16#$(echo "$event_payload" | cut -c1-2)))
local val=$((16#$(echo "$event_payload" | cut -c3-4)))
log 3 "GPIO IRQ: pin=$pin val=$val"
echo "gpio_irq|$pin|$val|$(date +%s)" >> "$CONFIG_DIR/events"
;;
a1) # SubGHz RX data
log 3 "SubGHz RX: $event_payload"
echo "subghz_rx|$event_payload|$(date +%s)" >> "$CONFIG_DIR/events"
;;
a2) # IR RX
log 3 "IR RX: $event_payload"
echo "ir_rx|$event_payload|$(date +%s)" >> "$CONFIG_DIR/events"
;;
a3) # NFC field detected
log 3 "NFC field event: $event_payload"
echo "nfc_field|$event_payload|$(date +%s)" >> "$CONFIG_DIR/events"
;;
a4) # Button press on Flipper
local btn=$((16#$(echo "$event_payload" | cut -c1-2)))
local state=$((16#$(echo "$event_payload" | cut -c3-4)))
log 3 "Button: btn=$btn state=$state"
echo "button|$btn|$state|$(date +%s)" >> "$CONFIG_DIR/events"
;;
a5) # CPU offload request from Flipper
local task_id=$(echo "$event_payload" | cut -c1-8)
local workload=$(echo "$event_payload" | cut -c9-)
log 2 "CPU offload request: task=$task_id"
handle_cpu_offload "$task_id" "$workload" &
;;
esac
fi
done
}
#############################
# CPU sharing — run Flipper's workload on phone
#############################
handle_cpu_offload() {
local task_id="$1"
local workload_hex="$2"
log 2 "Processing CPU offload task $task_id"
# Decode workload — it's a simple bytecode or shell command
local workload=$(echo "$workload_hex" | xxd -r -p)
local result=""
# Execute in sandboxed context
result=$(echo "$workload" | timeout 30 sh 2>&1)
local exit_code=$?
# Send result back to Flipper
local status_hex=$(printf '%02x' $exit_code)
local result_hex=$(echo -n "$result" | xxd -p | tr -d '\n')
send_cmd "81" "${task_id}${status_hex}${result_hex}" 5
log 2 "CPU task $task_id complete (exit=$exit_code)"
}
#############################
# Connection monitor
#############################
connection_monitor() {
while true; do
sleep 10
local dev=$(cat "$CONFIG_DIR/flipper_dev" 2>/dev/null)
local status=$(cat "$CONFIG_DIR/status" 2>/dev/null)
if [ "$status" = "connected" ]; then
# Check if device still exists
if [ ! -c "$dev" ]; then
log 1 "Flipper device $dev disappeared — disconnected"
echo "disconnected" > "$CONFIG_DIR/status"
# Try to rediscover
sleep 5
local new_dev=""
for tty in /dev/ttyACM*; do
[ -c "$tty" ] && new_dev="$tty" && break
done
if [ -n "$new_dev" ]; then
log 2 "Flipper reconnected on $new_dev"
echo "$new_dev" > "$CONFIG_DIR/flipper_dev"
stty -F "$new_dev" ${BAUD_RATE:-115200} raw -echo -echoe -echok 2>/dev/null
handshake && echo "connected" > "$CONFIG_DIR/status"
fi
else
# Heartbeat ping
local resp=$(send_cmd "01" "" 2)
if ! echo "$resp" | grep -q "^OK"; then
log 1 "Heartbeat failed — marking disconnected"
echo "disconnected" > "$CONFIG_DIR/status"
fi
fi
elif [ "$status" = "disconnected" ]; then
# Try to find Flipper
for tty in /dev/ttyACM*; do
if [ -c "$tty" ]; then
log 2 "Found new device $tty, attempting handshake"
echo "$tty" > "$CONFIG_DIR/flipper_dev"
stty -F "$tty" ${BAUD_RATE:-115200} raw -echo -echoe -echok 2>/dev/null
if handshake; then
echo "connected" > "$CONFIG_DIR/status"
fi
break
fi
done
fi
done
}
#############################
# Main loop
#############################
log 2 "FlipperDroid bridge daemon starting"
# Create command FIFO
rm -f "$CMD_FIFO"
mkfifo "$CMD_FIFO" 2>/dev/null
chmod 660 "$CMD_FIFO"
# Initial handshake
if [ "$(cat "$CONFIG_DIR/status" 2>/dev/null)" = "connected" ]; then
handshake
fi
# Start connection monitor in background
connection_monitor &
MONITOR_PID=$!
# Start event listener in background
event_listener &
LISTENER_PID=$!
# Store PIDs for cleanup
echo "$MONITOR_PID" > "$CONFIG_DIR/monitor.pid"
echo "$LISTENER_PID" > "$CONFIG_DIR/listener.pid"
# Main command processing loop — reads from FIFO
log 2 "Bridge daemon ready, listening on $CMD_FIFO"
while true; do
if read -r cmd_line < "$CMD_FIFO" 2>/dev/null; then
[ -n "$cmd_line" ] && process_api_command "$cmd_line" &
else
sleep 1
fi
done

View File

@@ -0,0 +1,96 @@
# FlipperDroid Bridge Protocol v0.1
Binary protocol over USB CDC serial or BT rfcomm.
All messages are framed: [MAGIC(2)][LEN(2)][CMD(1)][PAYLOAD(N)][CRC8(1)]
## Magic
0xFD 0x01 ("Flipper Droid" v01)
## Commands (Phone -> Flipper)
### System
0x01 PING -> expects PONG
0x02 VERSION -> returns firmware version, device name
0x03 CAPABILITIES -> returns bitmask of available subsystems
0x04 STATUS -> returns battery, temp, uptime
### GPIO
0x10 GPIO_INIT pin(1) mode(1) -> OK/ERR
0x11 GPIO_WRITE pin(1) value(1) -> OK/ERR
0x12 GPIO_READ pin(1) -> value(1)
0x13 GPIO_PWM pin(1) freq(4) duty(1) -> OK/ERR
0x14 GPIO_ADC_READ pin(1) -> value(2)
0x15 GPIO_INTERRUPT pin(1) edge(1) enable(1) -> OK/ERR (Flipper pushes events)
### SubGHz
0x20 SUBGHZ_SET_FREQ freq_hz(4) -> OK/ERR
0x21 SUBGHZ_TX data(N) -> OK/ERR
0x22 SUBGHZ_RX_START - -> OK/ERR (starts streaming)
0x23 SUBGHZ_RX_STOP - -> OK/ERR
0x24 SUBGHZ_GET_RSSI - -> rssi(2)
0x25 SUBGHZ_SET_MODULATION mod(1) bandwidth(1) -> OK/ERR
0x26 SUBGHZ_REPLAY slot(1) -> OK/ERR (replay captured signal)
### RFID (125kHz)
0x30 RFID_READ - -> uid(N) protocol(1)
0x31 RFID_EMULATE uid(N) protocol(1) -> OK/ERR
0x32 RFID_WRITE uid(N) protocol(1) -> OK/ERR
### NFC (13.56MHz)
0x40 NFC_POLL - -> type(1) uid(N) atqa(2) sak(1)
0x41 NFC_READ_FULL - -> dump(N)
0x42 NFC_EMULATE data(N) type(1) -> OK/ERR
0x43 NFC_RELAY_START - -> OK/ERR (relay mode via phone network)
0x44 NFC_RELAY_STOP - -> OK/ERR
0x45 NFC_RAW_EXCHANGE data(N) -> response(N)
### Infrared
0x50 IR_TX protocol(1) addr(4) cmd(4) -> OK/ERR
0x51 IR_TX_RAW timings(N*2) -> OK/ERR
0x52 IR_RX_START - -> OK/ERR (starts streaming)
0x53 IR_RX_STOP - -> OK/ERR
0x54 IR_REPLAY slot(1) -> OK/ERR
### iButton
0x60 IBUTTON_READ - -> key(8) type(1)
0x61 IBUTTON_EMULATE key(8) type(1) -> OK/ERR
0x62 IBUTTON_WRITE key(8) type(1) -> OK/ERR
### BadUSB (Flipper acts as HID to another target)
0x70 BADUSB_START - -> OK/ERR
0x71 BADUSB_EXEC script(N) -> OK/ERR
0x72 BADUSB_STOP - -> OK/ERR
### CPU Share (Phone -> Flipper offload)
0x80 CPU_TASK_SUBMIT task_id(4) code(N) -> OK/ERR
0x81 CPU_TASK_RESULT task_id(4) -> status(1) result(N)
0x82 CPU_TASK_CANCEL task_id(4) -> OK/ERR
### File Transfer
0x90 FILE_LIST path(N) -> entries(N)
0x91 FILE_READ path(N) -> data(N)
0x92 FILE_WRITE path(N) data(N) -> OK/ERR
0x93 FILE_DELETE path(N) -> OK/ERR
## Commands (Flipper -> Phone)
### Async Events
0xA0 EVENT_GPIO_IRQ pin(1) value(1) timestamp(4)
0xA1 EVENT_SUBGHZ_RX data(N) rssi(2) freq(4)
0xA2 EVENT_IR_RX protocol(1) addr(4) cmd(4)
0xA3 EVENT_NFC_FIELD type(1)
0xA4 EVENT_BUTTON button(1) state(1)
0xA5 EVENT_CPU_REQUEST task_id(4) workload(N) -> phone runs it, returns result
## Responses
0xFE OK optional_data(N)
0xFF ERR error_code(1) message(N)
## Error Codes
0x01 UNKNOWN_CMD
0x02 INVALID_PARAMS
0x03 SUBSYSTEM_DISABLED
0x04 HARDWARE_ERROR
0x05 BUSY
0x06 TIMEOUT
0x07 NOT_SUPPORTED