Silent Infiltration: Chromium Preference Attacks
2025-09-05 , Second track

This presentation introduces a stealthy technique for injecting arbitrary extensions into Chromium-based browsers by manipulating the Preferences file.

The method, which remains relatively obscure, expands on the groundwork laid by Pablo Picazo-Sanchez, Gerardo Schneider, and Andrei Sabelfeld in their 2020 whitepaper.

The focus of the presentation is on refining and enhancing this approach to circumvent recent security measures implemented in the latest Chromium versions. It demonstrates the automation of this process through an exploitation script and showcases various post-exploitation attacks that leverage the chromium API which permits :
- Stealing of cookies and Localstorage credentials
- Getting history of navigation
- Partial access to the FS
- And much more ...


On Windows, browser extensions are registered in one of these
files:

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

The injection method exploits Chromium's Preferences file, which allows loading
unpacked extensions from a specified path. It is basically a config files
listing users preferences, including installed browser extensions.

Non-domain joinded Windows devices uses "Secure preferences" to register
extension, and in a domain they use "Preferences".

When a chromium extension is installed, the browser adds its extension ID
(let's call it crx_id) to the extensions.settings object inside The Preferences
file.

Inside this object, you'll find another entry where the key is the crx_id.
Under this, there's a manifest that holds all the details about the extension
like its name, permission, location etc...

Below is the structure of a Preference file and Secure preference file.

# Preferences JSON structure
{
    "extensions":
        {
            "settings":
                {
                    "<crx_id>":{
                        "name" : "Extension title",
                        [...manifest.json content]
                    }
                "...other_exts":{...},
                {...}
                }
        }
        [...]
    "protection":{
        "macs":{
            "extensions":{
                "settings":{
                    "<crx_id>" : "<MAC>"
                }
            }
        }
    }

}
# Secure Preferences JSON structure :
{
    "extensions":
        {
            "settings":
                {
                    "<crx_id>":{
                        "name" : "blabla",
                        [...manifest.json content]
                    }
                "...other_exts":{...},
                {...}
                }
        }
        [...]
    "protection":{
        "macs":{
            "extensions":{
                "settings":{
                    "<crx_id>" : "<MAC>"
                }
            }
        },
    "super_mac": "<SUPERMAC>"
    }
}

Secure Preferences have mostly the same structure, the browser will use this
file instead of Preferences when it is a personal device not linked to domain.
The only differences is that, it in addition to the
protections.macs.extension.settings., there is also a super_mac which uses the
user sid without the rid part.

In a nutshell the algorithm is as follows for the SPF:

# sid without rid + with of all the previous macs entry
seed = b'\xe7H\xf26[...]'
s1 = sid_wo_userid + json.dumpers(data['protection']['macs'], deparators=(',',':')) 
supermac = hmac.new(seed, s1.encode("utf-8"), hashlib.sha256).hexdigest().upper()

And for preferences file only the signing of the part is sufficient we don't
need the json key (the seed is the same for both method).

# seed + "extensions.settings.<crx_id>" + {extension_settings...}
ext_mac = hmac.new(b'\xe7H\xf26[...]', "extensions.settings.<crx_id>", '{"active_permissions":{"api"}, path:"C:\\\\Users\\\public\extension"}, state:1, etc....').hexdigest().upper()

CRX_ID generation

Chrome extension crx_id is generated by the browser. it is basically a SHA256
of the extension path in base16 in mpdecimal format [a-p] rather than [0-9af]
for usual sha256sum.

// 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
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;
}

If we want a consistent crx_id, we can make it "sticky" and 100% predictible by
adding a key attribute to the extension's manifest. This key is a public X.509
certificate in base64 format, and it helps derive the crx_id. This method
ensures that the crx_id remains the same across different devices, making it
completely path-agnostic and deterministic.

Signing Chromium Preference's file

Once we found a solution to make a deterministic crx_id, we needed to sign the
Chromium preferences file.

The proper signing of the preference file with the seed is required and based
on the research, researchers made several tests on multiple machines. It
appears that the seed is not securely randomly generated.

So in-sum:

Deterministic crx_id + Preferences update + HMAC seed signing = Ability to
backdoor any chromium based browsed with a write primitive.

SPF github of the research

My research reveals that the seed can be extracted by unpacking the resources.pak file located under Program Files :

xxd -p extracted/146
e748f33[...]a8

Bypassing Security Measures

UPDATE : Chromium version 134 and later introduces a new security feature in
the preferences file: a developer mode UI entry that must be enabled and signed
with a MAC before any extension can be activated. Additionally, another entry
is also signed. However, the HMAC signing of this value is signed using the
same seed key.

Companies often deploy Chromium GPO administrative templates to prevent the
installation of malicious extensions.

However, according to the LSDOU principle in Windows environments, local
configuration is privileged over GPO. In Chromium documentation, they explain
how to apply these administrative templates using HKLM, but after testing with
HKCU, it seems that even the current user can circumvent these restrictions.

I have found that these restrictions can be easily bypassed since Chromium
prefers HKCU settings, which we can abuse within the current user's context.

Security recommendations designed to assist Blue Teamers in detecting this attack within their environments will also be shared.

An offensive security engineer with experience in penetration testing and tool development, with a background in web development.