377 lines
16 KiB
Bash
Executable File
377 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ==============================================================================
|
||
# frida_workphone_oneclick.sh — 工作手机·Frida 主控一键启动
|
||
#
|
||
# 目标:插上 Type-C 数据线 → 自动完成"Frida 主、ADB 辅"的全链路启动
|
||
# 1) 探测 USB 设备、确认 ADB 授权
|
||
# 2) 锁定主机 frida 版本(默认 16.5.6)
|
||
# 3) 在设备上推送/启动 frida-server-16(Root 模式 = 主路径)
|
||
# 4) Root 不可用时自动尝试 Frida Gadget 备背端口转发
|
||
# 5) 启动 SDK 服务(uvicorn 8899)
|
||
# 6) 通过 /api/v3/hook/probe/{device_id} 探测 Hook 是否就绪
|
||
# 7) 启动 Agent(WebSocket 通道,带 hook=enabled)
|
||
# 8) 输出对接清单(Type-C 模式下存客宝直接调用 8899)
|
||
#
|
||
# 设计参考:
|
||
# - 开发文档/8、部署/设备端Hook安装部署.md(Root + frida-server 标准)
|
||
# - 机擎/references/快速入机部署_Root_Frida_Hook.md(10/10 验证基线)
|
||
# - sdk/agent/hook/frida_manager.py(usb / gadget / remote 三模式)
|
||
# - sdk/app/routers/unified.py(/hook/probe/{id}, /hook/execute, channel=hook)
|
||
#
|
||
# 用法:
|
||
# ./scripts/frida_workphone_oneclick.sh # 默认设备 + 默认参数
|
||
# ./scripts/frida_workphone_oneclick.sh -d <serial> # 指定 ADB 序列号
|
||
# ./scripts/frida_workphone_oneclick.sh --gadget # 强制走 Gadget 备背
|
||
# ./scripts/frida_workphone_oneclick.sh --no-agent # 仅起 SDK + Frida,不起 Agent
|
||
# ./scripts/frida_workphone_oneclick.sh --probe-only # 只做探测,不启服务
|
||
# ==============================================================================
|
||
set -euo pipefail
|
||
|
||
# ── 路径与默认值 ──────────────────────────────────────────────────────────────
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
SDK_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||
PROJ_ROOT="$(cd "${SDK_ROOT}/.." && pwd)"
|
||
|
||
FRIDA_VERSION_DEFAULT="16.5.6"
|
||
FRIDA_VERSION="${WORKPHONE_FRIDA_VERSION:-${FRIDA_VERSION_DEFAULT}}"
|
||
SDK_PORT="${WORKPHONE_SDK_PORT:-8899}"
|
||
SDK_BASE="http://127.0.0.1:${SDK_PORT}"
|
||
GADGET_DEFAULT_PORT="${WORKPHONE_GADGET_PORT:-27042}"
|
||
LOG_DIR="${SDK_ROOT}/logs"
|
||
mkdir -p "${LOG_DIR}"
|
||
|
||
DEVICE_SERIAL=""
|
||
FORCE_GADGET=0
|
||
NO_AGENT=0
|
||
PROBE_ONLY=0
|
||
|
||
# ── 参数解析 ──────────────────────────────────────────────────────────────────
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
-d|--device) DEVICE_SERIAL="$2"; shift 2 ;;
|
||
--gadget) FORCE_GADGET=1; shift ;;
|
||
--no-agent) NO_AGENT=1; shift ;;
|
||
--probe-only) PROBE_ONLY=1; shift ;;
|
||
-h|--help)
|
||
sed -n '2,30p' "$0"; exit 0 ;;
|
||
*) echo "[!] 未知参数: $1"; exit 1 ;;
|
||
esac
|
||
done
|
||
|
||
# ── 颜色与日志 ────────────────────────────────────────────────────────────────
|
||
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
|
||
log() { echo -e "${GREEN}[+]${NC} $*"; }
|
||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||
fail() { echo -e "${RED}[✗]${NC} $*" >&2; }
|
||
fatal() { fail "$*"; exit 1; }
|
||
|
||
banner() {
|
||
echo "================================================================"
|
||
echo " 工作手机 · Frida 主控一键启动 (Type-C / 主:Frida,辅:ADB)"
|
||
echo " SDK_ROOT = ${SDK_ROOT}"
|
||
echo " FRIDA_TARGET = ${FRIDA_VERSION}"
|
||
echo " SDK_PORT = ${SDK_PORT}"
|
||
echo "================================================================"
|
||
}
|
||
|
||
# ── 步骤 1:检查 ADB ──────────────────────────────────────────────────────────
|
||
step_check_adb() {
|
||
command -v adb >/dev/null 2>&1 || fatal "未找到 adb,请先安装 platform-tools 并加入 PATH"
|
||
log "ADB version: $(adb version | head -1)"
|
||
|
||
# 启动 server
|
||
adb start-server >/dev/null 2>&1 || true
|
||
|
||
if [[ -n "${DEVICE_SERIAL}" ]]; then
|
||
bash "${SDK_ROOT}/scripts/wait_adb_device.sh" "${DEVICE_SERIAL}" "${ADB_WAIT_SEC:-90}" || {
|
||
fail "ADB 未连接: ${DEVICE_SERIAL}"
|
||
echo " 无线:开发者选项 → 无线调试 → 配对后把 IP:端口 设为 DEVICE_SERIAL"
|
||
echo " 或有线:Type-C 连接并授权 USB 调试"
|
||
exit 1
|
||
}
|
||
fi
|
||
|
||
if [[ -z "${DEVICE_SERIAL}" ]]; then
|
||
DEVICE_SERIAL="$(adb devices | awk '/\tdevice$/ {print $1; exit}')"
|
||
fi
|
||
if [[ -z "${DEVICE_SERIAL}" ]]; then
|
||
fail "未检测到已授权的 USB 设备"
|
||
echo " 1) 请用 Type-C 数据线连接手机,并在手机上点'始终允许此电脑调试'"
|
||
echo " 2) 然后重新执行 $0"
|
||
exit 1
|
||
fi
|
||
log "目标设备: ${DEVICE_SERIAL}"
|
||
|
||
# 设备能力快照
|
||
ABI="$(adb -s "${DEVICE_SERIAL}" shell getprop ro.product.cpu.abi | tr -d '\r')"
|
||
MODEL="$(adb -s "${DEVICE_SERIAL}" shell getprop ro.product.model | tr -d '\r')"
|
||
ANDROID="$(adb -s "${DEVICE_SERIAL}" shell getprop ro.build.version.release | tr -d '\r')"
|
||
log "Model=${MODEL} ABI=${ABI} Android=${ANDROID}"
|
||
|
||
case "${ABI}" in
|
||
arm64-v8a) FRIDA_ARCH="arm64" ;;
|
||
armeabi-v7a) FRIDA_ARCH="arm" ;;
|
||
x86_64) FRIDA_ARCH="x86_64" ;;
|
||
x86) FRIDA_ARCH="x86" ;;
|
||
*) fatal "不支持的架构: ${ABI}" ;;
|
||
esac
|
||
}
|
||
|
||
# ── 步骤 2:锁定主机 frida 版本 ───────────────────────────────────────────────
|
||
step_lock_host_frida() {
|
||
command -v python3 >/dev/null 2>&1 || fatal "未找到 python3"
|
||
|
||
HOST_VER="$(python3 -c 'import frida; print(frida.__version__)' 2>/dev/null || true)"
|
||
if [[ "${HOST_VER}" != "${FRIDA_VERSION}" ]]; then
|
||
warn "主机 frida 版本=${HOST_VER:-(未安装)} ≠ ${FRIDA_VERSION},开始锁定…"
|
||
pip install --quiet "frida==${FRIDA_VERSION}" "frida-tools==12.5.0" || \
|
||
fatal "pip 安装 frida==${FRIDA_VERSION} 失败"
|
||
HOST_VER="$(python3 -c 'import frida; print(frida.__version__)')"
|
||
fi
|
||
log "主机 frida=${HOST_VER}"
|
||
}
|
||
|
||
# ── 步骤 3:检测 Root,部署反检测 frida-server(随机名+随机端口)────────────
|
||
step_setup_frida_server_root() {
|
||
ROOT_RAW="$(adb -s "${DEVICE_SERIAL}" shell 'su -c id' 2>/dev/null || true)"
|
||
if [[ "${ROOT_RAW}" != *"uid=0"* ]]; then
|
||
warn "Root 不可用,将切换到 Gadget 备背模式"
|
||
return 1
|
||
fi
|
||
log "Root 已确认 (${ROOT_RAW//$'\r'/})"
|
||
|
||
LOCAL_BIN_DIR="${SDK_ROOT}/scripts"
|
||
LOCAL_CACHED="${LOCAL_BIN_DIR}/frida-server-${FRIDA_VERSION}-android-${FRIDA_ARCH}"
|
||
REMOTE_BIN="/data/local/tmp/frida-server-16"
|
||
|
||
if [[ ! -f "${LOCAL_CACHED}" ]]; then
|
||
log "下载 frida-server-${FRIDA_VERSION}-${FRIDA_ARCH}…"
|
||
TMP="$(mktemp -d)"
|
||
URL="https://github.com/frida/frida/releases/download/${FRIDA_VERSION}/frida-server-${FRIDA_VERSION}-android-${FRIDA_ARCH}.xz"
|
||
curl -fL "${URL}" -o "${TMP}/fs.xz" || fatal "下载 frida-server 失败:${URL}"
|
||
xz -d -f "${TMP}/fs.xz"
|
||
mv "${TMP}/fs" "${LOCAL_CACHED}"
|
||
chmod 755 "${LOCAL_CACHED}"
|
||
fi
|
||
|
||
if [[ -z "$(adb -s "${DEVICE_SERIAL}" shell "su -c 'ls ${REMOTE_BIN} 2>/dev/null'" | tr -d '\r')" ]]; then
|
||
log "推送 ${LOCAL_CACHED} → ${REMOTE_BIN}"
|
||
adb -s "${DEVICE_SERIAL}" push "${LOCAL_CACHED}" "${REMOTE_BIN}" >/dev/null
|
||
adb -s "${DEVICE_SERIAL}" shell "su -c 'chmod 755 ${REMOTE_BIN}'"
|
||
fi
|
||
|
||
# 反检测:随机二进制名 + 随机监听端口(避免 stock frida-server 被微信 attach 检测)
|
||
if command -v shuf >/dev/null 2>&1; then
|
||
PHANTOM_PORT=$(shuf -i 10000-60000 -n 1)
|
||
else
|
||
PHANTOM_PORT=$((10000 + RANDOM % 50001))
|
||
fi
|
||
PHANTOM_NAME="fs_$(head -c 3 /dev/urandom | xxd -p 2>/dev/null || echo $$)"
|
||
PHANTOM_REMOTE="/data/local/tmp/${PHANTOM_NAME}"
|
||
PHANTOM_CONFIG="${SDK_ROOT}/scripts/anti_detect/phantom_frida_config.json"
|
||
|
||
log "部署反检测 Frida:${PHANTOM_NAME} @ 0.0.0.0:${PHANTOM_PORT}"
|
||
adb -s "${DEVICE_SERIAL}" shell "su -c 'cp ${REMOTE_BIN} ${PHANTOM_REMOTE} && chmod 755 ${PHANTOM_REMOTE}'"
|
||
adb -s "${DEVICE_SERIAL}" shell "su -c 'killall frida-server-16 frida-server 2>/dev/null; pkill -f \"^/data/local/tmp/fs_\" 2>/dev/null; true'"
|
||
sleep 1
|
||
adb -s "${DEVICE_SERIAL}" shell "su -c 'nohup ${PHANTOM_REMOTE} -l 0.0.0.0:${PHANTOM_PORT} >/dev/null 2>&1 &'"
|
||
sleep 2
|
||
PH_PID="$(adb -s "${DEVICE_SERIAL}" shell "su -c 'pidof ${PHANTOM_NAME}'" 2>/dev/null | tr -d '\r')"
|
||
[[ -n "${PH_PID}" ]] || fatal "反检测 frida-server 启动失败 (port=${PHANTOM_PORT})"
|
||
log "反检测 frida-server 已运行 (pid=${PH_PID}, port=${PHANTOM_PORT})"
|
||
|
||
adb -s "${DEVICE_SERIAL}" forward "tcp:${PHANTOM_PORT}" "tcp:${PHANTOM_PORT}" >/dev/null
|
||
mkdir -p "$(dirname "${PHANTOM_CONFIG}")"
|
||
python3 -c "import json; json.dump({'device_serial':'${DEVICE_SERIAL}','listen_port':${PHANTOM_PORT},'binary_name':'${PHANTOM_NAME}','binary_path':'${PHANTOM_REMOTE}'}, open('${PHANTOM_CONFIG}','w'), indent=2)"
|
||
log "phantom 配置已写入 ${PHANTOM_CONFIG}(unified.py 自动读取)"
|
||
|
||
# 确保微信运行(Hook 需要附着)
|
||
WX_PID="$(adb -s "${DEVICE_SERIAL}" shell pidof com.tencent.mm 2>/dev/null | tr -d '\r' || true)"
|
||
if [[ -z "${WX_PID}" ]]; then
|
||
log "拉起微信..."
|
||
adb -s "${DEVICE_SERIAL}" shell am start -n com.tencent.mm/.ui.LauncherUI >/dev/null 2>&1 || true
|
||
sleep 5
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# ── 步骤 4:Gadget 备背(无 Root 时) ─────────────────────────────────────────
|
||
step_setup_gadget_fallback() {
|
||
warn "进入 Gadget 备背模式 — 请确保已用 sdk/scripts/inject_frida_gadget.sh 重打包并安装微信"
|
||
PORT="${GADGET_DEFAULT_PORT}"
|
||
log "adb forward tcp:${PORT} tcp:${PORT}"
|
||
adb -s "${DEVICE_SERIAL}" forward "tcp:${PORT}" "tcp:${PORT}" >/dev/null
|
||
log "Gadget endpoint = 127.0.0.1:${PORT}(FridaManager 选 mode=gadget 时可用)"
|
||
export WORKPHONE_FRIDA_MODE="gadget"
|
||
export WORKPHONE_FRIDA_GADGET_PORT="${PORT}"
|
||
}
|
||
|
||
# ── 步骤 5:启动 SDK 服务(8899)──────────────────────────────────────────────
|
||
step_start_sdk() {
|
||
if curl -sS "${SDK_BASE}/health" >/dev/null 2>&1; then
|
||
log "SDK 已在 ${SDK_BASE} 监听,跳过启动"
|
||
return 0
|
||
fi
|
||
|
||
log "启动 SDK 服务(uvicorn 0.0.0.0:${SDK_PORT})…"
|
||
(
|
||
cd "${SDK_ROOT}/app"
|
||
export SDK_MATRIX_VERIFY=1
|
||
export WECHAT_WS_HOOK_ONLY=1
|
||
export WECHAT_BACKEND_ONLY=1
|
||
nohup python3 -m uvicorn main:app --host 0.0.0.0 --port "${SDK_PORT}" \
|
||
> "${LOG_DIR}/sdk.log" 2>&1 &
|
||
echo $! > "${LOG_DIR}/sdk.pid"
|
||
)
|
||
for i in {1..15}; do
|
||
if curl -sS "${SDK_BASE}/health" >/dev/null 2>&1; then
|
||
log "SDK 已就绪 (pid=$(cat "${LOG_DIR}/sdk.pid"))"
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
done
|
||
fatal "SDK 启动失败,请查看 ${LOG_DIR}/sdk.log"
|
||
}
|
||
|
||
# ── 步骤 6:调用 /hook/probe 让服务端正式承认 Hook 能力 ───────────────────────
|
||
step_hook_probe() {
|
||
log "调用 ${SDK_BASE}/api/v3/hook/probe/${DEVICE_SERIAL}"
|
||
RESP="$(curl -sS "${SDK_BASE}/api/v3/hook/probe/${DEVICE_SERIAL}" || true)"
|
||
echo "${RESP}" | python3 -m json.tool 2>/dev/null || echo "${RESP}"
|
||
|
||
SUPPORTS="$(echo "${RESP}" | python3 -c 'import json,sys
|
||
try:
|
||
d=json.load(sys.stdin); print(d.get("data",d).get("supports_hook",False))
|
||
except Exception:
|
||
print(False)' 2>/dev/null || echo False)"
|
||
if [[ "${SUPPORTS}" == "True" ]]; then
|
||
log "Hook 通道就绪 ✅(服务端已写入 device_modules)"
|
||
else
|
||
warn "Hook 探测未通过(设备能力为 supports_hook=false)"
|
||
warn "可能原因:Root/frida-server 未起、微信未运行、frida 版本错配"
|
||
fi
|
||
}
|
||
|
||
# ── 步骤 7:启动设备端 Agent(USB-reverse + WS)──────────────────────────────
|
||
step_start_agent() {
|
||
if [[ "${NO_AGENT}" == "1" ]]; then
|
||
warn "--no-agent 已指定,跳过 Agent 启动"
|
||
return 0
|
||
fi
|
||
log "adb reverse tcp:${SDK_PORT} tcp:${SDK_PORT}"
|
||
adb -s "${DEVICE_SERIAL}" reverse "tcp:${SDK_PORT}" "tcp:${SDK_PORT}" >/dev/null || true
|
||
|
||
AGENT_DIR="${SDK_ROOT}/agent"
|
||
if [[ ! -f "${AGENT_DIR}/agent.py" ]]; then
|
||
warn "未找到 ${AGENT_DIR}/agent.py,跳过 Agent 启动"
|
||
return 0
|
||
fi
|
||
|
||
log "启动 Agent(device=${DEVICE_SERIAL},WebSocket + Frida remote)…"
|
||
PH_PORT=""
|
||
PH_CFG="${SDK_ROOT}/scripts/anti_detect/phantom_frida_config.json"
|
||
if [[ -f "${PH_CFG}" ]]; then
|
||
PH_PORT=$(python3 -c "import json;print(json.load(open('${PH_CFG}')).get('listen_port',''))" 2>/dev/null || true)
|
||
fi
|
||
|
||
# 避免重复 Agent 抢同一 device_id
|
||
if [[ -f "${LOG_DIR}/agent.pid" ]]; then
|
||
kill "$(cat "${LOG_DIR}/agent.pid" 2>/dev/null)" 2>/dev/null || true
|
||
fi
|
||
pkill -f "agent.py -d ${DEVICE_SERIAL} -s ws://127.0.0.1:${SDK_PORT}" 2>/dev/null || true
|
||
sleep 1
|
||
|
||
# Agent 启动前再次确认微信已运行(Frida attach 依赖)
|
||
WX_PID="$(adb -s "${DEVICE_SERIAL}" shell pidof com.tencent.mm 2>/dev/null | tr -d '\r' || true)"
|
||
if [[ -z "${WX_PID}" ]]; then
|
||
log "拉起微信(Agent 启动前)..."
|
||
adb -s "${DEVICE_SERIAL}" shell am start -n com.tencent.mm/.ui.LauncherUI >/dev/null 2>&1 || true
|
||
sleep 6
|
||
fi
|
||
|
||
(
|
||
cd "${AGENT_DIR}"
|
||
export WP_FRIDA_MODE=remote
|
||
export WP_DEVICE_SERIAL="${DEVICE_SERIAL}"
|
||
if [[ -n "${PH_PORT}" ]]; then
|
||
export WP_FRIDA_PORT="${PH_PORT}"
|
||
adb -s "${DEVICE_SERIAL}" forward "tcp:${PH_PORT}" "tcp:${PH_PORT}" >/dev/null 2>&1 || true
|
||
fi
|
||
nohup python3 agent.py \
|
||
-d "${DEVICE_SERIAL}" \
|
||
-s "ws://127.0.0.1:${SDK_PORT}/ws/device" \
|
||
--heartbeat 10 \
|
||
>> "${LOG_DIR}/agent.log" 2>&1 &
|
||
echo $! > "${LOG_DIR}/agent.pid"
|
||
)
|
||
log "Agent 已启动 (pid=$(cat "${LOG_DIR}/agent.pid"))"
|
||
|
||
# 等待 Frida RPC 就绪,避免 hook/execute 在 lazy attach 阶段超时
|
||
for _i in $(seq 1 30); do
|
||
if grep -q "RPC ping: pong" "${LOG_DIR}/agent.log" 2>/dev/null; then
|
||
log "Frida RPC 已就绪 ✅"
|
||
return 0
|
||
fi
|
||
sleep 2
|
||
done
|
||
warn "30s 内未看到 RPC ping: pong,请检查微信是否运行、phantom 端口转发"
|
||
}
|
||
|
||
# ── 步骤 8:对接清单 ──────────────────────────────────────────────────────────
|
||
print_summary() {
|
||
cat <<EOF
|
||
|
||
================================================================
|
||
Frida 主控一键启动 ✅ 完成
|
||
================================================================
|
||
设备序列号 : ${DEVICE_SERIAL}
|
||
SDK API : ${SDK_BASE}
|
||
API 文档 : ${SDK_BASE}/docs
|
||
控制中心 : ${SDK_BASE}/static/index.html
|
||
Hook 探测 : ${SDK_BASE}/api/v3/hook/probe/${DEVICE_SERIAL}
|
||
Phantom配置 : ${SDK_ROOT}/scripts/anti_detect/phantom_frida_config.json
|
||
Hook 执行 : POST ${SDK_BASE}/api/v3/hook/execute (hook_only=true)
|
||
Hook 取数 : ${SDK_BASE}/api/v3/hook/data/${DEVICE_SERIAL}
|
||
通道选择 : 主 = WebSocket Agent + Frida Hook(WECHAT_WS_HOOK_ONLY,禁止 ADB UI)
|
||
|
||
存客宝侧调用范例(PHP):
|
||
\$sdk = new \\Cunkebao\\WorkPhone\\WorkPhoneClient('${SDK_BASE}', 'TOKEN');
|
||
\$sdk->sendMessage('${DEVICE_SERIAL}', 'wechat', '文件传输助手',
|
||
'[E2E] Frida 主控通道', 'text', null, null, null,
|
||
'hook'); // ← channel=hook,强制走 Frida
|
||
|
||
日志:
|
||
SDK : ${LOG_DIR}/sdk.log
|
||
Agent : ${LOG_DIR}/agent.log
|
||
|
||
停止:
|
||
kill \$(cat ${LOG_DIR}/sdk.pid 2>/dev/null) 2>/dev/null
|
||
kill \$(cat ${LOG_DIR}/agent.pid 2>/dev/null) 2>/dev/null
|
||
================================================================
|
||
EOF
|
||
}
|
||
|
||
# ── 主流程 ────────────────────────────────────────────────────────────────────
|
||
banner
|
||
step_check_adb
|
||
step_lock_host_frida
|
||
|
||
if [[ "${FORCE_GADGET}" == "1" ]]; then
|
||
step_setup_gadget_fallback
|
||
else
|
||
if ! step_setup_frida_server_root; then
|
||
step_setup_gadget_fallback
|
||
fi
|
||
fi
|
||
|
||
if [[ "${PROBE_ONLY}" == "1" ]]; then
|
||
step_start_sdk
|
||
step_hook_probe
|
||
exit 0
|
||
fi
|
||
|
||
step_start_sdk
|
||
step_hook_probe
|
||
step_start_agent
|
||
print_summary
|