Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/chrome v20 decrypt #56

Merged
merged 8 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ on:
- main
pull_request:
paths:
- '.github/workflows/lint-cli.yml'
- 'rookie-rs/src/**'
- ".github/workflows/lint-cli.yml"
- "rookie-rs/src/**"
workflow_dispatch:

env:
Expand Down Expand Up @@ -49,4 +49,4 @@ jobs:
workspaces: rookie-rs

- name: run Clippy
run: cargo clippy --manifest-path Cargo.toml --all-targets --all-features -- -D warnings
run: cargo clippy --manifest-path Cargo.toml --all-targets -- -D warnings
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ Cargo.lock
__pycache__
*.pyc
node_modules/
*.lockb
*.lockb
*.exe
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["rookie-rs", "bindings/python", "bindings/node", "cli"]
members = ["rookie-rs", "bindings/python", "bindings/node", "cli", "unprotect"]

exclude = ["examples/*"]

Expand Down
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Load cookies from any browser on any platform
- Available for `Rust`, `Python`, and `JavaScript`
- Ensures type safety (e.g., `TypeScript`, `Python` with type hints)
- Super Fast, Built with `Rust`
- Bypass `Chrome` restriction like file locking
- Bypass `Chrome` restriction of file locking and appbound encryption
- Read session cookies from `Chrome` based browsers! (requires admin rights on `Windows`)
- Wide browsers support
- Cross-platform support for `Windows`, `Linux`, and `macOS`
Expand Down Expand Up @@ -125,21 +125,21 @@ look at [src/windows/config.rs](https://github.com/thewh1teagle/rookie/blob/main

## Testing Dates (DD/MM/YY) 📅

| Browser | Linux | macOS | Windows |
| :-------- | :------: | :------: | :------: |
| Arc | 07/08/24 | 07/08/24 | 07/08/24 |
| Brave | 01/10/23 | 25/11/23 | 01/10/23 |
| Cachy | 04/06/24 | N/A | N/A |
| Chromium | 01/10/23 | 25/11/23 | 01/10/23 |
| Chrome | 01/10/23 | 25/11/23 | 16/03/24 |
| Edge | 01/10/23 | - | 01/10/23 |
| Firefox | 01/10/23 | 25/11/23 | 16/03/24 |
| IE | N/A | N/A | 01/10/23 |
| LibreWolf | 01/10/23 | 25/11/23 | 01/10/23 |
| Opera | 01/10/23 | - | 01/10/23 |
| Opera GX | N/A | - | 01/10/23 |
| Safari | N/A | 02/10/23 | N/A |
| Vivaldi | 01/10/23 | 25/11/23 | 01/10/23 |
| Browser | Linux | macOS | Windows |
| :-------- | :--------: | :--------: | :--------: |
| Arc | 2024/08/07 | 2024/08/07 | 2024/08/07 |
| Brave | 2024/10/26 | 2024/10/26 | 2024/10/26 |
| Cachy | 2024/06/04 | N/A | N/A |
| Chromium | 2024/10/26 | 2024/10/26 | 2023/10/01 |
| Chrome | 2024/10/26 | 2024/10/26 | 2024/03/16 |
| Edge | 2023/10/01 | - | 2023/10/01 |
| Firefox | 2024/10/26 | 2023/11/25 | 2024/03/16 |
| IE | N/A | N/A | 23/10/01 |
| LibreWolf | 2023/10/01 | 2023/11/25 | 23/10/01 |
| Opera | 2023/10/01 | - | 23/10/01 |
| Opera GX | N/A | - | 23/10/01 |
| Safari | N/A | 2023/11/25 | N/A |
| Vivaldi | 2023/10/01 | 2023/11/25 | 2023/10/01 |

## Credits 🙌

Expand Down
3 changes: 2 additions & 1 deletion rookie-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ rust-ini = "0.21"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
url = "2"
rand = "0.8.5"

[dev-dependencies]
tracing-subscriber = "0.3.18"

[features]
default = []
pyo3 = ["eyre/pyo3"]
appbound = []

[target.'cfg(unix)'.dependencies]
pbkdf2 = "0.12"
Expand All @@ -56,7 +58,6 @@ base64 = "0.22"
libesedb = "0.2"
rawcopy-rs-next = "0.1.3"
privilege = "0.3.0"
rand = "0.8.5"
windows = { version = "0.51", features = [
"Win32_Security_Cryptography",
"Win32_Foundation",
Expand Down
3 changes: 3 additions & 0 deletions rookie-rs/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#[cfg(all(not(windows), feature = "appbound"))]
compile_error!("The `appbound` feature is supported only on Windows");

fn commit_hash() -> String {
let output = std::process::Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
Expand Down
60 changes: 47 additions & 13 deletions rookie-rs/src/browser/chromium.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::path::PathBuf;
use crate::windows;

#[cfg(not(target_os = "linux"))]
#[allow(unused)]
use eyre::ContextCompat;

#[cfg(target_os = "macos")]
Expand All @@ -32,16 +33,34 @@ pub fn chromium_based(
let key_dict: serde_json::Value =
serde_json::from_str(content.as_str()).context("Can't read json file")?;

let os_crypt = key_dict.get("os_crypt").context("Can't get os crypt")?;
let legacy_key = key_dict["os_crypt"]["encrypted_key"]
.as_str()
.unwrap_or_default();

let key64 = os_crypt
.get("encrypted_key")
.context("Can't get encrypted_key")?
#[allow(unused)]
let appbound_key = key_dict["os_crypt"]["app_bound_encrypted_key"]
.as_str()
.context("Can't convert encrypted_key to str")?;
.unwrap_or_default();

let keys = get_keys(key64)?;
query_cookies(keys, db_path, domains)
#[cfg(feature = "appbound")]
{
let keys = if !appbound_key.is_empty() {
if !privilege::user::privileged() {
bail!("Chrome cookies from version v130 can be decrypted only when running as admin due to appbound encryption!")
}
let key = crate::windows::appbound::get_keys(appbound_key)?;
vec![key]
} else {
get_keys(legacy_key)?
};
query_cookies(keys, db_path, domains)
}

#[cfg(not(feature = "appbound"))]
{
let keys = get_keys(legacy_key)?;
query_cookies(keys, db_path, domains)
}
}

/// Returns cookies from chromium based browser
Expand Down Expand Up @@ -123,7 +142,7 @@ fn get_keys(config: &BrowserConfig) -> Result<Vec<Vec<u8>>> {
}

/// Decrypt cookie value using aes GCM
#[cfg(target_os = "windows")]
#[cfg(windows)]
fn decrypt_encrypted_value(
value: String,
encrypted_value: &[u8],
Expand All @@ -135,8 +154,10 @@ fn decrypt_encrypted_value(
log::warn!("Unknown key type: {:?}", key_type);
return Ok(value);
}
log::debug!("key type: {:?}", key_type);

let encrypted_value = &encrypted_value[3..];
let nonce = &encrypted_value[..12];
let nonce = &encrypted_value[..12]; // iv
let ciphertext = &encrypted_value[12..];

// Create a new AES block cipher.
Expand All @@ -148,7 +169,12 @@ fn decrypt_encrypted_value(
.decrypt(nonce, ciphertext.as_ref())
.map_err(eyre::Error::msg)
.context("Can't decrypt using key")?;
let plaintext = String::from_utf8(plaintext).context("Can't decode encrypted value")?;

let plaintext = if key_type == b"v20" {
String::from_utf8(plaintext[32..].to_vec()).context("Can't decode encrypted value")
} else {
String::from_utf8(plaintext).context("Can't decode encrypted value")
}?;
return Ok(plaintext);
}
bail!("decrypt_encrypted_value failed")
Expand All @@ -170,9 +196,12 @@ fn decrypt_encrypted_value(
return Ok("".into());
}
let key_type = &encrypted_value[..3];
if !(key_type == b"v11" || key_type == b"v10") {

if !(key_type == b"v11" || key_type == b"v10" || key_type == b"v20") {
return Ok(value);
}
log::debug!("key type: {:?}", key_type);

use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};

type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
Expand All @@ -195,8 +224,13 @@ fn decrypt_encrypted_value(
return Ok(decoded);
}
Err(_) => {
log::warn!("Error in decode decrypt value with utf8");
return Ok("".into());
log::debug!("Error in decode decrypt value with utf8. trying from index 32");

let decoded = String::from_utf8(plaintext[32..].to_vec()).unwrap_or_else(|_| {
log::warn!("Error decoding from index 32 with UTF-8");
"".into()
});
return Ok(decoded);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions rookie-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

// Common
pub mod common;
mod utils;
pub use common::enums;

// Browser
Expand Down
25 changes: 25 additions & 0 deletions rookie-rs/src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use std::fs;
use std::path::PathBuf;

use eyre::Result;
use rand::distributions::Alphanumeric;
use rand::Rng;

#[allow(unused)]
pub fn random_string(length: usize, prefix: &str, suffix: &str) -> String {
let random_part: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(length)
.map(char::from)
.collect();

format!("{}{}{}", prefix, random_part, suffix)
}

#[allow(unused)]
pub fn temp_dir() -> Result<PathBuf> {
let tmp_path = std::env::temp_dir();
let tmp_path = tmp_path.join(random_string(6, "rookie", ""));
fs::create_dir_all(&tmp_path)?;
Ok(tmp_path)
}
82 changes: 82 additions & 0 deletions rookie-rs/src/windows/appbound.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
See https://gist.github.com/thewh1teagle/d0bbc6bc678812e39cba74e1d407e5c7 and https://github.com/runassu/chrome_v20_decryption/blob/main/decrypt_chrome_v20_cookie.py

wget.exe https://github.com/thewh1teagle/rookie/releases/download/appbound-binaries/paexec.exe
wget.exe https://github.com/thewh1teagle/rookie/releases/download/appbound-binaries/unprotect.exe
cargo build --release --features appbound
*/
use base64::{prelude::BASE64_STANDARD, Engine};
use eyre::{bail, Context, Result};
use std::fs;
use std::process::{Command, Stdio};

use crate::utils::temp_dir;

use aes_gcm::{
aead::{generic_array::GenericArray, Aead, KeyInit},
Aes256Gcm, Key,
};

static UNPROTECT_NAME: &str = "unprotect.exe";
static PAEXEC_NAME: &str = "paexec.exe";

static PAEXEC_BYTES: &[u8] = include_bytes!("../../../paexec.exe"); // Place it in workspace root: wget.exe https://github.com/thewh1teagle/rookie/releases/download/appbound-binaries/paexec.exe
static CRYPT_UNPROTECT_BYTES: &[u8] = include_bytes!("../../../unprotect.exe"); // cargo build --release -p cryptunprotect

fn decrypt(key_b64: &str, as_system: bool) -> Result<String> {
let dir = temp_dir()?;
let unprotect_path = dir.join(UNPROTECT_NAME);

fs::write(&unprotect_path, CRYPT_UNPROTECT_BYTES).expect("Failed to write unprotect.exe");

let output = if as_system {
let paexec_path = dir.join(PAEXEC_NAME);
fs::write(&paexec_path, PAEXEC_BYTES).expect("Failed to write paexec.exe");
let args = ["-s", unprotect_path.to_str().unwrap(), key_b64];
Command::new(paexec_path)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.context("Failed to execute paexec")?
} else {
Command::new(unprotect_path)
.arg(key_b64)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.context("Failed to execute unprotect")?
};
// fs::remove_dir_all(dir)?;

let result = String::from_utf8(output.stdout).context("utf8")?;
Ok(result.trim().to_string())
}

pub fn get_keys(key64: &str) -> Result<Vec<u8>> {
let key_u8 = BASE64_STANDARD.decode(key64)?;
if !key_u8.starts_with(b"APPB") {
bail!("key not starts with APPB")
}
let key_u8 = &key_u8[4..];
let key64 = BASE64_STANDARD.encode(key_u8);
let system_decrypted = decrypt(&key64, true)?;
let user_decrypted = decrypt(system_decrypted.trim(), false)?;
let key = BASE64_STANDARD.decode(user_decrypted)?;
let decrypted_key = &key[key.len() - 61..];
if decrypted_key[0] != 1 {
bail!("key[0] != 1")
}

let aes_key = BASE64_STANDARD.decode("sxxuJBrIRnKNqcH6xJNmUc/7lE0UOrgWJ2vMbaAoR4c=")?;
let iv = &decrypted_key[1..1 + 12];
let mut ciphertext = decrypted_key[1 + 12..1 + 12 + 32].to_vec();
let tag = &decrypted_key[1 + 12 + 32..];
ciphertext.extend(tag);

let aes_key = Key::<Aes256Gcm>::from_slice(&aes_key);
let cipher = Aes256Gcm::new(aes_key);
let nonce = GenericArray::from_slice(iv); // 96-bits; unique per message
let plain = cipher.decrypt(nonce, ciphertext.as_slice()).unwrap();
Ok(plain)
}
3 changes: 3 additions & 0 deletions rookie-rs/src/windows/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#[cfg(feature = "appbound")]
pub(crate) mod appbound;

pub mod config;
pub(crate) mod dpapi;
pub(crate) mod restart_manager;
Expand Down
14 changes: 14 additions & 0 deletions unprotect/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "unprotect"
version = "0.1.0"
edition = "2021"

[dependencies]
windows = { version = "0.51", features = [
"Win32_Security_Cryptography",
"Win32_Foundation",
"Win32_System",
"Win32_System_RestartManager",
] }
base64 = "0.22"
eyre = "0.6.12"
Loading
Loading