228 lines
8.1 KiB
Python
Executable File
228 lines
8.1 KiB
Python
Executable File
#!/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())
|