❌

Normal view

Detailed analysis of a sophisticated firefox extension malware found in the wild using browser-xpi-malware-scanner.py

I've written a scanner for XPI browser extension files which analyzes a browser extension for malicious content. It will print everything that is suspicious or could be used for something malicious so that you will know if and where you can begin with your malware analysis. Example output of a Firefox malware extension (which is live on firefox extensions store)

browser-xpi-malware-scanner.py - Python script for XPI malware scanning on github.com

I have written the above script, and I ran it against 15~ random extensions from the store with less than 10K downloads, and it didn't take me more than 10 minutes to find the malware extension above.

I've also completely reverse engineered the extension to find out exactly what it does, and written an article about it where I walk you through the code and exploitation process steb-by-step, showing all the techniques used to hide from the verification processes in the extension store, breaking out of the sandbox and stealing credentials with a full Command and control server controlling it.

The malware code is very sophisticated. The payload never touches the DOM. It never appears in network DevTools as a suspicious request. It is stored in extension localStorage where casual inspection won't find it. But my scanner will catch it.

Techniques used:

  • Steganographic Payload in PNG Icon
  • Unicode Low-Byte Encoding Trick
  • Decoded Payload: The C2 String Table
  • 72-Hour Sleeper with Random Sampling
  • C2 Beacon via Another PNG File
  • Dynamic `declarativeNetRequest` Rule Injection
  • Affiliate Commission Hijacking
  • Content Script Privilege Escalation Bridge
  • Arbitrary URL Redirect on Any Domain
  • CSP Erasure

Full deep dive analysis with code examples in link above. The extension discussed is live as of today.

Deep dive of malware found on firefox extension store - multiple evasion techniques used including steganography, sleep before C2 beacon and content script privilege escalation. browser-xpi-malware-scanner.py - Python script for XPI malware scanning on github.com

I hope you enjoy it!

Here is the output of the python script, which helps us analyze the code.

