#!/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 # 指定 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}" export ANDROID_ADB_SERVER_PORT="${ANDROID_ADB_SERVER_PORT:-5038}" LOG_DIR="${SDK_ROOT}/logs" mkdir -p "${LOG_DIR}" DEVICE_SERIAL="" FORCE_GADGET=0 NO_AGENT=0 PROBE_ONLY=0 # 所有 ADB 调用固定到同一个 server 端口,避免 SDK/CLI/脚本各看一个设备列表。 adb() { command adb -P "${ANDROID_ADB_SERVER_PORT}" "$@" } # ── 参数解析 ────────────────────────────────────────────────────────────────── 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) port=${ANDROID_ADB_SERVER_PORT}" # 启动 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 # 用 setsid 彻底脱离 adb shell 会话,避免 shell 退出时 frida-server 随之被 SIGHUP 杀掉 # (本会话踩坑:nohup & 经 adb shell 启动仍随会话结束被杀 → 后续 attach connection closed) adb -s "${DEVICE_SERIAL}" shell "su -c 'setsid ${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 env ANDROID_ADB_SERVER_PORT="${ANDROID_ADB_SERVER_PORT:-5038}" \ 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="" SUPPORTS="False" for _i in $(seq 1 20); do RESP="$(curl -sS "${SDK_BASE}/api/v3/hook/probe/${DEVICE_SERIAL}" || true)" 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 break fi sleep 2 done echo "${RESP}" | python3 -m json.tool 2>/dev/null || echo "${RESP}" if [[ "${SUPPORTS}" == "True" ]]; then log "Hook 通道就绪 ✅(服务端已写入 device_modules)" else warn "Hook 探测未通过(设备能力为 supports_hook=false)" warn "可能原因:Root/frida-server 未起、微信未运行、frida 版本错配" return 1 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" AGENT_LOG="${LOG_DIR}/agent.log" LAN_ENV="${SDK_ROOT}/config/lan.env" [[ -f "${LAN_ENV}" ]] && source "${LAN_ENV}" || true # Mac 侧 Agent 连本机 SDK;lan.env 的 WORKPHONE_LAN_IP 供手机 Termux 寻服 WS_HOST="127.0.0.1" 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 : > "${AGENT_LOG}" { echo "=== $(date '+%Y-%m-%d %H:%M:%S') oneclick agent start ===" echo "device=${DEVICE_SERIAL} sdk=${SDK_BASE} frida_port=${PH_PORT:-auto}" } >> "${AGENT_LOG}" AGENT_PID="$( cd "${AGENT_DIR}" export WP_FRIDA_MODE=remote export WP_DEVICE_SERIAL="${DEVICE_SERIAL}" export WORKPHONE_AGENT_LOG="${AGENT_LOG}" export WORKPHONE_SDK_PORT="${SDK_PORT}" export WORKPHONE_WS_HOST="${WS_HOST}" 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 python3 - <<'PY' import json import os import subprocess import sys log_path = os.environ["WORKPHONE_AGENT_LOG"] device_serial = os.environ["WP_DEVICE_SERIAL"] sdk_port = os.environ["WORKPHONE_SDK_PORT"] sdk_root = os.path.abspath(os.path.join(os.getcwd(), "..")) build = os.path.join(sdk_root, "scripts", "build_agent_config.py") public_servers = [] try: cfg = json.loads( subprocess.check_output( [sys.executable, build, "--device-id", device_serial], text=True, ) ) public_servers = [s for s in cfg.get("public_servers") or [] if s] except Exception: pass # Mac 本机 Agent 优先连本地 SDK;断连时按 BIND-03 公网/局域网候选回退 ws_host = os.environ.get("WORKPHONE_WS_HOST", "127.0.0.1") if ws_host in ("127.0.0.1", "localhost"): server_url = f"ws://127.0.0.1:{sdk_port}/ws/device" else: server_url = f"ws://{ws_host}:{sdk_port}/ws/device" cmd = [ sys.executable, "agent.py", "-d", device_serial, "-s", server_url, "--heartbeat", "10", ] if public_servers: cmd.extend(["--public-servers", ",".join(public_servers)]) with open(log_path, "ab", buffering=0) as log: proc = subprocess.Popen( cmd, cwd=os.getcwd(), env=os.environ.copy(), stdin=subprocess.DEVNULL, stdout=log, stderr=subprocess.STDOUT, start_new_session=True, close_fds=True, ) print(proc.pid) PY )" echo "${AGENT_PID}" > "${LOG_DIR}/agent.pid" log "Agent 已启动 (pid=${AGENT_PID})" # 等待 Frida RPC 与 WebSocket 注册同时就绪,避免 hook/execute 在 lazy attach 阶段超时 for _i in $(seq 1 45); do if ! kill -0 "${AGENT_PID}" 2>/dev/null; then tail -80 "${AGENT_LOG}" >&2 || true fatal "Agent 启动后退出,请查看 ${AGENT_LOG}" fi if grep -q "RPC ping: pong" "${AGENT_LOG}" 2>/dev/null && \ grep -q "服务器确认注册" "${AGENT_LOG}" 2>/dev/null; then log "Frida RPC + WebSocket 注册已就绪 ✅" return 0 fi sleep 2 done tail -80 "${AGENT_LOG}" >&2 || true fatal "90s 内未看到 RPC ping 或服务器确认注册,请检查微信、phantom 端口转发与 SDK WebSocket" } # ── 步骤 8:对接清单 ────────────────────────────────────────────────────────── print_summary() { cat <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_start_agent step_hook_probe print_summary