"""静态整理 ``passport_account_image`` / ``WeaponHI.dd(21)`` 链路证据。 这个工具只读 apktool 反编译目录,输出机器可读 JSON。目标不是替代动态 抓证,而是把当前本地 APK 中能静态证明的链路固定下来,避免后续继续靠记忆 判断: ``/f/a/p`` 上传态 ``VIMG_`` → 服务端/配置回写 ``wcfg`` → ``WeaponHI.dd(21)`` 读取 ``wcfg["a_y_q_z"]`` → 账号登录请求 body 注入 ``passport_account_image``。 """ from __future__ import annotations import argparse import base64 import json import re import sys from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] DEFAULT_APKTOOL_DIR = ROOT / "out" / "agent_apk_triage" / "apktool" RELATIVE_FILES = { "weaponhi": Path("smali_classes15/com/kuaishou/weapon/i/WeaponHI.smali"), "gzip": Path("smali_classes15/com/kuaishou/weapon/ks/i0.smali"), "z0": Path("smali_classes15/com/kuaishou/weapon/ks/z0.smali"), "x0": Path("smali_classes15/com/kuaishou/weapon/ks/x0.smali"), "r1": Path("smali_classes15/com/kuaishou/weapon/ks/r1.smali"), "i": Path("smali_classes15/com/kuaishou/weapon/ks/i.smali"), "y0": Path("smali_classes15/com/kuaishou/weapon/ks/y0.smali"), "zvl_c": Path("smali_classes8/zvl/c.smali"), } def _read_lines(path: Path) -> list[str]: return path.read_text(encoding="utf-8", errors="ignore").splitlines() def _rel(path: Path, base: Path) -> str: try: return path.relative_to(base).as_posix() except ValueError: return path.as_posix() def _line_refs(path: Path, base: Path, marker: str, *, limit: int = 20) -> list[dict[str, Any]]: if not path.exists(): return [] refs: list[dict[str, Any]] = [] for line_no, line in enumerate(_read_lines(path), 1): if marker in line: refs.append( { "file": _rel(path, base), "line": line_no, "text": line.strip(), } ) if len(refs) >= limit: break return refs def _first_line(path: Path, base: Path, marker: str) -> dict[str, Any]: refs = _line_refs(path, base, marker, limit=1) return refs[0] if refs else {} def _method_bounds(lines: list[str], method_marker: str) -> tuple[int, int] | None: start = -1 for index, line in enumerate(lines): if method_marker in line: start = index break if start < 0: return None end = len(lines) for index in range(start + 1, len(lines)): if lines[index].strip() == ".end method": end = index + 1 break return start, end def _first_line_in_method(path: Path, base: Path, method_marker: str, marker: str) -> dict[str, Any]: if not path.exists(): return {} lines = _read_lines(path) bounds = _method_bounds(lines, method_marker) if bounds is None: return {} start, end = bounds for index in range(start, end): if marker in lines[index]: return { "file": _rel(path, base), "line": index + 1, "text": lines[index].strip(), } return {} def _exists(path: Path) -> bool: return path.exists() and path.is_file() def _extract_field_string(path: Path, field_name: str) -> str: if not path.exists(): return "" pattern = re.compile(rf"\.field\s+public\s+static\s+{re.escape(field_name)}:.*?=\s+\"([^\"]*)\"") for line in _read_lines(path): match = pattern.search(line) if match: return match.group(1) return "" def _decode_base64_text(value: str) -> str: if not value: return "" try: return base64.b64decode(value).decode("utf-8") except Exception: # noqa: BLE001 - 诊断工具保留空值即可 return "" def _grep_tree(root: Path, marker: str, *, subdir: str = "smali_classes15/com/kuaishou/weapon", limit: int = 80) -> list[dict[str, Any]]: start = root / subdir if not start.exists(): return [] refs: list[dict[str, Any]] = [] for path in start.rglob("*.smali"): try: lines = _read_lines(path) except OSError: continue for line_no, line in enumerate(lines, 1): if marker in line: refs.append({"file": _rel(path, root), "line": line_no, "text": line.strip()}) if len(refs) >= limit: return refs return refs def _file_map(apktool_dir: Path) -> dict[str, Path]: return {name: apktool_dir / rel for name, rel in RELATIVE_FILES.items()} def analyze_static_chain(apktool_dir: str | Path = DEFAULT_APKTOOL_DIR) -> dict[str, Any]: """分析本地 smali,返回 ``passport_account_image`` 静态证据报告。""" root = Path(apktool_dir) if not root.is_absolute(): root = (ROOT / root).resolve() files = _file_map(root) encoded_fap = _extract_field_string(files["x0"], "d") encoded_cfg_pull = _extract_field_string(files["x0"], "c") encoded_cfg_push = _extract_field_string(files["x0"], "b") direct_a_y_q_z_refs = _grep_tree(root, "a_y_q_z") generic_writer_refs = _grep_tree(root, "Lcom/kuaishou/weapon/ks/z0;->a(ZLjava/lang/String;Ljava/lang/String;)V") facts: dict[str, Any] = { "files": { name: { "path": _rel(path, root), "exists": _exists(path), } for name, path in files.items() }, "weaponhi_b_upload_encrypt": { "summary": "WeaponHI.b(str) = str.getBytes -> i0.a([B) GZIP -> MXSec.atlasEncrypt(privacykit, UUID, 0, bytes) -> Base64", "method": _first_line(files["weaponhi"], root, ".method public static b(Ljava/lang/String;)Ljava/lang/String;"), "gzip_call": _first_line_in_method( files["weaponhi"], root, ".method public static b(Ljava/lang/String;)Ljava/lang/String;", "Lcom/kuaishou/weapon/ks/i0;->a([B)[B", ), "gzip_impl": _first_line(files["gzip"], root, "Ljava/util/zip/GZIPOutputStream;"), "product": _first_line_in_method( files["weaponhi"], root, ".method public static b(Ljava/lang/String;)Ljava/lang/String;", 'const-string v1, "privacykit"', ), "sdk_id": _first_line_in_method( files["weaponhi"], root, ".method public static b(Ljava/lang/String;)Ljava/lang/String;", 'const-string v2, "7e46b28a-8c93-4940-8238-4c60e64e3c81"', ), "atlas_encrypt": _first_line_in_method( files["weaponhi"], root, ".method public static b(Ljava/lang/String;)Ljava/lang/String;", "->atlasEncrypt(Ljava/lang/String;Ljava/lang/String;I[B)[B", ), "base64": _first_line_in_method( files["weaponhi"], root, ".method public static b(Ljava/lang/String;)Ljava/lang/String;", "Lcom/kuaishou/weapon/ks/q;->c([BI)Ljava/lang/String;", ), }, "weaponhi_dd21_read": { "summary": "WeaponHI.dd(21) 先复用内存 img;不是 VIMG_ 时读取 SharedPreferences wcfg['a_y_q_z'] 并缓存回 img", "method": _first_line(files["weaponhi"], root, ".method public static dd(I)Ljava/lang/String;"), "vimg_cache_guard": _first_line_in_method( files["weaponhi"], root, ".method public static dd(I)Ljava/lang/String;", 'const-string v0, "VIMG_"', ), "z0_singleton": _first_line_in_method( files["weaponhi"], root, ".method public static dd(I)Ljava/lang/String;", "Lcom/kuaishou/weapon/ks/z0;->a(Landroid/content/Context;)Lcom/kuaishou/weapon/ks/z0;", ), "wcfg_key": _first_line_in_method( files["weaponhi"], root, ".method public static dd(I)Ljava/lang/String;", 'const-string v0, "a_y_q_z"', ), "z0_get_string": _first_line_in_method( files["weaponhi"], root, ".method public static dd(I)Ljava/lang/String;", "Lcom/kuaishou/weapon/ks/z0;->a(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", ), }, "z0_wcfg_store": { "summary": "z0 构造函数打开 SharedPreferences 'wcfg';a(String,String) 读取 getString;a(boolean,key,value) 是通用 putString 写入", "open_wcfg": _first_line_in_method( files["z0"], root, ".method public constructor (Landroid/content/Context;)V", 'const-string v0, "wcfg"', ), "get_shared_preferences": _first_line_in_method( files["z0"], root, ".method public constructor (Landroid/content/Context;)V", "->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;", ), "get_string": _first_line_in_method( files["z0"], root, ".method public a(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", "->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", ), "generic_write_method": _first_line(files["z0"], root, ".method public a(ZLjava/lang/String;Ljava/lang/String;)V"), "put_string": _first_line_in_method( files["z0"], root, ".method public a(ZLjava/lang/String;Ljava/lang/String;)V", "SharedPreferences$Editor;->putString(Ljava/lang/String;Ljava/lang/String;)", ), }, "passport_map_injection": { "summary": "账号登录公共参数开关 ugAccoutNetAddRiskParam 开启时调用 WeaponHI.dd(0x15),再 put('passport_account_image', value)", "switch": _first_line(files["zvl_c"], root, 'const-string v2, "ugAccoutNetAddRiskParam"'), "dd21_call": _first_line(files["zvl_c"], root, "Lcom/kuaishou/weapon/i/WeaponHI;->dd(I)Ljava/lang/String;"), "map_key": _first_line(files["zvl_c"], root, 'const-string v1, "passport_account_image"'), "map_put": _first_line(files["zvl_c"], root, "Ljava/util/Map;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"), }, "fap_upload": { "summary": "x0.d 解码为 /f/a/p;r1.a 组 JSON {data: 'VIMG_'+WeaponHI.b(payload)} 后经 i/y0 POST", "encoded_endpoint": encoded_fap, "decoded_endpoint": _decode_base64_text(encoded_fap), "encoded_cfg_pull_endpoint": encoded_cfg_pull, "decoded_cfg_pull_endpoint": _decode_base64_text(encoded_cfg_pull), "encoded_cfg_push_endpoint": encoded_cfg_push, "decoded_cfg_push_endpoint": _decode_base64_text(encoded_cfg_push), "x0_endpoint_line": _first_line(files["x0"], root, f'"{encoded_fap}"') if encoded_fap else {}, "r1_weaponhi_b_call": _first_line(files["r1"], root, "Lcom/kuaishou/weapon/i/WeaponHI;->b(Ljava/lang/String;)Ljava/lang/String;"), "r1_vimg_prefix": _first_line(files["r1"], root, 'const-string v4, "VIMG_"'), "r1_payload_data_key": _first_line(files["r1"], root, 'const-string v5, "data"'), "r1_http_dispatch": _first_line(files["r1"], root, "Lcom/kuaishou/weapon/ks/i;->a(Landroid/content/Context;Lcom/kuaishou/weapon/ks/k1;)Ljava/lang/String;"), "okhttp_execute": _first_line(files["i"], root, "Lokhttp3/Call;->execute()Lokhttp3/Response;"), "urlconnection_post": _first_line(files["y0"], root, 'const-string v2, "POST"'), }, "wcfg_writer_search": { "summary": "静态全库只发现 dd(21) 处直接出现 a_y_q_z;写入端更像服务端响应驱动的通用 key/value 落盘,需要动态证据确认具体 value", "direct_a_y_q_z_refs": direct_a_y_q_z_refs, "direct_a_y_q_z_write_found": any("putString" in item["text"] for item in direct_a_y_q_z_refs), "generic_z0_writer_refs": generic_writer_refs, }, } proven = { "weaponhi_b_upload_encrypt": bool( facts["weaponhi_b_upload_encrypt"]["method"] and facts["weaponhi_b_upload_encrypt"]["gzip_call"] and facts["weaponhi_b_upload_encrypt"]["atlas_encrypt"] ), "weaponhi_dd21_reads_a_y_q_z": bool( facts["weaponhi_dd21_read"]["method"] and facts["weaponhi_dd21_read"]["wcfg_key"] and facts["weaponhi_dd21_read"]["z0_get_string"] ), "z0_uses_wcfg": bool(facts["z0_wcfg_store"]["open_wcfg"] and facts["z0_wcfg_store"]["get_string"]), "passport_map_injection": bool( facts["passport_map_injection"]["dd21_call"] and facts["passport_map_injection"]["map_key"] ), "fap_upload_endpoint": facts["fap_upload"]["decoded_endpoint"] == "/f/a/p", "direct_local_a_y_q_z_writer": bool(facts["wcfg_writer_search"]["direct_a_y_q_z_write_found"]), } chain = [ "r1.a(payload, context) 构造上传态 VIMG_ + WeaponHI.b(payload)", "x0.d Base64 解码后命中 /f/a/p", "i/y0 发送 POST 并返回响应字符串", "wcfg 通用写入方法 z0.a(boolean,key,value) 可写任意 key", "WeaponHI.dd(21) 读取 wcfg['a_y_q_z']", "zvl.c 把 dd(21) 结果注入 passport_account_image", ] unresolved = [] if not proven["direct_local_a_y_q_z_writer"]: unresolved.append( "未在 smali 中发现硬编码 a_y_q_z 的本地写入;最终 VIMG_...$AI_... 仍需 /f/a/p 响应或 wcfg 动态抓证。" ) if not all(proven[key] for key in ("weaponhi_b_upload_encrypt", "weaponhi_dd21_reads_a_y_q_z", "z0_uses_wcfg", "passport_map_injection", "fap_upload_endpoint")): unresolved.append("部分静态锚点缺失,需检查 apktool 目录是否对应当前 APP 版本。") return { "apktool_dir": str(root), "facts": facts, "proven": proven, "chain": chain, "pure_calc_status": { "upload_vimg_building_block": bool(proven["weaponhi_b_upload_encrypt"]), "final_passport_ticket": "server_or_wcfg_state", "final_ticket_offline_proven": False, "next_evidence": [ "抓 /f/a/p 返回体完整内容", "抓 z0.a(boolean,key,value) 或 SharedPreferences.Editor.putString 写入 a_y_q_z 的 value_full", "对比写入 value 与随后 mobile/checker / login body 的 passport_account_image", ], }, "unresolved": unresolved, } def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="静态整理 passport_account_image / WeaponHI.dd(21) 链路证据") parser.add_argument("--apktool-dir", default=str(DEFAULT_APKTOOL_DIR), help="apktool 反编译目录") parser.add_argument("--out", default="", help="可选 JSON 输出路径") parser.add_argument("--pretty", action="store_true", help="格式化 JSON 输出") return parser def main() -> int: args = build_parser().parse_args() result = analyze_static_chain(args.apktool_dir) text = json.dumps(result, ensure_ascii=False, indent=2 if args.pretty else None) if args.out: out = Path(args.out) if not out.is_absolute(): out = ROOT / out out.parent.mkdir(parents=True, exist_ok=True) out.write_text(text + "\n", encoding="utf-8") print(f"[OK] 写入 {out}") print(text) return 0 if __name__ == "__main__": raise SystemExit(main())