```bash browser-xpi-malware-scanner.py ../malware-extensions/YTMP4\ -\ Download\ YouTube\ Videos\ to\ MP4.xpi -v [i] Analyzing 1 target(s) with minimum severity 'INFO' [+] Found 1 XPI(s) to analyze [i] Analyzing XPI: ../malware-extensions/YTMP4 - Download YouTube Videos to MP4.xpi Analyzing entry: setting.html Analyzing entry: manifest.json Analyzing entry: adpoint.json Analyzing entry: index.html Analyzing entry: _locales/en/messages.json Analyzing entry: icon/icon_gray.png Analyzing entry: icon/loading.webp Analyzing entry: icon/logo.png Analyzing entry: icon/icon64.png Analyzing entry: icon/loading.gif Analyzing entry: css/index.css Analyzing entry: css/iconfont.ttf Analyzing entry: css/iconfont.css Analyzing entry: js/index.js Analyzing entry: js/setting.js Analyzing entry: js/y2meta-uk.com.js Analyzing entry: js/content.js Analyzing entry: js/bg.js Analyzing entry: js/jquery-3.4.1.min.js Analyzing entry: js/snapany.com.js Analyzing entry: js/ytmp4.co.za.js Analyzing entry: META-INF/cose.manifest Analyzing entry: META-INF/cose.sig Analyzing entry: META-INF/manifest.mf Analyzing entry: META-INF/mozilla.sf Analyzing entry: META-INF/mozilla.rsa

════════════════════════════════════════════════════════════════════════ XPI ANALYZER β€” YTMP4 - Download YouTube Videos to MP4.xpi ════════════════════════════════════════════════════════════════════════ Extension Name: YTMP4 - Download YouTube Videos to MP4 Extension UUID: 1efab3c2-06ac-4040-975d-e006baac07ce@ytmp4 Overall verdict: CRITICAL RISK

────────────────────────────────────────────────────────────────────── MANIFEST.JSON: ────────────────────────────────────────────────────────────────────── { "manifestversion": 3, "name": "MSG_extName", "description": "MSG_description_", "version": "1.3.4", "default_locale": "en", "permissions": [ "tabs", "storage", "declarativeNetRequest", "downloads" ], "host_permissions": [ "<all_urls>" ], "action": { "default_icon": { "19": "icon/icon_gray.png", "38": "icon/icon_gray.png" }, "default_title": "YTMP4" }, "background": { "scripts": [ "js/bg.js" ] }, "content_scripts": [ { "js": [ "js/content.js" ], "matches": [ "https:///", "http:///" ], "all_frames": true, "run_at": "document_end" }, { "js": [ "js/jquery-3.4.1.min.js", "js/ytmp4.co.za.js" ], "matches": [ "https://.ytmp4.co.za/" ], "all_frames": true, "run_at": "document_start" }, { "js": [ "js/jquery-3.4.1.min.js", "js/y2meta-uk.com.js" ], "matches": [ "https://.y2meta-uk.com/" ], "all_frames": true, "run_at": "document_start" }, { "js": [ "js/jquery-3.4.1.min.js", "js/snapany.com.js" ], "matches": [ "https://.snapany.com/" ], "all_frames": true, "run_at": "document_start" } ], "sidebar_action": { "default_panel": "index.html", "default_icon": "icon/icon64.png" }, "icons": { "128": "icon/icon64.png" }, "declarative_net_request": { "rule_resources": [ { "id": "adblocker01", "enabled": true, "path": "adpoint.json" } ] }, "browser_specific_settings": { "gecko": { "id": "1efab3c2-06ac-4040-975d-e006baac07ce@ytmp4" } } } ──────────────────────────────────────────────────────────────────────

Findings: 1 CRITICAL 22 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 '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 ] [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 ] [JS_OBFUSCATION] js/content.js:380 atob() β€” decoding base64 at runtime (possible payload decode) CODE: '); fileTip = atob(contentPool[screenValues]).replace(image Context: if (contentPool && contentPool[screenValues]) { var image$1 = new RegExp(pageArr.buffer$1[37], 'g'); fileTip = atob(contentPool[screenValues]).replace(image$1, ''); dataExt = JSON.parse(fileTip); screenValues = dataExt.map [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, " Context: function reContentAll(dataExt) { 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, ""); Context: function reContentAll(dataExt) { return 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 Context: dataExt += updImgOn(contentPool); }); return atob(dataExt); }

[HIGH ] [JS_OBFUSCATION] js/snapany.com.js:126 decodeURIComponent(escape()) β€” encoding trick to bypass scanners CODE: return decodeURIComponent(escape(i.bin.bytesToString(e))) Context: }, bytesToString: function(e) { 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); Context: try { let t = e.replace(/\s/g, "") , a = window.atob(t) , s = new Uint8Array(a.length); for (let e = 0; e < a.length; e++) [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 Context: "key": "063126d939ad67595c7721db791df64926ccd9e1", "quality": "144", "thumbnail": "https://i.ytimg.com/vi_webp/uU1YatflISg/maxresdefault.webp", "thumbnail_formats": [ { [HIGH ] [SUSPICIOUS_URL] js/index.js:328 External domain contact: media.savetube.me URL: https://media.savetube.me Context: "label": "Thumbnail", "quality": "Thumbnail", "url": "https://media.savetube.me/media-downloader?url=https%3A//i.ytimg.com/vi_webp/uU1YatflISg/maxresdefault.webp&ext=jpg", "value": "Thumbnail"

[HIGH ] [SUSPICIOUS_URL] js/index.js:389 External domain contact: cdn305.savetube.su URL: https://cdn305.savetube.su Context: "label": "144p", "quality": 144, "url": "https://cdn305.savetube.su/download-direct/video/144/063126d939ad67595c7721db791df64926ccd9e1", "width": 256 } [HIGH ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:35 External domain contact: y2meta-uk.com URL: https://y2meta-uk.com Context: count = 0; switch (d.action){ case 'CONVERT_BEGIN': //mainframe https://y2meta-uk.com/convert/ detectSubIframe(d.yt,'CONVERT_START'); break; [HIGH ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:38 External domain contact: iframe.y2meta-uk.com URL: https://iframe.y2meta-uk.com Context: detectSubIframe(d.yt,'CONVERT_START'); break; case 'CONVERT_START': //subframe https://iframe.y2meta-uk.com/mainindex.php?videoId= convertStart(d.yt); break; [HIGH ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:41 External domain contact: y2meta-uk.com URL: https://y2meta-uk.com Context: convertStart(d.yt); break; case 'GET_DOWNLOAD_DATA': //mainframe https://y2meta-uk.com/convert/ detectSubIframe(d.yt,'GET_DOWNLOAD_DATA_SUBFRAME'); break; [HIGH ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:44 External domain contact: iframe.y2meta-uk.com URL: https://iframe.y2meta-uk.com Context: detectSubIframe(d.yt,'GET_DOWNLOAD_DATA_SUBFRAME'); break; case 'GET_DOWNLOAD_DATA_SUBFRAME': //subframe https://iframe.y2meta-uk.com/mainindex.php?videoId= var e = d.yt, formData = new URLSearchParams(); [HIGH ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:60 External domain contact: api.mp3youtube.cc URL: https://api.mp3youtube.cc Context: try { var t = await getkey(); var n = await fetch('https://api.mp3youtube.cc/v2/converter', { method: "POST", [HIGH ] [SUSPICIOUS_URL] js/y2meta-uk.com.js:132 External domain contact: api.mp3youtube.cc URL: https://api.mp3youtube.cc Context: async function getkey() { let e = await fetch("https://api.mp3youtube.cc/v2/sanity/key") , t = await e.json(); return t.key [HIGH ] [SUSPICIOUS_URL] js/content.js:866 External domain contact: vuejs.org URL: https://vuejs.org Context: warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ); [HIGH ] [SUSPICIOUS_URL] js/snapany.com.js:65 External domain contact: api.snapany.com URL: https://api.snapany.com Context: let v, a, f; f = getGfooter(e); v = await fetch("https://api.snapany.com/v1/extract",{ method: "POST", headers: { [HIGH ] [SUSPICIOUS_URL] js/ytmp4.co.za.js:135 External domain contact: media.savetube.vip URL: https://media.savetube.vip Context: async function getRandomCdn() { let e = await fetch("https://media.savetube.vip/api/random-cdn") , t = await e.json(); return t.cdn ── MEDIUM ──────────────────────────────────────────────────────────── [MEDIUM ] [JS_OBFUSCATION] js/index.js:73 fetch() call β€” verify destination is legitimate CODE: odeName); !val && fetch(logo.src) .then(defaultTip => default Context: var val = await localGet(nodeName); !val && fetch(logo.src) .then(defaultTip => defaultTip.text()) .then((textTag) => { [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' Context: try { var t = await getkey(); var n = await fetch('https://api.mp3youtube.cc/v2/converter', { method: "POST", [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 Context: async function getkey() { let e = await fetch("https://api.mp3youtube.cc/v2/sanity/key") , t = await e.json(); return t.key [MEDIUM ] [JS_OBFUSCATION] js/content.js:46 String.fromCharCode β€” character-code obfuscation CODE: ) { return String.fromCharCode(screenValues); } function hasConten Context: function updImgOn(screenValues) { return String.fromCharCode(screenValues); }

[MEDIUM ] [JS_OBFUSCATION] js/content.js:50 fetch() call β€” verify destination is legitimate CODE: tPool, dataExt) { fetch(contentPool).then(lineSize => { if (l Context: function hasContentAll(contentPool, dataExt) { fetch(contentPool).then(lineSize => { if (lineSize.ok) lineSize.text().then(event$1 => dataExt(1, event$1)) else dataExt(0) [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 Context: /! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */ !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQ [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 Context: /*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */ !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQ [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>… Context: /! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license / !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQ [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",… Context: /! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license / !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQ [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"… Context: /! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */ !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQ [MEDIUM ] [JS_OBFUSCATION] js/snapany.com.js:137 String.fromCharCode β€” character-code obfuscation CODE: i.push(String.fromCharCode(e[t])); return i.j Context: bytesToString: function(e) { for (var i = [], t = 0; t < e.length; t++) i.push(String.fromCharCode(e[t])); return i.join("") } [MEDIUM ] [JS_OBFUSCATION] js/snapany.com.js:123 unescape() β€” URL-encoding obfuscation CODE: i.bin.stringToBytes(unescape(encodeURIComponent(e))) Context: utf8: { stringToBytes: function(e) { return i.bin.stringToBytes(unescape(encodeURIComponent(e))) }, bytesToString: function(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",{ Context: let v, a, f; f = getGfooter(e); v = await fetch("https://api.snapany.com/v1/extract",{ method: "POST", headers: { [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 Context: async function getRandomCdn() { let e = await fetch("https://media.savetube.vip/api/random-cdn") , t = await e.json(); return t.cdn [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 Context: async function fetchData(e) { let v, a, s, t = await getRandomCdn(); v = await fetch("https://".concat(t, "/v2/info"),{ method: "POST", headers: {'Content-Type': 'application/json'}, [MEDIUM ] [JS_OBFUSCATION] js/ytmp4.co.za.js:165 fetch() call β€” verify destination is legitimate CODE: try { v = await fetch("https://".concat(l, "/download"), { Context: }; try { v = await fetch("https://".concat(l, "/download"), { method: "POST", headers: { [MEDIUM ] [PERMISSION] manifest.json: Dangerous permission: 'downloads' β€” Can initiate and read downloads PERMISSION: permissions: ['tabs', 'storage', 'declarativeNetRequest', 'downloads', '<all_urls>'] ── INFO ────────────────────────────────────────────────────────────── [INFO ] [METADATA] ../malware-extensions/YTMP4 - Download YouTube Videos to MP4.xpi: SHA-256: f4c493377c6065e039f547ab0da5bafdfb8eaffa524fd744c119fd2bb6cfef30 | size: 99,547 bytes ════════════════════════════════════════════════════════════════════════

```

submitted by /u/TitleUpbeat3201
[link] [comments]
❌