1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-03 02:03:53 +00:00

Rename admin.exe to bitwarden_chromium_import_helper.exe

This commit is contained in:
Dmitry Yakimenko
2025-10-25 18:11:31 +02:00
parent b9ca1ae57d
commit ca21df5f8e
6 changed files with 66 additions and 65 deletions

View File

@@ -60,6 +60,6 @@ workspace = true
windows-binary = ["sysinfo"]
[[bin]]
name = "admin"
path = "src/bin/admin.rs"
name = "bitwarden_chromium_import_helper"
path = "src/bin/bitwarden_chromium_import_helper.rs"
required-features = ["windows-binary"]

View File

@@ -2,11 +2,10 @@
## Overview
The Windows **Application Bound Encryption (ABE)** subsystem consists of two main components that
work together:
The Windows **Application Bound Encryption (ABE)** subsystem consists of two main components that work together:
- **client library** — a library that is part of the desktop client application
- **admin.exe** — a password decryptor running as **ADMINISTRATOR** and later as **SYSTEM**
- **bitwarden_chromium_import_helper.exe** — a password decryptor running as **ADMINISTRATOR** and later as **SYSTEM**
_(The name of the binary will be changed in the released product.)_
@@ -14,16 +13,15 @@ See the last section for a concise summary of the entire process.
## Goal
The goal of this subsystem is to decrypt the master encryption key used to encrypt login information
on the local Windows system. This applies to the most recent versions of Chrome, Brave, and
(untested) Edge that use the ABE/v20 encryption scheme for some local profiles.
The goal of this subsystem is to decrypt the master encryption key used to encrypt login information on the local
Windows system. This applies to the most recent versions of Chrome, Brave, and (untested) Edge that use the ABE/v20
encryption scheme for some local profiles.
The general idea of this encryption scheme is as follows:
1. Chrome generates a unique random encryption key.
2. This key is first encrypted at the **user level** with a fixed key.
3. It is then encrypted at the **user level** again using the Windows **Data Protection API
(DPAPI)**.
3. It is then encrypted at the **user level** again using the Windows **Data Protection API (DPAPI)**.
4. Finally, it is sent to a special service that encrypts it with DPAPI at the **system level**.
This triply encrypted key is stored in the `Local State` file.
@@ -32,76 +30,74 @@ The following sections describe how the key is decrypted at each level.
## 1. Client Library
This is a Rust module that is part of the Chromium importer. It compiles and runs only on Windows
(see `abe.rs` and `abe_config.rs`). Its main task is to launch `admin.exe` with elevated privileges,
presenting the user with the UAC prompt. See the `abe::decrypt_with_admin` call in `windows.rs`.
This is a Rust module that is part of the Chromium importer. It compiles and runs only on Windows (see `abe.rs` and
`abe_config.rs`). Its main task is to launch `bitwarden_chromium_import_helper.exe` with elevated privileges, presenting
the user with the UAC prompt. See the `abe::decrypt_with_admin` call in `windows.rs`.
This function takes two arguments:
1. Absolute path to `admin.exe`
1. Absolute path to `bitwarden_chromium_import_helper.exe`
2. Base64 string of the ABE key extracted from the browser's local state
First, `admin.exe` is launched by calling a variant of `ShellExecute` with the `runas` verb. This
displays the UAC screen. If the user accepts, `admin.exe` starts with **ADMINISTRATOR** privileges.
First, `bitwarden_chromium_import_helper.exe` is launched by calling a variant of `ShellExecute` with the `runas` verb.
This displays the UAC screen. If the user accepts, `bitwarden_chromium_import_helper.exe` starts with **ADMINISTRATOR**
privileges.
> **The user must approve the UAC prompt or the process is aborted.**
Because it is not possible to read the standard output of an application launched in this way, a
named pipe server is created at the user level before `admin.exe` is launched. This pipe is used to
send the decryption result from `admin.exe` back to the client.
Because it is not possible to read the standard output of an application launched in this way, a named pipe server is
created at the user level before `bitwarden_chromium_import_helper.exe` is launched. This pipe is used to send the
decryption result from `bitwarden_chromium_import_helper.exe` back to the client.
The data to be decrypted are passed via the command line to `admin.exe` like this:
The data to be decrypted are passed via the command line to `bitwarden_chromium_import_helper.exe` like this:
```bat
admin.exe --encrypted "QVBQQgEAAADQjJ3fARXREYx6AMBPwpfrAQAAA..."
bitwarden_chromium_import_helper.exe --encrypted "QVBQQgEAAADQjJ3fARXREYx6AMBPwpfrAQAAA..."
```
## 2. Admin Executable
Although the process starts with **ADMINISTRATOR** privileges, its ultimate goal is to elevate to
**SYSTEM**. To achieve this, it uses a technique to impersonate a system-level process.
Although the process starts with **ADMINISTRATOR** privileges, its ultimate goal is to elevate to **SYSTEM**. To achieve
this, it uses a technique to impersonate a system-level process.
First, `admin.exe` ensures that the `SE_DEBUG_PRIVILEGE` privilege is enabled by calling
First, `bitwarden_chromium_import_helper.exe` ensures that the `SE_DEBUG_PRIVILEGE` privilege is enabled by calling
`RtlAdjustPrivilege`. This allows it to enumerate running system-level processes.
Next, it finds an instance of `lsass.exe` or `winlogon.exe`, which are known to run at the
**SYSTEM** level. Once a system process is found, its token is duplicated by calling
`DuplicateToken`.
Next, it finds an instance of `services.exe` or `winlogon.exe`, which are known to run at the **SYSTEM** level. Once a
system process is found, its token is duplicated by calling `DuplicateToken`.
With the duplicated token, `ImpersonateLoggedOnUser` is called to impersonate a system-level
process.
With the duplicated token, `ImpersonateLoggedOnUser` is called to impersonate a system-level process.
> **At this point `admin.exe` is running as SYSTEM.**
> **At this point `bitwarden_chromium_import_helper.exe` is running as SYSTEM.**
The received encryption key can now be decrypted using DPAPI at the system level.
The decrypted result is sent back to the client via the named pipe. `admin.exe` connects to the pipe
and writes the result.
The decrypted result is sent back to the client via the named pipe. `bitwarden_chromium_import_helper.exe` connects to
the pipe and writes the result.
The response can indicate success or failure:
- On success: a Base64-encoded string.
- On failure: an error message prefixed with `!`.
In either case, the response is sent to the named pipe server created by the client. The client
responds with `ok` (ignored).
In either case, the response is sent to the named pipe server created by the client. The client responds with `ok`
(ignored).
Finally, `admin.exe` exits.
Finally, `bitwarden_chromium_import_helper.exe` exits.
## 3. Back to the Client Library
The decrypted Base64-encoded string is returned from `admin.exe` to the named pipe server at the
user level. At this point it has been decrypted only once—at the system level.
The decrypted Base64-encoded string is returned from `bitwarden_chromium_import_helper.exe` to the named pipe server at
the user level. At this point it has been decrypted only once—at the system level.
Next, the string is decrypted at the **user level** with DPAPI.
Finally, for Google Chrome (but not Brave), it is decrypted again with a hard-coded key found in
`elevation_service.exe` from the Chrome installation. Based on the version of the encrypted string
(encoded within the string itself), this step uses either **AES-256-GCM** or **ChaCha20-Poly1305**.
See `windows.rs` for details.
Finally, for Google Chrome (but not Brave), it is decrypted again with a hard-coded key found in `elevation_service.exe`
from the Chrome installation. Based on the version of the encrypted string (encoded within the string itself), this step
uses either **AES-256-GCM** or **ChaCha20-Poly1305**. See `windows.rs` for details.
After these steps, the master key is available and can be used to decrypt the password information
stored in the browsers local database.
After these steps, the master key is available and can be used to decrypt the password information stored in the
browsers local database.
## TL;DR Steps
@@ -109,14 +105,15 @@ stored in the browsers local database.
1. Extract the encrypted key from Chromes settings.
2. Create a named pipe server.
3. Launch `admin.exe` with **ADMINISTRATOR** privileges, passing the key to be decrypted via CLI arguments.
4. Wait for the response from `admin.exe`.
3. Launch `bitwarden_chromium_import_helper.exe` with **ADMINISTRATOR** privileges, passing the key to be decrypted
via CLI arguments.
4. Wait for the response from `bitwarden_chromium_import_helper.exe`.
2. **Admin side:**
1. Start.
2. Ensure `SE_DEBUG_PRIVILEGE` is enabled (not strictly necessary in tests).
3. Impersonate a system process such as `lsass.exe` or `winlogon.exe`.
3. Impersonate a system process such as `services.exe` or `winlogon.exe`.
4. Decrypt the key using DPAPI at the **SYSTEM** level.
5. Send the result or error back via the named pipe.
6. Exit.

