Cracking a Malvertising DGA From the Device Side

When piracy streaming sites inject third-party JavaScript into your browser, the domains hosting that JavaScript are designed to be invisible. They rotate every three hours, use algorithmically generated names on cheap TLDs, and vanish before anyone notices them.

I cracked the algorithm that generates them. Using application-layer traffic from mobile devices, I recovered the full domain generation algorithm (DGA), validated it against every domain observed in the wild, and can now predict every future domain before it's registered.

The Discovery

While analyzing mobile proxy traffic for suspicious SDK behavior, I noticed requests to domains like these:

xybev.9m5kp8e5r687jn1w3t3.cfd
rn3u23u4v4xrux2b.jct83bclvg8c7h91f.cfd
5.kt81r6fbpevjr867571juq7p.cfd
hkcy.hvi25o1wciyxq4el8hn59biuip9.cfd
z7.ux3g1wivcklvno.cfd

Every request shared the same characteristics:

  • Content-Type: application/javascript; charset=utf-8
  • Sec-Fetch-Dest: script
  • Sec-Fetch-Site: cross-site
  • TLD: .cfd

These domains were being loaded as <script> tags, injected cross-site into piracy streaming pages. Over 14 days, I observed 20 unique .cfd domains generating 500 total requests across two independent mobile users.

The URL Structure

Every request followed the same pattern:

https://{sub}.{parent}.cfd/k{random_chars}/{campaign_id}

Three elements stood out:

Double-DGA naming. Both the subdomain and the parent domain are algorithmically generated. Most DGAs rotate only the second-level domain. Here, both components are random strings, making pattern-based blocking harder.

The /k prefix. Every URL path begins with /k, followed by a random string of 10–24 characters that changes with every request — defeating URL-based caching and deduplication.

Campaign IDs. The final path segment is a stable 5-character identifier:

Campaign ID Referrer User
VvMrO stream.sanction.tv User 2 (Chrome iOS)
aOqBk hurawatch.cc User 2 (Chrome iOS)
AvjBB (not captured) User 1 (Safari)

The campaign ID changes based on which piracy site injected the script. The operator is tracking impressions per distribution partner.

The Injection Chain

Referer headers revealed the full chain:

User visits hurawatch.cc or cybermovies.net
  → Page loads embed from stream.sanction.tv
    → Embed injects <script src="https://{dga}.cfd/k{rnd}/{campaign}">
      → Browser executes JavaScript

The injection site actively prevents inspection. It loads disable-devtool, an open-source anti-debugging library that redirects the browser to a 404 page whenever DevTools is opened:

<script src='https://unpkg.com/disable-devtool@0.3.9/disable-devtool.min.js'>
</script>
<script>
    DisableDevtool({
        clearLog: true,
        disableSelect: true,
        disableCopy: true,
        disableCut: true,
        disablePaste: true
    })
</script>

This blocks browser-based analysis but doesn't block network-level capture or fetching the page source via curl.

Recovering the Algorithm

The DGA implementation was found inline in the HTML source of stream.sanction.tv's embed pages.

The Obfuscated Config

The domain generation parameters are stored in an obfuscated string, decoded at runtime by a character substitution cipher. The key is split in half: the second half serves as a lookup table, the first half provides the replacements. After decoding:

{
  "s": {
    "t1": ".cfd",
    "t1s": "G25",
    "t2": ".rest",
    "t2s": "G26",
    "d": ".cyou",
    "ds": "G27"
  },
  "l": "/k{rnd}/VvMrO"
}

Three TLDs are configured, each with a unique seed:

Purpose TLD Seed
Primary .cfd G25
Fallback .rest G26
Pop-under .cyou G27

If the primary .cfd domain is unreachable, the script retries on .rest. The .cyou domain is used for pop-under ads, opened on the first mouse click.

Domain Generation (function y)

  1. Take the current UTC time. Round the hour down to the nearest 3-hour boundary (0, 3, 6, 9, 12, 15, 18, 21).
  2. Build a date key: YYYYMMDDHH (e.g., 2026040503).
  3. Concatenate: {seed}|{date_key}G25|2026040503.
  4. SHA-256 hash the result.
  5. Use hash byte 0 to determine total domain length: 15 + (hash[0] % 26).
  6. Use hash byte 1 to determine subdomain length: 1 + (hash[1] % (total - 14)).
  7. Encode the hash with a custom base32 alphabet: BCEFGHIJKLMNOPQRTUVWXYZ123456789.
  8. Split into {subdomain}.{parent}, append TLD.

Domains rotate every 3 hours. Eight new domains per day, per TLD.

Validation

I reimplemented the algorithm in Python and tested it against every .cfd domain observed in the traffic data.

Timestamp (UTC) Observed Domain Generated Domain
2026-03-30 17:43 xybev.9m5kp8e5r687jn1w3t3 xybev.9m5kp8e5r687jn1w3t3
2026-03-30 18:24 rn3u23u4v4xrux2b.jct83bclvg8c7h91f rn3u23u4v4xrux2b.jct83bclvg8c7h91f
2026-03-31 11:22 y8pfwyeg66nll6w.o7lf1m1fl1ki6fel4e1 y8pfwyeg66nll6w.o7lf1m1fl1ki6fel4e1
2026-03-31 12:05 yqzmum3gh7lh8.ftvrm39rox222hj1y yqzmum3gh7lh8.ftvrm39rox222hj1y
2026-04-04 03:16 5.kt81r6fbpevjr867571juq7p 5.kt81r6fbpevjr867571juq7p
2026-04-04 06:17 w3h.miu8ekuryezmw8 w3h.miu8ekuryezmw8
2026-04-04 16:47 8368r.bj1igpq93k1yfw145yk7j3 8368r.bj1igpq93k1yfw145yk7j3
2026-04-04 18:22 hkcy.hvi25o1wciyxq4el8hn59biuip9 hkcy.hvi25o1wciyxq4el8hn59biuip9
2026-04-05 00:46 xhb8o6.z32zl64equighu xhb8o6.z32zl64equighu
2026-04-05 03:00 z7.ux3g1wivcklvno z7.ux3g1wivcklvno
2026-04-05 16:37 z.22i5m1khi17ki6 z.22i5m1khi17ki6
2026-04-06 02:38 p9qnr.cn79266rjxwvnbgtf p9qnr.cn79266rjxwvnbgtf

