Files
workphone-sdk/sdk/scripts/build_wechat_java_bridge_iife.py

228 lines
8.1 KiB
Python
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 python3
"""用锁定Java Bridge工具链构建微信Hook IIFE。
与历史已通过真机的入口保持一致显式导入frida-java-bridge赋值
globalThis.Java再拼接唯一Hook真源。生成后必须通过Bridge指纹和启动探针门禁。
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import subprocess
import tempfile
from datetime import datetime, timedelta, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "sdk/agent/hook/wechat_hook_v2.js"
OUTPUT = ROOT / "sdk/android-app/app/src/main/assets/wechat_hook_v2.with_java.iife.js"
MANIFEST = ROOT / "sdk/android-app/app/src/main/assets/wechat_hook_v2.with_java.iife.manifest.json"
COMPILE_VERSION = "19.0.5"
BRIDGE_VERSION = "7.0.13"
PROBE_MARKER = "wp_java_bridge_startup_probe_v1"
ENTRY_PREFIX = f'''import JavaBridge from "frida-java-bridge";
globalThis.Java = JavaBridge;
(function () {{
const probe = {{
marker: "{PROBE_MARKER}",
event: "wp_java_bridge_startup_probe",
java_available: globalThis.Java.available === true,
java_perform_callable: typeof globalThis.Java.perform === "function",
java_perform_entered: false,
status: "checking"
}};
globalThis.__wpJavaBridgeRuntimeProbe = probe;
function emitProbe(success, error) {{
send({{
event: "wp_java_bridge_startup_probe",
success: success === true,
marker: probe.marker,
java_available: probe.java_available,
java_perform_callable: probe.java_perform_callable,
java_perform_entered: probe.java_perform_entered,
status: probe.status,
error: String(error || ""),
process: Process.name,
pid: Process.id,
channel: "frida_rpc"
}});
}}
if (!probe.java_available || !probe.java_perform_callable) {{
probe.status = "java_bridge_runtime_unavailable";
emitProbe(false, probe.status);
throw new Error(probe.status);
}}
try {{
globalThis.Java.perform(function () {{
probe.java_perform_entered = true;
probe.status = "java_bridge_runtime_ready";
emitProbe(true, "");
}});
}} catch (error) {{
probe.status = "java_bridge_perform_failed";
emitProbe(false, error);
throw error;
}}
}})();
'''
REQUIRED_IIFE_TOKENS = (
"globalThis.Java=",
"Java API not available",
"wp_java_bridge_startup_probe_v1",
"wp_java_bridge_startup_probe",
"java_available",
"java_perform_callable",
"java_perform_entered",
"java_bridge_runtime_ready",
)
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _package_version(path: Path) -> str:
try:
return json.loads(path.read_text(encoding="utf-8"))["version"]
except Exception:
return ""
def find_locked_toolchain() -> tuple[Path, Path]:
candidates: list[Path] = []
configured = os.environ.get("WP_FRIDA_NPM_ROOT")
if configured:
candidates.append(Path(configured).expanduser())
candidates.extend(sorted((Path.home() / ".npm/_npx").glob("*")))
for base in candidates:
compile_package = base / "node_modules/frida-compile/package.json"
bridge_package = base / "node_modules/frida-java-bridge/package.json"
compiler = base / "node_modules/.bin/frida-compile"
if (
compiler.exists()
and _package_version(compile_package) == COMPILE_VERSION
and _package_version(bridge_package) == BRIDGE_VERSION
):
return base, compiler
raise RuntimeError(
"locked_frida_toolchain_missing: "
f"frida-compile={COMPILE_VERSION},frida-java-bridge={BRIDGE_VERSION}"
)
def build_entry(hook_source: str) -> str:
return ENTRY_PREFIX + "\n" + hook_source
def verify_embedded_bridge(path: Path) -> list[str]:
text = path.read_text(encoding="utf-8", errors="replace")
return [token for token in REQUIRED_IIFE_TOKENS if token not in text]
def build(write: bool) -> dict:
npm_root, compiler = find_locked_toolchain()
entry = build_entry(SOURCE.read_text(encoding="utf-8"))
with tempfile.TemporaryDirectory(prefix="wp-java-bridge-iife-", dir=npm_root) as temp:
temp_dir = Path(temp)
entry_path = temp_dir / "entry.js"
candidate = temp_dir / "wechat_hook_v2.with_java.iife.js"
entry_path.write_text(entry, encoding="utf-8")
command = [
str(compiler), str(entry_path), "-o", str(candidate),
"-B", "iife", "-S", "-T", "none", "-c",
]
completed = subprocess.run(
command, cwd=npm_root, text=True, capture_output=True, check=False
)
if completed.returncode != 0:
raise RuntimeError(
"frida_compile_failed:" + (completed.stderr or completed.stdout)
)
syntax = subprocess.run(
["node", "--check", str(candidate)],
text=True, capture_output=True, check=False,
)
if syntax.returncode != 0:
raise RuntimeError("iife_node_check_failed:" + syntax.stderr)
missing = verify_embedded_bridge(candidate)
if missing:
raise RuntimeError("java_bridge_tokens_missing:" + ",".join(missing))
candidate_bytes = candidate.read_bytes()
if write:
OUTPUT.write_bytes(candidate_bytes)
result = {
"task_id": "WP-CORE71-66-68-JAVA-BRIDGE-IIFE-FIX",
"write": write,
"source": str(SOURCE.relative_to(ROOT)),
"output": str(OUTPUT.relative_to(ROOT)),
"source_sha256": sha256(SOURCE),
"iife_sha256": hashlib.sha256(candidate_bytes).hexdigest(),
"source_bytes": SOURCE.stat().st_size,
"iife_bytes": len(candidate_bytes),
"builder": f"frida-compile@{COMPILE_VERSION} + frida-java-bridge@{BRIDGE_VERSION}",
"compiler": str(compiler),
"command": "frida-compile <entry.js> -o <output> -B iife -S -T none -c",
"entry_prelude_sha256": hashlib.sha256(ENTRY_PREFIX.encode()).hexdigest(),
"java_bridge_embedded": True,
"runtime_probe": {
"marker": PROBE_MARKER,
"checks": ["Java.available", "Java.perform", "perform_callback_entered"],
"event": "wp_java_bridge_startup_probe",
"required": True,
},
"node_check": "pass",
"missing_bridge_tokens": [],
}
if write:
manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) if MANIFEST.exists() else {}
primary_task_id = manifest.get("task_id") or result["task_id"]
merged = list(manifest.get("merged_task_ids", []))
if result["task_id"] not in merged:
merged.append(result["task_id"])
manifest.update({key: value for key, value in result.items() if key != "task_id"})
manifest.update({
"task_id": primary_task_id,
"build_task_id": result["task_id"],
"release_task_id": result["task_id"],
"merged_task_ids": merged,
"frida_compile_version": COMPILE_VERSION,
"frida_java_bridge_version": BRIDGE_VERSION,
"bundle_format": "iife",
"source_maps": False,
"source_map": False,
"type_check": False,
"compress": True,
"minify": True,
"built_at": datetime.now(timezone(timedelta(hours=8))).isoformat(timespec="seconds"),
"capability_status": "java_bridge_embedded_runtime_probe_offline_pass",
"device_calls": 0,
"deployments": 0,
})
MANIFEST.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
return result
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--write", action="store_true", help="写入IIFE和manifest")
parser.add_argument("--receipt", type=Path, help="保存构建回执")
args = parser.parse_args()
result = build(args.write)
payload = json.dumps(result, ensure_ascii=False, indent=2) + "\n"
if args.receipt:
args.receipt.parent.mkdir(parents=True, exist_ok=True)
args.receipt.write_text(payload, encoding="utf-8")
print(payload, end="")
return 0
if __name__ == "__main__":
raise SystemExit(main())