实施 Windows 凭据提供程序

IHa*_*You 4 windows com rust windows-credential-provider windows-rs

我最近发现了windows-rs框架,并一直在寻求通过实现其ICredentialProvider COM 接口来在 Rust 中构建Windows 凭据提供程序

我一直在使用现有问题之一中汇总的信息进行概念验证实现,但我不确定如何实际将编译后的 Rust 公开为正确的 DLL,然后向 Windows 系统注册。

use std::cell::RefCell;

use windows::{
    core::implement,
    Win32::UI::Shell::{ICredentialProvider, ICredentialProvider_Impl},
};

fn main() -> windows::core::Result<()> {
    #[implement(ICredentialProvider)]
    struct Provider {
        mutable_state: RefCell<u32>,
    }

    impl Provider {
        fn new() -> Self {
           Self {
               mutable_state: RefCell::new(0),
           }
       }
    }

    impl ICredentialProvider_Impl for Provider {
        fn Advise(
            &self,
            pcpe: &core::option::Option<windows::Win32::UI::Shell::ICredentialProviderEvents>,
            upadvisecontext: usize,
        ) -> windows::core::Result<()> {
           *self.mutable_state.borrow_mut() = 42;
            todo!();
        }

        fn GetCredentialAt(
            &self,
            dwindex: u32,
        ) -> windows::core::Result<windows::Win32::UI::Shell::ICredentialProviderCredential>
        {
            todo!();
        }

        fn GetCredentialCount(
            &self,
            pdwcount: *mut u32,
            pdwdefault: *mut u32,
            pbautologonwithdefault: *mut windows::Win32::Foundation::BOOL,
        ) -> windows::core::Result<()> {
            todo!();
        }

        fn GetFieldDescriptorAt(
            &self,
            dwindex: u32,
        ) -> windows::core::Result<
            *mut windows::Win32::UI::Shell::CREDENTIAL_PROVIDER_FIELD_DESCRIPTOR,
        > {
            todo!();
        }

        fn GetFieldDescriptorCount(&self) -> windows::core::Result<u32> {
            todo!();
        }

        fn SetSerialization(
            &self,
            pcpcs: *const windows::Win32::UI::Shell::CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION,
        ) -> windows::core::Result<()> {
            todo!();
        }

        fn SetUsageScenario(
            &self,
            cpus: windows::Win32::UI::Shell::CREDENTIAL_PROVIDER_USAGE_SCENARIO,
            dwflags: u32,
        ) -> windows::core::Result<()> {
            todo!();
        }

        fn UnAdvise(&self) -> windows::core::Result<()> {
            todo!();
        }
    }

    Ok(())
}
Run Code Online (Sandbox Code Playgroud)

我编译了一个用 C++ 编写的示例凭据提供程序,由 Windows 在其 SDK 中提供,并使用一个工具来查看生成的 DLL 中可用的导出函数

windows-rs 社区也做出了类似的努力来公开 WinRT 的 rust,但是凭证提供程序所需的 COM 接口非常不同,我真的不知道从哪里开始。

是否有任何 Rust 技巧可以生成类似的 DLL,可以公开我的接口并使其可用于 Windows?任何帮助表示赞赏。

IIn*_*ble 10

凭证提供程序需要作为COM 服务器来实现。COM 服务器是一个 PE 映像(EXE 或 DLL),提供以下导出:

DllGetClassObject神奇之处在于:它检查请求的类(由 标识rclsid)是否由模块实现,然后返回指向请求的接口riid(通常是IClassFactoryorIClassFactory2接口)的指针。一旦客户端(操作系统,如果是凭证提供者)收到类工厂,它就可以使用它来实例化实现接口(例如ICredentialProvider接口)的对象。

这里的关键点是,这两个导出是模块成为 COM 服务器所需的全部,可以公开任意接口2

这涵盖了系统不变量。转向 Rust 需要解决以下问题:

问题陈述

  1. 创建一个生成 DLL 的 crate
  2. 指示链接器按给定名称导出符号
  3. 实现接口
  4. 实现导出的函数

