The Phantom Extension: Backdooring chrome through uncharted pathways

Written by Riadh Bouchahoua - 23/09/2025 - in Pentest - Download

The increasing hardening of traditional Windows components such as LSASS has pushed attackers to explore alternative entry points. Among these, web browsers have emerged as highly valuable targets since they are now the primary gateway to sensitive data and enterprise cloud services. Numerous secrets, including tokens and credentials, flows through browsers, and their compromise can provide attackers with extensive access across an organization. This article presents a little-known technique for compromising Chromium-based browsers within Windows domains by forcing the loading of arbitrary extensions. When successfully applied, this method results in complete browser compromise.

Looking to improve your skills? Discover our trainings sessions! Learn more.

Introduction

Chromium extensions are modules that allow users to customize and enhance their browsing experience. Built with the same technologies as the web pages we visit every day (HTML, CSS, and JavaScript), they interact with the browser through a well‑defined set of APIs. Thanks to these mechanisms, extensions can introduce new features, modify the content or behavior of existing web pages, or integrate seamlessly with third‑party services. Their use cases range from productivity tools to ad blockers, as well as advanced integrations designed for developers and power users.  

Today, the web browser has evolved far beyond a simple tool for accessing content: it has become a true work platform, comparable to a lightweight operating system. Naturally, this central role makes it a prime target for attackers, further underlining the importance of securing it effectively. Our research, initiated on Chromium version 130 and valid up to the latest release available at the time of writing, introduces a backdooring and post‑exploitation technique. By leveraging a simple disk write primitive, it becomes possible to silently install custom extensions on Chromium‑based browsers deployed within Windows environments. 

Anatomy of a Chromium extension

A browser extension is fundamentally composed of several key parts that define its structure, behavior, and capabilities:

  • manifest.json: This is the core configuration file of every Chromium extension. It provides essential metadata such as the extension’s name, version, and permissions, and specifies which scripts to run in different contexts (e.g., background or content scripts) and where to render HTML files (e.g., popups, new tab pages). It serves as the blueprint that defines the extension’s structure and behavior.

  • Service Worker scripts:This background JavaScript runs independently of any web page and has extensive access to Chrome extension APIs. It enables the extension to manage browser settings, handle network requests, communicate with other extension components, and perform sensitive actions such as intercepting network traffic, capturing screenshots, or accessing all cookies.

  • Content scripts: JavaScript files injected into specific web pages as defined in the manifest.json. Running within the page’s context, they can interact directly with the Document Object Model (DOM), allowing them to read, modify, and manipulate page content. They can be used for tasks such as altering page appearance, injecting forms (including malicious ones), or scraping data, including credentials stored in local or session storage.

  • HTML Files (for popups, new pages, etc.): Extensions can also include standard HTML files to create user interfaces. These interfaces are commonly used for:

    • Popups: Small windows that appear when a user clicks the extension’s icon in the browser toolbar (e.g., default_popup in manifest.json).
    • New Tab Pages: Replacing the default new tab page with a custom one.
    • Options Pages: Dedicated pages for configuring extension settings.

Browser extensions operate within the Chrome environment and have direct access to its APIs. This allows them to interact with data that the browser processes, including information that might otherwise be protected by App-Bound Encryption at the operating system level, as the browser itself handles the decryption for its normal operation. Consequently, loaded extensions can access and manipulate sensitive data, such as credentials, intercept network traffic, redirect users, and capture screen content.

Chromium Extension installation

Extensions are typically distributed in a special package format, with a .crx file extension. This .crx file is a zipped archive containing all the extension’s files (manifest, scripts, icons, etc.), digitally signed to ensure its integrity and origin.

Generally, Chromium extensions are installed through a few primary mechanisms:

  • Chrome Web Store: The most common installation method is through the official Chrome Web Store. In this case, the browser automatically downloads the corresponding .crx package, verifies its digital signature, and ensures it originates from a trusted source. Extensions distributed via the store also undergo Google’s review process before being made available to users.

