Search code examples
winapirustffi

Rust FFI with windows CryptoUnprotectData


I'm trying to learn FFI by starting with something simple (and with a practical use), but this doesn't seem to work:

mod bindings {
    ::windows::include_bindings!();
}

use std::{convert::TryFrom, ptr};
 
use bindings::{
    windows::win32::security::CryptUnprotectData,
    windows::win32::security::CRYPTOAPI_BLOB
};
 
// Powershell code to generate the token
// $pw = read-host "Enter Token" -AsSecureString
// ConvertFrom-SecureString $pw
 
fn main() -> windows::Result<()> {
    // The encrypted string is 'foobar'
    let encrypted_token = "01000000d08c9ddf0115d1118c7a00c04fc297eb01000000c336dca1c99b7d40ae3f797c2b5d2951000000000200000000001066000000010000200000007a87d6ac2fc8037bef45e3dbcb0b652432a22a9b48fc5fa3e4fcfd9aaf922949000000000e8000000002000020000000eeaa76a44b6cd5da837f4b0f7040de8e2795ed846f8abe2c7f2d2365d00cf89c1000000069fcaa7fa475178d623f4adab1b08ac4400000008af807014cba53ed2f1e7b8a54c6ad89ff57f0ee3d8c51ecd8c5b48e99b58d0e738c9fae9fc41b4280938865a047f2724106d34313c88a0f3852d5ba9d75abfd";
    let mut et_bytes = hex::decode(encrypted_token).unwrap();
    let size = u32::try_from(et_bytes.len()).unwrap();
    let mut decrypted = vec![0u8; et_bytes.len()];
    let dt_bytes = &mut decrypted;

    let mut p_data_in = CRYPTOAPI_BLOB {
        cb_data: size,
        pb_data: et_bytes.as_mut_ptr(),
    };
    let mut p_data_out = CRYPTOAPI_BLOB {
        cb_data: size,
        pb_data: dt_bytes.as_mut_ptr(),
    };

    let pin = &mut p_data_in;
    let pout = &mut p_data_out;
 
    unsafe {
        let result = CryptUnprotectData(
            pin,
            ptr::null_mut(),
            ptr::null_mut(),
            ptr::null_mut(),
            ptr::null_mut(),
            0,
            pout
        );

        println!("{:?}, {:?}", dt_bytes, result);
    }
 
    Ok(())
}

Basically it returns the all zero array, but the result of the CryptUnprotectData returns 1, which according to the docs means success: https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptunprotectdata

I've verified that by trying to mangle the hex string thus corrupting the encrypted data, which causes it to return 0. I'm not sure if it's writing to the wrong location or something, but presumably the success condition means it wrote somewhere.


Solution

  • The CryptUnprotectData API allocates the output buffer for you. It doesn't write into the buffer you provided. That's why you keep getting the original data, irrespective of the API call's result.

    Instead, you'll want to pass in a (default-initialized) CRYPTOAPI_BLOB structure, and observe the values the API passed back, something like the following will do:

    fn main() -> windows::Result<()> {
        // The encrypted string is 'foobar'
        let encrypted_token = "01000000d08c9ddf0115d1118c7a00c04fc297eb01000000c336dca1c99b7d40ae3f797c2b5d2951000000000200000000001066000000010000200000007a87d6ac2fc8037bef45e3dbcb0b652432a22a9b48fc5fa3e4fcfd9aaf922949000000000e8000000002000020000000eeaa76a44b6cd5da837f4b0f7040de8e2795ed846f8abe2c7f2d2365d00cf89c1000000069fcaa7fa475178d623f4adab1b08ac4400000008af807014cba53ed2f1e7b8a54c6ad89ff57f0ee3d8c51ecd8c5b48e99b58d0e738c9fae9fc41b4280938865a047f2724106d34313c88a0f3852d5ba9d75abfd";
        let mut et_bytes = hex::decode(encrypted_token).unwrap();
        let size = u32::try_from(et_bytes.len()).unwrap();
    
        let mut p_data_in = CRYPTOAPI_BLOB {
            cb_data: size,
            pb_data: et_bytes.as_mut_ptr(),
        };
        // Default-initialze; don't allocate any memory
        let mut p_data_out = CRYPTOAPI_BLOB::default();
    
        let pin = &mut p_data_in;
        let pout = &mut p_data_out;
     
        unsafe {
            let result = CryptUnprotectData(
                pin,
                ptr::null_mut(),
                ptr::null_mut(),
                ptr::null_mut(),
                ptr::null_mut(),
                0,
                pout
            );
    
            // Probably safe to ignore `result`
            if !p_data_out.pb_data.is_null() {
                // Construct a slice from the returned data
                let output = from_raw_parts(p_data_out.pb_data, p_data_out.cb_data as _);
                println!("{:?}", output);
    
                // Cleanup
                LocalFree(p_data_out.pb_data as _);
        }
     
        Ok(())
    }
    

    That produces the following output for me:

    [102, 0, 111, 0, 111, 0, 98, 0, 97, 0, 114, 0]
    

    which is the UTF-16LE encoding for foobar.


    Note that you need have to generate and import windows::win32::system_services::LocalFree to perform the cleanup.