12 out of 12 domains matched.

Predicting Future Domains

With the algorithm and seeds in hand, generating domains for any future time window is trivial:

2026-04-06 12:00 UTC → vyxzb4.6e4hoe355t17yt2vm2uxlwbrxyrr.cfd
2026-04-06 15:00 UTC → 4j.j4qxryhfbz51nk.cfd
2026-04-06 18:00 UTC → qoc.j4wgztotkr76wrqfow79rwy.cfd
2026-04-06 21:00 UTC → yw.i4jxgewgl3rfgmgguhy899n8klpnn.cfd
2026-04-07 00:00 UTC → 7.ncqtxzmuffxiwk.cfd
2026-04-07 03:00 UTC → m2yzcfnq.z2ny961rqc3qiig.cfd
2026-04-07 06:00 UTC → 8mhb.2j9xi6ztt6vr4fo7477.cfd
2026-04-07 09:00 UTC → vkv9.fowky5txtxfixyr6bj1xot7xvkz.cfd

The same applies to .rest and .cyou domains using seeds G26 and G27. These domains can be preemptively blocked or sinkholed before the operator deploys them.

IOCs

Seeds and Parameters

Primary:   seed=G25, TLD=.cfd, rotation=3h
Fallback:  seed=G26, TLD=.rest, rotation=3h
Pop-under: seed=G27, TLD=.cyou, rotation=3h

Base32 Alphabet

BCEFGHIJKLMNOPQRTUVWXYZ123456789

Injection Sources

stream.sanction.tv
hurawatch.cc
cybermovies.net

Campaign IDs

VvMrO (stream.sanction.tv)
aOqBk (hurawatch.cc)
AvjBB (unidentified source)

Observed .cfd Domains (March 28 – April 6, 2026)

z7.ux3g1wivcklvno.cfd
5.kt81r6fbpevjr867571juq7p.cfd
xhb8o6.z32zl64equighu.cfd
8368r.bj1igpq93k1yfw145yk7j3.cfd
rn3u23u4v4xrux2b.jct83bclvg8c7h91f.cfd
lubqzhp4qubvg2p.6epv2lqboiwp3b5zrgcm39zj.cfd
hkcy.hvi25o1wciyxq4el8hn59biuip9.cfd
y8pfwyeg66nll6w.o7lf1m1fl1ki6fel4e1.cfd
w3h.miu8ekuryezmw8.cfd
fv.x337cc1f9rxk9ff.cfd
xybev.9m5kp8e5r687jn1w3t3.cfd
yqzmum3gh7lh8.ftvrm39rox222hj1y.cfd
p9qnr.cn79266rjxwvnbgtf.cfd
fq.f5jmetugzxrkhk.cfd
z.22i5m1khi17ki6.cfd
82q4.fr3q7i2p5pxv5o1xib4egio.cfd
ra.sojabern.cfd
assis.retenuegerbilamen.cfd
d6.tbfiles.cfd
cdn1.tlfiles.cfd
cloudnestra.com
wanderlynest.com (heartbeat: tmstr3.wanderlynest.com)
neonhorizonworkshops.com (heartbeat: tmstr3.neonhorizonworkshops.com)

Python Implementation

import hashlib
from datetime import datetime, timezone

ALPHABET = 'BCEFGHIJKLMNOPQRTUVWXYZ123456789'

def custom_base32(data):
    r, n, e = 0, '', 0
    for byte in data:
        e = (e << 8) | byte
        r += 8
        while r >= 5:
            n += ALPHABET[(e >> (r - 5)) & 31]
            r -= 5
    if r:
        n += ALPHABET[(e << (5 - r)) & 31]
    return n.lower()

def generate_domain(seed, now_utc, tld, offset=3):
    rounded_hour = (now_utc.hour // offset) * offset
    date_key = (f"{now_utc.year}"
                f"{str(now_utc.month).zfill(2)}"
                f"{str(now_utc.day).zfill(2)}"
                f"{str(rounded_hour).zfill(2)}")
    h = hashlib.sha256(f"{seed}|{date_key}".encode()).digest()
    total = 15 + (h[0] % 26)
    sub = 1 + (h[1] % (total - 14))
    enc = custom_base32(h)
    return f"{enc[:sub]}.{enc[sub:total]}{tld}"

now = datetime.now(timezone.utc)
print(generate_domain("G25", now, ".cfd"))
print(generate_domain("G26", now, ".rest"))
print(generate_domain("G27", now, ".cyou"))

Methodology

Traffic was captured from a consented panel of US mobile users via application-layer proxy. No systems were accessed without authorization. The DGA source code was recovered from publicly accessible web pages using standard HTTP requests. No individual user data is disclosed.

Subscribe to Buchodi's Threat Intel

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe