I am trying to update an existing piece of library code that prompts an end-user for a username and password (using the normal Windows UI), and then passes these credentials to LogonUser to obtain the impersonation token for network resources.
The existing code uses CredUIPromptForCredentials
and works as expected, but the UI is the old-style WinXP dialog. This method has been superseded by a bunch of hoop-jumping, using CredUIPromptForWindowsCredentials
to show the dialog, then CredUnPackAuthenticationBuffer
to decode the credential blob into username, domain and password strings.
The canonical examples for P/Invoking CredUnPackAuthenticationBuffer
, from around the web, marshal the LPTSTR
fields using StringBuilder
. This is fine for username and domain, but there is no guarantee that the password won't be paged, moved or otherwise meddled with, unless the StringBuilder
pins the memory (which I can't force it to do), and I can't programatically force the zeroing of each character when I'm done.
In the old-style code, we declare LogonUser
and CredUIPromptForCredentials
with an IntPtr
password parameter. We create the char[]
manually, wrap it in a GCHandle
(with GCHandleType.Pinned
) and pass the gch.AddrOfPinnedObject()
to CredUIPromptForCredentials
; then pass the same gch.AddrOfPinnedObject()
to LogonUser
. Finally, we call Array.Clear
on the pinned char[]
and dispose the GCHandle
.
However, in the new-style code, the password parameter to CredUnPackAuthenticationBuffer
is an out-only parameter, so any buffer I create is ignored. I have declared the password parameter to be an out IntPtr
and intend to pass this IntPtr
to LogonUser
(in the same manner as the old-style). Once this has returned I intend to pass the IntPtr
to SecureZeroMemory
to clear it out. I am trusting (rightly or wrongly, but what option do I have?) that CredUnPackAuthenticationBuffer
will pin the memory accordingly.
When I run the code, the CredUIPromptForWindowsCredentials
call displays the dialog correctly, and the CredUnPackAuthenticationBuffer
runs. If I put a breakpoint/watch here, I can see that the username and domain have been parsed correctly into the StringBuilder
parameters (although the username length is hugely off), the password IntPtr
is set to a valid value and the password length parameter is correct.
However, when I pass that to the LogonUser
procedure, the call fails with an AccessViolationException: Attempted to read or write protected memory. This is often an indication that the other memory is corrupt.
Why does this happen?
Is it because I am trying to read past the end of the buffer, or because the buffer is encrypted (e.g. by DPAPI)?
Note that the username/password is confirmed correct. If I rip all the IntPtr
stuff out and replace it with StringBuilder
it does work properly. However, this potentially allows my program to leak the password, which is against all the recommendations I have read and try to work towards.
What does the built-in marshalling of StringBuilder do, that I might do as well?
Some sample code (cropped some subsidary methods/enums for brevity, but the DllImport
/extern
definitions, and subsequent usages are the same):
private class Native
{
[DllImport("credui", CharSet = CharSet.Unicode)]
public static extern CredUIReturnCodes CredUIPromptForWindowsCredentials(
ref CredUIInfo credUIInfo,
int errorCode,
ref uint authPackage,
IntPtr sourceAuthBuffer,
uint sourceAuthBufferSize,
out IntPtr targetAuthBuffer,
out uint targetAuthBufferSize,
[MarshalAs(UnmanagedType.Bool)] ref bool save,
CredWinUIFlags flags
);
[DllImport("credui", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CredUnPackAuthenticationBuffer(
CredPackFlags flags,
IntPtr authBuffer,
uint authBufferSize,
StringBuilder username,
out int usernameLength,
StringBuilder domain,
out int domainLength,
out IntPtr password,
out int passwordLength
);
[DllImport("credui", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CredUnPackAuthenticationBuffer(
CredPackFlags flags,
IntPtr authBuffer,
uint authBufferSize,
StringBuilder username,
out int usernameLength,
StringBuilder domain,
out int domainLength,
StringBuilder password,
out int passwordLength
);
[DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool LogonUser(
string username,
string domain,
IntPtr password,
LogonType logonType,
LogonProvider logonProvider,
out IntPtr handle
);
[DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseHandle(IntPtr handle);
}
public void Test()
{
var info = GetCredUIInfo();
uint authPackage = 0;
bool saveChecked = false;
IntPtr buffer;
uint bufferLength;
var ui = Native.CredUIPromptForWindowsCredentials(
ref info,
0,
ref authPackage,
IntPtr.Zero,
0,
out buffer,
out bufferLength,
CredWinUIFlags.GenericCredentials
);
if (uiResult == CredUIReturnCodes.NoError)
{
int usernameLength = 513,
domainLength = 337,
passwordLength = 256;
StringBuilder username = new StringBuilder(usernameLength);
StringBuilder domain = new StringBuilder(domainLength);
IntPtr password;
if (Native.CredUnPackAuthenticationBuffer(
CredPackFlags.PackProtectedCredentials,
buffer, bufferLength,
username, ref usernameLength,
domain, ref domainLength,
password, ref passwordLength
))
{
IntPtr logonToken;
if (Native.LogonUser(
username,
domain,
password,
LogonType.NewCredentials,
LogonProvider.WindowsNT50,
out logonToken
)) /*** fails here with AccessViolationException ***/
{
using (WindowsIdentity.Impersonate(logonToken))
{
TestNetworkAccess();
}
CloseHandle(logonToken);
}
// TODO: Native.SecureZeroMemory(password, passwordLength);
// - possibly Marshal.ZeroFreeGlobalAllocUnicode(password)
}
// TODO: Native.SecureZeroMemory(buffer, bufferLength);
// - possibly Marshal.ZeroFreeGlobalAllocUnicode(buffer)
}
}