容器内手动就可以
docker exec -it ks_sig3 bash -lc 'frida -H 192.168.50.66:27042 -f com.kuaishou.nebula -l /app/frida/ks_sig_rpc_full.js'
自动就失败
这是代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# server.py — KS 本地签名服务(Frida Python API 版:attach 优先 + 进程探测 + 实时签名)
#
# 运行前准备:
# pip install fastapi uvicorn frida==17.* frida-tools==12.*
# 确保设备 frida-server 与 frida 主版本一致,且可通过 TCP 访问(FRIDA_HOST:FRIDA_PORT)
#
# 关键环境变量(可选,均有默认值):
# FRIDA_HOST=192.168.50.66
# FRIDA_PORT=27042
# FRIDA_APP_ID=com.kuaishou.nebula
# FRIDA_PROCESS=com.kuaishou.nebula # 精确进程名(优先 attach 到它)
# FRIDA_JS=/app/frida/ks_sig_rpc_full.js
# KS_INJECT_MODE=attach # attach|spawn,推荐 attach
# SIG_TIMEOUT=5 # /sig3 现算超时秒
# LOG_LEVEL=INFO # DEBUG|INFO|WARNING|ERROR
# VERBOSE_FRIDA=false
#
# 路由:
# POST/GET /sig3 (推荐,用于签名三件套)
# POST/GET /sig (同上)
# POST/GET /compute_signature (别名)
# POST /inject (手动注入,可选,启动时也会自动注入)
# GET /status
# GET /logs?count=50
# POST /logs/level { "level": "DEBUG" }
import os, time, json, threading, asyncio
from typing import Optional, Dict, Any, List
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
import uvicorn
from dataclasses import dataclass
from datetime import datetime
from collections import deque
# ====== Frida Python API ======
import frida
app = FastAPI(title="ks-sig3 (Frida API Attach-First)")
# ---------- LOGGING ----------
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
VERBOSE_FRIDA = os.getenv("VERBOSE_FRIDA", "false").lower() == "true"
MAX_LOG_HISTORY = int(os.getenv("MAX_LOG_HISTORY", "200"))
LEVELS = {"DEBUG": 0, "INFO": 1, "SUCCESS": 1, "WARNING": 2, "ERROR": 3}
current_level = LEVELS.get(LOG_LEVEL, 1)
class LogMgr:
def __init__(self):
self.buf = deque(maxlen=MAX_LOG_HISTORY)
self.last = {}
self.suppressed = 0
def _ok(self, lvl, msg, thr):
if LEVELS.get(lvl, 1) < current_level:
return False
if thr > 0:
k = f"{lvl}:{hash(msg)}"
now = time.time()
if k in self.last and (now - self.last[k]) < thr:
self.suppressed += 1
return False
self.last[k] = now
return True
def log(self, *args, level="INFO", throttle=0):
msg = " ".join(str(a) for a in args)
if not self._ok(level, msg, throttle): return
ts = datetime.now().strftime("%H:%M:%S")
color = {"DEBUG":"\033[37m","INFO":"\033[94m","SUCCESS":"\033[92m","WARNING":"\033[93m","ERROR":"\033[91m"}.get(level,"\033[94m")
if level in ("ERROR","WARNING") or LOG_LEVEL=="DEBUG":
print(f"{color}[{ts}] {level}: {msg}\033[0m", flush=True)
elif level=="SUCCESS" and current_level<=1:
print(f"{color}[{ts}] {level}: {msg}\033[0m", flush=True)
elif level=="INFO" and current_level==0:
print(f"{color}[{ts}] {level}: {msg}\033[0m", flush=True)
self.buf.append({"t": time.time(), "level": level, "msg": msg})
def recent(self, n=50):
return list(self.buf)[-n:]
log = LogMgr().log
get_logs = LogMgr().recent # not used; we keep one instance below
logmgr = LogMgr()
def LOG(*args, **kw): logmgr.log(*args, **kw)
def RECENT(n=50): return logmgr.recent(n)
# ---------- ENV ----------
FRIDA_HOST = os.getenv("FRIDA_HOST", "127.0.0.1")
FRIDA_PORT = int(os.getenv("FRIDA_PORT", "27042"))
FRIDA_APP_ID = os.getenv("FRIDA_APP_ID", "com.kuaishou.nebula")
FRIDA_PROCESS = os.getenv("FRIDA_PROCESS", "").strip() # 优先 attach 到它
FRIDA_JS = os.getenv("FRIDA_JS", "/app/frida/ks_sig_rpc_full.js")
KS_INJECT_MODE = os.getenv("KS_INJECT_MODE", "attach").lower() # attach|spawn
DEFAULT_TIMEOUT = float(os.getenv("SIG_TIMEOUT", "5"))
# ---------- STATE ----------
@dataclass
class SignatureData:
sig: Optional[str] = None
sig3: Optional[str] = None
nssig: Optional[str] = None
xfalcon: Optional[str] = None
ts: float = 0.0
source: str = "empty"
@dataclass
class SysStats:
requests_total: int = 0
signatures_generated: int = 0
errors: int = 0
last_activity: float = 0.0
class State:
def __init__(self):
self.ready = False
self.java_ready = False
self.hooks_installed = False
self.start = time.time()
self.sign = SignatureData()
self.stats = SysStats()
self.lock = threading.Lock()
def set_sig(self, k, v, src="rpc"):
with self.lock:
if k == "sig": self.sign.sig = v
elif k == "__NS_sig3": self.sign.sig3 = v
elif k == "__NStokensig": self.sign.nssig = v
elif k == "__NS_xfalcon": self.sign.xfalcon = v
self.sign.ts = time.time()
self.sign.source = src
self.stats.signatures_generated += 1
self.stats.last_activity = self.sign.ts
def snapshot(self):
with self.lock:
age = time.time()-self.sign.ts if self.sign.ts else None
return {
"Sig": self.sign.sig or "",
"Sig3": self.sign.sig3 or "",
"NsSig": self.sign.nssig or "",
"xfalcon": self.sign.xfalcon or "",
"timestamp": self.sign.ts,
"age": age,
"source": self.sign.source
}
STATE = State()
# ---------- FRIDA HANDLES ----------
G = {"device":None, "session":None, "script":None, "pid":None, "lock": threading.Lock()}
def _on_frida_message(message, data):
try:
typ = message.get("type")
if typ == "send":
payload = message.get("payload")
if isinstance(payload, dict):
if payload.get("type") == "ks-hooks-installed":
STATE.hooks_installed = True
LOG("Hook 安装成功", level="SUCCESS")
elif payload.get("type") in ("rpc_exports","script_loaded"):
# JS 侧脚本就绪
pass
else:
if VERBOSE_FRIDA or LOG_LEVEL=="DEBUG":
LOG(f"[FRIDA] {payload}", level="DEBUG")
else:
s = str(payload)
if "Java.available true" in s:
STATE.java_ready = True
LOG("Java 就绪", level="SUCCESS")
elif "MessageDigest" in s or "atlasSign" in s or "doCommandNative" in s:
if VERBOSE_FRIDA or LOG_LEVEL=="DEBUG":
LOG(s, level="DEBUG", throttle=1)
elif typ == "error":
LOG(f"[FRIDA-ERR] {message}", level="ERROR")
else:
if LOG_LEVEL=="DEBUG":
LOG(f"[FRIDA] {message}", level="DEBUG")
except Exception as e:
LOG(f"[FRIDA-MSG-HANDLER] {e}", level="ERROR")
def _pick_target_pid(dev, pkg: str, hint: str, timeout=15.0):
"""在超时时间内不断枚举进程,优先:
1) 精确 FRIDA_PROCESS
2) 包名同名进程
3) 包名前缀的子进程(:ads/:web/:mini 等)"""
t0 = time.time()
while time.time() - t0 < timeout:
try:
procs = dev.enumerate_processes()
except Exception:
time.sleep(0.2); continue
# 1) 精确 hint
if hint:
for p in procs:
if p.name == hint or getattr(p, "identifier", "") == hint:
return p.pid
# 2) 同名(主进程)
for p in procs:
if p.name == pkg or getattr(p, "identifier", "") == pkg:
return p.pid
# 3) 子进程,按常见优先序靠前
cands = [p for p in procs if isinstance(p.name, str) and p.name.startswith(pkg + ":")]
prio = ("ads","ad","mini","web","nebula","main","push")
def rank(n: str):
if ":" not in n: return 999
suf = n.split(":",1)[1]
for i,k in enumerate(prio):
if k in suf: return i
return 500
cands.sort(key=lambda x: rank(x.name))
if cands:
return cands[0].pid
time.sleep(0.3)
return None
def inject_via_frida() -> bool:
"""
自动探测 Java 进程并注入:
1) 若设置了 FRIDA_PROCESS:优先尝试该进程;
2) 否则遍历所有 com.kuaishou.nebula* 进程,逐个 attach + 加载“探针脚本”检测 Java.available;
3) 找到 Java 可用的进程后,卸载探针脚本,在同一 session 里加载你的 ks_sig_rpc_full.js;
4) waitjava/status 置位 ready。
"""
with G["lock"]:
if G["script"] is not None:
return True
host, port, pkg, script_path = FRIDA_HOST, FRIDA_PORT, FRIDA_APP_ID, FRIDA_JS
dev = frida.get_device_manager().add_remote_device(f"{host}:{port}")
# 1) 枚举候选进程
try:
procs = dev.enumerate_processes()
except Exception as e:
LOG(f"[FRIDA] enumerate_processes failed: {e}", level="ERROR")
return False
# 候选优先级:FRIDA_PROCESS(如果提供)> 主进程(同名)> 子进程(:ads/:mini/:web…)
candidates = []
if FRIDA_PROCESS:
for p in procs:
if p.name == FRIDA_PROCESS or getattr(p, "identifier", "") == FRIDA_PROCESS:
candidates.append(p)
break
else:
for p in procs:
if p.name == pkg or getattr(p, "identifier", "") == pkg:
candidates.append(p)
break
# 同名没找到或还需兜底:把所有子进程也加进来
subs = [p for p in procs if isinstance(p.name, str) and p.name.startswith(pkg + ":")]
# 常见优先序
order = ("ads", "ad", "mini", "web", "nebula", "main", "push")
def rank(n: str):
if ":" not in n: return 999
suf = n.split(":", 1)[1]
for i, key in enumerate(order):
if key in suf: return i
return 500
subs.sort(key=lambda x: rank(x.name))
candidates.extend(subs)
if not candidates:
LOG(f"[FRIDA] 未发现 {pkg} 相关进程。请先在手机上打开 App,再调用 /inject。", level="WARNING")
return False
# 2) 探针脚本(极简,快速判断 Java.available)
probe_source = r"""
rpc.exports = {
probe: function () {
return Java.available;
}
};
"""
chosen = None
session = None
for p in candidates:
try:
sess = dev.attach(p.pid)
except Exception as e:
# 有些进程会 attach 失败,跳过
continue
try:
probe = sess.create_script(probe_source)
ok_holder = {"ok": False}
def _on_msg(m, d):
pass
probe.on("message", _on_msg)
probe.load()
# 调 probe
try:
available = probe.exports.probe()
except Exception:
available = False
probe.unload()
if available:
chosen = p
session = sess
break
else:
# 不是 Java 进程,分离
try:
sess.detach()
except Exception:
pass
except Exception:
try:
sess.detach()
except Exception:
pass
if not chosen or session is None:
LOG("[FRIDA] 没有任何候选进程的 Java 可用。请确认 App 在前台/对应页面。", level="WARNING")
return False
# 3) 在同一 session 里加载你的主脚本
with open(script_path, "r", encoding="utf-8") as f:
source = f.read()
script = session.create_script(source)
script.on("message", _on_frida_message)
script.load()
# 4) 等待 Java / hooks
try:
if hasattr(script.exports, "waitjava"):
script.exports.waitjava(); STATE.java_ready = True
except Exception as e:
LOG(f"[FRIDA] waitjava error: {e}", level="WARNING")
try:
if hasattr(script.exports, "status"):
t0 = time.time()
while time.time() - t0 < 10.0:
st = {}
try:
st = script.exports.status()
except Exception:
pass
if st.get("java"): STATE.java_ready = True
if st.get("hooks"): STATE.hooks_installed = True
if STATE.java_ready and STATE.hooks_installed:
break
time.sleep(0.2)
except Exception as e:
LOG(f"[FRIDA] status polling error: {e}", level="WARNING")
G.update({"device": dev, "session": session, "script": script, "pid": chosen.pid})
STATE.ready = bool(STATE.java_ready and STATE.hooks_installed)
LOG(f"[FRIDA] attach 到 Java 进程成功:{chosen.name} (pid={chosen.pid}) ready={STATE.ready}", level="SUCCESS")
return STATE.ready
def compute_via_frida(path: str, query: str, post: str, salt: str) -> Dict[str,str]:
"""直接调用 JS 导出的 computesig,统一字段名返回"""
if G["script"] is None:
raise RuntimeError("frida not injected")
res = G["script"].exports.computesig(path, query, post, salt)
if not isinstance(res, dict): res = {}
out = {
"Sig": res.get("Sig") or res.get("sig") or "",
"Sig3": res.get("Sig3") or res.get("__NS_sig3") or res.get("sig3") or "",
"NsSig": res.get("NsSig") or res.get("__NStokensig") or res.get("tokensig") or "",
}
if out["Sig"]: STATE.set_sig("sig", out["Sig"], src="rpc")
if out["Sig3"]: STATE.set_sig("__NS_sig3", out["Sig3"], src="rpc")
if out["NsSig"]: STATE.set_sig("__NStokensig", out["NsSig"], src="rpc")
return out
# ---------- HELPERS ----------
async def parse_params(request: Request) -> Dict[str,Any]:
qp = request.query_params
path = qp.get("path","")
query= qp.get("query","")
post = qp.get("post","")
salt = qp.get("salt","")
timeout = float(qp.get("timeout", DEFAULT_TIMEOUT) or DEFAULT_TIMEOUT)
if request.method == "POST":
ctype = (request.headers.get("content-type") or "").lower()
raw = (await request.body()).decode("utf-8", errors="ignore")
data = {}
if "application/json" in ctype:
try: data = json.loads(raw or "{}")
except: data = {}
elif "application/x-www-form-urlencoded" in ctype or "multipart/form-data" in ctype:
try:
form = await request.form()
data = dict(form)
except:
data = {}
path = data.get("path", path) or ""
query= data.get("query", query) or ""
post = data.get("post", post) or ""
salt = data.get("salt", salt) or ""
try:
timeout = float(data.get("timeout", timeout) or timeout)
except: pass
return {"path":str(path), "query":str(query), "post":str(post), "salt":str(salt), "timeout":float(timeout)}
# ---------- ROUTES ----------
@app.get("/")
async def root():
return JSONResponse({
"service":"ks-sig3 (Frida API Attach-First)",
"env":{"FRIDA_HOST":FRIDA_HOST,"FRIDA_PORT":FRIDA_PORT,"APP_ID":FRIDA_APP_ID,"MODE":KS_INJECT_MODE},
"endpoints":["/sig","/sig3","/compute_signature","/inject","/status","/logs","/logs/level"]
})
@app.post("/inject")
async def inject():
ok = inject_via_frida()
return JSONResponse({"ok": ok, "pid": G.get("pid"), "ready": STATE.ready})
@app.get("/status")
async def status():
snap = STATE.snapshot()
return JSONResponse({
"system":{
"frida_running": G["session"] is not None,
"java_ready": STATE.java_ready,
"hooks_installed": STATE.hooks_installed,
"ready": STATE.ready,
"uptime": time.time()-STATE.start
},
"signatures":{
"count": int(bool(snap["Sig"]))+int(bool(snap["Sig3"]))+int(bool(snap["NsSig"])),
"has_valid": bool(snap["Sig"] or snap["Sig3"] or snap["NsSig"]),
"age": snap["age"],
"sample": (snap["Sig"] or snap["Sig3"] or snap["NsSig"] or "")[:16]
},
"statistics":{
"requests_total": STATE.stats.requests_total,
"signatures_generated": STATE.stats.signatures_generated,
"errors": STATE.stats.errors
}
})
@app.get("/logs")
async def logs(count: int = 50):
return JSONResponse({
"logs": RECENT(count),
"suppressed": logmgr.suppressed,
"level": LOG_LEVEL
})
@app.post("/logs/level")
async def set_level(request: Request):
global current_level
body = (await request.body()).decode("utf-8", errors="ignore")
try:
lvl = json.loads(body or "{}").get("level","INFO").upper()
except:
lvl = "INFO"
if lvl in LEVELS:
current_level = LEVELS[lvl]
LOG(f"日志级别已调整为: {lvl}", level="INFO")
return JSONResponse({"ok": True, "level": lvl})
return JSONResponse({"ok": False, "error": "invalid level"}, status_code=400)
@app.api_route("/sig", methods=["GET","POST"])
async def sig(request: Request):
return await _compute(request, "sig")
@app.api_route("/sig3", methods=["GET","POST"])
async def sig3(request: Request):
return await _compute(request, "sig3")
app.add_api_route("/compute_signature", sig3, methods=["GET","POST"])
async def _compute(request: Request, name: str):
STATE.stats.requests_total += 1
try:
p = await parse_params(request)
path, query, post, salt, timeout = p["path"], p["query"], p["post"], p["salt"], p["timeout"]
if LOG_LEVEL=="DEBUG":
LOG(f"/{name}: path={path[:64]} qlen={len(query)} plen={len(post)}", level="DEBUG")
if not STATE.ready or G["script"] is None:
return JSONResponse({
"Sig":"","Sig3":"","NsSig":"",
"error":"system_not_ready",
"java_ready": STATE.java_ready,
"hooks_installed": STATE.hooks_installed
}, status_code=503)
async def run():
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, compute_via_frida, path, query, post, salt)
try:
res = await asyncio.wait_for(run(), timeout=max(0.5, float(timeout)))
except asyncio.TimeoutError:
STATE.stats.errors += 1
return JSONResponse({"Sig":"","Sig3":"","NsSig":"","error":"compute_timeout"}, status_code=504)
if not (res.get("Sig") or res.get("Sig3") or res.get("NsSig")):
STATE.stats.errors += 1
return JSONResponse({"Sig":"","Sig3":"","NsSig":"","error":"no_signature_fresh"}, status_code=502)
LOG(f"/{name} 成功: Sig?{bool(res['Sig'])} Sig3?{bool(res['Sig3'])} NsSig?{bool(res['NsSig'])}", level="SUCCESS", throttle=2)
return JSONResponse({"Sig":res["Sig"], "Sig3":res["Sig3"], "NsSig":res["NsSig"]})
except Exception as e:
STATE.stats.errors += 1
LOG(f"/{name} 异常: {e}", level="ERROR")
return JSONResponse({"Sig":"","Sig3":"","NsSig":"","error":"internal_error","detail":str(e)}, status_code=500)
# ---------- STARTUP/SHUTDOWN ----------
@app.on_event("startup")
async def on_startup():
LOG("ks-sig3 服务启动", level="INFO")
LOG(f"模式: {KS_INJECT_MODE}, 目标: {FRIDA_APP_ID}, 进程提示: {FRIDA_PROCESS or 'N/A'}", level="INFO")
def auto():
time.sleep(1.5)
ok = inject_via_frida()
if ok: LOG("自动注入完成", level="SUCCESS")
else: LOG("自动注入失败(可尝试:先打开App前台 + KS_INJECT_MODE=attach + /inject)", level="WARNING")
threading.Thread(target=auto, daemon=True).start()
@app.on_event("shutdown")
async def on_shutdown():
LOG("服务关闭中...", level="INFO")
try:
if G["script"]: G["script"].unload()
except Exception: pass
try:
if G["session"]: G["session"].detach()
except Exception: pass
G.update({"device":None,"session":None,"script":None,"pid":None})
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8088, log_level="warning")
最新发布