Daniel Schilling
Daniel Schilling

Reputation: 4977

Pass .NET SecureString to COM Interop

How do you manually marshal a BSTR from .NET to COM?

I am trying to pass a secret stored in a SecureString from my .NET application to a method on a COM object. I want to avoid converting the SecureString to a String because that's not how you're supposed to use SecureString. The secret would then be in clear text in managed memory and hang around there for a long while. Instead I'm trying to do it the Right Way™: converting it to a BSTR in unmanaged memory, passing it to the COM method, and then clearing the BSTR with Marshal.ZeroFreeBSTR, so that the password is exposed for only a limited time.

The .NET interface that TblImp.exe generates for this COM object expects the secret to be passed as a String, which it then marshals to a BSTR. That's not what I want since it means I have to put my secret in a String, so following this Lean Method for Invoking COM, I've created my own interface so that I can customize it and get rid of the String. Here's what I'm starting with:

[ComImport, Guid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ITheComObjectDispatcher
{                                                     // +--- the naughty string
    [return: MarshalAs(UnmanagedType.BStr)]           // |
    [DispId(1)]                                       // V
    string TheMethod([In, MarshalAs(UnmanagedType.BStr)] string secret);
}

[ComImport, Guid("YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY")]
public class TheComObject
{}

This works - but I have to convert the SecureString to a String to use it, like so:

private string UseTheMethod(SecureString secret)
{
    var comObject = new TheComObject();
    var comObjectDispatcher = (ITheComObjectDispatcher)comObject;
    var secretAsString = NaughtilyConvertSecureStringToString(secret);
    return comObjectDispatcher.TheMethod(secretAsString);
}

private static string NaughtilyConvertSecureStringToString(SecureString value)
{
    var valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value);
    try
    {
        return Marshal.PtrToStringUni(valuePtr); // now the secret is in managed memory :(
    }
    finally
    {
        Marshal.ZeroFreeGlobalAllocUnicode(valuePtr);
    }
}

Instead, I want to use a method like Marshal.SecureStringToBSTR and skip the conversion to String. I've tried this...

    [return: MarshalAs(UnmanagedType.BStr)]
    [DispId(1)]
    string TheMethod([In, MarshalAs(UnmanagedType.BStr)] IntPtr secret);

... and this...

    [return: MarshalAs(UnmanagedType.BStr)]
    [DispId(1)]
    string TheMethod([In] IntPtr secret);

... with this calling code...

private string UseTheMethod(SecureString secret)
{
    var comObject = new TheComObject();
    var comObjectDispatcher = (ITheComObjectDispatcher)comObject;
    var secretPtr = Marshal.SecureStringToBSTR(secret);
    try
    {
        return comObjectDispatcher.TheMethod(secretPtr);
    }
    finally
    {
        Marshal.ZeroFreeBSTR(secretPtr);
    }
}

... but it doesn't work: the wrong value is getting passed to the COM method. It doesn't fail with an error - it just gives a different result.

Questions:

  1. Can I pass a BSTR directly as a IntPtr like this? What am I doing wrong?
  2. Is there a way to debug the marshaling process to see what value is being passed?

Upvotes: 3

Views: 207

Answers (1)

Simon Mourier
Simon Mourier

Reputation: 139158

Let's suppose you have that in .idl:

interface ITheComObjectDispatcher : IDispatch
{
    HRESULT TheMethod(BSTR secret, [out, retval] BSTR *pOut);
};

This will become something like this with .NET's tlbimp (as you have seen):

[ComImport, TypeLibType((short) 0x10c0), Guid("D4089F1D-5D83-4D1C-92CD-5941B35D43AA")]
public interface ITheComObjectDispatcher
{
    [return: MarshalAs(UnmanagedType.BStr)]
    [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType=MethodCodeType.Runtime), DispId(0x60020000)]
    string TheMethod([MarshalAs(UnmanagedType.BStr)] string secret);
}

So you cannot use this directly if you don't want to use unsafe string arguments. The solution is to redefine the COM interface in C#, like this for example (note the InterfaceIsDual option):

[InterfaceType(ComInterfaceType.InterfaceIsDual), Guid("D4089F1D-5D83-4D1C-92CD-5941B35D43AA")]
public interface ITheComObjectDispatcher2
{
    [DispId(0x60020000)]
    IntPtr TheMethod(IntPtr secret);
}

And use it like this:

var comObject = new TheComObject();
var comObjectDispatcher = (ITheComObjectDispatcher2)comObject;
var ptr = Marshal.SecureStringToBSTR(mySecureString);
try
{
    var outPtr = doc.TheMethod(ptr);

    // get the output string
    var output = Marshal.PtrToStringBSTR(outPtr);

    // free the callee-allocated BSTR
    Marshal.FreeBSTR(outPtr);
}
finally
{
    // free the secure string allocated BSTR
    Marshal.ZeroFreeBSTR(ptr);
}

Upvotes: 3

Related Questions