ksjsb/tools/static_passport_chain_report.py
2026-07-30 20:25:56 +08:00

378 lines
16 KiB
Python
Raw Permalink 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.

"""静态整理 ``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) 读取 getStringa(boolean,key,value) 是通用 putString 写入",
"open_wcfg": _first_line_in_method(
files["z0"],
root,
".method public constructor <init>(Landroid/content/Context;)V",
'const-string v0, "wcfg"',
),
"get_shared_preferences": _first_line_in_method(
files["z0"],
root,
".method public constructor <init>(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/pr1.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())