Chrome web store

 

  • Unpacked or self-signed extensions: During development or testing, extensions can be loaded directly from a local folder rather than through the Chrome Web Store. This is done by enabling “Developer mode” in the browser’s extension management page and selecting “Load unpacked” to point to the extension’s source folder.
    Alternatively, users can drag and drop a .crx package into the browser window to install it, though if the package is self-signed and not from the official store, Chrome will show warnings or may block the installation depending on policy settings.
Unpacked extension upload

 

  • Command Line: Extensions can be loaded at browser startup by specifying their directory with the --load-extension flag. For Chromium versions 137 and above, this flag is disabled by default. To re-enable this functionality and load an extension via the command line, an additional flag, --disable-features=DisableLoadExtensionCommandLineSwitch, is required.

From an attacker’s perspective, abusing the Chrome Web Store is significantly more difficult due to Google’s review and approval process. While techniques like obfuscation or WebAssembly can occasionally evade detection, the store still poses a far higher barrier compared to other forms of deployment methods such as local installation or enterprise mass deployment. The command-line method, while functional, is a well-known technique. Its visibility and the fact that it has been disabled by default in recent Chromium versions make it less suitable for stealthy and persistent operations. Furthermore, this method is frequently leveraged by malicious actors, including infostealers like Chromeloader, making it easily detectable.

Our investigation, therefore, shifts to understanding the underlying mechanisms that enable the “Load unpacked” functionality within developer mode, seeking a less documented pathway that bypasses these typical constraints and offers a more discreet method for extension loading.

Chromium extension loading mechanism

Upon installation, Chromium browser extensions are registered within specific user preference files on Windows systems. These files, located within the user’s AppData directory, serve as configuration repositories that enumerate installed extensions and their associated settings:

  • %USERNAME%/AppData/Local/Google/User Data/Default/Secure preferences
  • %USERNAME%/AppData/Local/Google/User Data/Default/Preferences

The choice between these files depends on the system’s domain join status: Secure preferences is utilized on non-domain-joined Windows devices, while Preferences is used in domain-joined environments.

Below is the structure of a typical Chrome preference file, showing its core sections and their contents:

{
    "extensions":
        {
            "settings":
                {
                    "<extension_hash>":{
                        "name" : "Extension name",
                        [...rest of manifest.json]
                    },
                {...}
                },
                 "ui": {
            "developer_mode": true
        }
        }
        [...]
    "protection":{
        "macs":{
            "extensions":{
                "settings":{
                    "<extension_hash>" : "<MAC>"
                }
            },
             "extensions": {
                    "ui": {
                        "developer_mode": "<MAC>"
                    }
                },
        }
    }
}

The Secure Preferences file, used on non-domain joined computers, shares a largely similar structure with the standard Preferences file, but includes an additional super_mac JSON property at its root level.

Successfully modifying these preference files requires addressing three key challenges:

  • Pre-calculating the extension hash
  • Calculating the MAC for protection.macs.extensions.settings.[extension_id]
  • Calculating the MAC for developer mode (a new security measure introduced in recent Chromium versions)

Extension hash generation

When an extension is installed in a Chromium-based browser, it is assigned a unique extension ID. This identifier is generated by first computing the SHA-256 hash of the DER-encoded X.509 SubjectPublicKeyInfo block from the extension’s public key, if it is packaged (signed with a key). If the extension is unpacked or in development mode and does not have a public key, the browser instead uses the absolute path of the extension’s installation directory as the input for the hash. In both cases, the first 32 characters of the resulting hexadecimal hash are mapped to a custom alphabet (a–p), producing the final extension ID.

The code below from chromium source responsible for ID generation for unpacked extensions in Chromium. For packed extensions, the public key is used as input instead of the path, but the rest of the process (hashing and mapping) is handled by similar utility functions.