View File

@@ -107,7 +107,7 @@ where
}
}
pub async fn decrypt_with_admin(admin_exe: &str, encrypted: &str) -> Result<String> {
pub async fn decrypt_with_admin_exe(admin_exe: &str, encrypted: &str) -> Result<String> {
let (tx, mut rx) = channel::<String>(1);
debug!(
@@ -126,11 +126,11 @@ pub async fn decrypt_with_admin(admin_exe: &str, encrypted: &str) -> Result<Stri
Err(e) => return Err(anyhow!("Failed to start named pipe server: {}", e)),
};
debug!("Launching '{}' as admin...", admin_exe);
decrypt_with_admin_internal(admin_exe, encrypted);
debug!("Launching '{}' as ADMINISTRATOR...", admin_exe);
decrypt_with_admin_exe_internal(admin_exe, encrypted);
// TODO: Don't wait forever, but for a reasonable time
debug!("Waiting for message from admin...");
debug!("Waiting for message from {}...", admin_exe);
let message = match rx.recv().await {
Some(msg) => msg,
None => return Err(anyhow!("Failed to receive message from admin")),
@@ -142,7 +142,7 @@ pub async fn decrypt_with_admin(admin_exe: &str, encrypted: &str) -> Result<Stri
Ok(message)
}
fn decrypt_with_admin_internal(admin_exe: &str, encrypted: &str) {
fn decrypt_with_admin_exe_internal(admin_exe: &str, encrypted: &str) {
// Convert strings to wide strings for Windows API
let exe_wide = OsStr::new(admin_exe)
.encode_wide()

View File

@@ -52,7 +52,7 @@ mod windows_binary {
use bitwarden_chromium_importer::abe_config;
#[derive(Parser)]
#[command(name = "admin")]
#[command(name = "bitwarden_chromium_import_helper")]
#[command(about = "Admin tool for ABE service management")]
struct Args {
/// Base64 encoded encrypted data to process
@@ -61,8 +61,8 @@ mod windows_binary {
}
// Enable this to log to a file. The way this executable is used, it's not easy to debug and the stdout gets lost.
const NEED_LOGGING: bool = false;
const LOG_FILENAME: &str = "c:\\path\\to\\log.txt"; // This is an example filename, replace it with you own
const NEED_LOGGING: bool = true;
const LOG_FILENAME: &str = "c:\\temp\\admin-log.txt"; // This is an example filename, replace it with you own
// This should be enabled for production
const NEED_SERVER_SIGNATURE_VALIDATION: bool = false;
@@ -413,7 +413,7 @@ mod windows_binary {
}
fn run() -> Result<String> {
debug!("Starting admin.exe");
debug!("Starting bitwarden_chromium_import_helper.exe");
let args = Args::try_parse()?;
@@ -421,7 +421,7 @@ mod windows_binary {
return Err(anyhow!("Expected to run with admin privileges"));
}
debug!("Running as admin");
debug!("Running as ADMINISTRATOR");
// Impersonate a SYSTEM process to be able to decrypt data encrypted for the machine
let system_decrypted_base64 = {

View File

@@ -61,6 +61,8 @@ pub fn configure_windows_crypto_service(_admin_exe_path: &str) {
// Private
//
const ADMIN_EXE_FILENAME: &'static str = "bitwarden_chromium_import_helper.exe";
//
// CryptoService
//
@@ -182,9 +184,9 @@ impl WindowsCryptoService {
let admin_exe_path = get_admin_exe_path()?;
let admin_exe_str = admin_exe_path
.to_str()
.ok_or_else(|| anyhow!("Failed to convert admin.exe path to string"))?;
.ok_or_else(|| anyhow!("Failed to convert {} path to string", ADMIN_EXE_FILENAME))?;
let key_base64 = abe::decrypt_with_admin(
let key_base64 = abe::decrypt_with_admin_exe(
&admin_exe_str,
self.app_bound_encrypted_key
.as_ref()
@@ -311,10 +313,11 @@ fn get_admin_exe_path() -> Result<PathBuf> {
get_dist_admin_exe_path(&current_exe_full_path)?
};
// check if admin.exe exists
// check if bitwarden_chromium_import_helper.exe exists
if !admin_exe_full_path.exists() {
return Err(anyhow!(
"admin.exe not found at path: {:?}",
"{} not found at path: {:?}",
ADMIN_EXE_FILENAME,
admin_exe_full_path
));
}
@@ -325,13 +328,13 @@ fn get_admin_exe_path() -> Result<PathBuf> {
fn get_dist_admin_exe_path(current_exe_full_path: &PathBuf) -> Result<PathBuf> {
let admin_exe = current_exe_full_path
.parent()
.map(|p| p.join("admin.exe"))
.map(|p| p.join(ADMIN_EXE_FILENAME))
.ok_or_else(|| anyhow!("Failed to get parent directory of current executable"))?;
Ok(admin_exe)
}
// Try to find admin.exe in debug build folders. This might not cover all the cases.
// Try to find bitwarden_chromium_import_helper.exe in debug build folders. This might not cover all the cases.
// Tested on `npm run electron` from apps/desktop and apps/desktop/desktop_native.
fn get_debug_admin_exe_path() -> Result<PathBuf> {
let current_dir = std::env::current_dir()?;
@@ -344,7 +347,8 @@ fn get_debug_admin_exe_path() -> Result<PathBuf> {
)),
Some("desktop_native") => Ok(get_target_admin_exe_path(current_dir)),
_ => Err(anyhow!(
"Cannot determine admin.exe path from current directory: {}",
"Cannot determine {} path from current directory: {}",
ADMIN_EXE_FILENAME,
current_dir.display()
)),
}
@@ -354,7 +358,7 @@ fn get_target_admin_exe_path(desktop_native_dir: PathBuf) -> PathBuf {
desktop_native_dir
.join("target")
.join("debug")
.join("admin.exe")
.join(ADMIN_EXE_FILENAME)
}
//

View File

@@ -51,7 +51,7 @@ function buildImporterBinaries(target, release = true) {
return;
}
["admin"].forEach(bin => {
["bitwarden_chromium_import_helper"].forEach(bin => {
const targetArg = target ? `--target ${target}` : "";
const releaseArg = release ? "--release" : "";
child_process.execSync(`cargo build --bin ${bin} ${releaseArg} ${targetArg} --features windows-binary`, {stdio: 'inherit', cwd: path.join(__dirname, "bitwarden_chromium_importer")});