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)
- Take the current UTC time. Round the hour down to the nearest 3-hour boundary (0, 3, 6, 9, 12, 15, 18, 21).
- Build a date key:
YYYYMMDDHH(e.g.,2026040503). - Concatenate:
{seed}|{date_key}→G25|2026040503. - SHA-256 hash the result.
- Use hash byte 0 to determine total domain length:
15 + (hash[0] % 26). - Use hash byte 1 to determine subdomain length:
1 + (hash[1] % (total - 14)). - Encode the hash with a custom base32 alphabet:
BCEFGHIJKLMNOPQRTUVWXYZ123456789. - 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
Related Infrastructure
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.