init
This commit is contained in:
Vendored
+1
@@ -0,0 +1 @@
|
||||
"""Vendored offline helpers (no network)."""
|
||||
+1010
File diff suppressed because it is too large
Load Diff
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline reproducer for the Coruna PLServerPool domain generator.
|
||||
|
||||
This script performs no DNS lookups and makes no network requests. It is
|
||||
intended for IOC generation and static-analysis verification only.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
||||
|
||||
MASK32 = 0xFFFFFFFF
|
||||
HASH_SEED = 0x9E3779B1
|
||||
MURMUR_M = 0x5BD1E995
|
||||
ALNUM = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
ALNUM_HYPHEN = "abcdefghijklmnopqrstuvwxyz0123456789-"
|
||||
TLDS = (
|
||||
".com",
|
||||
".net",
|
||||
".org",
|
||||
".cc",
|
||||
".so",
|
||||
".online",
|
||||
".cfd",
|
||||
".site",
|
||||
".lol",
|
||||
".net", # Deliberately duplicated in the sample's 15-entry table.
|
||||
".live",
|
||||
".store",
|
||||
".app",
|
||||
".icu",
|
||||
".info",
|
||||
)
|
||||
|
||||
KNOWN_SEEDS = {
|
||||
"deployment": "09d0b8d58a71653cd1c89c64c866f2e6",
|
||||
"reporting": "2d2aebba0bf3d7d694194a7ab93b0a96",
|
||||
"placeholder-deployment": "UNDEFINED_DEPLOYMENT_SEED",
|
||||
"placeholder-reporting": "UNDEFINED_REPORTING_SEED",
|
||||
}
|
||||
|
||||
|
||||
def murmur_hash2(value: str, seed: int = HASH_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 xorshift32(state: int) -> int:
|
||||
state ^= (state << 13) & MASK32
|
||||
state ^= state >> 17
|
||||
state ^= (state << 5) & MASK32
|
||||
return state & MASK32
|
||||
|
||||
|
||||
def generate_domains(seed: str, count: int = 5) -> list[str]:
|
||||
"""Generate the sample's externally used candidate slice.
|
||||
|
||||
The native helper builds 512 strings internally, while PLServerPool asks
|
||||
for and retains the first five. ``count`` is therefore capped at 512 for
|
||||
analysis, although five is the operational pool size in this build.
|
||||
"""
|
||||
|
||||
if not 1 <= count <= 512:
|
||||
raise ValueError("count must be between 1 and 512")
|
||||
|
||||
base_hash = murmur_hash2(seed)
|
||||
domains: list[str] = []
|
||||
|
||||
for index in range(512):
|
||||
state = murmur_hash2(f"{seed}{index}") ^ base_hash
|
||||
if state == 0:
|
||||
state = 1
|
||||
|
||||
state = xorshift32(state)
|
||||
label_length = 16 + state % 9
|
||||
|
||||
state = xorshift32(state)
|
||||
label = ALNUM[state % len(ALNUM)]
|
||||
|
||||
for _ in range(1, label_length - 1):
|
||||
state = xorshift32(state)
|
||||
alphabet = ALNUM if label[-1] == "-" else ALNUM_HYPHEN
|
||||
label += alphabet[state % len(alphabet)]
|
||||
|
||||
state = xorshift32(state)
|
||||
label += ALNUM[state % len(ALNUM)]
|
||||
|
||||
state = xorshift32(state)
|
||||
domains.append(f"www.{label}{TLDS[state % len(TLDS)]}")
|
||||
|
||||
return domains[:count]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Reproduce Coruna DGA candidates offline (no network access)."
|
||||
)
|
||||
parser.add_argument(
|
||||
"seed",
|
||||
nargs="?",
|
||||
default="deployment",
|
||||
help=(
|
||||
"deployment, reporting, placeholder-deployment, "
|
||||
"placeholder-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()
|
||||
Reference in New Issue
Block a user