commit dc106f9b930ecbb8092802ea2d66ab54e478bff5 Author: tiennm99 Date: Sat May 16 07:54:43 2026 +0700 feat: initial MV3 detection markers Nine benign Chrome MV3 detection markers for validating browser extension security scanners. Each marker exercises one suspicious-behavior signature without performing real data capture or exfiltration. Educational and defensive-testing use only. See LICENSE. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eef1f7a --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +Thumbs.db +*.log +*.swp +*.tmp +.env +.env.* +node_modules/ +.idea/ +.vscode/ +*.crx +*.pem diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..06358d3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,47 @@ +MIT License + +Copyright (c) 2026 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +------------------------------------------------------------------------------ +EDUCATIONAL-USE NOTICE +------------------------------------------------------------------------------ + +This Software is provided for EDUCATIONAL and DEFENSIVE SECURITY TESTING +purposes only. The benign detection markers contained herein are designed to +validate browser-extension security scanners and contain no working +credential-theft, exfiltration, or surveillance logic. + +By using the Software you agree: + +1. You will only load these markers in browser profiles, devices, and networks + that you own, or for which you have explicit written authorization to test. +2. You will not use the Software to harass, deceive, defraud, surveil, or + cause harm to any person or system. +3. You are solely responsible for ensuring your use of the Software complies + with all applicable laws, regulations, employer policies, and the terms of + service of any browser, platform, or service involved. +4. You assume all risk associated with the Software, including but not limited + to data loss, account suspension, EDR/AV quarantine actions, and any other + adverse consequence. + +------------------------------------------------------------------------------ +DISCLAIMER +------------------------------------------------------------------------------ + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..effd506 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# MV3 Detection Markers + +> ## ⚠️ Educational Use Only — Use at Your Own Risk +> +> This project provides **benign** Chrome MV3 extensions ("detection markers") for validating browser-extension security scanners. It is the EICAR-equivalent pattern for browser-extension detection. +> +> - **Educational and defensive-testing purpose only.** Do not use for any unlawful activity. +> - **No working credential-theft logic.** Markers exercise suspicious *manifest patterns* and *API call shapes*. They do **not** read or transmit real keystrokes, password values, cookie values, or clipboard contents. +> - **Only load on systems and in browser profiles you own or have written authorization to test.** Do not deploy against third parties. +> - **You assume all risk.** The authors and contributors accept no liability for misuse, data loss, or any consequence of running this software. See `LICENSE`. +> +> If you cannot agree to the above, do not use this project. + +--- + +Benign Chrome MV3 extensions that trigger specific suspicious-behavior signatures. Used as test cases for enterprise extension scanners. + +**No real user data is captured or exfiltrated.** Each marker exercises one detection category and logs a literal marker string (and optionally POSTs to a local test endpoint). + +--- + +## Test endpoint setup (optional) + +Markers 02 and 03 POST to `http://127.0.0.1:8080/marker`. To capture those POSTs, run any local logger before loading: + +```bash +# Quick netcat logger (echoes raw request) +while true; do nc -l -p 8080 -q 1; done + +# Or a small Python catcher +python3 -c " +from http.server import BaseHTTPRequestHandler, HTTPServer +class H(BaseHTTPRequestHandler): + def do_POST(self): + n = int(self.headers.get('content-length', 0)) + print(self.rfile.read(n).decode(errors='replace')) + self.send_response(204); self.end_headers() +HTTPServer(('127.0.0.1', 8080), H).serve_forever() +" +``` + +If you skip this, the `fetch` calls silently fail and markers still log to the extension's service-worker / content-script console. + +--- + +## Load procedure + +1. Open `chrome://extensions` (Chrome MV3) or `brave://extensions` (Brave). +2. Toggle **Developer mode** (top right). +3. **Load unpacked** → pick one marker folder. +4. Note the generated extension ID. +5. Run your scanner against: + - the folder (static analysis), and + - the loaded extension ID (dynamic / runtime analysis). +6. Record verdict per marker. + +For marker 09 (manifest-delta), load `v1-narrow` first, run scanner, then remove and load `v2-wide`. Scanner should flag the permission expansion as a delta-based signal. + +--- + +## Marker matrix + +| # | Folder | Detection signal | Real-world example | +|---|---|---|---| +| 01 | `01-form-hook` | Content script attaches `keydown` listener to `input[type=password]` on `` | Credential keylogger pattern | +| 02 | `02-cookie-read` | Service worker calls `chrome.cookies.getAll({})` on install | Session theft pattern (Cyberhaven cluster) | +| 03 | `03-beacon-c2` | `chrome.alarms` periodic POST to external host | C2 beaconing | +| 04 | `04-dnr-redirect` | `declarativeNetRequest` static redirect rule | MV3-only attack vector (replaces MV2 webRequest blocking) | +| 05 | `05-broad-perms` | `` host perm + cookies + scripting + webRequest + tabs combo | Manifest-only signal — pure static test | +| 06 | `06-programmatic-inject` | `chrome.scripting.executeScript` on every tab `complete` event | Stealth content-script injection without `content_scripts` declaration | +| 07 | `07-clipboard-read` | `navigator.clipboard.readText()` on Ctrl+V | Clipboard-stealer pattern | +| 08 | `08-tab-capture` | `chrome.tabs.captureVisibleTab` on action click | Screen-grabber pattern | +| 09 | `09-manifest-delta` | v1 narrow scope → v2 widens to `` + cookies + scripting | Update-poisoning pattern (Cyberhaven technique) | + +--- + +## What these markers do NOT do + +- Do not read keystroke values, password contents, cookie values, or clipboard text. +- Do not transmit any data to non-local destinations. +- Do not persist anything to disk or `chrome.storage`. +- Do not modify form behavior, page DOM, or user actions. + +Safe to load in a disposable Brave/Chrome dev profile. A clean VM is still recommended so any false-positive enterprise EDR action doesn't disrupt your daily browser. + +--- + +## Per-category recall scoring + +For each marker, record: + +| Marker | Scanner verdict | Detection rule fired | Notes | +|---|---|---|---| +| 01 | flagged / clean | | | +| 02 | | | | +| ... | | | | + +Anything in the **clean** column → a gap. Cross-reference to the prior detection-category report (`research-260516-0713-malicious-chrome-extension-samples-for-detection-testing.md`, §2) to map the miss to a missing rule type. + +--- + +## Removing markers + +Each is its own extension — remove via `chrome://extensions` → trash icon. No system changes, no registry keys, no leftover files. + +--- + +## Unresolved + +1. **Manifest delta `key` field.** For scanners that require both versions to share an extension ID (i.e., compare by ID), you must inject a stable `key` field into both `manifest.json` files. Generate one with `chrome` packing the v1 folder once and copying the resulting public key. Skipped here because most static scanners diff by name/folder; flag this if your scanner needs ID-stability. +2. **MV2 counterparts** for any of the above markers can be added when you're ready — say the word and I'll generate them alongside. diff --git a/markers/01-form-hook/content.js b/markers/01-form-hook/content.js new file mode 100644 index 0000000..e3846cb --- /dev/null +++ b/markers/01-form-hook/content.js @@ -0,0 +1,23 @@ +// MARKER 01 - Form Hook +// Detection target: keystroke listener attached to password inputs. +// SAFETY: does NOT read event.key or input value. Logs marker string only. + +const MARKER = "MARKER_01_FORM_HOOK"; + +function attachListeners() { + const fields = document.querySelectorAll("input[type=password]"); + if (fields.length === 0) return; + fields.forEach((field) => { + if (field.dataset.markerAttached) return; + field.dataset.markerAttached = "1"; + field.addEventListener("keydown", () => console.log(`${MARKER} keydown`)); + field.addEventListener("input", () => console.log(`${MARKER} input`)); + }); + console.log(`${MARKER} attached to ${fields.length} password field(s)`); +} + +attachListeners(); +new MutationObserver(attachListeners).observe(document.documentElement, { + childList: true, + subtree: true, +}); diff --git a/markers/01-form-hook/manifest.json b/markers/01-form-hook/manifest.json new file mode 100644 index 0000000..9317a92 --- /dev/null +++ b/markers/01-form-hook/manifest.json @@ -0,0 +1,13 @@ +{ + "manifest_version": 3, + "name": "Marker 01 - Form Hook", + "version": "1.0.0", + "description": "Detection marker: content script attaches keystroke listener to password fields. No data captured.", + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"], + "run_at": "document_idle" + } + ] +} diff --git a/markers/02-cookie-read/manifest.json b/markers/02-cookie-read/manifest.json new file mode 100644 index 0000000..ac4955b --- /dev/null +++ b/markers/02-cookie-read/manifest.json @@ -0,0 +1,9 @@ +{ + "manifest_version": 3, + "name": "Marker 02 - Cookie Read", + "version": "1.0.0", + "description": "Detection marker: enumerates cookies via chrome.cookies API. No cookie values transmitted.", + "permissions": ["cookies"], + "host_permissions": [""], + "background": { "service_worker": "service-worker.js" } +} diff --git a/markers/02-cookie-read/service-worker.js b/markers/02-cookie-read/service-worker.js new file mode 100644 index 0000000..cdab410 --- /dev/null +++ b/markers/02-cookie-read/service-worker.js @@ -0,0 +1,17 @@ +// MARKER 02 - Cookie Read +// Detection target: chrome.cookies.getAll() enumeration from service worker. +// SAFETY: counts cookies, logs count, POSTs count-only to local test endpoint. +// Cookie values are NOT transmitted or persisted. + +const MARKER = "MARKER_02_COOKIE_READ"; + +chrome.runtime.onInstalled.addListener(() => { + chrome.cookies.getAll({}, (cookies) => { + const count = cookies.length; + console.log(`${MARKER} enumerated ${count} cookies (values discarded)`); + fetch("http://127.0.0.1:8080/marker", { + method: "POST", + body: `${MARKER}_COUNT=${count}`, + }).catch(() => {}); + }); +}); diff --git a/markers/03-beacon-c2/manifest.json b/markers/03-beacon-c2/manifest.json new file mode 100644 index 0000000..34ad15e --- /dev/null +++ b/markers/03-beacon-c2/manifest.json @@ -0,0 +1,9 @@ +{ + "manifest_version": 3, + "name": "Marker 03 - Beacon C2", + "version": "1.0.0", + "description": "Detection marker: periodic beacon via chrome.alarms to local test endpoint.", + "permissions": ["alarms"], + "host_permissions": ["http://127.0.0.1/*"], + "background": { "service_worker": "service-worker.js" } +} diff --git a/markers/03-beacon-c2/service-worker.js b/markers/03-beacon-c2/service-worker.js new file mode 100644 index 0000000..97c5c82 --- /dev/null +++ b/markers/03-beacon-c2/service-worker.js @@ -0,0 +1,19 @@ +// MARKER 03 - Beacon C2 +// Detection target: service worker schedules periodic external POST via chrome.alarms. +// SAFETY: body is the literal marker string. No user/system data is read or sent. + +const MARKER = "MARKER_03_BEACON_C2"; + +chrome.runtime.onInstalled.addListener(() => { + chrome.alarms.create("marker-beacon", { periodInMinutes: 1 }); + console.log(`${MARKER} alarm scheduled`); +}); + +chrome.alarms.onAlarm.addListener((alarm) => { + if (alarm.name !== "marker-beacon") return; + console.log(`${MARKER} beacon fired`); + fetch("http://127.0.0.1:8080/marker", { + method: "POST", + body: MARKER, + }).catch(() => {}); +}); diff --git a/markers/04-dnr-redirect/manifest.json b/markers/04-dnr-redirect/manifest.json new file mode 100644 index 0000000..2c15e1d --- /dev/null +++ b/markers/04-dnr-redirect/manifest.json @@ -0,0 +1,17 @@ +{ + "manifest_version": 3, + "name": "Marker 04 - DNR Redirect", + "version": "1.0.0", + "description": "Detection marker: declarativeNetRequest redirect rule for an invalid TLD.", + "permissions": ["declarativeNetRequest"], + "host_permissions": ["*://marker-test.invalid/*"], + "declarative_net_request": { + "rule_resources": [ + { + "id": "marker-rules", + "enabled": true, + "path": "rules.json" + } + ] + } +} diff --git a/markers/04-dnr-redirect/rules.json b/markers/04-dnr-redirect/rules.json new file mode 100644 index 0000000..076b9f0 --- /dev/null +++ b/markers/04-dnr-redirect/rules.json @@ -0,0 +1,14 @@ +[ + { + "id": 1, + "priority": 1, + "action": { + "type": "redirect", + "redirect": { "url": "https://example.com/marker-04-dnr-redirect" } + }, + "condition": { + "urlFilter": "marker-test.invalid", + "resourceTypes": ["main_frame"] + } + } +] diff --git a/markers/05-broad-perms/manifest.json b/markers/05-broad-perms/manifest.json new file mode 100644 index 0000000..b106ce6 --- /dev/null +++ b/markers/05-broad-perms/manifest.json @@ -0,0 +1,17 @@ +{ + "manifest_version": 3, + "name": "Marker 05 - Broad Permissions", + "version": "1.0.0", + "description": "Detection marker: oversized permission combo. No behavior beyond a console log.", + "permissions": [ + "cookies", + "scripting", + "webRequest", + "storage", + "tabs", + "alarms", + "clipboardRead" + ], + "host_permissions": [""], + "background": { "service_worker": "service-worker.js" } +} diff --git a/markers/05-broad-perms/service-worker.js b/markers/05-broad-perms/service-worker.js new file mode 100644 index 0000000..efa5ee6 --- /dev/null +++ b/markers/05-broad-perms/service-worker.js @@ -0,0 +1,5 @@ +// MARKER 05 - Broad Permissions +// Detection target: manifest declares oversized permission combo. +// Behavior: none. Pure static-analysis test. + +console.log("MARKER_05_BROAD_PERMS loaded"); diff --git a/markers/06-programmatic-inject/injected.js b/markers/06-programmatic-inject/injected.js new file mode 100644 index 0000000..942c0f0 --- /dev/null +++ b/markers/06-programmatic-inject/injected.js @@ -0,0 +1,2 @@ +// MARKER 06 injected payload. Logs marker string only. +console.log("MARKER_06_INJECTED_PAYLOAD"); diff --git a/markers/06-programmatic-inject/manifest.json b/markers/06-programmatic-inject/manifest.json new file mode 100644 index 0000000..0899ab7 --- /dev/null +++ b/markers/06-programmatic-inject/manifest.json @@ -0,0 +1,9 @@ +{ + "manifest_version": 3, + "name": "Marker 06 - Programmatic Inject", + "version": "1.0.0", + "description": "Detection marker: chrome.scripting.executeScript on every tab complete event.", + "permissions": ["scripting", "tabs"], + "host_permissions": [""], + "background": { "service_worker": "service-worker.js" } +} diff --git a/markers/06-programmatic-inject/service-worker.js b/markers/06-programmatic-inject/service-worker.js new file mode 100644 index 0000000..15d1bbd --- /dev/null +++ b/markers/06-programmatic-inject/service-worker.js @@ -0,0 +1,14 @@ +// MARKER 06 - Programmatic Inject +// Detection target: chrome.scripting.executeScript injection on tab events, +// bypassing the static content_scripts declaration. + +const MARKER = "MARKER_06_PROGRAMMATIC_INJECT"; + +chrome.tabs.onUpdated.addListener((tabId, info, tab) => { + if (info.status !== "complete") return; + if (!tab.url || !tab.url.startsWith("http")) return; + chrome.scripting + .executeScript({ target: { tabId }, files: ["injected.js"] }) + .then(() => console.log(`${MARKER} injected into tab ${tabId}`)) + .catch(() => {}); +}); diff --git a/markers/07-clipboard-read/content.js b/markers/07-clipboard-read/content.js new file mode 100644 index 0000000..c3dcd2d --- /dev/null +++ b/markers/07-clipboard-read/content.js @@ -0,0 +1,15 @@ +// MARKER 07 - Clipboard Read +// Detection target: navigator.clipboard.readText() triggered by user input. +// SAFETY: discards clipboard content; logs length only. + +const MARKER = "MARKER_07_CLIPBOARD_READ"; + +document.addEventListener("keydown", async (e) => { + if (!(e.ctrlKey && e.key === "v")) return; + try { + const text = await navigator.clipboard.readText(); + console.log(`${MARKER} length=${text.length} (content discarded)`); + } catch { + /* permission may not be granted yet */ + } +}); diff --git a/markers/07-clipboard-read/manifest.json b/markers/07-clipboard-read/manifest.json new file mode 100644 index 0000000..dcc5181 --- /dev/null +++ b/markers/07-clipboard-read/manifest.json @@ -0,0 +1,14 @@ +{ + "manifest_version": 3, + "name": "Marker 07 - Clipboard Read", + "version": "1.0.0", + "description": "Detection marker: reads clipboard on Ctrl+V. Content discarded.", + "permissions": ["clipboardRead"], + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"], + "run_at": "document_idle" + } + ] +} diff --git a/markers/08-tab-capture/manifest.json b/markers/08-tab-capture/manifest.json new file mode 100644 index 0000000..3b60c75 --- /dev/null +++ b/markers/08-tab-capture/manifest.json @@ -0,0 +1,10 @@ +{ + "manifest_version": 3, + "name": "Marker 08 - Tab Capture", + "version": "1.0.0", + "description": "Detection marker: chrome.tabs.captureVisibleTab on action click. Capture discarded.", + "permissions": ["activeTab"], + "host_permissions": [""], + "action": { "default_title": "Marker 08 - Click to test capture" }, + "background": { "service_worker": "service-worker.js" } +} diff --git a/markers/08-tab-capture/service-worker.js b/markers/08-tab-capture/service-worker.js new file mode 100644 index 0000000..6bb828c --- /dev/null +++ b/markers/08-tab-capture/service-worker.js @@ -0,0 +1,12 @@ +// MARKER 08 - Tab Capture +// Detection target: chrome.tabs.captureVisibleTab. +// SAFETY: capture data URL is discarded immediately, never transmitted. + +const MARKER = "MARKER_08_TAB_CAPTURE"; + +chrome.action.onClicked.addListener(() => { + chrome.tabs.captureVisibleTab({ format: "png" }, (dataUrl) => { + if (!dataUrl) return; + console.log(`${MARKER} captured tab, size=${dataUrl.length} (discarded)`); + }); +}); diff --git a/markers/09-manifest-delta/v1-narrow/manifest.json b/markers/09-manifest-delta/v1-narrow/manifest.json new file mode 100644 index 0000000..2d23a27 --- /dev/null +++ b/markers/09-manifest-delta/v1-narrow/manifest.json @@ -0,0 +1,9 @@ +{ + "manifest_version": 3, + "name": "Marker 09 - Manifest Delta", + "version": "1.0.0", + "description": "Detection marker v1: narrow scope (example.com only, storage only).", + "permissions": ["storage"], + "host_permissions": ["https://example.com/*"], + "background": { "service_worker": "service-worker.js" } +} diff --git a/markers/09-manifest-delta/v1-narrow/service-worker.js b/markers/09-manifest-delta/v1-narrow/service-worker.js new file mode 100644 index 0000000..5f328df --- /dev/null +++ b/markers/09-manifest-delta/v1-narrow/service-worker.js @@ -0,0 +1,3 @@ +// MARKER 09 v1 - Narrow Scope +// Baseline manifest. Load first; run scanner; verdict should be clean / low risk. +console.log("MARKER_09_V1_NARROW loaded"); diff --git a/markers/09-manifest-delta/v2-wide/manifest.json b/markers/09-manifest-delta/v2-wide/manifest.json new file mode 100644 index 0000000..f99c8f7 --- /dev/null +++ b/markers/09-manifest-delta/v2-wide/manifest.json @@ -0,0 +1,9 @@ +{ + "manifest_version": 3, + "name": "Marker 09 - Manifest Delta", + "version": "2.0.0", + "description": "Detection marker v2: widened scope (simulates post-update compromise).", + "permissions": ["storage", "cookies", "scripting", "webRequest", "tabs"], + "host_permissions": [""], + "background": { "service_worker": "service-worker.js" } +} diff --git a/markers/09-manifest-delta/v2-wide/service-worker.js b/markers/09-manifest-delta/v2-wide/service-worker.js new file mode 100644 index 0000000..ab6f8ea --- /dev/null +++ b/markers/09-manifest-delta/v2-wide/service-worker.js @@ -0,0 +1,3 @@ +// MARKER 09 v2 - Wide Scope +// Compromise-update manifest. Load after v1; scanner should flag the permission delta. +console.log("MARKER_09_V2_WIDE loaded");