1
votes

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://docs.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.

1
I believe the final parameter should be the address of a DATA_BLOB variable that, on successful return, receives the decrypted data. The hint is in the parameter description where it says that you need to free the storage using LocalFree. - IInspectable
The library can be improved in two aspects: Error reporting and flexibility. The call to to_string_lossy() should really replaced by to_string(), and propagate the error information. Otherwise decrypt will report success even when the input doesn't meet the expectations, causing errors at a later point, with nothing pointing to the root cause anymore. And the library's interface should be based on &[u8] slices. The CryptoAPI operates on bytes, and having this library only support some byte sequences unduly limits its utility. - IInspectable
Please do not edit your question to add answers to it. Instead, you can answer your own question, and even accept that answer if it is the one you like best. - trentcl
Initially I tried to answer my own, but stackexchange said I don't have enough points to answer my own question or something like that. - Dragoon

1 Answers

1
votes

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.