148 lines
4.3 KiB
Python
148 lines
4.3 KiB
Python
#!/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()
|