CVE-2025-12439
Overview
Background
- App-Bound Encryption
- Chrome on Windows protects data by sending it to a SYSTEM elevation service, which wraps it in two DPAPI layers: user context, then SYSTEM context (
elevator.cc:116-143). - `Elevator::DecryptData`
- the COM method that removes the SYSTEM DPAPI layer, then (impersonating the caller) the user layer, then validates the caller (
elevator.cc:156-240). - `PreProcessData` / `PostProcessData`
- Google Chrome-branded hooks that run on the plaintext inside the DPAPI layers (
elevator.cc:83-91,217-227). They are not defined in any file shown; they presumably come from the branded-only includechrome/elevation_service/internal/elevation_service_internal.h(elevator.cc:32-34) (inferred), which lives in a private repository that is not public, so this analysis does not describe their behavior. - Padding oracle
- a side channel that reveals whether a tampered CBC ciphertext decrypts to correctly padded plaintext, which lets an attacker recover plaintext without the key. The reporter says the Windows DPAPI operational event log provides this signal (reporter’s claim).
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: All file:line citations check out. Two unsupported assertions stated as fact: no pre-v3 protection (contradicted by PreProcessData at elevator.cc:83-91 and v2 key mention) and that v3 migration protects data (private, unverifiable).
06a2376446b5. An earlier analysis was withdrawn because it had been written against a commit that was not the security fix (c4d7b188040c). Fix coverage caveat: v3 logic (PostProcessData/PreProcessData/InternalFlags) lives in private chrome/elevation_service/internal, which is not shown. Client re-encrypt is now off by default. Only affects Google Chrome branded builds. SYSTEM-context decrypt of caller blobs (elevator.cc:171) is unchanged.Elevator::DecryptData runs ::CryptUnprotectData under the service’s SYSTEM context on any caller-supplied BSTR (elevator.cc:164-175), and only afterwards impersonates the caller and runs ValidateData (elevator.cc:180-211). Any client can therefore make SYSTEM attempt DPAPI decryption of modified blobs. The reporter says that OS-side DPAPI event logging, which the encrypt path’s CRYPTPROTECT_AUDIT requests (elevator.cc:123, 139), turns this into a padding oracle; the logging behavior itself is outside the material (inferred). Recovering the SYSTEM-layer plaintext yields the inner user-DPAPI blob, which the low-privilege user can open in their own context (inferred). Pre-v3 data was not entirely unprocessed: pre-fix EncryptData already runs the branded PreProcessData on all plaintext (elevator.cc:83-91), and the commit message refers to an existing “v2 key (or earlier)”. What that pre-v3 processing does is private. That it did not withstand the oracle is only suggested by the fix forcing migration to v3 (inferred). The visible fix does not change this decrypt order: it replaces the default-constructed InternalFlags flags; with InternalFlags flags{.use_latest_encryption = true}; before the call to PostProcessData (elevator.cc:218-219). According to the commit message and test rationale, data under a v2-or-earlier key then sets post_process_should_reencrypt, and DecryptData returns kSuccessShouldReencrypt (elevator.cc:224-226, 239) so the browser re-encrypts it under v3 through EncryptData. What v3 actually does, and whether it defeats the oracle, lives in the private, non-public chrome/elevation_service/internal code (elevator.cc:32-34) and cannot be verified here. The same commit flips kAppBoundDataReencrypt to base::FEATURE_DISABLED_BY_DEFAULT (app_bound_encryption_win.cc:60-62), and that flag gates the browser-side re-encrypt (app_bound_encryption_win.cc:245-246), so migration of existing data rolls out through variations rather than being on by default.Attack Path
- Obtain app-bound ciphertext A low-privilege local process reads the app-bound-encrypted blob that belongs to its own user (inferred; how the data is stored is not shown).
- Use the service as a SYSTEM decryptor
The process submits modified versions of the blob to
Elevator::DecryptData, which runs::CryptUnprotectDataunder SYSTEM before any caller validation (elevator.cc:171-175). - Read a side channel The attacker tells padding failures apart from MAC failures through OS DPAPI event logging, as the reporter claims. This is outside the code shown (inferred).
- Recover the inner layer Recovering the SYSTEM-layer plaintext exposes the inner user-DPAPI blob, which the same user can decrypt (inferred). Whether v3 post-processing makes that result useless is not visible because the code is private.
Impact Assessment
DecryptData path does accept arbitrary blobs (elevator.cc:164-172), and this diff does not change that. Any protection from this change can therefore apply at most to Chrome’s own data once it is migrated to v3, and only in Google Chrome-branded builds (elevator.cc:217). Whether v3 actually defeats the oracle lives in private code and cannot be verified here; the diff itself only sets use_latest_encryption = true (elevator.cc:218).Files Changed
chrome/browser/os_crypt/app_bound_encryption_win.ccchrome/browser/os_crypt/app_bound_encryption_win_browsertest.ccchrome/elevation_service/elevator.cctesting/variations/fieldtrial_testing_config.json
Audit Directions
- Privileged decrypt before authorizationIn
DecryptData, the SYSTEM-contextCryptUnprotectData(elevator.cc:171) runs before caller validation (elevator.cc:206). Check whether caller validation, or limits on repeated failures, can happen before any privileged cryptographic operation on caller-controlled input. - Distinguishable failure signalsCheck whether separate error codes (
kErrorCouldNotDecryptWithSystemContextand others) or OS audit records (CRYPTPROTECT_AUDIT) let a caller tell different kinds of decryption failure apart. Any such distinction can act as an oracle. - Migration behind disabled flagsTrack when
kAppBoundDataReencryptis enabled. Until it is, data encrypted under the older key stays in its pre-v3 form on disk.
Patch
diff --git a/chrome/browser/os_crypt/app_bound_encryption_win.cc b/chrome/browser/os_crypt/app_bound_encryption_win.cc
index 17fa966..f496fbbf 100644
--- a/chrome/browser/os_crypt/app_bound_encryption_win.cc
+++ b/chrome/browser/os_crypt/app_bound_encryption_win.cc
@@ -59,7 +59,7 @@
namespace features {
BASE_FEATURE(kAppBoundDataReencrypt,
"AppBoundDataReencrypt",
- base::FEATURE_ENABLED_BY_DEFAULT);
+ base::FEATURE_DISABLED_BY_DEFAULT);
} // namespace features
SupportLevel GetAppBoundEncryptionSupportLevel(PrefService* local_state) {
diff --git a/chrome/elevation_service/elevator.cc b/chrome/elevation_service/elevator.cc
index 1335df5..d2d1759 100644
--- a/chrome/elevation_service/elevator.cc
+++ b/chrome/elevation_service/elevator.cc
@@ -215,7 +215,7 @@
}
bool should_reencrypt = false;
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
- InternalFlags flags;
+ InternalFlags flags{.use_latest_encryption = true};
auto post_process_result = PostProcessData(plaintext_str, &flags);
if (!post_process_result.has_value()) {
return post_process_result.error();
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index 6822a8879..f066efa 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -1232,6 +1232,21 @@
]
}
],
+ "AppBoundDataReencrypt": [
+ {
+ "platforms": [
+ "windows"
+ ],
+ "experiments": [
+ {
+ "name": "Enabled",
+ "enable_features": [
+ "AppBoundDataReencrypt"
+ ]
+ }
+ ]
+ }
+ ],
"AppListLaunchRecorder": [
{
"platforms": [
diff --git a/chrome/browser/os_crypt/app_bound_encryption_win_browsertest.cc b/chrome/browser/os_crypt/app_bound_encryption_win_browsertest.cc
index 9ee4935..6337d6ae0 100644
--- a/chrome/browser/os_crypt/app_bound_encryption_win_browsertest.cc
+++ b/chrome/browser/os_crypt/app_bound_encryption_win_browsertest.cc
@@ -512,9 +512,10 @@
std::string ciphertext;
DWORD last_error;
base::HistogramTester histograms;
+ elevation_service::EncryptFlags flags{.use_latest_key = true};
HRESULT hr =
EncryptAppBoundString(ProtectionLevel::PROTECTION_PATH_VALIDATION,
- plaintext, ciphertext, last_error);
+ plaintext, ciphertext, last_error, &flags);
ASSERT_HRESULT_SUCCEEDED(hr);
Regression Test / PoC
diff --git a/chrome/browser/os_crypt/app_bound_encryption_win_browsertest.cc b/chrome/browser/os_crypt/app_bound_encryption_win_browsertest.cc
index 9ee4935..6337d6ae0 100644
--- a/chrome/browser/os_crypt/app_bound_encryption_win_browsertest.cc
+++ b/chrome/browser/os_crypt/app_bound_encryption_win_browsertest.cc
@@ -512,9 +512,10 @@
std::string ciphertext;
DWORD last_error;
base::HistogramTester histograms;
+ elevation_service::EncryptFlags flags{.use_latest_key = true};
HRESULT hr =
EncryptAppBoundString(ProtectionLevel::PROTECTION_PATH_VALIDATION,
- plaintext, ciphertext, last_error);
+ plaintext, ciphertext, last_error, &flags);
ASSERT_HRESULT_SUCCEEDED(hr);
Original Bug Report
AppBound Decryption with Padding Oracle Attack
Report description
AppBound Decryption with Padding Oracle Attack
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
https://github.com/chromium/chromium/tree/main/chrome/elevation_service, https://github.com/chromium/chromium/tree/main/chrome/browser/os_crypt
The problem
Please describe the technical details of the vulnerability
Hello Chrome VRP team :)
I found a vulnerability in the implementation of AppBound Encryption with the elevation service that would allow a low-privilege user to decrypt cookies and other sensitive data. I realize local attacks aren’t generally considered in the chrome threat model. But since the App Bound feature is meant specifically to handle local attacks, and since this attack impacts Windows security as a whole, I though it’s important enough to bring up.
The technique implements an attack called “Padding Oracle Attack,” which allows decrypting ciphertexts without a key under very specific circumstances. For the attack to work, two things are necessary. First, the encryption has to use a vulnerable type of encryption mode like AES-CBC, which is what DPAPI uses. The other thing required is a “Padding Oracle,” a black box that takes a ciphertext as input and returns it if the plaintext that matches the ciphertext has valid padding.
In this implementation, the “Padding Oracle” comes from the Windows Event Viewer in a channel called Microsoft-Windows-Crypto-DPAPI/Operational. This channel records events related to DPAPI. DPAPI blobs have serval fields, including the ciphertext and a signature, aka MAC, to ensure no tampering was done to the DPAPI blob.
If an attacker modifies the blob randomly before trying to decrypt it, then we will usually be able to see an error event in the event viewer that says the reason for failure is an invalid MAC. However, if an attacker modifies the ciphertext in the blob so that the padding of the plaintext is invalid, we can see a different error event where the reason for failure is unknown. This turns the event viewer into a “Padding Oracle” and allows an attacker to use the “Padding Oracle Attack” to decrypt a DPAPI encrypted blob.
The root cause of the issue is that the elevation service allows a low-privilege user to try to decrypt a DPAPI blob encrypted by SYSTEM. Typically, users can only try to decrypt blobs encrypted by the same user, so they don’t need the “Padding Oracle Attack.” If a user tries to decrypt a blob that was encrypted by a different user, such as SYSTEM, the key can’t be found, and then the “Padding Oracle Attack” can’t be implemented. It’s only in this hybrid situation that the attack is viable. An important thing to mention is that the attack does take several hours. The creation of the event logs is slow. The VM I tested this on took about a second for each log. In theory, decrypting each byte should take an average of a little over 2 minutes. But specifically when decrypting the app bound encrypted key, a lot of the plain text is known ahead of time (it’s just another DPAPI encrypted blob except this time for a low privileged user), so it’s possible to save some of the time by just assuming those values are fixed instead of decrypting them.
I’ll be attaching a zip file with a demo and the code is used to make it. Sorry I know it’s explicitly written not to upload a zip. But I was having trouble getting python to function as a COM client so the code is divided between python that does most of the work and a cpp program to do the COM requests to the elevation service. I also attached a video to show that it works but since it takes several hours I skip ahead a lot in the video.
Please briefly explain who can exploit the vulnerability, and what they gain when doing so
These issues can be exploited by any process running with low privileges. It allows them to bypass the AppBound Encryption mechanism. Furthermore, it allows a low-privilege user to decrypt anything encrypted with DPAPI from the SYSTEM user.
The cause
What version of Chrome have you found the security issue in?
127 stable and above
Is the security issue related to a crash?
No
Choose the type of vulnerability
Permissions Bypass
How would you like to be publicly acknowledged for your report?
ari