- What: A researcher cracked a malvertising DGA used by piracy streaming sites.
- Impact: Users may be exposed to malicious domains.
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.hvi25o...