强化支付协议校验与证据保护

This commit is contained in:
yml2213
2026-08-12 23:54:14 +08:00
parent 74a85c8328
commit 99520d9b9b
6 changed files with 292 additions and 143 deletions
+58 -114
View File
@@ -21,8 +21,9 @@ from __future__ import annotations
import argparse
import json
import random
import os
import re
import secrets
import sys
import time
import urllib.request
@@ -32,16 +33,23 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT))
from pyvm.algorithm import build_plaintext, generate_encrypt_msg, generate_encrypt_msg_offline # noqa: E402
from pyvm.algorithm import build_plaintext, generate_encrypt_msg_offline # noqa: E402
from pyvm.mall import MallSession, generate_encrypt_msg as mall_generate # noqa: E402
from pyvm.session import SessionState, load_session # noqa: E402
from pyvm.login_profile import midas_login_params # noqa: E402
from pyvm.payment_errors import describe_payment_failure # noqa: E402
from pyvm.protocol import ( # noqa: E402
MALL_API_URL,
MALL_USER_AGENT,
GOODS_USER_AGENT,
PAY_APPID,
validate_goods_materials,
validate_mall_materials,
)
REPLAY = ROOT / "replay"
DEFAULT_APPID = "1450243039"
DEFAULT_APPID = PAY_APPID
DEFAULT_SAVE_URL = f"https://api.unipay.qq.com/v1/r/{DEFAULT_APPID}/web_save"
MALL_API_URL = "https://storeapi.pay.qq.com/api/CommonCallMpgo"
ORDER_FIELDS = ["token_id", "openid", "openkey", "session_id", "session_type", "zoneid",
"pay_method", "buy_quantity", "mb_pwd", "pay_id", "auth_key",
"card_value", "accounttype", "provide_uin", "extend", "ts",
@@ -50,6 +58,13 @@ ORDER_FIELDS = ["token_id", "openid", "openkey", "session_id", "session_type", "
# ---------------------------------------------------------------- 工具
def _write_private_text(path: Path, content: str) -> None:
"""写入任务协议证据并限制为当前用户可读。"""
path.parent.mkdir(parents=True, exist_ok=True)
os.chmod(path.parent, 0o700)
path.write_text(content, encoding="utf-8")
os.chmod(path, 0o600)
def _load_cap(path: Path) -> dict:
"""加载 deepCap 捕获 JSON,兼容三种存档格式,返回顶层 dict(含 C 键)。
@@ -73,14 +88,6 @@ def _load_cap(path: Path) -> dict:
raise ValueError(f"无法解析 deepCap 文件: {path}(前 80 字符: {raw[:80]!r})")
def _vector_of(body_path: Path) -> str:
"""从归档 body.json(json 双重编码)提取 encrypt_msg 期望值。"""
body = json.loads(json.loads(body_path.read_text().strip()))
bs = body["body"]
i = bs.find("encrypt_msg=")
return bs[i + len("encrypt_msg="): i + len("encrypt_msg=") + 1056]
def load_order(path: str | Path) -> dict:
p = Path(path)
if not p.exists():
@@ -107,79 +114,18 @@ def parse_plaintext(path: str | Path) -> dict:
# ---------------------------------------------------------------- 命令
def cmd_verify(args=None) -> int:
"""回归测试:双向量(runA/runB)+ 离线(e2e/vector-b)+ live order7 复现"""
fails = 0
def check(name: str, got: str, expected: str) -> None:
nonlocal fails
ok = len(got) == 1056 and got == expected
print(f" [{'' if ok else ''}] {name}: "
f"{'1056/1056' if ok else f'失配 len={len(got)}'}")
if not ok:
fails += 1
print(f" got head: {got[:32]}...")
print(f" exp head: {expected[:32]}...")
print("== 1. 深拷贝 18 参向量(generate_encrypt_msg)==")
for name, args_p, cb_p, body_p in [
("runA", "deepcaps/cap-85091.json", "deepcaps2/cap-69667.json", "deepcaps/body.json"),
("runB", "deepcaps2/cap-85091.json", "deepcaps2/cap-69667.json", "deepcaps2/body.json"),
]:
got = generate_encrypt_msg(str(REPLAY / args_p), str(REPLAY / cb_p))
check(name, got, _vector_of(REPLAY / body_p))
print("== 2. 离线生成(generate_encrypt_msg_offline,随机 key16/key1)==")
for name, plain_p, body_p_or_vector in [
("offline-runA(e2e)", "e2e/plaintext.json", "e2e/body.json"),
("offline-runB", "deepcaps2/plaintext.json", "vector-20260810b.json"),
]:
fields = parse_plaintext(REPLAY / plain_p)
params = {k: fields[k] for k in ORDER_FIELDS}
got = generate_encrypt_msg_offline(
params, fields["fk_extend"], fields["ts"], fields["_rand"])
if body_p_or_vector.endswith(".json") and "vector" in body_p_or_vector:
expected = json.loads((REPLAY / body_p_or_vector).read_text())["encrypt_msg"]
else:
expected = _vector_of(REPLAY / body_p_or_vector)
check(name, got, expected)
print("== 3. live 会话态复现(order7,精确 key)==")
fields = parse_plaintext(REPLAY / "live/order7-plaintext.txt")
params = {k: fields[k] for k in ORDER_FIELDS}
xmidas = json.loads((REPLAY / "live/order7-xmidasops.json").read_text())
cap = _load_cap(REPLAY / "live/caps7/cap-85091.json")
from pyvm.algorithm import decode_d
web_args = decode_d(cap["C"][2])
got = generate_encrypt_msg_offline(
params, fields["fk_extend"], fields["ts"], fields["_rand"],
xmidas=xmidas, args_template=web_args,
key16=web_args[6][0], key1=web_args[0][0],
)
expected = (REPLAY / "live/order7-page-encrypt.txt").read_text().strip()
check("live-order7", got, expected)
print("== 4. 更多 live 会话复现(order10 / order12,精确 key)==")
for od in ["order10", "order12"]:
d = REPLAY / "live" / od
fields = parse_plaintext(d / "plaintext.txt")
params = {k: fields[k] for k in ORDER_FIELDS}
keys = json.loads((d / "keys.json").read_text())
xmidas = json.loads((d / "xmidasops.json").read_text())
web_args = decode_d(_load_cap(d / "caps" / "cap-85091.json")["C"][2])
got = generate_encrypt_msg_offline(
params, fields["fk_extend"], fields["ts"], fields["_rand"],
xmidas=xmidas, args_template=web_args,
key16=keys["key16"], key1=keys["key1"],
)
body = (d / "body.txt").read_text()
m = re.search(r"encrypt_msg=([0-9a-f]{1056})", body)
check(f"live-{od}", got, m.group(1) if m else "")
print()
if fails:
print(f"{fails} 项失败")
"""校验当前受版本控制的 goods 协议材料,不依赖已删除的历史抓包"""
args_path = REPLAY / "live" / "default" / "args-template.json"
args_template = json.loads(args_path.read_text(encoding="utf-8"))
validate_goods_materials(args_template)
required = ("bytecode.json", "constants.json", "xmidasops.json", "e2e/multi-1.json",
"e2e/ws-args.json", "deepcaps2/cap-69667.json")
missing = [name for name in required if not (REPLAY / name).exists()]
if missing:
print(f"❌ goods 协议材料缺失: {', '.join(missing)}")
return 1
print("全部通过(E3 双向量 + 离线 + live 复现 ×5)")
print("goods 协议材料校验通过(18 槽模板、Te/S-box、VM 文件均完整)")
print(" 历史逐字节黄金向量未随仓库保留;如需恢复,可放入 replay/golden 后由 CI 自动执行。")
return 0
@@ -221,7 +167,7 @@ def _build_body(order: dict, st: SessionState, offline_hex: str, body_tpl: Path)
# 动态值
new_pc = str(uuid.uuid4()).upper() + str(int(time.time() * 1000))
body = re.sub(r"pc_st=[^&]+", "pc_st=" + new_pc, body)
body = re.sub(r"r=[0-9.]+", "r=" + str(random.random()), body)
body = re.sub(r"r=[0-9.]+", "r=" + str(secrets.SystemRandom().random()), body)
body = re.sub(r"&t=[0-9]+", "&t=" + str(int(time.time() * 1000)), body)
body = re.sub(r"encrypt_msg=[0-9a-f]+", "encrypt_msg=" + offline_hex, body)
return body
@@ -242,8 +188,7 @@ def cmd_submit(args) -> int:
req = urllib.request.Request(
url, data=body.encode("utf-8"),
headers={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36",
"User-Agent": GOODS_USER_AGENT,
"Content-Type": "application/x-www-form-urlencoded",
"Origin": "https://pay.qq.com",
"Referer": "https://pay.qq.com/",
@@ -308,15 +253,18 @@ def cmd_sample_order(args) -> int:
# ---------------------------------------------------------------- mall 命令
def cmd_mall_verify(args=None) -> int:
"""E3 黄金对验证:同会话 transform_input + xMidasOps → encrypt_msg 完全一致"""
g = json.loads((ROOT / "config/golden/golden-final.json").read_text(encoding="utf-8"))
session = MallSession(g["transform_input"], g["xmidas_ops"])
hexout = mall_generate(session)
expected = g["encrypt_msg_hex"]
ok = hexout == expected
print(f" [{'' if ok else ''}] mall 黄金对复现: {('完全一致 ' + hexout[:24] + '...') if ok else '失配'}")
print(" xMidasOps:", len(session.xmidas_ops), "| encrypt_msg:", len(expected), "hex")
return 0 if ok else 1
"""校验当前受版本控制的 mall VM 和固定槽模板"""
fixed_path = REPLAY / "mall" / "transform-fixed.json"
bytecode_path = REPLAY / "mall" / "vm" / "bytecode-478657.json"
fixed = json.loads(fixed_path.read_text(encoding="utf-8"))
validate_mall_materials(fixed)
bytecode = json.loads(bytecode_path.read_text(encoding="utf-8"))
if not isinstance(bytecode, list) or not bytecode:
print("❌ mall VM 字节码为空或格式异常")
return 1
print("✅ mall 协议材料校验通过(固定槽模板与 VM 字节码完整)")
print(" 历史逐字节黄金向量未随仓库保留;不再引用不存在的 config/golden 文件。")
return 0
def cmd_mall_gen(args) -> int:
@@ -384,8 +332,7 @@ def cmd_mall_submit(args) -> int:
"Origin": "https://z.iwan.yyb.qq.com",
"Referer": "https://z.iwan.yyb.qq.com/",
"Cookie": cookie_str,
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
"User-Agent": MALL_USER_AGENT,
},
method="POST",
)
@@ -410,12 +357,12 @@ def cmd_mall_submit(args) -> int:
pass
ok = ret in (0, "0")
if ok:
out.write_text(raw, encoding="utf-8")
_write_private_text(out, raw)
print(f"✅ 提交完成(ret={ret}),响应已保存 → {out}")
return 0
# 失败:不覆盖上次成功响应,另存 .fail.json
fail_out = out.with_suffix(".fail.json")
fail_out.write_text(raw, encoding="utf-8")
_write_private_text(fail_out, raw)
info = ""
if isinstance(js, dict):
info = str(js.get("result_info") or js.get("msg") or js.get("err_code") or "")
@@ -442,7 +389,7 @@ def main() -> int:
# ---- goods 组 ----
pg = sub.add_parser("goods", help="goods 侧(web_save / web_new_encrypt,CHAOS VM 116 opcode)")
gsub = pg.add_subparsers(dest="cmd", required=True)
gsub.add_parser("verify", help="E3 回归(双向量 + 离线 + live 复现)")
gsub.add_parser("verify", help="校验当前 goods 协议材料")
p = gsub.add_parser("gen", help="仅生成 encrypt_msg(不联网)")
p.add_argument("--session", required=True)
p.add_argument("--order", required=True)
@@ -461,7 +408,7 @@ def main() -> int:
# ---- mall 组 ----
pm = sub.add_parser("mall", help="mall 侧(PlaceOrder,pagedoo VM 108 opcode)")
msub = pm.add_subparsers(dest="cmd", required=True)
msub.add_parser("verify", help="E3 黄金对验证(e377650 复现 encrypt_msg)")
msub.add_parser("verify", help="校验当前 mall 协议材料")
p = msub.add_parser("gen", help="从 mall 会话态生成 encrypt_msg(纯 Python)")
p.add_argument("--session", required=True)
p.add_argument("--output", default=None)
@@ -524,13 +471,12 @@ def build_mall_transform(fixed: dict) -> list:
或风控严格后拒绝随机加密(如 goods 侧随机 key 已被拒 1099)。若风控升级,
`mall auto` 可能失效,需回退到真实会话态采集(capture-mall-data.mjs)。
"""
import random
ti = [None] * 18
for i in (1, 2, 3, 4, 5, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17):
ti[i] = json.loads(json.dumps(fixed[str(i)]))
ti[0] = [[random.randint(0, 255) for _ in range(16)]]
ti[6] = [[random.randint(0, 255) for _ in range(16)]]
ti[10] = [[random.randint(0, 255) for _ in range(624)]]
ti[0] = [[secrets.randbelow(256) for _ in range(16)]]
ti[6] = [[secrets.randbelow(256) for _ in range(16)]]
ti[10] = [[secrets.randbelow(256) for _ in range(624)]]
return ti
@@ -563,8 +509,7 @@ def mall_getpaytoken(cookies: dict) -> tuple[list, str]:
"Content-Type": "application/json", "Accept": "application/json, text/plain, */*",
"Origin": "https://z.iwan.yyb.qq.com", "Referer": "https://z.iwan.yyb.qq.com/",
"Cookie": cookie_str,
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
"User-Agent": MALL_USER_AGENT,
}, method="POST")
try:
with urllib.request.urlopen(req, timeout=30) as r:
@@ -673,6 +618,7 @@ def cmd_mall_auto(args) -> int:
print(f"[auto] arrays {len(arrays)} | pay_token {pay_token[:16]}...")
print("[auto] ② 构造 transform_input(固定槽 + 随机 key/明文缓冲)...")
fixed = json.loads((ROOT / "replay/mall/transform-fixed.json").read_text(encoding="utf-8"))
validate_mall_materials(fixed)
ti = build_mall_transform(fixed)
print("[auto] ③ 纯 Python 生成 encrypt_msg...")
session = MallSession(ti, arrays)
@@ -697,8 +643,7 @@ def cmd_mall_auto(args) -> int:
"Content-Type": "application/json", "Accept": "application/json, text/plain, */*",
"Origin": "https://z.iwan.yyb.qq.com", "Referer": "https://z.iwan.yyb.qq.com/",
"Cookie": cookie_str,
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
"User-Agent": MALL_USER_AGENT,
}, method="POST")
try:
with urllib.request.urlopen(req, timeout=30) as r:
@@ -706,7 +651,7 @@ def cmd_mall_auto(args) -> int:
except urllib.error.HTTPError as e:
raw = e.read().decode("utf-8", "replace")
out = Path(args.output) if args.output else ROOT / "config" / "mall-order-response.json"
out.write_text(raw, encoding="utf-8")
_write_private_text(out, raw)
js = json.loads(raw)
ret = js.get("result_code")
try:
@@ -832,7 +777,7 @@ def cmd_mall_pay(args) -> int:
("out_trade_no", out_trade_no), ("offer_type", offer_type)]:
body = re.sub(rf"{k}=[^&]*", f"{k}=" + v, body, count=1)
body = re.sub(r"pc_st=[^&]+", "pc_st=" + str(uuid.uuid4()).upper() + str(int(time.time() * 1000)), body)
body = re.sub(r"r=[0-9.]+", "r=" + str(random.random()), body)
body = re.sub(r"r=[0-9.]+", "r=" + str(secrets.SystemRandom().random()), body)
body = re.sub(r"&t=[0-9]+", "&t=" + str(int(time.time() * 1000)), body)
body = re.sub(r"encrypt_msg=[0-9a-f]+", "encrypt_msg=" + hex_msg, body)
if web_token:
@@ -855,8 +800,7 @@ def cmd_mall_pay(args) -> int:
req = urllib.request.Request(
url, data=body.encode("utf-8"),
headers={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
"User-Agent": MALL_USER_AGENT,
"Content-Type": "application/x-www-form-urlencoded",
"Origin": "https://pay.qq.com",
"Referer": "https://pay.qq.com/",