I need to implement functionality that impersonates a domain user. The impersonated thread needs to be able to read from/write to HKCU registry hive for the impersonated user. I am able to impersonate the user, but when I attempt to load any registry keys, I receive a Win32 "Access is denied" exception.
NOTE: The intention here is to provide a pseudo impersonated command-line to perform a specific set of actions as a service account. The service account may not have interactive logon rights, so I am required to use the BATCH logon type. As a test, I did also try the INTERACTIVE logon type, but the result was the same.
I followed this CodeProject article as a general guide. Here is what I have:
partial class Program
{
[DllImport("advapi32.dll")]
public static extern int LogonUser(String lpszUserName, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int DuplicateToken(IntPtr hToken, int impersonationLevel, ref IntPtr hNewToken);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool RevertToSelf();
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool CloseHandle(IntPtr handle);
[DllImport("advapi32.dll", CharSet = CharSet.Auto)]
public static extern int RegOpenCurrentUser(int samDesired, out IntPtr phkResult);
[DllImport("userenv.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool LoadUserProfile(IntPtr hToken, ref ProfileInfo lpProfileInfo);
[DllImport("Userenv.dll", CallingConvention = CallingConvention.Winapi, SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool UnloadUserProfile(IntPtr hToken, IntPtr lpProfileInfo);
[StructLayout(LayoutKind.Sequential)]
public struct ProfileInfo
{
public int dwSize;
public int dwFlags;
public string lpUserName;
public string lpProfilePath;
public string lpDefaultPath;
public string lpServerName;
public string lpPolicyPath;
public IntPtr hProfile;
}
private static string ImpUser = string.Empty;
private static string ImpDomain = string.Empty;
private static string FullyQualifiedImpUser
{
get
{
return $"{ImpDomain}\\{ImpUser}";
}
}
private static SecureString ImpSecret = new SecureString();
private static bool CurrentlyImpersonating = false;
private static WindowsIdentity ImpersonatedIdentity = null;
private static IntPtr Token = IntPtr.Zero;
private static IntPtr TokenDuplicate = IntPtr.Zero;
//*** THIS IS THE CORE METHOD ***
private static void EnterModeImpersonated()
{
bool loadSuccess;
int errCode;
try
{
if (RevertToSelf())
{
if (LogonUser(ImpUser, ImpDomain,
ImpSecret.Plaintext(), Constants.LOGON32_LOGON_TYPE_BATCH,
Constants.LOGON32_PROVIDER_DEFAULT, ref Token) != 0)
{
if (DuplicateToken(Token, Constants.SecurityImpersonation, ref TokenDuplicate) != 0)
{
ImpersonatedIdentity = new WindowsIdentity(TokenDuplicate);
using (WindowsImpersonationContext m_ImpersonationContext = ImpersonatedIdentity.Impersonate())
{
if (m_ImpersonationContext != null)
{
#region LoadUserProfile
// Load user profile
ProfileInfo profileInfo = new ProfileInfo();
profileInfo.dwSize = Marshal.SizeOf(profileInfo);
profileInfo.lpUserName = ImpUser;
profileInfo.dwFlags = 1;
//Here is where I die:
loadSuccess = LoadUserProfile(TokenDuplicate, ref profileInfo);
if (!loadSuccess)
{
errCode = Marshal.GetLastWin32Error();
Win32Exception ex = new Win32Exception(errCode);
throw new Exception($"Failed to load profile for {FullyQualifiedImpUser}. Error code: {errCode}", ex);
}
if (profileInfo.hProfile == IntPtr.Zero)
{
errCode = Marshal.GetLastWin32Error();
Win32Exception ex = new Win32Exception(errCode);
throw new Exception($"Failed accessing HKCU registry for {FullyQualifiedImpUser}. Error code: {errCode}", ex);
}
#endregion
CloseHandle(Token);
CloseHandle(TokenDuplicate);
RegistryAgent.GetRootKeys(profileInfo.hProfile);
EnterMode();
UnloadUserProfile(TokenDuplicate, profileInfo.hProfile);
m_ImpersonationContext.Undo();
RegistryAgent.GetRootKeys(Constants.RevertToInvoker);
}
}
}
else
{
Console.WriteLine("DuplicateToken() failed with error code: " +
Marshal.GetLastWin32Error());
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
}
}
catch (Win32Exception we)
{
throw we;
}
catch
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
finally
{
if (Token != IntPtr.Zero) CloseHandle(Token);
if (TokenDuplicate != IntPtr.Zero) CloseHandle(TokenDuplicate);
Console.WriteLine("After finished impersonation: " +
WindowsIdentity.GetCurrent().Name);
}
}
//Toggles on impersonation mode
//Here, we grab the username, domain and password.
private static bool EnableImpersonation(string userInfo)
{
if (userInfo.Contains('\\'))
{
string[] parts = Parameter.ImpUser.TextValue.Split('\\');
ImpUser = parts[1];
ImpDomain = parts[0];
}
else
{
ImpUser = userInfo;
ImpDomain = Environment.UserDomainName;
}
//Prompt for the invoker to enter the impersonated account password
GetSecret();
if (TryImpersonate())
{
CurrentlyImpersonating = true;
}
else
{
DisableImpersonation();
}
return CurrentlyImpersonating;
}
//Toggles off impersontation & cleans up
private static void DisableImpersonation()
{
ImpSecret = null;
ImpersonatedIdentity = null;
Token = IntPtr.Zero;
TokenDuplicate = IntPtr.Zero;
ImpUser = string.Empty;
ImpDomain = string.Empty;
CurrentlyImpersonating = false;
}
//Implements a console prompt to grab the impersonated account password
//as a SecureString object
private static void GetSecret()
{
ImpSecret = new SecureString();
ConsoleKeyInfo key;
Console.Write($"\r\nEnter the password for {FullyQualifiedImpUser}: ");
do
{
key = Console.ReadKey(true);
if (key.Key != ConsoleKey.Backspace && key.Key != ConsoleKey.Enter)
{
ImpSecret.AppendChar(key.KeyChar);
Console.Write("*");
}
else
{
if (key.Key == ConsoleKey.Backspace && ImpSecret.Length != 0)
{
ImpSecret.RemoveAt(ImpSecret.Length - 1);
Console.Write("\b \b");
}
}
}
while (key.Key != ConsoleKey.Enter);
Console.WriteLine();
}
//This method is intended to ensure that the credentials entered
//for the impersonated user are correct.
private static bool TryImpersonate()
{
IntPtr testToken = IntPtr.Zero;
int result;
try
{
result = LogonUser(ImpUser, ImpDomain, ImpSecret.Plaintext(), Constants.LOGON32_LOGON_TYPE_BATCH, Constants.LOGON32_PROVIDER_DEFAULT, ref testToken);
if (result == 0)
{
int errCode = Marshal.GetLastWin32Error();
Win32Exception ex = new Win32Exception(errCode);
throw new Exception($"Failed to impersonate {FullyQualifiedImpUser}. Error code: {errCode}", ex);
}
return true;
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
return false;
}
}
}
I also read The MSDN documentation for LoadUserProfileA (I didn't find an article for LoadUserProfile() so I have to assume this is the ultimate COM function being called). It indicates: The token must have TOKEN_QUERY, TOKEN_IMPERSONATE, and TOKEN_DUPLICATE access.. I'm wondering if the logon token or duplicated token needs to be created differently in order to include these rights? I wasn't able to find any documentation on how to manipulate the token rights, though...