mirror of
https://github.com/tiennm99/mv3-detection-markers.git
synced 2026-10-03 04:54:50 +00:00
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.
This commit is contained in:
commit
dc106f9b93
24 files changed
+413
No files matched your search
+12
@@ -0,0 +1,12 @@
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.log
|
||||
*.swp
|
||||
*.tmp
|
||||
.env
|
||||
.env.*
|
||||
node_modules/
|
||||
.idea/
|
||||
.vscode/
|
||||
*.crx
|
||||
*.pem
|
||||
@@ -0,0 +1,47 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 <author>
|
||||
|
||||
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.
|
||||
@@ -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 `<all_urls>` | 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` | `<all_urls>` 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 `<all_urls>` + 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.
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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": ["<all_urls>"],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": ["<all_urls>"],
|
||||
"background": { "service_worker": "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(() => {});
|
||||
});
|
||||
});
|
||||
@@ -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" }
|
||||
}
|
||||
@@ -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(() => {});
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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": ["<all_urls>"],
|
||||
"background": { "service_worker": "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");
|
||||
@@ -0,0 +1,2 @@
|
||||
// MARKER 06 injected payload. Logs marker string only.
|
||||
console.log("MARKER_06_INJECTED_PAYLOAD");
|
||||
@@ -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": ["<all_urls>"],
|
||||
"background": { "service_worker": "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(() => {});
|
||||
});
|
||||
@@ -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 */
|
||||
}
|
||||
});
|
||||
@@ -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": ["<all_urls>"],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": ["<all_urls>"],
|
||||
"action": { "default_title": "Marker 08 - Click to test capture" },
|
||||
"background": { "service_worker": "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)`);
|
||||
});
|
||||
});
|
||||
@@ -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" }
|
||||
}
|
||||
@@ -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");
|
||||
@@ -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": ["<all_urls>"],
|
||||
"background": { "service_worker": "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");
|
||||
Reference in new issue
Block a user