// chromium/src/chrome/browser/extensions/unpacked_installer.cc:224
int UnpackedInstaller::GetFlags() {
  std::string id = crx_file::id_util::GenerateIdForPath(extension_path_);
  bool allow_file_access =
      Manifest::ShouldAlwaysAllowFileAccess(Manifest::UNPACKED);
  ExtensionPrefs* prefs = ExtensionPrefs::Get(service_weak_->profile());
  if (allow_file_access_.has_value()) {
    allow_file_access = *allow_file_access_;
  } else if (prefs->HasAllowFileAccessSetting(id)) {
    allow_file_access = prefs->AllowFileAccess(id);
  }
  int result = Extension::FOLLOW_SYMLINKS_ANYWHERE;
  if (allow_file_access)
    result |= Extension::ALLOW_FILE_ACCESS;
  if (require_modern_manifest_version_)
    result |= Extension::REQUIRE_MODERN_MANIFEST_VERSION;
  return result;
}

// chromium/src/components/crx_file/id_util.cc:59
std::string GenerateIdForPath(const base::FilePath& path) {
  base::FilePath new_path = MaybeNormalizePath(path);
  std::string path_bytes =
      std::string(reinterpret_cast<const char*>(new_path.value().data()),
                  new_path.value().size() * sizeof(base::FilePath::CharType));
  return GenerateId(path_bytes);
}

std::string GenerateId(const std::string& input) {
  uint8 hash[kIdSize];
  crypto::SHA256HashString(input, hash, sizeof(hash));
  std::string output = base::StringToLowerASCII(base::HexEncode(hash, sizeof(hash)));
  ConvertHexadecimalToIDAlphabet(&output);
  return output;
}

This extension ID acts as the key in the browser’s internal extension management system (such as the extensions.settings object), where the corresponding value stores the extension’s manifest. The manifest contains information like the extension’s name, declared permissions, installation location, and other configuration parameters as explained earlier. To ensure a consistent extension ID across various target systems, we can leverage the key attribute within our extension’s manifest.json. This allows for pre-calculation and control over the extension’s unique identifier.

