Files
workphone-sdk/sdk/scripts/frida_workphone_oneclick.sh

465 lines
19 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# ==============================================================================
# frida_workphone_oneclick.sh — 工作手机·Frida 主控一键启动
#
# 目标:插上 Type-C 数据线 → 自动完成"Frida 主、ADB 辅"的全链路启动
# 1) 探测 USB 设备、确认 ADB 授权
# 2) 锁定主机 frida 版本(默认 16.5.6
# 3) 在设备上推送/启动 frida-server-16Root 模式 = 主路径)
# 4) Root 不可用时自动尝试 Frida Gadget 备背端口转发
# 5) 启动 SDK 服务uvicorn 8899
# 6) 通过 /api/v3/hook/probe/{device_id} 探测 Hook 是否就绪
# 7) 启动 AgentWebSocket 通道,带 hook=enabled
# 8) 输出对接清单Type-C 模式下存客宝直接调用 8899
#
# 设计参考:
# - 开发文档/8、部署/设备端Hook安装部署.mdRoot + frida-server 标准)
# - 机擎/references/快速入机部署_Root_Frida_Hook.md10/10 验证基线)
# - sdk/agent/hook/frida_manager.pyusb / 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}"
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 / 主FridaADB)"
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 >/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
}
# ── 步骤 4Gadget 备背(无 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启动设备端 AgentUSB-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 "启动 Agentdevice=${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 连本机 SDKlan.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 <<EOF
================================================================
Frida 主控一键启动 ✅ 完成
================================================================
设备序列号 : ${DEVICE_SERIAL}
SDK API : ${SDK_BASE}
API 文档 : ${SDK_BASE}/docs
控制中心 : ${SDK_BASE}/hub
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 HookWECHAT_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_start_agent
step_hook_probe
print_summary