解决方案

  1. 首先,让我们创建一个箱。将执行以下操作:

    cargo new --lib cp_demo
    
    Run Code Online (Sandbox Code Playgroud)

    将其添加到Cargo.toml(删除[package]表格之外的任何内容):

    [lib]
    crate-type = ["cdylib"]
    
    Run Code Online (Sandbox Code Playgroud)

    这通过让 crate 生成 DLL 解决了第一个问题。运行将在targets/debugcargo build生成cp_demo.dll(假设默认设置)。请注意,这还没有取得进展。它只是创建一个不导出任何内容的 DLL(通过运行来证明)。它只是消除了拥有.dumpbin /EXPORTS targets\debug\cp_demo.dllfn main()

  2. 有了一个“空”DLL,我们就想向外界公开功能。机制与引用的资源extern "system"非常相似,需要块和属性的组合no_mangle,以便系统可以通过名称发现导出。

    前者指定调用约定,即调用者(系统)和被调用者(实现)之间的契约,正式化参数如何传递以及谁负责返回时的(堆栈)清理。后者指示链接器保持导出的符号不被修饰,以便系统可以通过调用来发现它们GetProcAddress(hmod, "DllGetClassObject"),例如。

    将以下内容转储到src/lib.rs中

    use std::{ffi, ptr};
    
    use windows::{
        core::{GUID, HRESULT},
        Win32::Foundation::{CLASS_E_CLASSNOTAVAILABLE, E_POINTER, S_OK},
    };
    
    #[no_mangle]
    extern "system" fn DllGetClassObject(
        _rclsid: *const GUID,
        _riid: *const GUID,
        ppv: *mut *mut ffi::c_void,
    ) -> HRESULT {
        // Implement basic COM contract
        if ppv.is_null() {
            E_POINTER
        } else {
            unsafe { *ppv = ptr::null_mut() };
            CLASS_E_CLASSNOTAVAILABLE
        }
    }
    
    #[no_mangle]
    extern "system" fn DllCanUnloadNow() -> HRESULT {
        // It's always safe to unload this module
        S_OK
    }
    
    Run Code Online (Sandbox Code Playgroud)

    并更新Cargo.toml以包含

    [dependencies.windows]
    version = "0.44.0"
    features = [
        "Win32_Foundation",
    ]
    
    Run Code Online (Sandbox Code Playgroud)

    符合凭证提供者框架 DLL。再次运行dumpbin /EXPORTS targets\debug\cp_demo.dll会产生输出,其中包括

        ordinal hint RVA      name
    
              1    0 00001080 DllCanUnloadNow = DllCanUnloadNow
              2    1 00001000 DllGetClassObject = DllGetClassObject
    
    Run Code Online (Sandbox Code Playgroud)

    甜的!现在我们有一个不提供任何内容(当然也不提供凭据)的凭据提供程序。但它看起来已经是系统的潜在凭证提供者,可以使用以下 .reg 脚本进行注册(确保使用新的 GUID;当您阅读时,该提供者将不再是“全局唯一”)这)。

    Windows Registry Editor Version 5.00
    
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\{DED30376-B312-4168-B2D3-2D0B3EADE513}]
    @="cp_demo"
    
    [HKEY_CLASSES_ROOT\CLSID\{DED30376-B312-4168-B2D3-2D0B3EADE513}]
    @="cp_demo"
    
    [HKEY_CLASSES_ROOT\CLSID\{DED30376-B312-4168-B2D3-2D0B3EADE513}\InprocServer32]
    @="cp_demo.dll"
    "ThreadingModel"="Apartment"
    
    Run Code Online (Sandbox Code Playgroud)
  3. 接口实现遵循您在问题中已使用的模式:声明一个自定义类型来保存本地状态(如果有),应用属性#[implement],并为生成的特征提供名为 的实现<interface>_Impl

    对于ICredentialProvider接口3,这大致类似于问题中的内容。唯一的例外是以下代码片段将todo!()' 替换为返回错误代码,因为恐慌跨越 ABI 4是不合法的。

    #[implement(ICredentialProvider)]
    struct Provider {
        _mutable_state: cell::RefCell<u32>,
    }
    
    impl Provider {
        fn new() -> Self {
            Self {
                _mutable_state: cell::RefCell::new(0),
            }
        }
    }
    
    impl ICredentialProvider_Impl for Provider {
        fn SetUsageScenario(
            &self,
            _cpus: CREDENTIAL_PROVIDER_USAGE_SCENARIO,
            _dwflags: u32,
        ) -> Result<()> {
             Err(E_NOTIMPL.into())
        }
    
        // ...
    }
    
    Run Code Online (Sandbox Code Playgroud)

    除了在系统设法实例化此实现时优雅地失败之外,这没有做任何有用的事情。为此,它需要一个IClassFactory执行以下操作的实现:

    #[implement(IClassFactory)]
    struct ProviderFactory;
    
    impl IClassFactory_Impl for ProviderFactory {
        fn CreateInstance(
            &self,
            punkouter: &core::option::Option<windows::core::IUnknown>,
            riid: *const windows::core::GUID,
            ppvobject: *mut *mut core::ffi::c_void,
        ) -> windows::core::Result<()> {
            // Validate arguments
            if ppvobject.is_null() {
                return Err(E_POINTER.into());
            }
            unsafe { *ppvobject = ptr::null_mut() };
            if riid.is_null() {
                return Err(E_INVALIDARG.into());
            }
            let riid = unsafe { *riid };
            if punkouter.is_some() {
                return Err(CLASS_E_NOAGGREGATION.into());
            }
    
            // We're only handling requests for `IID_ICredentialProvider`
            if riid != ICredentialProvider::IID {
                return Err(E_NOINTERFACE.into());
            }
    
            // Construct credential provider and return it as an `ICredentialProvider`
            // interface
            let provider: ICredentialProvider = Provider::new().into();
            unsafe { *ppvobject = mem::transmute(provider) };
            Ok(())
        }
    
        fn LockServer(&self, _flock: windows::Win32::Foundation::BOOL) -> windows::core::Result<()> {
            Err(E_NOTIMPL.into())
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    ICredentialProvider这代表了由 实现的接口的功能齐全的类工厂Provider。更好的部分CreateInstance()包括合同规定的参数验证IClassFactory::CreateInstance。真正的魔法发生在这里:

    let provider: ICredentialProvider = Provider::new().into();
    
    Run Code Online (Sandbox Code Playgroud)

    这件事做的很多啊!Provider::new()是显而易见的部分:它实例化一个新Provider对象。该部分的作用并不那么明显into()。它执行最终由宏生成的以下From特征实现#[implement]cargo-expand是发现这些隐藏细节的不可或缺的工具):

    impl ::core::convert::From<Provider> for ICredentialProvider {
        fn from(this: Provider) -> Self {
            let this = Provider_Impl::new(this);
            let mut this = ::core::mem::ManuallyDrop::new(::std::boxed::Box::new(this));
            let vtable_ptr = &this.vtables.0;
            unsafe { ::core::mem::transmute(vtable_ptr) }
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    它获取一个Provider实例,将其移动到(生成的)中Provider_Impl,然后将其移动到堆存储(Box::new())中,将所有内容包装在 a 后面ManuallyDrop,最后将对象指针转换为相应的接口指针。

    其中的每一点都至关重要:将Provider实例移动到堆内存中可确保它作为函数返回的一部分跨堆栈展开保持有效,将其包装在Boxa 内部会在对象超出范围时ManuallyDrop禁止运行实现(否则会递减)Drop引用计数为 0,沿途销毁对象),并返回接口指针允许系统通过已知接口调用未知实现。

    后者的推论是对象清理由实现自行决定:当引用计数下降到 0 时,当客户端调用时Release(),具体实现会释放资源(Provider_Impl::Release()具体来说),无论调用者是谁。 。Provider_Impl确保与From特征实现使用的分配策略相匹配,因此我们不必担心这个细节。

    关于实现的注释LockServer:它重新调整E_NOTIMPL,主要是因为我没有信心理解它的目的。这在调试时似乎没有产生任何不利影响,所以我暂时保留它(稍后会详细介绍)。

  4. 一切就绪后,我们就处于社交的有利位置:我们有一个ICredentialProvider实现 ( Provider),它将忠实地响应E_NOTIMPL每个请求,还有一个实现IClassFactory( ProviderFactory),它将尽职尽责地招揽尽可能多的上述流氓。要求。与操作系统成为好朋友所缺少的是让它参与我们的父母身份。

    • 绕道凭证提供程序查找和实例化:每当系统需要发现凭证提供程序时,它都会枚举注册表中该键下的所有值。HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\每个值的数据命名一个UUID(在 Windows 编程中通常称为“GUID”),该 UUID 标识凭据提供程序实现。

      通过标识 COM 接口实现的 GUID,接下来是相当通用的 COM 基础设施业务。这适用于所有 COM,而不仅仅是凭据提供程序。(大致准确)算法是这样的:

      1. 查找该HKEY_CLASSES_ROOT\CLSID\密钥下的 GUID。其InprocServer32数据包含实现模块的(完全限定)路径。
      2. 加载模块并请求导出名为DllGetClassObject(文字名称是合同的一部分,因此是#[no_mangle]属性的一部分)。
      3. 请求一个能够实例化由给定 GUID 标识的 COM 对象的类工厂(请注意,GUID 用于标识 COM 接口以及接口实现,或“COM 对象”;前者通常拼写为“IID接口 ID” ,而后者CLSID在“类 ID”中被称为)。
      4. 让类工厂创建一个实例

    顺便说一句,为什么上面的 .reg 脚本写入两个不同的键,以及推动系统确认我们自己的创建所需的内容应该很明显:3.上面的步骤是缺少的链接,所以让我们填写它:

    #[no_mangle]
    extern "system" fn DllGetClassObject(
        rclsid: *const GUID,
        riid: *const GUID,
        ppv: *mut *mut ffi::c_void,
    ) -> HRESULT {
        // The "class ID" this credential provider is identified by. This value needs to
        // match the value used when registering the credential provider (see the .reg
        // script above)
        const CLSID_CP_DEMO: GUID = GUID::from_u128(0xDED30376_B312_4168_B2D3_2D0B3EADE513);
    
        // Validate arguments
        if ppv.is_null() {
            return E_POINTER;
        }
        unsafe { *ppv = ptr::null_mut() };
        if rclsid.is_null() || riid.is_null() {
            return E_INVALIDARG;
        }
    
        let rclsid = unsafe { *rclsid };
        let riid = unsafe { *riid };
        // The following isn't strictly correct; a client *could* request an interface other
        // than `IClassFactory::IID`, which this implementation is simply failing.
        // This is safe, even if overly restrictive
        if rclsid != CLSID_CP_DEMO || riid != IClassFactory::IID {
            return CLASS_E_CLASSNOTAVAILABLE;
        }
    
        // Construct the factory object and return its `IClassFactory` interface
        let factory: IClassFactory = ProviderFactory.into();
        unsafe { *ppv = mem::transmute(factory) };
        S_OK
    }
    
    #[no_mangle]
    extern "system" fn DllCanUnloadNow() -> HRESULT {
        // Since we aren't tracking module references (yet), it's never safe to unload this
        // module
        S_FALSE
    }
    
    Run Code Online (Sandbox Code Playgroud)

    这遵循一个熟悉的模式:大部分DllGetClassObject()是参数验证,并let factory: IClassFactory = ProviderFactory.into();进行实际工作。

    这里唯一的区别是它ProviderFactory是无状态的(Rust 术语中的“单元结构”)。除了类型之外,它不携带任何信息。尽管它确实允许我们在其上实现特征(例如 ),但它看起来并没有立即有用From。通过ProviderFactory单元结构表达式,我们可以调用into()它,启动与上述相同的机制,保留使用位于堆内存中的手动管理的 COM 对象,以便我们可以返回指向它的指针 ( *ppv = mem::transmute(factory))。

    请注意,DllCanUnloadNow实现从返回更改S_OKS_FALSE. 这本身并没有错,但这也不是我想做的1。为了解决这个问题,我们需要将所有引用记录到该模块中,在每次对象创建时增加引用计数,并在该模块实现的对象被销毁时减少引用计数。我还不完全确定该怎么做。

最后一步结束循环。通过上面的内容,我们可以编译一个凭据提供程序 DLL,允许系统发现它并加载模块,从中请求一个类工厂,并让它创建一个凭据提供程序。凭证提供者还没有做任何事情;用功能充实ICredentialProvider骨架是另一次更新。

调试

为了调试,我使用Hyper-V设置了一个虚拟机。选择是为了方便,因为 Hyper-V 作为可选系统组件提供,操作系统安装可立即下载(我使用“Windows 10 MSIX 打包环境”作为最小选项)。任何其他虚拟机解决方案或操作系统安装都应具有相同的工作原理。

  • TBD:虚拟机和调试器设置

  • 虽然可以将调试器附加到 Windows 登录屏幕,但在本地用户帐户下运行所有​​内容要方便得多。为此,我实现了一个小型测试程序 ( cp_test),它通过CredUIPromptForWindowsCredentialsWAPI 调用来测试凭证提供程序。

    该程序可以从调试器启动,所有凭据提供程序业​​务都在进程中运行。无需将调试器附加到外部进程,一切都会按您的预期运行。

    这是测试应用程序:

    Cargo.toml

    [package]
    name = "cp_test"
    version = "0.0.0"
    edition = "2021"
    
    [dependencies.windows]
    version = "0.44.0"
    features = [
        "Win32_Foundation",
        "Win32_Graphics_Gdi",
        "Win32_Security_Credentials",
    ]
    
    Run Code Online (Sandbox Code Playgroud)

    src/main.rs

    use std::{mem, ptr};
    
    use windows::{
        w,
        Win32::{
            Foundation::BOOL,
            Security::Credentials::{
                CredUIPromptForWindowsCredentialsW, CREDUIWIN_CHECKBOX, CREDUI_INFOW,
            },
        },
    };
    
    fn main() {
        let ui_info = CREDUI_INFOW {
            cbSize: mem::size_of::<CREDUI_INFOW>() as _,
            pszMessageText: w!("Enter credentials"),
            pszCaptionText: w!("Testing custom credential provider"),
            ..Default::default()
        };
        let mut auth_package = 0;
        let mut auth_buffer = ptr::null_mut();
        let mut auth_buffer_size = 0;
        let mut save = BOOL::default();
        let _ = unsafe {
            CredUIPromptForWindowsCredentialsW(
                Some(&ui_info),
                0,
                &mut auth_package,
                None,
                0,
                &mut auth_buffer,
                &mut auth_buffer_size,
                Some(&mut save),
                CREDUIWIN_CHECKBOX,
            )
        };
    }
    
    Run Code Online (Sandbox Code Playgroud)

    如果凭据提供程序已正确注册,系统将发现它并将 COM 服务器加载到此进程中。在 WinDbg 中使用sxe ld cp_demo.dll可确保我们在加载模块时进入调试器,从而使我们能够根据需要轻松设置断点。

开发经验

  • TBD:添加 一个构建脚本
    • 在每次构建时生成一个 GUID
    • 生成 Rust 源用于CLSID_CP_DEMO
    • 生成具有匹配 GUID 的 register.reg 和 unregister.reg 文件

1 这个并没有严格实现功能;其最初的目的是作为性能优化,允许实现模块在不再需要时被卸载。符合要求的实现可以简单地S_FALSE无条件返回,并选择退出此优化,尽管对于凭证提供者来说,允许系统尽快卸载它可能是明智的。将攻击面保留在高价值信息中的时间超过必要的时间并不是明智之举。

2 实际的接口方法不需要导出;它们通过函数指针数组返回给客户端。

3 完整的实现还需要我们提供 的实现ICredentialProviderCredential,因为它是从ICredentialProvider::GetCredentialAt.