Files
2026-08-13 06:30:07 +08:00

148 lines
4.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Offline reproducer for xxbb PLServerPool domain generation.
Native path (type-0x01 `_generateDomainsLocked` → helper at 0x81fe0):
srandom(murmur2_0x12345678(seed))
for i in 0 .. count-1:
burn murmur2(seed + "%ld" % i) % 10000 calls to random()
15 chars from [a-z0-9] via random() % 36
format as "%@.icu"
The shared Deployment/Reporting pools both pass the reporting-c CFString
as `seed`. This script performs no DNS lookups.
"""
from __future__ import annotations
import argparse
MASK32 = 0xFFFFFFFF
MURMUR_M = 0x5BD1E995
MURMUR_SEED = 0x12345678
ALNUM = "abcdefghijklmnopqrstuvwxyz0123456789"
LABEL_LEN = 15
TLD = ".icu"
NATIVE_POOL_SIZE = 512
KNOWN_SEEDS = {
"channel-c": "202700cfb1ad3de68e11239dcc26c30b",
"deployment": "321fb0c812b46265421b5ad9654c2b81",
"reporting": "68143bfa7130bb97a642196db0292a12",
}
def murmur_hash2(value: str, seed: int = MURMUR_SEED) -> int:
data = value.encode("utf-8")
result = (seed ^ len(data)) & MASK32
offset = 0
while offset + 4 <= len(data):
block = int.from_bytes(data[offset : offset + 4], "little")
block = (block * MURMUR_M) & MASK32
block ^= block >> 24
block = (block * MURMUR_M) & MASK32
result = (result * MURMUR_M) & MASK32
result ^= block
offset += 4
tail = data[offset:]
if len(tail) == 3:
result ^= tail[2] << 16
if len(tail) >= 2:
result ^= tail[1] << 8
if len(tail) >= 1:
result ^= tail[0]
result = (result * MURMUR_M) & MASK32
result ^= result >> 13
result = (result * MURMUR_M) & MASK32
result ^= result >> 15
return result & MASK32
def _to_i32(value: int) -> int:
value &= MASK32
return value - 0x100000000 if value >= 0x80000000 else value
def _good_rand(value: int) -> int:
"""Park–Miller LCG used by Apple/BSD srandom() (not the weak 1103515245 seed)."""
x = _to_i32(value)
if x == 0:
x = 123459876
hi = int(x / 127773) # C truncates toward zero; Python // floors
lo = x - hi * 127773
x = 16807 * lo - 2836 * hi
if x < 0:
x += 0x7FFFFFFF
return x & MASK32
class BSDRandom:
"""Apple libsystem_c TYPE_3 random()/srandom() (deg=31, sep=3)."""
DEG = 31
SEP = 3
def __init__(self) -> None:
self.state = [0] * self.DEG
self.f = self.SEP
self.r = 0
def srandom(self, seed: int) -> None:
self.state[0] = seed & MASK32
for i in range(1, self.DEG):
self.state[i] = _good_rand(self.state[i - 1])
self.f = self.SEP
self.r = 0
for _ in range(10 * self.DEG):
self.random()
def random(self) -> int:
self.state[self.f] = (self.state[self.f] + self.state[self.r]) & MASK32
value = (self.state[self.f] >> 1) & 0x7FFFFFFF
self.f += 1
if self.f >= self.DEG:
self.f = 0
self.r += 1
else:
self.r += 1
if self.r >= self.DEG:
self.r = 0
return value
def generate_domains(seed: str, count: int = 5) -> list[str]:
"""First `count` hosts the native pool would try (pool itself holds 512)."""
if not 1 <= count <= NATIVE_POOL_SIZE:
raise ValueError(f"count must be between 1 and {NATIVE_POOL_SIZE}")
rng = BSDRandom()
rng.srandom(murmur_hash2(seed))
domains: list[str] = []
for index in range(count):
burn = murmur_hash2(f"{seed}{index}") % 10000
for _ in range(burn):
rng.random()
label = "".join(ALNUM[rng.random() % len(ALNUM)] for _ in range(LABEL_LEN))
domains.append(f"{label}{TLD}")
return domains
def main() -> None:
parser = argparse.ArgumentParser(
description="Reproduce xxbb PLServerPool DGA candidates offline (no network access)."
)
parser.add_argument(
"seed",
nargs="?",
default="channel-c",
help="channel-c, deployment, reporting, or a literal seed",
)
parser.add_argument("-n", "--count", type=int, default=5)
args = parser.parse_args()
seed = KNOWN_SEEDS.get(args.seed, args.seed)
print(f"seed={seed}")
for index, domain in enumerate(generate_domains(seed, args.count), 1):
print(f"{index:03d} {domain}")
if __name__ == "__main__":
main()