The Python script below demonstrates how to generate an extension ID along with the corresponding public and private keys:

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

 def generate_extension_keys() -> tuple[str, str, str]:
     private_key = rsa.generate_private_key(
         public_exponent=65537,
         key_size=2048
     )
     public_key = private_key.public_key()

     pub_key_bytes = public_key.public_bytes(
         encoding=serialization.Encoding.DER,
         format=serialization.PublicFormat.SubjectPublicKeyInfo
     )

     sha256_hash = hashlib.sha256(pub_key_bytes).digest()
     crx_id = translate_crx_id(sha256_hash[:16].hex())

     pub_key = base64.b64encode(pub_key_bytes).decode('utf-8')

     priv_key_bytes = private_key.private_bytes(
         encoding=serialization.Encoding.DER,
         format=serialization.PrivateFormat.TraditionalOpenSSL,
         encryption_algorithm=serialization.NoEncryption()
     )
     priv_key = base64.b64encode(priv_key_bytes).decode('utf-8')

     return crx_id, pub_key, priv_key

 def translate_crx_id(input_str: str) -> str:
     translation = {
         '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(translation.get(c, c) for c in input_str)

print(generate_extension_keys())

The script generates a deterministic Chrome extension ID and its corresponding base64-encoded public key. This key should be added to the key property within the extension’s manifest.json:

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

Extension MAC Calculation: Bypassing Integrity Checks

For an extension entry in the Preferences file to be considered valid and loaded by Chromium, its associated MAC must be correctly calculated. This MAC acts as an integrity check, preventing arbitrary modification of the preferences file by external processes as explained above. Regarding MAC calculation, a 2020 research paper by Pablo Picazo-Sanchez, Gerardo Schneider, and Andrei Sabelfeld, titled “HMAC and ‘Secure Preferences’,” provided valuable insights into the HMAC calculation part. The research indicated that the seed used by Chrome for this HMAC calculation is hard-coded within the resources.pak binary file usually located in C:\Program Files\Google\Chrome\Application\\resources.pak.

Remarkably, despite the research being based on an older Chromium version, this described HMAC seed mechanism proved to still be valid even for Chromium version 139 at the time of this writing (https://github.com/Pica4x6/SecurePreferencesFile/blob/main/Seed.py#L60).

Identifying the seed from resources.pak

For a more precise identification of which specific file contains the seed value, pak_util from GRIT (Google Resource and Internationalization Tools) has been used. A differential analysis was performed to precisely identify the resources.pak file containing the seed value. By extracting and comparing the contents of an original resources.pak with a subtly modified version (e.g., a single byte alteration), we determined that file 146 holds the seed.

$ python3 pak_util.py extract resources.pak -o resources_v139/
$ python3 pak_util.py extract resources.pak -o resources_v139_dirty/
$ diff -bry resources_v139 resources_v139_dirty
Binary files resources_v139/146 and resources_v139_dirty/146 differ
$ xxd -p resources_v139/146
e748f336d85ea5f9dcdf25d8f347a65b4cdf667600f02df6724a2af18a212d26b788a25086910cf3a90313696871f3dc05823730c91df8ba5c4fd9c884b505a8

The MAC is computed using a combination of this static seed, the extension’s crx_id, and the JSON content of the extension’s settings within the preferences file. The calculation can be conceptualized as:

# Pseudo-code for MAC calculation
# seed + "extensions.settings.<crx_id>" + {extension_settings...}
ext_mac = hmac.new(bytes.fromhex("e748f[...]4b505a8"),
                   ("extensions.settings.<crx_id>" + json.dumps({extension_settings...})).encode('utf-8'),
                   hashlib.sha256).hexdigest().upper()

Notably, for other Chromium-based browsers such as Microsoft Edge and Brave, the seed value was observed to be null. This finding is consistent with the original research paper’s observations and remains valid as of the time of this writing, five years later.


Chromium’s Enhanced Developer Mode Security

In Chromium versions 134 and above, managing the “Developer Mode” UI toggle state has been tightened. Its status is now precisely controlled within the Preferences or Secure Preferences file, specifically via the extensions.developer_mode entry. It’s important to note that if Developer Mode isn’t enabled here, extensions can be registered, but they’ll never actually load or become active. This boolean value (true/false) controlling Developer Mode isn’t just stored; it also needs to be signed with a Message Authentication Code (MAC), just like the registration of extensions themselves.

Our research has revealed a critical dependency here: the static seed used to correctly sign the developer_mode property’s value, generating its MAC, is the exact same one that underpins the system’s overall extension integrity.

Bypassing chromium policies

While the aforementioned technique provides a method for silently loading extensions, enterprise environments often implement security policies to restrict browser behavior. A common control is the use of Chrome policies, typically enforced via Group Policy Objects (GPOs) in Windows domains, to control extension installation and execution. These policies can whitelist, blacklist, or entirely block extensions, as well as disable developer mode.

Organizations generally deploy Chrome Administrative Templates to configure these policies. For instance, the ExtensionInstallAllowlist and ExtensionInstallBlocklist policies are frequently used to define which extensions are permitted or forbidden based on their extension hash.

Chrome administrative template

Technique 1: Whitelisted extension hash spoofing

Recognizing that GPOs typically rely on extension hash for whitelisting, we explored the possibility of spoofing the ID of a legitimate allowed extension.

The concept relies on the scenario where a specific extension hash, such as that of a corporate-approved plugin like Adobe Acrobat Reader for Chrome, is explicitly whitelisted by policy despite not being installed. As the Chromium extension identifier is deterministically derived from the base64-encoded RSA public key defined in the manifest, an attacker could leverage this key material to reproduce the same Chrome extension identifier and deploy a modified version as an unpacked extension.

Here is your process clearly structured into 5 steps:  

  1. Install a legitimate and allowed extension (e.g., Adobe Acrobat PDF) from the Chrome Web Store.
  2. Retrieve the extension ID from the browser’s extension page URL or details.
  3. Extract the extension’s public key attribute by inspecting its service worker console (using chrome.runtime.getManifest().key) or by downloading and analyzing the .crx package from Google’s update servers.
  4. Update your extension’s manifest.json by setting the key attribute to the extracted public key, thereby spoofing the extension ID.
  5. Inject the modified extension into the browser’s preferences and sign the MAC to complete the installation.

Technique 2: Extension stomping

We observed an interesting behavior when an unpacked extension was configured to spoof the identifier of an already installed one. In this scenario, Chromium gave priority to the unpacked version, which replaced the legitimate Web Store extension in the chrome://extensions page.

adobe extension

This behavior, confirmed on both Chrome and Edge (for example with Adobe PDF), shows that when extension IDs collide, the browser favors locally loaded unpacked extensions providing a stealthy way to override trusted ones.

Adobe extension stomping.

Technique 3: Neutralizing GPO via HKCU

The most effective method found for bypassing GPO-enforced extension restrictions leverages the Windows Group Policy processing order.

Policies are applied in a specific sequence known as LSDOU:

  • Local (Local Group Policy on the machine itself)
  • Site (Active Directory Sites)
  • Domain (Active Directory Domains)
  • Organizational Unit (Active Directory OUs)

Chromium policies applied through GPO are stored under HKCU\Software\Policies\*. Although this location is within HKCU, it is protected from modification by standard users but can be changed by anyone with administrative privileges. An attacker with administrative privileges can therefore modify, add, or delete entries in HKCU\Software\Policies\Google\Chrome\ExtensionInstallAllowlist or ExtensionInstallBlocklist, allowing them to bypass extension restrictions and compromise the enforcement of Chrome policies.

PS> reg delete "HKCU\Software\Policies\Google\Chrome\ExtensionInstallAllowlist" /f
PS> reg delete "HKCU\Software\Policies\Google\Chrome\ExtensionInstallBlocklist" /f

Demonstration

The manual execution of these steps, from deriving the extension ID hash using the public key to modifying the manifest, recomputing the MAC, and updating preference files, is both complex and error-prone. To simplify the process, a script will be published on Synacktiv’s GitHub.

Below is a demonstration using our toolkit that highlights browser backdooring via extensions, featuring remote SMB-based deployment and a custom command and control (C2) server.

Video file

Conclusion

This research revealed a potent technique, not widely used for loading arbitrary extensions into Chromium-based browsers within Windows domain environments. By manipulating Chromium’s internal preference files and their associated MACs, we demonstrated how to programmatically inject and activate extensions, bypassing standard installation methods.

  • For red teams, this technique provides a stealthy and persistent avenue for browser compromise and data exfiltration. Deployment is versatile, supporting remote loading over SMB, integration with existing OS-level implants, or leveraging projects like CursedChrome to transform the browser into an HTTP proxy. Developing custom agents and C2 frameworks allows remote execution of JavaScript directly within Chromium’s native process, bypassing security mechanisms such as app-bound encryption.
  • For blue teams, having a deep understanding of these techniques is essential to improving detection capabilities. It underscores the critical need to monitor for unauthorized changes to browser preference files (especially those made by non-browser processes), unusual extension registrations, and suspicious modifications to HKCU or HKLM registry keys related to Software policies.

The browser, as a primary interface to sensitive cloud data, remains a critical attack surface. While this knowledge aims to strengthen both offensive and defensive postures, it also highlights the inherent challenge in cryptographically protecting browser-internal secrets like the MAC seed, as any truly robust solution would need to account for diverse operating system-specific security mechanisms (like DPAPI on Windows) without affecting cross-platform compatibility.