Skip to content

Instantly share code, notes, and snippets.

@sysopfb
Created June 18, 2026 14:45
Show Gist options
  • Select an option

  • Save sysopfb/2e6f9e59b0a6f474df2cc0b837d63bcb to your computer and use it in GitHub Desktop.

Select an option

Save sysopfb/2e6f9e59b0a6f474df2cc0b837d63bcb to your computer and use it in GitHub Desktop.
Tracker_DGA

#Malware related RMMSystem that uses Metastealer C2 traffic, tracker traffic: /pmxs/logs

"""
Full DGA (Domain Generation Algorithm) replication of sub_14000BF00 and friends.

Calling generate_domains() replicates the exact output of the binary with the
hardcoded seed, count, width, and word list found in sub_14000BF00.
"""

from __future__ import annotations

# ---------------------------------------------------------------------------
# Constants extracted directly from sub_14000BF00
# ---------------------------------------------------------------------------

WORDLIST_RAW = (
    "void,star,orbit,probe,moon,sun,core,crust,dust,gas,rock,ice,ring,disc,belt,"
    "flare,ray,beam,light,dark,cold,heat,mass,weight,force,pull,push,drift,glide,"
    "spin,tilt,axis,pole,cap,crater,ridge,peak,rift,vent,plume,cloud,mist,fog,veil,"
    "spark,flash,glow,pulse,wave,tide,flow,stream,path,track,zone,rim,edge,brink,"
    "shell,skin,hull,hatch,port,bay,dock,deck,mast,sail,wing,fin,nose,tail,tank,"
    "fuel,bolt,nut,wire,cord,link,joint,gear,lens,glass,mirror,film,chip,grid,plug,"
    "switch,knob,dial,screen,frame,base,stand,mount,arm,dish,beam,mast,boom"
)

SEED   = 0x1AED   # 6893
LEVELS = 1000     # number of domains to generate
WIDTH  = 3        # word picks per domain
TLD    = "xyz"    # suffix pointed to by the hardcoded address 8026488


# ---------------------------------------------------------------------------
# sub_140007B50 — parse comma-separated word list into a list of strings
# ---------------------------------------------------------------------------

def parse_wordlist(raw: str) -> list[str]:
    """
    Replicates sub_140007B50.

    Parses a comma-separated string into a list of tokens, mirroring the
    binary's exact behaviour:
      - Iterates character by character, accumulating into a buffer.
      - On comma: trims leading (space, tab) and trailing (space, tab, CR, LF)
        whitespace from the accumulated token, then pushes if non-empty.
      - After the loop: flushes the final token (no trailing comma in the list).
      - Duplicates are preserved (e.g. 'beam' and 'mast' appear twice in the
        hardcoded word list).
    """
    LEAD_TRIM  = {' ', '\t'}              # chars stripped from left
    TRAIL_TRIM = {' ', '\t', '\r', '\n'}  # chars stripped from right

    results: list[str] = []
    accumulator: list[str] = []

    def flush() -> None:
        token = "".join(accumulator)
        # trim leading whitespace (mirrors forward scan)
        start = 0
        while start < len(token) and token[start] in LEAD_TRIM:
            start += 1
        # trim trailing whitespace (mirrors backward scan)
        end = len(token)
        while end > start and token[end - 1] in TRAIL_TRIM:
            end -= 1
        trimmed = token[start:end]
        if trimmed:
            results.append(trimmed)
        accumulator.clear()

    for ch in raw:
        if ch == ',':
            flush()
        else:
            accumulator.append(ch)

    flush()  # mirrors sub_140005AD0 call — flush final token after loop

    return results


# ---------------------------------------------------------------------------
# u32 — keep arithmetic in unsigned 32-bit range (x86-64 register semantics)
# ---------------------------------------------------------------------------

def u32(value: int) -> int:
    return value & 0xFFFFFFFF


# ---------------------------------------------------------------------------
# sub_140006F10 — hash-driven DGA core
# ---------------------------------------------------------------------------

def _generate_paths(
    seed: int,
    levels: int,
    width: int,
    nodes: list[str],
    suffix: str,
) -> list[str]:
    """
    Replicates sub_140006F10.
    Pseudorandomly selects 'width' words per row using a rolling 32-bit hash,
    inserts '-' when hash is even (and col > 1), appends '.<suffix>'.
    """
    if not nodes or levels <= 0 or width <= 0:
        return []

    node_count = len(nodes)

    # 'step' mirrors the binary's scan of offset+16 on each 32-byte node struct.
    # offset+16 in MSVC std::string is the SIZE field, so the scan finds the
    # maximum string length. step = max_len + 1.
    step = max(len(w) for w in nodes) + 1

    results: list[str] = []
    h = u32(seed)

    for level in range(1, levels + 1):
        h = u32(level + step * h)
        parts: list[str] = []

        for col in range(1, width + 1):
            # a2 += (a2 / v19) ^ (a2 % ((unsigned int)v13 * v16 * v19))
            # Operator precedence: division and modulo bind before XOR.
            # u32 wraps only the final addition result.
            mod = u32(node_count * level * col)
            h = u32(h + ((h // col) ^ (h % mod)))

            if col != 1 and (h & 1) == 0:
                parts.append("-")

            parts.append(nodes[h % node_count])

        parts.append(".")
        parts.append(suffix)
        results.append("".join(parts))

    return results


# ---------------------------------------------------------------------------
# sub_14000BF00 — top-level entry point
# ---------------------------------------------------------------------------

def generate_domains(
    seed: int     = SEED,
    levels: int   = LEVELS,
    width: int    = WIDTH,
    tld: str      = TLD,
    wordlist: str = WORDLIST_RAW,
) -> list[str]:
    """
    Replicates sub_14000BF00 — the full DGA bootstrap.

    Parses the hardcoded word list, runs the hash-driven generator, and
    returns the list of generated domain names.

    Args:
        seed:     Initial hash value (default: 0x1AED as hardcoded in binary).
        levels:   Number of domains to generate (default: 1000).
        width:    Word selections per domain (default: 3).
        tld:      Domain suffix / TLD (default: 'com').
        wordlist: Comma-separated word pool (default: hardcoded binary string).

    Returns:
        List of generated domain name strings.
    """
    nodes = parse_wordlist(wordlist)
    return _generate_paths(seed, levels, width, nodes, tld)


# ---------------------------------------------------------------------------
# Demo
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    domains = generate_domains()

    print(f"Generated {len(domains)} domains (seed=0x{SEED:04X}, width={WIDTH}, tld={TLD!r})")
    print(f"Word pool: {len(parse_wordlist(WORDLIST_RAW))} words")
    print(f"Step (max_word_len+1): {max(len(w) for w in parse_wordlist(WORDLIST_RAW)) + 1}\n")

    print("First 10 domains:")
    for i, d in enumerate(domains[:10], 1):
        print(f"  {i:>4}. {d}")
    print(f"\nLast 5 domains:")
    for i, d in enumerate(domains[-5:], len(domains) - 4):
        print(f"  {i:>4}. {d}")

    # Known-correct spot checks from the binary
    assert domains[0] == "lensclouddisc.xyz",      f"Domain 1 mismatch: {domains[0]}"
    assert domains[1] == "drift-joint-axis.xyz",   f"Domain 2 mismatch: {domains[1]}"
    assert domains[2] == "flow-pull-edge.xyz",     f"Domain 3 mismatch: {domains[2]}"
    print("\nSpot-check assertions passed.")

    assert generate_domains() == domains
    print("Determinism check passed.")
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment